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
-
- } onClick={markAllRead} disabled={unread === 0}>
- Mark all read
-
-
-
- {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
-
- }>Export
-
- }
+ action={ } onClick={load}>Refresh}
/>
-
-
-
-
-
+ {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} )}
-
- }
- />
-
-
-
-
-
-
-
-
-
-
- } sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
- Jun 01 β Jun 05
-
- { 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
-
-
-
-
-
-
-
-
-
-
-
-
- setOpen(false)} color="inherit">Close
- setOpen(false)}>Update
-
-
- >
- );
-}
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={
- } onClick={save}>
- Save Changes
-
+
+ {dirty && }
+ Discard
+ } onClick={save}>Save Changes
+
}
/>
+ {/* 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.
+
+ Contact 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 }))} />
+ Change logo
-
+
+
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+ {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); }} />
+
+
+
+
+
+
+
+ } onClick={() => { localStorage.removeItem('auth_token'); window.location.href = '/login'; }}>Sign out everywhere
+
+
+
+
+ )}
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?
-
- navigate('/dashboard')}>
- Sign In
-
+
+
+ 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?
-
-
-
+ { localStorage.setItem('auth_token', 'demo-session'); navigate('/dashboard'); }}
+ sx={{
+ mt: 2,
+ py: 1.5,
+ borderRadius: 2,
+ fontSize: '1.05rem',
+ fontWeight: 700,
+ textTransform: 'none',
+ boxShadow: '0 8px 16px rgba(192, 18, 39, 0.25)',
+ '&:hover': {
+ boxShadow: '0 12px 20px rgba(192, 18, 39, 0.35)',
+ transform: 'translateY(-1px)'
+ },
+ transition: 'all 0.2s ease'
+ }}
+ >
+ Sign In
+
+
+
+
+
+
+ Β© {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} )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- navigate('/customers')}>Cancel
- navigate('/customers')}>Create
-
-
- >
- );
-}
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: }}
- />
- } onClick={() => navigate('/customers/create')}>
- Add Customer
-
-
- }
- />
-
-
- {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}
-
- ))}
-
- >
- )}
-
-
- setViewOpen(false)}>Close
- { setViewOpen(false); openEdit(viewer); }}
- >
- Edit
-
-
-
-
- setEditOpen(false)} maxWidth="md" fullWidth>
- Edit Customer
-
-
-
-
-
-
-
- +91 }}
- />
-
-
-
-
-
-
- {locations.map((l) => {l} )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setEditOpen(false)}>Close
- setEditOpen(false)}>Update
-
-
- >
- );
-}
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' }}>
-
-
-
- } onClick={() => setPayOpen(true)}>
- Update Payment
-
- } onClick={() => window.print()}>
- Print
-
-
- }
- />
-
-
-
- {/* 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
-
-
-
-
-
-
-
- setPayOpen(false)} color="inherit">Cancel
- setPayOpen(false)}>Update
-
-
- >
- );
-}
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 } }} />
-
-
-
- setDateOpen(false)} color="inherit">Cancel
- setDateOpen(false)}>Apply
-
-
- >
- );
-}
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={
- }>Re-Assign
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
- #
- 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} )}
-
-
-
-
-
-
- } onClick={() => navigate('/orders')}>Back
- } onClick={() => navigate('/orders')}>
- Assign Orders
-
-
- >
- );
-}
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' ? (
-
- }>
- Upload CSV
-
-
- {}}
- sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }}
- />
-
- ) : (
- } onClick={() => setDialogOpen(true)}>
- Select Customers
-
- )}
-
-
-
-
-
-
-
-
-
-
- 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)}
-
-
-
-
-
-
-
-
-
-
-
- } onClick={() => navigate('/orders')}>
- Create
-
-
-
-
-
-
- {/* 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)}>
-
-
-
-
-
-
-
- ))}
-
-
-
- setDialogOpen(false)}>Cancel
-
- Add Selected{picked.length ? ` (${picked.length})` : ''}
-
-
-
- >
- );
-}
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} )}
-
-
-
-
-
-
-
-
-
-
- navigate('/orders')}>
- Cancel
-
- } onClick={() => navigate('/orders')}>
- Create Order
-
-
-
-
- >
- );
-}
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={
-
- Cancel Order
- }>Edit Order
-
- }
- />
-
-
- {/* 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
-
-
-
- }>Call Rider
-
-
-
-
-
- >
- );
-}
-
-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 (
- <>
-
- } onClick={() => navigate('/orders/create-multiple')}>
- Create Multiple
-
- } onClick={() => navigate('/orders/create')}>
- Create Order
-
-
- }
- />
-
-
-
-
-
-
-
-
-
- {/* filter toolbar */}
-
- setSearch(e.target.value)}
- sx={{ minWidth: 240 }}
- InputProps={{ startAdornment: }}
- />
-
- } sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
- Jun 01 β Jun 05
-
- 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
- }>Delete
-
- )}
-
-
-
-
-
-
- 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} )}
-
- } sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
- Jun 01 β Jun 05
-
- { 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: }}
- />
-
- } onClick={() => setExportOpen(true)}>
- Export Report
-
-
-
-
-
-
-
- #
-
- 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:
-
-
-
-
-
-
-
-
-
-
-
- setExportOpen(false)}>Cancel
- } onClick={() => setExportOpen(false)}>Download CSV
-
-
- >
- );
-}
-
-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} )}
-
- }
- />
-
-
-
- } sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
- Jun 01 β Jun 05
-
-
- 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
- }>Refresh
-
-
-
-
- >
- );
-}
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} )}
-
- }
- />
-
-
-
- } sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
- Jun 01 β Jun 05
-
-
-
-
-
-
-
-
- #
- 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 }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- navigate('/riders')}>
- Cancel
-
- } onClick={() => navigate('/riders')}>
- Create
-
-
-
-
- >
- );
-}
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} )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- } onClick={() => navigate('/riders')}>
- Back
-
- navigate('/riders')}>
- Update
-
-
-
- >
- );
-}
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} )}
-
- } onClick={() => navigate('/riders/create')}>
- Add Rider
-
-
- }
- />
-
-
-
-
-
-
-
-
-
-
- { 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 (
+ <>
+
+ } onClick={load} disabled={loading}>Refresh
+ } onClick={() => setDialog({ open: true, mode: 'add', initial: null })}>Add User
+
+ }
+ />
+
+
+ `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.
+
+
+
+ setToDelete(null)} disabled={deleting}>Cancel
+ : null}>Delete
+
+
+ >
+ );
+}
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} )}
+
+
+
+
+
+
+ Cancel
+ : null}>
+ {isEdit ? 'Save Changes' : 'Create User'}
+
+
+
+ );
+}
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} )}
+
+
+
+
+
+
+ Cancel
+ : null}>
+ {isEdit ? 'Save Changes' : 'Create Client'}
+
+
+
+ );
+}
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} )}
-
-
- )
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- navigate('/tenants')}>Cancel
- navigate('/tenants')}>Create
-
-
- >
- );
-}
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 (
-
-
- }>Add Pricing
+
+
+
+ {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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update
+
+
+
+
+
+
+ {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}
+
+
+
+
+ } onClick={() => onEdit(row)}>Edit Client
+
+
+
+
+
+
+
+
+
+
+
+ {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'}
+
+
+
+ }
+ sx={{
+ py: 0.25, px: 1.25, minWidth: 0, fontSize: '0.75rem', fontWeight: 600, borderRadius: 5,
+ color: 'primary.main', borderColor: 'primary.100', bgcolor: 'primary.lighter',
+ '&:hover': { borderColor: 'primary.main', bgcolor: 'primary.lighter' }
+ }}
+ >
+ View on map
+
+
+ ) : β }
+
+
+
+
+
+
+
+
+
+
+ {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
-
+
+ } onClick={load} disabled={loading}>Refresh
+ } onClick={() => setDialog({ open: true, mode: 'add', initial: null })}>Add 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.
+
+
+
+ setToDelete(null)} disabled={deleting}>Cancel
+ : null}>Delete
+
+
>
);
}
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#');
+ });
+ }
+ }
+ }
}
});