updates on the astryx desing
This commit is contained in:
@@ -1,28 +1,7 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Button,
|
||||
Grid,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import {
|
||||
MdPeopleAlt,
|
||||
MdOutlinePeopleAlt,
|
||||
@@ -34,21 +13,33 @@ import {
|
||||
MdPersonAddAlt1,
|
||||
MdOutlineReceiptLong
|
||||
} from 'react-icons/md';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { Grid } from '@astryxdesign/core/Grid';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||||
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
||||
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
||||
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { Divider } from '@astryxdesign/core/Divider';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
import TableLoader from 'components/nearle_components/TableLoader';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
import { getAdminCustomers, updateAdminCustomer } from 'pages/api/doormileApi';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { DT, soft, edge } from 'themes/dt/tokens';
|
||||
import { AccentAvatar } from 'themes/dt/primitives';
|
||||
import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
import { TableScroll, AccentAvatar } from 'themes/dt/primitives';
|
||||
|
||||
// ============================================================================
|
||||
// GET /admin/customers confirmed live: { appcustomerid, createdat, email,
|
||||
@@ -65,8 +56,7 @@ import { AccentAvatar } from 'themes/dt/primitives';
|
||||
// not persisted" pattern as `pages/nearle/clients/createCustomer.js`).
|
||||
// ============================================================================
|
||||
|
||||
const BRAND = '#000000';
|
||||
const BRAND_LIGHT = '#3A3A3A';
|
||||
const BRAND = DT.brand;
|
||||
|
||||
const custId = (row) => row.appcustomerid;
|
||||
const custName = (row) => row.name || '—';
|
||||
@@ -75,8 +65,7 @@ const custEmail = (row) => row.email || '—';
|
||||
const custBookings = (row) => row.totalbookings ?? 0;
|
||||
|
||||
const Customers = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMobile = useIsNarrowViewport();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchword, setSearchword] = useState('');
|
||||
@@ -100,8 +89,9 @@ const Customers = () => {
|
||||
const stats = useMemo(() => {
|
||||
const total = customers.length;
|
||||
const withEmail = customers.filter((c) => c.email).length;
|
||||
const withPhone = customers.filter((c) => c.phone).length;
|
||||
const totalBookings = customers.reduce((sum, c) => sum + (Number(c.totalbookings) || 0), 0);
|
||||
return { total, withEmail, totalBookings };
|
||||
return { total, withEmail, withPhone, totalBookings };
|
||||
}, [customers]);
|
||||
|
||||
const opentoast = (message, variant = 'error') =>
|
||||
@@ -177,395 +167,214 @@ const Customers = () => {
|
||||
subtitle="Live · B2C Directory"
|
||||
live
|
||||
action={
|
||||
<Tooltip title="Adds a contact to your tenant's own customer book (used when creating orders) — separate from the order-derived B2C directory below">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<MdPersonAddAlt1 size={16} />}
|
||||
onClick={() => navigate('/doormile/customer/create')}
|
||||
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: BRAND } }}
|
||||
>
|
||||
Add Customer
|
||||
</Button>
|
||||
<Tooltip content="Adds a contact to your tenant's own customer book (used when creating orders) — separate from the order-derived B2C directory below">
|
||||
<Button label="Add Customer" variant="primary" icon={<MdPersonAddAlt1 size={16} />} onClick={() => navigate('/doormile/customer/create')} />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
<Grid item xs={6} md={3}>
|
||||
<StatCard title="Total Customers" value={stats.total} icon={<MdOutlinePeopleAlt size={20} />} color={BRAND} />
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<StatCard title="With Email" value={stats.withEmail} icon={<MdOutlineMailOutline size={20} />} color="#0ea5e9" />
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<StatCard title="Total Bookings" value={stats.totalBookings} icon={<MdOutlineReceiptLong size={20} />} color="#10b981" />
|
||||
</Grid>
|
||||
<Grid columns={{ minWidth: 200, max: 4, repeat: 'fit' }} gap={3}>
|
||||
<StatCard title="Total Customers" value={stats.total} icon={<MdOutlinePeopleAlt size={20} />} color={BRAND} />
|
||||
<StatCard title="With Email" value={stats.withEmail} icon={<MdOutlineMailOutline size={20} />} color="#0ea5e9" />
|
||||
<StatCard title="With Phone" value={stats.withPhone} icon={<MdOutlinePhoneAndroid size={20} />} color="#8b5cf6" />
|
||||
<StatCard title="Total Bookings" value={stats.totalBookings} icon={<MdOutlineReceiptLong size={20} />} color="#10b981" />
|
||||
</Grid>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mt: { xs: 1.5, md: 2 },
|
||||
p: { xs: 1, md: 1.5 },
|
||||
borderTopLeftRadius: DT.radiusCard / 8,
|
||||
borderTopRightRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
borderBottom: 0,
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={1.25}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPeopleAlt size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||||
>
|
||||
Directory
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{customers.length} total · {rows.length} shown
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box sx={{ width: { xs: '100%', sm: 280 } }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search customers (ctrl+k)"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<VStack gap={0} padding={0} style={{ marginTop: 16 }}>
|
||||
<Card padding={3} elevation="none" style={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderBottom: 'none' }}>
|
||||
<HStack justify="between" vAlign="center" gap={3} wrap="wrap">
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPeopleAlt size={18} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
||||
Directory
|
||||
</Text>
|
||||
<Text weight="bold">
|
||||
{customers.length} total · {rows.length} shown
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<div style={{ width: 280, maxWidth: '100%' }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search customers (ctrl+k)"
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</Card>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderBottomLeftRadius: DT.radiusCard / 8,
|
||||
borderBottomRightRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
overflow: 'hidden',
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
rows.length === 0 && !isLoading ? (
|
||||
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdPeopleAlt size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
No customers to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<MobileCardList scroll>
|
||||
{rows.map((row, index) => (
|
||||
<MobileCard
|
||||
key={custId(row) || index}
|
||||
accent={BRAND}
|
||||
header={
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
|
||||
<AccentAvatar color={BRAND} size={36}>
|
||||
<MdPeopleAlt size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }} noWrap>
|
||||
{custName(row)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{custId(row) ?? '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<IconButton size="small" onClick={() => openEdit(row)} sx={{ color: BRAND, flexShrink: 0 }}>
|
||||
<MdEdit size={14} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="Phone" value={custPhone(row)} />
|
||||
<MobileField label="Email" value={custEmail(row)} />
|
||||
<MobileField label="Bookings" value={custBookings(row)} />
|
||||
</MobileFieldGrid>
|
||||
</MobileCard>
|
||||
))}
|
||||
</MobileCardList>
|
||||
)
|
||||
) : (
|
||||
<TableContainer
|
||||
sx={{
|
||||
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
|
||||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: edge(BRAND), borderRadius: 8 },
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader sx={{ minWidth: 720 }}>
|
||||
<TableHead>
|
||||
<TableRow
|
||||
sx={{
|
||||
'& th': {
|
||||
backgroundColor: DT.surfaceAlt,
|
||||
color: DT.textSecondary,
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.6,
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
borderBottom: `1px solid ${DT.borderSubtle}`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Customer</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell align="center">Bookings</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading && <OrdersTableSkeleton col={4} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdPeopleAlt size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
No customers to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<TableRow
|
||||
key={custId(row) || index}
|
||||
sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` }, '&:hover': { backgroundColor: DT.surfaceAlt } }}
|
||||
>
|
||||
<TableCell>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPeopleAlt size={16} />
|
||||
<Card padding={0} elevation="none" style={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, overflow: 'hidden' }}>
|
||||
{isMobile ? (
|
||||
rows.length === 0 && !isLoading ? (
|
||||
<EmptyState title="No customers to show" description="Customers appear here once they've placed an order." icon={<MdPeopleAlt size={28} />} />
|
||||
) : (
|
||||
<MobileCardList scroll>
|
||||
{rows.map((row, index) => (
|
||||
<MobileCard
|
||||
key={custId(row) || index}
|
||||
accent={BRAND}
|
||||
header={
|
||||
<HStack justify="between" vAlign="center" gap={1}>
|
||||
<HStack gap={2} vAlign="center" minWidth={0}>
|
||||
<AccentAvatar color={BRAND} size={36}>
|
||||
<MdPeopleAlt size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="bold" maxLines={1}>
|
||||
{custName(row)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{custId(row) ?? '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdPhone size={12} color={DT.textMuted} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{custPhone(row)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{row.email && (
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdMail size={12} color={DT.textMuted} />
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.email}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="center">{custBookings(row)}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Edit customer">
|
||||
<IconButton size="small" onClick={() => openEdit(row)} sx={{ color: BRAND }}>
|
||||
<MdEdit size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<IconButton label="Edit customer" tooltip="Edit customer" size="sm" variant="ghost" icon={<MdEdit size={14} />} onClick={() => openEdit(row)} />
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="Phone" value={custPhone(row)} />
|
||||
<MobileField label="Email" value={custEmail(row)} />
|
||||
<MobileField label="Bookings" value={custBookings(row)} />
|
||||
</MobileFieldGrid>
|
||||
</MobileCard>
|
||||
))}
|
||||
</MobileCardList>
|
||||
)
|
||||
) : (
|
||||
<TableScroll minWidth={720}>
|
||||
<Table density="balanced" dividers="rows" hasHover>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>#</TableHeaderCell>
|
||||
<TableHeaderCell>Customer</TableHeaderCell>
|
||||
<TableHeaderCell>Contact</TableHeaderCell>
|
||||
<TableHeaderCell>Bookings</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading && <TableLoader rows={5} columns={5} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>
|
||||
<EmptyState title="No customers to show" description="Customers appear here once they've placed an order." icon={<MdPeopleAlt size={28} />} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Paper>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<TableRow key={custId(row) || index}>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPeopleAlt size={16} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="bold">{custName(row)}</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{custId(row) ?? '—'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<VStack gap={0} padding={0}>
|
||||
<HStack gap={1} vAlign="center">
|
||||
<MdPhone size={12} color={DT.textMuted} />
|
||||
<Text weight="bold">{custPhone(row)}</Text>
|
||||
</HStack>
|
||||
{row.email && (
|
||||
<HStack gap={1} vAlign="center">
|
||||
<MdMail size={12} color={DT.textMuted} />
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.email}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</TableCell>
|
||||
<TableCell>{custBookings(row)}</TableCell>
|
||||
<TableCell>
|
||||
<HStack justify="end">
|
||||
<IconButton label="Edit customer" tooltip="Edit customer" size="sm" variant="ghost" icon={<MdEdit size={16} />} onClick={() => openEdit(row)} />
|
||||
</HStack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
)}
|
||||
</Card>
|
||||
</VStack>
|
||||
|
||||
<Dialog
|
||||
open={!!editRow}
|
||||
onClose={() => setEditRow(null)}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
|
||||
color: '#fff',
|
||||
py: 2
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 36, height: 36, bgcolor: 'rgba(255,255,255,0.2)', color: '#fff' }}>
|
||||
<FaRegEdit style={{ fontSize: 18 }} />
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
sx={{ fontSize: 11, fontWeight: 700, opacity: 0.85, letterSpacing: 0.6, textTransform: 'uppercase', lineHeight: 1 }}
|
||||
>
|
||||
Customer
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 800, fontSize: '1.1rem', lineHeight: 1.2, mt: 0.25 }}>
|
||||
Edit {editRow ? custName(editRow) : 'Customer'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Customer Name</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={form.name || ''}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Contact Number</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextField variant="outlined" value="+91" disabled sx={{ width: 64, cursor: 'not-allowed' }} />
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={form.phone || ''}
|
||||
inputProps={{ maxLength: 10, inputMode: 'numeric', pattern: '[0-9]*' }}
|
||||
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value.replace(/\D/g, '') }))}
|
||||
InputProps={{ startAdornment: <MdOutlinePhoneAndroid size={14} style={{ marginRight: 6, color: DT.textMuted }} /> }}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography sx={{ mb: 1 }}>Email</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="email"
|
||||
value={form.email || ''}
|
||||
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
||||
InputProps={{ startAdornment: <MdMail size={14} style={{ marginRight: 6, color: DT.textMuted }} /> }}
|
||||
/>
|
||||
</Grid>
|
||||
<Dialog isOpen={!!editRow} onOpenChange={(next) => !next && setEditRow(null)} width={760} purpose="form">
|
||||
<Layout
|
||||
header={
|
||||
<DialogHeader
|
||||
title={`Edit ${editRow ? custName(editRow) : 'Customer'}`}
|
||||
subtitle="Customer"
|
||||
onOpenChange={(next) => !next && setEditRow(null)}
|
||||
startContent={
|
||||
<AccentAvatar color={BRAND} selected size={36}>
|
||||
<MdEdit size={18} />
|
||||
</AccentAvatar>
|
||||
}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<LayoutContent padding={4}>
|
||||
<VStack gap={4} padding={0}>
|
||||
<Grid columns={{ minWidth: 220 }} gap={3}>
|
||||
<TextInput label="Customer Name" value={form.name || ''} onChange={(v) => setForm((f) => ({ ...f, name: v }))} />
|
||||
<TextInput
|
||||
label="Contact Number"
|
||||
value={form.phone || ''}
|
||||
onChange={(v) => setForm((f) => ({ ...f, phone: v.replace(/\D/g, '').slice(0, 10) }))}
|
||||
startIcon={MdOutlinePhoneAndroid}
|
||||
/>
|
||||
<TextInput label="Email" type="email" value={form.email || ''} onChange={(v) => setForm((f) => ({ ...f, email: v }))} startIcon={MdMail} />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontStyle: 'italic' }}>
|
||||
The backend doesn't store an address against a customer yet — these fields aren't required and won't be saved
|
||||
on submit.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Divider />
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography sx={{ mb: 1 }}>Address</Typography>
|
||||
<AddressAutocomplete
|
||||
id="edit-customer-address"
|
||||
fullWidth
|
||||
value={addressInput}
|
||||
onChange={(text) => setAddressInput(text)}
|
||||
onPlaceSelected={handleAddressPlaceSelected}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Location</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={pickAddress.suburb || ''}
|
||||
onChange={(e) => setPickAddress((p) => ({ ...p, suburb: e.target.value }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>City</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={pickAddress.city || ''}
|
||||
onChange={(e) => setPickAddress((p) => ({ ...p, city: e.target.value }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>State</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={pickAddress.state || ''}
|
||||
onChange={(e) => setPickAddress((p) => ({ ...p, state: e.target.value }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Postcode</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={pickAddress.postcode || ''}
|
||||
onChange={(e) => setPickAddress((p) => ({ ...p, postcode: e.target.value }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography sx={{ mb: 1 }}>Landmark</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={pickAddress.landmark || ''}
|
||||
onChange={(e) => setPickAddress((p) => ({ ...p, landmark: e.target.value }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
|
||||
<Typography sx={{ mb: 1 }}>Latitude</Typography>
|
||||
<TextField variant="outlined" fullWidth disabled value={latLng.latitude || ''} sx={{ cursor: 'not-allowed' }} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
|
||||
<Typography sx={{ mb: 1 }}>Longitude</Typography>
|
||||
<TextField variant="outlined" fullWidth disabled value={latLng.longitude || ''} sx={{ cursor: 'not-allowed' }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setEditRow(null)} sx={{ color: DT.textSecondary }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: BRAND } }}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<Text type="supporting" color="secondary" style={{ fontStyle: 'italic' }}>
|
||||
The backend doesn't store an address against a customer yet — these fields aren't required and won't be saved on submit.
|
||||
</Text>
|
||||
|
||||
<AddressAutocomplete id="edit-customer-address" label="Address" value={addressInput} onChange={setAddressInput} onPlaceSelected={handleAddressPlaceSelected} />
|
||||
|
||||
<Grid columns={{ minWidth: 200 }} gap={3}>
|
||||
<TextInput label="Location" value={pickAddress.suburb || ''} onChange={(v) => setPickAddress((p) => ({ ...p, suburb: v }))} />
|
||||
<TextInput label="City" value={pickAddress.city || ''} onChange={(v) => setPickAddress((p) => ({ ...p, city: v }))} />
|
||||
<TextInput label="State" value={pickAddress.state || ''} onChange={(v) => setPickAddress((p) => ({ ...p, state: v }))} />
|
||||
<TextInput label="Postcode" value={pickAddress.postcode || ''} onChange={(v) => setPickAddress((p) => ({ ...p, postcode: v }))} />
|
||||
<TextInput label="Landmark" value={pickAddress.landmark || ''} onChange={(v) => setPickAddress((p) => ({ ...p, landmark: v }))} />
|
||||
<TextInput label="Latitude" value={latLng.latitude || ''} isDisabled />
|
||||
<TextInput label="Longitude" value={latLng.longitude || ''} isDisabled />
|
||||
</Grid>
|
||||
</VStack>
|
||||
</LayoutContent>
|
||||
}
|
||||
footer={
|
||||
<LayoutFooter hasDivider>
|
||||
<HStack gap={2} justify="end">
|
||||
<Button label="Cancel" variant="secondary" onClick={() => setEditRow(null)} isDisabled={saveMutation.isPending} />
|
||||
<Button label="Save Changes" variant="primary" onClick={() => saveMutation.mutate()} isLoading={saveMutation.isPending} />
|
||||
</HStack>
|
||||
</LayoutFooter>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user