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 ```bash
# Install dependencies
npm install npm install
npm run dev # http://localhost:3000
npm run build # production build → dist/ # Run the development server (default port: 5173 or similar)
npm run preview # preview the production build 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 | | Route Path | Page Component | Description |
| --- | --- | | --- | --- | --- |
| Overview | Dashboard | | `/dashboard` | `Dashboard.jsx` | Overview of registered clients, active contracts, total parcel volume, status donuts, and quick-view charts. |
| Orders | Orders list · Order Details (tracking + timeline) · Create Order · Create Multiple Orders · Assign Orders | | `/tenants` | `Tenants.jsx` | Management view for clients, including expandable details (parcel volume, logistics segments, transit routes, and coordinates map link). |
| Deliveries | Deliveries (expandable products, status tabs) | | `/survey` | `Survey.jsx` | Provider survey records mapping competitors, branch locations, and rate-per-kg answers in an aligned grid layout. |
| Network | Tenants · Create Client · Customers · Create Customer · Pricing · Riders · Create Rider · Edit Rider | | `/pricing` | `Pricing.jsx` | Pricing matrix slabs and logistics carrier rates management with search and active-provider selectors. |
| Reports | Order Summary · Order Details · Riders Summary · Riders Logs (live map) | | `/bookings` | `Bookings.jsx` | Courier bookings creation and management dialogs, along with pricing checks. |
| Finance | Invoices · Invoice Preview (printable A4) · Requests | | `/app-users` | `TeamUsers.jsx` | Team member list, user roles configuration, and initial permission setup. |
| Account | User Profile | | `/settings` | `Settings.jsx` | Preferences dashboard covering organization profiles, system notification switches, and security/password updates. |
| Auth & states | Login · 404 · 500 · Under Construction · Coming Soon |
## 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`. - **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. - **Public Sans** typography scale, clean border-radii (`8px` to `16px`), soft shadows, and subtle responsive layouts (flex-based grid column alignment).
- All tuning lives in `src/theme/` (`palette.js`, `typography.js`, `shadows.js`, `componentsOverride.js`). - Theme configuration resides in `src/theme/`.
## Project structure ## Project Folders
``` ```
src/ src/
theme/ Doormile red theme + MUI component overrides components/ Reusable UI widgets (PageHeader, StatCard, StatusChip, EmptyState)
layout/ MainLayout (red header + collapsible sidebar) · MinimalLayout layout/ MainLayout (header + collapsible sidebar) & MinimalLayout
menu/ sidebar nav config menu/ Sidebar navigation mappings (`navItems.jsx`)
components/ PageHeader, StatCard, StatusChip, MainCard, EmptyState, pages/ CRM modules and page components (dashboard, tenants, survey, pricing, bookings, team, settings)
UserAvatar, MapPlaceholder, Logo, charts/ (Area, Donut) theme/ Doormile custom theme configurations and component style overrides
data/mock.js static demo data powering every screen utils/ API fetching wrapper client
pages/ all 27 screens (lazy-loaded) App.jsx App router and route definitions
App.jsx router · main.jsx app entry 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="/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 path="/settings" element={load(() => import('@/pages/Settings'))} />
</Route> </Route>

View File

@@ -158,13 +158,6 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
</Box> </Box>
))} ))}
</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> </Box>
); );

View File

@@ -15,7 +15,7 @@ const navItems = [
{ id: 'survey', title: 'Providers', url: '/survey', icon: DashboardOutlinedIcon }, { id: 'survey', title: 'Providers', url: '/survey', icon: DashboardOutlinedIcon },
{ id: 'pricing', title: 'Pricing Matrix', url: '/pricing', icon: AttachMoneyOutlinedIcon }, { id: 'pricing', title: 'Pricing Matrix', url: '/pricing', icon: AttachMoneyOutlinedIcon },
{ id: 'bookings', title: 'Bookings', url: '/bookings', icon: DashboardOutlinedIcon }, { 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 { useState, useEffect } from 'react';
import { 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, 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'; } from '@mui/material';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; 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 PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState';
import { fetchClients } from '@/utils/apiClient'; import { fetchClients } from '@/utils/apiClient';
import ClientFormDialog from '../tenants/ClientFormDialog'; import ClientFormDialog from '../tenants/ClientFormDialog';
@@ -427,6 +433,8 @@ export default function Bookings() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [sourceFilter, setSourceFilter] = useState('All'); const [sourceFilter, setSourceFilter] = useState('All');
const [formOpen, setFormOpen] = useState(false); const [formOpen, setFormOpen] = useState(false);
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
@@ -459,29 +467,62 @@ export default function Bookings() {
return matchesSearch && matchesSource; 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 ( return (
<Box sx={{ p: 3, bgcolor: '#f8fafc', minHeight: '80vh' }}> <>
<PageHeader title="Bookings Management" breadcrumbs={[{ label: 'Bookings' }]} /> <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)' }}> <Grid container spacing={2.5} sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', 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>
<TextField <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>
placeholder="Search bookings..." <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>
value={search} <Grid item xs={12} sm={6} lg={3}><StatCard accent title="Pending Pickup" value={stats.pending} icon={PendingActionsOutlinedIcon} color="primary" caption="Awaiting collection" /></Grid>
onChange={(e) => setSearch(e.target.value)} </Grid>
size="small"
InputProps={{ <Card sx={{ overflow: 'hidden' }}>
startAdornment: <InputAdornment position="start"><SearchIcon /></InputAdornment>, <Box
sx: { borderRadius: 3, bgcolor: alpha('#e2e8f0', 0.4) } 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 }}> <Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: 'primary.lighter', color: 'primary.main', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
New Booking <LocalShippingOutlinedIcon fontSize="small" />
</Button> </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>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}> <Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<Tabs value={sourceFilter} onChange={(e, val) => setSourceFilter(val)} aria-label="booking source filter tabs"> <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="All Bookings" value="All" />
<Tab label="App Bookings" value="Customer_App" /> <Tab label="App Bookings" value="Customer_App" />
<Tab label="CRM Bookings" value="CRM_Console" /> <Tab label="CRM Bookings" value="CRM_Console" />
@@ -489,25 +530,24 @@ export default function Bookings() {
</Box> </Box>
{loading ? ( {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> <TableContainer>
<Table> <Table>
<TableHead> <TableHead>
<TableRow sx={{ bgcolor: alpha('#f1f5f9', 0.6) }}> <TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell sx={{ fontWeight: 700 }}>Booking No</TableCell> <TableCell>Booking No</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Delivery City</TableCell> <TableCell>Delivery City</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Provider</TableCell> <TableCell>Provider</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Source</TableCell> <TableCell>Source</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Price</TableCell> <TableCell>Price</TableCell>
<TableCell sx={{ fontWeight: 700 }}>Status</TableCell> <TableCell>Status</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{displayBookings.length === 0 ? ( {paged.map(b => (
<TableRow><TableCell colSpan={6} align="center" sx={{ py: 4 }}>No bookings found.</TableCell></TableRow>
) : (
displayBookings.map(b => (
<TableRow key={b.bookingid} hover> <TableRow key={b.bookingid} hover>
<TableCell sx={{ fontWeight: 600 }}>{b.bookingno}</TableCell> <TableCell sx={{ fontWeight: 600 }}>{b.bookingno}</TableCell>
<TableCell>{b.deliverycity || b.deliverypincode || 'N/A'}</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'} /> <Chip label={b.status} size="small" color={b.status === 'Pending_Pickup' ? 'warning' : 'primary'} />
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))}
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </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> </Card>
<BookingFormDialog <BookingFormDialog
@@ -541,6 +584,6 @@ export default function Bookings() {
clients={clients} clients={clients}
surveys={surveys} surveys={surveys}
/> />
</Box> </>
); );
} }

View File

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

View File

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