Files
edr-platform/apps/edr-freight-api/src/modules/overview/overview.controller.ts
ghost2023 17e41aa767 feat(overview): serve GET /overview/layouts filtered by permission
New catalog endpoint, same shape as GET /reports: returns the overview
layouts (key + label) the caller holds the matching
edr_freight_app:overview:<layout>:view permission for, in priority
order. Backend enforcement to go with the permission-based frontend
resolver (next commit) — a caller can no longer land on a layout their
JWT doesn't actually carry the permission for.
2026-08-21 15:43:26 +03:00

173 lines
6.0 KiB
TypeScript

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<OverviewResponseDto> {
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<OverviewBookingsTabDto> {
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<OverviewContractsTabDto> {
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<OverviewBillingTabDto> {
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<OverviewOperationsTabDto> {
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<OverviewFleetTabDto> {
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<OverviewClearanceTabDto> {
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<OverviewCustomersTabDto> {
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<OverviewStaffTabDto> {
return this.overviewService.getStaffTab(query.range ?? '30d');
}
}