This commit is contained in:
2026-09-11 16:32:50 +05:30
parent 81b7d32672
commit 0a11f7543c
24 changed files with 2036 additions and 798 deletions

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@astryxdesign/core/Button';
import { Selector } from '@astryxdesign/core/Selector';
import { Bike, Route, Truck, UserCheck } from 'lucide-react';
import { Route, UserCheck } from 'lucide-react';
import { errorMessage } from '@/api/client';
import {
RIDER_MESSAGE,
@@ -13,7 +13,7 @@ import type { OrderRow, TenantLocation } from '@/api/types';
import { useAllPartners, useOwnTenant, useRiders } from '@/queries/hooks';
import { queryKeys } from '@/queries/keys';
import { useBranchScope } from './BranchScope';
import { buildDeliveries, riderName, riderScope, riderVehicle } from './assignDelivery';
import { buildDeliveries, riderName, riderVehicle } from './assignDelivery';
import { RoutePlanDrawer } from './RoutePlanDrawer';
import './pages/deliveries.css';
@@ -60,7 +60,19 @@ export function AssignBar({ orders, branchOf, assigned, onClear, onDone }: Assig
const { tenantid } = useBranchScope();
const shop = useOwnTenant(tenantid || undefined);
const partnerid = Number((shop.data as unknown as Record<string, number>)?.['partnerid'] ?? 0);
const [source, setSource] = useState<'own' | 'partner'>(partnerid > 0 ? 'partner' : 'own');
/*
Both fleets are read, and the picker lists them together.
This used to be an either/or toggle, so a shop with a partner had to switch
back and forth to compare who was free — and the whole question at this
moment is "who can take this", across everybody available. Two reads and one
grouped list answers it in a glance instead.
Each rider still shows whose they are, because they are different people with
different employers and an operator handing over a parcel should know which.
*/
const ownRiders = useRiders(tenantid ? { tenantid } : {});
const partnerRiders = useRiders(partnerid ? { partnerid } : {});
// The partner's NAME on the tab, not "Partner riders". An operator handing an
// order to Xpress-Cbe-Main should read that, not a category.
@@ -76,8 +88,16 @@ export function AssignBar({ orders, branchOf, assigned, onClear, onDone }: Assig
2026-09-09: 117 of the platform's 118 riders belong to a partner, so region
scope was quietly offering other companies' fleets.
*/
const riders = useRiders(riderScope({ tenantid, partnerid, source }));
const fleet = riders.data ?? [];
const riders = { isLoading: ownRiders.isLoading || partnerRiders.isLoading };
const fleet = useMemo(
() => [...(ownRiders.data ?? []), ...(partnerRiders.data ?? [])],
[ownRiders.data, partnerRiders.data],
);
/** Whose rider this is, for the label beside their name. */
const ownIds = useMemo(
() => new Set((ownRiders.data ?? []).map((entry) => entry.userid)),
[ownRiders.data],
);
const rider = fleet.find((entry) => String(entry.userid) === riderId);
const send = useMutation({
@@ -125,10 +145,24 @@ export function AssignBar({ orders, branchOf, assigned, onClear, onDone }: Assig
onError: (error) => setOutcome(errorMessage(error)),
});
const options = fleet.map((entry) => ({
value: String(entry.userid),
label: riderVehicle(entry) ? `${riderName(entry)} · ${riderVehicle(entry)}` : riderName(entry),
}));
/*
Whose rider, on every row.
The two fleets are one list now, so the label has to carry the employer —
otherwise a shop with a partner reads eleven names and cannot tell which of
them it pays. The vehicle stays because it is the other thing an operator
picks on.
*/
const options = fleet.map((entry) => {
const whose = ownIds.has(entry.userid) ? 'yours' : partnerName || 'partner';
const vehicle = riderVehicle(entry);
return {
value: String(entry.userid),
label: vehicle
? `${riderName(entry)} · ${vehicle} · ${whose}`
: `${riderName(entry)} · ${whose}`,
};
});
return (
<div className="assign-bar" role="region" aria-label="Assign selected orders to a rider">
@@ -137,37 +171,6 @@ export function AssignBar({ orders, branchOf, assigned, onClear, onDone }: Assig
{orders.length} selected
</span>
{/* Only when there is a choice to make. A shop with no partner has one
source, and a toggle with one option is a control that asks a question
it already knows the answer to. */}
{partnerid > 0 ? (
<div className="assign-source" role="group" aria-label="Which riders to offer">
<button
type="button"
className="assign-source-btn"
aria-pressed={source === 'own'}
onClick={() => {
setSource('own');
setRiderId('');
}}
>
<Bike size={13} />
Own riders
</button>
<button
type="button"
className="assign-source-btn"
aria-pressed={source === 'partner'}
onClick={() => {
setSource('partner');
setRiderId('');
}}
>
<Truck size={13} />
{partnerName || 'Partner riders'}
</button>
</div>
) : null}
<div className="assign-bar-picker">
<Selector
@@ -186,19 +189,18 @@ export function AssignBar({ orders, branchOf, assigned, onClear, onDone }: Assig
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. */
/* Four situations, and the placeholder names which one — an operator
looking at an empty list needs to know whether to wait, to hire, or
to ask for a partner. */
/* An empty list has three different causes and the operator needs to
know which: wait for somebody to clock on, hire a rider, or ask
Nearle for a partner. Naming the fleets that were searched is what
distinguishes them. */
placeholder={
riders.isLoading
? 'Loading riders…'
: options.length > 0
? 'Select a rider…'
: source === 'partner'
? `No ${partnerName || 'partner'} rider has clocked on today`
: partnerid > 0
? 'This shop has no riders of its own'
: 'No rider has clocked on today'
: partnerid > 0
? `Nobody on duty — neither this shop's riders nor ${partnerName || "the partner's"}`
: 'Nobody on duty, and this shop has no delivery partner'
}
/>
</div>