api integration on the dispatch page

This commit is contained in:
2026-06-12 15:24:05 +05:30
parent 5378f2df1f
commit 0519e3b19c
7 changed files with 750 additions and 33 deletions

View File

@@ -0,0 +1,195 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Shared constants and pure helpers for dispatch operations.
* Reusable across DispatchView and child components.
*/
// ── Status Palette ────────────────────────────────────────────────────────────
export const STATUS_STYLES = {
created: { label: 'Created', bg: '#3b82f6', fg: '#fff' },
pending: { label: 'Pending', bg: '#f59e0b', fg: '#fff' },
accepted: { label: 'Accepted', bg: '#8b5cf6', fg: '#fff' },
arrived: { label: 'Arrived', bg: '#ea580c', fg: '#fff' },
picked: { label: 'Picked', bg: '#0ea5e9', fg: '#fff' },
active: { label: 'Active', bg: '#0ea5e9', fg: '#fff' },
delivered: { label: 'Delivered', bg: '#22c55e', fg: '#fff' },
skipped: { label: 'Skipped', bg: '#94a3b8', fg: '#fff' },
cancelled: { label: 'Cancelled', bg: '#ef4444', fg: '#fff' },
};
export interface StatusStyle {
label: string;
bg: string;
fg: string;
}
export function getStatusStyle(status: string | unknown): StatusStyle {
const key = String(status || '').toLowerCase() as keyof typeof STATUS_STYLES;
return STATUS_STYLES[key] || {
label: String(status || 'Unknown'),
bg: '#64748b',
fg: '#fff',
};
}
// ── Order Status Sets ────────────────────────────────────────────────────────
export const FINAL_STATUSES = new Set(['delivered']);
export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
// ── Step-wise Color Palette (for >10 stops) ──────────────────────────────────
export const STEP_PALETTE = [
'#2563eb', // blue-600
'#dc2626', // red-600
'#16a34a', // green-600
'#ea580c', // orange-600
'#9333ea', // purple-600
'#0891b2', // cyan-600
'#ca8a04', // yellow-600
'#db2777', // pink-600
'#0f766e', // teal-700
'#7c3aed', // violet-600
'#65a30d', // lime-600
'#0284c7', // sky-600
'#b91c1c', // red-700
'#15803d', // green-700
'#a16207', // yellow-700
'#86198f', // fuchsia-800
];
export function stepColor(index: number): string {
return STEP_PALETTE[((index % STEP_PALETTE.length) + STEP_PALETTE.length) % STEP_PALETTE.length];
}
// ── Rider/Zone Color Palette ──────────────────────────────────────────────────
const RIDER_COLORS = [
'#3b82f6', '#a855f7', '#10b981', '#f59e0b',
'#ef4444', '#6366f1', '#14b8a6', '#ec4899',
'#f97316', '#06b6d4',
];
export function colorFor(key: string): string {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = key.charCodeAt(i) + ((hash << 5) - hash);
}
return RIDER_COLORS[Math.abs(hash) % RIDER_COLORS.length];
}
// ── Delivery Status Checkers ──────────────────────────────────────────────────
export function isActiveDelivery(order: Record<string, unknown>): boolean {
const status = String(order?.orderstatus || '').toLowerCase();
return !FINAL_STATUSES.has(status) && !SKIPPED_STATUSES.has(status);
}
export function getActiveOrder(orders: Record<string, unknown>[]): Record<string, unknown> | null {
if (!Array.isArray(orders) || !orders.length) return null;
const sorted = [...orders].sort((a, b) => {
const tA = Number(a.trip_number) || 1;
const tB = Number(b.trip_number) || 1;
if (tA !== tB) return tA - tB;
return (Number(a.step) || 0) - (Number(b.step) || 0);
});
return sorted.find(isActiveDelivery) || null;
}
// ── Time Batch Helpers ────────────────────────────────────────────────────────
export interface TimeBatch {
id: string;
name?: string;
label: string;
range: string;
startHour: number;
endHour: number;
}
const BATCHES_DEFAULT_RAW = [
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 },
];
export function formatHourLabel(h: number): string {
const wholeHour = Math.floor(h);
const minutes = Math.round((h - wholeHour) * 60);
const hr = ((wholeHour + 11) % 12) + 1;
const ampm = wholeHour >= 12 && wholeHour < 24 ? 'PM' : 'AM';
if (minutes === 0) return `${hr} ${ampm}`;
const mm = String(minutes).padStart(2, '0');
return `${hr}:${mm} ${ampm}`;
}
export function formatSlotLabel(idx: number, startHour: number): string {
return `Slot ${idx + 1} · ${formatHourLabel(startHour)}`;
}
export function formatSlotRange(startHour: number, endHour: number): string {
if (endHour >= 24) return `After ${formatHourLabel(startHour)}`;
return `${formatHourLabel(startHour)}${formatHourLabel(endHour)}`;
}
export function getDefaultBatches(): TimeBatch[] {
return BATCHES_DEFAULT_RAW.map((s, i) => ({
...s,
label: s.name || formatSlotLabel(i, s.startHour),
range: formatSlotRange(s.startHour, s.endHour),
}));
}
export function getBatchForHour(h: number, batches: TimeBatch[]): string | null {
for (const b of batches) {
if (h >= b.startHour && h < b.endHour) return b.id;
}
return null;
}
// ── Ordinal Numbers ───────────────────────────────────────────────────────────
export function ordinal(n: number | null | undefined): string {
if (n == null) return '';
const s = ['th', 'st', 'nd', 'rd'];
const v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
// ── Distance Calculations ─────────────────────────────────────────────────────
export function haversineKm(
a: [number, number],
b: [number, number],
): number {
const R = 6371; // Earth radius in km
const toRad = (d: number) => (d * Math.PI) / 180;
const lat1 = toRad(a[0]);
const lat2 = toRad(b[0]);
const dLat = toRad(b[0] - a[0]);
const dLon = toRad(b[1] - a[1]);
const s =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.min(1, Math.sqrt(s)));
}
export function polylineLengthKm(points: Array<[number, number]>): number {
if (!Array.isArray(points) || points.length < 2) return 0;
let total = 0;
for (let i = 1; i < points.length; i++) {
total += haversineKm(points[i - 1], points[i]);
}
return total;
}
// ── Scooter Icon SVG ──────────────────────────────────────────────────────────
export const SCOOTER_SVG_PATH =
'M19,17A2,2 0 0,1 17,19A2,2 0 0,1 15,17A2,2 0 0,1 17,15A2,2 0 0,1 19,17M7,17A2,2 0 0,1 5,19A2,2 0 0,1 3,17A2,2 0 0,1 5,15A2,2 0 0,1 7,17M21.43,11.33L19.43,6.33C19.23,5.84 18.75,5.5 18.21,5.5H15V3H11V9H15.6L16.96,12.44C15.82,12.87 15,13.97 15,15.25V16H13.68C13.23,14.82 12.1,14 10.75,14C9.4,14 8.27,14.82 7.82,16H6.18C5.73,14.82 4.6,14 3.25,14C1.9,14 0.77,14.82 0.32,16H0V18H2V17C2,15.9 2.9,15 4,15C5.1,15 6,15.9 6,17H8C8,15.9 8.9,15 10,15C11.1,15 12,15.9 12,17H14C14,15.9 14.9,15 16,15C16.59,15 17.11,15.26 17.47,15.68L18.66,12.7L21.84,13.33L21.43,11.33Z';
// ── Time Window Helpers ───────────────────────────────────────────────────────
export function extractTimeOnly(raw: unknown): string {
const m = String(raw || '').match(/(\d{1,2}):(\d{2})/);
return m ? `${m[1]}:${m[2]}` : '';
}
export interface Row {
[key: string]: unknown;
}

View File

@@ -914,3 +914,89 @@ export async function createTenantLocation(input: CreateTenantLocationInput): Pr
return fiestaSend<Row>('tenants/createtenantlocation', 'POST', input);
}
// ════════════════════════════════════════════════════════════════════════════
// RIDERS / DISPATCH
// ════════════════════════════════════════════════════════════════════════════
/** /riders/getriderperiodiclogs?userid=&riderid=&fromdate=&todate=&tenantid=&applocationid= —
* periodic GPS/status snapshots for a rider across a date range. */
export async function getRiderPeriodicLogs(opts: {
userid?: number;
riderid?: number;
fromdate: string;
todate: string;
tenantid?: number;
applocationid?: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('riders/getriderperiodiclogs', {
userid: opts.userid,
riderid: opts.riderid,
fromdate: opts.fromdate,
todate: opts.todate,
tenantid: opts.tenantid,
applocationid: opts.applocationid,
}),
);
}
/** /riders/getriderlogs?userid=&riderid=&fromdate=&todate=&tenantid=&applocationid= —
* full telemetry logs (GPS traces, events, etc.) for a rider. */
export async function getRiderLogs(opts: {
userid?: number;
riderid?: number;
fromdate: string;
todate: string;
tenantid?: number;
applocationid?: number;
pageno?: number;
pagesize?: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('riders/getriderlogs', {
userid: opts.userid,
riderid: opts.riderid,
fromdate: opts.fromdate,
todate: opts.todate,
tenantid: opts.tenantid,
applocationid: opts.applocationid,
pageno: opts.pageno ?? 1,
pagesize: opts.pagesize ?? 200,
}),
);
}
/** /partners/getbatchefficiency?partnerid=&tenantid=&fromdate=&todate= —
* batch/trip efficiency metrics. */
export async function getBatchEfficiency(opts: {
partnerid?: number;
tenantid: number;
fromdate: string;
todate: string;
}): Promise<Row[]> {
return toRows(
await fiestaGet('partners/getbatchefficiency', {
partnerid: opts.partnerid,
tenantid: opts.tenantid,
fromdate: opts.fromdate,
todate: opts.todate,
}),
);
}
/** PUT /deliveries/updatedelivery — Manually assign/update a delivery's rider or status. */
export async function updateDelivery(deliveryid: number, updates: Row): Promise<Row> {
return fiestaSend<Row>('deliveries/updatedelivery', 'PUT', {
deliveryid,
...updates,
});
}
/** POST /riders/reassigndeliveries — Batch-reassign multiple deliveries to a new rider. */
export async function reassignDeliveries(opts: {
userid: number;
deliveryids: number[];
}): Promise<Row> {
return fiestaSend<Row>('riders/reassigndeliveries', 'POST', opts);
}

View File

@@ -32,6 +32,11 @@ import {
getCustomerOrders,
getRiders,
getRiderShifts,
getRiderPeriodicLogs,
getRiderLogs,
getBatchEfficiency,
updateDelivery,
reassignDeliveries,
getTenantLocations,
getAllTenants,
getTenantCustomers,
@@ -65,6 +70,9 @@ export const fiestaKeys = {
deliveryInsight: (tenantid: number) => ['fiesta', 'deliveryInsight', tenantid] as const,
riders: (params: Record<string, unknown>) => ['fiesta', 'riders', params] as const,
riderShifts: (applocationid: number) => ['fiesta', 'riderShifts', applocationid] as const,
riderPeriodicLogs: (params: Record<string, unknown>) => ['fiesta', 'riderPeriodicLogs', params] as const,
riderLogs: (params: Record<string, unknown>) => ['fiesta', 'riderLogs', params] as const,
batchEfficiency: (params: Record<string, unknown>) => ['fiesta', 'batchEfficiency', params] as const,
// v2: bumped when test-row filtering was added to getTenantLocations so any
// warm cache holding the old unfiltered (duplicated/junk) rows is bypassed.
tenantLocations: (tenantid: number) => ['fiesta', 'tenantLocations', 'v2', tenantid] as const,
@@ -256,6 +264,76 @@ export function useFiestaRiderShifts(applocationid: number = FIESTA_APPLOCATION_
});
}
// ── Dispatch / Telemetry ─────────────────────────────────────────────────────
export function useFiestaRiderPeriodicLogs(opts: {
userid?: number;
riderid?: number;
fromdate: string;
todate: string;
tenantid?: number;
applocationid?: number;
}) {
return useQuery({
queryKey: fiestaKeys.riderPeriodicLogs(opts),
queryFn: () => getRiderPeriodicLogs(opts),
enabled: Boolean(opts.fromdate && opts.todate),
});
}
export function useFiestaRiderLogs(opts: {
userid?: number;
riderid?: number;
fromdate: string;
todate: string;
tenantid?: number;
applocationid?: number;
pageno?: number;
pagesize?: number;
}) {
return useQuery({
queryKey: fiestaKeys.riderLogs(opts),
queryFn: () => getRiderLogs(opts),
enabled: Boolean(opts.fromdate && opts.todate),
});
}
export function useFiestaBatchEfficiency(opts: {
partnerid?: number;
tenantid: number;
fromdate: string;
todate: string;
}) {
return useQuery({
queryKey: fiestaKeys.batchEfficiency(opts),
queryFn: () => getBatchEfficiency(opts),
enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate),
});
}
export function useFiestaUpdateDelivery() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: { deliveryid: number; updates: Row }) =>
updateDelivery(input.deliveryid, input.updates),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] });
qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] });
},
});
}
export function useFiestaReassignDeliveries() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: { userid: number; deliveryids: number[] }) =>
reassignDeliveries(input),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] });
qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] });
},
});
}
// ── Tenants / Customers ─────────────────────────────────────────────────────────
export function useFiestaTenantLocations(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({