diff --git a/src/api/stock.ts b/src/api/stock.ts index 025b3bd..273ac6b 100644 --- a/src/api/stock.ts +++ b/src/api/stock.ts @@ -22,8 +22,20 @@ import type { StockRequest, StockStatementRow } from './types'; * read back, which is enough to drive a queue but is not a state machine — the * backend will accept any string at all. */ +/** + * The ladder a request climbs. + * + * `Approved` sits between asking and arriving, and adding it is the point: + * approving used to put the stock on the shelf immediately, so the count said + * the goods were there from the moment the admin agreed to send them — which is + * days before they arrive, and the branch sells against a shelf that is empty. + * + * Only `Received` moves the ledger. Fiesta keys the stock write on that exact + * word, so `Approved` is a status and nothing else. + */ export const STOCK_REQUEST_STATUS = { pending: 'Pending', + approved: 'Approved', received: 'Received', rejected: 'Rejected', } as const; @@ -92,13 +104,25 @@ export const stockApi = { }), /** - * Approve a request — which means marking it Received. + * Agree to send the stock. Nothing reaches the shelf yet. * - * This adds `request.qty` to the branch's stock. There is no way to approve a - * different amount: the service reads the quantity off the request row, not - * off this call. + * The shelf is written when the branch confirms the goods ARRIVED, not when + * the admin agrees to send them — see `confirmArrival`. */ approve: (requestid: number) => + api.put(`${WEB}/products/updatestockrequest`, { + requestid, + status: STOCK_REQUEST_STATUS.approved, + }), + + /** + * The goods turned up. THIS is what adds `request.qty` to the branch's stock. + * + * There is no way to receive a different amount: the service reads the + * quantity off the request row, not off this call. A short delivery has to be + * corrected on the stock ledger afterwards. + */ + confirmArrival: (requestid: number) => api.put(`${WEB}/products/updatestockrequest`, { requestid, status: STOCK_REQUEST_STATUS.received, @@ -119,7 +143,9 @@ export const stockApi = { decideMany: (requestids: number[], status: StockRequestStatus) => api.put(`${WEB}/products/updatestockrequest`, { requestids, status }), - approveMany: (requestids: number[]) => stockApi.decideMany(requestids, STOCK_REQUEST_STATUS.received), + approveMany: (requestids: number[]) => stockApi.decideMany(requestids, STOCK_REQUEST_STATUS.approved), + confirmArrivalMany: (requestids: number[]) => + stockApi.decideMany(requestids, STOCK_REQUEST_STATUS.received), rejectMany: (requestids: number[]) => stockApi.decideMany(requestids, STOCK_REQUEST_STATUS.rejected), /** diff --git a/src/features/store-admin/pages/InventoryPage.tsx b/src/features/store-admin/pages/InventoryPage.tsx index 497e2c5..e5b9252 100644 --- a/src/features/store-admin/pages/InventoryPage.tsx +++ b/src/features/store-admin/pages/InventoryPage.tsx @@ -216,8 +216,12 @@ function RequestsPanel({ }; const decide = useMutation({ - mutationFn: ({ requestid, approve }: { requestid: number; approve: boolean }) => - approve ? stockApi.approve(requestid) : stockApi.reject(requestid), + mutationFn: ({ requestid, action }: { requestid: number; action: 'approve' | 'reject' | 'arrive' }) => + action === 'approve' + ? stockApi.approve(requestid) + : action === 'arrive' + ? stockApi.confirmArrival(requestid) + : stockApi.reject(requestid), onSettled: async () => { setActing(null); await refreshAfterDecision(); @@ -225,7 +229,17 @@ function RequestsPanel({ }); const pending = requests.filter((entry) => entry.status === 'Pending'); + /** + * Agreed to, not yet arrived. + * + * The stage that did not exist: approving wrote the stock onto the shelf + * straight away, so the count claimed the goods were there from the moment + * the admin agreed to send them — days before the van turns up, and the + * branch sells against a shelf that is empty. + */ + const inTransit = requests.filter((entry) => entry.status === 'Approved'); const selection = useSelection(pending.map((entry) => entry.requestid)); + const arriving = useSelection(inTransit.map((entry) => entry.requestid)); /** * The batch decision. @@ -235,18 +249,24 @@ function RequestsPanel({ * really been received and the merchant must not approve them again. */ const decideBatch = useMutation({ - mutationFn: ({ ids, approve }: { ids: number[]; approve: boolean }) => - approve ? stockApi.approveMany(ids) : stockApi.rejectMany(ids), - onSuccess: (result, { ids, approve }) => { + mutationFn: ({ ids, action }: { ids: number[]; action: 'approve' | 'reject' | 'arrive' }) => + action === 'approve' + ? stockApi.approveMany(ids) + : action === 'arrive' + ? stockApi.confirmArrivalMany(ids) + : stockApi.rejectMany(ids), + onSuccess: (result, { ids, action }) => { const done = result?.updated?.length ?? ids.length; const failed = result?.failed?.length ?? 0; - const verb = approve ? 'approved' : 'rejected'; + const verb = + action === 'approve' ? 'approved' : action === 'arrive' ? 'added to the shelf' : 'rejected'; setOutcome( failed === 0 ? `${count(done)} ${verb}.` : `${count(done)} ${verb}. ${count(failed)} could not be — they are still waiting.`, ); selection.clear(); + arriving.clear(); }, onError: () => setOutcome('Nothing was changed. Try again, or decide them one at a time.'), onSettled: refreshAfterDecision, @@ -342,7 +362,7 @@ function RequestsPanel({ size="sm" icon={} isDisabled={decideBatch.isPending} - onClick={() => decideBatch.mutate({ ids: selection.chosen, approve: false })} + onClick={() => decideBatch.mutate({ ids: selection.chosen, action: 'reject' })} />