updates on the ui changes and changed into doormile
This commit is contained in:
245
src/pages/doormile/dispatch/ActiveSection.jsx
Normal file
245
src/pages/doormile/dispatch/ActiveSection.jsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
MdTwoWheeler,
|
||||
MdLocationOn,
|
||||
MdRestaurant,
|
||||
MdMyLocation,
|
||||
MdAccessTime,
|
||||
MdInventory2
|
||||
} from 'react-icons/md';
|
||||
|
||||
import { List, ListItem } from '@astryxdesign/core/List';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { Center } from '@astryxdesign/core/Center';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
||||
|
||||
import { parseDoormileTimestamp } from 'utils/doormileTimestamp';
|
||||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
||||
import StatusBadge from 'components/nearle_components/StatusBadge';
|
||||
import { AccentAvatar } from 'themes/dt/primitives';
|
||||
import { getActiveOrder } from './dispatchShared';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
|
||||
// ==============================|| ACTIVE DELIVERIES (sidebar) ||============================== //
|
||||
// Rows, not cards. Astryx's design rule is that dense operator data reads as
|
||||
// edge-to-edge rows — the old `.adcard` bordered cards cost ~16px of chrome per
|
||||
// delivery, which is a lot of scrolling once a slot has 20+ riders out.
|
||||
//
|
||||
// Status comes from <StatusBadge> (themes/dt/status.js), which is now the only
|
||||
// status-colour table in this folder — dispatchShared's rival copy has been
|
||||
// deleted and its getStatusStyle reduced to an adapter over the same source.
|
||||
|
||||
// Small labelled metric — icon + value, used for the distance/ETA pair.
|
||||
// The old markup carried a native `title` for the label ("Distance to drop");
|
||||
// Astryx's BaseProps deliberately omits `title`, so the affordance moves to the
|
||||
// real <Tooltip> rather than silently disappearing onto a dropped attribute.
|
||||
const Metric = ({ icon, children, label }) => (
|
||||
<Tooltip content={label}>
|
||||
<HStack gap={0.5} vAlign="center">
|
||||
<Center isInline width={14} height={14} style={{ color: 'var(--color-icon-secondary)' }}>
|
||||
{icon}
|
||||
</Center>
|
||||
<Text size="sm" type="supporting" hasTabularNumbers>
|
||||
{children}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
Metric.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
label: PropTypes.string
|
||||
};
|
||||
|
||||
// One line of secondary context (rider / drop area / pickup) under the customer.
|
||||
// `maxLines={1}` turns on Text's own truncate tooltip, which replaces the
|
||||
// `title` the old spans used to show the full value when clipped.
|
||||
const ContextLine = ({ icon, children }) => (
|
||||
<HStack gap={1} vAlign="center">
|
||||
<Center isInline width={13} height={13} style={{ flexShrink: 0, color: 'var(--color-icon-secondary)' }}>
|
||||
{icon}
|
||||
</Center>
|
||||
<Text size="sm" type="supporting" maxLines={1}>
|
||||
{children}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
ContextLine.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
const ActiveSection = ({
|
||||
visibleRiders,
|
||||
riders,
|
||||
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);
|
||||
|
||||
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 prev = prevStatusMapRef.current;
|
||||
if (prev !== null) {
|
||||
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;
|
||||
}, [visibleRiders]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<VStack gap={2} padding={4} hAlign="center" vAlign="center" minHeight={200}>
|
||||
<LoaderWithImage size={44} imgSize={24} />
|
||||
<Text type="supporting">Loading active deliveries…</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeDeliveries.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="No active deliveries"
|
||||
description="No deliveries are currently in progress for this slot."
|
||||
icon={<MdInventory2 size={28} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderActiveDelivery = (o) => {
|
||||
const rid = o.rider_id || o.userid;
|
||||
const rider = riders.find((r) => String(r.id) === String(rid));
|
||||
const color = getRiderColor(rid);
|
||||
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 isSelected = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid);
|
||||
const initials =
|
||||
riderName
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0])
|
||||
.join('')
|
||||
.toUpperCase() || '•';
|
||||
|
||||
const deliveredAt = o.deliverytime
|
||||
? parseDoormileTimestamp(o.deliverytime).isValid()
|
||||
? parseDoormileTimestamp(o.deliverytime).format('HH:mm:ss')
|
||||
: String(o.deliverytime)
|
||||
: null;
|
||||
|
||||
// 20 km/h is the same city-average assumption the old card used.
|
||||
const etaMin = estMeters !== null ? (estMeters / 1000 / 20) * 60 : null;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={o.orderid}
|
||||
label={customer}
|
||||
isSelected={!!isSelected}
|
||||
onClick={() => {
|
||||
if (rider) handleRiderFocus(rider);
|
||||
if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
|
||||
}}
|
||||
startContent={
|
||||
// Rider colour is an arbitrary per-rider hex assigned by the map's
|
||||
// palette, which no semantic token can express — this is the same
|
||||
// caller-supplied-accent escape hatch StatCard uses.
|
||||
<AccentAvatar color={color} selected size={32}>
|
||||
<Text size="sm" weight="bold" style={{ color: 'inherit' }}>
|
||||
{initials}
|
||||
</Text>
|
||||
</AccentAvatar>
|
||||
}
|
||||
description={
|
||||
<VStack gap={0.5} padding={0}>
|
||||
<ContextLine icon={<MdTwoWheeler size={13} />}>{riderName}</ContextLine>
|
||||
{dropArea && <ContextLine icon={<MdLocationOn size={13} />}>{dropArea}</ContextLine>}
|
||||
{o.pickupcustomer && <ContextLine icon={<MdRestaurant size={13} />}>{o.pickupcustomer}</ContextLine>}
|
||||
</VStack>
|
||||
}
|
||||
endContent={
|
||||
<VStack gap={1} padding={0} hAlign="end">
|
||||
<StatusBadge status={o.orderstatus} />
|
||||
{deliveredAt && (
|
||||
<Text size="sm" type="supporting" hasTabularNumbers>
|
||||
{deliveredAt}
|
||||
</Text>
|
||||
)}
|
||||
{estMeters !== null && (
|
||||
<HStack gap={2} vAlign="center">
|
||||
<Metric icon={<MdMyLocation size={14} />} label="Distance to drop">
|
||||
{formatMeters(estMeters)}
|
||||
</Metric>
|
||||
<Metric icon={<MdAccessTime size={14} />} label="Estimated time to drop">
|
||||
{etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`}
|
||||
</Metric>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<List density="balanced" hasDividers>
|
||||
{activeDeliveries.map(renderActiveDelivery)}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveSection;
|
||||
Reference in New Issue
Block a user