updates on the api integration and the design changes on the whole website
This commit is contained in:
@@ -17,8 +17,18 @@
|
||||
|
||||
.dispatch-container {
|
||||
width: calc(100% + 48px);
|
||||
height: calc(100vh - 88px);
|
||||
margin: -24px;
|
||||
/* MainLayout/index.js's .main-content-area has NO padding-top (only
|
||||
left/right/bottom at 24px) — Astryx's Layout.tsx renders the TopNav as
|
||||
a normal-flow flex child ahead of the content area, so content already
|
||||
starts right below it with no extra offset needed. This page fills the
|
||||
screen by negative-margining the 24px padding away entirely and sizing
|
||||
to the viewport height minus the TopNav's own (dynamically measured)
|
||||
height. An earlier version of this rule compensated for a padding-top
|
||||
that main-content-area briefly had and no longer does — if that
|
||||
padding-top ever comes back, this needs to account for it again (see
|
||||
git history around this comment for that version's math). */
|
||||
height: calc(100vh - var(--appshell-header-height, 64px));
|
||||
margin: 0 -24px -24px -24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg);
|
||||
@@ -10982,7 +10992,7 @@
|
||||
its stacked children instead of being clipped to a single row height. */
|
||||
.dispatch-container {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 88px);
|
||||
min-height: calc(100vh - var(--appshell-header-height, 64px));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -1314,7 +1314,16 @@ const Dispatch = ({
|
||||
|
||||
const { data: riderInfoData, isFetching: riderInfoFetching, isError: riderInfoIsError, error: riderInfoError } = useQuery({
|
||||
queryKey: ['riderPeriodicLog', riderInfoUserid],
|
||||
queryFn: () => getRiderPeriodicLogs(riderInfoUserid),
|
||||
// riderInfoUserid is actually a userid (it's sourced from ridersAllDay,
|
||||
// which comes from booking data — assignedmileruserid, confirmed live)
|
||||
// but /admin/milers/:id/* routes need milerprofileid. liveRiderLocations
|
||||
// (declared below) carries both, so resolve the real id here rather than
|
||||
// changing riderInfoUserid itself, which is compared against elsewhere
|
||||
// in this file for sidebar row highlighting in userid-space.
|
||||
queryFn: () => {
|
||||
const match = liveRiderLocations.find((r) => String(r.userid) === String(riderInfoUserid));
|
||||
return getRiderPeriodicLogs(match?.milerprofileid ?? riderInfoUserid);
|
||||
},
|
||||
enabled: viewMode === 'rider-info' && riderInfoUserid != null,
|
||||
// Auto-refresh the rider snapshot every 15s while the view is open and a
|
||||
// rider is selected. Don't poll while the tab is hidden so we don't burn
|
||||
@@ -1544,8 +1553,14 @@ const Dispatch = ({
|
||||
const lon = parseFloat(r?.longitude);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
||||
return {
|
||||
// id stays userid-keyed — it's what joins this live-GPS feed against
|
||||
// `riders`/orders elsewhere in this file, which are themselves keyed
|
||||
// by a booking's assignedmileruserid (also a userid, confirmed live).
|
||||
// milerprofileid is kept separately for the one place that actually
|
||||
// needs it: /admin/milers/:id/* calls (the rider-info panel).
|
||||
id: String(r.userid ?? ''),
|
||||
userid: r.userid,
|
||||
milerprofileid: r.milerprofileid,
|
||||
username: r.username || `Rider #${r.userid}`,
|
||||
status: String(r.status || '').toLowerCase(),
|
||||
contactno: r.contactno,
|
||||
@@ -1558,12 +1573,15 @@ const Dispatch = ({
|
||||
.filter(Boolean);
|
||||
}, [ridersLocationLogs]);
|
||||
|
||||
// Set of rider ids whose latest GPS log row is `active` (i.e. on the road
|
||||
// Set of rider ids whose latest position is `active` (i.e. on the road
|
||||
// right now). The "All Active Routes" view (viewMode === 'all') uses this to
|
||||
// show ONLY currently-active riders — their cards, routes, drop markers and
|
||||
// live bike markers — and hide everyone who is offline/idle for the slot.
|
||||
// GET /admin/milers/summary's availabilitystatus enum is confirmed live as
|
||||
// Available/Assigned/On_Pickup/Offline — not active/pending, which never
|
||||
// matched anything and left this set permanently empty.
|
||||
const activeRiderIdSet = useMemo(
|
||||
() => new Set(liveRiderLocations.filter((r) => r.status === 'active' || r.status === 'pending').map((r) => String(r.id))),
|
||||
() => new Set(liveRiderLocations.filter((r) => r.status !== 'offline' && r.status !== 'blocked').map((r) => String(r.id))),
|
||||
[liveRiderLocations]
|
||||
);
|
||||
// Default to the slot containing the current wall-clock time. Use a
|
||||
@@ -1707,6 +1725,12 @@ const Dispatch = ({
|
||||
const riderDailyData = {};
|
||||
|
||||
liveRows.forEach(r => {
|
||||
// Cancelled/skipped orders never delivered, so they shouldn't earn
|
||||
// revenue or be charged fuel/slot cost either — matches the same fix
|
||||
// already applied to ProfitabilitySection.js's calcRiderMetrics.
|
||||
const status = String(r.orderstatus ?? r.status ?? '').toLowerCase();
|
||||
if (SKIPPED_STATUSES.has(status)) return;
|
||||
|
||||
// Filter by selectedDate to align with daily slot aggregations
|
||||
const dateStr = r.assigntime
|
||||
? dayjs(r.assigntime).format('YYYY-MM-DD')
|
||||
@@ -2053,7 +2077,7 @@ const Dispatch = ({
|
||||
() =>
|
||||
isAllActiveView
|
||||
? liveRiderLocations
|
||||
.filter((r) => (r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id)))
|
||||
.filter((r) => r.status !== 'offline' && r.status !== 'blocked' && activeOrderRiderIdSet.has(String(r.id)))
|
||||
.map((r) => [r.lat, r.lon])
|
||||
: [],
|
||||
[isAllActiveView, liveRiderLocations, activeOrderRiderIdSet]
|
||||
@@ -5129,7 +5153,8 @@ const Dispatch = ({
|
||||
|
||||
{/* Live rider GPS markers from GET /admin/milers/summary (see
|
||||
fetchRidersLogs in api.js). Green pin when the rider's status
|
||||
is `active`, red otherwise, with the rider's username as a
|
||||
isn't `offline` (Available/Assigned/On_Pickup — confirmed
|
||||
live enum), red otherwise, with the rider's username as a
|
||||
label. Scoped to riders who actually have orders in the
|
||||
currently selected slot — `riders` is derived from
|
||||
filteredLiveRows so it already reflects the slot filter. A
|
||||
@@ -5139,12 +5164,12 @@ const Dispatch = ({
|
||||
{liveRiderLocations
|
||||
.filter((r) =>
|
||||
isAllActiveView
|
||||
? ((r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id)))
|
||||
? (r.status !== 'offline' && r.status !== 'blocked' && activeOrderRiderIdSet.has(String(r.id)))
|
||||
: riders.some((rd) => String(rd.id) === String(r.id))
|
||||
)
|
||||
.filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))
|
||||
.map((r) => {
|
||||
const isActive = r.status === 'active' || r.status === 'pending';
|
||||
const isActive = r.status !== 'offline' && r.status !== 'blocked';
|
||||
const pinColor = isActive ? '#16a34a' : '#dc2626';
|
||||
// Look up the rider's in-progress order so the popup can show
|
||||
// where they're heading next (drop customer/area + originating
|
||||
|
||||
@@ -328,7 +328,6 @@ const Preview = () => {
|
||||
const deliveryData = stateData.deliveryData || [];
|
||||
const autoRiders = stateData.autoRiders || [];
|
||||
const absentRidersPayload = stateData.absentRidersPayload || [];
|
||||
const rider = stateData.rider || null;
|
||||
|
||||
const appId = useMemo(() => {
|
||||
if (stateData.appId) return stateData.appId;
|
||||
@@ -409,11 +408,34 @@ const Preview = () => {
|
||||
});
|
||||
|
||||
const createFinalDeliveryMutation = useMutation({
|
||||
// finalCreatedeliveries now assigns each order directly on Doormile's
|
||||
// own booking record (POST /admin/bookings/:id/assign-miler per order,
|
||||
// resolved internally) instead of posting to jupiter — see api.js for
|
||||
// why: jupiter's createdeliveries wrote into jupiter's own orphaned
|
||||
// database, which neither the Orders "pending" list nor the Deliveries
|
||||
// "dispatched" filter ever read (both come from GET /admin/bookings).
|
||||
mutationFn: finalCreatedeliveries,
|
||||
onSuccess: () => {
|
||||
OpenToast('Delivery Created Successfully', 'success', 2000);
|
||||
setIsLoading(false);
|
||||
if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
|
||||
// stateData.rider (a single rider forwarded via navigate() from the
|
||||
// Orders page) is never actually populated in the real flow — that
|
||||
// page's navigate() call doesn't include a `rider` key at all — so
|
||||
// this was a permanent no-op and no rider ever got notified after
|
||||
// assignment. Notify every rider actually present in the committed
|
||||
// list instead. notifyRider expects a milerprofileid (not an FCM
|
||||
// token — the server looks the device up itself); rider_id/userid
|
||||
// here is the id this page already treats as canonical throughout
|
||||
// (see flattenRiders/moveOrderInPreviewData above) since it's the
|
||||
// only rider identifier the solver echoes back — unconfirmed whether
|
||||
// that's actually a milerprofileid by the time it reaches here.
|
||||
const notifiedRiderIds = new Set();
|
||||
finaldeliveryList.forEach((order) => {
|
||||
const riderId = order.rider_id ?? order.userid;
|
||||
if (riderId == null || notifiedRiderIds.has(String(riderId))) return;
|
||||
notifiedRiderIds.add(String(riderId));
|
||||
notifyRiderMutation.mutate(riderId);
|
||||
});
|
||||
navigate('/doormile/deliveries');
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -479,6 +501,18 @@ const Preview = () => {
|
||||
OpenToast('No deliveries to assign', 'error', 3000);
|
||||
return;
|
||||
}
|
||||
// "Change Rider" and "Reconcile" were two independent buttons with
|
||||
// nothing linking them — an operator could edit a rider's steps and hit
|
||||
// Assign Orders without ever pressing Reconcile. Committing stale step
|
||||
// ordering corrupts route sequences server-side (see this folder's
|
||||
// CLAUDE.md §3, called out as the single biggest production risk here).
|
||||
// dirtyRiderIds only ever holds riders edited since the last successful
|
||||
// reconcile of that specific rider, so size > 0 means real unreconciled
|
||||
// edits are pending.
|
||||
if (dirtyRiderIds.size > 0) {
|
||||
OpenToast(`Reconcile ${dirtyRiderIds.size} edited rider(s) before assigning`, 'warning', 4000);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList });
|
||||
};
|
||||
@@ -789,9 +823,18 @@ const Preview = () => {
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="contained" fullWidth={isMobile} onClick={handleFinalCreateDelivery}>
|
||||
Assign Orders
|
||||
</Button>
|
||||
<Tooltip title={dirtyRiderIds.size > 0 ? `Reconcile ${dirtyRiderIds.size} edited rider(s) first` : ''}>
|
||||
<span style={isMobile ? { width: '100%' } : undefined}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth={isMobile}
|
||||
disabled={dirtyRiderIds.size > 0}
|
||||
onClick={handleFinalCreateDelivery}
|
||||
>
|
||||
Assign Orders
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -18,16 +18,20 @@ import './ProfitabilitySection.css';
|
||||
// Constants
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Exported so reports/profitability.js can reuse the exact same profit math
|
||||
// instead of hand-duplicating it (a duplication that had already drifted
|
||||
// into a sync-risk between the two files).
|
||||
|
||||
/** Revenue rule: ₹30 base for ≤8 km, ₹6/km beyond. */
|
||||
const BASE_REVENUE = 30;
|
||||
const BASE_KM_LIMIT = 8;
|
||||
const EXTRA_RATE_KM = 6;
|
||||
export const BASE_REVENUE = 30;
|
||||
export const BASE_KM_LIMIT = 8;
|
||||
export const EXTRA_RATE_KM = 6;
|
||||
|
||||
/** Fixed salary cost sliced per slot (₹5000 / 30 days / 1 slot). */
|
||||
const FIXED_COST_PER_SLOT = 500 / 3;
|
||||
export const FIXED_COST_PER_SLOT = 500 / 3;
|
||||
|
||||
/** Variable fuel / wear cost per km. */
|
||||
const VARIABLE_RATE_KM = 2.5;
|
||||
export const VARIABLE_RATE_KM = 2.5;
|
||||
|
||||
/** Status display config keyed by normalised status string. */
|
||||
const STATUS_MAP = {
|
||||
@@ -54,7 +58,7 @@ function orderRevenue(order) {
|
||||
return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM;
|
||||
}
|
||||
|
||||
const BATCHES_DEFAULT = [
|
||||
export const BATCHES_DEFAULT = [
|
||||
{ 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 }
|
||||
@@ -77,7 +81,16 @@ const getRowBatch = (r, batches = BATCHES_DEFAULT) => {
|
||||
return getBatchForHour(d.hour() + d.minute() / 60, batches);
|
||||
};
|
||||
|
||||
// Orders in these statuses never delivered, so they earn no revenue and
|
||||
// shouldn't be charged fuel/slot cost either — including them here made
|
||||
// every rider's profit/margin KPI count cancelled and skipped stops as if
|
||||
// they'd been completed.
|
||||
const EXCLUDED_FROM_PROFIT_STATUSES = new Set(['cancelled', 'skipped']);
|
||||
|
||||
function calcRiderMetrics(rider, selectedDate, batches) {
|
||||
// `orders` stays the full date-filtered set — the breakdown table and
|
||||
// order-count KPI intentionally still show cancelled/skipped stops for
|
||||
// context. Only the money/km math below excludes them.
|
||||
const orders = (rider.orders ?? []).filter((o) => {
|
||||
if (!selectedDate) return true;
|
||||
const dateStr = o.assigntime
|
||||
@@ -87,11 +100,15 @@ function calcRiderMetrics(rider, selectedDate, batches) {
|
||||
: 'unknown';
|
||||
return dateStr === selectedDate;
|
||||
});
|
||||
const billableOrders = orders.filter((o) => {
|
||||
const status = String(o.orderstatus ?? o.status ?? '').toLowerCase();
|
||||
return !EXCLUDED_FROM_PROFIT_STATUSES.has(status);
|
||||
});
|
||||
let revenue = 0;
|
||||
let kms = 0;
|
||||
const slotsByDate = {};
|
||||
|
||||
for (const o of orders) {
|
||||
for (const o of billableOrders) {
|
||||
revenue += orderRevenue(o);
|
||||
kms += parseFloat(o.riderkms || 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user