diff --git a/backedn.txt b/backedn.txt new file mode 100644 index 0000000..87cd73c --- /dev/null +++ b/backedn.txt @@ -0,0 +1,26 @@ +Here is the expanded architectural design base, updated specifically to handle a rapidly growing database with many collections beyond just Auth and Clients. + +1. The UX Flow (Built for Scale) +When you have dozens of collections, hardcoding every single view isn't scalable. The dashboard needs to act like a true "Command Center" with dynamic discovery: + +Zone A: "Dedicated Modules" (The Custom Experiences) +Purpose: Highly tailored, customized views for your most important business logic. +Flow: The sidebar permanently pins Team Access (doormile_auth) and Field Operations (doormile_clients). These open the specialized Tables, Kanban boards, and GeoMaps we discussed earlier. +Zone B: "The Data Explorer" (For the remaining dozens of collections) +Purpose: A dynamic, auto-generating view to inspect any collection that exists on the server, even ones created yesterday. +Flow: Admin scrolls down the sidebar to a "Data Lake" section -> Uses a mini search bar to find a specific collection by name -> Clicks it -> The app dynamically generates a generic table based on whatever payload data is inside it. +2. The Design Components (The Base for Scale) +To build this massive multi-collection system, you will add these components to the base: + +πŸ—‚οΈ For the Sidebar / Navigation +CollectionDiscovery Component: A scrollable list on the left menu that automatically asks Qdrant for a list of all collections and renders them. +SidebarFilter Component: A tiny search box sitting right above the collection list so you can instantly filter out collection names (e.g., typing "survey" shows survey_2024, survey_2025, survey_archived). +🧬 For the Generic / Unknown Collections (The Explorer) +Since you won't build a custom UI for every single minor collection, you need components that adapt to any data: + +AdaptiveDataGrid Component: A smart table that looks at the first vector it fetches from Qdrant, reads the JSON payload keys, and automatically creates the table columns on the fly. (If a collection has item_name and price, the table creates those two columns automatically). +VectorInspector Modal: When you click a row in an unknown collection, this opens a sleek, dark-themed JSON code viewer. It shows the raw payload and the raw floating-point vector arrays perfectly formatted, similar to how a developer console looks. +ClusterHealthWidget: A component at the very top of the dashboard that shows the sum of all vectors across all collections, total RAM usage, and server uptime. +βš™οΈ The Updated Shared Components +ViewToggle Component: A simple switch at the top right of the screen that lets an admin toggle any collection between "Card View" (visual) and "Table View" (dense data). +GlobalSearch (Upgraded): The search bar now includes a dropdown filter to let you select which of the 50+ collections you actually want to search inside. \ No newline at end of file diff --git a/index.html b/index.html index c64eb87..d29e4ac 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" /> - Doormile Console + Doormile CRM
diff --git a/src/App.jsx b/src/App.jsx index a50e7b9..6a7a428 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,8 @@ import { Box, CircularProgress } from '@mui/material'; import MainLayout from '@/layout/MainLayout'; import MinimalLayout from '@/layout/MinimalLayout'; +import AuthGuard from '@/components/AuthGuard'; + const load = (factory) => { const C = lazy(factory); return ( @@ -24,39 +26,13 @@ export default function App() { return ( {/* Shell pages */} - }> + }> import('@/pages/Dashboard'))} /> - import('@/pages/orders/OrdersList'))} /> - import('@/pages/orders/CreateOrder'))} /> - import('@/pages/orders/CreateMultipleOrders'))} /> - import('@/pages/orders/AssignOrders'))} /> - import('@/pages/orders/OrderDetails'))} /> - - import('@/pages/Deliveries'))} /> - import('@/pages/tenants/Tenants'))} /> - import('@/pages/tenants/CreateClient'))} /> - import('@/pages/customers/Customers'))} /> - import('@/pages/customers/CreateCustomer'))} /> + import('@/pages/team/TeamUsers'))} /> - import('@/pages/Pricing'))} /> - - import('@/pages/riders/Riders'))} /> - import('@/pages/riders/CreateRider'))} /> - import('@/pages/riders/EditRider'))} /> - - import('@/pages/reports/OrdersSummary'))} /> - import('@/pages/reports/OrdersDetails'))} /> - import('@/pages/reports/RidersSummary'))} /> - import('@/pages/reports/RidersLogs'))} /> - - import('@/pages/invoice/Invoices'))} /> - import('@/pages/invoice/InvoicePreview'))} /> - - import('@/pages/Requests'))} /> - import('@/pages/Profile'))} /> import('@/pages/Settings'))} /> diff --git a/src/assets/mid-mile-approach.jpg b/src/assets/mid-mile-approach.jpg new file mode 100644 index 0000000..0bb2252 Binary files /dev/null and b/src/assets/mid-mile-approach.jpg differ diff --git a/src/assets/premium_logistics_bg.png b/src/assets/premium_logistics_bg.png new file mode 100644 index 0000000..f46292f Binary files /dev/null and b/src/assets/premium_logistics_bg.png differ diff --git a/src/components/AuthGuard.jsx b/src/components/AuthGuard.jsx new file mode 100644 index 0000000..425f39e --- /dev/null +++ b/src/components/AuthGuard.jsx @@ -0,0 +1,11 @@ +import { Navigate, Outlet } from 'react-router-dom'; + +export default function AuthGuard({ children }) { + const token = localStorage.getItem('auth_token'); + + if (!token) { + return ; + } + + return children || ; +} diff --git a/src/components/MainCard.jsx b/src/components/MainCard.jsx deleted file mode 100644 index b86272d..0000000 --- a/src/components/MainCard.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Card, CardHeader, CardContent, Divider, Box } from '@mui/material'; - -// ==============================|| MAIN CARD (titled surface) ||============================== // - -export default function MainCard({ title, action, children, divider = true, contentSx, sx, noPadding = false }) { - return ( - - {title && ( - <> - - {divider && } - - )} - {noPadding ? {children} : {children}} - - ); -} diff --git a/src/components/MapPlaceholder.jsx b/src/components/MapPlaceholder.jsx deleted file mode 100644 index 0f5e2e1..0000000 --- a/src/components/MapPlaceholder.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, Typography, Chip } from '@mui/material'; -import RoomIcon from '@mui/icons-material/Room'; -import TwoWheelerIcon from '@mui/icons-material/TwoWheeler'; - -// Stylised static "map" surface β€” pins + red route line. Swap for Leaflet/Google later. -export default function MapPlaceholder({ height = 360, pins = [], showRoute = true, label = 'Live Tracking', riders = [] }) { - return ( - - {/* faux roads */} - - - - - {showRoute && ( - - - - )} - - {(pins.length ? pins : [ - { x: '18%', y: '78%', label: 'Pickup', color: '#00A854' }, - { x: '78%', y: '22%', label: 'Drop', color: '#C01227' } - ]).map((p, i) => ( - - - {p.label && ( - - )} - - ))} - - {riders.map((r, i) => ( - - - - - - ))} - - - - Map data Β© Doormile demo - - - ); -} diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx index 5adb385..483aca0 100644 --- a/src/components/StatCard.jsx +++ b/src/components/StatCard.jsx @@ -4,46 +4,74 @@ import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded'; // ==============================|| STAT / KPI CARD ||============================== // -export default function StatCard({ title, value, icon: Icon, color = 'primary', trend, caption }) { +export default function StatCard({ title, value, icon: Icon, color = 'primary', trend, caption, accent = false }) { const trendUp = typeof trend === 'number' ? trend >= 0 : null; return ( - - - - - - {title} - - - {value} - - + `linear-gradient(145deg, ${theme.palette.background.paper} 0%, ${theme.palette[color].lighter}15 100%)` + }} + > + {Icon && ( + + + + )} + + + + {title} + {Icon && ( - - + )} + + + + {value} + + + {(trendUp !== null || caption) && ( - + {trendUp !== null && ( - <> + {trendUp ? ( - + ) : ( - + )} - + {Math.abs(trend)}% - + )} {caption && ( - + {caption} )} diff --git a/src/components/StatusChip.jsx b/src/components/StatusChip.jsx index ce201e2..318571d 100644 --- a/src/components/StatusChip.jsx +++ b/src/components/StatusChip.jsx @@ -18,6 +18,16 @@ const MAP = { skipped: { color: 'warning', label: 'Skipped' }, failed: { color: 'error', label: 'Failed' }, cancelled: { color: 'error', label: 'Cancelled' }, + // clients (doormile_clients) + newclient: { color: 'info', label: 'New Client' }, + contacted: { color: 'warning', label: 'Contacted' }, + onboarded: { color: 'success', label: 'Onboarded' }, + lost: { color: 'error', label: 'Lost' }, + // team users (doormile_auth) + admin: { color: 'primary', label: 'Admin' }, + rep: { color: 'info', label: 'Rep' }, + manager: { color: 'success', label: 'Manager' }, + support: { color: 'warning', label: 'Support' }, // riders / tenants online: { color: 'success', label: 'Online' }, offline: { color: 'default', label: 'Offline' }, @@ -42,7 +52,8 @@ const TONE = { export default function StatusChip({ status, size = 'small', label, sx }) { const key = String(status || '').toLowerCase().replace(/\s+/g, '-'); - const cfg = MAP[key] || { color: 'default', label: status }; + const humanized = String(status || '').replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + const cfg = MAP[key] || { color: 'default', label: humanized || status }; const tone = TONE[cfg.color] || TONE.default; return ( s.data); - const max = Math.max(...all, 1); - const min = 0; - const n = labels.length; - - const x = (i) => pad.l + (n <= 1 ? 0 : (i / (n - 1)) * innerW); - const y = (v) => pad.t + innerH - ((v - min) / (max - min)) * innerH; - - const ticks = 4; - - return ( - - - {Array.from({ length: ticks + 1 }).map((_, i) => { - const gy = pad.t + (i / ticks) * innerH; - const val = Math.round(max - (i / ticks) * (max - min)); - return ( - - - - {val} - - - ); - })} - {labels.map((lb, i) => ( - - {lb} - - ))} - {series.map((s) => { - const line = s.data.map((v, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(v)}`).join(' '); - const area = `${line} L ${x(s.data.length - 1)} ${pad.t + innerH} L ${x(0)} ${pad.t + innerH} Z`; - const gid = `grad-${s.name.replace(/\s/g, '')}`; - return ( - - - - - - - - {s.fill !== false && } - - {s.data.map((v, i) => ( - - ))} - - ); - })} - - - ); -} diff --git a/src/data/mock.js b/src/data/mock.js deleted file mode 100644 index 72b9b53..0000000 --- a/src/data/mock.js +++ /dev/null @@ -1,181 +0,0 @@ -// ==============================|| DOORMILE - MOCK DATA ||============================== // -// Static demo data powering every screen. - -export const locations = ['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune']; - -export const tenantsList = [ - 'Freshly Foods', - 'UrbanCart', - 'MediQuick Pharma', - 'BloomBox Florists', - 'TechNova Retail', - 'GreenLeaf Organics' -]; - -export const orders = [ - { id: 'DM-10241', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'HSR Layout, Sec 2', customer: 'Riya Sharma', qty: 3, cod: 0, kms: 4.2, charges: 86, notes: 'Leave at door', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, - { id: 'DM-10242', tenant: 'UrbanCart', location: 'Bengaluru', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', customer: 'Karthik Rao', qty: 1, cod: 1299, kms: 12.8, charges: 142, notes: 'Call on arrival', status: 'active', date: '2026-06-05', payment: 'cod' }, - { id: 'DM-10243', tenant: 'MediQuick Pharma', location: 'Mumbai', pickup: 'Andheri West', drop: 'Bandra East', customer: 'Neha Kulkarni', qty: 2, cod: 0, kms: 6.5, charges: 98, notes: '', status: 'picked', date: '2026-06-05', payment: 'prepaid' }, - { id: 'DM-10244', tenant: 'BloomBox Florists', location: 'Delhi NCR', pickup: 'Connaught Place', drop: 'Saket, Block C', customer: 'Arjun Mehta', qty: 1, cod: 450, kms: 9.1, charges: 110, notes: 'Fragile', status: 'pending', date: '2026-06-05', payment: 'cod' }, - { id: 'DM-10245', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'BTM Layout', customer: 'Sneha Iyer', qty: 4, cod: 0, kms: 3.4, charges: 74, notes: '', status: 'cancelled', date: '2026-06-03', payment: 'prepaid' }, - { id: 'DM-10246', tenant: 'TechNova Retail', location: 'Hyderabad', pickup: 'Hitech City', drop: 'Gachibowli', customer: 'Vikram Reddy', qty: 1, cod: 4999, kms: 5.6, charges: 120, notes: 'ID required', status: 'created', date: '2026-06-05', payment: 'cod' }, - { id: 'DM-10247', tenant: 'GreenLeaf Organics', location: 'Pune', pickup: 'Koregaon Park', drop: 'Viman Nagar', customer: 'Pooja Desai', qty: 2, cod: 0, kms: 7.2, charges: 102, notes: '', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, - { id: 'DM-10248', tenant: 'UrbanCart', location: 'Chennai', pickup: 'T. Nagar', drop: 'Adyar', customer: 'Suresh Kumar', qty: 1, cod: 799, kms: 8.3, charges: 108, notes: 'Weekend slot', status: 'pending', date: '2026-06-05', payment: 'cod' } -]; - -export const orderTimeline = [ - { label: 'Order Placed', time: '09:12 AM', done: true }, - { label: 'Picked Up', time: '09:48 AM', done: true }, - { label: 'In Transit', time: '10:05 AM', done: true }, - { label: 'Delivered', time: 'β€”', done: false } -]; - -export const deliveries = orders.map((o, i) => ({ - ...o, - rider: ['Mohan Das', 'Imran Sheikh', 'Ravi Teja', 'Sandeep Roy', 'Faisal Khan'][i % 5], - amount: o.charges, - products: [ - { name: 'Item A', description: 'Standard pack', qty: o.qty, cost: 40, price: 60, tax: 5, amount: 60 * o.qty } - ] -})); - -export const riders = [ - { id: 'RDR-001', userId: 'U1043', name: 'Mohan Das', phone: '+91 98450 11223', address: 'Koramangala, Bengaluru', vehicle: 'Honda Activa', vehicleNo: 'KA-05-HJ-4521', shift: 'Morning', start: '08:00', end: '16:00', fare: 320, fuel: 110, status: 'online', deliveries: 14, rating: 4.8 }, - { id: 'RDR-002', userId: 'U1044', name: 'Imran Sheikh', phone: '+91 98860 44781', address: 'Andheri, Mumbai', vehicle: 'TVS Jupiter', vehicleNo: 'MH-02-CD-9087', shift: 'Evening', start: '14:00', end: '22:00', fare: 280, fuel: 95, status: 'on-delivery', deliveries: 11, rating: 4.6 }, - { id: 'RDR-003', userId: 'U1045', name: 'Ravi Teja', phone: '+91 99000 22119', address: 'Gachibowli, Hyderabad', vehicle: 'Hero Splendor', vehicleNo: 'TS-09-AB-3344', shift: 'Morning', start: '08:00', end: '16:00', fare: 300, fuel: 120, status: 'online', deliveries: 9, rating: 4.9 }, - { id: 'RDR-004', userId: 'U1046', name: 'Sandeep Roy', phone: '+91 90080 55672', address: 'Saket, Delhi', vehicle: 'Bajaj Pulsar', vehicleNo: 'DL-3C-XY-1290', shift: 'Night', start: '22:00', end: '06:00', fare: 350, fuel: 130, status: 'offline', deliveries: 0, rating: 4.4 }, - { id: 'RDR-005', userId: 'U1047', name: 'Faisal Khan', phone: '+91 97410 88123', address: 'Adyar, Chennai', vehicle: 'Honda Dio', vehicleNo: 'TN-07-PQ-6655', shift: 'Evening', start: '14:00', end: '22:00', fare: 290, fuel: 100, status: 'on-delivery', deliveries: 7, rating: 4.7 } -]; - -export const riderLogs = [ - { location: '12.9352, 77.6245', battery: '82%', charging: 'No', speed: '24 km/h', accuracy: '5 m', time: '10:42 AM', order: 'DM-10242', status: 'active' }, - { location: '12.9298, 77.6848', battery: '80%', charging: 'No', speed: '0 km/h', accuracy: '8 m', time: '10:38 AM', order: 'DM-10242', status: 'arrived' }, - { location: '12.9101, 77.6446', battery: '84%', charging: 'No', speed: '31 km/h', accuracy: '4 m', time: '10:25 AM', order: 'DM-10242', status: 'picked' } -]; - -export const customers = [ - { id: 1, name: 'Riya Sharma', phone: '+91 98111 22334', email: 'riya.sharma@mail.com', address: '24, 1st Cross, HSR Layout', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560102', totalOrders: 18, joined: '2025-03-12' }, - { id: 2, name: 'Karthik Rao', phone: '+91 98222 33445', email: 'karthik.rao@mail.com', address: '8, Palm Meadows, Whitefield', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560066', totalOrders: 7, joined: '2025-07-21' }, - { id: 3, name: 'Neha Kulkarni', phone: '+91 98333 44556', email: 'neha.k@mail.com', address: '102, Sea View, Bandra East', location: 'Mumbai', city: 'Mumbai', state: 'Maharashtra', postcode: '400051', totalOrders: 31, joined: '2024-11-02' }, - { id: 4, name: 'Arjun Mehta', phone: '+91 98444 55667', email: 'arjun.m@mail.com', address: 'C-14, Saket', location: 'Delhi NCR', city: 'New Delhi', state: 'Delhi', postcode: '110017', totalOrders: 5, joined: '2025-09-18' }, - { id: 5, name: 'Pooja Desai', phone: '+91 98555 66778', email: 'pooja.d@mail.com', address: '4, Lane 5, Koregaon Park', location: 'Pune', city: 'Pune', state: 'Maharashtra', postcode: '411001', totalOrders: 12, joined: '2025-01-30' } -]; - -export const tenants = [ - { id: 1, name: 'Freshly Foods', contact: 'Anil Gupta', phone: '+91 80451 22000', email: 'ops@freshlyfoods.in', address: 'Koramangala 4th Block', city: 'Bengaluru', postcode: '560034', lat: '12.9352', lng: '77.6245', status: 'active', volume: 1240 }, - { id: 2, name: 'UrbanCart', contact: 'Meera Nair', phone: '+91 22480 11233', email: 'logistics@urbancart.com', address: 'Indiranagar 100ft Rd', city: 'Bengaluru', postcode: '560038', lat: '12.9719', lng: '77.6412', status: 'active', volume: 980 }, - { id: 3, name: 'MediQuick Pharma', contact: 'Rohit Sen', phone: '+91 22556 99001', email: 'dispatch@mediquick.in', address: 'Andheri West', city: 'Mumbai', postcode: '400058', lat: '19.1351', lng: '72.8290', status: 'pending', volume: 410 }, - { id: 4, name: 'BloomBox Florists', contact: 'Tina Roy', phone: '+91 11409 33221', email: 'hello@bloombox.in', address: 'Connaught Place', city: 'New Delhi', postcode: '110001', lat: '28.6315', lng: '77.2167', status: 'inactive', volume: 120 }, - { id: 5, name: 'TechNova Retail', contact: 'Sameer Joshi', phone: '+91 40229 77654', email: 'ops@technova.in', address: 'Hitech City', city: 'Hyderabad', postcode: '500081', lat: '17.4435', lng: '78.3772', status: 'active', volume: 760 } -]; - -export const tenantPricing = [ - { date: '2025-04-01', slab: '0-5 km', basePrice: 40, minKms: 2, pricePerKm: 9, otherCharges: 0 }, - { date: '2025-04-01', slab: '5-10 km', basePrice: 60, minKms: 5, pricePerKm: 8, otherCharges: 10 }, - { date: '2025-04-01', slab: '10+ km', basePrice: 90, minKms: 10, pricePerKm: 7, otherCharges: 15 } -]; - -export const pricing = [ - { id: 1, location: 'Bengaluru', pricingId: 'PR-001', name: 'Standard', slab: '0-5 km', basePrice: 40, minKm: 2, pricePerKm: 9, maxKm: 5, minOrders: 1 }, - { id: 2, location: 'Bengaluru', pricingId: 'PR-002', name: 'Express', slab: '5-10 km', basePrice: 65, minKm: 5, pricePerKm: 8, maxKm: 10, minOrders: 1 }, - { id: 3, location: 'Mumbai', pricingId: 'PR-003', name: 'Standard', slab: '0-5 km', basePrice: 45, minKm: 2, pricePerKm: 10, maxKm: 5, minOrders: 1 }, - { id: 4, location: 'Delhi NCR', pricingId: 'PR-004', name: 'Bulk', slab: '10+ km', basePrice: 95, minKm: 10, pricePerKm: 7, maxKm: 25, minOrders: 5 } -]; - -export const invoices = [ - { id: 1, invoiceId: 'INV-2026-0041', client: 'Freshly Foods', invoiceDate: '2026-06-01', dueDate: '2026-06-15', period: 'May 2026', count: 1240, amount: 106800, status: 'paid' }, - { id: 2, invoiceId: 'INV-2026-0042', client: 'UrbanCart', invoiceDate: '2026-06-01', dueDate: '2026-06-15', period: 'May 2026', count: 980, amount: 84300, status: 'open' }, - { id: 3, invoiceId: 'INV-2026-0043', client: 'MediQuick Pharma', invoiceDate: '2026-05-01', dueDate: '2026-05-15', period: 'Apr 2026', count: 410, amount: 35200, status: 'overdue' }, - { id: 4, invoiceId: 'INV-2026-0044', client: 'TechNova Retail', invoiceDate: '2026-06-01', dueDate: '2026-06-20', period: 'May 2026', count: 760, amount: 65400, status: 'open' }, - { id: 5, invoiceId: 'INV-2026-0045', client: 'GreenLeaf Organics', invoiceDate: '2026-05-01', dueDate: '2026-05-15', period: 'Apr 2026', count: 540, amount: 46900, status: 'paid' } -]; - -export const invoiceLineItems = [ - { particulars: 'Standard delivery (0-5 km)', unit: 'order', qty: 720, rate: 74, other: 0, amount: 53280 }, - { particulars: 'Express delivery (5-10 km)', unit: 'order', qty: 380, rate: 108, other: 0, amount: 41040 }, - { particulars: 'Bulk delivery (10+ km)', unit: 'order', qty: 140, rate: 142, other: 8, amount: 21000 }, - { particulars: 'COD handling fee', unit: 'order', qty: 260, rate: 6, other: 0, amount: 1560 } -]; - -export const requests = [ - { id: 1, requestor: 'Freshly Foods', bank: 'HDFC Bank', ifsc: 'HDFC0001234', refNo: 'RQ-88121', amount: 24500, reason: 'Weekly settlement payout', contact: 'Anil Gupta', address: 'Koramangala 4th Block', city: 'Bengaluru', zip: '560034', accountNo: '5012 3344 7788', pricing: [{ category: 'Standard', skill: 'Bike', cost: 9 }, { category: 'Express', skill: 'Bike', cost: 12 }] }, - { id: 2, requestor: 'UrbanCart', bank: 'ICICI Bank', ifsc: 'ICIC0004567', refNo: 'RQ-88122', amount: 18900, reason: 'Fuel reimbursement', contact: 'Meera Nair', address: 'Indiranagar', city: 'Bengaluru', zip: '560038', accountNo: '6022 1199 4455', pricing: [{ category: 'Standard', skill: 'Bike', cost: 8 }] }, - { id: 3, requestor: 'MediQuick Pharma', bank: 'Axis Bank', ifsc: 'UTIB0007788', refNo: 'RQ-88123', amount: 9700, reason: 'Adjustment - April', contact: 'Rohit Sen', address: 'Andheri West', city: 'Mumbai', zip: '400058', accountNo: '7033 5566 1122', pricing: [{ category: 'Standard', skill: 'Bike', cost: 10 }] } -]; - -// reports -export const ordersSummary = tenants.map((t, i) => ({ - id: t.id, - tenant: t.name, - location: t.city, - orders: { pending: [2, 1, 0, 3, 1][i], cancelled: [1, 0, 2, 0, 1][i], completed: [38, 27, 11, 4, 22][i] }, - deliveries: { pending: [1, 1, 0, 2, 0][i], cancelled: [0, 0, 1, 0, 1][i], completed: [38, 26, 10, 4, 22][i] }, - collection: [12400, 8800, 3200, 900, 6700][i], - kms: [184, 142, 66, 28, 121][i], - actualKms: [190, 148, 70, 30, 124][i], - amount: [4280, 3120, 1420, 520, 2410][i], - riders: [ - { rider: 'Mohan Das', orders: 14, deliveries: 14, pending: 1, cancelled: 0, completed: 13, collection: 5200, kms: 64, actualKms: 66, charges: 1480 }, - { rider: 'Imran Sheikh', orders: 11, deliveries: 10, pending: 0, cancelled: 1, completed: 9, collection: 4100, kms: 58, actualKms: 60, charges: 1280 } - ] -})); - -export const ordersDetailReport = orders.map((o, i) => ({ - id: o.id, - client: o.tenant, - pickup: o.pickup, - drop: o.drop, - status: o.status, - assigned: '09:12', - accepted: i % 5 === 3 ? 'β€”' : '09:14', - arrived: ['09:40', 'β€”', '09:35', 'β€”', 'β€”', 'β€”', '09:50', 'β€”'][i], - picked: ['09:48', 'β€”', '09:42', 'β€”', 'β€”', 'β€”', '09:58', 'β€”'][i], - active: ['10:05', 'β€”', '10:00', 'β€”', 'β€”', 'β€”', '10:12', 'β€”'][i], - delivered: o.status === 'delivered' ? '10:30' : 'β€”', - cancelled: o.status === 'cancelled' ? '09:20' : 'β€”', - notes: o.notes, - kms: o.kms, - charges: o.charges -})); - -export const ridersSummary = riders.map((r, i) => ({ - id: r.id, - rider: r.name, - orders: r.deliveries + [2, 1, 1, 0, 1][i], - pending: [1, 0, 1, 0, 0][i], - cancelled: [0, 1, 0, 0, 1][i], - delivered: r.deliveries, - kms: [64, 58, 47, 0, 39][i], - actualKms: [66, 60, 49, 0, 41][i], - amount: [1480, 1280, 1120, 0, 980][i], - clients: [ - { client: 'Freshly Foods', all: 8, pending: 1, completed: 7, cancelled: 0, kms: 32, actualKms: 33, amount: 740 }, - { client: 'UrbanCart', all: 6, pending: 0, completed: 6, cancelled: 0, kms: 32, actualKms: 33, amount: 740 } - ] -})); - -export const ridersLive = riders.map((r, i) => ({ - ...r, - userid: r.userId, - lastLog: ['10:42 AM', '10:39 AM', '10:40 AM', 'Yesterday', '10:35 AM'][i], - active: r.status !== 'offline', - lat: [12.9352, 19.1351, 17.4435, 28.6315, 13.0067][i], - lng: [77.6245, 72.829, 78.3772, 77.2167, 80.2206][i] -})); - -// dashboard chart series (monthly orders) -export const ordersTrend = [ - { m: 'Jan', orders: 820, delivered: 760 }, - { m: 'Feb', orders: 932, delivered: 880 }, - { m: 'Mar', orders: 1010, delivered: 965 }, - { m: 'Apr', orders: 1180, delivered: 1120 }, - { m: 'May', orders: 1290, delivered: 1240 }, - { m: 'Jun', orders: 1402, delivered: 1330 } -]; - -export const statusBreakdown = [ - { label: 'Delivered', value: 1240, color: '#00A854' }, - { label: 'In Transit', value: 96, color: '#00A2AE' }, - { label: 'Pending', value: 48, color: '#FFBF00' }, - { label: 'Cancelled', value: 18, color: '#F04134' } -]; diff --git a/src/layout/MainLayout/Header.jsx b/src/layout/MainLayout/Header.jsx index 46a9e16..63d7e99 100644 --- a/src/layout/MainLayout/Header.jsx +++ b/src/layout/MainLayout/Header.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { AppBar, @@ -6,72 +6,78 @@ import { IconButton, Box, InputBase, - Badge, Avatar, Typography, + Stack, Menu, MenuItem, Divider, ListItemIcon, - ListItemText, - Tooltip, - Button, - Stack, + Popper, + Paper, + ClickAwayListener, + CircularProgress, alpha } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; -import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; -import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; -import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; import LogoutIcon from '@mui/icons-material/Logout'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; -import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined'; -import DoneAllIcon from '@mui/icons-material/DoneAll'; import Logo from '@/components/Logo'; +import UserAvatar from '@/components/UserAvatar'; +import { fetchPoints, COLLECTIONS } from '@/utils/qdrant'; const RED = '#C01227'; -const INITIAL_NOTIFICATIONS = [ - { id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false }, - { id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/riders', read: false }, - { id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false }, - { id: 4, icon: AssignmentOutlinedIcon, title: '3 new onboarding requests', time: '3 hrs ago', to: '/requests', read: true } -]; - -const MESSAGES = [ - { id: 1, name: 'Priya Nair', text: 'Can we reroute the MG Road batch?', time: '5 min ago', initials: 'PN' }, - { id: 2, name: 'Imran Khan', text: 'Reached the warehouse, loading now.', time: '22 min ago', initials: 'IK' }, - { id: 3, name: 'Acme Logistics', text: 'Please confirm the revised pricing.', time: '2 hrs ago', initials: 'AL' } -]; - export default function Header({ onToggle }) { const navigate = useNavigate(); const [account, setAccount] = useState(null); - const [notifAnchor, setNotifAnchor] = useState(null); - const [msgAnchor, setMsgAnchor] = useState(null); - const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS); const [search, setSearch] = useState(''); - const unread = notifications.filter((n) => !n.read).length; + // Live client search for the top bar. + const searchRef = useRef(null); + const [clients, setClients] = useState([]); + const [loadedClients, setLoadedClients] = useState(false); + const [loadingClients, setLoadingClients] = useState(false); + const [openResults, setOpenResults] = useState(false); - const openNotif = (e) => setNotifAnchor(e.currentTarget); - const closeNotif = () => setNotifAnchor(null); - const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); - const onNotifClick = (n) => { - setNotifications((prev) => prev.map((x) => (x.id === n.id ? { ...x, read: true } : x))); - closeNotif(); - navigate(n.to); + const ensureClients = () => { + if (loadedClients || loadingClients) return; + setLoadingClients(true); + fetchPoints(COLLECTIONS.clients) + .then((points) => setClients(points.map((p) => ({ + id: p.id, + name: p.payload?.name || 'β€”', + city: p.payload?.city || '', + businessType: p.payload?.businessType || '', + phone: p.payload?.phone || '' + })))) + .catch(() => {}) + .finally(() => { setLoadedClients(true); setLoadingClients(false); }); + }; + + const q = search.trim().toLowerCase(); + const results = q + ? clients.filter((c) => [c.name, c.city, c.businessType, c.phone].join(' ').toLowerCase().includes(q)).slice(0, 6) + : []; + + const onSearchChange = (e) => { + setSearch(e.target.value); + ensureClients(); + setOpenResults(true); + }; + + const goToClients = (term) => { + navigate(`/tenants?q=${encodeURIComponent(term)}`); + setSearch(''); + setOpenResults(false); }; const submitSearch = (e) => { e.preventDefault(); - const q = search.trim(); - if (q) navigate(`/orders?q=${encodeURIComponent(q)}`); + const term = search.trim(); + if (term) goToClients(term); }; return ( @@ -95,55 +101,82 @@ export default function Header({ onToggle }) { - {/* Search β€” moved to the right */} - - - setSearch(e.target.value)} - placeholder="Search orders, riders, customers…" - sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }} - inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }} - /> - + {/* Search β€” live client lookup */} + setOpenResults(false)}> + + + + { ensureClients(); if (search.trim()) setOpenResults(true); }} + placeholder="Search clients…" + sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }} + inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }} + /> + - - setMsgAnchor(e.currentTarget)}> - - - - - - - - - - - - + + + {loadingClients && results.length === 0 ? ( + + ) : results.length === 0 ? ( + + No clients match β€œ{search.trim()}”. + + ) : ( + <> + {results.map((c) => ( + goToClients(c.name)} sx={{ py: 1, gap: 1.25 }}> + + + {c.name} + + {[c.businessType, c.city].filter(Boolean).join(' Β· ') || c.phone} + + + + ))} + + goToClients(search.trim())} sx={{ py: 1.25, color: 'primary.main', fontWeight: 600 }}> + + See all results for β€œ{search.trim()}” + + + )} + + + + setAccount(e.currentTarget)} sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: alpha('#fff', 0.14) } }} > - AD + A - Aman Deshmukh + Admin Operations Admin @@ -151,87 +184,6 @@ export default function Header({ onToggle }) { - {/* Notifications dropdown */} - - - - Notifications - - - - - {notifications.length === 0 && ( - - - - )} - {notifications.map((n) => { - const Icon = n.icon; - return ( - onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> - - - - - - - {!n.read && } - - ); - })} - - { closeNotif(); navigate('/requests'); }} sx={{ justifyContent: 'center', color: 'primary.main', fontWeight: 600 }}> - View all activity - - - - {/* Messages dropdown */} - setMsgAnchor(null)} - transformOrigin={{ horizontal: 'right', vertical: 'top' }} - anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} - PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }} - > - - Messages - - - {MESSAGES.map((m) => ( - setMsgAnchor(null)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> - - - {m.initials} - - - - - {m.time} - - - ))} - - {/* Account dropdown */} setAccount(null)} transformOrigin={{ horizontal: 'right', vertical: 'top' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} - PaperProps={{ sx: { mt: 1, minWidth: 200 } }} + PaperProps={{ sx: { mt: 1, minWidth: 220 } }} > - { setAccount(null); navigate('/profile'); }}> - - View Profile - + + + A + + Admin + Operations Admin + + + + { setAccount(null); navigate('/settings'); }}> Settings diff --git a/src/layout/MainLayout/Sidebar.jsx b/src/layout/MainLayout/Sidebar.jsx index 1be9cab..5a9f845 100644 --- a/src/layout/MainLayout/Sidebar.jsx +++ b/src/layout/MainLayout/Sidebar.jsx @@ -157,7 +157,7 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) { {expanded && ( - Doormile Console v1.0 + Doormile CRM v1.0 )} diff --git a/src/menu/navItems.jsx b/src/menu/navItems.jsx index 571e8c5..8f9f657 100644 --- a/src/menu/navItems.jsx +++ b/src/menu/navItems.jsx @@ -1,53 +1,23 @@ import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined'; import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined'; -import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined'; -import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; -import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined'; -import SummarizeOutlinedIcon from '@mui/icons-material/SummarizeOutlined'; -import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; -import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; +import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; // ==============================|| DOORMILE - SIDEBAR NAV CONFIG ||============================== // const navItems = [ { - group: 'Operations', + group: 'CRM', items: [ { id: 'dashboard', title: 'Dashboard', url: '/dashboard', icon: DashboardOutlinedIcon }, - { id: 'orders', title: 'Orders', url: '/orders', icon: Inventory2OutlinedIcon }, - { id: 'deliveries', title: 'Deliveries', url: '/deliveries', icon: MopedOutlinedIcon } + { id: 'tenants', title: 'Clients', url: '/tenants', icon: ApartmentOutlinedIcon }, + { id: 'team-users', title: 'App Users', url: '/team-users', icon: GroupsOutlinedIcon } ] }, { - group: 'Network', + group: 'System', items: [ - { id: 'tenants', title: 'Tenants', url: '/tenants', icon: ApartmentOutlinedIcon }, - { id: 'pricing', title: 'Pricing', url: '/pricing', icon: PaymentsOutlinedIcon }, - { id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon }, - { id: 'riders', title: 'Riders', url: '/riders', icon: TwoWheelerOutlinedIcon } - ] - }, - { - group: 'Finance & Insights', - items: [ - { - id: 'reports', - title: 'Reports', - icon: BarChartOutlinedIcon, - children: [ - { id: 'orders-summary', title: 'Order Summary', url: '/reports/orders-summary', icon: SummarizeOutlinedIcon }, - { id: 'orders-details', title: 'Order Details', url: '/reports/orders-details', icon: FactCheckOutlinedIcon }, - { id: 'riders-summary', title: 'Riders Summary', url: '/reports/riders-summary', icon: TwoWheelerOutlinedIcon }, - { id: 'riders-logs', title: 'Riders Logs', url: '/reports/riders-logs', icon: MapOutlinedIcon } - ] - }, - { id: 'invoice', title: 'Invoice', url: '/invoice', icon: ReceiptLongOutlinedIcon }, - { id: 'requests', title: 'Requests', url: '/requests', icon: AssignmentOutlinedIcon } + { id: 'settings', title: 'Settings', url: '/settings', icon: SettingsOutlinedIcon } ] } ]; diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 838ea5b..3c95b0c 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,124 +1,260 @@ -import { Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Avatar, MenuItem, TextField } from '@mui/material'; +import { useState, useEffect, useMemo } from 'react'; +import { + Grid, Card, Box, Stack, Typography, Button, Divider, LinearProgress, CircularProgress, Alert, + Table, TableBody, TableCell, TableHead, TableRow, TableContainer +} from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined'; +import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; -import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import HandshakeOutlinedIcon from '@mui/icons-material/HandshakeOutlined'; +import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined'; +import DonutLargeOutlinedIcon from '@mui/icons-material/DonutLargeOutlined'; +import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined'; +import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; import PageHeader from '@/components/PageHeader'; import StatCard from '@/components/StatCard'; -import MainCard from '@/components/MainCard'; import StatusChip from '@/components/StatusChip'; -import AreaChart from '@/components/charts/AreaChart'; import DonutChart from '@/components/charts/DonutChart'; import UserAvatar from '@/components/UserAvatar'; -import { ordersTrend, statusBreakdown, orders, riders } from '@/data/mock'; -import { inr } from '@/utils/format'; +import EmptyState from '@/components/EmptyState'; +import { fetchPoints, COLLECTIONS } from '@/utils/qdrant'; +import bgImage from '@/assets/premium_logistics_bg.png'; + +const titleCase = (s) => + String(s || '').replace(/[_-]+/g, ' ').replace(/([a-z\d])([A-Z])/g, '$1 $2').replace(/\b\w/g, (c) => c.toUpperCase()).trim(); + +const STATUS_COLOR = { newclient: '#00A2AE', contacted: '#FFBF00', onboarded: '#00A854', lost: '#F04134' }; +const statusColor = (s) => STATUS_COLOR[String(s || '').toLowerCase()] || '#8C8C8C'; +const BAR_COLORS = ['#C01227', '#00A2AE', '#00A854', '#FFBF00', '#9E0E20', '#8C8C8C', '#D6515C']; + +const generateLogicalId = (id) => { + const str = String(id).replace(/[^a-zA-Z0-9]/g, '').toUpperCase(); + return 'CLI-' + str.substring(0, 6).padStart(6, '0'); +}; + +function toClient(point) { + const p = point.payload || {}; + return { + id: point.id, + logicalId: generateLogicalId(point.id), + name: p.name || 'β€”', + businessType: p.businessType || '', + city: p.city || '', + businessState: p.businessState || '', + status: p.status || 'unknown', + parcelVolume: Number(p.parcelVolume) || 0, + activeContracts: Number(p.activeContracts) || 0, + lastUpdated: p.lastUpdated || '' + }; +} +function toUser(point) { + const p = point.payload || {}; + return { id: point.id, name: p.name || 'β€”', email: p.email || '', role: p.role || 'unknown' }; +} + +// Card with a tinted icon header band. +function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false, children }) { + return ( + + `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, transparent 100%)` + }} + > + + + + {title} + {action} + + {children} + + ); +} export default function Dashboard() { + const [clients, setClients] = useState([]); + const [team, setTeam] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const load = () => { + setLoading(true); + setError(null); + Promise.all([fetchPoints(COLLECTIONS.clients), fetchPoints(COLLECTIONS.teamUsers)]) + .then(([cs, us]) => { + setClients(cs.map(toClient)); + setTeam(us.map(toUser)); + }) + .catch((e) => setError(e.message || 'Failed to load dashboard data')) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + const stats = useMemo(() => ({ + total: clients.length, + newCount: clients.filter((c) => c.status === 'newClient').length, + parcels: clients.reduce((s, c) => s + c.parcelVolume, 0), + contracts: clients.reduce((s, c) => s + c.activeContracts, 0) + }), [clients]); + + const statusData = useMemo(() => { + const m = {}; + clients.forEach((c) => { m[c.status] = (m[c.status] || 0) + 1; }); + return Object.entries(m).sort((a, b) => b[1] - a[1]).map(([status, value]) => ({ label: titleCase(status), value, color: statusColor(status) })); + }, [clients]); + + const byType = useMemo(() => { + const m = {}; + clients.forEach((c) => { const t = c.businessType || 'other'; m[t] = (m[t] || 0) + 1; }); + return Object.entries(m).sort((a, b) => b[1] - a[1]); + }, [clients]); + const maxType = byType[0]?.[1] || 1; + + const recent = useMemo( + () => [...clients].sort((a, b) => String(b.lastUpdated).localeCompare(String(a.lastUpdated))).slice(0, 6), + [clients] + ); + + const today = new Date().toLocaleDateString('en-IN', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); + + if (loading) { + return ( + <> + + + + ); + } + return ( <> - - All Locations - Bengaluru - Mumbai - - - - } + action={} /> - - - - - + {error && Retry}>{error}} + + + + + + + + - } - > - d.m)} - series={[ - { name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) }, - { name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) } - ]} - /> - - - - - - - - + + {recent.length === 0 ? ( + + ) : ( + + + + + Client + Type + Location + Parcels + Status + + + + {recent.map((c) => ( + + + + + {c.name} + + + {titleCase(c.businessType) || 'β€”'} + {c.city || 'β€”'}{c.businessState ? `, ${c.businessState}` : ''} + {c.parcelVolume.toLocaleString('en-IN')} + + + ))} + +
+
+ )} +
- - - - - - Order ID - Customer - Route - Status - Amount - - - - {orders.slice(0, 6).map((o) => ( - - {o.id} - {o.customer} - - {o.pickup} β†’ {o.drop} - - - {inr(o.charges)} - - ))} - -
-
+ + + + {statusData.length === 0 + ? + : } + + - - - } spacing={0}> - {riders.slice(0, 5).map((r, i) => ( - - {i + 1} - - - {r.name} - {r.vehicle} · ⭐ {r.rating} + + + + {byType.length === 0 ? ( + + ) : ( + + {byType.map(([type, count], i) => ( + + + + + {titleCase(type)} + + + {count} · {Math.round((count / clients.length) * 100)}% + + + - - {r.deliveries} - deliveries - - - ))} - - + ))} + + )} + + + + + + {team.length === 0 ? ( + + ) : ( + } spacing={0}> + {team.slice(0, 6).map((u) => ( + + + + {u.name} + {u.email} + + + + ))} + + )} +
); } - -function Legend({ color, label }) { - return ( - - - {label} - - ); -} diff --git a/src/pages/Deliveries.jsx b/src/pages/Deliveries.jsx deleted file mode 100644 index ae51448..0000000 --- a/src/pages/Deliveries.jsx +++ /dev/null @@ -1,279 +0,0 @@ -import { useState, useMemo, Fragment } from 'react'; -import { - Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - Tooltip, TablePagination, Typography, Collapse, Menu -} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import NotificationsActiveOutlinedIcon from '@mui/icons-material/NotificationsActiveOutlined'; -import SwapHorizOutlinedIcon from '@mui/icons-material/SwapHorizOutlined'; -import EditLocationAltOutlinedIcon from '@mui/icons-material/EditLocationAltOutlined'; -import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; - -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import EmptyState from '@/components/EmptyState'; -import UserAvatar from '@/components/UserAvatar'; -import TabLabelCount from '@/components/TabLabelCount'; -import { deliveries, locations, tenantsList, riders } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const TABS = [ - { key: 'assigned', label: 'Assigned' }, - { key: 'accepted', label: 'Accepted' }, - { key: 'arrived', label: 'Arrived' }, - { key: 'picked', label: 'Picked' }, - { key: 'active', label: 'Active' }, - { key: 'skipped', label: 'Skipped' }, - { key: 'delivered', label: 'Delivered' }, - { key: 'cancelled', label: 'Cancelled' } -]; - -function ProductTable({ products = [] }) { - return ( - - - Products - - - - - S.No - Product Name - Description - Quantity - Cost - Price - Tax - Amount - - - - {products.map((p, i) => ( - - {i + 1} - {p.name} - - {p.description} - - {p.qty} - {inr(p.cost)} - {inr(p.price)} - {p.tax}% - {inr(p.amount)} - - ))} - -
-
- ); -} - -function DeliveryRow({ row, index }) { - const [open, setOpen] = useState(false); - const [anchor, setAnchor] = useState(null); - - return ( - - *': { borderBottom: open ? 'unset' : undefined } }}> - - setOpen((o) => !o)}> - {open ? : } - - - {index + 1} - {row.tenant} - {row.location} - {row.pickup} - {row.drop} - - - - {row.rider} - - - - {row.notes || 'β€”'} - - {row.qty} - {row.cod ? inr(row.cod) : 'β€”'} - {row.kms} - {inr(row.amount)} - - - setAnchor(e.currentTarget)}> - - - - setAnchor(null)}> - setAnchor(null)}> - Notify Rider - - setAnchor(null)}> - Change Rider - - setAnchor(null)}> - Update Delivery Status - - setAnchor(null)} sx={{ color: 'error.main' }}> - Cancel Delivery - - - - - - - - - - - - - ); -} - -export default function Deliveries() { - const [tab, setTab] = useState(0); - const [search, setSearch] = useState(''); - const [tenant, setTenant] = useState('all'); - const [location, setLocation] = useState('all'); - const [rider, setRider] = useState('all'); - const [headerLocation, setHeaderLocation] = useState('all'); - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(5); - - const tabKey = TABS[tab].key; - - const counts = useMemo(() => { - const c = {}; - TABS.forEach((t) => { c[t.key] = deliveries.filter((d) => d.status === t.key).length; }); - return c; - }, []); - - const stats = useMemo(() => ({ - created: deliveries.length, - pending: deliveries.filter((d) => d.status === 'pending').length, - delivered: deliveries.filter((d) => d.status === 'delivered').length, - cancelled: deliveries.filter((d) => d.status === 'cancelled').length - }), []); - - const filtered = useMemo( - () => - deliveries.filter((d) => { - const matchTab = d.status === tabKey; - const matchTenant = tenant === 'all' || d.tenant === tenant; - const matchLocation = location === 'all' || d.location === location; - const matchRider = rider === 'all' || d.rider === rider; - const matchSearch = - !search || - [d.id, d.tenant, d.pickup, d.drop, d.rider, d.location].join(' ').toLowerCase().includes(search.toLowerCase()); - return matchTab && matchTenant && matchLocation && matchRider && matchSearch; - }), - [tabKey, tenant, location, rider, search] - ); - - const paged = filtered.slice(page * rpp, page * rpp + rpp); - - return ( - <> - setHeaderLocation(e.target.value)} - sx={{ minWidth: 180 }} label="Location" - > - All Locations - {locations.map((l) => {l})} - - } - /> - - - - - - - - - - - - { setTenant(e.target.value); setPage(0); }} sx={{ minWidth: 160 }} label="Tenant"> - All Tenants - {tenantsList.map((t) => {t})} - - { setLocation(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - { setRider(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Rider"> - All Riders - {riders.map((r) => {r.name})} - - - { setSearch(e.target.value); setPage(0); }} - sx={{ minWidth: 220 }} - InputProps={{ startAdornment: }} - /> - - - - { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> - {TABS.map((t, i) => ( - } /> - ))} - - - - - - - - - S.No - Tenant - Order Location - Pickup - Drop - Rider - Notes - Qty - COD - Kms - Amount - Action - - - - {paged.length === 0 ? ( - - - - - - ) : ( - paged.map((row, i) => ) - )} - -
-
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} - /> -
- - ); -} diff --git a/src/pages/Pricing.jsx b/src/pages/Pricing.jsx deleted file mode 100644 index 231bffe..0000000 --- a/src/pages/Pricing.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState, useMemo } from 'react'; -import { - Autocomplete, TextField, Box, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Chip, Typography -} from '@mui/material'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; -import EmptyState from '@/components/EmptyState'; -import { pricing, locations } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const SLAB_COLORS = { '0-5 km': 'info', '5-10 km': 'warning', '10+ km': 'success' }; -const NAME_COLORS = { Standard: 'primary', Express: 'warning', Bulk: 'success' }; - -export default function Pricing() { - const [location, setLocation] = useState(null); - - const filtered = useMemo( - () => (location ? pricing.filter((p) => p.location === location) : pricing), - [location] - ); - - return ( - <> - setLocation(v)} - sx={{ minWidth: 240 }} - renderInput={(params) => } - /> - } - /> - - - {filtered.length === 0 ? ( - - ) : ( - - - - - S.No - Location - Pricing Id - Name - Slab - Base Price - MinKm - Price/Km - MaxKm - Min Orders - - - - {filtered.map((p, i) => ( - - {i + 1} - - - - - {p.pricingId} - - - - - - - - {inr(p.basePrice)} - {p.minKm} - {inr(p.pricePerKm)} - {p.maxKm} - - - - - ))} - -
-
- )} -
- - ); -} diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx deleted file mode 100644 index a49e2b0..0000000 --- a/src/pages/Profile.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Grid, Card, CardContent, Stack, Typography, TextField } from '@mui/material'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; -import UserAvatar from '@/components/UserAvatar'; - -const PROFILE = { - userId: 'U1001', - userName: 'Aarav Menon', - appLocation: 'Bengaluru', - authName: 'admin@doormile.in', - contactNo: '+91 98450 11223', - email: 'aarav.menon@doormile.in', - address: 'No. 7, Brigade Road, MG Road Area', - location: 'Bengaluru', - city: 'Bengaluru', - state: 'Karnataka', - postcode: '560001', - role: 'Operations Administrator' -}; - -const ro = (value) => ({ - fullWidth: true, - variant: 'standard', - value, - InputProps: { readOnly: true } -}); - -export default function Profile() { - return ( - <> - - - - - - - - -
- {PROFILE.userName} - {PROFILE.role} -
-
-
-
-
- - - - - - - - - - - - - - - - - - -
- - ); -} diff --git a/src/pages/Requests.jsx b/src/pages/Requests.jsx deleted file mode 100644 index fe1a5b5..0000000 --- a/src/pages/Requests.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useState } from 'react'; -import { - Card, Stack, Button, Box, Collapse, Tabs, Tab, Typography, Grid, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, TextField -} from '@mui/material'; -import AddIcon from '@mui/icons-material/Add'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; - -import PageHeader from '@/components/PageHeader'; -import { requests } from '@/data/mock'; -import { inr } from '@/utils/format'; - -function RequestRow({ row, index }) { - const [open, setOpen] = useState(false); - const [tab, setTab] = useState(0); - - return ( - <> - *': { borderBottom: open ? 'unset' : undefined } }}> - {index + 1} - {row.requestor} - {row.bank} - {row.ifsc} - {row.refNo} - {inr(row.amount)} - {row.reason} - - setOpen((o) => !o)}> - {open ? : } - - - - - - - - setTab(v)} sx={{ mb: 2 }}> - - - - - {tab === 0 && ( - - - Contact Name - {row.contact} - - - Address - {row.address} - - - City - {row.city} - - - Zip Code - {row.zip} - - - )} - - {tab === 1 && ( - - - - # - Category - Skill - Cost/Hr - - - - {row.pricing.map((p, i) => ( - - {i + 1} - {p.category} - {p.skill} - {inr(p.cost)} - - ))} - -
- )} -
-
-
-
- - ); -} - -export default function Requests() { - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(10); - const [open, setOpen] = useState(false); - - const paged = requests.slice(page * rpp, page * rpp + rpp); - - return ( - <> - } onClick={() => setOpen(true)}> - Create Request - - } - /> - - - - - - - # - Requestor - Bank - IFSC - Ref No - Amount - Reason - - - - - {paged.map((row, idx) => ( - - ))} - -
-
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]} - /> -
- - setOpen(false)} maxWidth="sm" fullWidth> - Create Request - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index 2bac162..c5253f5 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -1,72 +1,153 @@ import { useState } from 'react'; import { - Grid, - Tabs, - Tab, - Box, - Stack, - TextField, - MenuItem, - Switch, - FormControlLabel, - Button, - Typography, - Divider, - Snackbar, - Alert + Grid, Card, Box, Stack, TextField, MenuItem, Switch, Button, Typography, Divider, + Snackbar, Alert, Chip, IconButton, InputAdornment, LinearProgress, Avatar } from '@mui/material'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined'; import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined'; +import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined'; +import BusinessOutlinedIcon from '@mui/icons-material/BusinessOutlined'; +import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; +import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; +import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded'; +import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import Visibility from '@mui/icons-material/Visibility'; +import VisibilityOff from '@mui/icons-material/VisibilityOff'; import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)']; const LANGUAGES = ['English', 'ΰ€Ήΰ€Ώΰ€¨ΰ₯ΰ€¦ΰ₯€ (Hindi)', 'Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© (Arabic)']; -function TabPanel({ value, index, children }) { - if (value !== index) return null; - return {children}; +const INITIAL_GENERAL = { + orgName: 'Doormile Logistics Pvt. Ltd.', + supportEmail: 'support@doormile.in', + contact: '+91 63749 46729', + timezone: TIMEZONES[0], + language: LANGUAGES[0] +}; +const INITIAL_NOTIFY = { + newClient: true, contractStatus: true, teamAccess: false, databaseSync: true, emailAlerts: true, smsAlerts: false +}; +const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false }; + +const NAV = [ + { icon: TuneOutlinedIcon, label: 'General', desc: 'Organisation profile' }, + { icon: NotificationsNoneIcon, label: 'Notifications', desc: 'Alerts & channels' }, + { icon: LockOutlinedIcon, label: 'Security', desc: 'Password & 2FA' } +]; + +const NOTIFY_ROWS = [ + { k: 'newClient', t: 'New client onboarded', d: 'Notify when a new client is added to the system' }, + { k: 'contractStatus', t: 'Contract status', d: 'When a client contract becomes active or expires' }, + { k: 'teamAccess', t: 'Team access', d: 'When a new team member is granted or revoked access' }, + { k: 'databaseSync', t: 'Database sync', d: 'Alerts for vector database synchronization events' } +]; + +const STRENGTH = [ + { label: 'Too weak', color: 'error' }, + { label: 'Weak', color: 'error' }, + { label: 'Fair', color: 'warning' }, + { label: 'Good', color: 'info' }, + { label: 'Strong', color: 'success' } +]; +const scorePassword = (pw) => { + let s = 0; + if (pw.length >= 8) s++; + if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++; + if (/\d/.test(pw)) s++; + if (/[^A-Za-z0-9]/.test(pw)) s++; + return s; +}; + +// Section surface with a tinted icon header band. +function Section({ icon: Icon, title, subtitle, color = 'primary', danger = false, children }) { + return ( + + `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, ${theme.palette.background.paper} 72%)` + }} + > + + + + + {title} + {subtitle && {subtitle}} + + + {children} + + ); +} + +// Two-column row: label + helper on the left, control on the right. +function Row({ label, description, children, align = 'center' }) { + return ( + + + {label} + {description && {description}} + + {children} + + ); +} + +const rightAlign = { display: 'flex', justifyContent: { sm: 'flex-end' } }; + +function PasswordField({ label, value, onChange, autoComplete }) { + const [show, setShow] = useState(false); + return ( + + setShow((s) => !s)} edge="end" size="small"> + {show ? : } + + + ) + }} + /> + ); } export default function Settings() { const [tab, setTab] = useState(0); const [toast, setToast] = useState(false); + const [dirty, setDirty] = useState(false); - // General - const [general, setGeneral] = useState({ - orgName: 'Doormile Logistics Pvt. Ltd.', - supportEmail: 'support@doormile.in', - contact: '+91 98450 11223', - timezone: TIMEZONES[0], - language: LANGUAGES[0] - }); + const [general, setGeneral] = useState(INITIAL_GENERAL); + const [notify, setNotify] = useState(INITIAL_NOTIFY); + const [security, setSecurity] = useState(INITIAL_SECURITY); - // Notifications - const [notify, setNotify] = useState({ - newOrders: true, - riderStatus: true, - invoicePaid: true, - weeklyDigest: false, - emailAlerts: true, - smsAlerts: false - }); + const setG = (k) => (e) => { setGeneral((p) => ({ ...p, [k]: e.target.value })); setDirty(true); }; + const setN = (k) => (e) => { setNotify((p) => ({ ...p, [k]: e.target.checked })); setDirty(true); }; + const setSText = (k) => (e) => { setSecurity((p) => ({ ...p, [k]: e.target.value })); setDirty(true); }; - // Security - const [security, setSecurity] = useState({ - currentPassword: '', - newPassword: '', - confirmPassword: '', - twoFactor: false - }); + const save = () => { setToast(true); setDirty(false); }; + const discard = () => { + setGeneral(INITIAL_GENERAL); + setNotify(INITIAL_NOTIFY); + setSecurity(INITIAL_SECURITY); + setDirty(false); + }; - const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value })); - const setN = (k) => (e) => setNotify((p) => ({ ...p, [k]: e.target.checked })); - const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.value ?? e.target.checked })); - - const save = () => setToast(true); + const pwScore = scorePassword(security.newPassword); + const pwMeta = STRENGTH[pwScore]; + const mismatch = security.confirmPassword && security.newPassword !== security.confirmPassword; return ( <> @@ -74,120 +155,190 @@ export default function Settings() { title="Settings" breadcrumbs={[{ label: 'Settings' }]} action={ - + + {dirty && } + + + } /> + {/* Sidebar */} - - setTab(v)} - sx={{ - '& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 } - }} - > - } iconPosition="start" label="General" /> - } iconPosition="start" label="Notifications" /> - } iconPosition="start" label="Security" /> - - + + + Preferences + + {NAV.map((item, i) => { + const active = tab === i; + const Icon = item.icon; + return ( + setTab(i)} + sx={{ + px: 1.5, py: 1.25, borderRadius: 2, cursor: 'pointer', position: 'relative', + bgcolor: active ? 'primary.lighter' : 'transparent', + transition: 'background-color .15s', + '&:hover': { bgcolor: active ? 'primary.lighter' : 'grey.50' }, + '&::before': active ? { content: '""', position: 'absolute', left: 0, top: 9, bottom: 9, width: 3, borderRadius: 3, bgcolor: 'primary.main' } : {} + }} + > + + + + + {item.label} + {item.desc} + + + + ); + })} + + + + + + + + + + Need a hand? + Our team is available 24/7 for operational support. + + + + + + {/* Content */} - {/* General */} - - - - - - - - - - - - - - - {TIMEZONES.map((t) => ( - {t} - ))} - - - - - {LANGUAGES.map((l) => ( - {l} - ))} - - - - - - - {/* Notifications */} - - - } spacing={0}> - {[ - { k: 'newOrders', t: 'New orders', d: 'Notify when a new order is placed' }, - { k: 'riderStatus', t: 'Rider status', d: 'When a rider goes online or offline' }, - { k: 'invoicePaid', t: 'Invoice paid', d: 'When a client settles an invoice' }, - { k: 'weeklyDigest', t: 'Weekly digest', d: 'A summary of operations every Monday' } - ].map((row) => ( - - - {row.t} - {row.d} - - - - ))} - - - Channels - - } label="Email alerts" /> - } label="SMS alerts" /> - - - - - {/* Security */} - + {tab === 0 && ( - - - - - - - - - - - - - - - - - - Authenticator app - - Require a one-time code at sign-in for extra security. - + {/* Organisation identity banner */} + + `linear-gradient(90deg, ${theme.palette.primary.lighter}88 0%, ${theme.palette.background.paper} 75%)` }} + > + + + + + + {general.orgName} + } label="Verified" sx={{ fontWeight: 700, bgcolor: 'success.lighter', color: 'success.dark', '& .MuiChip-icon': { color: 'inherit' } }} /> + + {general.supportEmail} Β· {general.contact} - setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} /> + - + + +
+ }> + + + + + + + + + + + + {TIMEZONES.map((t) => {t})} + + + + + {LANGUAGES.map((l) => {l})} + + + +
-
+ )} + + {tab === 1 && ( + +
+ }> + {NOTIFY_ROWS.map((row) => ( + + + + ))} + +
+
+ }> + + + + + + + +
+
+ )} + + {tab === 2 && ( + +
+ }> + + + + + + + {security.newPassword && ( + + + {pwMeta.label} + + )} + + + + + + {mismatch && Passwords do not match} + + + +
+ +
+ + + } + label={security.twoFactor ? 'Enabled' : 'Disabled'} + sx={{ fontWeight: 700, bgcolor: security.twoFactor ? 'success.lighter' : 'grey.100', color: security.twoFactor ? 'success.dark' : 'grey.600', '& .MuiChip-icon': { color: 'inherit' } }} + /> + { setSecurity((p) => ({ ...p, twoFactor: e.target.checked })); setDirty(true); }} /> + + +
+ +
+ + + + + +
+
+ )}
diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx index 6b98b22..484b6a3 100644 --- a/src/pages/auth/Login.jsx +++ b/src/pages/auth/Login.jsx @@ -16,109 +16,139 @@ import { } from '@mui/material'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; -import BoltIcon from '@mui/icons-material/Bolt'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; import Logo from '@/components/Logo'; +import bgImage from '../../assets/mid-mile-approach.jpg'; export default function Login() { const navigate = useNavigate(); const [show, setShow] = useState(false); - const [auth, setAuth] = useState('admin@doormile.in'); + const [auth, setAuth] = useState(''); const [pwd, setPwd] = useState(''); return ( - - {/* Brand panel */} - + - - - - - - Move every parcel, -
on time, every time. -
- - The command center for your last-mile operation β€” orders, riders, pricing and settlements in one corporate console. - - - {[ - { icon: BoltIcon, t: 'AI-assisted route optimisation' }, - { icon: LocalShippingOutlinedIcon, t: 'Real-time rider & delivery tracking' }, - { icon: VerifiedOutlinedIcon, t: 'Automated client invoicing & payouts' } - ].map((f) => ( - - - - - {f.t} - - ))} - -
- - Β© 2026 Doormile Logistics Pvt. Ltd. + + Welcome back + + Sign in to your Doormile operations account. -
- {/* Form panel */} - - - - Welcome back - - Sign in to your Doormile operations account. - - - - - Auth Name - setAuth(e.target.value)} /> - - - Password - setPwd(e.target.value)} - InputProps={{ - endAdornment: ( - - setShow((s) => !s)} edge="end" size="small"> - {show ? : } - - - ) - }} - /> - - - } label={Remember me} /> - Forgot password? - - + + + Auth Name + setAuth(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + bgcolor: 'rgba(255,255,255,0.6)', + borderRadius: 2, + transition: 'all 0.2s ease', + '&:hover': { bgcolor: 'rgba(255,255,255,0.9)' }, + '&.Mui-focused': { bgcolor: '#fff', boxShadow: '0 4px 12px rgba(0,0,0,0.05)' } + } + }} + /> + + + Password + setPwd(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + bgcolor: 'rgba(255,255,255,0.6)', + borderRadius: 2, + transition: 'all 0.2s ease', + '&:hover': { bgcolor: 'rgba(255,255,255,0.9)' }, + '&.Mui-focused': { bgcolor: '#fff', boxShadow: '0 4px 12px rgba(0,0,0,0.05)' } + } + }} + InputProps={{ + endAdornment: ( + + setShow((s) => !s)} edge="end" size="small"> + {show ? : } + + + ) + }} + /> + + + } label={Remember me} /> + Forgot password? - - -
+ + + + + + + Β© {new Date().getFullYear()} Doormile Logistics Pvt. Ltd. All rights reserved. + + + ); } diff --git a/src/pages/customers/CreateCustomer.jsx b/src/pages/customers/CreateCustomer.jsx deleted file mode 100644 index e5eb1cf..0000000 --- a/src/pages/customers/CreateCustomer.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Grid, Stack, Button, TextField, MenuItem, InputAdornment } from '@mui/material'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; -import { locations, tenantsList } from '@/data/mock'; - -const COUNTRY_CODES = ['+91', '+1', '+44', '+61', '+971']; - -export default function CreateCustomer() { - const navigate = useNavigate(); - const [code, setCode] = useState('+91'); - const [topLocation, setTopLocation] = useState(''); - const [client, setClient] = useState(''); - const [form, setForm] = useState({ - name: '', phone: '', email: '', doorNo: '', address: '', - location: '', city: '', state: '', postcode: '', landmark: '' - }); - const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); - - return ( - <> - - - - setTopLocation(e.target.value)} sx={{ minWidth: 220 }}> - {locations.map((l) => {l})} - - setClient(e.target.value)} sx={{ minWidth: 220 }}> - {tenantsList.map((t) => {t})} - - - - - - - - - - - setCode(e.target.value)} - InputProps={{ disableUnderline: true }} sx={{ minWidth: 56 }} - > - {COUNTRY_CODES.map((c) => {c})} - - - ) - }} - /> - - - - - - - - - - - - - {locations.map((l) => {l})} - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/customers/Customers.jsx b/src/pages/customers/Customers.jsx deleted file mode 100644 index 514a3be..0000000 --- a/src/pages/customers/Customers.jsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useState, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Stack, Button, TextField, MenuItem, InputAdornment, Box, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - Tooltip, TablePagination, Typography, - Dialog, DialogTitle, DialogContent, DialogActions, Divider -} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import AddIcon from '@mui/icons-material/Add'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; -import EmptyState from '@/components/EmptyState'; -import UserAvatar from '@/components/UserAvatar'; -import { customers, locations } from '@/data/mock'; - -const EMPTY_FORM = { - name: '', phone: '', address: '', location: '', city: '', state: '', - postcode: '', landmark: '', lat: '', lng: '' -}; - -export default function Customers() { - const navigate = useNavigate(); - const [search, setSearch] = useState(''); - const [location, setLocation] = useState('all'); - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(5); - const [editOpen, setEditOpen] = useState(false); - const [form, setForm] = useState(EMPTY_FORM); - const [viewOpen, setViewOpen] = useState(false); - const [viewer, setViewer] = useState(null); - - const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); - - const filtered = useMemo( - () => - customers.filter((c) => { - const matchLocation = location === 'all' || c.location === location; - const matchSearch = - !search || - [c.name, c.phone, c.email, c.address, c.location, c.city] - .join(' ') - .toLowerCase() - .includes(search.toLowerCase()); - return matchLocation && matchSearch; - }), - [search, location] - ); - - const paged = filtered.slice(page * rpp, page * rpp + rpp); - - const openEdit = (c) => { - setForm({ - name: c.name, phone: c.phone, address: c.address, location: c.location, - city: c.city, state: c.state, postcode: c.postcode, landmark: c.landmark || '', - lat: c.lat || '', lng: c.lng || '' - }); - setEditOpen(true); - }; - - const openView = (c) => { - setViewer(c); - setViewOpen(true); - }; - - return ( - <> - - { setLocation(e.target.value); setPage(0); }} - sx={{ minWidth: 160 }} label="Location" - > - All Locations - {locations.map((l) => {l})} - - { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }} - InputProps={{ startAdornment: }} - /> - - - } - /> - - - {filtered.length === 0 ? ( - - ) : ( - <> - - - - - # - Name - Contact - Address - Location - Action - - - - {paged.map((c, i) => ( - - {page * rpp + i + 1} - - - - {c.name} - - - - {c.phone} - {c.email} - - - {c.address} - {c.city}, {c.state} {c.postcode} - - {c.location} - - openView(c)}> - openEdit(c)}> - - - - ))} - -
-
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} - rowsPerPageOptions={[5, 10, 25]} - /> - - )} -
- - setViewOpen(false)} maxWidth="sm" fullWidth> - Customer Details - - - {viewer && ( - <> - - - - {viewer.name} - {viewer.location} - - - - {[ - ['Phone', viewer.phone], - ['Email', viewer.email], - ['Address', viewer.address], - ['City', viewer.city], - ['State', viewer.state], - ['Postcode', viewer.postcode], - ['Landmark', viewer.landmark || 'β€”'], - ['Coordinates', viewer.lat && viewer.lng ? `${viewer.lat}, ${viewer.lng}` : 'β€”'] - ].map(([label, value]) => ( - - {label} - {value} - - ))} - - - )} - - - - - - - - setEditOpen(false)} maxWidth="md" fullWidth> - Edit Customer - - - - - - - - +91 }} - /> - - - - - - - {locations.map((l) => {l})} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/invoice/InvoicePreview.jsx b/src/pages/invoice/InvoicePreview.jsx deleted file mode 100644 index 6f02edc..0000000 --- a/src/pages/invoice/InvoicePreview.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import { useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Box, Card, Stack, Button, Typography, Chip, Divider, IconButton, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Grid, - Dialog, DialogTitle, DialogContent, DialogActions, TextField -} from '@mui/material'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined'; -import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; - -import PageHeader from '@/components/PageHeader'; -import Logo from '@/components/Logo'; -import { invoices, invoiceLineItems, tenants } from '@/data/mock'; -import { inr } from '@/utils/format'; - -export default function InvoicePreview() { - const navigate = useNavigate(); - const { id } = useParams(); - const [payOpen, setPayOpen] = useState(false); - - const invoice = invoices.find((i) => String(i.id) === String(id)) || invoices[0]; - const tenant = tenants[0]; - - const subTotal = invoiceLineItems.reduce((a, l) => a + l.amount, 0); - const discount = Math.round(subTotal * 0.05); - const taxable = subTotal - discount; - const tax = Math.round(taxable * 0.18); - const grandTotal = taxable + tax; - - return ( - <> - - navigate('/invoice')} sx={{ border: 1, borderColor: 'grey.300' }}> - - - - - - - } - /> - - - - {/* Top band */} - - - - From - Doormile Logistics Pvt. Ltd. - No. 7, Brigade Road - Bengaluru, Karnataka 560001 - GSTIN: 29ABCDE1234F1Z5 - billing@doormile.in - - - INVOICE - To - {tenant.name} - {tenant.contact} - {tenant.address} - {tenant.city} {tenant.postcode} - {tenant.email} - - - - - - {/* Invoice meta */} - - - Invoice No - {invoice.invoiceId} - - - Date - {invoice.invoiceDate} - - - Due Date - {invoice.dueDate} - - - Period - {invoice.period} - - - - {/* Line items */} - - - - - S.No - Particulars - Unit - Quantity - Rate - Other Charges - Amount - - - - {invoiceLineItems.map((l, idx) => ( - - {idx + 1} - {l.particulars} - {l.unit} - {l.qty} - {inr(l.rate)} - {inr(l.other)} - {inr(l.amount)} - - ))} - -
-
- - {/* Totals */} - - - - Sub Total - {inr(subTotal)} - - - Discount (5%) - - {inr(discount)} - - - Tax (18% GST) - {inr(tax)} - - - - Grand Total - {inr(grandTotal)} - - - - - {/* Notes + accent */} - - Notes & Terms - - Payment is due within 15 days of the invoice date. Please make payments via NEFT/RTGS to the - registered account. A 1.5% monthly interest applies to overdue balances. This is a - computer-generated invoice and does not require a signature. - - - -
-
- - {/* Update Payment Dialog */} - setPayOpen(false)} maxWidth="xs" fullWidth> - Update Payment - - - - - - - - - - - - - ); -} diff --git a/src/pages/invoice/Invoices.jsx b/src/pages/invoice/Invoices.jsx deleted file mode 100644 index 30df382..0000000 --- a/src/pages/invoice/Invoices.jsx +++ /dev/null @@ -1,154 +0,0 @@ -import { useState, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Card, Stack, Button, Box, Tabs, Tab, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - Tooltip, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, Typography -} from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import dayjs from 'dayjs'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; -import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined'; - -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import StatusChip from '@/components/StatusChip'; -import TabLabelCount from '@/components/TabLabelCount'; -import { invoices } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const TABS = [ - { key: 'all', label: 'All' }, - { key: 'open', label: 'Open' }, - { key: 'overdue', label: 'Overdue' }, - { key: 'paid', label: 'Paid' } -]; - -export default function Invoices() { - const navigate = useNavigate(); - const [tab, setTab] = useState(0); - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(10); - const [dateOpen, setDateOpen] = useState(false); - const [from, setFrom] = useState(dayjs().startOf('month')); - const [to, setTo] = useState(dayjs()); - - const totals = useMemo(() => { - const sum = (s) => invoices.filter((i) => i.status === s).reduce((a, i) => a + i.amount, 0); - return { - billed: invoices.reduce((a, i) => a + i.amount, 0), - paid: sum('paid'), - open: sum('open'), - overdue: sum('overdue') - }; - }, []); - - const counts = { - all: invoices.length, - open: invoices.filter((i) => i.status === 'open').length, - overdue: invoices.filter((i) => i.status === 'overdue').length, - paid: invoices.filter((i) => i.status === 'paid').length - }; - - const tabKey = TABS[tab].key; - const filtered = useMemo( - () => invoices.filter((i) => (tabKey === 'all' ? true : i.status === tabKey)), - [tabKey] - ); - const paged = filtered.slice(page * rpp, page * rpp + rpp); - - return ( - <> - } onClick={() => setDateOpen(true)} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}> - {from.format('MMM DD')} – {to.format('MMM DD')} - - } - /> - - - - - - - - - - - { setTab(v); setPage(0); }}> - {TABS.map((t, i) => ( - } /> - ))} - - - - - - - - S.No - Client - Invoice Id - Invoice Date - Due Date - Count - Amount - Status - Action - - - - {paged.map((row, idx) => ( - - {page * rpp + idx + 1} - {row.client} - navigate(`/invoice/${row.id}`)}>{row.invoiceId} - {row.invoiceDate} - {row.dueDate} - {row.count} - {inr(row.amount)} - - - - navigate(`/invoice/${row.id}`)}> - - - - - - ))} - -
-
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]} - /> -
- - setDateOpen(false)} maxWidth="xs" fullWidth> - Filter by Date - - - Select a date range to filter invoices. - - - v && setFrom(v)} slotProps={{ textField: { fullWidth: true } }} /> - v && setTo(v)} slotProps={{ textField: { fullWidth: true } }} /> - - - - - - - - - ); -} diff --git a/src/pages/orders/AssignOrders.jsx b/src/pages/orders/AssignOrders.jsx deleted file mode 100644 index d01b7dd..0000000 --- a/src/pages/orders/AssignOrders.jsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Stack, Button, IconButton, Typography, Box, TextField, MenuItem, Chip, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow -} from '@mui/material'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import AutorenewIcon from '@mui/icons-material/Autorenew'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; -import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; -import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined'; - -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import MainCard from '@/components/MainCard'; -import StatusChip from '@/components/StatusChip'; -import UserAvatar from '@/components/UserAvatar'; -import { orders, riders } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const ZONES = ['Zone A', 'Zone B', 'Zone C', 'Zone D']; -const PROFIT = [42, 58, 36, 50, 28, 64]; - -// derive assignment rows from orders + riders mock -const ROWS = orders.slice(0, 6).map((o, i) => ({ - ...o, - zone: ZONES[i % ZONES.length], - rider: riders[i % riders.length].name, - profit: PROFIT[i % PROFIT.length] -})); - -export default function AssignOrders() { - const navigate = useNavigate(); - const [payment, setPayment] = useState('all'); - const [rider, setRider] = useState('auto'); - - return ( - <> - - navigate('/orders')} size="small"> - Assign Orders - - } - breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Assign Orders' }]} - action={ - - } - /> - - - - - - - - - - - - - - # - Zone - Tenant - Order Location - Pickup - Delivery - Notes - Rider - Type - Profit - Charges - KMS - - - - {ROWS.map((r) => ( - - {r.id} - - - - {r.tenant} - {r.location} - {r.pickup} - {r.drop} - {r.notes || 'β€”'} - - - - {r.rider} - - - - {inr(r.profit)} - {inr(r.charges)} - {r.kms} - - ))} - -
-
-
- - - - - setPayment(e.target.value)}> - All Payments - Prepaid - COD - - - - setRider(e.target.value)}> - Auto Assign - {riders.map((rd) => {rd.name})} - - - - - - - - - - - ); -} diff --git a/src/pages/orders/CreateMultipleOrders.jsx b/src/pages/orders/CreateMultipleOrders.jsx deleted file mode 100644 index b5d948d..0000000 --- a/src/pages/orders/CreateMultipleOrders.jsx +++ /dev/null @@ -1,254 +0,0 @@ -import { useState, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, Box, - RadioGroup, Radio, FormControlLabel, FormControl, FormLabel, Chip, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip, - Dialog, DialogTitle, DialogContent, DialogActions, List, ListItem, ListItemButton, - ListItemIcon, ListItemText, Checkbox, InputAdornment -} from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import dayjs from 'dayjs'; -import UploadFileOutlinedIcon from '@mui/icons-material/UploadFileOutlined'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import SearchIcon from '@mui/icons-material/Search'; -import AddIcon from '@mui/icons-material/Add'; -import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; -import UserAvatar from '@/components/UserAvatar'; -import { locations, tenantsList, tenants, customers } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const TIME_SLOTS = ['09:00 - 11:00', '11:00 - 13:00', '13:00 - 15:00', '15:00 - 17:00', '17:00 - 19:00']; - -// derive table rows from the customers mock -const buildRows = () => - customers.slice(0, 4).map((c, i) => ({ - id: c.id, - customer: c.name, - address: c.address, - qty: [2, 1, 3, 1][i], - cash: [0, 1299, 0, 450][i], - kms: [4.2, 12.8, 6.5, 9.1][i], - charge: [86, 142, 98, 110][i] - })); - -export default function CreateMultipleOrders() { - const navigate = useNavigate(); - const [date, setDate] = useState(dayjs()); - const [slot, setSlot] = useState(''); - const [dropMode, setDropMode] = useState('csv'); - const [rows, setRows] = useState(buildRows); - const [dialogOpen, setDialogOpen] = useState(false); - const [search, setSearch] = useState(''); - const [picked, setPicked] = useState([]); - - const totals = useMemo( - () => - rows.reduce( - (acc, r) => ({ - qty: acc.qty + r.qty, - cash: acc.cash + r.cash, - kms: +(acc.kms + r.kms).toFixed(1), - charge: acc.charge + r.charge - }), - { qty: 0, cash: 0, kms: 0, charge: 0 } - ), - [rows] - ); - - const removeRow = (id) => setRows((p) => p.filter((r) => r.id !== id)); - - const filteredCustomers = customers.filter( - (c) => !search || `${c.name} ${c.location}`.toLowerCase().includes(search.toLowerCase()) - ); - const toggle = (id) => setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id])); - - const addSelected = () => { - const existing = new Set(rows.map((r) => r.id)); - const additions = customers - .filter((c) => picked.includes(c.id) && !existing.has(c.id)) - .map((c) => ({ id: c.id, customer: c.name, address: c.address, qty: 1, cash: 0, kms: 5, charge: 90 })); - setRows((p) => [...p, ...additions]); - setPicked([]); - setDialogOpen(false); - }; - - return ( - <> - - - - - - {/* top selects */} - - - - {locations.map((l) => {l})} - - - - - {tenantsList.map((t) => {t})} - - - - - {tenants.map((t) => {t.address}, {t.city})} - - - - - - - - - - - setSlot(e.target.value)}> - {TIME_SLOTS.map((s) => {s})} - - - - - - Drop - - - Add drop points using - setDropMode(e.target.value)}> - } label="Excel / CSV" /> - } label="Selection" /> - - - - - {dropMode === 'csv' ? ( - - - {}} - sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} - /> - - ) : ( - - )} - - - - - - - - - - - S.No - Customer - Address - Quantity - Cash Collect - Kms - Charge - Action - - - - {rows.map((r, i) => ( - - {i + 1} - - - - {r.customer} - - - {r.address} - {r.qty} - {r.cash ? inr(r.cash) : 'β€”'} - {r.kms} - {inr(r.charge)} - - - removeRow(r.id)}> - - - - - - ))} - - Total - {totals.qty} - {inr(totals.cash)} - {totals.kms} - {inr(totals.charge)} - - - -
-
-
- - - - - - - - - -
- - {/* Customer selection dialog */} - setDialogOpen(false)} fullWidth maxWidth="sm"> - Select Customers - - setSearch(e.target.value)} - sx={{ mb: 1.5 }} - InputProps={{ startAdornment: }} - /> - - {filteredCustomers.map((c) => ( - - toggle(c.id)}> - - - - - - - - ))} - - - - - - - - - ); -} diff --git a/src/pages/orders/CreateOrder.jsx b/src/pages/orders/CreateOrder.jsx deleted file mode 100644 index 5613814..0000000 --- a/src/pages/orders/CreateOrder.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, - Autocomplete, FormControlLabel, Checkbox, Box -} from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import dayjs from 'dayjs'; -import AddIcon from '@mui/icons-material/Add'; - -import PageHeader from '@/components/PageHeader'; -import { locations, tenantsList, tenants } from '@/data/mock'; - -const TIME_SLOTS = ['09:00 - 11:00', '11:00 - 13:00', '13:00 - 15:00', '15:00 - 17:00', '17:00 - 19:00']; - -const SectionTitle = ({ children }) => ( - <> - {children} - - -); - -function AddressFields({ saveForLater, onSaveForLater }) { - return ( - - - - - - - - - - - - - - - - {locations.map((l) => {l})} - - - - - - - - - - onSaveForLater(e.target.checked)} />} - label="Save for later" - /> - - - ); -} - -export default function CreateOrder() { - const navigate = useNavigate(); - const [deliveryDate, setDeliveryDate] = useState(dayjs()); - const [slot, setSlot] = useState(''); - const [savePickup, setSavePickup] = useState(false); - const [saveDrop, setSaveDrop] = useState(false); - - return ( - <> - - - - - {/* Top row */} - - - } - /> - - - } - /> - - - `${t.name} β€” ${t.address}`)} - renderInput={(params) => } - /> - - - - - Pickup Details - - - - - Drop Details - - - - - Schedule - - - - - - setSlot(e.target.value)}> - {TIME_SLOTS.map((s) => {s})} - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/orders/OrderDetails.jsx b/src/pages/orders/OrderDetails.jsx deleted file mode 100644 index cab61d6..0000000 --- a/src/pages/orders/OrderDetails.jsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useParams, useNavigate } from 'react-router-dom'; -import { - Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, IconButton, Avatar, - Step, Stepper, StepLabel, StepConnector, stepConnectorClasses, styled, Chip -} from '@mui/material'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import CallOutlinedIcon from '@mui/icons-material/CallOutlined'; -import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; -import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; -import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; -import CheckIcon from '@mui/icons-material/Check'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; -import StatusChip from '@/components/StatusChip'; -import MapPlaceholder from '@/components/MapPlaceholder'; -import UserAvatar from '@/components/UserAvatar'; -import { orders, orderTimeline, deliveries } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const RedConnector = styled(StepConnector)(({ theme }) => ({ - [`& .${stepConnectorClasses.line}`]: { borderColor: theme.palette.grey[300], borderLeftWidth: 2, minHeight: 28 }, - [`&.${stepConnectorClasses.active} .${stepConnectorClasses.line}, &.${stepConnectorClasses.completed} .${stepConnectorClasses.line}`]: - { borderColor: theme.palette.primary.main } -})); - -function Dot({ active }) { - return ( - - {active ? : } - - ); -} - -export default function OrderDetails() { - const { id } = useParams(); - const navigate = useNavigate(); - const order = orders.find((o) => o.id === id) || orders[1]; - const delivery = deliveries.find((d) => d.id === order.id) || deliveries[1]; - - return ( - <> - - navigate('/orders')} size="small"> - Order Details - - } - breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: order.id }]} - action={ - - - - - } - /> - - - {/* Left column */} - - - - - - - Order ID - {order.id} - - - - - - - - } /> - - {inr(order.charges)}} /> - - - - - - - - - {order.customer} - Recipient - - - - - - - - - - } sx={{ ml: 0.5 }}> - {orderTimeline.map((s) => ( - - }> - - {s.label} - {s.time} - - - - ))} - - - - - - {/* Right column */} - - - - - - - - - - - - - - - - - - {delivery.rider} - Rider Β· +91 98450 11223 - - - - - - - - - - - ); -} - -const Row = ({ label, value }) => ( - - {label} - {typeof value === 'string' ? {value} : value} - -); - -const IconRow = ({ icon: Icon, text }) => ( - - - {text} - -); - -const RouteEnd = ({ color, title, text }) => ( - - - - {title} - {text} - - -); diff --git a/src/pages/orders/OrdersList.jsx b/src/pages/orders/OrdersList.jsx deleted file mode 100644 index 62a42bf..0000000 --- a/src/pages/orders/OrdersList.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useState, useMemo } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { - Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, IconButton, - Tooltip, TablePagination, Typography, SpeedDial, SpeedDialAction, Chip -} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import AddIcon from '@mui/icons-material/Add'; -import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; -import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined'; -import TuneIcon from '@mui/icons-material/Tune'; - -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import StatusChip from '@/components/StatusChip'; -import TabLabelCount from '@/components/TabLabelCount'; -import { orders } from '@/data/mock'; -import { inr } from '@/utils/format'; - -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; - -const TABS = [ - { key: 'created', label: 'Created' }, - { key: 'pending', label: 'Pending' }, - { key: 'delivered', label: 'Delivered' }, - { key: 'cancelled', label: 'Cancelled' } -]; - -export default function OrdersList() { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const [tab, setTab] = useState(0); - const [search, setSearch] = useState(searchParams.get('q') || ''); - const [tenant, setTenant] = useState('all'); - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(5); - const [selected, setSelected] = useState([]); - - const tabKey = TABS[tab].key; - const filtered = useMemo( - () => - orders.filter((o) => { - const matchTab = tabKey === 'created' ? true : o.status === tabKey; - const matchTenant = tenant === 'all' || o.tenant === tenant; - const matchSearch = - !search || - [o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase()); - return matchTab && matchTenant && matchSearch; - }), - [tabKey, tenant, search] - ); - - const paged = filtered.slice(page * rpp, page * rpp + rpp); - const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id])); - const counts = { - created: orders.length, - pending: orders.filter((o) => o.status === 'pending').length, - delivered: orders.filter((o) => o.status === 'delivered').length, - cancelled: orders.filter((o) => o.status === 'cancelled').length - }; - - return ( - <> - - - - - } - /> - - - - - - - - - - {/* filter toolbar */} - - setSearch(e.target.value)} - sx={{ minWidth: 240 }} - InputProps={{ startAdornment: }} - /> - - - setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant"> - All Tenants - {[...new Set(orders.map((o) => o.tenant))].map((t) => {t})} - - - All Locations - {[...new Set(orders.map((o) => o.location))].map((l) => {l})} - - - - - { setTab(v); setPage(0); }}> - {TABS.map((t, i) => ( - } /> - ))} - - - - {selected.length > 0 && ( - - {selected.length} selected - - - )} - - - - - - - 0 && selected.length < paged.length} - checked={paged.length > 0 && selected.length === paged.length} - onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])} - /> - - # - Tenant - Location - Pickup - Drop - QTY - COD - KMS - Charges - Notes - Status - Actions - - - - {paged.map((o) => ( - - toggle(o.id)} /> - navigate(`/orders/${o.id}`)}>{o.id} - {o.tenant} - {o.location} - {o.pickup} - {o.drop} - {o.qty} - {o.cod ? inr(o.cod) : 'β€”'} - {o.kms} - {inr(o.charges)} - {o.notes || 'β€”'} - - - navigate(`/orders/${o.id}`)}> - - - - - ))} - -
-
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} - /> -
- - } sx={{ position: 'fixed', bottom: 28, right: 28 }} FabProps={{ color: 'primary' }}> - } tooltipTitle="AI Optimisation" onClick={() => navigate('/orders/assign')} /> - } tooltipTitle="Manual Assign" onClick={() => navigate('/orders/assign')} /> - } tooltipTitle="Delete" /> - - - ); -} diff --git a/src/pages/reports/OrdersDetails.jsx b/src/pages/reports/OrdersDetails.jsx deleted file mode 100644 index e052756..0000000 --- a/src/pages/reports/OrdersDetails.jsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useState, useMemo } from 'react'; -import { - Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, IconButton, Tooltip, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Typography, - Dialog, DialogTitle, DialogContent, DialogActions -} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; -import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; -import CloseIcon from '@mui/icons-material/Close'; - -import PageHeader from '@/components/PageHeader'; -import StatusChip from '@/components/StatusChip'; -import MapPlaceholder from '@/components/MapPlaceholder'; -import { ordersDetailReport, locations, tenantsList } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const STATUSES = ['all', 'created', 'pending', 'picked', 'active', 'delivered', 'cancelled']; - -export default function OrdersDetails() { - const [location, setLocation] = useState('all'); - const [tenant, setTenant] = useState('all'); - const [loc2, setLoc2] = useState('all'); - const [status, setStatus] = useState('all'); - const [search, setSearch] = useState(''); - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(10); - const [mapRow, setMapRow] = useState(null); - const [exportOpen, setExportOpen] = useState(false); - - const filtered = useMemo( - () => - ordersDetailReport.filter((o) => { - const matchStatus = status === 'all' || o.status === status; - const matchTenant = tenant === 'all' || o.client === tenant; - const matchSearch = - !search || - [o.id, o.client, o.pickup, o.drop].join(' ').toLowerCase().includes(search.toLowerCase()); - return matchStatus && matchTenant && matchSearch; - }), - [status, tenant, search] - ); - - const paged = filtered.slice(page * rpp, page * rpp + rpp); - - return ( - <> - setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} -
- } - /> - - - - setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant"> - All Tenants - {tenantsList.map((t) => {t})} - - setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - - { setStatus(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Status"> - {STATUSES.map((s) => {s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)})} - - { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }} - InputProps={{ startAdornment: }} - /> - - - - - - - - - # - - Client - Pickup - Drop - Status - Assigned - Accepted - Arrived - Picked - Active - Delivered - Cancelled - Notes - KMS - Charges - - - - {paged.map((o, i) => ( - - {page * rpp + i + 1} - - - setMapRow(o)}> - - - {o.client} - {o.pickup} - {o.drop} - - {o.assigned} - {o.accepted} - {o.arrived} - {o.picked} - {o.active} - {o.delivered} - {o.cancelled} - {o.notes || 'β€”'} - {o.kms} - {inr(o.charges)} - - ))} - -
-
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[10, 25, 50]} - /> -
- - {/* Map dialog */} - setMapRow(null)} fullScreen> - - - Route β€” {mapRow?.id} - - {mapRow?.pickup} β†’ {mapRow?.drop} - - - setMapRow(null)}> - - - - - - - {/* Export dialog */} - setExportOpen(false)} maxWidth="xs" fullWidth> - Export Report - - - The export will include {filtered.length} record(s) matching the current filters: - - - - - - - - - - - - - - - - - ); -} - -function Filter({ label, value }) { - return ( - - {label} - {value} - - ); -} diff --git a/src/pages/reports/OrdersSummary.jsx b/src/pages/reports/OrdersSummary.jsx deleted file mode 100644 index f97320a..0000000 --- a/src/pages/reports/OrdersSummary.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useState, Fragment } from 'react'; -import { - Grid, Card, Stack, Button, TextField, MenuItem, Box, Collapse, IconButton, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography -} from '@mui/material'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; - -import PageHeader from '@/components/PageHeader'; -import { ordersSummary, locations, tenantsList } from '@/data/mock'; -import { inr } from '@/utils/format'; - -// Show 0 in red, anything else normally. -function NumCell({ value, align = 'center', bold = false }) { - const zero = Number(value) === 0; - return ( - - {value} - - ); -} - -export default function OrdersSummary() { - const [open, setOpen] = useState({}); - const [location, setLocation] = useState('all'); - const [tenant, setTenant] = useState('all'); - const [loc2, setLoc2] = useState('all'); - - const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] })); - - const totals = ordersSummary.reduce( - (a, r) => ({ - oPending: a.oPending + r.orders.pending, - oCancelled: a.oCancelled + r.orders.cancelled, - oCompleted: a.oCompleted + r.orders.completed, - dPending: a.dPending + r.deliveries.pending, - dCancelled: a.dCancelled + r.deliveries.cancelled, - dCompleted: a.dCompleted + r.deliveries.completed, - collection: a.collection + r.collection, - kms: a.kms + r.kms, - actualKms: a.actualKms + r.actualKms, - amount: a.amount + r.amount - }), - { oPending: 0, oCancelled: 0, oCompleted: 0, dPending: 0, dCancelled: 0, dCompleted: 0, collection: 0, kms: 0, actualKms: 0, amount: 0 } - ); - - const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' }; - - return ( - <> - setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} -
- } - /> - - - - - - setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant"> - All Tenants - {tenantsList.map((t) => {t})} - - setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - - - - - - - - # - Tenant / Location - Orders - Deliveries - Collection Amt - Kms / Actual - Amount - Action - - - Pending - Cancelled - Completed - Pending - Cancelled - Completed - - - - {ordersSummary.map((r, idx) => ( - - - - toggle(r.id)}> - {open[r.id] ? : } - - - {idx + 1} - - {r.tenant} - {r.location} - - - - - - - - {inr(r.collection)} - {r.kms} / {r.actualKms} - {inr(r.amount)} - - toggle(r.id)}> - - - - - - - Riders -
- - - # - Rider - Orders - Deliveries - Pending - Cancelled - Completed - Collection Amt - Kms / Actual - Charges - - - - {r.riders.map((rd, i) => ( - - {i + 1} - {rd.rider} - - - - - - {inr(rd.collection)} - {rd.kms} / {rd.actualKms} - {inr(rd.charges)} - - ))} - -
- - - - - - ))} - - {/* totals */} - - - Totals - - - - - - - {inr(totals.collection)} - {totals.kms} / {totals.actualKms} - {inr(totals.amount)} - - - - -
-
- - ); -} diff --git a/src/pages/reports/RidersLogs.jsx b/src/pages/reports/RidersLogs.jsx deleted file mode 100644 index c6ee68b..0000000 --- a/src/pages/reports/RidersLogs.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useState, useMemo } from 'react'; -import { - Box, Paper, Stack, Button, TextField, InputAdornment, IconButton, Checkbox, Typography, Divider, Tooltip -} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import MenuIcon from '@mui/icons-material/Menu'; -import RefreshIcon from '@mui/icons-material/Refresh'; - -import PageHeader from '@/components/PageHeader'; -import MapPlaceholder from '@/components/MapPlaceholder'; -import { ridersLive } from '@/data/mock'; - -// spread active riders across the map at distinct positions -const POSITIONS = [ - { x: '24%', y: '32%' }, - { x: '58%', y: '28%' }, - { x: '40%', y: '60%' }, - { x: '72%', y: '55%' }, - { x: '30%', y: '72%' }, - { x: '64%', y: '78%' } -]; - -export default function RidersLogs() { - const [search, setSearch] = useState(''); - const [selected, setSelected] = useState(ridersLive.filter((r) => r.active).map((r) => r.userid)); - - const filtered = useMemo( - () => - ridersLive.filter((r) => - !search || [r.name, r.phone, r.userid].join(' ').toLowerCase().includes(search.toLowerCase()) - ), - [search] - ); - - const allChecked = filtered.length > 0 && filtered.every((r) => selected.includes(r.userid)); - const someChecked = filtered.some((r) => selected.includes(r.userid)) && !allChecked; - - const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id])); - const toggleAll = (checked) => - setSelected(checked ? [...new Set([...selected, ...filtered.map((r) => r.userid)])] : selected.filter((id) => !filtered.some((r) => r.userid === id))); - - const mapRiders = ridersLive - .filter((r) => r.active && selected.includes(r.userid)) - .map((r, i) => ({ ...POSITIONS[i % POSITIONS.length], active: true })); - - return ( - <> - - - - {/* Left side panel */} - - - setSearch(e.target.value)} - InputProps={{ startAdornment: }} - /> - - - - toggleAll(e.target.checked)} /> - All - - {selected.length} selected - - - - {filtered.map((r) => ( - - toggle(r.userid)} sx={{ mt: -0.5 }} /> - - - - {r.name} - - {r.phone} - - {r.userid} - {r.lastLog} - - - - ))} - {filtered.length === 0 && ( - No riders found - )} - - - - {/* Map area */} - - - - Riders Locations - - - - - - - ); -} diff --git a/src/pages/reports/RidersSummary.jsx b/src/pages/reports/RidersSummary.jsx deleted file mode 100644 index 1203e58..0000000 --- a/src/pages/reports/RidersSummary.jsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useState, Fragment } from 'react'; -import { - Card, Stack, Button, TextField, MenuItem, Box, Collapse, IconButton, Chip, Tooltip, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, - Dialog, DialogTitle, DialogContent -} from '@mui/material'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import RoomOutlinedIcon from '@mui/icons-material/RoomOutlined'; -import CloseIcon from '@mui/icons-material/Close'; - -import PageHeader from '@/components/PageHeader'; -import UserAvatar from '@/components/UserAvatar'; -import MapPlaceholder from '@/components/MapPlaceholder'; -import { ridersSummary, locations } from '@/data/mock'; -import { inr } from '@/utils/format'; - -function NumCell({ value, align = 'center' }) { - const zero = Number(value) === 0; - return {value}; -} - -function KmsChips({ kms, actual }) { - return ( - - - - - ); -} - -export default function RidersSummary() { - const [open, setOpen] = useState({}); - const [location, setLocation] = useState('all'); - const [mapRider, setMapRider] = useState(null); - - const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] })); - const totalAmount = ridersSummary.reduce((a, r) => a + r.amount, 0); - - const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' }; - - return ( - <> - setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} -
- } - /> - - - - - - - - - - - - # - Rider - Orders - Pending - Cancelled - Delivered - KMS - Amount - Action - - - - {ridersSummary.map((r, idx) => ( - - - {idx + 1} - - - - {r.rider} - - - - - - - - {inr(r.amount)} - - - toggle(r.id)}> - {open[r.id] ? : } - - - - setMapRider(r)}> - - - - - - - - Client Summary -
- - - # - Client - All - Pending - Completed - Cancelled - Kms - Amount - - - - {r.clients.map((c, i) => ( - - {i + 1} - {c.client} - - - - - - {inr(c.amount)} - - ))} - -
- - - - - - ))} - - -
- - - Total Amount - {inr(totalAmount)} - -
- - setMapRider(null)} maxWidth="md" fullWidth> - - {mapRider?.rider} β€” Location - setMapRider(null)}> - - - - - - - ); -} diff --git a/src/pages/riders/CreateRider.jsx b/src/pages/riders/CreateRider.jsx deleted file mode 100644 index c84c08f..0000000 --- a/src/pages/riders/CreateRider.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { - Grid, Card, CardContent, Stack, Button, TextField, Divider, InputAdornment -} from '@mui/material'; -import AddIcon from '@mui/icons-material/Add'; - -import PageHeader from '@/components/PageHeader'; - -export default function CreateRider() { - const navigate = useNavigate(); - - return ( - <> - - - - - - - - - - +91 }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/riders/EditRider.jsx b/src/pages/riders/EditRider.jsx deleted file mode 100644 index 898694b..0000000 --- a/src/pages/riders/EditRider.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, Box, IconButton, InputAdornment -} from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import dayjs from 'dayjs'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; - -import PageHeader from '@/components/PageHeader'; -import { riders, tenantsList } from '@/data/mock'; - -const SHIFTS = ['Morning', 'Evening', 'Night']; -const ACCOUNT_TYPES = ['Savings', 'Current']; -const VEHICLES = ['Honda Activa', 'TVS Jupiter', 'Hero Splendor', 'Bajaj Pulsar', 'Honda Dio']; - -const SectionTitle = ({ children }) => ( - <> - {children} - - -); - -export default function EditRider() { - const navigate = useNavigate(); - const rider = riders[0]; - const [firstName] = rider.name.split(' '); - const lastName = rider.name.split(' ').slice(1).join(' '); - const [insuranceExpiry, setInsuranceExpiry] = useState(dayjs().add(8, 'month')); - - return ( - <> - - navigate('/riders')}> - Edit Rider - - } - breadcrumbs={[{ label: 'Riders', to: '/riders' }, { label: 'Edit Rider' }]} - /> - - - - Contact Information - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {tenantsList.map((t) => {t})} - - - - - - Charges - - - - {SHIFTS.map((s) => {s})} - - - - β‚Ή }} /> - - - β‚Ή }} /> - - - β‚Ή }} /> - - - - - - Bank Details - - - - - - - - - - {ACCOUNT_TYPES.map((a) => {a})} - - - - - - - - - - - - - - - - Vehicle Details - - - - {VEHICLES.map((v) => {v})} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/riders/Riders.jsx b/src/pages/riders/Riders.jsx deleted file mode 100644 index fc03fad..0000000 --- a/src/pages/riders/Riders.jsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useState, useMemo, Fragment } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip, - TablePagination, Typography, Chip, Collapse -} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import AddIcon from '@mui/icons-material/Add'; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import PowerSettingsNewOutlinedIcon from '@mui/icons-material/PowerSettingsNewOutlined'; - -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import StatusChip from '@/components/StatusChip'; -import UserAvatar from '@/components/UserAvatar'; -import TabLabelCount from '@/components/TabLabelCount'; -import { riders, riderLogs, locations } from '@/data/mock'; -import { inr } from '@/utils/format'; - -const TABS = [ - { key: 'all', label: 'ALL' }, - { key: 'active', label: 'Active' } -]; - -export default function Riders() { - const navigate = useNavigate(); - const [tab, setTab] = useState(0); - const [search, setSearch] = useState(''); - const [location, setLocation] = useState('all'); - const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(5); - const [expanded, setExpanded] = useState(null); - - const tabKey = TABS[tab].key; - const filtered = useMemo( - () => - riders.filter((r) => { - const matchTab = tabKey === 'all' ? true : r.status !== 'offline'; - const matchLocation = location === 'all' || r.address.includes(location); - const matchSearch = - !search || - [r.id, r.userId, r.name, r.phone, r.address, r.vehicle, r.vehicleNo] - .join(' ') - .toLowerCase() - .includes(search.toLowerCase()); - return matchTab && matchLocation && matchSearch; - }), - [tabKey, location, search] - ); - - const paged = filtered.slice(page * rpp, page * rpp + rpp); - - const counts = { - total: riders.length, - active: riders.filter((r) => r.status === 'online').length, - onDelivery: riders.filter((r) => r.status === 'on-delivery').length, - offline: riders.filter((r) => r.status === 'offline').length, - all: riders.length, - activeTab: riders.filter((r) => r.status !== 'offline').length - }; - - return ( - <> - - setLocation(e.target.value)} sx={{ minWidth: 170 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - - - } - /> - - - - - - - - - - - { setSearch(e.target.value); setPage(0); }} - sx={{ minWidth: 240 }} - InputProps={{ startAdornment: }} - /> - - - - - { setTab(v); setPage(0); }}> - {TABS.map((t, i) => ( - } - /> - ))} - - - - - - - - S.NO - User ID - Rider - Address - Vehicle - Shift - Time - Fare - Fuel - Status - Action - - - - {paged.map((r, i) => ( - - - {page * rpp + i + 1} - {r.userId} - - - - - {r.name} - {r.phone} - - - - {r.address} - - {r.vehicle} - {r.vehicleNo} - - {r.shift} - - - - - - - {inr(r.fare)} - {inr(r.fuel)} - - - - navigate(`/riders/${r.id}/edit`)}> - - - setExpanded(expanded === r.id ? null : r.id)}> - {expanded === r.id ? : } - - - - - - - - - Rider Logs -
- - - Location - Battery - Charging - Speed - Accuracy - Time - Order - Status - - - - {riderLogs.map((log, li) => ( - - {log.location} - {log.battery} - {log.charging} - {log.speed} - {log.accuracy} - {log.time} - {log.order} - - - ))} - -
- - - - - - ))} - - -
- setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} - /> -
- - ); -} diff --git a/src/pages/team/TeamUsers.jsx b/src/pages/team/TeamUsers.jsx new file mode 100644 index 0000000..1f23716 --- /dev/null +++ b/src/pages/team/TeamUsers.jsx @@ -0,0 +1,304 @@ +import { useState, useMemo, useEffect } from 'react'; +import { + Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Chip, Link, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, + TablePagination, Typography, CircularProgress, Alert, Tooltip, + Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import AddIcon from '@mui/icons-material/Add'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; +import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; +import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined'; +import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined'; +import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined'; +import ManageAccountsOutlinedIcon from '@mui/icons-material/ManageAccountsOutlined'; +import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'; + +import PageHeader from '@/components/PageHeader'; +import StatusChip from '@/components/StatusChip'; +import EmptyState from '@/components/EmptyState'; +import UserAvatar from '@/components/UserAvatar'; +import TabLabelCount from '@/components/TabLabelCount'; +import { fetchPoints, deletePoint, COLLECTIONS } from '@/utils/qdrant'; +import UserFormDialog from './UserFormDialog'; + +// Map a raw Qdrant point from doormile_auth to a flat team-user row. +function toUser(point) { + const p = point.payload || {}; + return { + id: point.id, + name: p.name || 'β€”', + email: p.email || '', + phone: p.phone || '', + role: p.role || 'unknown', + pin: p.pin || '' + }; +} + +const titleCase = (s) => String(s || '').replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + +// Per-role accent colour + icon, used for the avatar badge and role chip. +const ROLE_META = { + admin: { color: 'primary', icon: AdminPanelSettingsOutlinedIcon }, + manager: { color: 'success', icon: ManageAccountsOutlinedIcon }, + rep: { color: 'info', icon: BadgeOutlinedIcon }, + support: { color: 'warning', icon: SupportAgentOutlinedIcon } +}; +const roleMeta = (role) => ROLE_META[String(role || '').toLowerCase()] || { color: 'secondary', icon: PersonOutlineOutlinedIcon }; + +// Avatar with a small role-coloured badge in the corner. +function UserIdentity({ name, email, role }) { + const { color, icon: RoleIcon } = roleMeta(role); + const handle = email ? email.split('@')[0] : ''; + return ( + + + + + + + + + {name} + {handle && @{handle}} + + + ); +} + +function RoleCell({ role }) { + const { color, icon: RoleIcon } = roleMeta(role); + return ( + } + label={titleCase(role)} + sx={{ + fontWeight: 600, + bgcolor: `${color === 'secondary' ? 'grey.100' : `${color}.lighter`}`, + color: `${color === 'secondary' ? 'grey.700' : `${color}.dark`}`, + '& .MuiChip-icon': { color: 'inherit' } + }} + /> + ); +} + +export default function TeamUsers() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [tab, setTab] = useState(0); + const [search, setSearch] = useState(''); + const [page, setPage] = useState(0); + const [rpp, setRpp] = useState(10); + + const [dialog, setDialog] = useState({ open: false, mode: 'add', initial: null }); + const [toDelete, setToDelete] = useState(null); + const [deleting, setDeleting] = useState(false); + + const load = () => { + setLoading(true); + setError(null); + fetchPoints(COLLECTIONS.teamUsers) + .then((points) => setUsers(points.map(toUser))) + .catch((e) => setError(e.message || 'Failed to load team users')) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + const tabs = useMemo(() => { + const seen = []; + users.forEach((u) => { if (!seen.includes(u.role)) seen.push(u.role); }); + return [{ key: 'all', label: 'All' }, ...seen.map((r) => ({ key: r, label: titleCase(r) }))]; + }, [users]); + + const tabKey = tabs[Math.min(tab, tabs.length - 1)]?.key || 'all'; + + const counts = useMemo(() => { + const c = { all: users.length }; + users.forEach((u) => { c[u.role] = (c[u.role] || 0) + 1; }); + return c; + }, [users]); + + const filtered = useMemo( + () => + users.filter((u) => { + const matchTab = tabKey === 'all' || u.role === tabKey; + const matchSearch = + !search || [u.name, u.email, u.phone, u.role].join(' ').toLowerCase().includes(search.toLowerCase()); + return matchTab && matchSearch; + }), + [users, tabKey, search] + ); + + const paged = filtered.slice(page * rpp, page * rpp + rpp); + + const handleSaved = () => { setDialog({ open: false, mode: 'add', initial: null }); load(); }; + + const confirmDelete = async () => { + setDeleting(true); + try { + await deletePoint(COLLECTIONS.teamUsers, toDelete.id); + setToDelete(null); + load(); + } catch (e) { + setError(e.message || 'Failed to delete user'); + } finally { + setDeleting(false); + } + }; + + return ( + <> + + + + + } + /> + + + `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)` + }} + > + + + + + App Users Directory + Manage console members, roles and access + + + + + { setSearch(e.target.value); setPage(0); }} + sx={{ minWidth: 300 }} + InputProps={{ startAdornment: }} + /> + + {!loading && ( + + + {filtered.length} {filtered.length === 1 ? 'user' : 'users'} + + + + )} + + + + { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> + {tabs.map((t, i) => ( + } /> + ))} + + + + {error && Retry}>{error}} + + + + + + S.No + User + Email + Phone + Role + Actions + + + + {loading ? ( + + + + + + ) : paged.length === 0 ? ( + + + + + + ) : ( + paged.map((row, i) => ( + + {page * rpp + i + 1} + + + {row.email ? ( + + {row.email} + + ) : β€”} + + + {row.phone ? ( + + {row.phone} + + ) : β€”} + + + + setDialog({ open: true, mode: 'edit', initial: row })}> + setToDelete(row)}> + + + )) + )} + +
+
+ setPage(p)} + rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} + /> +
+ + setDialog({ open: false, mode: 'add', initial: null })} + onSaved={handleSaved} + /> + + setToDelete(null)}> + Delete user? + + + This will permanently remove {toDelete?.name} from the doormile_auth collection. This cannot be undone. + + + + + + + + + ); +} diff --git a/src/pages/team/UserFormDialog.jsx b/src/pages/team/UserFormDialog.jsx new file mode 100644 index 0000000..67bc237 --- /dev/null +++ b/src/pages/team/UserFormDialog.jsx @@ -0,0 +1,86 @@ +import { useState, useEffect } from 'react'; +import { + Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField, + MenuItem, Alert, CircularProgress, IconButton +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant'; + +const ROLES = ['admin', 'rep', 'manager']; + +const EMPTY = { name: '', email: '', phone: '', role: 'rep', pin: '' }; + +const withValue = (opts, v) => (v && !opts.includes(v) ? [v, ...opts] : opts); + +export default function UserFormDialog({ open, mode, initial, onClose, onSaved }) { + const isEdit = mode === 'edit'; + const [form, setForm] = useState(EMPTY); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (open) { + setForm({ ...EMPTY, ...(initial || {}) }); + setError(null); + } + }, [open, initial]); + + const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); + + const handleSave = async () => { + if (!form.name.trim()) { setError('Name is required.'); return; } + if (!form.email.trim()) { setError('Email is required.'); return; } + setSaving(true); + setError(null); + + const payload = { + name: form.name.trim(), + email: form.email.trim(), + phone: form.phone, + role: form.role, + ...(form.pin ? { pin: String(form.pin) } : {}) + }; + + try { + if (isEdit) { + await setPayload(COLLECTIONS.teamUsers, initial.id, payload); + } else { + await createPoint(COLLECTIONS.teamUsers, payload); + } + onSaved(); + } catch (e) { + setError(e.message || 'Failed to save user'); + } finally { + setSaving(false); + } + }; + + return ( + + + {isEdit ? 'Edit Team User' : 'Add Team User'} + + + + {error && {error}} + + + + + + + {withValue(ROLES, form.role).map((o) => {o})} + + + + + + + + + + + ); +} diff --git a/src/pages/tenants/ClientFormDialog.jsx b/src/pages/tenants/ClientFormDialog.jsx new file mode 100644 index 0000000..7fb497c --- /dev/null +++ b/src/pages/tenants/ClientFormDialog.jsx @@ -0,0 +1,158 @@ +import { useState, useEffect } from 'react'; +import { + Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField, + MenuItem, Box, Typography, Divider, Alert, CircularProgress, IconButton +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant'; + +const BUSINESS_TYPES = ['retail', 'wholesale', 'manufacturer', 'distributor', 'services', 'ecommerce', 'other']; +const STATUSES = ['newClient', 'contacted', 'onboarded', 'lost']; +const FREQUENCIES = ['Daily', 'Weekly', 'Fortnightly', 'Monthly', 'Occasional']; +const CONSENTS = ['basicOnly', 'full', 'none']; + +const EMPTY = { + name: '', phone: '', city: '', businessState: '', businessType: 'retail', status: 'newClient', + frequency: 'Daily', parcelVolume: 0, activeContracts: 0, provider: '', efficiency: '', + logisticsSegment: '', transitFrom: '', transitTo: '', neighbourhood: '', + surveyAddress: '', surveyLat: '', surveyLng: '', dataConsent: 'basicOnly', notes: '' +}; + +// Ensure a select always has its current value among the options. +const withValue = (opts, v) => (v && !opts.includes(v) ? [v, ...opts] : opts); + +export default function ClientFormDialog({ open, mode, initial, onClose, onSaved }) { + const isEdit = mode === 'edit'; + const [form, setForm] = useState(EMPTY); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (open) { + setForm({ ...EMPTY, ...(initial || {}) }); + setError(null); + } + }, [open, initial]); + + const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); + + const handleSave = async () => { + if (!form.name.trim()) { setError('Client name is required.'); return; } + setSaving(true); + setError(null); + + const lat = form.surveyLat === '' ? undefined : Number(form.surveyLat); + const lng = form.surveyLng === '' ? undefined : Number(form.surveyLng); + + const payload = { + name: form.name.trim(), + phone: form.phone, + city: form.city, + businessState: form.businessState, + businessType: form.businessType, + status: form.status, + frequency: form.frequency, + parcelVolume: Number(form.parcelVolume) || 0, + activeContracts: Number(form.activeContracts) || 0, + provider: form.provider, + efficiency: form.efficiency, + logisticsSegment: form.logisticsSegment, + transitFrom: form.transitFrom, + transitTo: form.transitTo, + neighbourhood: form.neighbourhood, + surveyAddress: form.surveyAddress, + surveyZone: form.neighbourhood, + dataConsent: form.dataConsent, + notes: form.notes, + lastUpdated: new Date().toISOString().slice(0, 10), + ...(lat != null ? { surveyLat: lat } : {}), + ...(lng != null ? { surveyLng: lng } : {}), + ...(lat != null && lng != null ? { surveyGeo: { lat, lon: lng } } : {}) + }; + + try { + if (isEdit) { + await setPayload(COLLECTIONS.clients, initial.id, payload); + } else { + await createPoint(COLLECTIONS.clients, { + ...payload, + clientId: `client_${Date.now()}`, + surveySubmitted: false + }); + } + onSaved(); + } catch (e) { + setError(e.message || 'Failed to save client'); + } finally { + setSaving(false); + } + }; + + return ( + + + {isEdit ? 'Edit Client' : 'Add Client'} + + + + {error && {error}} + + Business + + + + + + {withValue(BUSINESS_TYPES, form.businessType).map((o) => {o})} + + + + + {withValue(STATUSES, form.status).map((o) => {o})} + + + + + {withValue(FREQUENCIES, form.frequency).map((o) => {o})} + + + + + + + + + + + Location & Transit + + + + + + + + + + + + + Other + + + + {withValue(CONSENTS, form.dataConsent).map((o) => {o})} + + + + + + + + + + + ); +} diff --git a/src/pages/tenants/CreateClient.jsx b/src/pages/tenants/CreateClient.jsx deleted file mode 100644 index d998415..0000000 --- a/src/pages/tenants/CreateClient.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Grid, Stack, Button, TextField, MenuItem, InputAdornment } from '@mui/material'; - -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; - -const COUNTRY_CODES = ['+91', '+1', '+44', '+61', '+971']; - -export default function CreateClient() { - const navigate = useNavigate(); - const [code, setCode] = useState('+91'); - const [form, setForm] = useState({ - adminName: '', phone: '', email: '', address: '', - suburb: '', city: '', state: '', postcode: '', doorNo: '', landmark: '' - }); - const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); - - return ( - <> - - - - - - - - - - setCode(e.target.value)} - InputProps={{ disableUnderline: true }} sx={{ minWidth: 56 }} - > - {COUNTRY_CODES.map((c) => {c})} - - - ) - }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/pages/tenants/Tenants.jsx b/src/pages/tenants/Tenants.jsx index 6dc9d17..da703c4 100644 --- a/src/pages/tenants/Tenants.jsx +++ b/src/pages/tenants/Tenants.jsx @@ -1,160 +1,396 @@ -import { useState, useMemo, Fragment } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useState, useMemo, useEffect, Fragment } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { - Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, + Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Grid, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - Tooltip, TablePagination, Typography, Collapse, Grid + TablePagination, Typography, Collapse, CircularProgress, Alert, Tooltip, Chip, Divider, Link, + Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined'; +import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined'; +import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; +import HandshakeOutlinedIcon from '@mui/icons-material/HandshakeOutlined'; +import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; +import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; +import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; +import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import NotesOutlinedIcon from '@mui/icons-material/NotesOutlined'; +import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'; +import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; +import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined'; import PageHeader from '@/components/PageHeader'; +import StatCard from '@/components/StatCard'; import StatusChip from '@/components/StatusChip'; import EmptyState from '@/components/EmptyState'; import UserAvatar from '@/components/UserAvatar'; import TabLabelCount from '@/components/TabLabelCount'; -import { tenants, tenantPricing } from '@/data/mock'; -import { inr } from '@/utils/format'; +import { fetchPoints, deletePoint, COLLECTIONS } from '@/utils/qdrant'; +import ClientFormDialog from './ClientFormDialog'; -const TABS = [ - { key: 'active', label: 'Active' }, - { key: 'pending', label: 'Pending' }, - { key: 'inactive', label: 'Inactive' } -]; +const generateLogicalId = (id) => { + const str = String(id).replace(/[^a-zA-Z0-9]/g, '').toUpperCase(); + return 'CLI-' + str.substring(0, 6).padStart(6, '0'); +}; + +// Map a raw Qdrant point from doormile_clients to a flat client row. +function toClient(point) { + const p = point.payload || {}; + + // Qdrant point.id is usually a UUID. + // We generate a clean CLI-XXXXXX format based on it. + const logicalId = generateLogicalId(point.id); + + return { + id: point.id, + logicalId, + // Force override the ugly payload client_timestamp string with the clean ID + clientId: logicalId, + name: p.name || 'β€”', + phone: p.phone || '', + city: p.city || '', + businessState: p.businessState || '', + businessType: p.businessType || '', + status: p.status || 'unknown', + parcelVolume: p.parcelVolume ?? 0, + activeContracts: p.activeContracts ?? 0, + frequency: p.frequency || '', + provider: p.provider || '', + efficiency: p.efficiency || '', + logisticsSegment: p.logisticsSegment || '', + transitFrom: p.transitFrom || '', + transitTo: p.transitTo || '', + neighbourhood: p.neighbourhood || p.surveyZone || '', + surveyAddress: p.surveyAddress || '', + surveyLat: p.surveyLat ?? p.surveyGeo?.lat ?? '', + surveyLng: p.surveyLng ?? p.surveyGeo?.lon ?? '', + dataConsent: p.dataConsent || '', + lastUpdated: p.lastUpdated || '', + notes: p.notes || '' + }; +} + +// Humanize raw payload tokens like `basicOnly`, `newClient`, `first_mile` β†’ "Basic Only". +const humanize = (s) => + String(s || '') + .replace(/[_-]+/g, ' ') + .replace(/([a-z\d])([A-Z])/g, '$1 $2') + .replace(/\s+/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()) + .trim(); + +const titleCase = humanize; + +// Map categorical enum values to a semantic palette color. +const consentTone = (v) => ({ full: 'success', basiconly: 'info', none: 'default' }[String(v || '').toLowerCase()] || 'default'); +const efficiencyTone = (v) => { + const k = String(v || '').toLowerCase(); + if (/high|good|excellent/.test(k)) return 'success'; + if (/med|average|moderate/.test(k)) return 'warning'; + if (/low|poor|bad/.test(k)) return 'error'; + return 'info'; +}; + +const FIELD_LABEL_SX = { textTransform: 'uppercase', letterSpacing: 0.4, fontSize: '0.68rem', fontWeight: 700, color: 'grey.700' }; + +// A soft, theme-tinted pill for categorical values. Falls back to a dash placeholder. +function Pill({ label, color = 'default' }) { + if (label === undefined || label === null || label === '') { + return β€”; + } + return ( + + ); +} + +// Label + arbitrary node value (text, pill, or grouped chips). +function Field({ label, children }) { + return ( + + {label} + {children} + + ); +} function ReadField({ label, value }) { return ( - - {label} - {value || 'β€”'} - + + + {value || 'β€”'} + + ); } -function PricingTab() { +// A bordered sub-card with a tinted header strip β€” frames each group of fields. +function SectionCard({ icon: Icon, title, accent = 'primary', children }) { return ( - - - + + + + {title} - - - - - Date - Slab - Base Price - Min Kms - Price/Km - Other Charges - - - - {tenantPricing.map((p, i) => ( - - {p.date} - {p.slab} - {inr(p.basePrice)} - {p.minKms} - {inr(p.pricePerKm)} - {inr(p.otherCharges)} - - ))} - -
-
+ {children}
); } -function EditTab({ tenant }) { - const [form, setForm] = useState({ - name: tenant.name, contact: tenant.contact, phone: tenant.phone, email: tenant.email, - address: tenant.address, city: tenant.city, postcode: tenant.postcode, lat: tenant.lat, lng: tenant.lng - }); - const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); - +// Mini KPI tile used inside the Business card for the headline numbers. +function StatTile({ label, value, icon: Icon, color = 'primary' }) { return ( - - - - - - - - - - - - - - + + + + + + + {value} + {label} + ); } -function TenantRow({ row, index }) { +// Compact metric used inline in the table so key numbers show without expanding. +function Metric({ label, value, color = 'grey.800' }) { + return ( + + {value} + {label} + + ); +} + +function ClientRow({ row, index, onEdit, onDelete }) { const [open, setOpen] = useState(false); - const [inner, setInner] = useState(0); return ( - *': { borderBottom: open ? 'unset' : undefined } }}> + *': { borderBottom: open ? 'unset' : undefined }, + ...(open && { bgcolor: 'primary.lighter', '&:hover': { bgcolor: 'primary.lighter' } }) + }} + onClick={() => setOpen((o) => !o)} + > - setOpen((o) => !o)}> + { e.stopPropagation(); setOpen((o) => !o); }}> {open ? : } - {index + 1} + + + {row.logicalId} + + - - + + {row.name} - {row.volume} orders / mo + + {row.businessType && ( + + )} + - {row.contact} - {row.phone} + + + + {row.phone || 'β€”'} + {row.frequency && {row.frequency}} + + - {row.address} - {row.city} Β· {row.postcode} + + + + {row.city || 'β€”'}{row.businessState ? `, ${row.businessState}` : ''} + {row.neighbourhood && {row.neighbourhood}} + + + + + }> + + + + + { e.stopPropagation(); onEdit(row); }}> + { e.stopPropagation(); onDelete(row); }}> + - + - - - setInner(v)} sx={{ px: 2 }}> - - - - - - - {inner === 0 && ( - - - - - - - - - - + + `linear-gradient(90deg, ${theme.palette.primary.lighter}88 0%, ${theme.palette.background.paper} 75%)` + }} + > + + + + {row.name} + + + {row.clientId} + + + + + + + + + + + + + + + + + {row.clientId} + + + + - )} - {inner === 1 && } - {inner === 2 && } + + + + + {row.logisticsSegment ? ( + + {String(row.logisticsSegment).split(/[,/]/).map((seg) => seg.trim()).filter(Boolean).map((seg) => ( + + ))} + + ) : } + + + + + {(() => { + const stops = [row.transitFrom, ...String(row.transitTo || '').split(',')] + .map((s) => s.trim()).filter(Boolean); + if (!stops.length) return ; + return ( + + {stops.map((stop, idx) => ( + + {idx > 0 && } + + + ))} + + ); + })()} + + + + + + + + {[row.city, row.businessState].filter(Boolean).join(', ') || 'β€”'} + + + + + + {row.surveyAddress || 'β€”'} + + + + {row.surveyLat && row.surveyLng ? ( + + + + + + {[row.neighbourhood, row.city].filter(Boolean).join(', ') || 'Pinned location'} + + + + + + ) : β€”} + + + + + + + + + + + {row.notes && ( + + + + + Notes + + {row.notes} + + + )} + + + + @@ -165,85 +401,189 @@ function TenantRow({ row, index }) { } export default function Tenants() { - const navigate = useNavigate(); + const [clients, setClients] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [searchParams] = useSearchParams(); const [tab, setTab] = useState(0); - const [search, setSearch] = useState(''); + const [search, setSearch] = useState(searchParams.get('q') || ''); const [page, setPage] = useState(0); const [rpp, setRpp] = useState(10); - const tabKey = TABS[tab].key; + // Keep the search box in sync when navigated here with a ?q= query (e.g. from the top search bar). + useEffect(() => { + const q = searchParams.get('q'); + if (q != null) { setSearch(q); setPage(0); } + }, [searchParams]); + + const [dialog, setDialog] = useState({ open: false, mode: 'add', initial: null }); + const [toDelete, setToDelete] = useState(null); + const [deleting, setDeleting] = useState(false); + + const load = () => { + setLoading(true); + setError(null); + fetchPoints(COLLECTIONS.clients) + .then((points) => setClients(points.map(toClient))) + .catch((e) => setError(e.message || 'Failed to load clients')) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + const stats = useMemo(() => ({ + total: clients.length, + newCount: clients.filter((c) => c.status === 'newClient').length, + parcels: clients.reduce((s, c) => s + (Number(c.parcelVolume) || 0), 0), + contracts: clients.reduce((s, c) => s + (Number(c.activeContracts) || 0), 0) + }), [clients]); + + const tabs = useMemo(() => { + const seen = []; + clients.forEach((c) => { if (!seen.includes(c.status)) seen.push(c.status); }); + return [{ key: 'all', label: 'All' }, ...seen.map((s) => ({ key: s, label: titleCase(s) }))]; + }, [clients]); + + const tabKey = tabs[Math.min(tab, tabs.length - 1)]?.key || 'all'; const counts = useMemo(() => { - const c = {}; - TABS.forEach((t) => { c[t.key] = tenants.filter((d) => d.status === t.key).length; }); + const c = { all: clients.length }; + clients.forEach((cl) => { c[cl.status] = (c[cl.status] || 0) + 1; }); return c; - }, []); + }, [clients]); const filtered = useMemo( () => - tenants.filter((t) => { - const matchTab = t.status === tabKey; + clients.filter((t) => { + const matchTab = tabKey === 'all' || t.status === tabKey; const matchSearch = !search || - [t.name, t.contact, t.email, t.phone, t.city].join(' ').toLowerCase().includes(search.toLowerCase()); + [t.name, t.phone, t.city, t.businessType, t.clientId, t.neighbourhood] + .join(' ').toLowerCase().includes(search.toLowerCase()); return matchTab && matchSearch; }), - [tabKey, search] + [clients, tabKey, search] ); const paged = filtered.slice(page * rpp, page * rpp + rpp); + const handleSaved = () => { setDialog({ open: false, mode: 'add', initial: null }); load(); }; + + const confirmDelete = async () => { + setDeleting(true); + try { + await deletePoint(COLLECTIONS.clients, toDelete.id); + setToDelete(null); + load(); + } catch (e) { + setError(e.message || 'Failed to delete client'); + } finally { + setDeleting(false); + } + }; + return ( <> } onClick={() => navigate('/tenants/create')}> - Create Client - + + + + } /> - + + + + + + + + + `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)` + }} + > + + + + + Client Directory + Browse, search and manage every client account + + + { setSearch(e.target.value); setPage(0); }} - sx={{ minWidth: 260 }} + size="small" placeholder="Search by name, phone, city, ID…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} + sx={{ minWidth: 300 }} InputProps={{ startAdornment: }} /> + {!loading && ( + + + {filtered.length} {filtered.length === 1 ? 'client' : 'clients'} + + + + )} - { setTab(v); setPage(0); }}> - {TABS.map((t, i) => ( - } /> + { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> + {tabs.map((t, i) => ( + } /> ))} + {error && Retry}>{error}} + - +
- + - S.No + ID Client Contact - Address - Actions + Location + Volume + Status + Actions - {paged.length === 0 ? ( + {loading ? ( - - + + + + + ) : paged.length === 0 ? ( + + + ) : ( - paged.map((row, i) => ) + paged.map((row, i) => ( + setDialog({ open: true, mode: 'edit', initial: r })} + onDelete={(r) => setToDelete(r)} + /> + )) )}
@@ -253,6 +593,27 @@ export default function Tenants() { rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} />
+ + setDialog({ open: false, mode: 'add', initial: null })} + onSaved={handleSaved} + /> + + setToDelete(null)}> + Delete client? + + + This will permanently remove {toDelete?.name} from the doormile_clients collection. This cannot be undone. + + + + + + + ); } diff --git a/src/utils/qdrant.js b/src/utils/qdrant.js new file mode 100644 index 0000000..ae901d5 --- /dev/null +++ b/src/utils/qdrant.js @@ -0,0 +1,108 @@ +// ==============================|| QDRANT DATA LAYER ||============================== // +// Real connection to the Doormile Qdrant cluster (read + write). +// +// Requests go through the Vite dev proxy at `/qdrant` (see vite.config.js), which +// injects the api-key server-side so it never ships in the browser bundle and CORS +// is avoided. For a production build, point VITE_QDRANT_BASE at your own proxy. + +const BASE = import.meta.env.VITE_QDRANT_BASE || '/qdrant'; + +export const COLLECTIONS = { + clients: 'doormile_clients', + teamUsers: 'doormile_auth' +}; + +async function request(path, options = {}) { + const res = await fetch(`${BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options + }); + if (!res.ok) { + let detail = res.statusText; + try { + const body = await res.json(); + detail = body?.status?.error || body?.status || detail; + } catch { /* ignore non-json error bodies */ } + throw new Error(`Qdrant ${res.status}: ${detail}`); + } + return res.json(); +} + +/** + * Scroll every point of a collection (follows next_page_offset until exhausted). + * Returns an array of { id, payload } objects with the raw Qdrant payload. + */ +export async function fetchPoints(collection, { pageSize = 250, withVector = false } = {}) { + const all = []; + let offset = null; + + for (let guard = 0; guard < 1000; guard += 1) { + const data = await request(`/collections/${collection}/points/scroll`, { + method: 'POST', + body: JSON.stringify({ + limit: pageSize, + with_payload: true, + with_vector: withVector, + ...(offset != null ? { offset } : {}) + }) + }); + + const points = data?.result?.points || []; + all.push(...points); + + offset = data?.result?.next_page_offset ?? null; + if (offset == null || points.length === 0) break; + } + + return all; +} + +// Cache vector sizes per collection so we don't re-fetch the config on every write. +const _vectorSizeCache = {}; + +export async function getVectorSize(collection) { + if (_vectorSizeCache[collection] != null) return _vectorSizeCache[collection]; + const data = await request(`/collections/${collection}`); + const vectors = data?.result?.config?.params?.vectors; + // Single unnamed vector β†’ { size, distance }. Default to 1 if absent. + const size = typeof vectors?.size === 'number' ? vectors.size : 1; + _vectorSizeCache[collection] = size; + return size; +} + +/** + * Update the payload of an existing point (merges the given keys; vectors untouched). + */ +export async function setPayload(collection, id, payload) { + return request(`/collections/${collection}/points/payload?wait=true`, { + method: 'POST', + body: JSON.stringify({ payload, points: [id] }) + }); +} + +/** + * Create a brand-new point. The collection requires a vector of a fixed size, but + * this CRM doesn't do semantic search, so we store a zero-vector of the right length. + * Returns the generated point id. + */ +export async function createPoint(collection, payload) { + const size = await getVectorSize(collection); + const id = (crypto?.randomUUID && crypto.randomUUID()) || `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const vector = new Array(size).fill(0); + + await request(`/collections/${collection}/points?wait=true`, { + method: 'PUT', + body: JSON.stringify({ points: [{ id, vector, payload }] }) + }); + return id; +} + +/** + * Delete a point by id. + */ +export async function deletePoint(collection, id) { + return request(`/collections/${collection}/points/delete?wait=true`, { + method: 'POST', + body: JSON.stringify({ points: [id] }) + }); +} diff --git a/vite.config.js b/vite.config.js index 26d9664..fbf9cb6 100644 --- a/vite.config.js +++ b/vite.config.js @@ -12,6 +12,20 @@ export default defineConfig({ }, server: { port: 3000, - open: true + open: true, + // Proxy Qdrant so the api-key stays server-side and the browser avoids CORS. + // Frontend calls /qdrant/... β†’ forwarded to the Qdrant cluster with the key injected. + proxy: { + '/qdrant': { + target: 'http://66.116.207.225:6333', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/qdrant/, ''), + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq) => { + proxyReq.setHeader('api-key', 'Package@321#'); + }); + } + } + } } });