mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
Merge pull request #459 from Tria-plc/CutomerTruckAssign
Cutomer truck assign Fields: Truck Plate Number, Driver Name, Truck Type, Container Number to Load (now the Select I just wired) → Verify & Submit Assignment.
This commit is contained in:
@@ -7,6 +7,9 @@ RUN apk add --no-cache libc6-compat
|
|||||||
# `--mount=type=cache,target=/pnpm/store` cache actually persists deps across builds.
|
# `--mount=type=cache,target=/pnpm/store` cache actually persists deps across builds.
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
# Puppeteer uses the system Chromium installed in the runner stage — skip the
|
||||||
|
# ~150MB bundled-Chromium download during pnpm install.
|
||||||
|
ENV PUPPETEER_SKIP_DOWNLOAD=true
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -32,8 +35,14 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
|||||||
pnpm deploy --filter="@edr/freight-api" --prod --legacy /deploy
|
pnpm deploy --filter="@edr/freight-api" --prod --legacy /deploy
|
||||||
|
|
||||||
FROM node:24.15.0-alpine AS runner
|
FROM node:24.15.0-alpine AS runner
|
||||||
RUN apk add --no-cache libc6-compat
|
# Chromium + fonts for headless PDF rendering (puppeteer). Alpine ships the
|
||||||
|
# binary at /usr/bin/chromium-browser, which the PDF renderer auto-detects
|
||||||
|
# (also pinned via PUPPETEER_EXECUTABLE_PATH). Without this, PDF generation
|
||||||
|
# falls back to a degraded hand-built layout.
|
||||||
|
RUN apk add --no-cache libc6-compat \
|
||||||
|
chromium nss freetype harfbuzz ca-certificates ttf-freefont
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 --ingroup nodejs nestjs
|
&& adduser --system --uid 1001 --ingroup nodejs nestjs
|
||||||
|
|||||||
@@ -1086,6 +1086,9 @@ export class BookingTransitionService {
|
|||||||
offeredAmount: number;
|
offeredAmount: number;
|
||||||
paymentDeadline: Date;
|
paymentDeadline: Date;
|
||||||
} | null;
|
} | null;
|
||||||
|
/** Flat list of physical container numbers on this booking (for the
|
||||||
|
* customer truck-assignment container picker). */
|
||||||
|
containerNumbers: string[];
|
||||||
}
|
}
|
||||||
> {
|
> {
|
||||||
// This enrichment runs AFTER the transition has committed. A failure here
|
// This enrichment runs AFTER the transition has committed. A failure here
|
||||||
@@ -1141,12 +1144,20 @@ export class BookingTransitionService {
|
|||||||
`enrichBookingResponse: batch-offer lookup failed for ${booking.id}: ${(err as Error).message}`,
|
`enrichBookingResponse: batch-offer lookup failed for ${booking.id}: ${(err as Error).message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Physical container numbers entered at booking time (booking_container
|
||||||
|
// units), flattened for the customer truck-assignment container picker.
|
||||||
|
const containerNumbers = (booking.bookingContainers ?? [])
|
||||||
|
.flatMap((bc) => bc.units ?? [])
|
||||||
|
.map((unit) => unit.containerNumber)
|
||||||
|
.filter((n): n is string => Boolean(n));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...booking,
|
...booking,
|
||||||
latestChangeRequestNote: note?.note ?? null,
|
latestChangeRequestNote: note?.note ?? null,
|
||||||
contractSummary: summary,
|
contractSummary: summary,
|
||||||
nextStep,
|
nextStep,
|
||||||
activeBatchOffer,
|
activeBatchOffer,
|
||||||
|
containerNumbers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1265,7 +1265,9 @@ export class TrainSchedulingService {
|
|||||||
performedBy: 'DOCUMENT_GENERATION',
|
performedBy: 'DOCUMENT_GENERATION',
|
||||||
});
|
});
|
||||||
const html = this.buildImportLoadListHtml(loadList);
|
const html = this.buildImportLoadListHtml(loadList);
|
||||||
const buffer = await this.pdfDocuments.htmlToPdfBuffer(html);
|
// Generic render — NOT the release-order fallback (would mislabel this as a
|
||||||
|
// gate-clearance / release order when Chromium is unavailable).
|
||||||
|
const buffer = await this.pdfDocuments.renderDocumentHtml(html, 'Import marshalling / load list');
|
||||||
const reference = loadList.trainNumber ?? loadList.trainScheduleId;
|
const reference = loadList.trainNumber ?? loadList.trainScheduleId;
|
||||||
return {
|
return {
|
||||||
filename: `import-marshalling-${this.safeDocumentName(reference)}.pdf`,
|
filename: `import-marshalling-${this.safeDocumentName(reference)}.pdf`,
|
||||||
@@ -1283,7 +1285,8 @@ export class TrainSchedulingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const html = this.buildExportLoadListHtml(schedule);
|
const html = this.buildExportLoadListHtml(schedule);
|
||||||
const buffer = await this.pdfDocuments.htmlToPdfBuffer(html);
|
// Generic render — NOT the release-order fallback (see importLoadListDocument).
|
||||||
|
const buffer = await this.pdfDocuments.renderDocumentHtml(html, 'Export marshalling / load list');
|
||||||
const reference = schedule.trainNumber ?? schedule.id;
|
const reference = schedule.trainNumber ?? schedule.id;
|
||||||
return {
|
return {
|
||||||
filename: `export-marshalling-${this.safeDocumentName(reference)}.pdf`,
|
filename: `export-marshalling-${this.safeDocumentName(reference)}.pdf`,
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ export class WarehouseReleaseDocumentService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render arbitrary document HTML to PDF via the shared renderer WITHOUT the
|
||||||
|
* release-order fallback. Non-release documents (e.g. the import/export
|
||||||
|
* marshalling load list) must use this so a Chromium-less fallback degrades to
|
||||||
|
* a plain-text dump of *their own* content — instead of masquerading as a
|
||||||
|
* "Warehouse Gate Clearance / Release Order", which the release-specific
|
||||||
|
* fallback would otherwise draw regardless of the input HTML.
|
||||||
|
*/
|
||||||
|
renderDocumentHtml(html: string, label = 'Document'): Promise<Buffer> {
|
||||||
|
return this.pdf.htmlToPdfBuffer(html, { label });
|
||||||
|
}
|
||||||
|
|
||||||
private htmlToBasicPdfBuffer(html: string): Buffer {
|
private htmlToBasicPdfBuffer(html: string): Buffer {
|
||||||
const doc = this.extractReleaseDocument(html);
|
const doc = this.extractReleaseDocument(html);
|
||||||
const body: string[] = [
|
const body: string[] = [
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export function CustomerTruckAssignmentCard({
|
|||||||
);
|
);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Physical container numbers on this booking — the customer picks which one to
|
||||||
|
// load onto the truck instead of typing it. Falls back to free entry when the
|
||||||
|
// booking has no container numbers recorded.
|
||||||
|
const containerOptions = booking.containerNumbers ?? [];
|
||||||
|
|
||||||
const assignMutation = useMutation(api.bookings.assignCustomerTruck.mutationOptions());
|
const assignMutation = useMutation(api.bookings.assignCustomerTruck.mutationOptions());
|
||||||
const downloadMutation = useMutation(api.bookings.downloadCustomerTruckFreightOrder.mutationOptions());
|
const downloadMutation = useMutation(api.bookings.downloadCustomerTruckFreightOrder.mutationOptions());
|
||||||
|
|
||||||
@@ -120,13 +125,27 @@ export function CustomerTruckAssignmentCard({
|
|||||||
onChange={(value) => setTruckType(value ?? "")}
|
onChange={(value) => setTruckType(value ?? "")}
|
||||||
disabled={assigned}
|
disabled={assigned}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
{containerOptions.length > 0 ? (
|
||||||
label="Container Number to Load"
|
<Select
|
||||||
required
|
label="Container Number to Load"
|
||||||
value={containerNumberToLoad}
|
required
|
||||||
onChange={(e) => setContainerNumberToLoad(e.currentTarget.value.toUpperCase())}
|
placeholder="Select a container from this booking"
|
||||||
readOnly={assigned}
|
data={containerOptions}
|
||||||
/>
|
value={containerNumberToLoad || null}
|
||||||
|
onChange={(value) => setContainerNumberToLoad(value ?? "")}
|
||||||
|
searchable
|
||||||
|
disabled={assigned}
|
||||||
|
nothingFoundMessage="No matching container"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextInput
|
||||||
|
label="Container Number to Load"
|
||||||
|
required
|
||||||
|
value={containerNumberToLoad}
|
||||||
|
onChange={(e) => setContainerNumberToLoad(e.currentTarget.value.toUpperCase())}
|
||||||
|
readOnly={assigned}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
|
|||||||
@@ -466,6 +466,10 @@ export interface IBooking extends BaseEntity {
|
|||||||
|
|
||||||
containers?: Array<{ type: string; qty: number; vgm: number }> | null;
|
containers?: Array<{ type: string; qty: number; vgm: number }> | null;
|
||||||
|
|
||||||
|
/** Physical container numbers on this booking (from booking container units),
|
||||||
|
* surfaced for the customer truck-assignment container picker. */
|
||||||
|
containerNumbers?: string[] | null;
|
||||||
|
|
||||||
versionNumber: number;
|
versionNumber: number;
|
||||||
priorityScore: number;
|
priorityScore: number;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user