diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9885902 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(node -e \"require\\('@babel/parser'\\).parse\\(require\\('fs'\\).readFileSync\\('src/pages/nearle/reports/orderSummary.js','utf8'\\), { sourceType: 'module', plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'objectRestSpread'] }\\); console.log\\('OK'\\)\")", + "Bash(node -e \"require\\('@babel/parser'\\).parse\\(require\\('fs'\\).readFileSync\\('src/pages/nearle/reports/ridersummary.js','utf8'\\), { sourceType: 'module', plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'objectRestSpread'] }\\); console.log\\('OK'\\)\")", + "Bash(node -e \"require\\('@babel/parser'\\).parse\\(require\\('fs'\\).readFileSync\\('src/pages/nearle/invoice/invoice.js','utf8'\\), { sourceType: 'module', plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'objectRestSpread'] }\\); console.log\\('OK'\\)\")" + ] + } +} diff --git a/src/pages/nearle/_shared/ordersDesign.js b/src/pages/nearle/_shared/ordersDesign.js new file mode 100644 index 0000000..99900a9 --- /dev/null +++ b/src/pages/nearle/_shared/ordersDesign.js @@ -0,0 +1,341 @@ +// ============================================================================ +// ordersDesign.js — shared design-system primitives for the operator pages. +// +// Source of truth for the look-and-feel established on the Orders Details +// screen (src/pages/nearle/reports/ordersDetails.js). All other report and +// list pages import from here so the entire admin panel reads as one product. +// +// Exports: +// DT, BRAND, BRAND_LIGHT — palette tokens +// tint / soft / ring / edge — alpha helpers (08 / 18 / 26 / 55 hex) +// formatNumberToRupees — INR currency formatter +// STATUS_META — semantic per-status meta (label/color/icon) +// StatusBadge — filled pill, white text on solid color +// TimelineCell — time-dominant (bold) / date-secondary (muted) +// MetricPill — small money/number pill for table cells +// SoftPaper — autocomplete dropdown surface +// AccentAvatar — colored circular icon avatar +// pillFieldSx — rounded pill text-field styling +// PrimaryButtonSx / SecondaryButtonSx — unified 32-px action-button system +// ============================================================================ + +import React from 'react'; +import { Avatar, Box, Paper, Stack, Typography } from '@mui/material'; +import { + MdLocalShipping, + MdHourglassEmpty, + MdCheckCircle, + MdCancel, + MdAccessTime, + MdHistoryToggleOff, + MdAssignmentTurnedIn +} from 'react-icons/md'; +import dayjs from 'dayjs'; + +// ---------------------------------------------------------------------------- +// Design tokens +// ---------------------------------------------------------------------------- +export const DT = { + radiusPill: 999, + radiusCard: 16, + shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', + shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)', + shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)', + textPrimary: '#0f172a', + textSecondary: '#64748b', + textMuted: '#94a3b8', + borderSubtle: '#e2e8f0', + divider: '#f1f5f9', + surface: '#ffffff', + surfaceAlt: '#f8fafc' +}; + +export const BRAND = '#662582'; +export const BRAND_LIGHT = '#9255AB'; + +const dtA = (c, suffix) => `${c}${suffix}`; +export const tint = (c) => dtA(c, '08'); +export const soft = (c) => dtA(c, '18'); +export const ring = (c) => dtA(c, '26'); +export const edge = (c) => dtA(c, '55'); + +// ---------------------------------------------------------------------------- +// Money formatter +// ---------------------------------------------------------------------------- +export function formatNumberToRupees(value) { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + minimumFractionDigits: 2 + }).format(value || 0); +} + +// ---------------------------------------------------------------------------- +// Status meta — colors per brand standard: +// green=delivered, amber=pending, blue=created/processing, +// red=cancelled, dark-red=failed, purple=on-hold. +// ---------------------------------------------------------------------------- +export const STATUS_META = { + created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping }, + pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, + accepted: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn }, + ready: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn }, + arrived: { label: 'Arrived', color: '#06b6d4', icon: MdCheckCircle }, + picked: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping }, + active: { label: 'Active', color: '#0ea5e9', icon: MdLocalShipping }, + modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, + confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, + processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime }, + onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, + 'on hold': { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, + closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle }, + completed: { label: 'Completed', color: '#10b981', icon: MdCheckCircle }, + delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + skipped: { label: 'Skipped', color: '#f97316', icon: MdCancel }, + failed: { label: 'Failed', color: '#991b1b', icon: MdCancel }, + cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } +}; + +// ---------------------------------------------------------------------------- +// StatusBadge — filled pill, white text on solid color, high-contrast. +// ---------------------------------------------------------------------------- +export const StatusBadge = ({ status, minWidth = 86 }) => { + if (!status) return null; + const meta = STATUS_META[String(status).toLowerCase()] || { + label: status, + color: DT.textMuted, + icon: MdHistoryToggleOff + }; + const Icon = meta.icon; + return ( + + {meta.label} + + ); +}; + +// ---------------------------------------------------------------------------- +// TimelineCell — time large/bold/high-contrast, date small/muted/secondary. +// No decorative dot. +// ---------------------------------------------------------------------------- +export const TimelineCell = ({ value, utc: useUtc = false }) => { + if (!value) { + return ( + + ); + } + const d = useUtc ? dayjs(value).utc() : dayjs(value); + return ( + + + {d.format('hh:mm A')} + + + {d.format('DD MMM YYYY')} + + + ); +}; + +// ---------------------------------------------------------------------------- +// MetricPill — small money / number pill used in table cells. +// ---------------------------------------------------------------------------- +export const MetricPill = ({ value, color, icon, isMoney = false }) => { + const n = Number(value); + const display = isMoney ? formatNumberToRupees(n) : Number.isFinite(n) ? n : value || 0; + const isZero = !Number.isFinite(n) || n === 0; + if (isZero) { + return ( + + {display} + + ); + } + return ( + + {icon} + {display} + + ); +}; + +// ---------------------------------------------------------------------------- +// SoftPaper — autocomplete dropdown surface. +// ---------------------------------------------------------------------------- +export const SoftPaper = (props) => ( + +); + +// ---------------------------------------------------------------------------- +// AccentAvatar — colored circular icon used inside filter chips & headers. +// ---------------------------------------------------------------------------- +export const AccentAvatar = ({ color, selected, size = 24, children }) => ( + + {children} + +); + +// ---------------------------------------------------------------------------- +// pillFieldSx — rounded pill style for Autocomplete/TextField inputs. +// ---------------------------------------------------------------------------- +export const pillFieldSx = (color) => ({ + '& .MuiOutlinedInput-root': { + borderRadius: DT.radiusPill + 'px', + bgcolor: tint(color), + fontWeight: 600, + '& fieldset': { borderColor: edge(color), borderWidth: 1.5 }, + '&:hover fieldset': { borderColor: color }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, + '&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 } + } +}); + +// ---------------------------------------------------------------------------- +// Unified action-button styles — both render at 32px height with same typography. +// SecondaryButtonSx → outlined (white bg, neutral border) +// PrimaryButtonSx → filled brand (purple bg, white text) +// ---------------------------------------------------------------------------- +export const SecondaryButtonSx = (active = false) => ({ + height: 32, + px: 1.25, + borderRadius: 1.5, + textTransform: 'none', + fontSize: 12.5, + fontWeight: 700, + letterSpacing: 0.1, + bgcolor: '#fff', + borderColor: active ? edge(BRAND) : DT.borderSubtle, + color: active ? BRAND : DT.textPrimary, + '&:hover': { + bgcolor: '#fff', + borderColor: BRAND, + color: BRAND, + boxShadow: `0 0 0 3px ${ring(BRAND)}` + }, + '&:focus-visible': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } +}); + +export const PrimaryButtonSx = { + height: 32, + minHeight: 32, + px: 1.5, + borderRadius: 1.5, + bgcolor: BRAND, + color: '#fff', + textTransform: 'none', + fontSize: 12.5, + fontWeight: 700, + letterSpacing: 0.1, + boxShadow: `0 2px 6px ${ring(BRAND)}`, + '&:hover': { bgcolor: BRAND_LIGHT, boxShadow: `0 4px 12px ${ring(BRAND)}` }, + '&:focus-visible': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }, + '& .MuiButton-startIcon': { mr: 0.75, '& svg': { fontSize: 16 } } +}; + +// ---------------------------------------------------------------------------- +// PageHeaderShellSx — the gradient header strip used by every operator page. +// Spread onto the outer element. +// ---------------------------------------------------------------------------- +export const PageHeaderShellSx = { + mb: { xs: 1, md: 1.25 }, + px: { xs: 1.5, sm: 2 }, + py: { xs: 1, sm: 1.25 }, + borderRadius: 2, + border: '1px solid', + borderColor: DT.borderSubtle, + background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`, + boxShadow: DT.shadowMd +}; + +// ---------------------------------------------------------------------------- +// CompactKpiCardSx — visual envelope for a compact KPI card. +// Caller supplies the colored left bar via the `accent` arg. +// ---------------------------------------------------------------------------- +export const CompactKpiCardSx = (accentColor) => ({ + position: 'relative', + overflow: 'hidden', + px: { xs: 1.25, sm: 1.5 }, + py: { xs: 0.875, sm: 1.125 }, + borderRadius: 2, + border: '1px solid', + borderColor: DT.borderSubtle, + background: '#fff', + transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s', + '&:hover': { + transform: 'translateY(-1px)', + boxShadow: DT.shadowMd, + borderColor: edge(accentColor) + } +}); + +// MdAccessTime re-exported so callers can use the inline time icon +// without an extra react-icons import. +export { MdAccessTime }; diff --git a/src/pages/nearle/invoice/invoice.js b/src/pages/nearle/invoice/invoice.js index 86df296..368557c 100644 --- a/src/pages/nearle/invoice/invoice.js +++ b/src/pages/nearle/invoice/invoice.js @@ -173,54 +173,57 @@ const Invoice = () => { <> {isloader && } - {/* ============================================= || Header || ============================================= */} + {/* ============================================= || Header (compact) || ============================================= */} - + - + - + Invoices - + - + Live · {INVOICE_STATUS_TABS[value].label} @@ -228,8 +231,8 @@ const Invoice = () => { - {/* ============================================= || KPI Cards || ============================================= */} - + {/* ============================================= || KPI Cards (compact, clickable) || ============================================= */} + {kpiCards.map((item) => { const Icon = item.icon; const active = value === item.idx; @@ -242,15 +245,16 @@ const Invoice = () => { cursor: 'pointer', position: 'relative', overflow: 'hidden', - p: { xs: 1.25, sm: 1.75, md: 2.25 }, - borderRadius: DT.radiusCard / 8, + px: { xs: 1.25, sm: 1.5 }, + py: { xs: 0.875, sm: 1.125 }, + borderRadius: 2, border: '1px solid', borderColor: active ? edge(item.color) : DT.borderSubtle, background: '#fff', - boxShadow: active ? `0 6px 24px ${ring(item.color)}` : 'none', - transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s', + boxShadow: active ? `0 4px 14px ${ring(item.color)}` : 'none', + transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s', '&:hover': { - transform: 'translateY(-3px)', + transform: 'translateY(-1px)', boxShadow: DT.shadowMd, borderColor: edge(item.color) } @@ -261,21 +265,24 @@ const Invoice = () => { position: 'absolute', top: 0, left: 0, - right: 0, - height: 3, - background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)` + bottom: 0, + width: 3, + background: item.color }} /> - - + + {item.label} Invoices @@ -284,9 +291,10 @@ const Invoice = () => { sx={{ fontWeight: 800, color: DT.textPrimary, - lineHeight: 1.1, - fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' } + lineHeight: 1.15, + fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.2rem' } }} + noWrap > {insightdata && insightdata[item.countKey] != null ? ( insightdata[item.countKey] @@ -296,16 +304,17 @@ const Invoice = () => { - + @@ -314,14 +323,14 @@ const Invoice = () => { })} - {/* ============================================= || Status Tabs + Search || ============================================= */} + {/* ============================================= || Status Tabs + Search (compact) || ============================================= */} { - {/* ============================================= || Table || ============================================= */} + {/* ============================================= || Table (dense, sticky header) || ============================================= */} { > { '&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt } }} > - +
@@ -510,11 +520,11 @@ const Invoice = () => { {isloader && filteredList.length === 0 && ( - [0, 1, 2, 3, 4].map((_, idx) => ( + Array.from({ length: 10 }).map((_, idx) => ( {Array.from({ length: 8 }).map((__, ci) => ( - - + + ))} @@ -523,17 +533,50 @@ const Invoice = () => { {!isloader && filteredList.length === 0 && ( - - - - + + + + - + No {INVOICE_STATUS_TABS[value].label.toLowerCase()} invoices - - {search ? 'Try a different invoice number.' : 'Switch tabs above to load invoices.'} + + {search ? 'Try a different invoice number or clear the search.' : 'Switch tabs above to load invoices.'} + {search && ( + setSearch('')} + sx={{ + mt: 0.5, + border: 'none', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: 0.5, + height: 28, + px: 1, + fontSize: 12, + fontWeight: 700, + color: BRAND, + borderRadius: 1.25, + background: 'transparent', + '&:hover': { bgcolor: tint(BRAND) } + }} + > + Clear search + + )} @@ -546,13 +589,17 @@ const Invoice = () => { key={`${item.invoiceno}-${index}`} sx={{ cursor: 'pointer', - transition: 'background-color 0.15s', + transition: 'background-color 0.12s, box-shadow 0.12s', '& td': { borderBottom: `1px solid ${DT.divider}`, - py: { xs: 1, md: 1.25 }, - px: { xs: 1, md: 1.5 } + py: 0.5, + px: 1, + verticalAlign: 'middle' }, - '&:hover': { backgroundColor: DT.surfaceAlt } + '&:hover': { + backgroundColor: tint(BRAND), + boxShadow: `inset 3px 0 0 ${BRAND}` + } }} onClick={() => { setIsLoader(true); @@ -563,16 +610,16 @@ const Invoice = () => { }} > - + {page * rowsPerPage + index + 1} - + {item.tenantname} - + {item.contactperson} @@ -599,31 +646,33 @@ const Invoice = () => { - - - - - {item.transactiondate ? dayjs(item.transactiondate).format('DD MMM YYYY') : '—'} + {item.transactiondate ? ( + + + {dayjs(item.transactiondate).format('hh:mm A')} - - {item.transactiondate ? dayjs(item.transactiondate).format('hh:mm A') : ''} + + {dayjs(item.transactiondate).format('DD MMM YYYY')} - + ) : ( + + )} - - - - - {item.duedate ? dayjs(item.duedate).format('DD MMM YYYY') : '—'} + {item.duedate ? ( + + + {dayjs(item.duedate).format('hh:mm A')} - - {item.duedate ? dayjs(item.duedate).format('hh:mm A') : ''} + + {dayjs(item.duedate).format('DD MMM YYYY')} - + ) : ( + + )} diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js index ee96263..b8d66e1 100644 --- a/src/pages/nearle/orders/orders.js +++ b/src/pages/nearle/orders/orders.js @@ -125,17 +125,21 @@ const pillFieldSx = (color) => ({ } }); -// Semantic per-row status palette. +// Semantic per-row status palette — colors per brand standard: +// green=delivered, amber=pending, blue=created/processing, red=cancelled, +// dark-red=failed, purple=on-hold. const ROW_STATUS_META = { - created: { label: 'Created', color: '#0ea5e9', icon: MdLocalShipping }, + created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping }, pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, - processing: { label: 'Processing', color: BRAND, icon: MdAccessTime }, + processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime }, modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle }, active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping }, + onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle }, delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + failed: { label: 'Failed', color: '#991b1b', icon: MdCancel }, cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } }; @@ -147,6 +151,7 @@ const ORDERS_STATUS_TABS = [ { idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, countKey: 'cancelled' } ]; +// Filled status badge — high-contrast pill (white text on solid color). const StatusBadge = ({ status }) => { const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || { label: status || '—', @@ -160,15 +165,18 @@ const StatusBadge = ({ status }) => { display: 'inline-flex', alignItems: 'center', gap: 0.5, - px: 1, + px: 1.125, py: 0.375, borderRadius: 999, - bgcolor: tint(meta.color), - border: `1px solid ${edge(meta.color)}`, - color: meta.color, + bgcolor: meta.color, + color: '#fff', fontSize: 11, - fontWeight: 800, - whiteSpace: 'nowrap' + fontWeight: 700, + letterSpacing: 0.2, + whiteSpace: 'nowrap', + minWidth: 86, + justifyContent: 'center', + boxShadow: `0 1px 2px ${ring(meta.color)}` }} > {meta.label} @@ -445,13 +453,14 @@ const Orders = () => { )} - {/* ============================================= || Header || ============================================= */} + {/* ============================================= || Header (compact) || ============================================= */} { direction={{ xs: 'column', sm: 'row' }} alignItems={{ xs: 'flex-start', sm: 'center' }} justifyContent="space-between" - spacing={{ xs: 1.5, sm: 2 }} + spacing={{ xs: 1, sm: 1.5 }} > - + - + - + Orders - + - + Live · {locoName} · {datestatus} @@ -563,8 +574,8 @@ const Orders = () => { - {/* ============================================= || KPI Cards || ============================================= */} - + {/* ============================================= || KPI Cards (compact) || ============================================= */} + {kpiCards.map((item) => { const Icon = item.icon; return ( @@ -574,14 +585,15 @@ const Orders = () => { sx={{ position: 'relative', overflow: 'hidden', - p: { xs: 1.25, sm: 1.75, md: 2.25 }, - borderRadius: DT.radiusCard / 8, + px: { xs: 1.25, sm: 1.5 }, + py: { xs: 0.875, sm: 1.125 }, + borderRadius: 2, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', - transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s', + transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s', '&:hover': { - transform: 'translateY(-3px)', + transform: 'translateY(-1px)', boxShadow: DT.shadowMd, borderColor: edge(item.color) } @@ -592,55 +604,59 @@ const Orders = () => { position: 'absolute', top: 0, left: 0, - right: 0, - height: 3, - background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)` + bottom: 0, + width: 3, + background: item.color }} /> - - + + {item.label} - - {item.value === '' ? : item.value} - - {item.percentage != null && ( - - {item.percentage}% + + + {item.value === '' ? : item.value} - )} + {item.percentage != null && item.value !== '' && ( + + {item.percentage}% + + )} + - + @@ -649,13 +665,13 @@ const Orders = () => { })} - {/* ============================================= || Filter Bar || ============================================= */} + {/* ============================================= || Filter Bar (compact) || ============================================= */} { - {/* ============================================= || Status Tabs + Search || ============================================= */} + {/* ============================================= || Status Tabs + Search (compact) || ============================================= */} { - {/* ============================================= || Table || ============================================= */} + {/* ============================================= || Table (dense, sticky header) || ============================================= */} { onScroll={handleScroll} ref={containerRef} sx={{ - maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' }, + minHeight: 320, + maxHeight: { xs: 'calc(100vh - 320px)', md: 'calc(100vh - 290px)' }, overflow: 'auto', '&::-webkit-scrollbar': { width: 10, height: 10 }, '&::-webkit-scrollbar-thumb': { @@ -932,21 +949,21 @@ const Orders = () => { '&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt } }} > -
+
@@ -967,11 +984,11 @@ const Orders = () => { {(isLoadingGetOrders || loading) && rows.length === 0 && - [0, 1, 2, 3, 4, 5].map((_, idx) => ( + Array.from({ length: 10 }).map((_, idx) => ( {Array.from({ length: currentStatus === 'created' ? 11 : 10 }).map((__, ci) => ( - - + + ))} @@ -979,17 +996,45 @@ const Orders = () => { {!isLoadingGetOrders && rows.length === 0 && ( - - - - + + + + - + No {currentStatus} orders - - {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} + + {searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'} + {searchword && ( + + )} @@ -1000,13 +1045,17 @@ const Orders = () => { key={`${row.orderheaderid}-${index}`} sx={{ cursor: 'pointer', - transition: 'background-color 0.15s', + transition: 'background-color 0.12s, box-shadow 0.12s', '& td': { borderBottom: `1px solid ${DT.divider}`, - py: { xs: 1, md: 1.25 }, - px: { xs: 1, md: 1.5 } + py: 0.5, + px: 1, + verticalAlign: 'top' }, - '&:hover': { backgroundColor: DT.surfaceAlt } + '&:hover': { + backgroundColor: tint(BRAND), + boxShadow: `inset 3px 0 0 ${BRAND}` + } }} > @@ -1025,10 +1074,13 @@ const Orders = () => { {row.orderid} - - - - {dayjs(row.pickupslot).format('DD/MM/YYYY')} · {dayjs(row.pickupslot).format('hh:mm A')} + + + + {dayjs(row.pickupslot).format('hh:mm A')} + + + · {dayjs(row.pickupslot).format('DD MMM YY')} @@ -1143,19 +1195,29 @@ const Orders = () => { ))} {rows.length !== 0 && ( - - -
- {isFetchingNextPage ? ( - - ) : hasNextPage ? ( - + + + + {isFetchingNextPage || hasNextPage ? ( + <> + + + Loading more orders… + + ) : ( - - No more orders + + {rows.length} order{rows.length === 1 ? '' : 's'} · End of list )} -
+
)} diff --git a/src/pages/nearle/reports/orderSummary.js b/src/pages/nearle/reports/orderSummary.js index b8edcfc..451ab79 100644 --- a/src/pages/nearle/reports/orderSummary.js +++ b/src/pages/nearle/reports/orderSummary.js @@ -34,7 +34,6 @@ import { MdCheckCircle, MdCancel, MdMyLocation, - MdPlace, MdSearch, MdClear, MdCalendarMonth, @@ -333,13 +332,14 @@ export default function OrdersReport() { {(isLoadingReports || tenantLocationsIsLoading) && } {(isLoadingReports || tenantLocationsIsLoading) && } - {/* ============================================= || Header || ============================================= */} + {/* ============================================= || Header (compact) || ============================================= */} - + - + - + Orders Summary - + - + Live · {locoName} · {datestatus} - + {/* Location picker — chip when single, autocomplete when multiple (matches orders page) */} + {tenantLocations?.length === 1 ? ( setOpen(true)} sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75, px: 1.5, - py: 0.875, + py: 0.75, borderRadius: 999, - cursor: 'pointer', - bgcolor: '#fff', - border: `1.5px solid ${edge('#f59e0b')}`, - color: '#f59e0b', + bgcolor: tint(BRAND), + border: `1.5px solid ${edge(BRAND)}`, + color: BRAND, fontWeight: 800, - fontSize: 12, - transition: 'all 0.18s', - '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } + fontSize: 13 }} > - - {startdate && enddate - ? `${dayjs(startdate).format('DD/MM/YY')} – ${dayjs(enddate).format('DD/MM/YY')}` - : 'All time'} + {tenantLocations[0]?.locationname} - + ) : ( + (o ? `${o.locationname} (${o.suburb || ''})` : '')} + PaperComponent={SoftPaper} + onChange={(event, value) => { + setSelectedLocation(value); + setLocationId(value ? value.locationid : 0); + setLocoName(value ? value.locationname : 'All Locations'); + }} + renderInput={(params) => ( + + + + + + ) + }} + /> + )} + sx={{ width: { xs: '100%', sm: 280 } }} + /> + )} - {/* ============================================= || KPI Cards || ============================================= */} - + {/* ============================================= || KPI Cards (compact) || ============================================= */} + {kpiCards.map((item) => { const Icon = item.icon; return ( @@ -433,14 +460,15 @@ export default function OrdersReport() { sx={{ position: 'relative', overflow: 'hidden', - p: { xs: 1.25, sm: 1.75, md: 2.25 }, - borderRadius: DT.radiusCard / 8, + px: { xs: 1.25, sm: 1.5 }, + py: { xs: 0.875, sm: 1.125 }, + borderRadius: 2, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', - transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s', + transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s', '&:hover': { - transform: 'translateY(-3px)', + transform: 'translateY(-1px)', boxShadow: DT.shadowMd, borderColor: edge(item.color) } @@ -451,24 +479,24 @@ export default function OrdersReport() { position: 'absolute', top: 0, left: 0, - right: 0, - height: 3, - background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)` + bottom: 0, + width: 3, + background: item.color }} /> - - + + {item.label} @@ -477,9 +505,10 @@ export default function OrdersReport() { sx={{ fontWeight: 800, color: DT.textPrimary, - lineHeight: 1.1, - fontSize: { xs: '1.1rem', sm: '1.35rem', md: '1.55rem' } + lineHeight: 1.15, + fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.2rem' } }} + noWrap > {isLoadingReports ? ( @@ -491,16 +520,17 @@ export default function OrdersReport() { - + @@ -509,13 +539,13 @@ export default function OrdersReport() { })} - {/* ============================================= || Filter Bar || ============================================= */} + {/* ============================================= || Filter Bar (compact) || ============================================= */} + {/* Date range + active preset pills (matches orders page) */} + + + setOpen(true)} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: 0.75, + px: 1.25, + py: 0.75, + borderRadius: 999, + cursor: 'pointer', + bgcolor: tint('#f59e0b'), + border: `1.5px solid ${edge('#f59e0b')}`, + color: '#f59e0b', + fontWeight: 800, + fontSize: 12, + transition: 'all 0.18s', + '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` } + }} + > + + {startdate && enddate + ? `${dayjs(startdate).format('DD/MM/YY')} – ${dayjs(enddate).format('DD/MM/YY')}` + : 'All time'} + + + + {datestatus} + + + {/* Search */} - - {/* Location filter */} - {tenantLocations?.length === 1 ? ( - - {tenantLocations[0]?.locationname} - - ) : ( - (o ? `${o.locationname} (${o.suburb || ''})` : '')} - PaperComponent={SoftPaper} - onChange={(event, value) => { - setSelectedLocation(value); - setLocationId(value ? value.locationid : 0); - setLocoName(value ? value.locationname : 'All Locations'); - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - sx={{ width: { xs: '100%', md: 320 } }} - /> - )} @@ -628,7 +653,7 @@ export default function OrdersReport() { - - + + {row.locationname} - + Id : {row.locationid} @@ -918,8 +944,8 @@ export default function OrdersReport() { - - + + @@ -975,7 +1001,7 @@ export default function OrdersReport() { @@ -1090,8 +1116,8 @@ export default function OrdersReport() { backgroundColor: tint(BRAND), borderTop: `2px solid ${edge(BRAND)}`, borderBottom: 'none', - py: 1.25, - px: 1.5 + py: 0.75, + px: 1 }, '& td.band-o': { backgroundColor: soft(C_ORDERS) }, '& td.band-d': { backgroundColor: soft(C_DELIVERIES) }, diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js index 361f6b2..619e323 100644 --- a/src/pages/nearle/reports/ordersDetails.js +++ b/src/pages/nearle/reports/ordersDetails.js @@ -11,7 +11,6 @@ import { CircularProgress, Dialog, DialogContent, - Divider, Grid, IconButton, InputBase, @@ -27,7 +26,9 @@ import { Typography, Autocomplete, TextField, - Skeleton + Skeleton, + Menu, + Switch } from '@mui/material'; import { MdLocalShipping, @@ -48,7 +49,11 @@ import { MdInsights, MdLocalOffer, MdAssignmentTurnedIn, - MdFilterList + MdFilterList, + MdViewWeek, + MdRestartAlt, + MdLock, + MdDoneAll } from 'react-icons/md'; import dayjs from 'dayjs'; @@ -90,15 +95,21 @@ const BRAND = '#662582'; const BRAND_LIGHT = '#9255AB'; // Semantic per-status palette — drives both status badges and the timeline cells. +// Colors per brand standard: green=delivered, amber=pending, blue=processing, +// red=cancelled, dark-red=failed, purple=on-hold. const STATUS_META = { - created: { label: 'Created', color: '#0ea5e9', icon: MdLocalShipping }, + created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping }, pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, accepted: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn }, arrived: { label: 'Arrived', color: '#06b6d4', icon: MdCheckCircle }, picked: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping }, - active: { label: 'Active', color: '#14b8a6', icon: MdLocalShipping }, + active: { label: 'Active', color: '#0ea5e9', icon: MdLocalShipping }, + processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime }, + onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, + 'on hold': { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, skipped: { label: 'Skipped', color: '#f97316', icon: MdCancel }, + failed: { label: 'Failed', color: '#991b1b', icon: MdCancel }, cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } }; @@ -154,6 +165,7 @@ const pillFieldSx = (color) => ({ } }); +// Filled status badge — high-contrast pill (white text on solid color). const StatusBadge = ({ status }) => { const meta = STATUS_META[String(status || '').toLowerCase()] || { label: status || '—', @@ -167,17 +179,18 @@ const StatusBadge = ({ status }) => { display: 'inline-flex', alignItems: 'center', gap: 0.5, - px: 1, + px: 1.125, py: 0.375, borderRadius: 999, - bgcolor: tint(meta.color), - border: `1px solid ${edge(meta.color)}`, - color: meta.color, + bgcolor: meta.color, + color: '#fff', fontSize: 11, - fontWeight: 800, + fontWeight: 700, + letterSpacing: 0.2, whiteSpace: 'nowrap', - minWidth: 80, - justifyContent: 'center' + minWidth: 86, + justifyContent: 'center', + boxShadow: `0 1px 2px ${ring(meta.color)}` }} > {meta.label} @@ -219,35 +232,41 @@ const MetricPill = ({ value, color, icon, isMoney = false }) => { ); }; -// Compact timeline cell — shows event date+time with a coloured leading dot. -const TimelineCell = ({ value, color }) => { +// Timeline cell — time dominant (bold/high-contrast), date secondary (muted). +// No decorative dot; the column header already names the event. +const TimelineCell = ({ value }) => { if (!value) { return ( - + ); } return ( - - + - - - {dayjs(value).format('DD/MM/YYYY')} - - - {dayjs(value).format('hh:mm A')} - - + noWrap + > + {dayjs(value).format('hh:mm A')} + + + {dayjs(value).format('DD MMM YYYY')} + ); }; @@ -285,6 +304,58 @@ export default function OrdersDetails() { const [totalAmount, settotalAmount] = useState(0); const [searchword, setSearchword] = useState(''); + // Density toggle — operators choose how many rows to surface per screen. + // Default 'compact' (rows ≈ 32px) so the table starts dense like Linear / Vercel. + const [density, setDensity] = useState(() => { + try { return localStorage.getItem('ordersDetails.density') || 'compact'; } catch { return 'compact'; } + }); + const isCompact = density === 'compact'; + const rowPadY = isCompact ? 0.5 : 1; + useEffect(() => { + try { localStorage.setItem('ordersDetails.density', density); } catch {} + }, [density]); + + // Column visibility — eliminates horizontal scroll on smaller desktops by letting + // operators hide columns they don't need. Persists per browser. + const ALL_COLUMNS = [ + { key: 'index', label: '#', group: 'Core', required: true, defaultVisible: true, width: 36 }, + { key: 'location', label: 'Location / Order', group: 'Core', required: true, defaultVisible: true, minWidth: 150 }, + { key: 'pickup', label: 'Pickup', group: 'Core', defaultVisible: true, minWidth: 140 }, + { key: 'drop', label: 'Drop', group: 'Core', defaultVisible: true, minWidth: 140 }, + { key: 'status', label: 'Status', group: 'Core', required: true, defaultVisible: true, width: 110 }, + { key: 'assigned', label: 'Assigned', group: 'Lifecycle', defaultVisible: true, minWidth: 110 }, + { key: 'accepted', label: 'Accepted', group: 'Lifecycle', defaultVisible: false, minWidth: 90 }, + { key: 'arrived', label: 'Arrived', group: 'Lifecycle', defaultVisible: false, minWidth: 90 }, + { key: 'picked', label: 'Picked', group: 'Lifecycle', defaultVisible: true, minWidth: 90 }, + { key: 'delivered', label: 'Delivered', group: 'Lifecycle', defaultVisible: true, minWidth: 90 }, + { key: 'cancelled', label: 'Cancelled', group: 'Lifecycle', defaultVisible: false, minWidth: 90 }, + { key: 'kms', label: 'Kms', group: 'Metrics', defaultVisible: true, width: 70, align: 'center' }, + { key: 'charges', label: 'Charges', group: 'Metrics', defaultVisible: true, width: 100, align: 'right' } + ]; + const COLUMN_GROUPS = ['Core', 'Lifecycle', 'Metrics']; + const COLUMN_DEFAULTS = ALL_COLUMNS.reduce((acc, c) => ({ ...acc, [c.key]: c.defaultVisible }), {}); + const [colVis, setColVis] = useState(() => { + try { + const saved = JSON.parse(localStorage.getItem('ordersDetails.cols') || 'null'); + return saved && typeof saved === 'object' ? { ...COLUMN_DEFAULTS, ...saved } : COLUMN_DEFAULTS; + } catch { return COLUMN_DEFAULTS; } + }); + useEffect(() => { + try { localStorage.setItem('ordersDetails.cols', JSON.stringify(colVis)); } catch {} + }, [colVis]); + const isVisible = (key) => { + const col = ALL_COLUMNS.find((c) => c.key === key); + return col?.required || colVis[key] !== false; + }; + const visibleCount = ALL_COLUMNS.filter((c) => isVisible(c.key)).length; + const hiddenCount = ALL_COLUMNS.length - visibleCount; + + const [colMenuAnchor, setColMenuAnchor] = useState(null); + const toggleCol = (key) => setColVis((prev) => ({ ...prev, [key]: !(prev[key] !== false) })); + const resetCols = () => setColVis(COLUMN_DEFAULTS); + const showAllCols = () => setColVis(ALL_COLUMNS.reduce((acc, c) => ({ ...acc, [c.key]: true }), {})); + const isDefaultCols = ALL_COLUMNS.every((c) => isVisible(c.key) === !!c.defaultVisible); + const [riderCoordinates, setRiderCoordinates] = useState([]); const [riderStart, setRiderStart] = useState(); const [riderEnd, setRiderEnd] = useState(); @@ -470,26 +541,27 @@ export default function OrdersDetails() { locationcontactno: order.locationcontactno })); - // KPI tiles row. + // KPI tiles row — clearer financial terminology with tooltips. const kpiCards = [ - { key: 'total', label: `${currentStatus === 'All' ? 'Total' : currentStatus} Orders`, color: BRAND, icon: MdInsights, value: statusCount }, - { key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, value: deliverycount?.pending ?? 0 }, - { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, value: deliverycount?.delivered ?? 0 }, - { key: 'charges', label: 'Total Charges', color: '#6366f1', icon: MdLocalOffer, value: totalCharge, isMoney: true }, - { key: 'amount', label: 'Total Amount', color: '#10b981', icon: MdCurrencyRupee, value: totalAmount, isMoney: true } + { key: 'total', label: `${currentStatus === 'All' ? 'Total' : currentStatus} Orders`, color: BRAND, icon: MdInsights, value: statusCount, hint: 'Orders matching the current filters.' }, + { key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, value: deliverycount?.pending ?? 0, hint: 'Orders awaiting rider assignment or pickup.' }, + { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, value: deliverycount?.delivered ?? 0, hint: 'Successfully delivered orders.' }, + { key: 'charges', label: 'Delivery Charges', color: '#6366f1', icon: MdLocalOffer, value: totalCharge, isMoney: true, hint: 'Total platform / delivery fees billed across the filtered orders.' }, + { key: 'amount', label: 'Order Value', color: '#10b981', icon: MdCurrencyRupee, value: totalAmount, isMoney: true, hint: 'Total customer order value across the filtered orders.' } ]; return ( <> {isLoadingOrderDetails && } - {/* ============================================= || Header || ============================================= */} + {/* ============================================= || Header (compact, standardized actions) || ============================================= */} - + - + - + Orders Details - + - + Live · {locoName} · {datestatus} - - - setOpen(true)} + {/* Action bar — unified button system: 32px height, 8px radius, identical typography */} + + + + + + + + + + + + + {/* Download — CSVExport already renders its own Button; style it directly so + only ONE button paints in the action bar. Matches the contained brand look. */} div, & > a, & > button': { color: BRAND, fontWeight: 700 } + '& a': { textDecoration: 'none', lineHeight: 0 } }} > - {/* ============================================= || KPI Cards || ============================================= */} - + {/* ============================================= || KPI Cards (compact) || ============================================= */} + {kpiCards.map((item) => { const Icon = item.icon; return ( - - - + + - - - - {item.label} - - - {isLoadingOrderDetails && !item.value ? ( - - ) : item.isMoney ? ( - formatNumberToRupees(item.value) - ) : ( - item.value ?? 0 - )} - - - + - - - - + /> + + + + {item.label} + + + {isLoadingOrderDetails && !item.value ? ( + + ) : item.isMoney ? ( + formatNumberToRupees(item.value) + ) : ( + item.value ?? 0 + )} + + + + + + + + ); })} - {/* ============================================= || Filter Bar || ============================================= */} + {/* ============================================= || Filter Bar (compact) || ============================================= */} - + {/* Location filter */} {tenantLocations.length === 1 ? ( @@ -835,12 +1017,12 @@ export default function OrdersDetails() { - {/* ============================================= || Table || ============================================= */} + {/* ============================================= || Table (dense, sticky header, no h-scroll) || ============================================= */} -
+
isVisible(c.key)) + .reduce((acc, c) => acc + (c.width || c.minWidth || 100), 0), + tableLayout: 'auto' + }} + > - # - Location - Pickup - Drop - Status - Assigned - Accepted - Arrived - Picked - Delivered - Cancelled - Kms - Charges + {isVisible('index') && #} + {isVisible('location') && Location / Order} + {isVisible('pickup') && Pickup} + {isVisible('drop') && Drop} + {isVisible('status') && Status} + {isVisible('assigned') && Assigned} + {isVisible('accepted') && Accepted} + {isVisible('arrived') && Arrived} + {isVisible('picked') && Picked} + {isVisible('delivered') && Delivered} + {isVisible('cancelled') && Cancelled} + {isVisible('kms') && Kms} + {isVisible('charges') && Charges} {rows?.length === 0 && !isLoadingOrderDetails && ( - - - - + + + + - - No order details + + No orders match these filters - - {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} + + {searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'} + {searchword && ( + + )} @@ -918,11 +1140,11 @@ export default function OrdersDetails() { {isLoadingOrderDetails && rows.length === 0 && - [0, 1, 2, 3, 4].map((_, idx) => ( + Array.from({ length: 10 }).map((_, idx) => ( - {Array.from({ length: 13 }).map((__, ci) => ( - - + {Array.from({ length: visibleCount }).map((__, ci) => ( + + ))} @@ -933,220 +1155,390 @@ export default function OrdersDetails() { key={`${row.orderid}-${index}`} sx={{ cursor: 'pointer', - transition: 'background-color 0.15s', + transition: 'background-color 0.12s, box-shadow 0.12s', '& td': { borderBottom: `1px solid ${DT.divider}`, - py: { xs: 1, md: 1.25 }, - px: { xs: 1, md: 1.5 } + py: rowPadY, + px: 1, + verticalAlign: 'top' }, - '&:hover': { backgroundColor: DT.surfaceAlt } + '&:hover': { + backgroundColor: tint(BRAND), + boxShadow: `inset 3px 0 0 ${BRAND}` + } }} > - - {index + 1} - + {isVisible('index') && ( + + {index + 1} + + )} {/* Location */} - - - {row.locationname} - - - - {row.orderid} + {isVisible('location') && ( + + + {row.locationname} - - - - - - {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')} ·{' '} - {dayjs(row.deliverydate).utc().format('hh:mm A')} + + + {row.orderid} - - - + + + + + + {dayjs(row.deliverydate).utc().format('hh:mm A')} + + + · {dayjs(row.deliverydate).utc().format('DD MMM YY')} + + + + + )} {/* Pickup */} - - - - {row.pickupcustomer} - - - {row.pickupcontactno} - - - - {row.pickupsuburb || - row.pickuplocation || - (row.Pickupaddress ? `${row.Pickupaddress.slice(0, 20)}…` : '—')} + {isVisible('pickup') && ( + + + + {row.pickupcustomer} - - - + + {row.pickupcontactno} + + + + {row.pickupsuburb || + row.pickuplocation || + (row.Pickupaddress ? `${row.Pickupaddress.slice(0, 20)}…` : '—')} + + + + + )} {/* Drop */} - - - - {row.deliverycustomer} - - - {row.deliverycontactno} - - - - {row.deliverysuburb || - row.deliverylocation || - (row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')} + {isVisible('drop') && ( + + + + {row.deliverycustomer} - - - + + {row.deliverycontactno} + + + + {row.deliverysuburb || + row.deliverylocation || + (row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')} + + + + + )} {/* Order Status */} - - - + {isVisible('status') && ( + + + + )} {/* Assigned */} - - - {row.ridername && ( - - {row.ridername} - - )} - - - + {isVisible('assigned') && ( + + + {row.ridername && ( + + {row.ridername} + + )} + + + + )} {/* Accepted */} - - - + {isVisible('accepted') && ( + + + + )} {/* Arrived */} - - - + {isVisible('arrived') && ( + + + + )} {/* Picked */} - - - + {isVisible('picked') && ( + + + + )} {/* Delivered */} - - - + {isVisible('delivered') && ( + + + + )} {/* Cancelled */} - - - + {isVisible('cancelled') && ( + + + + )} {/* Kms */} - - } - /> - + {isVisible('kms') && ( + + } + /> + + )} {/* Charges */} - - } - isMoney - /> - + {isVisible('charges') && ( + + } + isMoney + /> + + )} ))} {rows.length !== 0 && ( - - -
- {isFetchingNextPage ? ( - - ) : hasNextPage ? ( - + + + + {isFetchingNextPage || hasNextPage ? ( + <> + + + Loading more orders… + + ) : ( - - No more orders + + {rows.length} order{rows.length === 1 ? '' : 's'} · End of list )} -
+
)}
+
- - - {/* ===================== || Footer totals strip || ===================== */} - setColMenuAnchor(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + slotProps={{ + paper: { + sx: { + mt: 0.75, + width: 300, + borderRadius: 2.5, + border: `1px solid ${DT.borderSubtle}`, + boxShadow: DT.shadowPop, + overflow: 'hidden' + } + } + }} + MenuListProps={{ sx: { py: 0 } }} + > + {/* Header — title, live count, quick actions */} + - - Total Charges · {formatNumberToRupees(totalCharge)} - - - Total Amount · {formatNumberToRupees(totalAmount)} - - -
+ + + + + + + + Columns + + + {visibleCount} of {ALL_COLUMNS.length} shown + + + + + + + + + + + + {/* Grouped, switch-driven column list */} + + {COLUMN_GROUPS.map((group) => { + const cols = ALL_COLUMNS.filter((c) => c.group === group); + const shownInGroup = cols.filter((c) => isVisible(c.key)).length; + return ( + + + + {group} + + + {shownInGroup}/{cols.length} + + + + {cols.map((c) => { + const checked = isVisible(c.key); + const disabled = !!c.required; + return ( + !disabled && toggleCol(c.key)} + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 1, + mx: 0.5, + px: 1, + py: 0.375, + borderRadius: 1.5, + cursor: disabled ? 'default' : 'pointer', + bgcolor: checked ? tint(BRAND) : 'transparent', + border: `1px solid ${checked ? edge(BRAND) : 'transparent'}`, + transition: 'background-color 0.15s, border-color 0.15s', + '&:hover': { bgcolor: disabled ? (checked ? tint(BRAND) : 'transparent') : soft(BRAND) } + }} + > + + + {c.label} + + {disabled && ( + + + + + + )} + + e.stopPropagation()} + onChange={() => !disabled && toggleCol(c.key)} + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { color: BRAND }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: BRAND, opacity: 1 }, + '& .MuiSwitch-switchBase.Mui-disabled.Mui-checked': { color: BRAND_LIGHT }, + '& .MuiSwitch-switchBase.Mui-disabled.Mui-checked + .MuiSwitch-track': { backgroundColor: BRAND_LIGHT, opacity: 0.6 } + }} + /> + + ); + })} + + ); + })} + + {/* ============================================= || Date Filter Dialog || ============================================= */} setOpen(false)} PaperProps={{ sx: { borderRadius: 3 } }}> diff --git a/src/pages/nearle/reports/ridersummary.js b/src/pages/nearle/reports/ridersummary.js index cffad34..9edc4d1 100644 --- a/src/pages/nearle/reports/ridersummary.js +++ b/src/pages/nearle/reports/ridersummary.js @@ -265,13 +265,14 @@ export default function RidersSummary() { <> {isLoadingReports && } - {/* ============================================= || Header || ============================================= */} + {/* ============================================= || Header (compact, standardized actions) || ============================================= */} - + - + - + Riders Summary - + - + Live · {filteredRows.length} riders · {datestatus} - - + - {/* ============================================= || KPI Cards || ============================================= */} - + {/* ============================================= || KPI Cards (compact) || ============================================= */} + {kpiCards.map((item) => { const Icon = item.icon; return ( @@ -365,14 +373,15 @@ export default function RidersSummary() { sx={{ position: 'relative', overflow: 'hidden', - p: { xs: 1.25, sm: 1.75, md: 2.25 }, - borderRadius: DT.radiusCard / 8, + px: { xs: 1.25, sm: 1.5 }, + py: { xs: 0.875, sm: 1.125 }, + borderRadius: 2, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', - transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s', + transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s', '&:hover': { - transform: 'translateY(-3px)', + transform: 'translateY(-1px)', boxShadow: DT.shadowMd, borderColor: edge(item.color) } @@ -383,24 +392,24 @@ export default function RidersSummary() { position: 'absolute', top: 0, left: 0, - right: 0, - height: 3, - background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)` + bottom: 0, + width: 3, + background: item.color }} /> - - + + {item.label} @@ -409,9 +418,10 @@ export default function RidersSummary() { sx={{ fontWeight: 800, color: DT.textPrimary, - lineHeight: 1.1, - fontSize: { xs: '1.1rem', sm: '1.35rem', md: '1.55rem' } + lineHeight: 1.15, + fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.2rem' } }} + noWrap > {isLoadingReports ? ( @@ -423,16 +433,17 @@ export default function RidersSummary() { - + @@ -441,13 +452,13 @@ export default function RidersSummary() { })} - {/* ============================================= || Filter Bar || ============================================= */} + {/* ============================================= || Filter Bar (compact) || ============================================= */} - {/* ============================================= || Table || ============================================= */} + {/* ============================================= || Table (dense, sticky header) || ============================================= */} - +
@@ -606,11 +619,11 @@ export default function RidersSummary() { {isLoadingReports && filteredRows.length === 0 && - [0, 1, 2, 3, 4].map((_, idx) => ( + Array.from({ length: 10 }).map((_, idx) => ( {Array.from({ length: 16 }).map((__, ci) => ( - - + + ))} @@ -618,17 +631,45 @@ export default function RidersSummary() { {!isLoadingReports && filteredRows.length === 0 && ( - - - - + + + + - + No riders to show - - {searchword ? 'Try a different rider name.' : 'Adjust the date range above.'} + + {searchword ? 'Try a different rider name or clear the search.' : 'Adjust the date range above.'} + {searchword && ( + + )} @@ -640,30 +681,35 @@ export default function RidersSummary() { - {index + 1} + {index + 1} - + {String(row.firstname || '?').charAt(0).toUpperCase()} - - + + {row.firstname} {row.lastname} - + Id : {row.userid} @@ -823,8 +869,8 @@ export default function RidersSummary() {