updates on the api integration and the design changes on the whole website

This commit is contained in:
2026-08-11 11:18:40 +05:30
parent d144ff71a9
commit bf28249528
48 changed files with 2888 additions and 6361 deletions

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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);