first commit

This commit is contained in:
2026-06-05 17:28:05 +05:30
commit a162fa89e5
62 changed files with 8729 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import { Box, Typography } from '@mui/material';
import InboxOutlinedIcon from '@mui/icons-material/InboxOutlined';
// ==============================|| EMPTY STATE ||============================== //
export default function EmptyState({ icon: Icon = InboxOutlinedIcon, title = 'No records found', caption, sx }) {
return (
<Box sx={{ textAlign: 'center', py: 6, px: 2, ...sx }}>
<Box
sx={{
width: 72,
height: 72,
borderRadius: '50%',
bgcolor: 'grey.100',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
mb: 2
}}
>
<Icon sx={{ fontSize: 34, color: 'grey.400' }} />
</Box>
<Typography variant="h5" color="text.secondary">
{title}
</Typography>
{caption && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
{caption}
</Typography>
)}
</Box>
);
}

51
src/components/Logo.jsx Normal file
View File

@@ -0,0 +1,51 @@
import { Box, Typography } from '@mui/material';
// ==============================|| DOORMILE WORDMARK LOGO ||============================== //
// Uses the brand wordmark asset (white PNG). `onDark` shows it as-is on dark/red
// surfaces; on light surfaces it is recoloured to near-black. `compact` (e.g. the
// collapsed sidebar) renders just the square "D" badge, since the wordmark won't fit.
const LOGO_SRC = '/Doormile-logo.png';
export default function Logo({ onDark = false, compact = false, height = 26, sx }) {
if (compact) {
const mark = onDark ? '#FFFFFF' : '#C01227';
const markText = onDark ? '#C01227' : '#FFFFFF';
return (
<Box sx={{ display: 'flex', alignItems: 'center', ...sx }}>
<Box
sx={{
width: 34,
height: 34,
borderRadius: 2,
bgcolor: mark,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: onDark ? 'none' : '0 4px 10px rgba(192, 18, 39,0.30)'
}}
>
<Typography sx={{ color: markText, fontWeight: 800, fontSize: '1.25rem', lineHeight: 1 }}>D</Typography>
</Box>
</Box>
);
}
return (
<Box sx={{ display: 'flex', alignItems: 'center', ...sx }}>
<Box
component="img"
src={LOGO_SRC}
alt="Doormile"
sx={{
height,
width: 'auto',
display: 'block',
// The asset is white; on light surfaces recolour it to near-black so it stays visible.
filter: onDark ? 'none' : 'brightness(0) saturate(100%)'
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,17 @@
import { Card, CardHeader, CardContent, Divider, Box } from '@mui/material';
// ==============================|| MAIN CARD (titled surface) ||============================== //
export default function MainCard({ title, action, children, divider = true, contentSx, sx, noPadding = false }) {
return (
<Card sx={sx}>
{title && (
<>
<CardHeader title={title} action={action} />
{divider && <Divider />}
</>
)}
{noPadding ? <Box>{children}</Box> : <CardContent sx={contentSx}>{children}</CardContent>}
</Card>
);
}

View File

@@ -0,0 +1,59 @@
import { Box, Typography, Chip } from '@mui/material';
import RoomIcon from '@mui/icons-material/Room';
import TwoWheelerIcon from '@mui/icons-material/TwoWheeler';
// Stylised static "map" surface — pins + red route line. Swap for Leaflet/Google later.
export default function MapPlaceholder({ height = 360, pins = [], showRoute = true, label = 'Live Tracking', riders = [] }) {
return (
<Box
sx={{
position: 'relative',
height,
borderRadius: 2,
overflow: 'hidden',
bgcolor: '#EAEEF3',
backgroundImage:
'linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px)',
backgroundSize: '32px 32px',
border: '1px solid',
borderColor: 'grey.200'
}}
>
{/* faux roads */}
<Box sx={{ position: 'absolute', top: '30%', left: 0, right: 0, height: 8, bgcolor: '#fff', opacity: 0.85 }} />
<Box sx={{ position: 'absolute', top: 0, bottom: 0, left: '60%', width: 8, bgcolor: '#fff', opacity: 0.85 }} />
<Box sx={{ position: 'absolute', top: '65%', left: 0, right: 0, height: 6, bgcolor: '#fff', opacity: 0.7 }} />
{showRoute && (
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<path d="M 18% 78% Q 45% 50% 78% 22%" fill="none" stroke="#C01227" strokeWidth="3" strokeDasharray="2 8" strokeLinecap="round" />
</svg>
)}
{(pins.length ? pins : [
{ x: '18%', y: '78%', label: 'Pickup', color: '#00A854' },
{ x: '78%', y: '22%', label: 'Drop', color: '#C01227' }
]).map((p, i) => (
<Box key={i} sx={{ position: 'absolute', left: p.x, top: p.y, transform: 'translate(-50%, -100%)', textAlign: 'center' }}>
<RoomIcon sx={{ color: p.color, fontSize: 34, filter: 'drop-shadow(0 2px 3px rgba(0,0,0,0.3))' }} />
{p.label && (
<Chip size="small" label={p.label} sx={{ bgcolor: '#fff', fontWeight: 600, mt: -0.5 }} />
)}
</Box>
))}
{riders.map((r, i) => (
<Box key={i} sx={{ position: 'absolute', left: r.x, top: r.y, transform: 'translate(-50%, -50%)' }}>
<Box sx={{ width: 30, height: 30, borderRadius: '50%', bgcolor: r.active ? '#C01227' : '#8C8C8C', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 6px rgba(0,0,0,0.3)' }}>
<TwoWheelerIcon sx={{ color: '#fff', fontSize: 18 }} />
</Box>
</Box>
))}
<Chip label={label} size="small" sx={{ position: 'absolute', top: 12, left: 12, bgcolor: '#fff', fontWeight: 600 }} />
<Typography variant="caption" sx={{ position: 'absolute', bottom: 8, right: 12, color: 'grey.500' }}>
Map data © Doormile demo
</Typography>
</Box>
);
}

View File

@@ -0,0 +1,42 @@
import { Box, Typography, Breadcrumbs, Link, Stack } from '@mui/material';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Link as RouterLink } from 'react-router-dom';
// ==============================|| PAGE HEADER (title + breadcrumb + actions) ||============================== //
export default function PageHeader({ title, breadcrumbs = [], action }) {
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', sm: 'center' }}
spacing={1.5}
sx={{ mb: 3 }}
>
<Box>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>
{title}
</Typography>
{breadcrumbs.length > 0 && (
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />} sx={{ mt: 0.5 }}>
<Link component={RouterLink} to="/dashboard" underline="hover" color="text.secondary" variant="caption">
Home
</Link>
{breadcrumbs.map((b, i) =>
b.to && i < breadcrumbs.length - 1 ? (
<Link key={i} component={RouterLink} to={b.to} underline="hover" color="text.secondary" variant="caption">
{b.label}
</Link>
) : (
<Typography key={i} variant="caption" color="primary.main" sx={{ fontWeight: 600 }}>
{b.label}
</Typography>
)
)}
</Breadcrumbs>
)}
</Box>
{action && <Box>{action}</Box>}
</Stack>
);
}

View File

@@ -0,0 +1,55 @@
import { Card, CardContent, Box, Typography, Avatar, Stack } from '@mui/material';
import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded';
import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded';
// ==============================|| STAT / KPI CARD ||============================== //
export default function StatCard({ title, value, icon: Icon, color = 'primary', trend, caption }) {
const trendUp = typeof trend === 'number' ? trend >= 0 : null;
return (
<Card sx={{ height: '100%' }}>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Box>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
{title}
</Typography>
<Typography variant="h3" sx={{ mt: 0.75, fontWeight: 700, color: 'grey.800' }}>
{value}
</Typography>
</Box>
{Icon && (
<Avatar
variant="rounded"
sx={{ bgcolor: `${color}.lighter`, color: `${color}.main`, width: 44, height: 44 }}
>
<Icon fontSize="small" />
</Avatar>
)}
</Stack>
{(trendUp !== null || caption) && (
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 1.5 }}>
{trendUp !== null && (
<>
{trendUp ? (
<ArrowUpwardRoundedIcon sx={{ fontSize: 16, color: 'success.main' }} />
) : (
<ArrowDownwardRoundedIcon sx={{ fontSize: 16, color: 'error.main' }} />
)}
<Typography variant="caption" sx={{ fontWeight: 600, color: trendUp ? 'success.main' : 'error.main' }}>
{Math.abs(trend)}%
</Typography>
</>
)}
{caption && (
<Typography variant="caption" color="text.secondary">
{caption}
</Typography>
)}
</Stack>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,54 @@
import { Chip } from '@mui/material';
// ==============================|| STATUS CHIP ||============================== //
// Soft-filled status chips used across orders, deliveries, riders, invoices.
const MAP = {
// orders / deliveries
pending: { color: 'warning', label: 'Pending' },
created: { color: 'info', label: 'Created' },
assigned: { color: 'info', label: 'Assigned' },
accepted: { color: 'info', label: 'Accepted' },
arrived: { color: 'info', label: 'Arrived' },
picked: { color: 'primary', label: 'Picked' },
'in-transit': { color: 'info', label: 'In Transit' },
active: { color: 'primary', label: 'Active' },
delivered: { color: 'success', label: 'Delivered' },
completed: { color: 'success', label: 'Completed' },
skipped: { color: 'warning', label: 'Skipped' },
failed: { color: 'error', label: 'Failed' },
cancelled: { color: 'error', label: 'Cancelled' },
// riders / tenants
online: { color: 'success', label: 'Online' },
offline: { color: 'default', label: 'Offline' },
'on-delivery': { color: 'info', label: 'On Delivery' },
inactive: { color: 'default', label: 'Inactive' },
// invoices
paid: { color: 'success', label: 'Paid' },
open: { color: 'info', label: 'Open' },
overdue: { color: 'error', label: 'Overdue' },
prepaid: { color: 'success', label: 'Prepaid' },
cod: { color: 'warning', label: 'COD' }
};
const TONE = {
success: { bg: '#E3F6EC', fg: '#00773B' },
warning: { bg: '#FFF7E0', fg: '#8A6500' },
info: { bg: '#E0F7F8', fg: '#00727B' },
error: { bg: '#FEEAE9', fg: '#A82216' },
primary: { bg: '#F8E0E3', fg: '#9E0E20' },
default: { bg: '#F0F0F0', fg: '#595959' }
};
export default function StatusChip({ status, size = 'small', label, sx }) {
const key = String(status || '').toLowerCase().replace(/\s+/g, '-');
const cfg = MAP[key] || { color: 'default', label: status };
const tone = TONE[cfg.color] || TONE.default;
return (
<Chip
size={size}
label={label || cfg.label}
sx={{ bgcolor: tone.bg, color: tone.fg, border: 'none', ...sx }}
/>
);
}

View File

@@ -0,0 +1,30 @@
import { Stack, Box } from '@mui/material';
// ==============================|| TAB LABEL WITH INLINE COUNT PILL ||============================== //
// Renders a tab label with the count laid out inline (not an overlapping Badge),
// so adjacent tabs never clip the number. The pill highlights when its tab is active.
export default function TabLabelCount({ label, count, active = false }) {
return (
<Stack direction="row" alignItems="center" spacing={1}>
<span>{label}</span>
<Box
component="span"
sx={{
minWidth: 20,
height: 20,
px: 0.75,
borderRadius: 10,
fontSize: '0.72rem',
fontWeight: 700,
lineHeight: '20px',
textAlign: 'center',
bgcolor: active ? 'primary.main' : 'grey.200',
color: active ? '#fff' : 'text.secondary'
}}
>
{count}
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,12 @@
import { Avatar } from '@mui/material';
import { stringToColor, initials } from '@/utils/format';
// ==============================|| INITIALS AVATAR ||============================== //
export default function UserAvatar({ name = '', size = 32, sx }) {
return (
<Avatar sx={{ width: size, height: size, bgcolor: stringToColor(name), fontSize: size * 0.42, ...sx }}>
{initials(name)}
</Avatar>
);
}

View File

@@ -0,0 +1,66 @@
import { Box, useTheme } from '@mui/material';
// Lightweight dependency-free area/line chart.
// series: [{ name, color, data: number[] }], labels: string[]
export default function AreaChart({ series = [], labels = [], height = 260 }) {
const theme = useTheme();
const W = 720;
const H = height;
const pad = { l: 40, r: 16, t: 16, b: 28 };
const innerW = W - pad.l - pad.r;
const innerH = H - pad.t - pad.b;
const all = series.flatMap((s) => s.data);
const max = Math.max(...all, 1);
const min = 0;
const n = labels.length;
const x = (i) => pad.l + (n <= 1 ? 0 : (i / (n - 1)) * innerW);
const y = (v) => pad.t + innerH - ((v - min) / (max - min)) * innerH;
const ticks = 4;
return (
<Box sx={{ width: '100%', overflow: 'hidden' }}>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height={H} preserveAspectRatio="xMidYMid meet">
{Array.from({ length: ticks + 1 }).map((_, i) => {
const gy = pad.t + (i / ticks) * innerH;
const val = Math.round(max - (i / ticks) * (max - min));
return (
<g key={i}>
<line x1={pad.l} y1={gy} x2={W - pad.r} y2={gy} stroke={theme.palette.grey[200]} strokeWidth="1" />
<text x={pad.l - 8} y={gy + 4} textAnchor="end" fontSize="11" fill={theme.palette.grey[500]}>
{val}
</text>
</g>
);
})}
{labels.map((lb, i) => (
<text key={lb} x={x(i)} y={H - 8} textAnchor="middle" fontSize="11" fill={theme.palette.grey[500]}>
{lb}
</text>
))}
{series.map((s) => {
const line = s.data.map((v, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(v)}`).join(' ');
const area = `${line} L ${x(s.data.length - 1)} ${pad.t + innerH} L ${x(0)} ${pad.t + innerH} Z`;
const gid = `grad-${s.name.replace(/\s/g, '')}`;
return (
<g key={s.name}>
<defs>
<linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={s.color} stopOpacity="0.28" />
<stop offset="100%" stopColor={s.color} stopOpacity="0" />
</linearGradient>
</defs>
{s.fill !== false && <path d={area} fill={`url(#${gid})`} />}
<path d={line} fill="none" stroke={s.color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
{s.data.map((v, i) => (
<circle key={i} cx={x(i)} cy={y(v)} r="3" fill="#fff" stroke={s.color} strokeWidth="2" />
))}
</g>
);
})}
</svg>
</Box>
);
}

View File

@@ -0,0 +1,59 @@
import { Box, Stack, Typography } from '@mui/material';
// Dependency-free donut chart. data: [{ label, value, color }]
export default function DonutChart({ data = [], size = 180, thickness = 26, centerLabel, centerValue }) {
const total = data.reduce((s, d) => s + d.value, 0) || 1;
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
let offset = 0;
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
alignItems="center"
justifyContent="center"
sx={{ flexWrap: 'wrap' }}
>
<Box sx={{ position: 'relative', width: size, maxWidth: '100%', aspectRatio: '1 / 1' }}>
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`}>
<g transform={`rotate(-90 ${size / 2} ${size / 2})`}>
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="#F0F0F0" strokeWidth={thickness} />
{data.map((d, i) => {
const len = (d.value / total) * c;
const seg = (
<circle
key={i}
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke={d.color}
strokeWidth={thickness}
strokeDasharray={`${len} ${c - len}`}
strokeDashoffset={-offset}
strokeLinecap="butt"
/>
);
offset += len;
return seg;
})}
</g>
</svg>
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="h3" sx={{ fontWeight: 700 }}>{centerValue ?? total}</Typography>
<Typography variant="caption" color="text.secondary">{centerLabel ?? 'Total'}</Typography>
</Box>
</Box>
<Stack spacing={1.25}>
{data.map((d) => (
<Stack key={d.label} direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: d.color }} />
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 80 }}>{d.label}</Typography>
<Typography variant="subtitle2">{d.value.toLocaleString('en-IN')}</Typography>
</Stack>
))}
</Stack>
</Stack>
);
}