mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
Optional IAM account per agent — nullable, nothing backfilled, so roster-only rows keep working. Staff invite existing ones; new ones get an account when an email is supplied. Activation reuses the shipping-line link path. SMS reachability widens to Djibouti. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
256 lines
8.6 KiB
TypeScript
256 lines
8.6 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Param,
|
|
ParseUUIDPipe,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
UploadedFiles,
|
|
UseInterceptors,
|
|
} from "@nestjs/common";
|
|
import { AnyFilesInterceptor } from "@nestjs/platform-express";
|
|
import {
|
|
ApiBearerAuth,
|
|
ApiConsumes,
|
|
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, PortalCustomer } from "../../common/booking-guards";
|
|
import { documentUploadMulterOptions } from "../../common/document-upload.options";
|
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
|
import { CreateTransitAssignmentDto } from "./dto/create-transit-assignment.dto";
|
|
import { MyAssignmentsQueryDto } from "./dto/my-assignments-query.dto";
|
|
import { SubmitTransitAssignmentDto } from "./dto/submit-transit-assignment.dto";
|
|
import { TransitAssignmentQueryDto } from "./dto/transit-assignment-query.dto";
|
|
import { UpdateTransitAssignmentDto } from "./dto/update-transit-assignment.dto";
|
|
import { TransitAssignmentsService } from "./transit-assignments.service";
|
|
|
|
/**
|
|
* Transit assignments — one transit agent's work on one booking.
|
|
*
|
|
* Distinct from the transit-assignee handshake under
|
|
* `/bookings/:id/clearance/transit-assignee/...`, which decides WHO will handle
|
|
* a shipment's customs. This is the work record that follows: status, timings
|
|
* and documents.
|
|
*/
|
|
@ApiTags("transit-assignments")
|
|
@Controller("transit-assignments")
|
|
@ApiBearerAuth()
|
|
export class TransitAssignmentsController {
|
|
constructor(
|
|
private readonly transitAssignmentsService: TransitAssignmentsService,
|
|
) {}
|
|
|
|
// ── Portal — the signed-in transit agent's own work ───────────────────────
|
|
// Declared first so the literal `my` segment is matched before `:id`.
|
|
// Every route resolves the agent from the session; none accepts an agent id.
|
|
|
|
@Get("my/stats")
|
|
@PortalCustomer()
|
|
@ApiOperation({
|
|
summary: "Dashboard figures for the signed-in transit agent's own work",
|
|
})
|
|
myStats(@CurrentUser() user: TCurrentUser) {
|
|
return this.transitAssignmentsService.myStats(user.id);
|
|
}
|
|
|
|
@Get("my")
|
|
@PortalCustomer()
|
|
@ApiOperation({
|
|
summary:
|
|
"The signed-in transit agent's assigned bookings (paginated, filterable)",
|
|
})
|
|
findMine(
|
|
@CurrentUser() user: TCurrentUser,
|
|
@Query() query: MyAssignmentsQueryDto,
|
|
) {
|
|
return this.transitAssignmentsService.findMine(user.id, query);
|
|
}
|
|
|
|
@Get("my/:id")
|
|
@PortalCustomer()
|
|
@ApiOperation({ summary: "One of my assignments, with its documents" })
|
|
findMineById(
|
|
@CurrentUser() user: TCurrentUser,
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
) {
|
|
return this.transitAssignmentsService.findMineById(user.id, id);
|
|
}
|
|
|
|
@Post("my/:id/files")
|
|
@PortalCustomer()
|
|
@ApiConsumes("multipart/form-data")
|
|
@UseInterceptors(AnyFilesInterceptor(documentUploadMulterOptions))
|
|
@ApiOperation({
|
|
summary:
|
|
"Upload documents to my assignment. Allowed only while the booking is DISPATCHED and the assignment is not finished.",
|
|
})
|
|
uploadMyFiles(
|
|
@CurrentUser() user: TCurrentUser,
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@UploadedFiles() files: Express.Multer.File[],
|
|
// One `titles` part per file, in the same order. A single-file upload posts
|
|
// one part, which multipart parsing hands back as a bare string rather than
|
|
// an array — normalised here so the service always sees a positional list.
|
|
@Body("titles") titles?: string | string[],
|
|
) {
|
|
return this.transitAssignmentsService.uploadMyFiles(
|
|
user.id,
|
|
id,
|
|
files,
|
|
{ userId: user.id, name: user.name?.en ?? undefined },
|
|
titles === undefined ? undefined : ([] as string[]).concat(titles),
|
|
);
|
|
}
|
|
|
|
@Delete("my/:id/files/:fileId")
|
|
@PortalCustomer()
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
@ApiOperation({ summary: "Remove a document from my assignment" })
|
|
removeMyFile(
|
|
@CurrentUser() user: TCurrentUser,
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("fileId", ParseUUIDPipe) fileId: string,
|
|
) {
|
|
return this.transitAssignmentsService.removeMyFile(user.id, id, fileId);
|
|
}
|
|
|
|
@Post("my/:id/submit")
|
|
@PortalCustomer()
|
|
@ApiOperation({
|
|
summary:
|
|
"Save progress, or finish the assignment (which locks its documents)",
|
|
})
|
|
submitMine(
|
|
@CurrentUser() user: TCurrentUser,
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: SubmitTransitAssignmentDto,
|
|
) {
|
|
return this.transitAssignmentsService.submitMine(user.id, id, dto);
|
|
}
|
|
|
|
// ── Backoffice ────────────────────────────────────────────────────────────
|
|
|
|
@Get()
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.view)
|
|
@ApiOperation({
|
|
summary:
|
|
"List transit assignments (paginated, filterable by booking / agent / status)",
|
|
})
|
|
findAll(@Query() query: TransitAssignmentQueryDto) {
|
|
return this.transitAssignmentsService.findAll(query);
|
|
}
|
|
|
|
/**
|
|
* Declared before `:id` — Nest matches routes in order, so a literal segment
|
|
* registered after a parameter would be swallowed by it.
|
|
*/
|
|
@Get("by-agent/:transitAgentId")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.view)
|
|
@ApiOperation({ summary: "Every assignment handed to one transit agent" })
|
|
findByTransitAgent(
|
|
@Param("transitAgentId", ParseUUIDPipe) transitAgentId: string,
|
|
) {
|
|
return this.transitAssignmentsService.findByTransitAgent(transitAgentId);
|
|
}
|
|
|
|
@Get("by-booking/:bookingId")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.view)
|
|
@ApiOperation({ summary: "Every transit agent assigned to one booking" })
|
|
findByBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) {
|
|
return this.transitAssignmentsService.findByBooking(bookingId);
|
|
}
|
|
|
|
@Get(":id")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.view)
|
|
@ApiOperation({
|
|
summary:
|
|
"One assignment, with its attached documents and computed duration",
|
|
})
|
|
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.transitAssignmentsService.findById(id);
|
|
}
|
|
|
|
@Post()
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.create)
|
|
@ApiOperation({ summary: "Assign a transit agent to a booking" })
|
|
create(
|
|
@Body() dto: CreateTransitAssignmentDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.transitAssignmentsService.create(dto, user?.id);
|
|
}
|
|
|
|
@Patch(":id")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.update)
|
|
@ApiOperation({
|
|
summary:
|
|
"Update status or note — status changes stamp the start/finish clocks",
|
|
})
|
|
update(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateTransitAssignmentDto,
|
|
) {
|
|
return this.transitAssignmentsService.update(id, dto);
|
|
}
|
|
|
|
@Delete(":id")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.delete)
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
@ApiOperation({ summary: "Soft-delete an assignment" })
|
|
remove(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.transitAssignmentsService.remove(id);
|
|
}
|
|
|
|
// ── Documents ─────────────────────────────────────────────────────────────
|
|
|
|
@Get(":id/files")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.view)
|
|
@ApiOperation({ summary: "An assignment's uploaded documents" })
|
|
listFiles(@Param("id", ParseUUIDPipe) id: string) {
|
|
return this.transitAssignmentsService.listFiles(id);
|
|
}
|
|
|
|
@Post(":id/files")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.update)
|
|
@ApiConsumes("multipart/form-data")
|
|
@UseInterceptors(AnyFilesInterceptor(documentUploadMulterOptions))
|
|
@ApiOperation({
|
|
summary:
|
|
"Upload one or more documents; re-uploading adds a version, it does not overwrite",
|
|
})
|
|
uploadFiles(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@UploadedFiles() files: Express.Multer.File[],
|
|
@CurrentUser() user: TCurrentUser,
|
|
@Body("titles") titles?: string | string[],
|
|
) {
|
|
return this.transitAssignmentsService.uploadFiles(
|
|
id,
|
|
files,
|
|
{ userId: user?.id, name: user?.name?.en ?? undefined },
|
|
titles === undefined ? undefined : ([] as string[]).concat(titles),
|
|
);
|
|
}
|
|
|
|
@Delete(":id/files/:fileId")
|
|
@BookingStaff(FREIGHT_PERMS.transitAssignments.update)
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
@ApiOperation({ summary: "Remove one document from an assignment" })
|
|
removeFile(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("fileId", ParseUUIDPipe) fileId: string,
|
|
) {
|
|
return this.transitAssignmentsService.removeFile(id, fileId);
|
|
}
|
|
}
|