Phase 2 tweaks: admin tenant creation, unit delete, frequency, UI overhaul

- Admin panel: create tenants with org + first user, manage org status
  (active/suspended/archived), contract number and plan level fields
- Units: delete with invoice check, assessment group dropdown binding
- Assessment groups: frequency field (monthly/quarterly/annual) with
  income calculations normalized to monthly equivalents
- Sidebar: grouped nav sections (Financials, Assessments, Transactions,
  Planning, Reports, Admin), renamed Chart of Accounts to Accounts
- Header: replaced text with SVG logo
- Capital projects: Kanban as default view, table-only PDF export,
  Future category (beyond 5-year plan)
- Auth: block login for suspended/archived organizations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 20:00:16 -05:00
parent 01502e07bc
commit 17fdacc0f2
20 changed files with 992 additions and 148 deletions

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Post, Put, Body, Param, UseGuards } from '@nestjs/common';
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { UnitsService } from './units.service';
@@ -21,4 +21,7 @@ export class UnitsController {
@Put(':id')
update(@Param('id') id: string, @Body() dto: any) { return this.unitsService.update(id, dto); }
@Delete(':id')
delete(@Param('id') id: string) { return this.unitsService.delete(id); }
}

View File

@@ -8,12 +8,17 @@ export class UnitsService {
async findAll() {
return this.tenant.query(`
SELECT u.*,
ag.name as assessment_group_name,
ag.regular_assessment as group_regular_assessment,
ag.frequency as group_frequency,
COALESCE((
SELECT SUM(i.amount - i.amount_paid)
FROM invoices i
WHERE i.unit_id = u.id AND i.status NOT IN ('paid', 'void', 'written_off')
), 0) as balance_due
FROM units u ORDER BY u.unit_number
FROM units u
LEFT JOIN assessment_groups ag ON ag.id = u.assessment_group_id
ORDER BY u.unit_number
`);
}
@@ -28,9 +33,9 @@ export class UnitsService {
if (existing.length) throw new BadRequestException(`Unit ${dto.unit_number} already exists`);
const rows = await this.tenant.query(
`INSERT INTO units (unit_number, address_line1, city, state, zip_code, owner_name, owner_email, owner_phone, monthly_assessment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
[dto.unit_number, dto.address_line1, dto.city, dto.state, dto.zip_code, dto.owner_name, dto.owner_email, dto.owner_phone, dto.monthly_assessment || 0],
`INSERT INTO units (unit_number, address_line1, city, state, zip_code, owner_name, owner_email, owner_phone, monthly_assessment, assessment_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`,
[dto.unit_number, dto.address_line1, dto.city, dto.state, dto.zip_code, dto.owner_name, dto.owner_email, dto.owner_phone, dto.monthly_assessment || 0, dto.assessment_group_id || null],
);
return rows[0];
}
@@ -42,10 +47,26 @@ export class UnitsService {
city = COALESCE($4, city), state = COALESCE($5, state), zip_code = COALESCE($6, zip_code),
owner_name = COALESCE($7, owner_name), owner_email = COALESCE($8, owner_email),
owner_phone = COALESCE($9, owner_phone), monthly_assessment = COALESCE($10, monthly_assessment),
status = COALESCE($11, status), updated_at = NOW()
status = COALESCE($11, status), assessment_group_id = $12, updated_at = NOW()
WHERE id = $1 RETURNING *`,
[id, dto.unit_number, dto.address_line1, dto.city, dto.state, dto.zip_code, dto.owner_name, dto.owner_email, dto.owner_phone, dto.monthly_assessment, dto.status],
[id, dto.unit_number, dto.address_line1, dto.city, dto.state, dto.zip_code, dto.owner_name, dto.owner_email, dto.owner_phone, dto.monthly_assessment, dto.status, dto.assessment_group_id !== undefined ? dto.assessment_group_id : null],
);
return rows[0];
}
async delete(id: string) {
await this.findOne(id);
// Check for outstanding invoices
const outstanding = await this.tenant.query(
`SELECT COUNT(*) as count FROM invoices WHERE unit_id = $1 AND status NOT IN ('paid', 'void', 'written_off')`,
[id],
);
if (parseInt(outstanding[0]?.count) > 0) {
throw new BadRequestException('Cannot delete unit with outstanding invoices. Please resolve all invoices first.');
}
await this.tenant.query('DELETE FROM units WHERE id = $1', [id]);
return { success: true };
}
}