2237 lines
94 KiB
JavaScript
2237 lines
94 KiB
JavaScript
import logger from '../../../utils/logger';
|
||
import { enqueueSnackbar } from 'notistack';
|
||
import { useState, useEffect, Fragment, useRef, useMemo } from 'react';
|
||
import dayjs from 'dayjs';
|
||
var utc = require('dayjs/plugin/utc');
|
||
dayjs.extend(utc);
|
||
import { notifyMiler, getConsignmentLogs } from 'pages/api/doormileApi';
|
||
import { kalmanSmoothGps, polylineLengthKm } from '../dispatch/dispatchShared';
|
||
import { parseDoormileTimestamp } from 'utils/doormileTimestamp';
|
||
import { BATCHES as SHARED_BATCHES, batchRangeLabel } from 'utils/batchBucket';
|
||
import {
|
||
MdOutlineDateRange,
|
||
MdDirectionsBike,
|
||
MdNotificationsActive,
|
||
MdCheckCircle,
|
||
MdCancel,
|
||
MdInventory2,
|
||
MdOutlineLocalShipping,
|
||
MdOutlinePendingActions,
|
||
MdOutlineCheckCircle,
|
||
MdOutlineCancel,
|
||
MdKeyboardArrowDown,
|
||
MdKeyboardArrowUp,
|
||
MdArrowDownward,
|
||
MdArrowUpward,
|
||
MdUnfoldMore,
|
||
MdViewColumn
|
||
} from 'react-icons/md';
|
||
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
||
// Same TanStack Table v9 entry point orders.js uses — table-core's feature
|
||
// modules come through react-table's re-export, see that file's comment.
|
||
import {
|
||
useTable,
|
||
flexRender,
|
||
tableFeatures,
|
||
rowSelectionFeature,
|
||
columnVisibilityFeature,
|
||
rowSortingFeature,
|
||
createSortedRowModel,
|
||
sortFns,
|
||
columnSizingFeature,
|
||
columnResizingFeature,
|
||
rowPaginationFeature,
|
||
createPaginatedRowModel
|
||
} from '@tanstack/react-table';
|
||
|
||
import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns';
|
||
import { DateRangePicker } from 'components/nearle_components/DateRangePicker';
|
||
|
||
import { Grid } from '@astryxdesign/core/Grid';
|
||
import { HStack } from '@astryxdesign/core/HStack';
|
||
import { VStack } from '@astryxdesign/core/VStack';
|
||
import { Center } from '@astryxdesign/core/Center';
|
||
import { Card } from '@astryxdesign/core/Card';
|
||
import { Text } from '@astryxdesign/core/Text';
|
||
import { Button } from '@astryxdesign/core/Button';
|
||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
||
import { Badge } from '@astryxdesign/core/Badge';
|
||
import { Divider } from '@astryxdesign/core/Divider';
|
||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||
import { NumberInput } from '@astryxdesign/core/NumberInput';
|
||
import { TextArea } from '@astryxdesign/core/TextArea';
|
||
import { Selector } from '@astryxdesign/core/Selector';
|
||
import { CheckboxInput } from '@astryxdesign/core/CheckboxInput';
|
||
import { Pagination } from '@astryxdesign/core/Pagination';
|
||
import { DropdownMenu, DropdownMenuCheckboxItem } from '@astryxdesign/core/DropdownMenu';
|
||
import { MoreMenu } from '@astryxdesign/core/MoreMenu';
|
||
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
||
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||
|
||
import Loader from 'components/Loader';
|
||
import {
|
||
cancelDeliveryAPI,
|
||
changeRiderAPI,
|
||
fetchCountAPI,
|
||
fetchDeliveries,
|
||
fetchRidersList,
|
||
notifyRider,
|
||
updateDeliveryAPI,
|
||
getorderdetails,
|
||
gettenantlocations,
|
||
getTenants
|
||
} from 'pages/api/api';
|
||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||
import { OpenToast } from 'components/third-party/OpenToast';
|
||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
||
import PageHeader from 'components/nearle_components/PageHeader';
|
||
import StatCard from 'components/nearle_components/StatCard';
|
||
import StatusBadge from 'components/nearle_components/StatusBadge';
|
||
import StatusTabs from 'components/nearle_components/StatusTabs';
|
||
import TableLoader from 'components/nearle_components/TableLoader';
|
||
import { PageShell, KpiGrid, FilterToolbar, DataCard } from 'components/nearle_components/PageLayout';
|
||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||
import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport';
|
||
|
||
// ============================================================================
|
||
// Design tokens — shared across every DT-styled operator page so filters,
|
||
// KPIs, tabs, status badges, and dialogs stay visually consistent.
|
||
// See src/themes/dt/tokens.js for the canonical source.
|
||
// ============================================================================
|
||
import { DT, tint } from 'themes/dt/tokens';
|
||
import { TableScroll, AccentAvatar } from 'themes/dt/primitives';
|
||
import { getStatusMeta } from 'themes/dt/status';
|
||
|
||
const BRAND = DT.brand;
|
||
|
||
// Ordered status list driving the tabs row (left → right). Each entry binds
|
||
// the canonical status registry (themes/dt/status.js) key to the
|
||
// `currentStatus` key the queries use AND to the `batchCounts` key (so the
|
||
// chip count for the tab is one lookup).
|
||
// 'other' is NOT in this list — it is appended at render time and only when
|
||
// rows actually land in it, so the strip doesn't carry a permanently-empty tab.
|
||
const KNOWN_TAB_STATUSES = ['pending', 'accepted', 'arrived', 'picked', 'active', 'skipped', 'delivered', 'cancelled', 'canceled'];
|
||
|
||
const STATUS_TABS = [
|
||
{ status: 'pending', countKey: 'uncoveredLength' },
|
||
{ status: 'accepted', countKey: 'assignedLength' },
|
||
{ status: 'arrived', countKey: 'arrivedLength' },
|
||
{ status: 'picked', countKey: 'pickedLength' },
|
||
{ status: 'active', countKey: 'activeLength' },
|
||
{ status: 'skipped', countKey: 'skippedLength' },
|
||
{ status: 'delivered', countKey: 'coveredLength' },
|
||
{ status: 'cancelled', countKey: 'cancelLength' }
|
||
];
|
||
|
||
// KPI palette + icons — mirrors the four cards across the top of the page.
|
||
const KPI_META = [
|
||
{ key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping },
|
||
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions },
|
||
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle },
|
||
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel }
|
||
];
|
||
|
||
// Batches mirror the dispatch page's slot definitions so an operator who
|
||
// segments the day there sees the same buckets here. Hours are 24h, half-open
|
||
// [startHour, endHour) — a delivery at exactly endHour falls into the *next*
|
||
// batch (or none, if the gap isn't covered).
|
||
// Windows and ranges come from utils/batchBucket.js — the same source Dispatch
|
||
// derives its own from. They were three hand-maintained copies of numbers that
|
||
// must agree; a row bucketed one way here and another on Dispatch is the same
|
||
// bug twice. Only the presentation (colour, icon) is local.
|
||
const BATCH_PRESENTATION = {
|
||
morning: { color: '#0ea5e9', iconKey: 'morning' },
|
||
afternoon: { color: '#f59e0b', iconKey: 'afternoon' },
|
||
evening: { color: '#6366f1', iconKey: 'evening' }
|
||
};
|
||
|
||
const BATCH_OPTIONS = [
|
||
{ id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' },
|
||
...SHARED_BATCHES.map((b) => ({
|
||
id: b.id,
|
||
label: b.label,
|
||
range: batchRangeLabel(b),
|
||
startHour: b.startHour,
|
||
endHour: b.endHour,
|
||
...BATCH_PRESENTATION[b.id]
|
||
}))
|
||
];
|
||
|
||
// Auto-pick the batch matching the operator's LOCAL wall-clock hour so the
|
||
// page lands them on the slot they're most likely curious about. Falls back
|
||
// to 'all' when the current hour lies in a configured gap (8–9 AM, 12 PM–4
|
||
// PM, after 7 PM). Local time, not UTC, to match the dispatch page's
|
||
// bucketing — both pages must agree on which batch a given row belongs to.
|
||
const detectInitialBatchId = () => {
|
||
const now = dayjs();
|
||
const h = now.hour() + now.minute() / 60;
|
||
for (const b of BATCH_OPTIONS) {
|
||
if (b.id === 'all') continue;
|
||
if (h >= b.startHour && h < b.endHour) return b.id;
|
||
}
|
||
return 'all';
|
||
};
|
||
|
||
// Bucket by `orderdate` only — the booking's `createdat`, i.e. when the order
|
||
// came in. Must stay identical to the dispatch page's bucket field
|
||
// (Dispatch.js's `BATCH_TIME_FIELD = 'created'`, whose `keys` is
|
||
// `['orderdate']`), or the same row lands in different batches on the two
|
||
// pages — see the dispatch folder's CLAUDE.md §1.
|
||
//
|
||
// Three earlier attempts were wrong:
|
||
// • `deliverytime` mis-bucketed delivered orders into the wave they were
|
||
// *completed* in rather than the wave they belonged to, so an order due at
|
||
// 07:00 but delivered at 11:00 fell into the 8–9 AM gap and vanished from
|
||
// every batch here while still showing on dispatch.
|
||
// • `assigntime` looked correct (the spec says "bucket by assign time") but
|
||
// the Doormile backend has no assignment timestamp — api.js maps that field
|
||
// to the booking's last-modified column, so it is re-stamped by any status
|
||
// change and orders drifted into whichever batch held the current clock
|
||
// time.
|
||
// • `expecteddeliverytime` is stable, but it is the PROMISED DELIVERY slot,
|
||
// not the wave the order belongs to. An order created at 12:22 whose SLA
|
||
// put its ETA at 16:30 counted towards Evening, so the Evening batch showed
|
||
// orders nobody had placed yet.
|
||
//
|
||
// Creation time has neither failure mode: it is immutable, and no batch later
|
||
// than the current clock time can ever show a count for today.
|
||
//
|
||
// Rows with no creation timestamp return null and intentionally don't bucket —
|
||
// matches dispatch.
|
||
const BATCH_TIME_KEYS = ['orderdate'];
|
||
|
||
// Returns one of the batch ids in BATCH_OPTIONS (excluding 'all'), or null
|
||
// when the row has no usable timestamp / falls in a gap between batches.
|
||
const getRowBatchId = (row) => {
|
||
let t = null;
|
||
for (const k of BATCH_TIME_KEYS) {
|
||
if (row?.[k]) {
|
||
t = row[k];
|
||
break;
|
||
}
|
||
}
|
||
if (!t) return null;
|
||
const str = String(t).trim();
|
||
// Bare YYYY-MM-DD with no time component would always parse to midnight and
|
||
// get mis-bucketed into Morning — skip those.
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
|
||
// Parse in LOCAL time to match the dispatch page's bucketing exactly
|
||
// (`Dispatch.js:218`). Dispatch never uses `.utc()` for batch assignment,
|
||
// so a row that lands in Evening Batch on the dispatch page must also land
|
||
// in Evening Batch here — otherwise an operator running in IST/PST/etc.
|
||
// sees different totals on the two pages for the same underlying rows.
|
||
// parseDoormileTimestamp (not bare dayjs()) strips a false trailing Z some
|
||
// Doormile timestamps carry (see utils/doormileTimestamp.js) — without
|
||
// this, a booking assigned "just now" could get shifted +5:30 (IST) past
|
||
// every batch window into a gap, silently vanishing from the table even
|
||
// though it was correctly assigned server-side.
|
||
const d = parseDoormileTimestamp(t);
|
||
if (!d.isValid()) return null;
|
||
const h = d.hour() + d.minute() / 60;
|
||
for (const b of BATCH_OPTIONS) {
|
||
if (b.id === 'all') continue;
|
||
if (h >= b.startHour && h < b.endHour) return b.id;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// Astryx's Badge IS this pill. The hand-rolled <div> hardcoded 11px/700, its
|
||
// own radius, border and a 75px min-width against the theme, so these cells
|
||
// couldn't move with the type scale. `variant` is an enumerated Badge colour
|
||
// rather than a hex: these are metric readouts (km, ₹), not lifecycle
|
||
// statuses, so they must not go through StatusBadge.
|
||
const MetricPill = ({ variant = 'neutral', label, tooltip }) =>
|
||
tooltip ? (
|
||
<Tooltip content={tooltip}>
|
||
<Badge variant={variant} label={label} />
|
||
</Tooltip>
|
||
) : (
|
||
<Badge variant={variant} label={label} />
|
||
);
|
||
|
||
// TanStack Table v9 registers features explicitly, same shape as orders.js's
|
||
// ORDERS_TABLE_FEATURES. Declared at module scope so the reference is stable
|
||
// across renders. Column FILTERING is deliberately absent — the tab strip,
|
||
// batch selector and search filter upstream in `filteredRows`.
|
||
const DELIVERIES_TABLE_FEATURES = tableFeatures({
|
||
rowSelectionFeature,
|
||
columnVisibilityFeature,
|
||
rowSortingFeature,
|
||
sortedRowModel: createSortedRowModel(),
|
||
sortFns,
|
||
columnSizingFeature,
|
||
columnResizingFeature,
|
||
rowPaginationFeature,
|
||
paginatedRowModel: createPaginatedRowModel()
|
||
});
|
||
|
||
// Page sizes offered under the desktop table. 50 keeps the previous default.
|
||
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
||
|
||
// ================================================= || deliveries (initial point)|| =================================================
|
||
const Deliveries = () => {
|
||
const userid = localStorage.getItem('userid');
|
||
// Below the narrow-viewport breakpoint we swap the wide data table for an
|
||
// app-style card list. Desktop keeps the exact same table.
|
||
const isMobile = useIsNarrowViewport();
|
||
const loadMoreRef = useRef();
|
||
const [deliverylist, setDeliverylist] = useState([]);
|
||
const [dialogopen, setDialogopen] = useState(false);
|
||
const [locaName, setLocoName] = useState('All');
|
||
const [appId, setAppId] = useState(0);
|
||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
||
const [tabstatus, setTabstatus] = useState('Pending');
|
||
const [open, setOpen] = useState(false);
|
||
const [kms, setKms] = useState('');
|
||
const [cumulativekms, setCumulativeKms] = useState();
|
||
const [deliveryamount, setDeliveryamount] = useState();
|
||
const [notes, setNotes] = useState('');
|
||
const [currentorder, setCurrentorder] = useState({});
|
||
|
||
// Real Actual KMs for the Update Status dialog, computed from real GPS
|
||
// ping history instead of leaving the field permanently blank (there is
|
||
// no backend-stored actual-distance field — see fetchDeliveries in
|
||
// api.js). Same sort -> Kalman-smooth -> polyline-length pipeline
|
||
// Dispatch.js's Compare feature already uses to show a real "actual km"
|
||
// figure, just reused here (see dispatchShared.js). Keyed off
|
||
// consignmentid, NOT deliveryid/bookingid — GET /admin/consignments/:id/logs
|
||
// is consignment-scoped (the same id-class mix-up already fixed in
|
||
// updateDeliveryAPI this session).
|
||
const { data: computedActualKm } = useQuery({
|
||
queryKey: ['deliveryActualKm', currentorder?.consignmentid],
|
||
queryFn: async () => {
|
||
let rows = [];
|
||
try {
|
||
rows = (await getConsignmentLogs(currentorder.consignmentid)) || [];
|
||
} catch {
|
||
return null;
|
||
}
|
||
const sorted = rows
|
||
.map((r) => {
|
||
const ts = r?.logdate ? dayjs(r.logdate) : null;
|
||
return {
|
||
lat: parseFloat(r?.latitude ?? r?.lat),
|
||
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
|
||
logdate: r?.logdate,
|
||
_ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
|
||
};
|
||
})
|
||
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
|
||
.sort((a, b) => a._ts - b._ts);
|
||
// Fewer than 2 usable pings (order not yet delivered, or no telemetry
|
||
// recorded) — a real, expected case, not an error. Leave the field
|
||
// to its existing blank/manual-entry behavior.
|
||
if (sorted.length < 2) return null;
|
||
const smoothed = kalmanSmoothGps(sorted);
|
||
const km = polylineLengthKm(smoothed.map((p) => [p.lat, p.lng]));
|
||
return km > 0 ? km : null;
|
||
},
|
||
enabled: dialogopen && !!currentorder?.consignmentid,
|
||
staleTime: 5 * 60 * 1000,
|
||
refetchOnWindowFocus: false
|
||
});
|
||
|
||
// Only pre-fill while the field is still empty — never overwrite an
|
||
// operator's own edit (or an earlier pre-fill) if this query happens to
|
||
// re-run while the dialog is already open.
|
||
useEffect(() => {
|
||
if (computedActualKm != null && !cumulativekms) {
|
||
setCumulativeKms(Number(computedActualKm.toFixed(2)));
|
||
}
|
||
}, [computedActualKm]);
|
||
const [deliverylat, setDeliverylat] = useState('');
|
||
const [deliverylong, setDeliverylong] = useState('');
|
||
const [currentStatus, setCurrentStatus] = useState('pending');
|
||
const [updateStatus, setUpdateStatus] = useState('delivered');
|
||
const [page, setPage] = useState(0);
|
||
const [rowsPerPage, setRowsPerPage] = useState(50);
|
||
const [productCollapse, setProductCollapse] = useState(null);
|
||
const [orderHeaderid, setOrderHeaderId] = useState(null);
|
||
const [searchword, setSearchword] = useState('');
|
||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||
const [selectedRow, setSelectedRow] = useState(null);
|
||
const [loading1, setLoading1] = useState(false);
|
||
const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false);
|
||
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
||
const [cancelFeed, setCancelFeed] = useState('');
|
||
const [selectedRider, setSelectedRider] = useState(null);
|
||
const [tenantid, setTenantid] = useState(0);
|
||
const [locationid, setLocationid] = useState(0);
|
||
const [riderid, setRiderid] = useState(0);
|
||
// Selected batch id — drives the client-side row filter. Defaults to the
|
||
// batch matching the current UTC hour (the operator is most likely curious
|
||
// about "now"); never `null` since there's no longer an "All" option.
|
||
const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId);
|
||
|
||
// showAction/showSelect only depend on tabstatus, computed here (rather than
|
||
// just before the return, where they used to live) so the TanStack Table
|
||
// hooks below — which need them for column visibility — can run
|
||
// unconditionally, before the `errorMessage` early return further down.
|
||
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
|
||
// No tab ever sets tabstatus to 'Created' (see handleChangetab below), so
|
||
// this — and the desktop table's whole select column — is currently
|
||
// unreachable. Left as-is; not this task's scope to fix.
|
||
const showSelect = tabstatus == 'Created';
|
||
|
||
// Desktop table's own pagination/sorting/selection/column-visibility state —
|
||
// deliberately separate from `page`/`rowsPerPage` above, which still drive
|
||
// fetchCountAPI's query params and the mobile card list's row numbering.
|
||
// Mixing the two would tie an unrelated API call's params to a UI-only
|
||
// pagination control.
|
||
const [sorting, setSorting] = useState([]);
|
||
const [tablePagination, setTablePagination] = useState({ pageIndex: 0, pageSize: 50 });
|
||
const [rowSelection, setRowSelection] = useState({});
|
||
const [userColumnVisibility, setUserColumnVisibility] = useState({});
|
||
|
||
useEffect(() => {
|
||
setTenantid(0);
|
||
setLocationid(0);
|
||
}, [appId]);
|
||
// to clear the location autocomplete
|
||
useEffect(() => {
|
||
setLocationid(0);
|
||
}, [tenantid]);
|
||
|
||
// =========================================== || cancelDelivery || ===========================================
|
||
|
||
const { mutate: cancelDelivery } = useMutation({
|
||
mutationFn: ({ selectedRow, cancelFeed }) => cancelDeliveryAPI(selectedRow, cancelFeed),
|
||
onSuccess: () => {
|
||
opentoast('Delivery Cancelled Successfully', 'success');
|
||
setCancelDeliveryOpen(false);
|
||
fetchCountRefetch(); // Refresh count data
|
||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||
},
|
||
onError: (error) => {
|
||
opentoast(error.message, 'error');
|
||
}
|
||
});
|
||
|
||
// ==============================|| cancelridernotification ||============================== //
|
||
|
||
const cancelridernotification = async () => {
|
||
try {
|
||
// /admin/milers/:id/notify keys off milerprofileid, not the userid
|
||
// stored on the booking (confirmed live) — see fetchDeliveries in api.js.
|
||
return await notifyMiler(selectedRow.milerprofileid, 'DoormileXpress', `${selectedRow.orderid} have been Cancelled`);
|
||
} catch (err) {
|
||
opentoast(err.message, 'error', 2000);
|
||
}
|
||
};
|
||
|
||
// ==============================|| getTenants ||============================== //
|
||
|
||
const {
|
||
data: tenantlist,
|
||
isLoading: fetchtenantsIsLoading,
|
||
isError: fetchtenantsIsError,
|
||
error: fetchtenantsError
|
||
} = useQuery({
|
||
queryKey: ['tenantlist', appId],
|
||
queryFn: () => getTenants(appId), // Ensure appId is passed
|
||
// appId 0 is a real, deliberate choice — the synthetic "All" zone option
|
||
// (fetchAppLocations in api.js) — not "nothing picked yet"; only
|
||
// null/undefined means that now. See LocationAutocomplete.js's onChange.
|
||
enabled: appId != null
|
||
});
|
||
// ==============================|| gettenantlocations ||============================== //
|
||
|
||
const {
|
||
data: locationlist,
|
||
isLoading: fetchlocationsIsLoading,
|
||
isError: fetchlocationsIsError,
|
||
error: fetchlocationsError
|
||
} = useQuery({
|
||
queryKey: ['gettenantlocations', tenantid],
|
||
queryFn: () => gettenantlocations(tenantid), // Ensure appId is passed
|
||
enabled: tenantid !== 0 // Ensures query runs only when appId is valid
|
||
});
|
||
// =========================================== || notifyrider || ===========================================
|
||
|
||
const notifyRiderMutation = useMutation({
|
||
mutationFn: notifyRider, // Using the separate function
|
||
onSuccess: () => {
|
||
enqueueSnackbar('Notification sent Successfully', {
|
||
variant: 'success',
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: 2000
|
||
});
|
||
},
|
||
onError: (error) => {
|
||
enqueueSnackbar(error.message, {
|
||
variant: 'error',
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: 2000
|
||
});
|
||
}
|
||
});
|
||
|
||
// =========================================== || changerider || ===========================================
|
||
const changeRiderMutation = useMutation({
|
||
mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow),
|
||
onSuccess: (res, { selectedRider, selectedRow }) => {
|
||
setLoading1(false);
|
||
setChangeDialogOpen(false);
|
||
if (res.success) {
|
||
logger.info(
|
||
`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${
|
||
selectedRider?.displayname || selectedRider?.authname
|
||
}`
|
||
);
|
||
opentoast('Rider Changed Successfully', 'success');
|
||
}
|
||
fetchCountRefetch(); // Refresh count data
|
||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||
// notifyMiler wants a miler profile id, not an FCM token — the new API
|
||
// looks the device token up server-side. selectedRider comes straight
|
||
// from getMilers(), which has milerprofileid directly (confirmed live;
|
||
// .userid there is the underlying app-user id, a different resource).
|
||
notifyRiderMutation.mutate(selectedRider.milerprofileid);
|
||
},
|
||
onError: (err, { selectedRow }) => {
|
||
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
|
||
opentoast(err.message, 'error');
|
||
setLoading1(false);
|
||
}
|
||
});
|
||
|
||
/* ============================================= || opentoast || ============================================= */
|
||
|
||
const opentoast = (message, variant) => {
|
||
enqueueSnackbar(message, {
|
||
variant,
|
||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||
autoHideDuration: 2000
|
||
});
|
||
};
|
||
|
||
// ==============================|| getorderdetails ||============================== //
|
||
|
||
const { data: orderdetails } = useQuery({
|
||
queryKey: ['orderdetails', orderHeaderid],
|
||
queryFn: () => getorderdetails(orderHeaderid),
|
||
enabled: !!orderHeaderid // ✅ prevent initial fetch when undefined
|
||
});
|
||
|
||
const dialogclose = () => {
|
||
setDialogopen(false);
|
||
};
|
||
|
||
const handleChangetab = (i) => {
|
||
setPage(0);
|
||
setRowsPerPage(50);
|
||
if (i === 0) {
|
||
setTabstatus('Pending');
|
||
setCurrentStatus('pending');
|
||
}
|
||
if (i === 1) {
|
||
setTabstatus('Assigned');
|
||
setCurrentStatus('accepted');
|
||
}
|
||
if (i === 2) {
|
||
setTabstatus('Arrived');
|
||
setCurrentStatus('arrived');
|
||
}
|
||
if (i === 3) {
|
||
setTabstatus('Picked');
|
||
setCurrentStatus('picked');
|
||
}
|
||
if (i === 4) {
|
||
setTabstatus('Active');
|
||
setCurrentStatus('active');
|
||
}
|
||
if (i === 5) {
|
||
setTabstatus('Skipped');
|
||
setCurrentStatus('skipped');
|
||
}
|
||
if (i === 6) {
|
||
setTabstatus('Delivered');
|
||
setCurrentStatus('delivered');
|
||
}
|
||
|
||
if (i === 7) {
|
||
setTabstatus('Cancelled');
|
||
setCurrentStatus('cancelled');
|
||
}
|
||
setSearchword('');
|
||
setTablePagination((p) => ({ ...p, pageIndex: 0 }));
|
||
setRowSelection({});
|
||
};
|
||
|
||
const okclicked = () => {
|
||
setOpen(false);
|
||
};
|
||
|
||
// Parallel "all-statuses" query used ONLY to derive batch-aware tab counts.
|
||
// The primary `fetchdeliveries` query above is keyed by `currentStatus`, so
|
||
// its rows are scoped to the active tab — they can't tell us how many
|
||
// delivered/cancelled/etc. exist in the same batch. We fetch the full day
|
||
// once (status='all'), keep it cached, and re-derive the counts per status
|
||
// whenever the operator switches batches. Search keyword is intentionally
|
||
// dropped from this key so the chip counts reflect the batch totals rather
|
||
// than the search-filtered subset.
|
||
const {
|
||
data: countSourceData,
|
||
fetchNextPage: countFetchNext,
|
||
hasNextPage: countHasNext,
|
||
isFetchingNextPage: countIsFetchingNext,
|
||
isLoading: countSourceIsLoading,
|
||
refetch: countSourceRefetch
|
||
} = useInfiniteQuery({
|
||
queryKey: ['fetchdeliveries-batchcounts', appId, userid, 'all', startdate, enddate, 200, '', tenantid, locationid, riderid],
|
||
queryFn: fetchDeliveries,
|
||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
|
||
});
|
||
|
||
// Newest activity first — an order just created and assigned should be the
|
||
// very first row an operator sees, not buried wherever GET /admin/bookings
|
||
// happens to return it (unconfirmed/unspecified server-side order). Every
|
||
// row here has been dispatched at least once (fetchDeliveries filters to
|
||
// that), so assigntime (booking.updatedat) is the more recent of the two
|
||
// timestamps on a fresh assignment; orderdate (createdat) covers the rare
|
||
// row missing it. parseDoormileTimestamp (not bare dayjs()) avoids the
|
||
// false-Z parsing bug documented in utils/doormileTimestamp.js.
|
||
const countSourceRows = useMemo(() => {
|
||
const rows = (countSourceData?.pages || []).flatMap((p) => p.rows || []);
|
||
return [...rows].sort(
|
||
(a, b) =>
|
||
parseDoormileTimestamp(b.assigntime || b.orderdate).valueOf() - parseDoormileTimestamp(a.assigntime || a.orderdate).valueOf()
|
||
);
|
||
}, [countSourceData]);
|
||
|
||
useEffect(() => {
|
||
if (countHasNext && !countIsFetchingNext) countFetchNext();
|
||
}, [countHasNext, countIsFetchingNext, countFetchNext, countSourceRows.length]);
|
||
|
||
// Loading flag for the table empty state — true while we're still streaming
|
||
// pages, so we don't show "No <status> Orders" prematurely before the full
|
||
// day's rows have arrived. Goes false once auto-pagination drains.
|
||
const countSourceLoading = countHasNext || countIsFetchingNext;
|
||
|
||
// The table now reads from the same source as the chip counts. That way the
|
||
// "74" in the Delivered chip and the rows shown in the table can never
|
||
// disagree (they used to, because the table consumed a separate per-status
|
||
// paginated query while the chips read from the full all-statuses dataset).
|
||
// Three filter steps:
|
||
// 1. Batch — selected via the dropdown. 'all' bypasses this filter.
|
||
// 2. Tab status — currentStatus comes from handleChangetab.
|
||
// 3. Search — client-side substring match across customer/address/order
|
||
// fields. Server-side search isn't needed since every row is loaded.
|
||
const filteredRows = useMemo(() => {
|
||
const wantStatus = String(currentStatus || '').toLowerCase();
|
||
const q = String(debouncedSearch || '')
|
||
.trim()
|
||
.toLowerCase();
|
||
return countSourceRows.filter((r) => {
|
||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
|
||
const s = String(r.orderstatus || '').toLowerCase();
|
||
if (wantStatus === 'other') {
|
||
if (KNOWN_TAB_STATUSES.includes(s)) return false;
|
||
} else if (wantStatus && s !== wantStatus) return false;
|
||
if (q) {
|
||
const hay = [
|
||
r.deliverycustomer,
|
||
r.deliveryaddress,
|
||
r.deliverysuburb,
|
||
r.pickupcustomer,
|
||
r.pickupaddress,
|
||
r.pickupsuburb,
|
||
r.orderid,
|
||
r.tenantname,
|
||
r.ridername,
|
||
r.username
|
||
]
|
||
.map((v) => String(v || '').toLowerCase())
|
||
.join(' ');
|
||
if (!hay.includes(q)) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}, [countSourceRows, selectedBatch, currentStatus, debouncedSearch]);
|
||
|
||
// A batch/search change re-scopes the list (tab changes already reset this
|
||
// in handleChangetab), so page 1 is the only page that still means
|
||
// anything. Sorting and page size are the operator's own choices.
|
||
useEffect(() => {
|
||
setTablePagination((p) => ({ ...p, pageIndex: 0 }));
|
||
}, [selectedBatch, debouncedSearch]);
|
||
|
||
// Moved above the columns useMemo below (was previously declared just
|
||
// before the `errorMessage` early return, alongside tenantOptions etc.) —
|
||
// the 'action' column's cell closes over it, and a useMemo's factory runs
|
||
// synchronously the moment it's called, so it needs buildRowMenuItems to
|
||
// already be initialized, not just declared later in the same function body.
|
||
const buildRowMenuItems = (row) => {
|
||
const items = [];
|
||
if (row.orderstatus !== 'delivered') {
|
||
items.push({
|
||
label: 'Notify Rider',
|
||
icon: <MdNotificationsActive size={16} />,
|
||
onClick: () => notifyRiderMutation.mutate(row.milerprofileid)
|
||
});
|
||
}
|
||
if (['pending', 'accepted', 'arrived'].includes(row.orderstatus)) {
|
||
items.push({
|
||
label: 'Change Rider',
|
||
icon: <MdDirectionsBike size={16} />,
|
||
onClick: () => {
|
||
if (appId == null) {
|
||
opentoast('Please select a location first!', 'warning');
|
||
return;
|
||
}
|
||
setSelectedRow(row);
|
||
setChangeDialogOpen(true);
|
||
}
|
||
});
|
||
}
|
||
// Was gated to roleid 1/2 (staff-only), matching the reference app's
|
||
// identical gate — opened to every login per explicit decision, so
|
||
// tenant operators can reach the same Update Status fields staff
|
||
// already could.
|
||
items.push({
|
||
label: 'Update Status',
|
||
icon: <MdCheckCircle size={16} />,
|
||
onClick: () => {
|
||
// PUT /admin/consignments/:id/status is consignment-scoped — it
|
||
// only exists once the rider marks pickup-complete (doormile-flow.md).
|
||
// Without a real consignmentid, updateDeliveryAPI's `?? deliveryid`
|
||
// fallback was sending the BOOKING id where a consignment id is
|
||
// required — a different id space, so the call failed against
|
||
// the wrong (or nonexistent) resource. Combined with the mutation's
|
||
// onSuccess only handling `res.success === true` with no else
|
||
// branch, that failure was completely silent — dialog just sat
|
||
// there looking like nothing happened. Block it here instead,
|
||
// with a message that explains why, rather than letting the
|
||
// submit silently no-op.
|
||
if (!row.consignmentid) {
|
||
opentoast("This order hasn't been picked up yet — status can only be updated once it becomes a consignment.", 'warning');
|
||
return;
|
||
}
|
||
setSelectedRow(row);
|
||
setKms(row.kms);
|
||
setCumulativeKms(row.cumulativekms);
|
||
setDeliverylat(row.droplat);
|
||
setDeliverylong(row.droplon);
|
||
setNotes(row.notes);
|
||
setDeliveryamount(row.deliveryamt ?? row.deliveryamount);
|
||
setUpdateStatus(row.orderstatus || 'delivered');
|
||
setCurrentorder(row);
|
||
setDialogopen(true);
|
||
}
|
||
});
|
||
if (row.orderstatus !== 'cancelled' && row.orderstatus !== 'delivered') {
|
||
items.push({
|
||
label: 'Cancel Delivery',
|
||
icon: <MdCancel size={16} />,
|
||
onClick: () => {
|
||
setSelectedRow(row);
|
||
setCancelDeliveryOpen(true);
|
||
}
|
||
});
|
||
}
|
||
return items;
|
||
};
|
||
|
||
// ==============================|| Desktop table model (TanStack Table v9) ||==============================
|
||
// Same engine and anatomy as orders.js's table: explicit column defs,
|
||
// sortable/resizable columns, a Columns visibility menu, page-scoped row
|
||
// selection, and page-number Pagination instead of scroll-triggered
|
||
// "load more". filteredRows is already the fully-loaded, fully-filtered
|
||
// dataset (see countSourceRows' auto-drain above), so — like orders.js —
|
||
// the table paginates a FULL in-memory array rather than a server page.
|
||
const columns = useMemo(
|
||
() => [
|
||
{
|
||
id: 'select',
|
||
size: 44,
|
||
minSize: 44,
|
||
enableSorting: false,
|
||
enableResizing: false,
|
||
enableHiding: false,
|
||
meta: { label: 'Select' },
|
||
header: ({ table: t }) => (
|
||
<CheckboxInput
|
||
label="Select all"
|
||
isLabelHidden
|
||
value={t.getIsAllPageRowsSelected() ? true : t.getIsSomePageRowsSelected() ? 'indeterminate' : false}
|
||
onChange={(checked) => t.toggleAllPageRowsSelected(!!checked)}
|
||
/>
|
||
),
|
||
cell: ({ row }) => (
|
||
<CheckboxInput
|
||
label="Select row"
|
||
isLabelHidden
|
||
value={row.getIsSelected()}
|
||
onChange={(checked) => row.toggleSelected(!!checked)}
|
||
/>
|
||
)
|
||
},
|
||
{
|
||
id: 'serial',
|
||
size: 56,
|
||
minSize: 48,
|
||
enableSorting: false,
|
||
meta: { label: '#' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
#
|
||
</Text>
|
||
),
|
||
cell: ({ table: t, row }) => (
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
{String(tablePagination.pageIndex * tablePagination.pageSize + t.getRowModel().rows.indexOf(row) + 1).padStart(2, '0')}
|
||
</Text>
|
||
)
|
||
},
|
||
{
|
||
id: 'status',
|
||
accessorKey: 'orderstatus',
|
||
size: 130,
|
||
minSize: 100,
|
||
meta: { label: 'Status' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Status
|
||
</Text>
|
||
),
|
||
// The badge explains itself when the value did not come from the
|
||
// booking. Without this an operator comparing the table against
|
||
// GET /admin/bookings sees "Active" next to
|
||
// `"status": "Converted_To_Consignment"` and reasonably assumes the
|
||
// page is wrong — it isn't; the booking simply stops moving at pickup.
|
||
cell: ({ getValue, row }) =>
|
||
row.original.statusfromconsignment ? (
|
||
<Tooltip
|
||
content={`From consignment #${row.original.consignmentid} (${row.original.consignmentstatus}). The booking itself stays at Converted_To_Consignment once it has been picked up.`}
|
||
>
|
||
<span>
|
||
<StatusBadge status={getValue()} />
|
||
</span>
|
||
</Tooltip>
|
||
) : (
|
||
<StatusBadge status={getValue()} />
|
||
)
|
||
},
|
||
{
|
||
id: 'tenant',
|
||
accessorKey: 'tenantname',
|
||
size: 170,
|
||
minSize: 130,
|
||
meta: { label: 'Tenant' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Tenant
|
||
</Text>
|
||
),
|
||
cell: ({ row }) => (
|
||
<Tooltip content={row.original.tenantadress || ''}>
|
||
<VStack gap={0} padding={0}>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.original.tenantname}
|
||
</Text>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{row.original.tenantsuburb}
|
||
</Text>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{row.original.applocation}
|
||
</Text>
|
||
</VStack>
|
||
</Tooltip>
|
||
)
|
||
},
|
||
{
|
||
id: 'orderLocation',
|
||
accessorFn: (row) => row.orderid,
|
||
size: 240,
|
||
minSize: 180,
|
||
enableSorting: false,
|
||
meta: { label: 'Order / Location' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Order / Location
|
||
</Text>
|
||
),
|
||
// Order and delivery are stacked, not side by side. They used to be two
|
||
// VStacks inside an HStack, and flex items default to `min-width: auto`
|
||
// — so the order id (`DM-BK-BC87D8B0-30828`, unbreakable and wider than
|
||
// half the column) refused to shrink and crushed the delivery stack to
|
||
// zero width. A zero-width cell doesn't hide its text, it wraps it one
|
||
// character per line, which is what rendered as a vertical strip of
|
||
// letters. Two 20-character ids never fitted a 220px column; labelling
|
||
// them and giving each its own line is what actually fixes it, and the
|
||
// bare "-" separator between them conveyed nothing.
|
||
cell: ({ row }) => {
|
||
const r = row.original;
|
||
// express-console-api.md: Doormile timestamps are IST wall-clock with
|
||
// no timezone marker. A Z-suffixed response reinterpreted as local
|
||
// time shifts by 5:30 — showing the wrong time and sometimes the
|
||
// wrong day. parseDoormileTimestamp strips that, matching
|
||
// getRowBatchId above.
|
||
const ordered = parseDoormileTimestamp(r.orderdate);
|
||
const delivered = parseDoormileTimestamp(r.deliverydate);
|
||
return (
|
||
<VStack gap={0} padding={0} minWidth={0}>
|
||
<Tooltip content="Location Name-Suburb">
|
||
<Text maxLines={1}>{r.locationsuburb ? `${r.locationname}-(${r.locationsuburb})` : r.locationname}</Text>
|
||
</Tooltip>
|
||
|
||
<Tooltip content="Order Id">
|
||
<Text maxLines={1}>{r.orderid}</Text>
|
||
</Tooltip>
|
||
<Text type="supporting" color="secondary">
|
||
Ordered {ordered.format('DD/MM/YYYY · hh:mm A')}
|
||
</Text>
|
||
|
||
<Tooltip content="Delivery Id">
|
||
<Text maxLines={1}>{r.deliveryid}</Text>
|
||
</Tooltip>
|
||
<Text type="supporting" color="secondary">
|
||
Delivery {delivered.format('DD/MM/YYYY · hh:mm A')}
|
||
</Text>
|
||
</VStack>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
id: 'pickup',
|
||
accessorKey: 'pickupcustomer',
|
||
size: 190,
|
||
minSize: 140,
|
||
meta: { label: 'Pickup' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Pickup
|
||
</Text>
|
||
),
|
||
// pickup — capped to the same maxWidth as drop below, with CSS ellipsis
|
||
// truncation, so one long address can't stretch this column wider than
|
||
// its neighbour. row.pickuplocation is just the full raw address (see
|
||
// fetchDeliveries in api.js), not a pre-shortened name, so the old
|
||
// `.slice(0, 14)` fallback never actually ran. Full text still
|
||
// available via the tooltip.
|
||
cell: ({ row }) => (
|
||
<VStack gap={0} padding={0} minWidth={0}>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.original.pickupcustomer}
|
||
</Text>
|
||
<Text type="supporting" color="secondary">
|
||
{row.original.pickupcontactno}
|
||
</Text>
|
||
<Tooltip content={row.original.Pickupaddress || ''}>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{row.original.pickuplocation || row.original.Pickupaddress || '—'}
|
||
</Text>
|
||
</Tooltip>
|
||
</VStack>
|
||
)
|
||
},
|
||
{
|
||
id: 'drop',
|
||
accessorKey: 'deliverycustomer',
|
||
size: 190,
|
||
minSize: 140,
|
||
meta: { label: 'Drop' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Drop
|
||
</Text>
|
||
),
|
||
cell: ({ row }) => (
|
||
<VStack gap={0} padding={0} minWidth={0}>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.original.deliverycustomer}
|
||
</Text>
|
||
<Text type="supporting" color="secondary">
|
||
{row.original.deliverycontactno}
|
||
</Text>
|
||
<Tooltip content={row.original.deliveryaddress || ''}>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{row.original.deliverylocation || row.original.deliveryaddress || '—'}
|
||
</Text>
|
||
</Tooltip>
|
||
</VStack>
|
||
)
|
||
},
|
||
{
|
||
id: 'rider',
|
||
accessorKey: 'ridername',
|
||
size: 180,
|
||
minSize: 140,
|
||
sortUndefined: 'last',
|
||
meta: { label: 'Rider' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Rider
|
||
</Text>
|
||
),
|
||
cell: ({ row }) => {
|
||
const r = row.original;
|
||
return r.ridername ? (
|
||
<HStack gap={1.5} vAlign="center">
|
||
<AccentAvatar color="#8b5cf6" size={28}>
|
||
<MdDirectionsBike size={14} />
|
||
</AccentAvatar>
|
||
<VStack gap={0} padding={0}>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{r.ridername}
|
||
</Text>
|
||
<Text type="supporting" color="secondary">
|
||
ID #{r.userid} · {r.ridercontact || '—'}
|
||
</Text>
|
||
</VStack>
|
||
</HStack>
|
||
) : (
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
Unassigned
|
||
</Text>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
id: 'eta',
|
||
accessorFn: (row) => row.expecteddeliverytime || undefined,
|
||
size: 100,
|
||
minSize: 90,
|
||
sortUndefined: 'last',
|
||
meta: { label: 'ETA' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
ETA
|
||
</Text>
|
||
),
|
||
cell: ({ getValue }) => {
|
||
const v = getValue();
|
||
return (
|
||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||
{v ? parseDoormileTimestamp(v).format('hh:mm A') : '—'}
|
||
</Text>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
id: 'kms',
|
||
accessorFn: (row) => Number(row.kms) || undefined,
|
||
size: 110,
|
||
minSize: 90,
|
||
sortUndefined: 'last',
|
||
meta: { label: 'Kms' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Kms
|
||
</Text>
|
||
),
|
||
cell: ({ getValue }) => (
|
||
<VStack gap={0.5} padding={0}>
|
||
<MetricPill variant="red" label={`${(getValue() ?? 0).toFixed(2)} km`} tooltip="Planned KMS" />
|
||
</VStack>
|
||
)
|
||
},
|
||
{
|
||
id: 'amount',
|
||
accessorFn: (row) => Number(row.deliverycharges) || undefined,
|
||
size: 150,
|
||
minSize: 120,
|
||
sortUndefined: 'last',
|
||
meta: { label: 'Amount' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Amount
|
||
</Text>
|
||
),
|
||
cell: ({ row }) => (
|
||
<VStack gap={0.5} padding={0}>
|
||
<MetricPill variant="red" label={`₹ ${row.original.deliverycharges?.toFixed(2) ?? '0.00'}`} tooltip="Delivery Charge" />
|
||
<MetricPill variant="green" label={`₹ ${row.original.deliveryamt?.toFixed(2) ?? '0.00'}`} tooltip="Delivery Amount" />
|
||
</VStack>
|
||
)
|
||
},
|
||
{
|
||
id: 'notes',
|
||
accessorKey: 'notes',
|
||
size: 170,
|
||
minSize: 120,
|
||
enableSorting: false,
|
||
meta: { label: 'Notes' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Notes
|
||
</Text>
|
||
),
|
||
cell: ({ getValue }) => {
|
||
const v = getValue();
|
||
return v ? (
|
||
<Tooltip content={v}>
|
||
<Text
|
||
type="supporting"
|
||
color="secondary"
|
||
style={{ maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'inline-block' }}
|
||
>
|
||
{v}
|
||
</Text>
|
||
</Tooltip>
|
||
) : (
|
||
<Text type="supporting" color="secondary">
|
||
—
|
||
</Text>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
id: 'qty',
|
||
accessorKey: 'Quantity',
|
||
size: 80,
|
||
minSize: 64,
|
||
meta: { label: 'Qty' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Qty
|
||
</Text>
|
||
),
|
||
cell: ({ row }) => (
|
||
<Text weight="semibold" style={{ color: row.original.Quantity ? undefined : DT.textMuted, whiteSpace: 'nowrap' }}>
|
||
{row.original.Quantity || '—'}
|
||
</Text>
|
||
)
|
||
},
|
||
{
|
||
id: 'action',
|
||
size: 90,
|
||
minSize: 80,
|
||
enableSorting: false,
|
||
enableResizing: false,
|
||
enableHiding: false,
|
||
meta: { label: 'Action', align: 'right' },
|
||
header: () => (
|
||
<Text type="label" weight="bold" color="secondary">
|
||
Action
|
||
</Text>
|
||
),
|
||
cell: ({ row }) => {
|
||
const r = row.original;
|
||
const isOpen = productCollapse?.orderid === r?.orderid;
|
||
return (
|
||
<HStack gap={0.75} justify="end">
|
||
{r.deliverytype == 'C' && (
|
||
<IconButton
|
||
label="Product details"
|
||
tooltip="Product details"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon={isOpen ? <MdKeyboardArrowUp size={16} /> : <MdKeyboardArrowDown size={16} />}
|
||
onClick={() => {
|
||
if (isOpen) {
|
||
setProductCollapse(null);
|
||
setOrderHeaderId(null);
|
||
} else {
|
||
setProductCollapse(r);
|
||
setOrderHeaderId(r.orderheaderid);
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
<MoreMenu label="Actions" items={buildRowMenuItems(r)} size="sm" />
|
||
</HStack>
|
||
);
|
||
}
|
||
}
|
||
],
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
[tablePagination.pageIndex, tablePagination.pageSize, productCollapse]
|
||
);
|
||
|
||
// Two sources feed one visibility slice: whatever the operator toggled in
|
||
// the Columns menu, plus the checkbox/action columns, which the ACTIVE TAB
|
||
// owns (same pattern as orders.js's `select` column).
|
||
const columnVisibility = useMemo(
|
||
() => ({ ...userColumnVisibility, select: showSelect, action: showAction }),
|
||
[userColumnVisibility, showSelect, showAction]
|
||
);
|
||
|
||
const table = useTable({
|
||
features: DELIVERIES_TABLE_FEATURES,
|
||
data: filteredRows,
|
||
columns,
|
||
getRowId: (row, index) => String(row.orderheaderid ?? `row-${index}`),
|
||
columnResizeMode: 'onChange',
|
||
state: { rowSelection, columnVisibility, sorting, pagination: tablePagination },
|
||
onRowSelectionChange: setRowSelection,
|
||
onColumnVisibilityChange: setUserColumnVisibility,
|
||
onSortingChange: setSorting,
|
||
onPaginationChange: setTablePagination
|
||
});
|
||
|
||
const colCount = table.getVisibleLeafColumns().length;
|
||
|
||
// The distinct statuses that matched no tab, named so the fix is obvious.
|
||
const unmappedStatuses = useMemo(() => {
|
||
const seen = new Set();
|
||
countSourceRows.forEach((r) => {
|
||
const s = String(r.orderstatus || '').toLowerCase();
|
||
if (s && !KNOWN_TAB_STATUSES.includes(s)) seen.add(s);
|
||
});
|
||
return [...seen];
|
||
}, [countSourceRows]);
|
||
|
||
// Counts per status, scoped to the selected batch. Keys mirror the legacy
|
||
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
|
||
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
|
||
const batchCounts = useMemo(() => {
|
||
const c = {
|
||
uncoveredLength: 0,
|
||
assignedLength: 0,
|
||
otherLength: 0,
|
||
arrivedLength: 0,
|
||
pickedLength: 0,
|
||
activeLength: 0,
|
||
skippedLength: 0,
|
||
coveredLength: 0,
|
||
cancelLength: 0
|
||
};
|
||
countSourceRows.forEach((r) => {
|
||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
|
||
const s = String(r.orderstatus || '').toLowerCase();
|
||
switch (s) {
|
||
case 'pending':
|
||
c.uncoveredLength += 1;
|
||
break;
|
||
case 'accepted':
|
||
case 'assigned':
|
||
c.assignedLength += 1;
|
||
break;
|
||
case 'arrived':
|
||
c.arrivedLength += 1;
|
||
break;
|
||
case 'picked':
|
||
c.pickedLength += 1;
|
||
break;
|
||
case 'active':
|
||
c.activeLength += 1;
|
||
break;
|
||
case 'skipped':
|
||
c.skippedLength += 1;
|
||
break;
|
||
case 'delivered':
|
||
c.coveredLength += 1;
|
||
break;
|
||
case 'cancelled':
|
||
case 'canceled':
|
||
c.cancelLength += 1;
|
||
break;
|
||
// Every status this page knows has a case above. Anything else — a
|
||
// backend enum nobody has mapped yet — used to fall through here AND
|
||
// fail every tab's equality filter, so the row was counted nowhere and
|
||
// displayed nowhere: silent data loss, not an empty state.
|
||
default:
|
||
c.otherLength += 1;
|
||
break;
|
||
}
|
||
});
|
||
return c;
|
||
}, [countSourceRows, selectedBatch]);
|
||
|
||
// Per-batch total (any status) for the dropdown badges. Computed once for
|
||
// every batch + 'all' so each option in the menu can show its own count
|
||
// without re-walking the list. Mirrors dispatch's batchCounts shape
|
||
// (`Dispatch.js:1198–1206`) — totals here should match the per-batch numbers
|
||
// visible on the dispatch page for the same date.
|
||
const batchTotals = useMemo(() => {
|
||
// `all` is the SUM of the three batch buckets — not the raw row count.
|
||
// Rows that fall in a gap (8–9 AM, 12:30–4 PM, after 7 PM) or have no
|
||
// expected delivery time belong to no batch, so they're excluded here too. This keeps
|
||
// the "All Batches" badge equal to Morning + Afternoon + Evening
|
||
// (e.g. 36 + 89 + 53 = 178) instead of over-reporting the un-bucketed total.
|
||
const totals = { all: 0 };
|
||
BATCH_OPTIONS.forEach((b) => {
|
||
if (b.id === 'all') return;
|
||
totals[b.id] = 0;
|
||
});
|
||
countSourceRows.forEach((r) => {
|
||
const id = getRowBatchId(r);
|
||
if (id) {
|
||
totals[id] = (totals[id] || 0) + 1;
|
||
totals.all += 1;
|
||
}
|
||
});
|
||
return totals;
|
||
}, [countSourceRows]);
|
||
|
||
// IntersectionObserver now watches the count-source query — that's the
|
||
// single source of truth for both the table and the chip counts. When the
|
||
// sentinel scrolls into view it nudges the next page of all-statuses
|
||
// deliveries to keep the dataset complete.
|
||
useEffect(() => {
|
||
if (!countHasNext) return;
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
if (entries[0].isIntersecting) {
|
||
countFetchNext();
|
||
}
|
||
},
|
||
{
|
||
root: document.querySelector('.dt-table-scroll'),
|
||
rootMargin: '0px',
|
||
threshold: 1.0
|
||
}
|
||
);
|
||
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
||
return () => {
|
||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||
};
|
||
}, [countHasNext, countFetchNext]);
|
||
|
||
/* ============================================= || fetchcount | ============================================= */
|
||
|
||
// `data` (fetchCountAPI's own count breakdown) is no longer read — the KPI
|
||
// tiles above now derive from batchCounts/batchTotals instead (see the
|
||
// comment there). This query still runs for isLoading/isError/refetch,
|
||
// which other code below still depends on.
|
||
const {
|
||
isLoading: fetchCountIsLoading,
|
||
isError: fetchCountIsError,
|
||
error: fetchCountError,
|
||
refetch: fetchCountRefetch
|
||
} = useQuery({
|
||
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
|
||
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
|
||
});
|
||
// ==============================|| fetchRidersList ||============================== //
|
||
|
||
const {
|
||
data: ridersList = [],
|
||
isLoading: riderListIsLoading,
|
||
isError: ridersListIsError,
|
||
error: ridersListError
|
||
} = useQuery({
|
||
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
|
||
// fetchRidersList now toasts + returns [] internally on failure (was
|
||
// throwing after already toasting there too, which showed this same
|
||
// error twice) — no onError needed here anymore.
|
||
queryFn: fetchRidersList,
|
||
enabled: appId != null
|
||
});
|
||
|
||
/* ============================================= || updatedelivery | ============================================= */
|
||
|
||
const updateDeliveryMutation = useMutation({
|
||
mutationFn: (orderData) => updateDeliveryAPI(orderData),
|
||
onSuccess: (res) => {
|
||
if (res.success) {
|
||
opentoast('Updated Successfully', 'success');
|
||
setDeliveryamount('');
|
||
setNotes('');
|
||
setDialogopen(false);
|
||
fetchCountRefetch();
|
||
countSourceRefetch();
|
||
} else {
|
||
// A 200 with success:false (the Doormile API's validation-failure
|
||
// shape, same pattern as customers.js's saveMutation) had no else
|
||
// branch here before — the dialog just sat open with zero feedback,
|
||
// looking exactly like the button didn't work.
|
||
opentoast(res.message || 'Failed to update delivery', 'error');
|
||
}
|
||
},
|
||
onError: (err) => {
|
||
opentoast(err.response?.data?.message || err.message || 'Failed to update delivery', 'error');
|
||
}
|
||
});
|
||
|
||
const errorMessage = fetchCountIsError
|
||
? `Error fetching count summary: ${fetchCountError?.message}`
|
||
: ridersListIsError
|
||
? `Error fetching riders: ${ridersListError?.message}`
|
||
: fetchtenantsIsError
|
||
? `Error tenant list: ${fetchtenantsError?.message}`
|
||
: fetchlocationsIsError
|
||
? `Error location list: ${fetchlocationsError?.message}`
|
||
: null;
|
||
|
||
if (errorMessage) {
|
||
OpenToast(errorMessage, 'error', 2000);
|
||
return null; // or return <></> if inside a component
|
||
}
|
||
|
||
const tenantOptions = (tenantlist || []).map((t) => ({ value: String(t.tenantid), label: t.tenantname }));
|
||
const locationOptions = (locationlist || []).map((l) => ({
|
||
value: String(l.locationid),
|
||
label: l.locationname ? `${l.locationname}${l.suburb ? ` (${l.suburb})` : ''}` : ''
|
||
}));
|
||
const riderOptions = (ridersList || []).map((r) => {
|
||
const name = r?.displayname || r?.authname || '';
|
||
return { value: String(r.userid), label: r?.contactno ? `${name} (${r.contactno})` : name };
|
||
});
|
||
|
||
// Trims the redundant " Batch" off BATCH_OPTIONS' own label (which stays
|
||
// full elsewhere, e.g. the empty-state message) — "Afternoon Batch · 89"
|
||
// was overflowing/wrapping inside the selector's compact trigger, squeezed
|
||
// to a 200px FilterToolbar track alongside four other filters.
|
||
const batchSelectorOptions = BATCH_OPTIONS.map((b) => ({
|
||
value: b.id,
|
||
label: `${b.label.replace(/ Batch$/, '')} · ${batchTotals[b.id] ?? 0}`
|
||
}));
|
||
|
||
const renderProductDetails = () => (
|
||
<Card padding={0} elevation="none" style={{ margin: 16 }}>
|
||
<HStack gap={2} vAlign="center" padding={2} style={{ borderBottom: `1px solid ${DT.divider}`, background: tint(BRAND) }}>
|
||
<AccentAvatar color={BRAND} selected size={28}>
|
||
<MdInventory2 size={14} />
|
||
</AccentAvatar>
|
||
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||
Product Details
|
||
</Text>
|
||
</HStack>
|
||
<TableScroll minWidth={700}>
|
||
<Table density="compact" dividers="rows">
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHeaderCell style={{ minWidth: 40, width: 40, maxWidth: 40 }}>#</TableHeaderCell>
|
||
<TableHeaderCell>Item</TableHeaderCell>
|
||
<TableHeaderCell>Category</TableHeaderCell>
|
||
<TableHeaderCell style={{ textAlign: 'right' }}>Weight</TableHeaderCell>
|
||
<TableHeaderCell style={{ textAlign: 'right' }}>Declared Value</TableHeaderCell>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{/* GET /admin/bookings has no product image/qty/tax breakdown —
|
||
this renders the real "parcels" array instead of the old
|
||
product-catalog shape. */}
|
||
{orderdetails?.parcels?.map((parcel, idx2) => (
|
||
<TableRow key={idx2}>
|
||
<TableCell>
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
{String(idx2 + 1).padStart(2, '0')}
|
||
</Text>
|
||
</TableCell>
|
||
<TableCell>
|
||
<HStack gap={1.5} vAlign="center">
|
||
<AccentAvatar color={BRAND} size={28}>
|
||
<MdInventory2 size={13} />
|
||
</AccentAvatar>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{parcel?.itemdescription || 'Item'}
|
||
</Text>
|
||
</HStack>
|
||
</TableCell>
|
||
<TableCell>{parcel?.itemcategory || '-'}</TableCell>
|
||
<TableCell style={{ textAlign: 'right' }}>{parcel?.weight ? `${parcel.weight}kg` : '-'}</TableCell>
|
||
<TableCell style={{ textAlign: 'right' }}>
|
||
<Text weight="bold">₹ {(parcel?.declaredvalue || 0).toFixed(2)}</Text>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
<TableRow>
|
||
<TableCell colSpan={4} style={{ textAlign: 'right' }}>
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
Delivery Charge
|
||
</Text>
|
||
</TableCell>
|
||
<TableCell style={{ textAlign: 'right' }}>
|
||
<Text weight="bold" style={{ color: '#10b981' }}>
|
||
₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)}
|
||
</Text>
|
||
</TableCell>
|
||
</TableRow>
|
||
</TableBody>
|
||
</Table>
|
||
</TableScroll>
|
||
</Card>
|
||
);
|
||
|
||
const emptyDescription =
|
||
selectedBatch === 'all'
|
||
? `No ${(getStatusMeta(currentStatus)?.label || tabstatus).toLowerCase()} orders for this filter.`
|
||
: `No ${(getStatusMeta(currentStatus)?.label || tabstatus).toLowerCase()} orders in ${
|
||
BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'
|
||
}.`;
|
||
|
||
return (
|
||
<>
|
||
{(fetchCountIsLoading || countSourceIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading) && (
|
||
<Loader />
|
||
)}
|
||
|
||
<PageShell fill>
|
||
<PageHeader
|
||
title="Deliveries"
|
||
subtitle={`Live · ${locaName || 'All Zones'}`}
|
||
live
|
||
action={
|
||
<LocationAutocomplete
|
||
locaName={locaName}
|
||
setAppId={setAppId}
|
||
setLocoName={setLocoName}
|
||
setPage={setPage}
|
||
placeholder="Select Zone"
|
||
/>
|
||
}
|
||
/>
|
||
|
||
{/* All four tiles now read off the same countSourceRows-derived data
|
||
the StatusTabs pills use (batchTotals/batchCounts), instead of
|
||
three of them reading a separate, unscoped `fetchCountAPI` call —
|
||
fetchCountAPI ignores every argument passed to it (see api.js) and
|
||
returns an ALL-TIME, un-batched, un-dated total, which is why
|
||
"Pending Orders" could show a number the Pending tab itself
|
||
disagreed with. Same fix orders.js's KPI row already applies:
|
||
a tile and its tab must read the same source so they can't drift. */}
|
||
<KpiGrid>
|
||
{[
|
||
{ ...KPI_META[0], value: batchTotals.all },
|
||
{ ...KPI_META[1], value: batchCounts.uncoveredLength },
|
||
{ ...KPI_META[2], value: batchCounts.coveredLength },
|
||
{ ...KPI_META[3], value: batchCounts.cancelLength }
|
||
].map((item) => {
|
||
const Icon = item.icon;
|
||
return (
|
||
<StatCard
|
||
key={item.key}
|
||
title={item.label}
|
||
value={item.value ?? 0}
|
||
icon={<Icon size={20} />}
|
||
color={item.color}
|
||
loading={countSourceIsLoading}
|
||
/>
|
||
);
|
||
})}
|
||
</KpiGrid>
|
||
|
||
<DataCard
|
||
fill
|
||
toolbar={
|
||
<FilterToolbar max={5} minWidth={200}>
|
||
<Selector
|
||
label="Batch"
|
||
isLabelHidden
|
||
options={batchSelectorOptions}
|
||
value={selectedBatch}
|
||
onChange={(v) => v && setSelectedBatch(v)}
|
||
placeholder="Batch"
|
||
/>
|
||
|
||
<Button
|
||
label={
|
||
startdate && enddate ? `${dayjs(startdate).format('DD MMM')} – ${dayjs(enddate).format('DD MMM')}` : 'Select date range'
|
||
}
|
||
variant="secondary"
|
||
icon={<MdOutlineDateRange size={16} />}
|
||
onClick={() => setOpen(true)}
|
||
/>
|
||
|
||
<Selector
|
||
label="Tenant"
|
||
isLabelHidden
|
||
options={tenantOptions}
|
||
value={tenantid ? String(tenantid) : ''}
|
||
onChange={(v) => {
|
||
if (!v) {
|
||
setTenantid(0);
|
||
return;
|
||
}
|
||
const t = (tenantlist || []).find((x) => String(x.tenantid) === v);
|
||
setTenantid(t?.tenantid || 0);
|
||
}}
|
||
placeholder="Select Tenant"
|
||
hasSearch
|
||
hasClear
|
||
isDisabled={appId == null}
|
||
disabledMessage="Please select your zone first!"
|
||
/>
|
||
|
||
<Selector
|
||
label="Location"
|
||
isLabelHidden
|
||
options={locationOptions}
|
||
value={locationid ? String(locationid) : ''}
|
||
onChange={(v) => {
|
||
if (!v) {
|
||
setLocationid(0);
|
||
return;
|
||
}
|
||
const l = (locationlist || []).find((x) => String(x.locationid) === v);
|
||
setLocationid(l?.locationid || 0);
|
||
}}
|
||
placeholder="Select Location"
|
||
hasSearch
|
||
hasClear
|
||
isDisabled={!tenantid}
|
||
disabledMessage={appId == null ? 'Please select your Zone and Tenant first!' : 'Please select your Tenant first!'}
|
||
/>
|
||
|
||
<Selector
|
||
label="Rider"
|
||
isLabelHidden
|
||
options={riderOptions}
|
||
value={riderid ? String(riderid) : ''}
|
||
onChange={(v) => setRiderid(v ? Number(v) : 0)}
|
||
placeholder="Choose Rider"
|
||
hasSearch
|
||
hasClear
|
||
isDisabled={riderListIsLoading || appId == null}
|
||
disabledMessage="Please select your zone first!"
|
||
/>
|
||
</FilterToolbar>
|
||
}
|
||
tabs={
|
||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap">
|
||
<StatusTabs
|
||
value={currentStatus}
|
||
onChange={(status) => {
|
||
const idx = STATUS_TABS.findIndex((t) => t.status === status);
|
||
if (idx !== -1) handleChangetab(idx);
|
||
}}
|
||
tabs={[
|
||
...STATUS_TABS.map((t) => ({ status: t.status, count: batchCounts[t.countKey] ?? 0 })),
|
||
// Only ever rendered when a status arrived that this page has
|
||
// no tab for. Its presence is the signal that the mapping in
|
||
// api.js needs extending — the rows are reachable meanwhile.
|
||
...(batchCounts.otherLength > 0
|
||
? [{ status: 'other', label: `Other (${unmappedStatuses.join(', ')})`, count: batchCounts.otherLength }]
|
||
: [])
|
||
]}
|
||
size="sm"
|
||
hasDivider={false}
|
||
/>
|
||
<HStack gap={2} vAlign="center">
|
||
<VStack maxWidth={260} width="100%" padding={0} gap={0}>
|
||
<DebounceSearchBar value={searchword} onChange={setSearchword} onDebouncedChange={setDebouncedSearch} />
|
||
</VStack>
|
||
|
||
{/* Columns menu — driven by the same `columnVisibility` slice
|
||
the tab uses for select/action, so there is one source of
|
||
truth for "is this column showing" (same pattern as
|
||
orders.js). Only rendered for the desktop table. */}
|
||
{!isMobile && (
|
||
<DropdownMenu
|
||
button={{ label: 'Columns', variant: 'secondary', size: 'sm', icon: <MdViewColumn size={16} /> }}
|
||
menuWidth={200}
|
||
placement="below"
|
||
>
|
||
{table
|
||
.getAllLeafColumns()
|
||
.filter((column) => column.getCanHide())
|
||
.map((column) => (
|
||
<DropdownMenuCheckboxItem
|
||
key={column.id}
|
||
label={column.columnDef.meta?.label ?? column.id}
|
||
value={column.getIsVisible()}
|
||
onChange={(checked) => column.toggleVisibility(checked)}
|
||
hasCloseOnSelect={false}
|
||
/>
|
||
))}
|
||
</DropdownMenu>
|
||
)}
|
||
</HStack>
|
||
</HStack>
|
||
}
|
||
>
|
||
{isMobile ? (
|
||
<MobileCardList scroll>
|
||
{filteredRows.length === 0 && !loading1 && !countSourceLoading && (
|
||
<EmptyState title="No deliveries to show" description={emptyDescription} icon={<MdInventory2 size={28} />} />
|
||
)}
|
||
{filteredRows.length === 0 && countSourceLoading && (
|
||
<VStack vAlign="center" padding={4}>
|
||
<LoaderWithImage />
|
||
<Text type="supporting" color="secondary" style={{ marginTop: 8 }}>
|
||
Loading deliveries…
|
||
</Text>
|
||
</VStack>
|
||
)}
|
||
{filteredRows.map((row, index) => {
|
||
const rowStatusMeta = getStatusMeta(row.orderstatus);
|
||
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
const isOpen = productCollapse?.orderid === row?.orderid;
|
||
return (
|
||
<MobileCard
|
||
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
|
||
accent={rowStatusMeta.color}
|
||
selected={isSelected}
|
||
header={
|
||
<VStack gap={1.5} padding={0}>
|
||
<HStack justify="between" vAlign="center" gap={1}>
|
||
<HStack gap={1.5} vAlign="center" minWidth={0}>
|
||
{showSelect && (
|
||
<CheckboxInput
|
||
label="Select row"
|
||
isLabelHidden
|
||
value={isSelected}
|
||
onChange={(checked) => {
|
||
if (checked) {
|
||
setDeliverylist([...deliverylist, { ...row, sno: deliverylist.length + 1 }]);
|
||
} else {
|
||
const res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||
if (res) {
|
||
const arr = deliverylist.filter((_, i) => i !== res.sno - 1);
|
||
arr.forEach((val, i) => (val.sno = i + 1));
|
||
setDeliverylist(arr);
|
||
}
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
|
||
</Text>
|
||
<StatusBadge status={row.orderstatus} />
|
||
</HStack>
|
||
<HStack gap={0.75}>
|
||
{row.deliverytype == 'C' && (
|
||
<IconButton
|
||
label="Product details"
|
||
tooltip="Product details"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon={isOpen ? <MdKeyboardArrowUp size={16} /> : <MdKeyboardArrowDown size={16} />}
|
||
onClick={() => {
|
||
if (isOpen) {
|
||
setProductCollapse(null);
|
||
setOrderHeaderId(null);
|
||
} else {
|
||
setProductCollapse(row);
|
||
setOrderHeaderId(row.orderheaderid);
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
{showAction && <MoreMenu label="Actions" items={buildRowMenuItems(row)} size="sm" />}
|
||
</HStack>
|
||
</HStack>
|
||
<VStack gap={0} padding={0} minWidth={0}>
|
||
<Text weight="bold" maxLines={1}>
|
||
{row.tenantname}
|
||
</Text>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
|
||
</Text>
|
||
</VStack>
|
||
</VStack>
|
||
}
|
||
>
|
||
<MobileFieldGrid>
|
||
<MobileField label="Order / Location" full>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname}
|
||
</Text>
|
||
<Text type="supporting" color="secondary">
|
||
{row.orderid} · {row.deliveryid}
|
||
</Text>
|
||
</MobileField>
|
||
<MobileField label="Pickup">
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.pickupcustomer || '—'}
|
||
</Text>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{row.pickupcontactno}
|
||
</Text>
|
||
</MobileField>
|
||
<MobileField label="Drop">
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.deliverycustomer || '—'}
|
||
</Text>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
{row.deliverycontactno}
|
||
</Text>
|
||
</MobileField>
|
||
<MobileField label="Rider" full>
|
||
{row.ridername ? (
|
||
<HStack gap={1.5} vAlign="center">
|
||
<AccentAvatar color="#8b5cf6" size={24}>
|
||
<MdDirectionsBike size={13} />
|
||
</AccentAvatar>
|
||
<VStack gap={0} padding={0} minWidth={0}>
|
||
<Text weight="semibold" maxLines={1}>
|
||
{row.ridername}
|
||
</Text>
|
||
<Text type="supporting" color="secondary" maxLines={1}>
|
||
ID #{row.userid} · {row.ridercontact || '—'}
|
||
</Text>
|
||
</VStack>
|
||
</HStack>
|
||
) : (
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
Unassigned
|
||
</Text>
|
||
)}
|
||
</MobileField>
|
||
<MobileField
|
||
label="ETA"
|
||
value={row.expecteddeliverytime ? parseDoormileTimestamp(row.expecteddeliverytime).format('hh:mm A') : '—'}
|
||
/>
|
||
<MobileField label="Transit">
|
||
<MetricPill variant="cyan" label={`${row.transitminutes || 0}m`} />
|
||
</MobileField>
|
||
<MobileField label="Kms · plan / act">
|
||
<HStack gap={1} wrap="wrap">
|
||
<MetricPill variant="red" label={`${Number(row.kms || 0).toFixed(2)} km`} />
|
||
</HStack>
|
||
</MobileField>
|
||
<MobileField label="Amount · chg / amt">
|
||
<HStack gap={1} wrap="wrap">
|
||
<MetricPill variant="red" label={`₹ ${row.deliverycharges?.toFixed(2) ?? '0.00'}`} />
|
||
<MetricPill variant="green" label={`₹ ${row.deliveryamt?.toFixed(2) ?? '0.00'}`} />
|
||
</HStack>
|
||
</MobileField>
|
||
<MobileField label="Qty" value={row.Quantity || '—'} />
|
||
<MobileField label="COD">
|
||
<Text weight="bold" style={{ color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
|
||
{row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'}
|
||
</Text>
|
||
</MobileField>
|
||
<MobileField label="Step" value={row.step || '—'} />
|
||
{row.notes && (
|
||
<MobileField label="Notes" full>
|
||
<Text type="supporting" color="secondary">
|
||
{row.notes}
|
||
</Text>
|
||
</MobileField>
|
||
)}
|
||
</MobileFieldGrid>
|
||
{isOpen && <div style={{ marginTop: 'var(--spacing-6)' }}>{renderProductDetails()}</div>}
|
||
</MobileCard>
|
||
);
|
||
})}
|
||
{countSourceRows?.length != 0 && (
|
||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||
{countIsFetchingNext || countHasNext ? (
|
||
<LoaderWithImage />
|
||
) : (
|
||
<Text type="supporting" weight="bold" color="secondary">
|
||
· End of list ·
|
||
</Text>
|
||
)}
|
||
</div>
|
||
)}
|
||
</MobileCardList>
|
||
) : (
|
||
<>
|
||
<VStack gap={0} padding={0} style={{ flex: 1, minHeight: 0 }}>
|
||
<TableScroll minWidth={table.getTotalSize()} maxHeight="100%" flush>
|
||
{/* table-layout: fixed makes the declared column widths
|
||
AUTHORITATIVE — same reasoning as orders.js's table.
|
||
Widths come from header.getSize(), the columnSizing slice
|
||
the drag handles write to. */}
|
||
<Table
|
||
density="compact"
|
||
dividers="none"
|
||
hasHover
|
||
style={{ tableLayout: 'fixed', width: '100%', minWidth: table.getTotalSize() }}
|
||
>
|
||
<TableHeader>
|
||
{table.getHeaderGroups().map((headerGroup) => (
|
||
<TableRow key={headerGroup.id} isHeaderRow>
|
||
{headerGroup.headers.map((header) => {
|
||
const align = header.column.columnDef.meta?.align;
|
||
const canSort = header.column.getCanSort();
|
||
const sorted = header.column.getIsSorted();
|
||
return (
|
||
<TableHeaderCell
|
||
key={header.id}
|
||
scope="col"
|
||
aria-sort={sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : canSort ? 'none' : undefined}
|
||
style={{
|
||
width: header.getSize(),
|
||
minWidth: header.column.columnDef.minSize,
|
||
maxWidth: header.column.columnDef.maxSize || header.getSize(),
|
||
textAlign: align,
|
||
position: 'relative'
|
||
}}
|
||
>
|
||
<HStack
|
||
gap={1}
|
||
vAlign="center"
|
||
justify={align === 'right' ? 'end' : align === 'center' ? 'center' : 'start'}
|
||
role={canSort ? 'button' : undefined}
|
||
tabIndex={canSort ? 0 : undefined}
|
||
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||
onKeyDown={
|
||
canSort
|
||
? (e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
header.column.toggleSorting();
|
||
}
|
||
}
|
||
: undefined
|
||
}
|
||
style={canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined}
|
||
>
|
||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||
{canSort &&
|
||
(sorted === 'asc' ? (
|
||
<MdArrowUpward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
|
||
) : sorted === 'desc' ? (
|
||
<MdArrowDownward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
|
||
) : (
|
||
<MdUnfoldMore size={12} color={DT.textMuted} style={{ flexShrink: 0, opacity: 0.6 }} />
|
||
))}
|
||
</HStack>
|
||
|
||
{header.column.getCanResize() && (
|
||
<Center
|
||
aria-hidden="true"
|
||
onMouseDown={header.getResizeHandler()}
|
||
onTouchStart={header.getResizeHandler()}
|
||
style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
right: 0,
|
||
height: '100%',
|
||
width: 10,
|
||
cursor: 'col-resize',
|
||
userSelect: 'none',
|
||
touchAction: 'none'
|
||
}}
|
||
>
|
||
<Center
|
||
style={{
|
||
width: 2,
|
||
height: '60%',
|
||
borderRadius: 'var(--radius-element)',
|
||
backgroundColor: header.column.getIsResizing() ? DT.brand : DT.borderSubtle,
|
||
transition: 'background-color 120ms ease'
|
||
}}
|
||
/>
|
||
</Center>
|
||
)}
|
||
</TableHeaderCell>
|
||
);
|
||
})}
|
||
</TableRow>
|
||
))}
|
||
</TableHeader>
|
||
|
||
<TableBody>
|
||
{(loading1 || countSourceIsLoading) && <TableLoader rows={8} columns={colCount} />}
|
||
|
||
{filteredRows.length === 0 && !loading1 && !countSourceLoading && (
|
||
<TableRow>
|
||
{/* Same fix as orders.js: in `fill` mode the row/cell
|
||
only shrink-to-fit their content — TableScroll's
|
||
outer div caps at maxHeight but doesn't stretch to
|
||
it — so a lone empty-state row sat at the top of a
|
||
lot of leftover space instead of centering in it. */}
|
||
<TableCell colSpan={colCount}>
|
||
<Center style={{ minHeight: 420 }}>
|
||
<EmptyState title="No deliveries to show" description={emptyDescription} icon={<MdInventory2 size={28} />} />
|
||
</Center>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
|
||
{filteredRows.length === 0 && countSourceLoading && (
|
||
<TableRow>
|
||
<TableCell colSpan={colCount} style={{ textAlign: 'center', padding: 32 }}>
|
||
<LoaderWithImage />
|
||
<Text type="supporting" color="secondary" style={{ display: 'block', marginTop: 8 }}>
|
||
Loading deliveries…
|
||
</Text>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
|
||
{table.getRowModel().rows.map((row) => {
|
||
const isOpen = productCollapse?.orderid === row.original?.orderid;
|
||
return (
|
||
<Fragment key={row.id}>
|
||
<TableRow style={{ backgroundColor: row.getIsSelected() ? tint(BRAND) : undefined }}>
|
||
{row.getVisibleCells().map((cell) => (
|
||
<TableCell key={cell.id} style={{ textAlign: cell.column.columnDef.meta?.align }}>
|
||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
{isOpen && (
|
||
<TableRow>
|
||
<TableCell colSpan={colCount} style={{ padding: 0, backgroundColor: DT.surfaceAlt }}>
|
||
{renderProductDetails()}
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</Fragment>
|
||
);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</TableScroll>
|
||
</VStack>
|
||
|
||
{/* Page-number Pagination replaces the scroll-to-reveal sentinel —
|
||
same swap orders.js made. filteredRows is the full, already-
|
||
loaded dataset (see countSourceRows' auto-drain above), so this
|
||
paginates an in-memory array, not a server page. */}
|
||
{filteredRows.length > 0 && (
|
||
<>
|
||
<Divider />
|
||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap" paddingInline={4} paddingBlock={2}>
|
||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||
{`${tablePagination.pageIndex * tablePagination.pageSize + 1}–${Math.min(
|
||
(tablePagination.pageIndex + 1) * tablePagination.pageSize,
|
||
filteredRows.length
|
||
)} of ${filteredRows.length} deliver${filteredRows.length === 1 ? 'y' : 'ies'}`}
|
||
</Text>
|
||
<Pagination
|
||
page={tablePagination.pageIndex + 1}
|
||
onChange={(nextPage) => table.setPageIndex(nextPage - 1)}
|
||
totalItems={filteredRows.length}
|
||
pageSize={tablePagination.pageSize}
|
||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||
onPageSizeChange={(size) => table.setPageSize(size)}
|
||
size="sm"
|
||
label="Deliveries pagination"
|
||
/>
|
||
</HStack>
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</DataCard>
|
||
</PageShell>
|
||
|
||
{/* =============================== || cancel dialog || =============================== */}
|
||
<Dialog isOpen={cancelDeliveryOpen} onOpenChange={(next) => !next && setCancelDeliveryOpen(false)} width={420} purpose="required">
|
||
<Layout
|
||
header={
|
||
<DialogHeader
|
||
title="Cancel Delivery?"
|
||
subtitle="This action will cancel the delivery and notify the rider. Please share a brief reason."
|
||
onOpenChange={(next) => !next && setCancelDeliveryOpen(false)}
|
||
startContent={
|
||
<AccentAvatar color="#ef4444" selected size={36}>
|
||
<MdCancel size={18} />
|
||
</AccentAvatar>
|
||
}
|
||
/>
|
||
}
|
||
content={
|
||
<LayoutContent padding={4}>
|
||
<TextArea
|
||
label="Reason for cancellation"
|
||
isLabelHidden
|
||
placeholder="Reason for cancellation"
|
||
value={cancelFeed}
|
||
onChange={setCancelFeed}
|
||
/>
|
||
</LayoutContent>
|
||
}
|
||
footer={
|
||
<LayoutFooter hasDivider>
|
||
<HStack gap={2} justify="end">
|
||
<Button label="Keep Delivery" variant="secondary" onClick={() => setCancelDeliveryOpen(false)} />
|
||
<Button
|
||
label="Yes, Cancel"
|
||
variant="destructive"
|
||
onClick={() => {
|
||
if (cancelFeed == '') {
|
||
opentoast('Enter FeedBack', 'error');
|
||
} else {
|
||
cancelDelivery({ selectedRow, cancelFeed });
|
||
}
|
||
cancelridernotification();
|
||
}}
|
||
/>
|
||
</HStack>
|
||
</LayoutFooter>
|
||
}
|
||
/>
|
||
</Dialog>
|
||
|
||
{/* =============================== || change rider dialog || =============================== */}
|
||
<Dialog isOpen={changeDialogOpen} onOpenChange={(next) => !next && setChangeDialogOpen(false)} width={480} purpose="form">
|
||
<Layout
|
||
header={
|
||
<DialogHeader
|
||
title="Change Rider"
|
||
subtitle="Assign this delivery to a different rider"
|
||
onOpenChange={(next) => !next && setChangeDialogOpen(false)}
|
||
startContent={
|
||
<AccentAvatar color="#8b5cf6" selected size={36}>
|
||
<MdDirectionsBike size={18} />
|
||
</AccentAvatar>
|
||
}
|
||
/>
|
||
}
|
||
content={
|
||
<LayoutContent padding={4}>
|
||
<Selector
|
||
label="Rider"
|
||
isLabelHidden
|
||
options={riderOptions}
|
||
value={selectedRider ? String(selectedRider.userid) : ''}
|
||
onChange={(v) => {
|
||
const rider = (ridersList || []).find((r) => String(r.userid) === v) || null;
|
||
setSelectedRider(rider);
|
||
logger.debug('Rider selected in dropdown:', rider ? `${rider.firstname} ${rider.lastname}` : 'None');
|
||
}}
|
||
placeholder="Choose Rider"
|
||
hasSearch
|
||
/>
|
||
</LayoutContent>
|
||
}
|
||
footer={
|
||
<LayoutFooter hasDivider>
|
||
<HStack gap={2} justify="end">
|
||
<Button label="Cancel" variant="secondary" onClick={() => setChangeDialogOpen(false)} />
|
||
<Button
|
||
label="Change Rider"
|
||
variant="primary"
|
||
isLoading={loading1}
|
||
onClick={() => {
|
||
setLoading1(true);
|
||
logger.info(
|
||
`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${
|
||
selectedRider?.displayname || selectedRider?.authname
|
||
}`
|
||
);
|
||
changeRiderMutation.mutate({ selectedRider, selectedRow });
|
||
}}
|
||
/>
|
||
</HStack>
|
||
</LayoutFooter>
|
||
}
|
||
/>
|
||
</Dialog>
|
||
|
||
{/* =============================== || Date filter Dialog || =============================== */}
|
||
<Dialog isOpen={open} onOpenChange={(next) => !next && setOpen(false)} width={640} purpose="form">
|
||
<DialogHeader
|
||
title="Date Range"
|
||
subtitle="Pick a window to filter the deliveries"
|
||
onOpenChange={(next) => !next && setOpen(false)}
|
||
/>
|
||
<VStack gap={4} padding={4}>
|
||
<DateRangePicker
|
||
open={open}
|
||
toggle={() => setOpen(!open)}
|
||
id="daterange1"
|
||
onChange={(range) => {
|
||
if (range.label === 'All') {
|
||
setStartdate('');
|
||
setEnddate('');
|
||
setOpen(false);
|
||
} else {
|
||
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
|
||
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
|
||
}
|
||
}}
|
||
definedRanges={[
|
||
{ label: 'Today', startDate: new Date(), endDate: new Date() },
|
||
{ label: 'Yesterday', startDate: addDays(new Date(), -1), endDate: addDays(new Date(), -1) },
|
||
{ label: 'Tomorrow', startDate: addDays(new Date(), +1), endDate: addDays(new Date(), +1) },
|
||
{ label: 'This Week', startDate: startOfWeek(new Date()), endDate: endOfWeek(new Date()) },
|
||
{ label: 'Last Week', startDate: startOfWeek(addWeeks(new Date(), -1)), endDate: endOfWeek(addWeeks(new Date(), -1)) },
|
||
{ label: 'Last 7 Days', startDate: addWeeks(new Date(), -1), endDate: new Date() },
|
||
{ label: 'This Month', startDate: startOfMonth(new Date()), endDate: endOfMonth(new Date()) },
|
||
{ label: 'Last Month', startDate: startOfMonth(addMonths(new Date(), -1)), endDate: endOfMonth(addMonths(new Date(), -1)) }
|
||
]}
|
||
/>
|
||
|
||
<HStack justify="end">
|
||
<Button label="Apply" variant="primary" onClick={okclicked} />
|
||
</HStack>
|
||
</VStack>
|
||
</Dialog>
|
||
|
||
{/* =============================== || Update Delivery Dialog || =============================== */}
|
||
<Dialog isOpen={dialogopen} onOpenChange={(next) => !next && dialogclose()} width={560} purpose="form">
|
||
<Layout
|
||
header={
|
||
<DialogHeader
|
||
title="Update Delivery Status"
|
||
subtitle={currentorder?.orderid ? `Order #${currentorder.orderid}` : 'Modify delivery details'}
|
||
onOpenChange={(next) => !next && dialogclose()}
|
||
startContent={
|
||
<AccentAvatar color="#10b981" selected size={36}>
|
||
<MdCheckCircle size={18} />
|
||
</AccentAvatar>
|
||
}
|
||
/>
|
||
}
|
||
content={
|
||
<LayoutContent padding={4}>
|
||
<VStack gap={3} padding={0}>
|
||
<Grid columns={{ minWidth: 200, repeat: 'fit' }} gap={2}>
|
||
<NumberInput label="KMs" value={kms === '' ? undefined : Number(kms)} onChange={(v) => setKms(v ?? '')} />
|
||
<NumberInput
|
||
label="Actual KMs"
|
||
value={cumulativekms === '' || cumulativekms == null ? undefined : Number(cumulativekms)}
|
||
onChange={(v) => setCumulativeKms(v ?? '')}
|
||
/>
|
||
<TextInput label="Delivery Latitude" value={String(deliverylat ?? '')} onChange={setDeliverylat} />
|
||
<TextInput label="Delivery Longitude" value={String(deliverylong ?? '')} onChange={setDeliverylong} />
|
||
</Grid>
|
||
<NumberInput
|
||
label="Amount"
|
||
value={deliveryamount === '' || deliveryamount == null ? undefined : Number(deliveryamount)}
|
||
onChange={(v) => setDeliveryamount(v ?? '')}
|
||
/>
|
||
<Selector
|
||
label="Status"
|
||
// 'picked' has to be here even though nothing sets it from
|
||
// this dialog: the dialog opens with the row's CURRENT status
|
||
// selected, and Converted_To_Consignment now classifies as
|
||
// picked — an option list without it would open blank on
|
||
// exactly the rows this dialog is reachable from (it needs a
|
||
// consignmentid, which is what being picked means).
|
||
options={['pending', 'accepted', 'started', 'arrived', 'picked', 'delivered', 'cancelled'].map((s) => ({
|
||
value: s,
|
||
label: getStatusMeta(s).label
|
||
}))}
|
||
value={updateStatus}
|
||
onChange={setUpdateStatus}
|
||
/>
|
||
<TextArea label="Notes" value={notes || ''} onChange={setNotes} />
|
||
</VStack>
|
||
</LayoutContent>
|
||
}
|
||
footer={
|
||
<LayoutFooter hasDivider>
|
||
<HStack gap={2} justify="end">
|
||
<Button label="Close" variant="secondary" onClick={dialogclose} />
|
||
<Button
|
||
label="Update"
|
||
variant="primary"
|
||
isLoading={updateDeliveryMutation.isPending}
|
||
onClick={() => {
|
||
if (!kms) {
|
||
opentoast('Fill Kms', 'warning');
|
||
} else if (!cumulativekms) {
|
||
opentoast('Fill Actual Kms', 'warning');
|
||
} else if (!deliverylat) {
|
||
opentoast('Fill Delivery Latitude', 'warning');
|
||
} else if (!deliverylong) {
|
||
opentoast('Fill Delivery Longitude', 'warning');
|
||
} else if (!deliveryamount) {
|
||
opentoast('Fill Delivery Amount', 'warning');
|
||
} else {
|
||
updateDeliveryMutation.mutate({
|
||
deliveryid: currentorder.deliveryid,
|
||
consignmentid: currentorder.consignmentid,
|
||
orderheaderid: currentorder.orderheaderid,
|
||
orderstatus: updateStatus,
|
||
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||
deliverylat,
|
||
deliverylong,
|
||
kms,
|
||
cumulativekms,
|
||
deliveryamt: parseFloat(deliveryamount),
|
||
notes,
|
||
userid: +selectedRow.userid
|
||
});
|
||
}
|
||
}}
|
||
/>
|
||
</HStack>
|
||
</LayoutFooter>
|
||
}
|
||
/>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default Deliveries;
|