diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx index 736af3fd5..08a5d12fd 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoesTable.tsx @@ -1,4 +1,5 @@ -import { useCargoesByContainer, useDeliverCargo, useUnloadCargo } from '@/hooks/useCargoes'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -6,9 +7,14 @@ import { LoadCargoDialog } from './LoadCargoDialog'; import type { Cargo } from '@/services/cargoService'; export function CargoesTable({ containerId }: { containerId: string }) { - const { data: cargoes, refetch } = useCargoesByContainer(containerId); - const deliver = useDeliverCargo(); - const unload = useUnloadCargo(); + const { data: cargoes, refetch } = useQuery( + api.cargoes.listByContainer.queryOptions({ + input: { containerId }, + enabled: !!containerId, + }), + ); + const deliver = useMutation(api.cargoes.deliver.mutationOptions()); + const unload = useMutation(api.cargoes.unload.mutationOptions()); if (!cargoes?.length) return
No cargoes for this container.
; @@ -34,8 +40,8 @@ export function CargoesTable({ containerId }: { containerId: string }) { {cargo.status} {cargo.status === 'PENDING' && refetch()} />} - {cargo.status === 'LOADED' && } - {cargo.status === 'LOADED' && } + {cargo.status === 'LOADED' && } + {cargo.status === 'LOADED' && } ))} diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx index caa554a10..546bd0def 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx @@ -6,7 +6,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -import { useDeliverCargo } from '@/hooks/useCargoes'; +import { useMutation } from '@tanstack/react-query'; +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; /** @@ -18,7 +19,7 @@ export function DeliverCargoDialog({ cargoId, onSuccess }: { cargoId: string; on const [receiverName, setReceiverName] = useState(''); const [pickupDate, setPickupDate] = useState(''); const [deliveryRemarks, setDeliveryRemarks] = useState(''); - const deliver = useDeliverCargo(); + const deliver = useMutation(api.cargoes.deliver.mutationOptions()); const { toast } = useToast(); const handleDeliver = async () => { diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx index 188726352..8094bd902 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/LoadCargoDialog.tsx @@ -3,7 +3,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useLoadCargo } from '@/hooks/useCargoes'; +import { useMutation } from '@tanstack/react-query'; +import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuccess?: () => void }) { @@ -11,7 +12,7 @@ export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuc const [quantity, setQuantity] = useState(0); const [weight, setWeight] = useState(0); const [volume, setVolume] = useState(); - const load = useLoadCargo(); + const load = useMutation(api.cargoes.load.mutationOptions()); const { toast } = useToast(); const handleLoad = async () => { diff --git a/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts b/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts deleted file mode 100644 index 3c1cdf517..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/useCargoes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { cargoService, type DeliverCargoPayload } from '@/services/cargoService'; - -export const cargoKeys = { - all: ['cargoes'] as const, - byContainer: (containerId: string) => [...cargoKeys.all, 'container', containerId] as const, - details: () => [...cargoKeys.all, 'detail'] as const, - detail: (id: string) => [...cargoKeys.details(), id] as const, -}; - -export function useCargoes() { - return useQuery({ queryKey: cargoKeys.all, queryFn: () => cargoService.getAll().then(res => res.data) }); -} - -export const useGetCargoes = useCargoes; - -export function useCargoesByContainer(containerId: string) { - return useQuery({ queryKey: cargoKeys.byContainer(containerId), queryFn: () => cargoService.getByContainer(containerId).then(res => res.data), enabled: !!containerId }); -} - -export function useCargo(id: string) { - return useQuery({ queryKey: cargoKeys.detail(id), queryFn: () => cargoService.getById(id).then(res => res.data), enabled: !!id }); -} - -export const useGetCargo = useCargo; - -export function useCreateCargo() { - const qc = useQueryClient(); - return useMutation({ mutationFn: cargoService.create, onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) }); -} - -export function useUpdateCargo() { - const qc = useQueryClient(); - return useMutation({ mutationFn: ({ id, data }: any) => cargoService.update(id, data), onSuccess: (_, { id }) => { - qc.invalidateQueries({ queryKey: cargoKeys.all }); - qc.invalidateQueries({ queryKey: cargoKeys.detail(id) }); - } }); -} - -export function useDeleteCargo() { - const qc = useQueryClient(); - return useMutation({ mutationFn: cargoService.delete, onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) }); -} - -export function useLoadCargo() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, quantity, weight, volume }: any) => cargoService.load(id, quantity, weight, volume), - onSuccess: (_, { id }) => qc.invalidateQueries({ queryKey: cargoKeys.all }) - }); -} - -export function useDeliverCargo() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, payload }: { id: string; payload?: DeliverCargoPayload }) => - cargoService.deliver(id, payload), - onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) - }); -} - -export function useUnloadCargo() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => cargoService.unload(id), - onSuccess: () => qc.invalidateQueries({ queryKey: cargoKeys.all }) - }); -} diff --git a/apps/edr-freight-web/backoffice/src/lib/queryClient.ts b/apps/edr-freight-web/backoffice/src/lib/queryClient.ts index 9ac8402d8..32b94f325 100644 --- a/apps/edr-freight-web/backoffice/src/lib/queryClient.ts +++ b/apps/edr-freight-web/backoffice/src/lib/queryClient.ts @@ -1,7 +1,29 @@ -import { QueryClient } from "@tanstack/react-query"; +import { MutationCache, QueryClient } from "@tanstack/react-query"; -/** Single app-wide React Query client (do not nest additional providers). */ +import type { InvalidatesMeta } from "@/utils/endpoint"; + +/** + * Single app-wide React Query client (do not nest additional providers). + * + * Declarative invalidation: any mutation built via `api.*.mutationOptions()` + * (see `services/api.ts` + `utils/endpoint.ts`) carries an `invalidates` + * function in its `meta`. The shared `MutationCache` below runs it on success + * and invalidates the returned query keys — so invalidation is declared once in + * the endpoint definition rather than re-wired in every component. + */ export const queryClient = new QueryClient({ + mutationCache: new MutationCache({ + onSuccess: (data, variables, _context, mutation) => { + const invalidates = mutation.meta?.invalidates as + | InvalidatesMeta + | undefined; + if (typeof invalidates !== "function") return; + + for (const queryKey of invalidates(variables, data)) { + void queryClient.invalidateQueries({ queryKey }); + } + }, + }), defaultOptions: { queries: { retry: 1, diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx index 9206372e4..538b8ff6d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx @@ -1,5 +1,8 @@ import { FormEvent, ReactNode, useMemo, useState } from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { Edit, Eye, Plus, Search, Trash2 } from 'lucide-react'; + +import { api } from '@/services/api'; import { ActionIcon, Badge as MantineBadge, @@ -42,7 +45,6 @@ import { useWagonTypes, } from '@/hooks/use-wagon-types'; import { useToast } from '@/hooks/use-toast'; -import { useCreateCargo, useDeleteCargo, useCargoes, useUpdateCargo } from '@/hooks/useCargoes'; import { useContainers, useCreateContainer, @@ -1041,7 +1043,7 @@ export function ContainersCrudPage() { } export function CargoesCrudPage() { - const query = useCargoes(); + const query = useQuery(api.cargoes.list.queryOptions()); const { data: cargoTypes = [] } = useCargoTypes(); const { data: containers = [] } = useContainers(); const cargoTypeOptions = cargoTypes.map((type: any) => ({ @@ -1059,9 +1061,9 @@ export function CargoesCrudPage() { addLabel="Add Cargo" data={query.data} isLoading={query.isLoading} - create={useCreateCargo()} - update={useUpdateCargo()} - remove={useDeleteCargo()} + create={useMutation(api.cargoes.create.mutationOptions())} + update={useMutation(api.cargoes.update.mutationOptions())} + remove={useMutation(api.cargoes.remove.mutationOptions())} searchText={(cargo) => [cargo.cargoReference, cargo.description, cargo.containerId, cargo.status].join(' ')} columns={[ { key: 'cargoReference', label: 'Reference' }, diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index 14bf3454b..410f68b0c 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -37,8 +37,724 @@ import { import type { BookingDetail } from "@/types/booking"; import type { IOverviewDashboard, OverviewRange } from "@/types/overview"; import { overviewService } from "./overview.service"; +import { + cargoService, + type Cargo, + type DeliverCargoPayload, +} from "./cargoService"; +import { warehouseService } from "./warehouse.service"; +import type { + AllocationCriteria, + AllocationPreviewResult, + AllocationRule, + ArrivalQueueItem, + AutoLoadResult, + AutoUnloadArrivedResult, + AutoUnloadResult, + BookingScheduleView, + BulkDispatchResult, + BulkInspectPayload, + BulkInspectResult, + BulkReceivePayload, + BulkReceiveResult, + DeliverInventoryPayload, + EligibleBooking, + FeePreview, + FeeRule, + ImportTrain, + ImportTrainItem, + ImportUnloadedItem, + InspectionAttachment, + InspectionReport, + InspectionReportPayload, + InventoryFilter, + InventoryInquiryFilter, + InventoryInquiryResult, + InventoryMovement, + LoadableWagon, + LoadInventoryPayload, + LoadPassedExportResult, + MoveInventoryPayload, + PayInvoicePayload, + ReadyToLoadRow, + ReceiveInventoryPayload, + ReleaseOrderPayload, + ReserveInventoryPayload, + SaveAllocationRulePayload, + SaveFeeRulePayload, + SaveWarehousePayload, + SaveYardPayload, + SaveZonePayload, + Warehouse, + WarehouseActivityLog, + WarehouseDashboard, + WarehouseFeeInvoice, + WarehouseFilter, + WarehouseInventoryItem, + WarehouseInvoiceFilter, + WarehouseLoading, + WarehouseYard, + WarehouseZone, +} from "@/types/warehouse"; + +/** Query keys for inventory-lifecycle mutations that ripple across views. */ +const INVENTORY_INVALIDATIONS: ReadonlyArray = [ + ["warehouse-inventory"], + ["warehouse-loadings"], + ["warehouses"], +]; export const api = { + warehouses: { + // ── Warehouses ───────────────────────────────────────────────────────── + list: endpoint<{ filter?: WarehouseFilter }, Warehouse[]>( + "warehouses", + "list", + ({ filter }) => warehouseService.list(filter).then((r) => r.data), + ), + + getById: endpoint<{ id: string }, Warehouse>( + "warehouses", + "getById", + ({ id }) => warehouseService.getById(id).then((r) => r.data), + ), + + dashboard: endpoint("warehouses", "dashboard", () => + warehouseService.dashboard().then((r) => r.data), + ), + + create: endpoint( + "warehouses", + "create", + (payload) => warehouseService.create(payload).then((r) => r.data), + undefined, + () => [["warehouses"]], + ), + + update: endpoint< + { id: string; payload: Partial }, + Warehouse + >( + "warehouses", + "update", + ({ id, payload }) => + warehouseService.update(id, payload).then((r) => r.data), + undefined, + () => [["warehouses"]], + ), + + // ── Yards ────────────────────────────────────────────────────────────── + listYards: endpoint<{ warehouseId: string }, WarehouseYard[]>( + "warehouses", + "listYards", + ({ warehouseId }) => + warehouseService.listYards(warehouseId).then((r) => r.data), + ), + + createYard: endpoint< + { warehouseId: string; payload: SaveYardPayload }, + WarehouseYard + >( + "warehouses", + "createYard", + ({ warehouseId, payload }) => + warehouseService.createYard(warehouseId, payload).then((r) => r.data), + undefined, + () => [["warehouses"]], + ), + + updateYard: endpoint< + { id: string; payload: Partial }, + WarehouseYard + >( + "warehouses", + "updateYard", + ({ id, payload }) => + warehouseService.updateYard(id, payload).then((r) => r.data), + undefined, + () => [["warehouses"], ["warehouse-yards"]], + ), + + // ── Zones ────────────────────────────────────────────────────────────── + listZones: endpoint<{ yardId: string }, WarehouseZone[]>( + "warehouses", + "listZones", + ({ yardId }) => warehouseService.listZones(yardId).then((r) => r.data), + ({ yardId }) => ["warehouse-yards", yardId, "zones"], + ), + + createZone: endpoint< + { yardId: string; payload: SaveZonePayload }, + WarehouseZone + >( + "warehouses", + "createZone", + ({ yardId, payload }) => + warehouseService.createZone(yardId, payload).then((r) => r.data), + undefined, + ({ yardId }) => [["warehouse-yards", yardId, "zones"]], + ), + + updateZone: endpoint< + { id: string; payload: Partial }, + WarehouseZone + >( + "warehouses", + "updateZone", + ({ id, payload }) => + warehouseService.updateZone(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-yards"]], + ), + + // ── Inventory (queries) ──────────────────────────────────────────────── + listInventory: endpoint< + { filter?: InventoryFilter }, + WarehouseInventoryItem[] + >( + "warehouse-inventory", + "list", + ({ filter }) => warehouseService.listInventory(filter).then((r) => r.data), + ), + + inquiry: endpoint< + { filter: InventoryInquiryFilter }, + InventoryInquiryResult[] + >( + "warehouse-inventory", + "inquiry", + ({ filter }) => warehouseService.inquiry(filter).then((r) => r.data), + ({ filter }) => ["warehouse-inventory", "inquiry", filter], + ), + + eligibleBookings: endpoint( + "warehouse-inventory", + "eligible-bookings", + () => warehouseService.eligibleBookings().then((r) => r.data), + () => ["warehouse-inventory", "eligible-bookings"], + ), + + readyToLoadExport: endpoint( + "warehouse-inventory", + "ready-to-load-export", + () => warehouseService.readyToLoadExport().then((r) => r.data), + () => ["warehouse-inventory", "ready-to-load-export"], + ), + + loadedExport: endpoint( + "warehouse-inventory", + "loaded-export", + () => warehouseService.loadedExport().then((r) => r.data), + () => ["warehouse-inventory", "loaded-export"], + ), + + importArriveQueue: endpoint( + "warehouse-inventory", + "import-arrive-queue", + () => warehouseService.importArriveQueue().then((r) => r.data), + () => ["warehouse-inventory", "import-arrive-queue"], + ), + + importTrainItems: endpoint<{ scheduleId: string }, ImportTrainItem[]>( + "warehouse-inventory", + "import-train-items", + ({ scheduleId }) => + warehouseService.importTrainItems(scheduleId).then((r) => r.data), + ({ scheduleId }) => ["warehouse-inventory", "import-train-items", scheduleId], + ), + + importUnloadedQueue: endpoint( + "warehouse-inventory", + "import-unloaded-queue", + () => warehouseService.importUnloadedQueue().then((r) => r.data), + () => ["warehouse-inventory", "import-unloaded-queue"], + ), + + importPickupReadyQueue: endpoint( + "warehouse-inventory", + "import-pickup-ready-queue", + () => warehouseService.importPickupReadyQueue().then((r) => r.data), + () => ["warehouse-inventory", "import-pickup-ready-queue"], + ), + + loadableWagons: endpoint( + "warehouse", + "loadable-wagons", + () => warehouseService.loadableWagons().then((r) => r.data), + () => ["warehouse", "loadable-wagons"], + ), + + loadings: endpoint< + { params?: { bookingId?: string; wagonId?: string } }, + WarehouseLoading[] + >( + "warehouse-loadings", + "list", + ({ params }) => warehouseService.loadings(params).then((r) => r.data), + ({ params }) => ["warehouse-loadings", params ?? {}], + ), + + bookingSchedule: endpoint<{ bookingId: string }, BookingScheduleView>( + "warehouse", + "booking-schedule", + ({ bookingId }) => + warehouseService.bookingSchedule(bookingId).then((r) => r.data), + ({ bookingId }) => ["warehouse", "booking-schedule", bookingId], + ), + + movements: endpoint<{ id: string }, InventoryMovement[]>( + "warehouse-inventory", + "movements", + ({ id }) => warehouseService.movements(id).then((r) => r.data), + ({ id }) => ["warehouse-inventory", id, "movements"], + ), + + activity: endpoint<{ id: string }, WarehouseActivityLog[]>( + "warehouse-inventory", + "activity", + ({ id }) => warehouseService.activity(id).then((r) => r.data), + ({ id }) => ["warehouse-inventory", id, "activity"], + ), + + arrivalQueue: endpoint( + "warehouse-inventory", + "arrival-queue", + () => warehouseService.arrivalQueue().then((r) => r.data), + () => ["warehouse-inventory", "arrival-queue"], + ), + + inspectionReports: endpoint<{ inventoryId: string }, InspectionReport[]>( + "warehouse-inventory", + "inspection-reports", + ({ inventoryId }) => + warehouseService.listInspectionReports(inventoryId).then((r) => r.data), + ({ inventoryId }) => + ["warehouse-inventory", inventoryId, "inspection-reports"], + ), + + allocationRules: endpoint( + "warehouse-allocation-rules", + "list", + () => warehouseService.listAllocationRules().then((r) => r.data), + () => ["warehouse-allocation-rules"], + ), + + feeRules: endpoint( + "warehouse-fee-rules", + "list", + () => warehouseService.listFeeRules().then((r) => r.data), + () => ["warehouse-fee-rules"], + ), + + feePreview: endpoint<{ inventoryId: string }, FeePreview[]>( + "warehouse-inventory", + "fee-preview", + ({ inventoryId }) => + warehouseService.feePreview(inventoryId).then((r) => r.data), + ({ inventoryId }) => ["warehouse-inventory", inventoryId, "fee-preview"], + ), + + invoices: endpoint<{ filter?: WarehouseInvoiceFilter }, WarehouseFeeInvoice[]>( + "warehouse-fee-invoices", + "list", + ({ filter }) => warehouseService.listInvoices(filter).then((r) => r.data), + ({ filter }) => ["warehouse-fee-invoices", filter ?? {}], + ), + + invoice: endpoint<{ id: string }, WarehouseFeeInvoice>( + "warehouse-fee-invoices", + "detail", + ({ id }) => warehouseService.getInvoice(id).then((r) => r.data), + ({ id }) => ["warehouse-fee-invoices", "detail", id], + ), + + invoicesForInventory: endpoint< + { inventoryId: string }, + WarehouseFeeInvoice[] + >( + "warehouse-inventory", + "fee-invoices", + ({ inventoryId }) => + warehouseService.invoicesForInventory(inventoryId).then((r) => r.data), + ({ inventoryId }) => ["warehouse-inventory", inventoryId, "fee-invoices"], + ), + + // ── Inventory (mutations) ────────────────────────────────────────────── + receiveInventory: endpoint( + "warehouse-inventory", + "receive", + (payload) => warehouseService.receiveInventory(payload).then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + store: endpoint( + "warehouse-inventory", + "store", + (id) => warehouseService.store(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + reserve: endpoint( + "warehouse-inventory", + "reserve", + (payload) => warehouseService.reserve(payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + markReadyForLoading: endpoint( + "warehouse-inventory", + "mark-ready-for-loading", + (id) => warehouseService.markReadyForLoading(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + load: endpoint< + { id: string; payload: LoadInventoryPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "load", + ({ id, payload }) => warehouseService.load(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + dispatch: endpoint( + "warehouse-inventory", + "dispatch", + (id) => warehouseService.dispatch(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + move: endpoint< + { id: string; payload: MoveInventoryPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "move", + ({ id, payload }) => warehouseService.move(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + markReadyForPickup: endpoint( + "warehouse-inventory", + "mark-ready-for-pickup", + (id) => warehouseService.markReadyForPickup(id).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + release: endpoint< + { id: string; payload: ReleaseOrderPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "release", + ({ id, payload }) => + warehouseService.release(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + deliver: endpoint< + { id: string; payload: DeliverInventoryPayload }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "deliver", + ({ id, payload }) => + warehouseService.deliver(id, payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + bulkReceive: endpoint( + "warehouse-inventory", + "bulk-receive", + (payload) => warehouseService.receiveBulk(payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + loadPassedExport: endpoint( + "warehouse-inventory", + "load-passed-export", + () => warehouseService.loadPassedExport().then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + bulkMarkInspected: endpoint( + "warehouse-inventory", + "bulk-mark-inspected", + (payload) => warehouseService.bulkMarkInspected(payload).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + bulkDispatchExport: endpoint( + "warehouse-inventory", + "bulk-dispatch-export", + (inventoryIds) => + warehouseService.bulkDispatchExport(inventoryIds).then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + autoUnloadArrivedBookings: endpoint( + "warehouse-inventory", + "auto-unload-arrived-bookings", + (scheduleId) => + warehouseService + .autoUnloadArrivedBookings(scheduleId) + .then((r) => r.data), + undefined, + () => INVENTORY_INVALIDATIONS, + ), + + autoUnloadArrived: endpoint( + "warehouse-inventory", + "auto-unload-arrived", + () => warehouseService.autoUnloadArrived().then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + autoLoadReady: endpoint( + "warehouse-inventory", + "auto-load-ready", + () => warehouseService.autoLoadReady().then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + unloadBooking: endpoint< + { bookingId: string; payload?: Record }, + WarehouseInventoryItem + >( + "warehouse-inventory", + "unload-booking", + ({ bookingId, payload }) => + warehouseService.unloadBooking(bookingId, payload).then((r) => r.data), + undefined, + () => [["warehouse-inventory"], ["warehouses"]], + ), + + createInspectionReport: endpoint< + { inventoryId: string; payload: InspectionReportPayload }, + InspectionReport + >( + "warehouse-inventory", + "create-inspection-report", + ({ inventoryId, payload }) => + warehouseService + .createInspectionReport(inventoryId, payload) + .then((r) => r.data), + undefined, + ({ inventoryId }) => [ + ["warehouse-inventory", inventoryId, "inspection-reports"], + ["warehouse-inventory"], + ], + ), + + uploadInspectionAttachments: endpoint< + { reportId: string; files: File[] }, + InspectionAttachment[] + >( + "warehouse-inventory", + "upload-inspection-attachments", + ({ reportId, files }) => + warehouseService + .uploadInspectionAttachments(reportId, files) + .then((r) => r.data), + ), + + // ── Allocation + fee rules ───────────────────────────────────────────── + previewAllocation: endpoint( + "warehouse-allocation-rules", + "preview", + (criteria) => + warehouseService.previewAllocation(criteria).then((r) => r.data), + ), + + createAllocationRule: endpoint( + "warehouse-allocation-rules", + "create", + (payload) => + warehouseService.createAllocationRule(payload).then((r) => r.data), + undefined, + () => [["warehouse-allocation-rules"]], + ), + + updateAllocationRule: endpoint< + { id: string; payload: Partial }, + AllocationRule + >( + "warehouse-allocation-rules", + "update", + ({ id, payload }) => + warehouseService.updateAllocationRule(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-allocation-rules"]], + ), + + deleteAllocationRule: endpoint( + "warehouse-allocation-rules", + "delete", + (id) => warehouseService.deleteAllocationRule(id).then(() => undefined), + undefined, + () => [["warehouse-allocation-rules"]], + ), + + createFeeRule: endpoint( + "warehouse-fee-rules", + "create", + (payload) => warehouseService.createFeeRule(payload).then((r) => r.data), + undefined, + () => [["warehouse-fee-rules"]], + ), + + updateFeeRule: endpoint< + { id: string; payload: Partial }, + FeeRule + >( + "warehouse-fee-rules", + "update", + ({ id, payload }) => + warehouseService.updateFeeRule(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-fee-rules"]], + ), + + deleteFeeRule: endpoint( + "warehouse-fee-rules", + "delete", + (id) => warehouseService.deleteFeeRule(id).then(() => undefined), + undefined, + () => [["warehouse-fee-rules"]], + ), + + // ── Invoices ─────────────────────────────────────────────────────────── + generateInvoice: endpoint< + { inventoryId: string; confirmZero?: boolean }, + WarehouseFeeInvoice + >( + "warehouse-fee-invoices", + "generate", + ({ inventoryId, confirmZero }) => + warehouseService + .generateInvoice(inventoryId, confirmZero) + .then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + + cancelInvoice: endpoint( + "warehouse-fee-invoices", + "cancel", + (id) => warehouseService.cancelInvoice(id).then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + + payInvoice: endpoint< + { id: string; payload: PayInvoicePayload }, + WarehouseFeeInvoice + >( + "warehouse-fee-invoices", + "pay", + ({ id, payload }) => + warehouseService.payInvoice(id, payload).then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + + gateClearance: endpoint( + "warehouse-fee-invoices", + "gate-clearance", + (inventoryId) => + warehouseService.gateClearance(inventoryId).then((r) => r.data), + undefined, + () => [["warehouse-fee-invoices"], ["warehouse-inventory"]], + ), + }, + + cargoes: { + list: endpoint("cargoes", "list", () => + cargoService.getAll().then((r) => r.data), + ), + + listByContainer: endpoint<{ containerId: string }, Cargo[]>( + "cargoes", + "listByContainer", + ({ containerId }) => + cargoService.getByContainer(containerId).then((r) => r.data), + ), + + getById: endpoint<{ id: string }, Cargo>("cargoes", "getById", ({ id }) => + cargoService.getById(id).then((r) => r.data), + ), + + create: endpoint, Cargo>( + "cargoes", + "create", + (payload) => cargoService.create(payload).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + update: endpoint<{ id: string; data: Partial }, Cargo>( + "cargoes", + "update", + ({ id, data }) => cargoService.update(id, data).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + remove: endpoint( + "cargoes", + "remove", + (id) => cargoService.delete(id).then(() => undefined), + undefined, + () => [["cargoes"]], + ), + + load: endpoint< + { id: string; quantity: number; weight: number; volume?: number }, + Cargo + >( + "cargoes", + "load", + ({ id, quantity, weight, volume }) => + cargoService.load(id, quantity, weight, volume).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + deliver: endpoint<{ id: string; payload?: DeliverCargoPayload }, Cargo>( + "cargoes", + "deliver", + ({ id, payload }) => + cargoService.deliver(id, payload).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + + unload: endpoint<{ id: string }, Cargo>( + "cargoes", + "unload", + ({ id }) => cargoService.unload(id).then((r) => r.data), + undefined, + () => [["cargoes"]], + ), + }, + fileUploadSettings: { list: endpoint( "file-upload-settings", diff --git a/apps/edr-freight-web/backoffice/src/utils/endpoint.ts b/apps/edr-freight-web/backoffice/src/utils/endpoint.ts index 4a4af69d6..02bc70e85 100644 --- a/apps/edr-freight-web/backoffice/src/utils/endpoint.ts +++ b/apps/edr-freight-web/backoffice/src/utils/endpoint.ts @@ -12,6 +12,27 @@ export type QueryConfig = Omit< "queryKey" | "queryFn" >; +/** + * Query keys a mutation should invalidate on success. Receives the mutation + * input and response so keys can be derived from them. Returns a list of query + * keys — each is matched as a *prefix* by React Query, so returning a service + * root (e.g. `["cargoes"]`) invalidates every query nested under it. + * + * The keys are surfaced through `mutationOptions().meta.invalidates`; the + * app-wide `MutationCache` (see `lib/queryClient.ts`) reads them and invalidates + * automatically, so components never wire `onSuccess` invalidation by hand. + */ +export type InvalidatesFn = ( + input: TInput, + data: TResponse, +) => ReadonlyArray; + +/** Shape stored in `mutation.meta.invalidates` and consumed by the MutationCache. */ +export type InvalidatesMeta = ( + variables: unknown, + data: unknown, +) => ReadonlyArray; + // --------------------------------------------------------------------------- // Endpoint interfaces // --------------------------------------------------------------------------- @@ -45,6 +66,7 @@ export function endpoint( action: string, execute: (input: TInput) => Promise, queryKeyBuilder?: (input: TInput) => readonly unknown[], + invalidates?: InvalidatesFn, ) { const buildKey = (input?: TInput): readonly unknown[] => { if (queryKeyBuilder && input !== undefined) { @@ -77,27 +99,25 @@ export function endpoint( }; const mutationOptions = ( - config?: Omit< - UseMutationOptions< - TResponse, - Error, - TInput - >, - "mutationFn" - >, -): UseMutationOptions< - TResponse, - Error, - TInput -> => { - return { - ...config, - mutationFn: ( - variables: TInput, - ): Promise => - execute(variables), + config?: Omit, "mutationFn">, + ): UseMutationOptions => { + const meta = invalidates + ? { + ...config?.meta, + invalidates: ((variables, data) => + invalidates( + variables as TInput, + data as TResponse, + )) satisfies InvalidatesMeta, + } + : config?.meta; + + return { + ...config, + meta, + mutationFn: (variables: TInput): Promise => execute(variables), + }; }; -}; return { call,