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,