mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
KM-Based Maintenance Scheduling ✅
This commit is contained in:
@@ -4087,10 +4087,11 @@ export class WarehouseInventoryService {
|
|||||||
await this.invoices.assertClearanceAllowed(item.id);
|
await this.invoices.assertClearanceAllowed(item.id);
|
||||||
|
|
||||||
const approvedAt = new Date();
|
const approvedAt = new Date();
|
||||||
|
const signatureImageUrl = signature?.signatureImageUrl ?? null;
|
||||||
const approval = {
|
const approval = {
|
||||||
approvedAt: approvedAt.toISOString(),
|
approvedAt: approvedAt.toISOString(),
|
||||||
signerDisplayName: name,
|
signerDisplayName: name,
|
||||||
signatureImageUrl: signature?.signatureImageUrl ?? null,
|
signatureImageUrl,
|
||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
const existingNotes = this.stripCustomerDeliveryApproval(item.notes);
|
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
|
// Sign the structured handover record(s) for this booking (self-haul: before
|
||||||
// the truck leaves). Kept alongside the legacy approval note.
|
// 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 {
|
return {
|
||||||
bookingId,
|
bookingId,
|
||||||
@@ -4149,6 +4150,8 @@ export class WarehouseInventoryService {
|
|||||||
throw new BadRequestException('Please enter your full name to sign the handover');
|
throw new BadRequestException('Please enter your full name to sign the handover');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signature = await this.signatures.getForUser(userId).catch(() => null);
|
||||||
|
|
||||||
const [h]: Array<{
|
const [h]: Array<{
|
||||||
bookingId: string;
|
bookingId: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
@@ -4181,7 +4184,7 @@ export class WarehouseInventoryService {
|
|||||||
);
|
);
|
||||||
if (inv) await this.invoices.assertClearanceAllowed(inv.id);
|
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);
|
const allSigned = await this.handover.isFullySigned(h.bookingId);
|
||||||
|
|
||||||
if (inv) {
|
if (inv) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { isViewable } from "@edr/ui-common";
|
|||||||
|
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { fetchViewableFile, downloadStoredFile } from "@/services/files.service";
|
import { fetchViewableFile, downloadStoredFile } from "@/services/files.service";
|
||||||
|
import { warehouseService } from "@/services/warehouse.service";
|
||||||
import { useFileViewer } from "@/hooks/useFileViewer";
|
import { useFileViewer } from "@/hooks/useFileViewer";
|
||||||
import { BookingActionModal } from "@/pages/bookings/clearance/BookingActionModal";
|
import { BookingActionModal } from "@/pages/bookings/clearance/BookingActionModal";
|
||||||
import { getBookingNextAction } from "@/pages/bookings/clearance/bookingNextAction";
|
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(
|
const customerDocs = useMemo(
|
||||||
() => (clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"),
|
() => (clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"),
|
||||||
[clearance],
|
[clearance],
|
||||||
@@ -486,6 +492,69 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) {
|
|||||||
</SectionCard>
|
</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) ──────────────────────── */}
|
{/* ── Warehouse documents (one-click bundle) ──────────────────────── */}
|
||||||
<SectionCard>
|
<SectionCard>
|
||||||
<CardTitle>Warehouse documents</CardTitle>
|
<CardTitle>Warehouse documents</CardTitle>
|
||||||
|
|||||||
@@ -41,6 +41,22 @@ export interface BookingScheduleView {
|
|||||||
} | null;
|
} | 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 = {
|
export const warehouseService = {
|
||||||
listInventory: async (filter?: InventoryFilter): Promise<WarehouseInventoryItem[]> => {
|
listInventory: async (filter?: InventoryFilter): Promise<WarehouseInventoryItem[]> => {
|
||||||
const { data } = await client.get("/warehouse-inventory", {
|
const { data } = await client.get("/warehouse-inventory", {
|
||||||
@@ -59,4 +75,9 @@ export const warehouseService = {
|
|||||||
const { data } = await client.get(`/warehouse-inventory/booking-schedule/${bookingId}`);
|
const { data } = await client.get(`/warehouse-inventory/booking-schedule/${bookingId}`);
|
||||||
return data?.data ?? data ?? { schedule: null, wagon: null };
|
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 ?? [];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user