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

@@ -1,5 +1,6 @@
import { Controller, Get, Put, Body, Param, Query, UseGuards, ParseIntPipe } from '@nestjs/common';
import { Controller, Get, Put, Post, Body, Param, Query, Res, UseGuards, ParseIntPipe } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { Response } from 'express';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { BudgetsService } from './budgets.service';
import { UpsertBudgetDto } from './dto/upsert-budget.dto';
@@ -11,6 +12,38 @@ import { UpsertBudgetDto } from './dto/upsert-budget.dto';
export class BudgetsController {
constructor(private budgetsService: BudgetsService) {}
@Post(':year/import')
@ApiOperation({ summary: 'Import budget data from parsed CSV/XLSX lines' })
importBudget(
@Param('year', ParseIntPipe) year: number,
@Body() lines: any[],
) {
return this.budgetsService.importBudget(year, lines);
}
@Get(':year/template')
@ApiOperation({ summary: 'Download budget CSV template for a fiscal year' })
async getTemplate(
@Param('year', ParseIntPipe) year: number,
@Res() res: Response,
) {
const csv = await this.budgetsService.getTemplate(year);
res.set({
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="budget_template_${year}.csv"`,
});
res.send(csv);
}
@Get(':year/vs-actual')
@ApiOperation({ summary: 'Budget vs actual comparison' })
budgetVsActual(
@Param('year', ParseIntPipe) year: number,
@Query('month') month?: string,
) {
return this.budgetsService.getBudgetVsActual(year, month ? parseInt(month) : undefined);
}
@Get(':year')
@ApiOperation({ summary: 'Get budgets for a fiscal year' })
findByYear(@Param('year', ParseIntPipe) year: number) {
@@ -25,13 +58,4 @@ export class BudgetsController {
) {
return this.budgetsService.upsert(year, budgets);
}
@Get(':year/vs-actual')
@ApiOperation({ summary: 'Budget vs actual comparison' })
budgetVsActual(
@Param('year', ParseIntPipe) year: number,
@Query('month') month?: string,
) {
return this.budgetsService.getBudgetVsActual(year, month ? parseInt(month) : undefined);
}
}