KM-Based Maintenance Scheduling

This commit is contained in:
Hagernesh
2026-07-23 07:47:08 +00:00
parent 4abcc909af
commit f2f6ac616f
3 changed files with 96 additions and 3 deletions

View File

@@ -4087,10 +4087,11 @@ export class WarehouseInventoryService {
await this.invoices.assertClearanceAllowed(item.id);
const approvedAt = new Date();
const signatureImageUrl = signature?.signatureImageUrl ?? null;
const approval = {
approvedAt: approvedAt.toISOString(),
signerDisplayName: name,
signatureImageUrl: signature?.signatureImageUrl ?? null,
signatureImageUrl,
userId,
};
const existingNotes = this.stripCustomerDeliveryApproval(item.notes);
@@ -4114,7 +4115,7 @@ export class WarehouseInventoryService {
// Sign the structured handover record(s) for this booking (self-haul: before
// the truck leaves). Kept alongside the legacy approval note.
await this.handover.signForBooking(bookingId, userId, name);
await this.handover.signForBooking(bookingId, userId, name, signatureImageUrl);
return {
bookingId,
@@ -4149,6 +4150,8 @@ export class WarehouseInventoryService {
throw new BadRequestException('Please enter your full name to sign the handover');
}
const signature = await this.signatures.getForUser(userId).catch(() => null);
const [h]: Array<{
bookingId: string;
reference: string;
@@ -4181,7 +4184,7 @@ export class WarehouseInventoryService {
);
if (inv) await this.invoices.assertClearanceAllowed(inv.id);
const signed = await this.handover.sign(handoverId, userId, name);
const signed = await this.handover.sign(handoverId, userId, name, signature?.signatureImageUrl ?? null);
const allSigned = await this.handover.isFullySigned(h.bookingId);
if (inv) {

View File

@@ -15,6 +15,7 @@ import { isViewable } from "@edr/ui-common";
import { api } from "@/services/api";
import { fetchViewableFile, downloadStoredFile } from "@/services/files.service";
import { warehouseService } from "@/services/warehouse.service";
import { useFileViewer } from "@/hooks/useFileViewer";
import { BookingActionModal } from "@/pages/bookings/clearance/BookingActionModal";
import { getBookingNextAction } from "@/pages/bookings/clearance/bookingNextAction";
@@ -241,6 +242,11 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) {
}),
);
const { data: handovers = [] } = useQuery({
queryKey: ["bookingHandovers", booking.id],
queryFn: () => warehouseService.bookingHandovers(booking.id),
});
const customerDocs = useMemo(
() => (clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"),
[clearance],
@@ -486,6 +492,69 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) {
</SectionCard>
)}
{/* ── 4. Handover signatures ──────────────────────────────────────── */}
{handovers.length > 0 && (
<SectionCard>
<CardTitle>Handover signatures</CardTitle>
<Text fz="12.5px" c="dimmed" mt={4} mb="sm">
Records of goods handover and customer signatures.
</Text>
<Stack gap={0}>
{handovers.map((h, i) => (
<Box
key={h.id}
px="md"
py="sm"
style={{
borderBottom:
i !== handovers.length - 1
? `1px solid ${BORDER}`
: "none",
}}
>
<Group justify="space-between" align="flex-start">
<Box>
<Text fw={500} fz="14px">
{h.reference}
</Text>
<Text fz="12.5px" c="dimmed" mt={2}>
{h.mileType === "SELF_HAUL"
? "Customer truck delivery"
: `EDR delivery${h.truckPlate ? ` (${h.truckPlate})` : ""}`}
</Text>
{h.signedAt && (
<Text fz="12.5px" c="dimmed" mt={1}>
Signed by {h.signerName || "Unknown"} on{" "}
{new Date(h.signedAt).toLocaleDateString()}
</Text>
)}
</Box>
<Pill
tone={h.signedAt ? "green" : "blue"}
label={h.signedAt ? "Signed" : "Pending signature"}
/>
</Group>
{h.signedAt && h.signatureImageUrl && (
<Box mt="sm">
<img
src={h.signatureImageUrl}
alt="Signature"
style={{
maxHeight: "60px",
maxWidth: "200px",
border: `1px solid ${BORDER}`,
borderRadius: "4px",
padding: "4px",
}}
/>
</Box>
)}
</Box>
))}
</Stack>
</SectionCard>
)}
{/* ── Warehouse documents (one-click bundle) ──────────────────────── */}
<SectionCard>
<CardTitle>Warehouse documents</CardTitle>

View File

@@ -41,6 +41,22 @@ export interface BookingScheduleView {
} | null;
}
export interface BookingHandover {
id: string;
bookingId: string;
truckAssignmentId?: string | null;
edrAssignmentId?: string | null;
truckPlate?: string | null;
mileType: 'SELF_HAUL' | 'EDR_LAST_MILE';
reference: string;
generatedAt: string;
signedAt?: string | null;
signerName?: string | null;
signedByUserId?: string | null;
signatureImageUrl?: string | null;
deliveredAt?: string | null;
}
export const warehouseService = {
listInventory: async (filter?: InventoryFilter): Promise<WarehouseInventoryItem[]> => {
const { data } = await client.get("/warehouse-inventory", {
@@ -59,4 +75,9 @@ export const warehouseService = {
const { data } = await client.get(`/warehouse-inventory/booking-schedule/${bookingId}`);
return data?.data ?? data ?? { schedule: null, wagon: null };
},
bookingHandovers: async (bookingId: string): Promise<BookingHandover[]> => {
const { data } = await client.get(`/warehouse-inventory/bookings/${bookingId}/handovers`);
return data?.data ?? data ?? [];
},
};