bulk request

This commit is contained in:
2026-09-02 16:49:47 +05:30
parent d22b79e109
commit aad72cfe42
9 changed files with 862 additions and 26 deletions

View File

@@ -30,6 +30,7 @@ import { PageHeader } from '@/components/PageHeader';
import { SectionHeader } from '@/components/SectionHeader';
import { queryKeys } from '@/queries/keys';
import { useStockRequests, useStockStatement } from '@/queries/hooks';
import { useSelection } from '@/components/useSelection';
import { useBranchScope } from '../BranchScope';
import { CataloguePanel } from '../CataloguePanel';
import { ProductsPanel } from '../ProductsPanel';
@@ -206,19 +207,50 @@ function RequestsPanel({
}) {
const queryClient = useQueryClient();
const [acting, setActing] = useState<number | null>(null);
const [outcome, setOutcome] = useState<string | null>(null);
const refreshAfterDecision = async () => {
await queryClient.invalidateQueries({ queryKey: queryKeys.stock.all });
// Approving moves stock, so anything counting stock is stale too.
await queryClient.invalidateQueries({ queryKey: queryKeys.products.all });
};
const decide = useMutation({
mutationFn: ({ requestid, approve }: { requestid: number; approve: boolean }) =>
approve ? stockApi.approve(requestid) : stockApi.reject(requestid),
onSettled: async () => {
setActing(null);
await queryClient.invalidateQueries({ queryKey: queryKeys.stock.all });
// Approving moves stock, so anything counting stock is stale too.
await queryClient.invalidateQueries({ queryKey: queryKeys.products.all });
await refreshAfterDecision();
},
});
const pending = requests.filter((entry) => entry.status === 'Pending');
const selection = useSelection(pending.map((entry) => entry.requestid));
/**
* The batch decision.
*
* Reports what actually happened rather than assuming it all worked, because
* approving moves stock: if three of ten fail, those seven deliveries have
* 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 }) => {
const done = result?.updated?.length ?? ids.length;
const failed = result?.failed?.length ?? 0;
const verb = approve ? 'approved' : 'rejected';
setOutcome(
failed === 0
? `${count(done)} ${verb}.`
: `${count(done)} ${verb}. ${count(failed)} could not be — they are still waiting.`,
);
selection.clear();
},
onError: () => setOutcome('Nothing was changed. Try again, or decide them one at a time.'),
onSettled: refreshAfterDecision,
});
const settled = requests.filter((entry) => entry.status !== 'Pending');
return (
@@ -270,10 +302,85 @@ function RequestsPanel({
</Card>
) : (
<VStack gap={1}>
{/* The bulk bar. Present but quiet at rest: a row of checkboxes with
no visible action reads as broken, and a permanently loud bar
pushes the queue itself down the page. */}
<div className="bulkbar" data-active={selection.count > 0 ? 'yes' : 'no'}>
<HStack justify="between" align="center" gap={2} wrap="wrap">
<label className="bulkbar-all">
<input
type="checkbox"
checked={selection.allChosen}
ref={(el) => {
// The third state: some ticked, not all. Only settable
// from script, and without it "select all" looks off when
// two of nine are chosen.
if (el) el.indeterminate = selection.someChosen;
}}
onChange={selection.toggleAll}
aria-label={selection.allChosen ? 'Clear selection' : 'Select every request shown'}
/>
<Text type="body" size="sm">
{selection.count > 0
? `${count(selection.count)} selected`
: `Select all ${count(pending.length)}`}
</Text>
</label>
{selection.count > 0 ? (
<HStack gap={1} align="center" wrap="wrap">
<Button
label="Clear"
variant="ghost"
size="sm"
isDisabled={decideBatch.isPending}
onClick={selection.clear}
/>
<Button
label={`Reject ${count(selection.count)}`}
variant="secondary"
size="sm"
icon={<X size={14} />}
isDisabled={decideBatch.isPending}
onClick={() => decideBatch.mutate({ ids: selection.chosen, approve: false })}
/>
<Button
label={
decideBatch.isPending
? 'Working…'
: `Approve ${count(selection.count)}`
}
variant="primary"
size="sm"
icon={<Check size={14} />}
isLoading={decideBatch.isPending}
isDisabled={decideBatch.isPending}
onClick={() => decideBatch.mutate({ ids: selection.chosen, approve: true })}
/>
</HStack>
) : null}
</HStack>
</div>
{outcome ? (
<Card padding={2} variant="transparent">
<Text type="body" size="sm" role="status">
{outcome}
</Text>
</Card>
) : null}
{pending.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={selection.has(request.requestid)}
onChange={() => selection.toggle(request.requestid)}
aria-label={`Select ${request.productname ?? `product ${request.productid}`}`}
/>
<span
style={{
width: 40,