Files
Doormilexpress_console/src/pages/nearle/customers/customers.js

1183 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { React, useState, useEffect, useRef, useMemo } from 'react';
import axios from 'axios';
import { FaRegEdit } from 'react-icons/fa';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
// material-ui
import {
Box,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Grid,
Typography,
IconButton,
Stack,
Tooltip,
Dialog,
DialogActions,
DialogTitle,
DialogContent,
Button,
TextField,
Autocomplete,
Avatar,
Paper,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdPeopleAlt,
MdMyLocation,
MdPersonPin,
MdPhone,
MdLocationOn,
MdEdit,
MdGroups,
MdHowToReg,
MdPlace
} from 'react-icons/md';
import Geocode from 'react-geocode';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import parse from 'autosuggest-highlight/parse';
import { debounce } from '@mui/material/utils';
// project imports
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { getallcustomers, getcustomersummary } from 'pages/api/api';
import { OpenToast } from 'components/third-party/OpenToast';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — shared with the deliveries / tenants / pricing pages so every
// surface (header, KPI tiles, table, badges, dialog) speaks the same visual
// language. Keep this block in sync with deliveries.js:109162.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 16,
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
// ==============================|| google address ||============================== //
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
function loadScript(src, position, id) {
if (!position) {
return;
}
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
// ==============================|| MUI TABLE - ENHANCED ||============================== //
export default function Customers() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const containerRef = useRef();
const loadMoreRef = useRef();
const [rowsPerPage] = useState(50);
const [page] = useState(0);
const [appId, setAppId] = useState(0);
const [locaName, setLocoName] = useState('All');
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
const [open, setOpen] = useState(false);
const [address, setAddress] = useState('');
const [latlong, setLatlong] = useState({});
const [city, setCity] = useState('');
const [postcode, setPostcode] = useState('');
const [state, setState] = useState('');
const [suburb, setSuburb] = useState('');
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
// ==============================|| for google address ||============================== //
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState('');
const [options, setOptions] = useState([]);
const loaded = useRef(false);
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
document.querySelector('head'),
'google-maps'
);
}
loaded.current = true;
}
const fetch = useMemo(
() =>
debounce((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 400),
[]
);
useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google) {
autocompleteService.current = new window.google.maps.places.AutocompleteService();
}
if (!autocompleteService.current) {
return undefined;
}
if (inputValue === '') {
setOptions(value ? [value] : []);
return undefined;
}
fetch({ input: inputValue }, (results) => {
if (active) {
let newOptions = [];
if (value) {
newOptions = [value];
}
if (results) {
newOptions = [...newOptions, ...results];
}
setOptions(newOptions);
}
});
return () => {
active = false;
};
}, [value, inputValue, fetch]);
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
useEffect(() => {
try {
console.log('selected address =>', address);
Geocode.fromAddress(address).then(
(response) => {
console.log('lat long response =>', response.results[0]);
if (response.status == 'OK') {
const { lat, lng } = response.results[0].geometry.location;
setLatlong({
lat,
lng
});
// setSelectedCustomer({
// ...selectedCustomer,
// latitude: lat,
// longitude: lng
// });
if (response.results[0].address_components) {
let place = response.results[0];
let city1, zipcode1, state1, suburb1;
for (let i = 0; i < place.address_components.length; i++) {
for (let j = 0; j < place.address_components[i].types.length; j++) {
switch (place.address_components[i].types[j]) {
case 'locality':
city1 = place.address_components[i].long_name;
break;
case 'administrative_area_level_1':
state1 = place.address_components[i].long_name;
break;
case 'postal_code':
zipcode1 = place.address_components[i].long_name;
break;
case 'sublocality':
suburb1 = place.address_components[i].long_name;
break;
}
}
}
setCity(city1 || '');
setState(state1 || '');
setPostcode(zipcode1 || '');
setSuburb(suburb1 || '');
setSelectedCustomer((prev) => ({
...prev,
city: city1 || '',
state: state1 || '',
postcode: zipcode1 || '',
suburb: suburb1 || '',
latitude: lat || '',
longitude: lng || ''
}));
}
}
},
(error) => {
console.log(error);
}
);
} catch (err) {
console.log(err);
}
}, [address]);
// useEffect(() => {
// selectedCustomer &&
// setLatlong({
// lat: selectedCustomer.latitude,
// lng: selectedCustomer.longitude
// });
// }, [selectedCustomer]);
// ==============================|| getallcustomers (customers) ||============================== //
const {
data,
isLoading: customersIsLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
refetch: getallcustomersRefetch
} = useInfiniteQuery({
queryKey: ['getAllCustomers', appId, debouncedSearch, rowsPerPage],
queryFn: getallcustomers,
getNextPageParam: (lastPage) => lastPage.nextPage,
keepPreviousData: true
});
const rows = data?.pages.flatMap((page) => page.data) || [];
useEffect(() => {
if (!hasNextPage) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
fetchNextPage();
}
},
{
root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
rootMargin: '0px',
threshold: 1.0
}
);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
return () => {
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
};
}, [hasNextPage, fetchNextPage]);
const handleScroll = (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}
};
// ==============================|| getcustomersummary (customers) ||============================== //
const { data: pageCount, isLoading: customerSummaryIsLoading } = useQuery({
queryKey: ['customersummary', appId],
queryFn: getcustomersummary
});
useEffect(() => {
console.log('pageCount', pageCount);
}, [pageCount]);
// ==============================|| updateCustomer (post)||============================== //
const updateCustomer = async () => {
console.log('selectedCustomer', selectedCustomer);
if (!selectedCustomer.firstname) {
OpenToast('Enter Door NO', 'warning', 1500);
} else if (!selectedCustomer.contactno) {
OpenToast('Enter Contact Number ', 'warning', 1500);
} else if (!selectedCustomer.address) {
OpenToast('Enter Valid Address', 'warning', 1500);
} else if (!selectedCustomer.suburb) {
OpenToast('Enter Suburb', 'warning', 1500);
} else if (!selectedCustomer.city) {
OpenToast('Enter City ', 'warning', 1500);
} else if (!selectedCustomer.state) {
OpenToast('Enter State', 'warning', 1500);
} else if (!selectedCustomer.postcode) {
OpenToast('Enter PostCode', 'warning', 1500);
} else if (!selectedCustomer.landmark) {
OpenToast('Enter Landmark', 'warning', 1500);
} else if (!selectedCustomer.latitude) {
OpenToast('Enter Latitude', 'warning', 1500);
} else if (!selectedCustomer.longitude) {
OpenToast('Enter Longitude', 'warning', 1500);
} else {
try {
const postUpdateResponse = await axios.put(`${process.env.REACT_APP_URL}/customers/update`, {
customerid: selectedCustomer.customerid,
configid: 1,
firstname: selectedCustomer.firstname,
applocationid: selectedCustomer.applocationid,
profileimage: '',
dialcode: '+91',
contactno: selectedCustomer.contactno,
devicetype: '',
deviceid: '',
customertoken: '123',
address: selectedCustomer.address,
suburb: suburb,
city: city,
state: state,
postcode: postcode,
landmark: selectedCustomer.landmark,
doorno: selectedCustomer.doorno,
latitude: selectedCustomer.latitude.toString(),
longitude: selectedCustomer.longitude.toString()
});
console.log('postUpdateResponse', postUpdateResponse);
if (postUpdateResponse.data.status) {
OpenToast(postUpdateResponse.data.message, 'success', 1500);
setOpen(false);
getallcustomersRefetch();
}
} catch (error) {
console.log('postUpdate error', error);
}
}
};
const KPI_META = [
{ key: 'total', label: 'Total Customers', color: '#662582', icon: MdGroups, value: pageCount?.Total ?? 0 },
{ key: 'loaded', label: 'Loaded in View', color: '#0ea5e9', icon: MdHowToReg, value: rows.length },
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdPlace, value: locaName || 'All Zones' }
];
return (
<>
{(customerSummaryIsLoading || customersIsLoading) && <Loader />}
{/* ============================================= || Header | ============================================= */}
<Paper
elevation={0}
sx={{
mb: { xs: 1.5, md: 2 },
p: { xs: 1.5, sm: 2, md: 2.5 },
borderRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
background: `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)`,
boxShadow: DT.shadowMd
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'flex-start', sm: 'center' }}
justifyContent="space-between"
spacing={{ xs: 1.5, sm: 2 }}
>
<Stack direction="row" alignItems="center" spacing={{ xs: 1.25, sm: 1.75 }}>
<Avatar
sx={{
width: { xs: 40, sm: 48 },
height: { xs: 40, sm: 48 },
bgcolor: '#662582',
color: '#fff',
boxShadow: '0 6px 18px rgba(99,102,241,0.32)'
}}
>
<MdPeopleAlt size={22} />
</Avatar>
<Stack>
<Typography
variant="h3"
sx={{
fontWeight: 800,
color: DT.textPrimary,
lineHeight: 1.1,
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
}}
>
Customers
</Typography>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.5 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: '#10b981',
boxShadow: '0 0 0 4px rgba(16,185,129,0.18)'
}}
/>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
Live · {locaName || 'All Zones'}
</Typography>
</Stack>
</Stack>
</Stack>
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor="#662582"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
</Stack>
</Paper>
{/* ============================================= || KPI Cards | ============================================= */}
<Grid container spacing={{ xs: 1.25, sm: 1.5, md: 2 }} sx={{ mt: '1px' }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={4}>
<Paper
elevation={0}
sx={{
position: 'relative',
overflow: 'hidden',
p: { xs: 1.25, sm: 1.75, md: 2.25 },
borderRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s',
'&:hover': {
transform: 'translateY(-3px)',
boxShadow: DT.shadowMd,
borderColor: edge(item.color)
}
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)`
}}
/>
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack spacing={0.5} sx={{ minWidth: 0, flex: 1 }}>
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
fontWeight: 700,
letterSpacing: 0.4,
textTransform: 'uppercase',
fontSize: { xs: 10, sm: 11 },
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{item.label}
</Typography>
<Typography
sx={{
fontWeight: 800,
color: DT.textPrimary,
lineHeight: 1.1,
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' },
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{item.value}
</Typography>
</Stack>
<Avatar
sx={{
width: { xs: 36, sm: 42, md: 48 },
height: { xs: 36, sm: 42, md: 48 },
bgcolor: soft(item.color),
color: item.color,
boxShadow: `inset 0 0 0 1px ${edge(item.color)}`,
flexShrink: 0
}}
>
<Icon size={20} />
</Avatar>
</Stack>
</Paper>
</Grid>
);
})}
</Grid>
{/* ============================================= || Search header | ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
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="#662582" size={32}>
<MdGroups 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 }}>
{pageCount?.Total ?? 0} total · {rows.length} loaded
</Typography>
</Stack>
</Stack>
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder={`Search customers (ctrl+k)`}
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: tint('#662582'),
'& fieldset': { borderColor: edge('#662582'), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: '#662582' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* ============================================= || Table || ============================================= */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{isMobile ? (
<MobileCardList scroll onScroll={handleScroll}>
{customersIsLoading && <LoaderWithImage />}
{rows?.length === 0 && !customersIsLoading ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdGroups size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No customers to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the directory.'}
</Typography>
</Stack>
) : (
rows?.map((row, index) => (
<MobileCard
key={row.customerid || `${row.firstname}-${index}`}
accent="#662582"
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color="#662582" size={36}>
<MdPersonPin size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.firstname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
</Typography>
</Stack>
</Stack>
<Tooltip title="Edit customer" placement="top">
<IconButton
size="small"
onClick={() => {
setSelectedCustomer(row);
setTimeout(() => setOpen(true), 0);
}}
sx={{
bgcolor: soft('#8b5cf6'),
color: '#8b5cf6',
border: `1px solid ${edge('#8b5cf6')}`,
flexShrink: 0,
'&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
}}
>
<MdEdit size={16} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Contact" value={row.contactno || '—'} />
<MobileField label="Location">
{row.suburb ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdLocationOn size={12} /> {row.suburb}
</Box>
) : (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textMuted }}></Typography>
)}
</MobileField>
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
{row.address || '—'}
</Typography>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))
)}
{rows?.length !== 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more customers
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<TableContainer
ref={containerRef}
onScroll={handleScroll}
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'),
borderRadius: 8,
'&:hover': { backgroundColor: '#662582' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: { xs: 720, md: 960 } }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: { xs: 10, md: 11 },
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: { xs: 1, md: 1.25 },
px: { xs: 1, md: 2 }
}
}}
>
<TableCell>#</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Location</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{customersIsLoading && <OrdersTableSkeleton />}
{rows?.length === 0 && !customersIsLoading ? (
<TableRow>
<TableCell colSpan={6} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdGroups size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No customers to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the directory.'}
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
rows?.map((row, index) => (
<TableRow
key={row.customerid || `${row.firstname}-${index}`}
sx={{
cursor: 'pointer',
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 }
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1 + page * rowsPerPage).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#662582" size={36}>
<MdPersonPin size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.firstname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell>
<Stack>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdPhone size={12} color={DT.textMuted} />
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.contactno || '—'}
</Typography>
</Stack>
{row.email && (
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.email}
</Typography>
)}
</Stack>
</TableCell>
<TableCell sx={{ maxWidth: 280 }}>
<Tooltip title={row.address || ''} placement="top">
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
>
{row.address || '—'}
</Typography>
</Tooltip>
</TableCell>
<TableCell>
{row.suburb ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdLocationOn size={12} /> {row.suburb}
</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
<TableCell align="right">
<Tooltip title="Edit customer" placement="top">
<IconButton
size="small"
onClick={() => {
setSelectedCustomer(row);
setTimeout(() => setOpen(true), 0);
}}
sx={{
bgcolor: soft('#8b5cf6'),
color: '#8b5cf6',
border: `1px solid ${edge('#8b5cf6')}`,
'&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
}}
>
<MdEdit size={16} />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))
)}
{rows?.length !== 0 && (
<TableRow>
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more customers
</Typography>
)}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
{/* ======================================== || Edit Dialog || ======================================== */}
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth="lg"
fullWidth
fullScreen={isMobile}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
>
<DialogTitle
id="alert-dialog-title"
sx={{
background: `linear-gradient(135deg, #662582 0%, #9255AB 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: { xs: '1.05rem', sm: '1.2rem' }, lineHeight: 1.2, mt: 0.25 }}>
Edit {selectedCustomer?.firstname || 'Customer'}
</Typography>
</Stack>
</Stack>
</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 2 }}>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Customer Name</Typography>
<TextField
variant="outlined"
fullWidth
defaultValue={selectedCustomer?.firstname}
onChange={(e) => {
setSelectedCustomer({
...selectedCustomer,
firstname: e.target.value
});
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Contact Number</Typography>
<Stack direction={'row'} spacing={1}>
<TextField readonly variant="outlined" value={'+91'} sx={{ width: 60 }} />
<TextField
variant="outlined"
fullWidth
type="text"
value={selectedCustomer?.contactno || ''}
inputProps={{
maxLength: 10,
inputMode: 'numeric', // mobile numeric keypad
pattern: '[0-9]*'
}}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ''); // allow only digits
setSelectedCustomer((prev) => ({
...prev,
contactno: value
}));
}}
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Address</Typography>
<Autocomplete
id="google-map-demo"
sx={{}}
fullWidth
getOptionLabel={(option) => (typeof option === 'string' ? option : option?.description || '')}
filterOptions={(x) => x}
options={options}
autoComplete
includeInputInList
filterSelectedOptions
value={selectedCustomer?.address}
noOptionsText="No locations"
onChange={(event, newValue) => {
setOptions(newValue ? [newValue, ...options] : options);
setValue(newValue);
console.log('newValue', newValue || '');
setAddress(newValue?.description);
setSelectedCustomer({
...selectedCustomer,
address: newValue?.description
});
}}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => <TextField {...params} fullWidth />}
renderOption={(props, option) => {
const matches = option.structured_formatting.main_text_matched_substrings || [];
const parts = parse(
option.structured_formatting.main_text,
matches.map((match) => [match.offset, match.offset + match.length])
);
return (
<li {...props}>
<Grid container alignItems="center">
<Grid item sx={{ display: 'flex', width: 44 }}>
<LocationOnIcon sx={{ color: 'text.secondary' }} />
</Grid>
<Grid
item
sx={{
width: 'calc(100% - 44px)',
wordWrap: 'break-word'
}}
>
{parts?.map((part, index) => (
<Box
key={index}
component="span"
sx={{
fontWeight: part.highlight ? 'bold' : 'regular'
}}
>
{part.text}
</Box>
))}
<Typography variant="body2" color="text.secondary">
{option?.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
</li>
);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Location</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer?.suburb}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
suburb: value
}));
// setSuburb(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>City</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer.city || city || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
city: value
}));
// setCity(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>State</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer.state || state || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
state: value
}));
// setState(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Postcode</Typography>
<TextField
variant="outlined"
fullWidth
value={postcode == '' ? selectedCustomer.postcode : postcode}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
postcode: value
}));
// setPostcode(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Landmark</Typography>
<TextField
variant="outlined"
fullWidth
defaultValue={selectedCustomer.landmark}
onChange={(e) => {
setSelectedCustomer({
...selectedCustomer,
landmark: e.target.value
});
}}
/>
</Grid>
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Latitude</Typography>
<TextField variant="outlined" fullWidth value={latlong.lat} sx={{ cursor: 'not-allowed' }} />
</Grid>
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Longitude</Typography>
<TextField readonly variant="outlined" fullWidth value={latlong.lng} />
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ mr: 2, mb: 2 }}>
<Button onClick={() => setOpen(false)} variant="contained" color="error" sx={{ mr: 2 }}>
Close
</Button>
<Button
onClick={() => {
updateCustomer();
}}
variant="contained"
autoFocus
>
Update
</Button>
</DialogActions>
</Dialog>
</>
);
}