Introduces a capability layer on top of existing roles that controls feature visibility and access. Capabilities follow an area.feature.action taxonomy (~35 capabilities) with sensible defaults per role. Tenant admins can customize via grant/revoke overrides stored in org settings JSONB. Key changes: - Add vice_president role to DB schema - Backend: capability constants, resolution logic, CapabilityGuard (global), @RequireCapability decorator on all 16 tenant controllers - Frontend: permission hooks (useCanEdit, useHasCapability), CapabilityGate component, sidebar filtering by capability, all 17 pages migrated from useIsReadOnly to capability-based checks - New admin UI: /settings/permissions matrix page for per-tenant role customization with grant/revoke delta model - GET /organizations/my-capabilities endpoint for capability refresh - Validation of permissionOverrides in settings updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
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 { RequireCapability } from '../../common/decorators/capability.decorator';
|
|
import { BudgetsService } from './budgets.service';
|
|
import { UpsertBudgetDto } from './dto/upsert-budget.dto';
|
|
|
|
@ApiTags('budgets')
|
|
@Controller('budgets')
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard)
|
|
export class BudgetsController {
|
|
constructor(private budgetsService: BudgetsService) {}
|
|
|
|
@Post(':year/import')
|
|
@ApiOperation({ summary: 'Import budget data from parsed CSV/XLSX lines' })
|
|
@RequireCapability('financials.budgets.edit')
|
|
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' })
|
|
@RequireCapability('financials.budgets.view')
|
|
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' })
|
|
@RequireCapability('financials.budgets.view')
|
|
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' })
|
|
@RequireCapability('financials.budgets.view')
|
|
findByYear(@Param('year', ParseIntPipe) year: number) {
|
|
return this.budgetsService.findByYear(year);
|
|
}
|
|
|
|
@Put(':year')
|
|
@ApiOperation({ summary: 'Upsert budgets for a fiscal year' })
|
|
@RequireCapability('financials.budgets.edit')
|
|
upsert(
|
|
@Param('year', ParseIntPipe) year: number,
|
|
@Body() budgets: UpsertBudgetDto[],
|
|
) {
|
|
return this.budgetsService.upsert(year, budgets);
|
|
}
|
|
}
|