- Fix compound inflation: use Math.pow(1 + rate/100, yearsGap) instead of flat rate so multi-year gaps (e.g., 2026→2029) compound annually - Budget Planner: add CSV import flow + Download Template button; show proper empty state when no base budget exists with Create/Import options - Budget Manager: remove CSV import, Download Template, and Save buttons; redirect users to Budget Planner when no budget exists for selected year - Fix getAvailableYears to return null latestBudgetYear when no budgets exist and include current year in year selector for fresh tenants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
201 lines
6.0 KiB
TypeScript
201 lines
6.0 KiB
TypeScript
import { Controller, Get, Post, Put, Delete, Body, Param, Query, Req, Res, UseGuards } from '@nestjs/common';
|
|
import { Response } from 'express';
|
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
import { AllowViewer } from '../../common/decorators/allow-viewer.decorator';
|
|
import { BoardPlanningService } from './board-planning.service';
|
|
import { BoardPlanningProjectionService } from './board-planning-projection.service';
|
|
import { BudgetPlanningService } from './budget-planning.service';
|
|
|
|
@ApiTags('board-planning')
|
|
@Controller('board-planning')
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard)
|
|
export class BoardPlanningController {
|
|
constructor(
|
|
private service: BoardPlanningService,
|
|
private projection: BoardPlanningProjectionService,
|
|
private budgetPlanning: BudgetPlanningService,
|
|
) {}
|
|
|
|
// ── Scenarios ──
|
|
|
|
@Get('scenarios')
|
|
@AllowViewer()
|
|
listScenarios(@Query('type') type?: string) {
|
|
return this.service.listScenarios(type);
|
|
}
|
|
|
|
@Get('scenarios/:id')
|
|
@AllowViewer()
|
|
getScenario(@Param('id') id: string) {
|
|
return this.service.getScenario(id);
|
|
}
|
|
|
|
@Post('scenarios')
|
|
createScenario(@Body() dto: any, @Req() req: any) {
|
|
return this.service.createScenario(dto, req.user.sub);
|
|
}
|
|
|
|
@Put('scenarios/:id')
|
|
updateScenario(@Param('id') id: string, @Body() dto: any) {
|
|
return this.service.updateScenario(id, dto);
|
|
}
|
|
|
|
@Delete('scenarios/:id')
|
|
deleteScenario(@Param('id') id: string) {
|
|
return this.service.deleteScenario(id);
|
|
}
|
|
|
|
// ── Scenario Investments ──
|
|
|
|
@Get('scenarios/:scenarioId/investments')
|
|
@AllowViewer()
|
|
listInvestments(@Param('scenarioId') scenarioId: string) {
|
|
return this.service.listInvestments(scenarioId);
|
|
}
|
|
|
|
@Post('scenarios/:scenarioId/investments')
|
|
addInvestment(@Param('scenarioId') scenarioId: string, @Body() dto: any) {
|
|
return this.service.addInvestment(scenarioId, dto);
|
|
}
|
|
|
|
@Post('scenarios/:scenarioId/investments/from-recommendation')
|
|
addFromRecommendation(@Param('scenarioId') scenarioId: string, @Body() dto: any) {
|
|
return this.service.addInvestmentFromRecommendation(scenarioId, dto);
|
|
}
|
|
|
|
@Put('investments/:id')
|
|
updateInvestment(@Param('id') id: string, @Body() dto: any) {
|
|
return this.service.updateInvestment(id, dto);
|
|
}
|
|
|
|
@Delete('investments/:id')
|
|
removeInvestment(@Param('id') id: string) {
|
|
return this.service.removeInvestment(id);
|
|
}
|
|
|
|
// ── Scenario Assessments ──
|
|
|
|
@Get('scenarios/:scenarioId/assessments')
|
|
@AllowViewer()
|
|
listAssessments(@Param('scenarioId') scenarioId: string) {
|
|
return this.service.listAssessments(scenarioId);
|
|
}
|
|
|
|
@Post('scenarios/:scenarioId/assessments')
|
|
addAssessment(@Param('scenarioId') scenarioId: string, @Body() dto: any) {
|
|
return this.service.addAssessment(scenarioId, dto);
|
|
}
|
|
|
|
@Put('assessments/:id')
|
|
updateAssessment(@Param('id') id: string, @Body() dto: any) {
|
|
return this.service.updateAssessment(id, dto);
|
|
}
|
|
|
|
@Delete('assessments/:id')
|
|
removeAssessment(@Param('id') id: string) {
|
|
return this.service.removeAssessment(id);
|
|
}
|
|
|
|
// ── Projections ──
|
|
|
|
@Get('scenarios/:id/projection')
|
|
@AllowViewer()
|
|
getProjection(@Param('id') id: string) {
|
|
return this.projection.getProjection(id);
|
|
}
|
|
|
|
@Post('scenarios/:id/projection/refresh')
|
|
refreshProjection(@Param('id') id: string) {
|
|
return this.projection.computeProjection(id);
|
|
}
|
|
|
|
// ── Comparison ──
|
|
|
|
@Get('compare')
|
|
@AllowViewer()
|
|
compareScenarios(@Query('ids') ids: string) {
|
|
const scenarioIds = ids.split(',').map((s) => s.trim()).filter(Boolean);
|
|
return this.projection.compareScenarios(scenarioIds);
|
|
}
|
|
|
|
// ── Execute Investment ──
|
|
|
|
@Post('investments/:id/execute')
|
|
executeInvestment(
|
|
@Param('id') id: string,
|
|
@Body() dto: { executionDate: string },
|
|
@Req() req: any,
|
|
) {
|
|
return this.service.executeInvestment(id, dto.executionDate, req.user.sub);
|
|
}
|
|
|
|
// ── Budget Planning ──
|
|
|
|
@Get('budget-plans')
|
|
@AllowViewer()
|
|
listBudgetPlans() {
|
|
return this.budgetPlanning.listPlans();
|
|
}
|
|
|
|
@Get('budget-plans/available-years')
|
|
@AllowViewer()
|
|
getAvailableYears() {
|
|
return this.budgetPlanning.getAvailableYears();
|
|
}
|
|
|
|
@Get('budget-plans/:year')
|
|
@AllowViewer()
|
|
getBudgetPlan(@Param('year') year: string) {
|
|
return this.budgetPlanning.getPlan(parseInt(year, 10));
|
|
}
|
|
|
|
@Post('budget-plans')
|
|
createBudgetPlan(@Body() dto: { fiscalYear: number; baseYear: number; inflationRate?: number }, @Req() req: any) {
|
|
return this.budgetPlanning.createPlan(dto.fiscalYear, dto.baseYear, dto.inflationRate ?? 2.5, req.user.sub);
|
|
}
|
|
|
|
@Put('budget-plans/:year/lines')
|
|
updateBudgetPlanLines(@Param('year') year: string, @Body() dto: { planId: string; lines: any[] }) {
|
|
return this.budgetPlanning.updateLines(dto.planId, dto.lines);
|
|
}
|
|
|
|
@Put('budget-plans/:year/inflation')
|
|
updateBudgetPlanInflation(@Param('year') year: string, @Body() dto: { inflationRate: number }) {
|
|
return this.budgetPlanning.updateInflation(parseInt(year, 10), dto.inflationRate);
|
|
}
|
|
|
|
@Put('budget-plans/:year/status')
|
|
advanceBudgetPlanStatus(@Param('year') year: string, @Body() dto: { status: string }, @Req() req: any) {
|
|
return this.budgetPlanning.advanceStatus(parseInt(year, 10), dto.status, req.user.sub);
|
|
}
|
|
|
|
@Post('budget-plans/:year/import')
|
|
importBudgetPlanLines(
|
|
@Param('year') year: string,
|
|
@Body() lines: any[],
|
|
@Req() req: any,
|
|
) {
|
|
return this.budgetPlanning.importLines(parseInt(year, 10), lines, req.user.sub);
|
|
}
|
|
|
|
@Get('budget-plans/:year/template')
|
|
async getBudgetPlanTemplate(
|
|
@Param('year') year: string,
|
|
@Res() res: Response,
|
|
) {
|
|
const csv = await this.budgetPlanning.getTemplate(parseInt(year, 10));
|
|
res.set({
|
|
'Content-Type': 'text/csv',
|
|
'Content-Disposition': `attachment; filename="budget_template_${year}.csv"`,
|
|
});
|
|
res.send(csv);
|
|
}
|
|
|
|
@Delete('budget-plans/:year')
|
|
deleteBudgetPlan(@Param('year') year: string) {
|
|
return this.budgetPlanning.deletePlan(parseInt(year, 10));
|
|
}
|
|
}
|