209 lines
7.7 KiB
JavaScript
209 lines
7.7 KiB
JavaScript
import React, { useRef, useEffect } from 'react';
|
|
import {
|
|
MdTwoWheeler,
|
|
MdLocationOn,
|
|
MdRestaurant,
|
|
MdMyLocation,
|
|
MdAccessTime,
|
|
MdInventory2
|
|
} from 'react-icons/md';
|
|
import dayjs from 'dayjs';
|
|
import { Spinner } from '@astryxdesign/core/Spinner';
|
|
import { getStatusStyle, getActiveOrder } from './dispatchShared';
|
|
import { OpenToast } from 'components/nearle_components/OpenToast';
|
|
|
|
const ActiveSection = ({
|
|
visibleRiders,
|
|
riders,
|
|
resetKey,
|
|
focusedStop,
|
|
handleRiderFocus,
|
|
setFocusedStop,
|
|
calculateEstMeters,
|
|
getRiderColor,
|
|
formatMeters,
|
|
isLoading
|
|
}) => {
|
|
// Sort by live distance ascending (closest drop-off first; null/unknown last)
|
|
const activeDeliveries = visibleRiders
|
|
.map((r) => getActiveOrder(r.orders))
|
|
.filter(Boolean)
|
|
.sort((a, b) => {
|
|
const mA = calculateEstMeters(a.rider_id || a.userid, a) ?? Infinity;
|
|
const mB = calculateEstMeters(b.rider_id || b.userid, b) ?? Infinity;
|
|
return mA - mB;
|
|
});
|
|
|
|
// Toast notifications: detect status transitions across renders
|
|
const prevStatusMapRef = useRef(null);
|
|
// Tracks which batch/date/location the baseline above belongs to. When it
|
|
// changes (e.g. operator switches Morning Batch → Afternoon Batch) the
|
|
// whole order set is replaced wholesale — that is a view switch, not a
|
|
// stream of real status transitions, so we must reseed the baseline
|
|
// silently instead of diffing against the old batch's orders (which would
|
|
// otherwise treat every order in the new batch as "brand-new and already
|
|
// active" and re-fire the delivering/reached toasts for all of them).
|
|
const prevResetKeyRef = useRef(resetKey);
|
|
|
|
useEffect(() => {
|
|
const currentMap = {};
|
|
visibleRiders.forEach((r) => {
|
|
(r.orders || []).forEach((o) => {
|
|
currentMap[String(o.orderid)] = {
|
|
status: String(o.orderstatus || '').toLowerCase(),
|
|
riderName: o.rider_name || o.ridername || 'Rider',
|
|
customer: o.deliverycustomer || o.customername || `Order #${o.orderid}`
|
|
};
|
|
});
|
|
});
|
|
|
|
const viewSwitched = prevResetKeyRef.current !== resetKey;
|
|
const prev = prevStatusMapRef.current;
|
|
if (prev !== null && !viewSwitched) {
|
|
Object.entries(currentMap).forEach(([oid, cur]) => {
|
|
const old = prev[oid];
|
|
if (!old) {
|
|
// Brand-new order that is already active → rider has taken a new order
|
|
if (cur.status === 'active') {
|
|
OpenToast(`🛵 ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000);
|
|
}
|
|
return;
|
|
}
|
|
// Rider transitioned into active → started heading to customer
|
|
if (old.status !== 'active' && cur.status === 'active') {
|
|
OpenToast(`🛵 ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000);
|
|
}
|
|
// Rider delivered → reached customer location
|
|
if (old.status === 'active' && cur.status === 'delivered') {
|
|
OpenToast(`📍 ${cur.riderName} has reached ${cur.customer}'s location`, 'info', 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
prevStatusMapRef.current = currentMap;
|
|
prevResetKeyRef.current = resetKey;
|
|
}, [visibleRiders, resetKey]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="empty-slot" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: '200px' }}>
|
|
<Spinner size="xl" style={{ marginBottom: '16px' }} />
|
|
<div className="empty-slot-title">Loading active deliveries...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activeDeliveries.length === 0) {
|
|
return (
|
|
<div className="empty-slot">
|
|
<div className="empty-slot-icon">
|
|
<MdInventory2 />
|
|
</div>
|
|
<div className="empty-slot-title">No active deliveries</div>
|
|
<div className="empty-slot-sub">
|
|
No deliveries are currently in progress for this slot
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const renderActiveDeliveryCard = (o) => {
|
|
const rid = o.rider_id || o.userid;
|
|
const rider = riders.find((r) => String(r.id) === String(rid));
|
|
const color = getRiderColor(rid);
|
|
const statusStyle = getStatusStyle(o.orderstatus);
|
|
const lat = parseFloat(o.droplat || o.deliverylat);
|
|
const lon = parseFloat(o.droplon || o.deliverylong);
|
|
const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
|
|
const estMeters = calculateEstMeters(rid, o);
|
|
const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`;
|
|
const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || '';
|
|
const riderName = o.rider_name || o.ridername || 'Unassigned';
|
|
const isActive = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid);
|
|
const initials =
|
|
riderName
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((w) => w[0])
|
|
.join('')
|
|
.toUpperCase() || '•';
|
|
|
|
return (
|
|
<div
|
|
key={o.orderid}
|
|
className={`adcard${isActive ? ' is-active' : ''}`}
|
|
style={{ '--ad-accent': color }}
|
|
onClick={() => {
|
|
if (rider) handleRiderFocus(rider);
|
|
if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
|
|
}}
|
|
>
|
|
<div className="adcard-top">
|
|
<div className="adcard-avatar" style={{ background: color }}>{initials}</div>
|
|
<div className="adcard-titles">
|
|
<div className="adcard-rider" title={riderName}>
|
|
<MdTwoWheeler style={{ fontSize: 13, flexShrink: 0 }} />
|
|
<span className="adcard-tx">{riderName}</span>
|
|
</div>
|
|
<div className="adcard-customer" title={customer}>{customer}</div>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
|
|
<span
|
|
className="adcard-status"
|
|
style={{ background: `${statusStyle.bg}1a`, color: statusStyle.bg }}
|
|
title={statusStyle.label}
|
|
>
|
|
{statusStyle.label}
|
|
</span>
|
|
{o.deliverytime && (
|
|
<span className="adcard-time" style={{ fontSize: '11px', color: '#64748b' }}>
|
|
{dayjs(o.deliverytime).isValid() ? dayjs(o.deliverytime).format('HH:mm:ss') : String(o.deliverytime)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{dropArea && (
|
|
<div className="adcard-addr">
|
|
<span className="adcard-ic"><MdLocationOn /></span>
|
|
<span className="adcard-tx adcard-addr-tx" title={dropArea}>{dropArea}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="adcard-foot">
|
|
{o.pickupcustomer ? (
|
|
<span className="adcard-pickup" title={o.pickupcustomer}>
|
|
<span className="adcard-ic"><MdRestaurant /></span>
|
|
<span className="adcard-tx">{o.pickupcustomer}</span>
|
|
</span>
|
|
) : (
|
|
<span />
|
|
)}
|
|
<span className="adcard-metrics">
|
|
{estMeters !== null && (
|
|
<>
|
|
<span className="adcard-m adcard-m-eta" title="Distance to drop">
|
|
<span className="adcard-ic"><MdMyLocation /></span>
|
|
{formatMeters(estMeters)}
|
|
</span>
|
|
<span className="adcard-m adcard-m-time" title="Estimated time to drop">
|
|
<span className="adcard-ic"><MdAccessTime /></span>
|
|
{(() => {
|
|
const etaMin = estMeters / 1000 / 20 * 60;
|
|
return etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`;
|
|
})()}
|
|
</span>
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return <div className="adcard-list">{activeDeliveries.map(renderActiveDeliveryCard)}</div>;
|
|
};
|
|
|
|
export default ActiveSection;
|