ask again

This commit is contained in:
2026-09-03 11:17:31 +05:30
parent aad72cfe42
commit d39956a2f1
5 changed files with 238 additions and 45 deletions

View File

@@ -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={<X size={14} />}
isDisabled={decideBatch.isPending}
onClick={() => decideBatch.mutate({ ids: selection.chosen, approve: false })}
onClick={() => decideBatch.mutate({ ids: selection.chosen, action: 'reject' })}
/>
<Button
label={
@@ -355,7 +375,7 @@ function RequestsPanel({
icon={<Check size={14} />}
isLoading={decideBatch.isPending}
isDisabled={decideBatch.isPending}
onClick={() => decideBatch.mutate({ ids: selection.chosen, approve: true })}
onClick={() => decideBatch.mutate({ ids: selection.chosen, action: 'approve' })}
/>
</HStack>
) : null}
@@ -424,7 +444,7 @@ function RequestsPanel({
isDisabled={decide.isPending && acting === request.requestid}
onClick={() => {
setActing(request.requestid);
decide.mutate({ requestid: request.requestid, approve: false });
decide.mutate({ requestid: request.requestid, action: 'reject' });
}}
/>
<Button
@@ -435,14 +455,100 @@ function RequestsPanel({
isDisabled={decide.isPending && acting === request.requestid}
onClick={() => {
setActing(request.requestid);
decide.mutate({ requestid: request.requestid, approve: true });
decide.mutate({ requestid: request.requestid, action: 'approve' });
}}
/>
</HStack>
</HStack>
</Card>
))}
</VStack>
{/* ── Agreed, not yet arrived ─────────────────────────────────────────
The stage between deciding and stocking. Nothing here has touched the
shelf: the ledger is written when the goods turn up, so the count
never claims stock that is still on a van. */}
{inTransit.length > 0 ? (
<VStack gap={1}>
<SectionHeader
title="On their way"
note="nothing reaches a shelf until arrival is confirmed"
/>
<div className="bulkbar" data-active={arriving.count > 0 ? 'yes' : 'no'}>
<HStack justify="between" align="center" gap={2} wrap="wrap">
<label className="bulkbar-all">
<input
type="checkbox"
checked={arriving.allChosen}
ref={(el) => {
if (el) el.indeterminate = arriving.someChosen;
}}
onChange={arriving.toggleAll}
aria-label={arriving.allChosen ? 'Clear selection' : 'Select everything on its way'}
/>
<Text type="body" size="sm">
{arriving.count > 0
? `${count(arriving.count)} selected`
: `Select all ${count(inTransit.length)}`}
</Text>
</label>
{arriving.count > 0 ? (
<HStack gap={1} align="center" wrap="wrap">
<Button label="Clear" variant="ghost" size="sm" isDisabled={decideBatch.isPending} onClick={arriving.clear} />
<Button
label={decideBatch.isPending ? 'Working…' : `Confirm arrival of ${count(arriving.count)}`}
variant="primary"
size="sm"
icon={<CheckCheck size={14} />}
isLoading={decideBatch.isPending}
isDisabled={decideBatch.isPending}
onClick={() => decideBatch.mutate({ ids: arriving.chosen, action: 'arrive' })}
/>
</HStack>
) : null}
</HStack>
</div>
{inTransit.map((request) => (
<Card key={request.requestid} padding={0} elevation="low">
<HStack justify="between" align="center" gap={2} style={{ padding: 16 }}>
<HStack gap={1.5} align="center" style={{ minWidth: 0 }}>
<input
type="checkbox"
className="bulkbar-tick"
checked={arriving.has(request.requestid)}
onChange={() => arriving.toggle(request.requestid)}
aria-label={`Select ${request.productname ?? `product ${request.productid}`}`}
/>
<VStack gap={0} style={{ minWidth: 0 }}>
<Text type="label" size="sm" weight="semibold" maxLines={1}>
{request.productname ?? `Product ${request.productid}`}
</Text>
<Text type="body" size="xsm" color="secondary">
{branchLabel(request.locationname) || `Branch ${request.locationid}`} · <strong>{count(request.qty)}</strong> units approved
</Text>
</VStack>
</HStack>
<Button
label={`Confirm arrival of ${count(request.qty)}`}
variant="primary"
size="sm"
icon={<CheckCheck size={14} />}
isDisabled={decide.isPending && acting === request.requestid}
onClick={() => {
setActing(request.requestid);
decide.mutate({ requestid: request.requestid, action: 'arrive' });
}}
/>
</HStack>
</Card>
))}
</VStack>
) : null}
</VStack>
)}
{/* Stated once, under the queue, rather than on every row. */}