Implement Phase 2 features: roles, assessment groups, budget import, Kanban

- Add hierarchical roles: SuperUser Admin (is_superadmin flag), Tenant Admin,
  Tenant User with separate /admin route and admin panel
- Add Assessment Groups module for property type-based assessment rates
  (SFHs, Condos, Estate Lots with different regular/special rates)
- Enhance Chart of Accounts: initial balance on create (with journal entry),
  archive/restore accounts, edit all fields including account number & fund type
- Add Budget CSV import with downloadable template and account mapping
- Add Capital Projects Kanban board with drag-and-drop between year columns,
  table/kanban view toggle, and PDF export via browser print
- Update seed data with assessment groups, second test user, superadmin flag
- Create repeatable reseed.sh script for clean database population
- Fix AgingReportPage Mantine v7 Table prop compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 14:28:46 -05:00
parent e0272f9d8a
commit 01502e07bc
29 changed files with 1792 additions and 142 deletions

View File

@@ -0,0 +1,45 @@
import { Controller, Get, Post, Body, Param, UseGuards, Req, ForbiddenException } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { UsersService } from '../users/users.service';
@ApiTags('admin')
@Controller('admin')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
export class AdminController {
constructor(private usersService: UsersService) {}
private async requireSuperadmin(req: any) {
const user = await this.usersService.findById(req.user.userId || req.user.sub);
if (!user?.isSuperadmin) {
throw new ForbiddenException('SuperUser Admin access required');
}
}
@Get('users')
async listUsers(@Req() req: any) {
await this.requireSuperadmin(req);
const users = await this.usersService.findAllUsers();
return users.map(u => ({
id: u.id, email: u.email, firstName: u.firstName, lastName: u.lastName,
isSuperadmin: u.isSuperadmin, lastLoginAt: u.lastLoginAt, createdAt: u.createdAt,
organizations: u.userOrganizations?.map(uo => ({
id: uo.organizationId, name: uo.organization?.name, role: uo.role,
})) || [],
}));
}
@Get('organizations')
async listOrganizations(@Req() req: any) {
await this.requireSuperadmin(req);
return this.usersService.findAllOrganizations();
}
@Post('users/:id/superadmin')
async toggleSuperadmin(@Req() req: any, @Param('id') id: string, @Body() body: { isSuperadmin: boolean }) {
await this.requireSuperadmin(req);
await this.usersService.setSuperadmin(id, body.isSuperadmin);
return { success: true };
}
}