api integration on the dispatch page
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -63,7 +63,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.7",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.7",
|
"@babel/generator": "^7.29.7",
|
||||||
@@ -1841,7 +1840,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
@@ -2689,8 +2687,7 @@
|
|||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
@@ -3227,7 +3224,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -3254,7 +3250,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3361,7 +3356,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3371,7 +3365,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -4268,7 +4261,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|||||||
@@ -10921,3 +10921,143 @@
|
|||||||
background: rgba(88, 28, 135, 0.14);
|
background: rgba(88, 28, 135, 0.14);
|
||||||
border-color: rgba(88, 28, 135, 0.45);
|
border-color: rgba(88, 28, 135, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────────────────────────────
|
||||||
|
RIDER TELEMETRY PANEL
|
||||||
|
Real-time GPS, signal, battery, and performance metrics
|
||||||
|
───────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.rider-telemetry-panel {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(88, 28, 135, 0.04);
|
||||||
|
border: 1px solid rgba(88, 28, 135, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-header {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #581c87;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-rider-name {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-loading,
|
||||||
|
.rider-telemetry-panel .rtm-empty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border: 1px solid rgba(88, 28, 135, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #581c87;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-label {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
word-break: break-all;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-link {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-bars {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 1px;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-bars .bar {
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 1px;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-bars .bar.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-dbm {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-battery-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-telemetry-panel .rtm-battery-fill {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s, background 0.3s;
|
||||||
|
}
|
||||||
@@ -4,19 +4,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch page — a faithful port of the operations console's dispatch cockpit
|
* Dispatch cockpit — integrates live deliveries, rider assignments, and route
|
||||||
* (nearle_console/dispatch). It reuses that page's actual stylesheet verbatim
|
* visualization. Reuses the dispatch page stylesheet from the operations console
|
||||||
* (`./DispatchView.css`, copied from Dispatch.css) and reproduces the same DOM /
|
* and reproduces its DOM structure: header, view-mode tabs, sidebar (KPI + group
|
||||||
* class structure: the `#hdr` bar, `#strat-row` view tabs, the 400px `#sidebar`
|
* cards), and map centrepiece with Leaflet.
|
||||||
* (RIDER DISPATCH header + KPI tiles + rider/zone cards + per-trip order cards),
|
|
||||||
* and the `#map-wrap` centrepiece.
|
|
||||||
*
|
*
|
||||||
* The source map is a Leaflet canvas of planned-vs-actual rider routes (OSRM
|
* Features:
|
||||||
* road-snapping, Kalman-smoothed GPS) plus AI rider-assignment posting to
|
* • Group deliveries by rider, zone, location, or status
|
||||||
* external optimisation services. Those need a mapping stack + dispatch backends
|
* • Focus on a specific group to see its trip blocks and detailed order cards
|
||||||
* this tenant doesn't expose, so the `#map-wrap` plots the real planned stop
|
* • Map-based route visualization with planned stops (actual GPS awaiting backend)
|
||||||
* order and marks the live-GPS / compare / AI-assign layers as awaiting backend —
|
* • Real-time KPI cards (orders, riders, completion %)
|
||||||
* no fabricated telemetry. Everything else is driven by the live Fiesta feed.
|
* • Date navigation for historical dispatch view
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
@@ -41,12 +39,23 @@ import {
|
|||||||
List,
|
List,
|
||||||
Play,
|
Play,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useFiestaDeliveries, useFiestaRiders } from '../services/fiestaQueries';
|
import {
|
||||||
|
useFiestaDeliveries,
|
||||||
|
useFiestaRiders,
|
||||||
|
useFiestaRiderPeriodicLogs,
|
||||||
|
} from '../services/fiestaQueries';
|
||||||
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
|
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
|
||||||
|
import {
|
||||||
|
colorFor,
|
||||||
|
getStatusStyle,
|
||||||
|
STATUS_STYLES,
|
||||||
|
extractTimeOnly,
|
||||||
|
} from '../services/dispatchShared';
|
||||||
import DispatchMap, { type MapPoint } from './DispatchMap';
|
import DispatchMap, { type MapPoint } from './DispatchMap';
|
||||||
|
import RiderTelemetryPanel from './RiderTelemetryPanel';
|
||||||
import './DispatchView.css';
|
import './DispatchView.css';
|
||||||
|
|
||||||
// ── Status colours (match the console palette) ───────────────────────────────────
|
// Legacy direct utilities (will be migrated to dispatchShared)
|
||||||
const STATUS_HEX: Record<string, string> = {
|
const STATUS_HEX: Record<string, string> = {
|
||||||
pending: '#f59e0b',
|
pending: '#f59e0b',
|
||||||
accepted: '#6366f1',
|
accepted: '#6366f1',
|
||||||
@@ -57,19 +66,12 @@ const STATUS_HEX: Record<string, string> = {
|
|||||||
delivered: '#22c55e',
|
delivered: '#22c55e',
|
||||||
cancelled: '#ef4444',
|
cancelled: '#ef4444',
|
||||||
};
|
};
|
||||||
|
|
||||||
function statusStyle(s: string): React.CSSProperties {
|
function statusStyle(s: string): React.CSSProperties {
|
||||||
const hex = STATUS_HEX[s.toLowerCase()] || '#64748b';
|
const hex = STATUS_HEX[s.toLowerCase()] || '#64748b';
|
||||||
return { background: `${hex}1f`, color: hex };
|
return { background: `${hex}1f`, color: hex };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stable rider/zone colour.
|
|
||||||
const COLORS = ['#3b82f6', '#a855f7', '#10b981', '#f59e0b', '#ef4444', '#6366f1', '#14b8a6', '#ec4899', '#f97316', '#06b6d4'];
|
|
||||||
function colorFor(key: string): string {
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < key.length; i++) hash = key.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
return COLORS[Math.abs(hash) % COLORS.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Drop coordinates from a delivery row (several field spellings), or null. */
|
/** Drop coordinates from a delivery row (several field spellings), or null. */
|
||||||
function dropLatLon(r: Row): [number, number] | null {
|
function dropLatLon(r: Row): [number, number] | null {
|
||||||
const lat = fnum(r.droplat) || fnum(r.deliverylat) || fnum(r.deliverylatitude);
|
const lat = fnum(r.droplat) || fnum(r.deliverylat) || fnum(r.deliverylatitude);
|
||||||
@@ -120,14 +122,24 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
|
|||||||
const [date, setDate] = useState<string>(ymd(today));
|
const [date, setDate] = useState<string>(ymd(today));
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('riders');
|
const [viewMode, setViewMode] = useState<ViewMode>('riders');
|
||||||
const [focusedId, setFocusedId] = useState<string | null>(null);
|
const [focusedId, setFocusedId] = useState<string | null>(null);
|
||||||
|
const [focusedRiderId, setFocusedRiderId] = useState<number | null>(null);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [tripSort, setTripSort] = useState<'planned' | 'time'>('planned');
|
const [tripSort, setTripSort] = useState<'planned' | 'time'>('planned');
|
||||||
const [animateNonce, setAnimateNonce] = useState(0);
|
const [animateNonce, setAnimateNonce] = useState(0);
|
||||||
const [animating, setAnimating] = useState(false);
|
const [animating, setAnimating] = useState(false);
|
||||||
|
|
||||||
|
// Core dispatch data
|
||||||
const deliveriesQ = useFiestaDeliveries({ tenantid: tenantId, fromdate: date, todate: date, locationid });
|
const deliveriesQ = useFiestaDeliveries({ tenantid: tenantId, fromdate: date, todate: date, locationid });
|
||||||
const ridersQ = useFiestaRiders({ tenantid: tenantId });
|
const ridersQ = useFiestaRiders({ tenantid: tenantId });
|
||||||
|
|
||||||
|
// Rider periodic logs (GPS snapshots) for the focused rider
|
||||||
|
const riderLogsQ = useFiestaRiderPeriodicLogs({
|
||||||
|
userid: focusedRiderId ?? undefined,
|
||||||
|
fromdate: date,
|
||||||
|
todate: date,
|
||||||
|
tenantid: tenantId,
|
||||||
|
});
|
||||||
|
|
||||||
// Live deliveries only — no sample/demo fallback. When the feed is empty the
|
// Live deliveries only — no sample/demo fallback. When the feed is empty the
|
||||||
// cockpit shows a genuine empty state rather than fabricated riders/stops.
|
// cockpit shows a genuine empty state rather than fabricated riders/stops.
|
||||||
const allRows = deliveriesQ.data ?? [];
|
const allRows = deliveriesQ.data ?? [];
|
||||||
@@ -410,6 +422,8 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
|
|||||||
setTripSort={setTripSort}
|
setTripSort={setTripSort}
|
||||||
onBack={() => setFocusedId(null)}
|
onBack={() => setFocusedId(null)}
|
||||||
fmtTime={fmtTime}
|
fmtTime={fmtTime}
|
||||||
|
riderLogs={riderLogsQ.data}
|
||||||
|
riderLogsLoading={riderLogsQ.isLoading}
|
||||||
/>
|
/>
|
||||||
) : groups.length === 0 ? (
|
) : groups.length === 0 ? (
|
||||||
<div className="ph">No deliveries for this day</div>
|
<div className="ph">No deliveries for this day</div>
|
||||||
@@ -421,8 +435,19 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
|
|||||||
{groups.map((g) => (
|
{groups.map((g) => (
|
||||||
<React.Fragment key={g.id}>
|
<React.Fragment key={g.id}>
|
||||||
{viewMode === 'riders'
|
{viewMode === 'riders'
|
||||||
? <RiderCard g={g} onClick={() => setFocusedId(g.id)} />
|
? <RiderCard
|
||||||
: <ZoneCard g={g} onClick={() => setFocusedId(g.id)} />}
|
g={g}
|
||||||
|
onClick={() => {
|
||||||
|
setFocusedId(g.id);
|
||||||
|
// Extract rider ID from first order in group for GPS logs
|
||||||
|
const rid = fnum(g.orders[0]?.userid);
|
||||||
|
if (rid) setFocusedRiderId(rid);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
: <ZoneCard g={g} onClick={() => {
|
||||||
|
setFocusedId(g.id);
|
||||||
|
setFocusedRiderId(null);
|
||||||
|
}} />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -575,6 +600,8 @@ function FocusedDetail({
|
|||||||
setTripSort,
|
setTripSort,
|
||||||
onBack,
|
onBack,
|
||||||
fmtTime,
|
fmtTime,
|
||||||
|
riderLogs,
|
||||||
|
riderLogsLoading,
|
||||||
}: {
|
}: {
|
||||||
focused: Group;
|
focused: Group;
|
||||||
tripBlocks: Array<{ label: string; color: string; orders: Row[] }>;
|
tripBlocks: Array<{ label: string; color: string; orders: Row[] }>;
|
||||||
@@ -583,6 +610,8 @@ function FocusedDetail({
|
|||||||
setTripSort: (v: 'planned' | 'time') => void;
|
setTripSort: (v: 'planned' | 'time') => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
fmtTime: (raw: unknown) => string;
|
fmtTime: (raw: unknown) => string;
|
||||||
|
riderLogs?: Row[];
|
||||||
|
riderLogsLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -590,6 +619,14 @@ function FocusedDetail({
|
|||||||
<span className="sbt-icon"><ChevronLeft size={15} /></span> Back to list
|
<span className="sbt-icon"><ChevronLeft size={15} /></span> Back to list
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{riderLogs && riderLogs.length > 0 && (
|
||||||
|
<RiderTelemetryPanel
|
||||||
|
logs={riderLogs}
|
||||||
|
riderName={focused.name}
|
||||||
|
isLoading={riderLogsLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{tripBlocks.map((blk, bi) => (
|
{tripBlocks.map((blk, bi) => (
|
||||||
<div className="trip-block" key={bi}>
|
<div className="trip-block" key={bi}>
|
||||||
<div className="trip-header" style={{ background: `${blk.color}12`, borderColor: `${blk.color}40` }}>
|
<div className="trip-header" style={{ background: `${blk.color}12`, borderColor: `${blk.color}40` }}>
|
||||||
|
|||||||
189
src/components/RiderTelemetryPanel.tsx
Normal file
189
src/components/RiderTelemetryPanel.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rider telemetry panel — displays GPS location, signal strength, battery,
|
||||||
|
* and other real-time metrics for the selected rider. Integrated into the
|
||||||
|
* dispatch sidebar when a rider card is focused.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { MapPin, Zap, Signal, Clock, Gauge } from 'lucide-react';
|
||||||
|
import { type Row } from '../services/fiestaApi';
|
||||||
|
|
||||||
|
interface RiderTelemetryPanelProps {
|
||||||
|
logs: Row[];
|
||||||
|
riderName?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RiderTelemetryPanel({ logs, riderName, isLoading }: RiderTelemetryPanelProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="rider-telemetry-panel">
|
||||||
|
<div className="rtm-header">Telemetry</div>
|
||||||
|
<div className="rtm-loading">Loading GPS data…</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="rider-telemetry-panel">
|
||||||
|
<div className="rtm-header">Telemetry</div>
|
||||||
|
<div className="rtm-empty">No GPS telemetry available</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest (most recent) GPS log
|
||||||
|
const latest = logs[logs.length - 1];
|
||||||
|
const lat = latest?.latitude || latest?.lat;
|
||||||
|
const lon = latest?.longitude || latest?.lng;
|
||||||
|
const battery = latest?.batterypercentage || latest?.battery;
|
||||||
|
const signal = latest?.signalstrength || latest?.signal;
|
||||||
|
const speed = latest?.speed || 0;
|
||||||
|
const timestamp = latest?.logdate || latest?.timestamp;
|
||||||
|
|
||||||
|
const getSignalBars = (signal: number | null) => {
|
||||||
|
if (!signal) return 0;
|
||||||
|
const s = Number(signal);
|
||||||
|
if (s <= -120) return 1;
|
||||||
|
if (s <= -100) return 2;
|
||||||
|
if (s <= -80) return 3;
|
||||||
|
if (s <= -60) return 4;
|
||||||
|
return 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bars = getSignalBars(signal ? Number(signal) : null);
|
||||||
|
const batteryPercent = battery ? Number(battery) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rider-telemetry-panel">
|
||||||
|
<div className="rtm-header">
|
||||||
|
Telemetry
|
||||||
|
{riderName && <span className="rtm-rider-name">{riderName}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rtm-grid">
|
||||||
|
{/* GPS Location */}
|
||||||
|
{lat && lon && (
|
||||||
|
<div className="rtm-item rtm-gps">
|
||||||
|
<div className="rtm-icon">
|
||||||
|
<MapPin size={14} />
|
||||||
|
</div>
|
||||||
|
<div className="rtm-content">
|
||||||
|
<div className="rtm-label">Location</div>
|
||||||
|
<div className="rtm-value">{Number(lat).toFixed(4)}, {Number(lon).toFixed(4)}</div>
|
||||||
|
<a
|
||||||
|
href={`https://maps.google.com/?q=${lat},${lon}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="rtm-link"
|
||||||
|
>
|
||||||
|
View on Maps
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Signal Strength */}
|
||||||
|
{signal && (
|
||||||
|
<div className="rtm-item rtm-signal">
|
||||||
|
<div className="rtm-icon">
|
||||||
|
<Signal size={14} />
|
||||||
|
</div>
|
||||||
|
<div className="rtm-content">
|
||||||
|
<div className="rtm-label">Signal</div>
|
||||||
|
<div className="rtm-value">
|
||||||
|
<span className="rtm-bars">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={`bar ${i < bars ? 'active' : ''}`}
|
||||||
|
style={{
|
||||||
|
height: `${(i + 1) * 20}%`,
|
||||||
|
background: bars - i <= 1 ? '#ef4444' : bars - i <= 2 ? '#f59e0b' : '#22c55e',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span className="rtm-dbm">{signal} dBm</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Battery */}
|
||||||
|
{batteryPercent !== null && (
|
||||||
|
<div className="rtm-item rtm-battery">
|
||||||
|
<div className="rtm-icon">
|
||||||
|
<Zap size={14} />
|
||||||
|
</div>
|
||||||
|
<div className="rtm-content">
|
||||||
|
<div className="rtm-label">Battery</div>
|
||||||
|
<div className="rtm-value">
|
||||||
|
<div className="rtm-battery-bar">
|
||||||
|
<div
|
||||||
|
className="rtm-battery-fill"
|
||||||
|
style={{
|
||||||
|
width: `${batteryPercent}%`,
|
||||||
|
background:
|
||||||
|
batteryPercent <= 20 ? '#ef4444' :
|
||||||
|
batteryPercent <= 50 ? '#f59e0b' :
|
||||||
|
'#22c55e',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{Math.round(batteryPercent)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Speed */}
|
||||||
|
{speed && (
|
||||||
|
<div className="rtm-item rtm-speed">
|
||||||
|
<div className="rtm-icon">
|
||||||
|
<Gauge size={14} />
|
||||||
|
</div>
|
||||||
|
<div className="rtm-content">
|
||||||
|
<div className="rtm-label">Speed</div>
|
||||||
|
<div className="rtm-value">{Number(speed).toFixed(1)} km/h</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Last Update */}
|
||||||
|
{timestamp && (
|
||||||
|
<div className="rtm-item rtm-timestamp">
|
||||||
|
<div className="rtm-icon">
|
||||||
|
<Clock size={14} />
|
||||||
|
</div>
|
||||||
|
<div className="rtm-content">
|
||||||
|
<div className="rtm-label">Last Update</div>
|
||||||
|
<div className="rtm-value">{formatTimeAgo(timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeAgo(timestamp: unknown): string {
|
||||||
|
if (!timestamp) return 'N/A';
|
||||||
|
const ts = new Date(String(timestamp)).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const diffMs = now - ts;
|
||||||
|
const diffSecs = Math.floor(diffMs / 1000);
|
||||||
|
const diffMins = Math.floor(diffSecs / 60);
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
|
||||||
|
if (diffSecs < 60) return `${diffSecs}s ago`;
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
return new Date(String(timestamp)).toLocaleString();
|
||||||
|
}
|
||||||
195
src/services/dispatchShared.ts
Normal file
195
src/services/dispatchShared.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -914,3 +914,89 @@ export async function createTenantLocation(input: CreateTenantLocationInput): Pr
|
|||||||
return fiestaSend<Row>('tenants/createtenantlocation', 'POST', input);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ import {
|
|||||||
getCustomerOrders,
|
getCustomerOrders,
|
||||||
getRiders,
|
getRiders,
|
||||||
getRiderShifts,
|
getRiderShifts,
|
||||||
|
getRiderPeriodicLogs,
|
||||||
|
getRiderLogs,
|
||||||
|
getBatchEfficiency,
|
||||||
|
updateDelivery,
|
||||||
|
reassignDeliveries,
|
||||||
getTenantLocations,
|
getTenantLocations,
|
||||||
getAllTenants,
|
getAllTenants,
|
||||||
getTenantCustomers,
|
getTenantCustomers,
|
||||||
@@ -65,6 +70,9 @@ export const fiestaKeys = {
|
|||||||
deliveryInsight: (tenantid: number) => ['fiesta', 'deliveryInsight', tenantid] as const,
|
deliveryInsight: (tenantid: number) => ['fiesta', 'deliveryInsight', tenantid] as const,
|
||||||
riders: (params: Record<string, unknown>) => ['fiesta', 'riders', params] as const,
|
riders: (params: Record<string, unknown>) => ['fiesta', 'riders', params] as const,
|
||||||
riderShifts: (applocationid: number) => ['fiesta', 'riderShifts', applocationid] 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
|
// v2: bumped when test-row filtering was added to getTenantLocations so any
|
||||||
// warm cache holding the old unfiltered (duplicated/junk) rows is bypassed.
|
// warm cache holding the old unfiltered (duplicated/junk) rows is bypassed.
|
||||||
tenantLocations: (tenantid: number) => ['fiesta', 'tenantLocations', 'v2', tenantid] as const,
|
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 ─────────────────────────────────────────────────────────
|
// ── Tenants / Customers ─────────────────────────────────────────────────────────
|
||||||
export function useFiestaTenantLocations(tenantid: number = FIESTA_TENANT_ID) {
|
export function useFiestaTenantLocations(tenantid: number = FIESTA_TENANT_ID) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
Reference in New Issue
Block a user