assign option
This commit is contained in:
156
src/features/store-admin/AssignBar.tsx
Normal file
156
src/features/store-admin/AssignBar.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
import { UserCheck } from 'lucide-react';
|
||||
import { errorMessage } from '@/api/client';
|
||||
import {
|
||||
RIDER_MESSAGE,
|
||||
RiderNotReachableError,
|
||||
deliveriesApi,
|
||||
} from '@/api/deliveries';
|
||||
import type { OrderRow, TenantLocation } from '@/api/types';
|
||||
import { useRiders } from '@/queries/hooks';
|
||||
import { queryKeys } from '@/queries/keys';
|
||||
import { buildDeliveries, riderName, riderVehicle } from './assignDelivery';
|
||||
|
||||
/**
|
||||
* Assigning riders, from the orders table itself.
|
||||
*
|
||||
* A bar above the list rather than a page or a drawer of its own, which is what
|
||||
* the old console did and the right shape for the job: assigning is something
|
||||
* you do WHILE reading the day's orders, glancing between the drop addresses
|
||||
* and who is free. A separate screen makes you leave the list to act on it and
|
||||
* come back to check.
|
||||
*
|
||||
* It appears when rows are ticked and stays afterwards to report what happened,
|
||||
* because the result is the only record — `createdeliveries` answers with a
|
||||
* message and no ids.
|
||||
*/
|
||||
export interface AssignBarProps {
|
||||
/** The ticked orders. */
|
||||
orders: OrderRow[];
|
||||
branchOf: (row: OrderRow) => TenantLocation | undefined;
|
||||
assigned: ReadonlySet<number>;
|
||||
onClear: () => void;
|
||||
onDone: () => void;
|
||||
}
|
||||
|
||||
export function AssignBar({ orders, branchOf, assigned, onClear, onDone }: AssignBarProps) {
|
||||
const client = useQueryClient();
|
||||
const [riderId, setRiderId] = useState('');
|
||||
const [outcome, setOutcome] = useState<string | null>(null);
|
||||
|
||||
// Every order in a batch shares a region in practice — they are one branch's
|
||||
// orders — so the first one carrying a region decides which fleet to ask for.
|
||||
const region =
|
||||
orders.map((row) => row.applocationid || branchOf(row)?.applocationid || 0).find(Boolean) ?? 0;
|
||||
|
||||
const riders = useRiders(region || undefined);
|
||||
const fleet = riders.data ?? [];
|
||||
const rider = fleet.find((entry) => String(entry.userid) === riderId);
|
||||
|
||||
const send = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!rider) throw new Error('Pick a rider first');
|
||||
const { drafts, skipped } = buildDeliveries(orders, rider, branchOf, new Date(), assigned);
|
||||
if (drafts.length === 0) {
|
||||
throw new Error(skipped[0]?.reason ?? 'None of these orders can be assigned');
|
||||
}
|
||||
await deliveriesApi.assign(drafts);
|
||||
return { count: drafts.length, skipped: skipped.length };
|
||||
},
|
||||
onSuccess: async ({ count, skipped }) => {
|
||||
const who = riderName(rider!);
|
||||
const base =
|
||||
skipped === 0
|
||||
? `${count} order${count === 1 ? '' : 's'} assigned to ${who}`
|
||||
: `${count} assigned to ${who} · ${skipped} could not be and are still waiting`;
|
||||
|
||||
await client.invalidateQueries({ queryKey: queryKeys.insights.all });
|
||||
onDone();
|
||||
|
||||
/*
|
||||
* The push is fired after the write and reported separately.
|
||||
*
|
||||
* The deliveries exist either way, so a failed notification must not read
|
||||
* as a failed assignment — but it must still be visible, because a rider
|
||||
* who was never told has work sitting unseen. This is the old console's
|
||||
* contract and it is the right one.
|
||||
*/
|
||||
setOutcome(`${base} · telling them…`);
|
||||
try {
|
||||
await deliveriesApi.notify(rider!.userfcmtoken ?? '', RIDER_MESSAGE.assigned(count));
|
||||
setOutcome(`${base} · rider notified`);
|
||||
} catch (error) {
|
||||
setOutcome(
|
||||
`${base} · NOT notified — ${
|
||||
error instanceof RiderNotReachableError
|
||||
? 'this rider has no device registered'
|
||||
: 'the push failed, tell them another way'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => setOutcome(errorMessage(error)),
|
||||
});
|
||||
|
||||
const options = fleet.map((entry) => ({
|
||||
value: String(entry.userid),
|
||||
label: riderVehicle(entry) ? `${riderName(entry)} · ${riderVehicle(entry)}` : riderName(entry),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="assign-bar" role="region" aria-label="Assign selected orders to a rider">
|
||||
<span className="assign-bar-count">
|
||||
<UserCheck size={15} />
|
||||
{orders.length} selected
|
||||
</span>
|
||||
|
||||
<div className="assign-bar-picker">
|
||||
<Selector
|
||||
label="Choose a rider"
|
||||
isLabelHidden
|
||||
size="sm"
|
||||
value={riderId}
|
||||
onChange={(value) => {
|
||||
setRiderId(String(value));
|
||||
setOutcome(null);
|
||||
}}
|
||||
options={options}
|
||||
isDisabled={orders.length === 0}
|
||||
/* The empty-list wording carries the actual information — which of
|
||||
the three situations you are in — so it lives in the placeholder
|
||||
rather than being flattened to "Select…". "No riders on duty" is a
|
||||
shift that has not started; the region case is a branch somebody
|
||||
has to configure. Neither is a fault in the orders. */
|
||||
placeholder={
|
||||
region === 0
|
||||
? 'No delivery region on this branch'
|
||||
: riders.isLoading
|
||||
? 'Loading riders…'
|
||||
: options.length > 0
|
||||
? 'Select a rider…'
|
||||
: 'No rider has clocked on today'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={send.isPending ? 'Assigning…' : 'Assign rider'}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<UserCheck size={14} />}
|
||||
isDisabled={!rider || orders.length === 0 || send.isPending}
|
||||
onClick={() => send.mutate()}
|
||||
/>
|
||||
<Button label="Clear" variant="ghost" size="sm" onClick={onClear} />
|
||||
|
||||
{outcome ? (
|
||||
<span className="assign-bar-outcome" role="status">
|
||||
{outcome}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user