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

@@ -30,6 +30,7 @@ import { assignability, assignedFrom, type Assignability } from '../assignDelive
import { useCustomers, useDeliveries, useOrders, useRiders, useTenantLocations } from '@/queries/hooks';
import { useDateScope } from '@/components/shell/DateScope';
import { useBranchScope } from '../BranchScope';
import { canAssign, scopeBranches, scopeLabel, scopeQuery, type DispatchScope } from '../dispatchScope';
import { count, money, moneyExact } from '../format';
import { DELIVERY_STATUS, statusColor } from '../orderStatus';
import { ActivePanel } from '../ActivePanel';
@@ -65,8 +66,25 @@ type Board = ViewMode | 'timing' | 'active' | 'profit';
const GROUPINGS = new Set<Board>(['riders', 'stores', 'customers']);
const isGrouping = (board: Board): board is ViewMode => GROUPINGS.has(board);
/**
* The shop's own dispatch — a thin wrapper that reads the branch scope from
* context and hands the board a tenant scope.
*
* The board itself knows nothing about logins. That is what lets the Nearle
* Admin render the same component against a partner instead, and why the
* platform board cannot reach a shop's own riders: there is no field on the
* partner scope to put a tenantid in.
*/
export function DispatchPage() {
const { branches, selected, tenantid } = useBranchScope();
return (
<DispatchBoard
scope={{ kind: 'tenant', tenantid, locationid: selected ?? undefined, branches }}
/>
);
}
export function DispatchBoard({ scope }: { scope: DispatchScope }) {
const dates = useDateScope();
const [mode, setMode] = useState<Board>('riders');
const [focused, setFocused] = useState<string | null>(null);
@@ -74,16 +92,23 @@ export function DispatchPage() {
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'in_progress' | 'delivered' | 'unassigned'>('all');
const query = tenantid
? { tenantid, locationid: selected ?? undefined, ...dates.range, pagesize: 200 }
: undefined;
/* One id or the other, never both — see `DispatchScope`. */
const isPartnerBoard = scope.kind === 'partner';
const tenantid = scope.kind === 'tenant' ? scope.tenantid : 0;
const branches = scopeBranches(scope);
const query =
isPartnerBoard || tenantid ? scopeQuery(scope, dates.range) : undefined;
const deliveriesQuery = useDeliveries(query);
const ordersQuery = useOrders(query);
const locationsQuery = useTenantLocations(tenantid || undefined);
/* The customer book is a tenant's own list, so a partner board has none —
and grouping a partner's rounds by customer would mean reading every
merchant's book to name them. The tab is not offered there. */
const customers = useCustomers(
tenantid && mode === 'customers'
? { tenantid, locationid: selected ?? undefined, pagesize: 500 }
? { tenantid, locationid: scope.kind === 'tenant' ? scope.locationid : undefined, pagesize: 500 }
: undefined,
);
@@ -228,14 +253,19 @@ export function DispatchPage() {
<span className="sbt-badge">{branches.length}</span>
</button>
<button
type="button"
className={`sbt ${mode === 'customers' ? 'active' : ''}`}
onClick={() => changeMode('customers')}
>
<span className="sbt-icon"><Users size={15} /></span>
<span>By Customer</span>
</button>
{/* A shop's customer book is its own. Grouping a partner's rounds by
customer would mean reading every merchant's book to put a name to
a phone number, so the tab is not offered on a partner board. */}
{scope.kind === 'tenant' ? (
<button
type="button"
className={`sbt ${mode === 'customers' ? 'active' : ''}`}
onClick={() => changeMode('customers')}
>
<span className="sbt-icon"><Users size={15} /></span>
<span>By Customer</span>
</button>
) : null}
<button
type="button"
@@ -395,7 +425,7 @@ export function DispatchPage() {
mode={mode}
onOpen={setDetail}
locations={locationsQuery.data ?? branches}
{...(open.id === UNASSIGNED
{...(open.id === UNASSIGNED && canAssign(scope)
? {
selection: picked,
verdictOf: (row: OrderRow) => assignability(row, branchOf(row), assigned),
@@ -442,8 +472,8 @@ export function DispatchPage() {
range is selected, which is usually history. */}
<GroupMap
stops={stops}
groupName={dates.range.fromdate ? `Dispatch ${dates.range.fromdate}` : 'All Rounds'}
emptyHint="Select a date range with active deliveries to view live GPS trails."
groupName={scopeLabel(scope)}
emptyHint="Change the date range in the top bar — the board opens on the current month, and a quiet month has nothing to draw."
locations={locationsQuery.data ?? branches}
/>
@@ -509,7 +539,7 @@ export function DispatchPage() {
<OrderDetailDrawer row={detail.row} kind={detail.kind} onClose={() => setDetail(null)} />
) : null}
{isAutoOpen ? (
{isAutoOpen && canAssign(scope) ? (
<AutoAssignDrawer
orders={waiting}
branches={branches}