first commit
This commit is contained in:
33
src/components/EmptyState.jsx
Normal file
33
src/components/EmptyState.jsx
Normal 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
51
src/components/Logo.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
src/components/MainCard.jsx
Normal file
17
src/components/MainCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/MapPlaceholder.jsx
Normal file
59
src/components/MapPlaceholder.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
src/components/PageHeader.jsx
Normal file
42
src/components/PageHeader.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
src/components/StatCard.jsx
Normal file
55
src/components/StatCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
src/components/StatusChip.jsx
Normal file
54
src/components/StatusChip.jsx
Normal 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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
30
src/components/TabLabelCount.jsx
Normal file
30
src/components/TabLabelCount.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
src/components/UserAvatar.jsx
Normal file
12
src/components/UserAvatar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
src/components/charts/AreaChart.jsx
Normal file
66
src/components/charts/AreaChart.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/charts/DonutChart.jsx
Normal file
59
src/components/charts/DonutChart.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user