diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts index c6910c7a7..3d9fa4254 100644 --- a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts @@ -91,11 +91,15 @@ export class LastMileRequestsController { return this.contractService.sign(id, dto, user?.id ?? null); } + // Customer-facing like :id/contract/view — the portal's confirm page opens + // this straight from the departure notification link before the customer + // has done anything else, so it can't be staff-only. Service ownership- + // checks against the resolved company; staff may also open it. @Get(':id') - @BookingStaff(FREIGHT_PERMS.lastMile.requestView) + @MixedAudience(FREIGHT_PERMS.lastMile.requestView) @ApiOperation({ summary: 'Get a last-mile confirmation request by ID' }) - findOne(@Param('id', ParseUUIDPipe) id: string) { - return this.requestsService.findById(id); + findOne(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: TCurrentUser) { + return this.requestsService.findById(id, user?.id ?? null); } // No @BookingStaff — the customer (portal) fills this, not backoffice staff. diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts index b89f06479..ebdc93746 100644 --- a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts @@ -199,11 +199,25 @@ export class LastMileRequestsService { }; } - async findById(id: string): Promise { + /** + * `userId` is set only when a portal customer calls this directly (the + * confirm-page deep link from the departure notification, before they've + * submitted or signed anything) — staff and every internal caller pass + * nothing and skip the check, same convention as submit()/sign(). + */ + async findById(id: string, userId?: string | null): Promise { const record = await this.requestsRepository.findById(id, { relations: { booking: { company: true } }, }); if (!record) throw new NotFoundException(`Last-mile request ${id} not found`); + + if (userId) { + const companyId = await this.bookingsService.resolveCustomerCompanyId(userId); + if (companyId && record.booking?.companyId && companyId !== record.booking.companyId) { + throw new BadRequestException('This request does not belong to your company'); + } + } + return record; } diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index 153ed5e8b..a0d33f5ba 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -224,10 +224,10 @@ export const URL_CONSTANTS = { }, LAST_MILE_REQUESTS: { - BY_ID: (id: string) => `/last-mile-requests/${id}`, - SUBMIT: (id: string) => `/last-mile-requests/${id}/submit`, - CONTRACT_VIEW: (id: string) => `/last-mile-requests/${id}/contract/view`, - CONTRACT_DOCUMENT: (id: string) => `/last-mile-requests/${id}/contract/document`, - CONTRACT_SIGN: (id: string) => `/last-mile-requests/${id}/contract/sign`, + BY_ID: (id: string) => `/api/last-mile-requests/${id}`, + SUBMIT: (id: string) => `/api/last-mile-requests/${id}/submit`, + CONTRACT_VIEW: (id: string) => `/api/last-mile-requests/${id}/contract/view`, + CONTRACT_DOCUMENT: (id: string) => `/api/last-mile-requests/${id}/contract/document`, + CONTRACT_SIGN: (id: string) => `/api/last-mile-requests/${id}/contract/sign`, }, }; diff --git a/packages/ui-common/src/components/data-table/table.tsx b/packages/ui-common/src/components/data-table/table.tsx index 957da3300..3dc2d5786 100644 --- a/packages/ui-common/src/components/data-table/table.tsx +++ b/packages/ui-common/src/components/data-table/table.tsx @@ -13,7 +13,10 @@ import { DataTableFooter } from "./footer"; export function DataTable({ columns, data, - status, + // The body only renders under "success", but the footer renders regardless — + // omitting status gave a blank table under a populated "Showing 1–N of N" + // footer. Having rows to draw is the default case, so default to success. + status = "success", onRowClick, rowStyle, rowClassName,