updates on the ui changes and readme file updates

This commit is contained in:
2026-07-14 13:29:03 +05:30
parent b66f5cf085
commit 808d4a2fdb
7 changed files with 351 additions and 411 deletions

View File

@@ -1,50 +1,54 @@
# Doormile Console — Admin
# Doormile CRM
A modern, corporate logistics console for **Doormile**, a last-mile / courier delivery operation. Built with **React 18 + Vite + Material-UI v5**, themed around the Doormile brand red `#C01227`.
A modern, corporate customer relationship management system for **Doormile**, a last-mile logistics and courier delivery operation. Built with **React 18 + Vite + Material-UI v5**, themed around the Doormile brand red `#C01227`.
## Quick start
## Quick Start
```bash
# Install dependencies
npm install
npm run dev # http://localhost:3000
npm run build # production build → dist/
npm run preview # preview the production build
# Run the development server (default port: 5173 or similar)
npm run dev
# Build the project for production
npm run build
# Preview the production build locally
npm run preview
```
## What's inside
## Application Structure & Pages
A clean, data-dense but breathable corporate shell — fixed **red top header** (search, notifications, messages, profile) + **collapsible red sidebar** (260px ↔ 78px icon rail) + light `#FAFAFB` content area — wrapping **27 screens**:
Doormile CRM features a clean, responsive layout consisting of a **collapsible sidebar** and a **header panel**, wrapping the following key modules:
| Area | Screens |
| --- | --- |
| Overview | Dashboard |
| Orders | Orders list · Order Details (tracking + timeline) · Create Order · Create Multiple Orders · Assign Orders |
| Deliveries | Deliveries (expandable products, status tabs) |
| Network | Tenants · Create Client · Customers · Create Customer · Pricing · Riders · Create Rider · Edit Rider |
| Reports | Order Summary · Order Details · Riders Summary · Riders Logs (live map) |
| Finance | Invoices · Invoice Preview (printable A4) · Requests |
| Account | User Profile |
| Auth & states | Login · 404 · 500 · Under Construction · Coming Soon |
| Route Path | Page Component | Description |
| --- | --- | --- |
| `/dashboard` | `Dashboard.jsx` | Overview of registered clients, active contracts, total parcel volume, status donuts, and quick-view charts. |
| `/tenants` | `Tenants.jsx` | Management view for clients, including expandable details (parcel volume, logistics segments, transit routes, and coordinates map link). |
| `/survey` | `Survey.jsx` | Provider survey records mapping competitors, branch locations, and rate-per-kg answers in an aligned grid layout. |
| `/pricing` | `Pricing.jsx` | Pricing matrix slabs and logistics carrier rates management with search and active-provider selectors. |
| `/bookings` | `Bookings.jsx` | Courier bookings creation and management dialogs, along with pricing checks. |
| `/app-users` | `TeamUsers.jsx` | Team member list, user roles configuration, and initial permission setup. |
| `/settings` | `Settings.jsx` | Preferences dashboard covering organization profiles, system notification switches, and security/password updates. |
## Design system
## Design System
- **Brand red** `#C01227` (`lighter #F8E0E3darker #7E0B17`), white-on-red header/sidebar.
- **Brand red** `#C01227` (`lighter #F8E0E3` and `darker #7E0B17`) representing the primary action theme color.
- **Status palette** — success `#00A854`, warning `#FFBF00`, info `#00A2AE`, error `#F04134`.
- **Public Sans** type scale, 610px radii, soft `0 1px 4px` card shadows, and a signature **red glow** on primary CTAs.
- All tuning lives in `src/theme/` (`palette.js`, `typography.js`, `shadows.js`, `componentsOverride.js`).
- **Public Sans** typography scale, clean border-radii (`8px` to `16px`), soft shadows, and subtle responsive layouts (flex-based grid column alignment).
- Theme configuration resides in `src/theme/`.
## Project structure
## Project Folders
```
src/
theme/ Doormile red theme + MUI component overrides
layout/ MainLayout (red header + collapsible sidebar) · MinimalLayout
menu/ sidebar nav config
components/ PageHeader, StatCard, StatusChip, MainCard, EmptyState,
UserAvatar, MapPlaceholder, Logo, charts/ (Area, Donut)
data/mock.js static demo data powering every screen
pages/ all 27 screens (lazy-loaded)
App.jsx router · main.jsx app entry
components/ Reusable UI widgets (PageHeader, StatCard, StatusChip, EmptyState)
layout/ MainLayout (header + collapsible sidebar) & MinimalLayout
menu/ Sidebar navigation mappings (`navItems.jsx`)
pages/ CRM modules and page components (dashboard, tenants, survey, pricing, bookings, team, settings)
theme/ Doormile custom theme configurations and component style overrides
utils/ API fetching wrapper client
App.jsx App router and route definitions
main.jsx Application entry point
```
Charts and maps are dependency-free SVG placeholders (`components/charts`, `MapPlaceholder`) — swap in Recharts / Leaflet / Google Maps when wiring real data. All data is mocked in `src/data/mock.js`; there is no backend.

View File

@@ -37,7 +37,7 @@ export default function App() {
<Route path="/bookings" element={load(() => import('@/pages/bookings/Bookings'))} />
<Route path="/team-users" element={load(() => import('@/pages/team/TeamUsers'))} />
<Route path="/app-users" element={load(() => import('@/pages/team/TeamUsers'))} />
<Route path="/settings" element={load(() => import('@/pages/Settings'))} />
</Route>

View File

@@ -158,13 +158,6 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
</Box>
))}
</Box>
{expanded && (
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Doormile CRM v1.0
</Typography>
</Box>
)}
</Box>
);

View File

@@ -15,7 +15,7 @@ const navItems = [
{ id: 'survey', title: 'Providers', url: '/survey', icon: DashboardOutlinedIcon },
{ id: 'pricing', title: 'Pricing Matrix', url: '/pricing', icon: AttachMoneyOutlinedIcon },
{ id: 'bookings', title: 'Bookings', url: '/bookings', icon: DashboardOutlinedIcon },
{ id: 'team-users', title: 'App Users', url: '/team-users', icon: GroupsOutlinedIcon }
{ id: 'team-users', title: 'App Users', url: '/app-users', icon: GroupsOutlinedIcon }
]
},
{

View File

@@ -1,15 +1,21 @@
import { useState, useEffect } from 'react';
import {
Card, Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
Card, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
Typography, TextField, Button, Dialog, DialogTitle, DialogContent, DialogActions,
Grid, IconButton, CircularProgress, Chip, MenuItem, InputAdornment, alpha, Autocomplete, Checkbox, FormControlLabel, Tabs, Tab, Snackbar, Alert
Grid, IconButton, CircularProgress, Chip, MenuItem, InputAdornment, TablePagination, Autocomplete, Checkbox, FormControlLabel, Tabs, Tab, Snackbar, Alert
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search';
import CloseIcon from '@mui/icons-material/Close';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import PhoneIphoneOutlinedIcon from '@mui/icons-material/PhoneIphoneOutlined';
import DesktopWindowsOutlinedIcon from '@mui/icons-material/DesktopWindowsOutlined';
import PendingActionsOutlinedIcon from '@mui/icons-material/PendingActionsOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState';
import { fetchClients } from '@/utils/apiClient';
import ClientFormDialog from '../tenants/ClientFormDialog';
@@ -427,6 +433,8 @@ export default function Bookings() {
const [search, setSearch] = useState('');
const [sourceFilter, setSourceFilter] = useState('All');
const [formOpen, setFormOpen] = useState(false);
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const loadData = async () => {
setLoading(true);
@@ -459,29 +467,62 @@ export default function Bookings() {
return matchesSearch && matchesSource;
});
const paged = displayBookings.slice(page * rpp, page * rpp + rpp);
const stats = {
total: bookings.length,
app: bookings.filter((b) => b.bookingsource === 'Customer_App').length,
crm: bookings.filter((b) => b.bookingsource === 'CRM_Console').length,
pending: bookings.filter((b) => b.status === 'Pending_Pickup').length
};
return (
<Box sx={{ p: 3, bgcolor: '#f8fafc', minHeight: '80vh' }}>
<>
<PageHeader title="Bookings Management" breadcrumbs={[{ label: 'Bookings' }]} />
<Card sx={{ mt: 3, p: 3, borderRadius: 4, boxShadow: '0 12px 24px -4px rgba(0,0,0,0.05)' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<TextField
placeholder="Search bookings..."
value={search}
onChange={(e) => setSearch(e.target.value)}
size="small"
InputProps={{
startAdornment: <InputAdornment position="start"><SearchIcon /></InputAdornment>,
sx: { borderRadius: 3, bgcolor: alpha('#e2e8f0', 0.4) }
<Grid container spacing={2.5} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Bookings" value={stats.total} icon={LocalShippingOutlinedIcon} color="primary" caption="All sources" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="App Bookings" value={stats.app} icon={PhoneIphoneOutlinedIcon} color="primary" caption="Via customer app" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="CRM Bookings" value={stats.crm} icon={DesktopWindowsOutlinedIcon} color="primary" caption="Created in console" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Pending Pickup" value={stats.pending} icon={PendingActionsOutlinedIcon} color="primary" caption="Awaiting collection" /></Grid>
</Grid>
<Card sx={{ overflow: 'hidden' }}>
<Box
sx={{
px: { xs: 2, sm: 2.5 }, py: 2, borderBottom: 1, borderColor: 'divider',
display: 'flex', alignItems: 'center', gap: 1.5,
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)`
}}
/>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setFormOpen(true)} sx={{ borderRadius: 2 }}>
New Booking
</Button>
>
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: 'primary.lighter', color: 'primary.main', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<LocalShippingOutlinedIcon fontSize="small" />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.2 }}>All Bookings</Typography>
<Typography variant="caption" color="text.secondary">Track and manage every shipment booking</Typography>
</Box>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={sourceFilter} onChange={(e, val) => setSourceFilter(val)} aria-label="booking source filter tabs">
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search bookings…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ width: { xs: '100%', md: 300 } }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
{!loading && (
<Typography variant="body2" color="text.secondary">
{displayBookings.length} {displayBookings.length === 1 ? 'booking' : 'bookings'}
</Typography>
)}
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setFormOpen(true)} sx={{ flex: { xs: 1, md: 'none' } }}>
New Booking
</Button>
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={sourceFilter} onChange={(e, val) => { setSourceFilter(val); setPage(0); }} aria-label="booking source filter tabs">
<Tab label="All Bookings" value="All" />
<Tab label="App Bookings" value="Customer_App" />
<Tab label="CRM Bookings" value="CRM_Console" />
@@ -489,25 +530,24 @@ export default function Bookings() {
</Box>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}><CircularProgress /></Box>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
) : displayBookings.length === 0 ? (
<EmptyState title="No bookings found" caption="Try a different filter or search term." />
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow sx={{ bgcolor: alpha('#f1f5f9', 0.6) }}>
<TableCell sx={{ fontWeight: 700 }}>Booking No</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Delivery City</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Provider</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Source</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Price</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Status</TableCell>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell>Booking No</TableCell>
<TableCell>Delivery City</TableCell>
<TableCell>Provider</TableCell>
<TableCell>Source</TableCell>
<TableCell>Price</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayBookings.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center" sx={{ py: 4 }}>No bookings found.</TableCell></TableRow>
) : (
displayBookings.map(b => (
{paged.map(b => (
<TableRow key={b.bookingid} hover>
<TableCell sx={{ fontWeight: 600 }}>{b.bookingno}</TableCell>
<TableCell>{b.deliverycity || b.deliverypincode || 'N/A'}</TableCell>
@@ -526,12 +566,15 @@ export default function Bookings() {
<Chip label={b.status} size="small" color={b.status === 'Pending_Pickup' ? 'warning' : 'primary'} />
</TableCell>
</TableRow>
))
)}
))}
</TableBody>
</Table>
</TableContainer>
)}
<TablePagination
component="div" count={displayBookings.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
<BookingFormDialog
@@ -541,6 +584,6 @@ export default function Bookings() {
clients={clients}
surveys={surveys}
/>
</Box>
</>
);
}

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import {
Card, Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
Typography, TextField, InputAdornment, CircularProgress, MenuItem,
Select, FormControl, OutlinedInput, alpha, Avatar, Chip, Stack, Collapse, Button,
Select, FormControl, OutlinedInput, Avatar, Chip, Stack, Collapse, Button,
Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Grid, Tooltip, Alert, Snackbar
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
@@ -149,7 +149,7 @@ function PricingFormDialog({ open, onClose, onSave, initialData, isCell = false,
<Grid container spacing={2}>
{isCell ? (
<Grid item xs={12}>
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: alpha('#c01227', 0.04), border: '1px solid', borderColor: alpha('#c01227', 0.15), mb: 0.5 }}>
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: 'primary.lighter', border: '1px solid', borderColor: 'primary.light', mb: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.500', letterSpacing: 0.5, display: 'block', mb: 0.5 }}>UPDATING RATE FOR</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.900' }}>{formData.company}</Typography>
<Typography variant="body2" sx={{ color: 'grey.600', mt: 0.25 }}>{formData.weight_slab} · {formData.zone || formData.service_type}</Typography>
@@ -199,7 +199,7 @@ function PricingFormDialog({ open, onClose, onSave, initialData, isCell = false,
</DialogContent>
<DialogActions sx={{ p: 2, bgcolor: 'grey.50' }}>
<Button onClick={onClose} disabled={saving} color="inherit">Cancel</Button>
<Button onClick={handleSave} disabled={saving} variant="contained" sx={{ bgcolor: '#c01227', '&:hover': { bgcolor: '#a00f20' }, borderRadius: 2 }}>
<Button onClick={handleSave} disabled={saving} variant="contained">
{saving ? 'Saving...' : 'Save Rate'}
</Button>
</DialogActions>
@@ -303,101 +303,60 @@ export default function Pricing() {
: allProviderSlabs;
return (
<Box sx={{ pb: 6, minHeight: '80vh', bgcolor: '#f8fafc', px: { xs: 2, md: 4 }, pt: 3 }}>
<>
<PageHeader
title="Logistics Pricing Board"
breadcrumbs={[{ label: 'Pricing Overview' }]}
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4, mt: 3, maxWidth: 1200, mx: 'auto' }}>
<Stack spacing={3}>
{/* Modern Premium Control Panel */}
<Card
sx={{
p: 3,
borderRadius: 4,
bgcolor: '#ffffff',
boxShadow: '0 20px 40px -10px rgba(0,0,0,0.03)',
border: '1px solid',
borderColor: 'grey.100',
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: 'center',
justifyContent: 'space-between',
gap: 3
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: { xs: '100%', md: 'auto' } }}>
<Box sx={{ width: 48, height: 48, borderRadius: 3, bgcolor: 'primary.lighter', color: 'primary.main', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<LocalShippingOutlinedIcon />
</Box>
<Box>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', letterSpacing: 0.8, textTransform: 'uppercase' }}>
Active Logistics Provider
</Typography>
<FormControl sx={{ mt: 0.5, minWidth: 260, display: 'block' }}>
{/* Control Panel */}
<Card sx={{ overflow: 'hidden' }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small"
placeholder="Search weight slabs…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
sx={{ width: { xs: '100%', md: 300 } }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
{/* Active Provider Dropdown */}
<FormControl size="small" sx={{ minWidth: 220 }}>
<Select
value={selectedProvider || ''}
onChange={(e) => setSelectedProvider(e.target.value)}
displayEmpty
size="small"
IconComponent={StorefrontOutlinedIcon}
input={<OutlinedInput sx={{
borderRadius: 3,
fontWeight: 700,
fontSize: '1.1rem',
bgcolor: alpha('#e2e8f0', 0.4),
'& fieldset': { border: 'none' },
'&:hover': { bgcolor: alpha('#e2e8f0', 0.6) },
'&.Mui-focused': { bgcolor: '#fff', '& fieldset': { border: '2px solid', borderColor: 'primary.main' } }
}} />}
MenuProps={{ PaperProps: { sx: { borderRadius: 3, mt: 1, boxShadow: '0 12px 24px rgba(0,0,0,0.1)' } } }}
input={<OutlinedInput sx={{ fontWeight: 700 }} />}
>
{companies.length === 0 ? (
<MenuItem value="" disabled>Loading Providers...</MenuItem>
) : (
companies.map(c => (
<MenuItem key={c} value={c} sx={{ fontWeight: 600, py: 1.5, borderRadius: 1, mx: 1 }}>{c}</MenuItem>
<MenuItem key={c} value={c} sx={{ fontWeight: 600 }}>{c}</MenuItem>
))
)}
</Select>
</FormControl>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<TextField
placeholder="Search weight slabs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: <InputAdornment position="start"><SearchIcon color="primary" /></InputAdornment>,
sx: {
borderRadius: 50,
bgcolor: alpha('#e2e8f0', 0.3),
'& fieldset': { border: 'none' },
'&:hover': { bgcolor: alpha('#e2e8f0', 0.5) },
'&.Mui-focused': { bgcolor: '#fff', '& fieldset': { border: '2px solid', borderColor: 'primary.main' } }
}
}}
/>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => { setEditingRecord({ company: selectedProvider, weight_slab: '', zone: '', rate: '' }); setIsCellEdit(false); setFormOpen(true); }} sx={{ bgcolor: '#c01227', '&:hover': { bgcolor: '#a00f20' }, borderRadius: 2, py: 1.2, fontWeight: 700, whiteSpace: 'nowrap' }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => { setEditingRecord({ company: selectedProvider, weight_slab: '', zone: '', rate: '' }); setIsCellEdit(false); setFormOpen(true); }} sx={{ whiteSpace: 'nowrap' }}>
Add Rate
</Button>
<Button
variant={isEditMode ? "contained" : "outlined"}
variant={isEditMode ? 'contained' : 'outlined'}
color={isEditMode ? 'inherit' : 'primary'}
startIcon={<EditOutlinedIcon />}
onClick={() => setIsEditMode(!isEditMode)}
sx={{
borderRadius: 2, py: 1.2, fontWeight: 700, whiteSpace: 'nowrap',
color: isEditMode ? 'white' : 'grey.700',
borderColor: isEditMode ? 'transparent' : 'grey.300',
bgcolor: isEditMode ? 'grey.800' : 'transparent',
'&:hover': { bgcolor: isEditMode ? 'grey.900' : alpha('#000', 0.05), borderColor: isEditMode ? 'transparent' : 'grey.400' }
}}
sx={{ whiteSpace: 'nowrap', ...(isEditMode && { bgcolor: 'grey.800', color: 'white', '&:hover': { bgcolor: 'grey.900' } }) }}
>
{isEditMode ? 'Done' : 'Edit'}
</Button>
</Box>
</Stack>
</Card>
{/* Minimalist Collapsible Provider Profile Widget */}
@@ -405,33 +364,31 @@ export default function Pricing() {
<Card
elevation={0}
sx={{
borderRadius: 4,
border: '1px solid',
borderColor: locationsOpen ? '#c01227' : 'grey.200',
bgcolor: 'white',
borderColor: locationsOpen ? 'primary.main' : 'grey.200',
transition: 'border-color 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
borderColor: '#c01227',
borderColor: 'primary.main',
boxShadow: '0 8px 30px rgba(192, 18, 39, 0.08)',
}
}}
>
<Stack
direction={{ xs: 'column', md: 'row' }} spacing={4} sx={{ p: { xs: 3, md: 3.5 }, cursor: 'pointer', userSelect: 'none' }}
direction={{ xs: 'column', md: 'row' }} spacing={3} sx={{ p: { xs: 2, sm: 2.5 }, cursor: 'pointer', userSelect: 'none' }}
alignItems={{ xs: 'flex-start', md: 'center' }} justifyContent="space-between"
onClick={() => setLocationsOpen(!locationsOpen)}
>
{/* Avatar & Title */}
<Stack direction="row" spacing={3} alignItems="center" sx={{ minWidth: 260 }}>
<Avatar sx={{ width: 68, height: 68, bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 800, fontSize: '1.8rem', borderRadius: 3 }}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ minWidth: 260 }}>
<Avatar sx={{ width: 42, height: 42, bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 800, borderRadius: 2 }}>
{(selectedProvider || 'A')[0].toUpperCase()}
</Avatar>
<Box>
<Typography variant="overline" sx={{ color: 'grey.500', fontWeight: 700, letterSpacing: 1.2 }}>LOGISTICS PARTNER</Typography>
<Typography variant="h5" sx={{ fontWeight: 800, color: 'grey.900', lineHeight: 1.1, mt: 0.25 }}>{selectedProvider}</Typography>
<Typography variant="body2" sx={{ color: 'grey.600', mt: 1, fontWeight: 500, display: 'flex', alignItems: 'center' }}>
<PlaceOutlinedIcon sx={{ fontSize: 16, mr: 0.5, color: '#c01227' }} />
<PlaceOutlinedIcon sx={{ fontSize: 16, mr: 0.5, color: 'primary.main' }} />
{providerSurveys.length} Registered Location{providerSurveys.length !== 1 && 's'}
</Typography>
</Box>
@@ -440,17 +397,17 @@ export default function Pricing() {
{/* Badges & Action */}
<Stack direction="row" spacing={3} alignItems="center">
<Stack direction="row" spacing={1} sx={{ display: { xs: 'none', md: 'flex' } }}>
<Chip size="small" label="First Mile" sx={{ bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 700, borderRadius: 1.5 }} />
<Chip size="small" label="Mid Mile" sx={{ bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 700, borderRadius: 1.5 }} />
<Chip size="small" label="Last Mile" sx={{ bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 700, borderRadius: 1.5 }} />
<Chip size="small" label="First Mile" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 700 }} />
<Chip size="small" label="Mid Mile" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 700 }} />
<Chip size="small" label="Last Mile" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 700 }} />
</Stack>
<Button
onClick={(e) => { e.stopPropagation(); setLocationsOpen(!locationsOpen); }}
sx={{
borderRadius: 2, py: 1, px: 3, fontWeight: 700,
color: locationsOpen ? '#c01227' : 'grey.700',
bgcolor: locationsOpen ? alpha('#c01227', 0.1) : 'grey.100',
'&:hover': { bgcolor: alpha('#c01227', 0.15), color: '#c01227' }
color: locationsOpen ? 'primary.main' : 'grey.700',
bgcolor: locationsOpen ? 'primary.lighter' : 'grey.100',
'&:hover': { bgcolor: 'primary.lighter', color: 'primary.main' }
}}
endIcon={locationsOpen ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
>
@@ -461,21 +418,21 @@ export default function Pricing() {
</Stack>
<Collapse in={locationsOpen} timeout="auto" unmountOnExit>
<Box sx={{ p: 3, pt: 0, borderTop: '1px dashed', borderColor: 'grey.200', bgcolor: 'grey.50', borderBottomLeftRadius: 16, borderBottomRightRadius: 16 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, mt: 3 }}>
<PlaceOutlinedIcon sx={{ color: '#c01227', fontSize: 20, mr: 1 }} />
<Box sx={{ p: 2, pt: 0, borderTop: '1px dashed', borderColor: 'grey.200', bgcolor: 'grey.50' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5, mt: 2 }}>
<PlaceOutlinedIcon sx={{ color: 'primary.main', fontSize: 20, mr: 1 }} />
<Typography variant="caption" sx={{ color: 'grey.600', fontWeight: 700, letterSpacing: 1 }}>ALL SERVICED AREAS:</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{providerSurveys.map((s, idx) => (
<Chip
key={s.id ?? idx}
label={s.area || s.address}
sx={{
borderRadius: 2, fontWeight: 600, px: 1, py: 2.5,
bgcolor: 'white', color: 'grey.800', border: '1px solid', borderColor: 'grey.300', boxShadow: '0 2px 8px rgba(0,0,0,0.02)',
'&:hover': { borderColor: '#c01227', color: '#c01227', bgcolor: alpha('#c01227', 0.05), boxShadow: '0 4px 12px rgba(192,18,39,0.08)' },
transition: 'border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease'
fontWeight: 600,
bgcolor: 'background.paper', color: 'grey.800', border: '1px solid', borderColor: 'grey.300',
'&:hover': { borderColor: 'primary.main', color: 'primary.main', bgcolor: 'primary.lighter' },
transition: 'border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease'
}}
/>
))}
@@ -486,28 +443,19 @@ export default function Pricing() {
)}
{/* Pricing Data Grid */}
<Card
sx={{
borderRadius: 4,
boxShadow: '0 24px 48px -12px rgba(0,0,0,0.05)',
border: '1px solid',
borderColor: 'grey.200',
overflow: 'hidden',
bgcolor: '#fff'
}}
>
<Card sx={{ overflow: 'hidden' }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 10 }}>
<CircularProgress size={48} thickness={4} sx={{ color: 'primary.light' }} />
<CircularProgress size={40} sx={{ color: 'primary.light' }} />
</Box>
) : (
<TableContainer>
<Table sx={{ minWidth: 700 }}>
<TableHead>
<TableRow sx={{ bgcolor: alpha('#f8fafc', 0.8) }}>
<TableCell sx={{ py: 2, px: 4, fontWeight: 700, color: 'grey.500', fontSize: '0.75rem', letterSpacing: 1.2, textTransform: 'uppercase', borderBottom: '2px solid', borderColor: 'grey.100' }}>Weight Slab</TableCell>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell>Weight Slab</TableCell>
{providerZones.map(zone => (
<TableCell key={zone} align="center" sx={{ py: 2, fontWeight: 700, color: 'grey.500', fontSize: '0.75rem', letterSpacing: 1.2, textTransform: 'uppercase', borderBottom: '2px solid', borderColor: 'grey.100' }}>
<TableCell key={zone} align="center">
{zone}
</TableCell>
))}
@@ -516,7 +464,7 @@ export default function Pricing() {
<TableBody>
{providerZones.length === 0 ? (
<TableRow>
<TableCell colSpan={2} align="center" sx={{ py: 8 }}>
<TableCell colSpan={1} align="center" sx={{ py: 8 }}>
<Typography variant="body1" color="text.secondary" sx={{ fontWeight: 500 }}>
No pricing data for this provider yet.
</Typography>
@@ -534,13 +482,10 @@ export default function Pricing() {
displaySlabs.map((slab) => (
<TableRow
key={slab}
sx={{
transition: 'background-color 0.15s',
'&:hover': { bgcolor: alpha('#f1f5f9', 0.5) },
'&:last-child td': { borderBottom: 0 }
}}
hover
sx={{ '&:last-child td': { borderBottom: 0 } }}
>
<TableCell sx={{ px: 4, py: 2, fontWeight: 600, color: 'grey.800', fontSize: '0.9rem', borderBottom: '1px solid', borderColor: 'grey.100' }}>
<TableCell sx={{ fontWeight: 600, color: 'grey.800' }}>
{slab}
</TableCell>
{providerZones.map(zone => {
@@ -573,12 +518,9 @@ export default function Pricing() {
key={zone}
align="center"
sx={{
py: 1.5,
borderBottom: '1px solid',
borderColor: 'grey.100',
cursor: isEditMode ? 'pointer' : 'default',
transition: 'background 0.15s',
'&:hover': isEditMode ? { bgcolor: alpha('#c01227', 0.04) } : {},
'&:hover': isEditMode ? { bgcolor: 'primary.lighter' } : {},
}}
onClick={isEditMode ? openCellEdit : undefined}
>
@@ -588,9 +530,9 @@ export default function Pricing() {
sx={{
display: 'inline-block',
fontWeight: hasRate ? 700 : 400,
color: hasRate ? 'success.dark' : (isEditMode ? alpha('#c01227', 0.4) : 'grey.300'),
color: hasRate ? 'success.dark' : (isEditMode ? 'primary.light' : 'grey.300'),
fontSize: hasRate ? '0.95rem' : '0.85rem',
bgcolor: hasRate ? alpha('#10b981', 0.08) : 'transparent',
bgcolor: hasRate ? 'success.lighter' : 'transparent',
px: hasRate ? 1.5 : 0,
py: hasRate ? 0.5 : 0,
borderRadius: 2,
@@ -601,7 +543,7 @@ export default function Pricing() {
{isEditMode && (
hasRate
? <EditOutlinedIcon sx={{ fontSize: 13, color: 'grey.400' }} />
: <AddCircleOutlineIcon sx={{ fontSize: 13, color: alpha('#c01227', 0.4) }} />
: <AddCircleOutlineIcon sx={{ fontSize: 13, color: 'primary.light' }} />
)}
</Box>
</Tooltip>
@@ -616,7 +558,7 @@ export default function Pricing() {
</TableContainer>
)}
</Card>
</Box>
</Stack>
<PricingFormDialog
open={formOpen}
@@ -628,6 +570,6 @@ export default function Pricing() {
providerColumns={providerZones}
providerUsesServiceType={providerUsesServiceType}
/>
</Box>
</>
);
}

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, Fragment } from 'react';
import { fetchUsers, deleteCompetitorBranch } from '@/utils/apiClient';
import {
Card, Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
Typography, TextField, InputAdornment, IconButton, Collapse, TablePagination, Stack, CircularProgress, alpha,
Typography, TextField, InputAdornment, IconButton, Collapse, TablePagination, Stack, CircularProgress,
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, MenuItem, Alert, Autocomplete, Snackbar
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
@@ -22,6 +22,7 @@ import CloseIcon from '@mui/icons-material/Close';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState';
const API_BASE = import.meta.env.VITE_API_BASE || 'https://api.doormile.com/api/v1';
@@ -51,61 +52,11 @@ function Field({ label, children }) {
);
}
function MagicStatCard({ title, value, icon: Icon, gradient, percentage }) {
return (
<Box
sx={{
position: 'relative',
overflow: 'hidden',
borderRadius: 3.5,
p: 3,
color: 'white',
background: gradient,
boxShadow: '0 10px 30px rgba(0,0,0,0.08)',
transition: 'box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 20px 40px rgba(0,0,0,0.12)',
}
}}
>
<Box sx={{ position: 'relative', zIndex: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="overline" sx={{ fontWeight: 700, letterSpacing: 1.2, opacity: 0.9 }}>
{title}
</Typography>
<Box sx={{ width: 36, height: 36, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}>
<Icon sx={{ fontSize: 20 }} />
</Box>
</Stack>
<Stack direction="row" alignItems="baseline" spacing={1.5} sx={{ mt: 2 }}>
<Typography variant="h3" sx={{ fontWeight: 800, lineHeight: 1 }}>
{value}
</Typography>
{percentage && (
<Chip size="small" label={percentage} sx={{ height: 22, bgcolor: 'rgba(255,255,255,0.25)', color: 'white', fontWeight: 700, borderRadius: 1.5, backdropFilter: 'blur(4px)' }} />
)}
</Stack>
</Box>
<Icon
sx={{
position: 'absolute',
right: -24,
bottom: -24,
fontSize: 160,
opacity: 0.1,
zIndex: 1,
transform: 'rotate(-15deg)'
}}
/>
</Box>
);
}
function SectionCard({ icon: Icon, title, children }) {
return (
<Box sx={{ height: '100%', borderRadius: 2, border: 1, borderColor: 'divider', bgcolor: 'background.paper', overflow: 'hidden' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ px: 2, py: 1.25, borderBottom: 1, borderColor: 'divider', bgcolor: alpha('#c01227', 0.08) }}>
<Icon sx={{ fontSize: 18, color: '#c01227' }} />
<Stack direction="row" spacing={1} alignItems="center" sx={{ px: 2, py: 1.25, borderBottom: 1, borderColor: 'divider', bgcolor: 'primary.lighter' }}>
<Icon sx={{ fontSize: 18, color: 'primary.main' }} />
<Typography variant="overline" sx={{ fontWeight: 700, color: 'grey.800', letterSpacing: 0.6, lineHeight: 1 }}>{title}</Typography>
</Stack>
<Stack spacing={2} sx={{ p: 2 }}>{children}</Stack>
@@ -422,7 +373,7 @@ function SurveyFormDialog({ open, onClose, onSave, initialData }) {
</DialogContent>
<DialogActions sx={{ p: 2, bgcolor: 'grey.50' }}>
<Button onClick={onClose} disabled={saving} color="inherit">Cancel</Button>
<Button onClick={handleSave} disabled={saving} variant="contained" sx={{ bgcolor: '#c01227', '&:hover': { bgcolor: '#a00f20' }, borderRadius: 2 }}>
<Button onClick={handleSave} disabled={saving} variant="contained">
{saving ? 'Saving...' : 'Save Record'}
</Button>
</DialogActions>
@@ -437,56 +388,73 @@ function BranchCard({ row, onEdit, onDelete, users }) {
const updater = users.find(u => u.id === row.updated_by);
return (
<Box sx={{ p: 2, borderRadius: 2.5, border: '1px solid', borderColor: 'divider', bgcolor: 'background.paper', transition: 'border-color 0.2s ease, box-shadow 0.2s ease', '&:hover': { borderColor: '#c01227', boxShadow: '0 4px 12px rgba(192, 18, 39, 0.05)' } }}>
<Stack
direction={{ xs: 'column', md: 'row' }} spacing={3} alignItems={{ xs: 'flex-start', md: 'center' }} justifyContent="space-between"
<Box sx={{ p: 1.5, py: 1.25, borderRadius: 2, border: '1px solid', borderColor: 'divider', bgcolor: 'background.paper', transition: 'border-color 0.2s ease, box-shadow 0.2s ease', '&:hover': { borderColor: 'primary.main', boxShadow: '0 4px 12px rgba(192, 18, 39, 0.05)' } }}>
<Grid
container
spacing={2}
alignItems="center"
onClick={() => setOpen(!open)}
sx={{ cursor: 'pointer', userSelect: 'none' }}
>
{/* Left: Location & Contact */}
<Stack direction="row" spacing={2} alignItems="center" sx={{ minWidth: 240 }}>
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: alpha('#c01227', 0.05) }}>
<PlaceOutlinedIcon sx={{ color: '#c01227' }} />
<Grid item xs={12} md={4.5}>
<Stack direction="row" spacing={1.5} alignItems="center">
<Box sx={{ p: 1, borderRadius: 1.5, bgcolor: 'primary.lighter', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PlaceOutlinedIcon sx={{ color: 'primary.main', fontSize: 18 }} />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.2 }}>
{row.area || 'Unknown Area'}
</Typography>
<Typography variant="body2" sx={{ color: 'grey.600', display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
<PhoneOutlinedIcon sx={{ fontSize: 14 }} /> {row.phone || '—'}
<Typography variant="caption" sx={{ color: 'grey.600', display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
<PhoneOutlinedIcon sx={{ fontSize: 12 }} /> {row.phone || '—'}
</Typography>
</Box>
</Stack>
</Grid>
{/* Middle: Rates */}
<Stack direction="row" spacing={4} sx={{ flex: 1 }} justifyContent={{ xs: 'flex-start', md: 'center' }}>
<Grid item xs={6} md={3.5}>
<Box>
<Typography variant="caption" sx={{ color: 'grey.500', fontWeight: 700, letterSpacing: 0.5, display: 'block', mb: 0.5 }}>RATE PER KG</Typography>
<Pill label={row.rate_per_kg} color="success" />
<Pill
label={row.rate_per_kg}
color={
!row.rate_per_kg ||
row.rate_per_kg.toLowerCase().includes('not answered') ||
row.rate_per_kg.toLowerCase().includes('wrong number')
? 'warning'
: 'success'
}
/>
</Box>
</Grid>
{/* Logistics */}
<Grid item xs={6} md={2}>
<Box>
<Typography variant="caption" sx={{ color: 'grey.500', fontWeight: 700, letterSpacing: 0.5, display: 'block', mb: 0.5 }}>LOGISTICS</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>
{row.offers_pickup === 'yes' || row.offers_drop === 'yes' ? `${row.offers_pickup === 'yes' ? 'Pickup' : ''} ${row.offers_drop === 'yes' ? 'Drop' : ''}` : '—'}
</Typography>
</Box>
</Stack>
</Grid>
{/* Right: Actions */}
<Stack direction="row" spacing={1} alignItems="center">
<Button size="small" sx={{ borderRadius: 1.5, color: open ? '#c01227' : 'grey.700', bgcolor: open ? alpha('#c01227', 0.05) : 'transparent', '&:hover': { bgcolor: alpha('#c01227', 0.1), color: '#c01227' } }}>
<Grid item xs={12} md={2} sx={{ display: 'flex', justifyContent: { xs: 'flex-start', md: 'flex-end' }, alignItems: 'center', gap: 0.5 }}>
<Button size="small" sx={{ borderRadius: 1.5, color: open ? 'primary.main' : 'grey.700', bgcolor: open ? 'primary.lighter' : 'transparent', '&:hover': { bgcolor: 'primary.lighter', color: 'primary.main' } }}>
{open ? 'Hide Info' : 'More Info'}
</Button>
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(row); }} sx={{ color: 'grey.400', '&:hover': { color: '#c01227', bgcolor: alpha('#c01227', 0.1) } }}>
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(row); }} sx={{ color: 'grey.400', '&:hover': { color: 'primary.main', bgcolor: 'primary.lighter' } }}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(row); }} sx={{ color: 'grey.400', '&:hover': { color: '#ef4444', bgcolor: alpha('#ef4444', 0.1) } }}>
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(row); }} sx={{ color: 'grey.400', '&:hover': { color: 'error.main', bgcolor: 'error.lighter' } }}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Stack>
</Grid>
</Stack>
</Grid>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ mt: 2.5, pt: 2.5, borderTop: '1px dashed', borderColor: 'divider' }}>
@@ -559,8 +527,8 @@ function BranchCard({ row, onEdit, onDelete, users }) {
sx={{
alignSelf: 'flex-start',
py: 0.5, px: 1.5, fontSize: '0.75rem', fontWeight: 600, borderRadius: 2,
color: '#c01227', borderColor: alpha('#c01227', 0.5), bgcolor: alpha('#c01227', 0.05),
'&:hover': { borderColor: '#c01227', bgcolor: alpha('#c01227', 0.1) }
color: 'primary.main', borderColor: 'primary.light', bgcolor: 'primary.lighter',
'&:hover': { borderColor: 'primary.main', bgcolor: 'primary.lighter' }
}}
>
View on map
@@ -605,14 +573,12 @@ function CompanyGroup({ companyName, branches, onEdit, onDelete, users }) {
<Card
elevation={0}
sx={{
borderRadius: 3,
border: '1px solid',
borderColor: open ? '#c01227' : 'grey.200',
p: { xs: 2.5, md: 3 },
mb: 2.5,
borderColor: open ? 'primary.main' : 'grey.200',
p: 2,
mb: 2,
transition: 'border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
borderColor: '#c01227',
borderColor: 'primary.main',
boxShadow: '0 8px 30px rgba(192, 18, 39, 0.08)',
}
}}
@@ -624,8 +590,8 @@ function CompanyGroup({ companyName, branches, onEdit, onDelete, users }) {
>
{/* Left Side: Avatar & Basic Info */}
<Stack direction="row" spacing={2.5} alignItems="center">
<Avatar sx={{ width: 64, height: 64, bgcolor: alpha('#c01227', 0.1), color: '#c01227', fontWeight: 800, fontSize: '1.5rem', borderRadius: 2.5 }}>
<Stack direction="row" spacing={2} alignItems="center">
<Avatar sx={{ width: 42, height: 42, bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 800, borderRadius: 2 }}>
{(companyName || 'A')[0].toUpperCase()}
</Avatar>
<Box>
@@ -641,17 +607,17 @@ function CompanyGroup({ companyName, branches, onEdit, onDelete, users }) {
{/* Right Side: Badges & Actions */}
<Stack direction="row" spacing={3} alignItems="center">
<Stack direction="row" spacing={1} sx={{ display: { xs: 'none', md: 'flex' } }}>
<Chip size="small" label="First Mile" sx={{ bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 700, borderRadius: 1.5 }} />
<Chip size="small" label="Mid Mile" sx={{ bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 700, borderRadius: 1.5 }} />
<Chip size="small" label="Last Mile" sx={{ bgcolor: alpha('#c01227', 0.08), color: '#c01227', fontWeight: 700, borderRadius: 1.5 }} />
<Chip size="small" label="First Mile" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 700 }} />
<Chip size="small" label="Mid Mile" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 700 }} />
<Chip size="small" label="Last Mile" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', fontWeight: 700 }} />
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center">
<Button
sx={{
borderRadius: 2, py: 1, px: 3, fontWeight: 700,
color: open ? '#c01227' : 'grey.700',
bgcolor: open ? alpha('#c01227', 0.1) : 'grey.100',
'&:hover': { bgcolor: alpha('#c01227', 0.15), color: '#c01227' }
color: open ? 'primary.main' : 'grey.700',
bgcolor: open ? 'primary.lighter' : 'grey.100',
'&:hover': { bgcolor: 'primary.lighter', color: 'primary.main' }
}}
endIcon={open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
>
@@ -662,8 +628,8 @@ function CompanyGroup({ companyName, branches, onEdit, onDelete, users }) {
</Stack>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ mt: 3, pt: 3, borderTop: '2px dashed', borderColor: 'grey.200' }}>
<Stack spacing={2}>
<Box sx={{ mt: 2, pt: 2, borderTop: '2px dashed', borderColor: 'grey.200' }}>
<Stack spacing={1.5}>
{branches.map((branch, idx) => <BranchCard key={branch.id ?? idx} row={branch} onEdit={onEdit} onDelete={onDelete} users={users} />)}
</Stack>
</Box>
@@ -748,89 +714,88 @@ export default function Survey() {
};
return (
<Box sx={{ pb: 6, bgcolor: '#f8fafc', px: { xs: 2, md: 4 }, pt: 3, minHeight: '100vh' }}>
<>
<PageHeader
title="Field Surveys"
breadcrumbs={[{ label: 'Survey Management' }]}
/>
<Grid container spacing={3} sx={{ mb: 5, mt: 1 }}>
<Grid container spacing={2.5} sx={{ mb: 3 }}>
<Grid item xs={12} md={4}>
<MagicStatCard
title="TOTAL ENQUIRIES"
value={loading ? '...' : stats.total}
icon={AssignmentOutlinedIcon}
gradient="linear-gradient(135deg, #e11d48 0%, #c01227 100%)"
<StatCard
accent title="Total Enquiries"
value={loading ? '' : stats.total}
icon={AssignmentOutlinedIcon} color="primary"
caption="All field surveys"
/>
</Grid>
<Grid item xs={12} md={4}>
<MagicStatCard
title="RATES QUOTED"
value={loading ? '...' : stats.quoted}
icon={StorefrontOutlinedIcon}
gradient="linear-gradient(135deg, #10b981 0%, #047857 100%)"
percentage={stats.total ? `${Math.round((stats.quoted / stats.total) * 100)}%` : '0%'}
<StatCard
accent title="Rates Quoted"
value={loading ? '' : stats.quoted}
icon={StorefrontOutlinedIcon} color="success"
caption={stats.total ? `${Math.round((stats.quoted / stats.total) * 100)}% of total` : '0% of total'}
/>
</Grid>
<Grid item xs={12} md={4}>
<MagicStatCard
title="MISSING INFO"
value={loading ? '...' : stats.missing}
icon={PhoneOutlinedIcon}
gradient="linear-gradient(135deg, #334155 0%, #0f172a 100%)"
percentage={stats.total ? `${Math.round((stats.missing / stats.total) * 100)}%` : '0%'}
<StatCard
accent title="Missing Info"
value={loading ? '' : stats.missing}
icon={PhoneOutlinedIcon} color="warning"
caption={stats.total ? `${Math.round((stats.missing / stats.total) * 100)}% of total` : '0% of total'}
/>
</Grid>
</Grid>
<Card sx={{ borderRadius: 2, boxShadow: '0 4px 20px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
<Card sx={{ overflow: 'hidden' }}>
{/* Header Section */}
<Box sx={{ p: 3, display: 'flex', alignItems: 'center', gap: 2, borderBottom: '1px solid', borderColor: 'grey.100' }}>
<Box sx={{
width: 48, height: 48, borderRadius: 2.5,
bgcolor: alpha('#c01227', 0.1), color: '#c01227',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<AssignmentOutlinedIcon />
<Box
sx={{
px: { xs: 2, sm: 2.5 }, py: 2, borderBottom: 1, borderColor: 'divider',
display: 'flex', alignItems: 'center', gap: 1.5,
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)`
}}
>
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: 'primary.lighter', color: 'primary.main', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<AssignmentOutlinedIcon fontSize="small" />
</Box>
<Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: 'grey.900' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.2 }}>
Survey Records
</Typography>
<Typography variant="body2" sx={{ color: 'grey.600', mt: 0.5 }}>
<Typography variant="caption" color="text.secondary">
Manage business enquiry and survey data from the field
</Typography>
</Box>
</Box>
{/* Toolbar Section */}
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'grey.100', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small"
placeholder="Search by company, area or phone..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ width: { xs: '100%', md: 400 } }}
sx={{ width: { xs: '100%', md: 300 } }}
InputProps={{
startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment>,
}}
/>
<Stack direction="row" spacing={2} alignItems="center">
<Box sx={{ flexGrow: 1 }} />
{!loading && (
<Typography variant="body2" sx={{ color: 'grey.500', fontWeight: 600 }}>
<Typography variant="body2" color="text.secondary">
{filtered.length} records
</Typography>
)}
<Button variant="contained" startIcon={<AddIcon />} onClick={() => { setEditingRecord(null); setFormOpen(true); }} sx={{ bgcolor: '#c01227', '&:hover': { bgcolor: '#a00f20' }, borderRadius: 2, px: 2, fontWeight: 700, whiteSpace: 'nowrap' }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => { setEditingRecord(null); setFormOpen(true); }} sx={{ flex: { xs: 1, md: 'none' } }}>
Add New Survey
</Button>
</Stack>
</Box>
{/* List Section */}
<Box sx={{ p: 3, bgcolor: 'grey.50' }}>
<Box sx={{ p: 2, bgcolor: 'grey.50' }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
<CircularProgress />
</Box>
) : paginatedKeys.length > 0 ? (
@@ -847,16 +812,10 @@ export default function Survey() {
))}
</Box>
) : (
<Box sx={{ py: 10, textAlign: 'center' }}>
<AssignmentOutlinedIcon sx={{ fontSize: 48, color: 'grey.300', mb: 2 }} />
<Typography variant="h6" color="text.secondary">No survey records found.</Typography>
<Typography variant="body2" color="text.disabled" sx={{ mt: 1 }}>Try adjusting your search filters.</Typography>
</Box>
<EmptyState icon={AssignmentOutlinedIcon} title="No survey records found." caption="Try adjusting your search filters." />
)}
</Box>
{/* Pagination Section */}
<Box sx={{ borderTop: '1px solid', borderColor: 'grey.200', bgcolor: 'white' }}>
<TablePagination
component="div"
count={companyKeys.length}
@@ -867,7 +826,6 @@ export default function Survey() {
rowsPerPageOptions={[5, 10, 25]}
labelRowsPerPage="Companies per page:"
/>
</Box>
</Card>
<SurveyFormDialog
@@ -890,6 +848,6 @@ export default function Survey() {
<Button color="error" variant="contained" onClick={confirmDelete} disabled={deleting} startIcon={deleting ? <CircularProgress size={16} color="inherit" /> : null}>Delete</Button>
</DialogActions>
</Dialog>
</Box>
</>
);
}