initial commit

This commit is contained in:
2026-05-13 17:48:36 +05:30
commit 5a80256856
305 changed files with 80994 additions and 0 deletions

View File

@@ -0,0 +1,693 @@
import { React, useState, useEffect, useRef, useMemo } from 'react';
import { Empty } from 'antd';
import axios from 'axios';
import { FaRegEdit } from 'react-icons/fa';
import { RiEdit2Fill } from 'react-icons/ri';
import { useTheme } from '@mui/material/styles';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
// material-ui
import {
Box,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Grid,
Typography,
IconButton,
Stack,
Tooltip,
Dialog,
DialogActions,
DialogTitle,
DialogContent,
Button,
TextField,
Autocomplete
} from '@mui/material';
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 MainCard from 'components/MainCard';
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';
// ==============================|| 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 containerRef = useRef();
const loadMoreRef = useRef();
const [rowsPerPage] = useState(50);
const [page] = useState(0);
const theme = useTheme();
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);
}
}
};
return (
<>
{(customerSummaryIsLoading || customersIsLoading) && (
<>
{/* <CircularLoader /> */}
<Loader />
</>
)}
{/* <TitleCard title={'Customers'} /> */}
<MainCard
content={false}
sx={{}}
title={
<Stack sx={{ m: 2 }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'flex-start', sm: 'center' }}
justifyContent="space-between"
spacing={2}
sx={{ width: '100%' }}
>
{/* Left: Title */}
<Typography variant="h3">Customers</Typography>
{/* Right: Controls */}
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="center" width={{ xs: '100%', sm: 'auto' }}>
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
sx={{
width: { xs: '100%', sm: 320 },
zIndex: 100
}}
/>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder={`Search ${pageCount?.Total || ''} Customer (ctrl+k)`}
sx={{
width: { xs: '100%', sm: 320 }
}}
/>
</Stack>
</Stack>
</Stack>
}
>
<TableContainer ref={containerRef} onScroll={handleScroll} sx={{ maxHeight: 'calc(100vh - 180px)' }}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell sx={{ position: 'sticky !important' }}>#</TableCell>
<TableCell sx={{ position: 'sticky !important' }}>Name</TableCell>
<TableCell sx={{ position: 'sticky !important' }}>Contact</TableCell>
<TableCell sx={{ position: 'sticky !important' }}>Address</TableCell>
<TableCell sx={{ position: 'sticky !important' }}>Location</TableCell>
<TableCell sx={{ position: 'sticky !important' }}>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{customersIsLoading && <OrdersTableSkeleton />}
{rows?.length == 0 && !customersIsLoading ? (
<TableRow>
<TableCell colSpan={6}>
<Stack width={'100%'} direction={'row'} justifyContent={'center'}>
<Empty />
</Stack>
</TableCell>
</TableRow>
) : (
rows?.map((row, index) => {
return (
<TableRow key={row.name}>
<TableCell padding="none">{index + 1 + page * rowsPerPage}</TableCell>
<TableCell align="left">
<Typography align="left" variant="subtitle1">
{row.firstname}
</Typography>
<Typography align="left" variant="caption" color="secondary">
Id : {row.customerid}
</Typography>
</TableCell>
<TableCell align="left">
<Typography align="left" variant="subtitle1">
{row.contactno}
</Typography>
<Typography align="left" variant="caption" color="secondary">
{row.email}
</Typography>
</TableCell>
<TableCell align="left">{row.address}</TableCell>
<TableCell align="left">{row.suburb}</TableCell>
<TableCell align="center">
<Tooltip title={'To Edit'}>
<IconButton
onClick={() => {
console.log('row', row);
setSelectedCustomer(row);
setTimeout(() => {
setOpen(true);
}, 0);
}}
>
<RiEdit2Fill
fontSize={'large'}
style={{
cursor: 'pointer',
color: theme.palette.primary.main
}}
/>
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})
)}
{rows?.length != 0 && (
<TableRow>
<TableCell colSpan={15} rowSpan={3}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage ? <LoaderWithImage /> : hasNextPage ? <LoaderWithImage /> : 'No More Orders'}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<Divider />
</MainCard>
{/* ======================================== || Edit Dialog || ======================================== */}
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth="lg"
fullWidth
>
<DialogTitle id="alert-dialog-title" sx={{ bgcolor: theme.palette.primary.main, color: 'white' }}>
<Stack direction={'row'} alignItems={'center'} spacing={1}>
<FaRegEdit style={{ fontSize: 25 }} /> <Typography variant="h3">Edit Customer</Typography>
</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>
</>
);
}