import { Controller, Get, Query } from '@nestjs/common'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { BookingStaff } from '../../common/booking-guards'; import { hasFreightPermission } from '../../common/freight-permission.util'; import { FREIGHT_PERMS, OVERVIEW_LAYOUT_KEYS, OVERVIEW_LAYOUT_LABELS, } from '../../seed/freight-permissions.registry'; import { OverviewLayoutDto } from './dto/overview-layout.dto'; import { OverviewQueryDto } from './dto/overview-query.dto'; import { OverviewResponseDto } from './dto/overview-response.dto'; import { OverviewBillingTabDto, OverviewBookingsTabDto, OverviewClearanceTabDto, OverviewContractsTabDto, OverviewCustomersTabDto, OverviewFleetTabDto, OverviewOperationsTabDto, OverviewStaffTabDto, } from './dto/overview-tab-response.dto'; import { OverviewService } from './overview.service'; import { UserTradeAccessService } from '../user-trade-access/user-trade-access.service'; @ApiTags('Overview') @ApiBearerAuth() @Controller('overview') export class OverviewController { constructor( private readonly overviewService: OverviewService, private readonly userTradeAccessService: UserTradeAccessService, ) {} /** * Layouts the caller has permission to render, in priority order — exactly * the same "server filters by permission, frontend just renders what comes * back" shape as GET /reports. A caller lands on exactly one layout, so the * frontend picks the first entry here rather than rendering the whole list. */ @Get('layouts') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Overview dashboard layouts the caller has permission to render' }) @ApiOkResponse({ type: OverviewLayoutDto, isArray: true }) getLayouts(@CurrentUser() user: TCurrentUser): OverviewLayoutDto[] { return OVERVIEW_LAYOUT_KEYS.filter((key) => hasFreightPermission(user, FREIGHT_PERMS.overview.layout(key)), ).map((key) => ({ key, label: OVERVIEW_LAYOUT_LABELS[key] })); } @Get() @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Aggregated dashboard summary for backoffice overview' }) @ApiOkResponse({ type: OverviewResponseDto }) async getDashboard( @Query() query: OverviewQueryDto, @CurrentUser() user: TCurrentUser, ): Promise { const allowed = await this.userTradeAccessService.resolveAllowedDirections(user); return this.overviewService.getDashboard( query.range ?? '30d', allowed ?? undefined, ); } @Get('bookings') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Bookings tab metrics and charts' }) @ApiOkResponse({ type: OverviewBookingsTabDto }) async getBookingsTab( @Query() query: OverviewQueryDto, @CurrentUser() user: TCurrentUser, ): Promise { const allowed = await this.userTradeAccessService.resolveAllowedDirections(user); return this.overviewService.getBookingsTab( query.range ?? '30d', allowed ?? undefined, ); } @Get('contracts') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Contracts tab metrics and charts' }) @ApiOkResponse({ type: OverviewContractsTabDto }) async getContractsTab( @Query() query: OverviewQueryDto, @CurrentUser() user: TCurrentUser, ): Promise { const allowed = await this.userTradeAccessService.resolveAllowedDirections(user); return this.overviewService.getContractsTab( query.range ?? '30d', allowed ?? undefined, ); } @Get('billing') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Billing tab metrics and charts' }) @ApiOkResponse({ type: OverviewBillingTabDto }) async getBillingTab( @Query() query: OverviewQueryDto, @CurrentUser() user: TCurrentUser, ): Promise { const allowed = await this.userTradeAccessService.resolveAllowedDirections(user); return this.overviewService.getBillingTab( query.range ?? '30d', allowed ?? undefined, ); } @Get('operations') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Operations tab metrics and charts' }) @ApiOkResponse({ type: OverviewOperationsTabDto }) getOperationsTab( @Query() query: OverviewQueryDto, ): Promise { return this.overviewService.getOperationsTab(query.range ?? '30d'); } @Get('fleet') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Wagon and locomotive fleet detail, plus turnaround' }) @ApiOkResponse({ type: OverviewFleetTabDto }) getFleetTab(@Query() query: OverviewQueryDto): Promise { return this.overviewService.getFleetTab(query.range ?? '30d'); } @Get('clearance') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Document review and invoice queues' }) @ApiOkResponse({ type: OverviewClearanceTabDto }) getClearanceTab(): Promise { return this.overviewService.getClearanceTab(); } @Get('customers') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Customers tab metrics and charts' }) @ApiOkResponse({ type: OverviewCustomersTabDto }) async getCustomersTab( @Query() query: OverviewQueryDto, @CurrentUser() user: TCurrentUser, ): Promise { const allowed = await this.userTradeAccessService.resolveAllowedDirections(user); return this.overviewService.getCustomersTab( query.range ?? '30d', allowed ?? undefined, ); } @Get('staff') @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Staff tab metrics and charts' }) @ApiOkResponse({ type: OverviewStaffTabDto }) getStaffTab(@Query() query: OverviewQueryDto): Promise { return this.overviewService.getStaffTab(query.range ?? '30d'); } }