updates on the api integration and the design changes on the whole website
This commit is contained in:
@@ -79,7 +79,7 @@ import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { getalltenants, gettenantsummary } from 'pages/api/api';
|
||||
import { getalltenants } from 'pages/api/api';
|
||||
import {
|
||||
getAdminPricing,
|
||||
getDoormilePricing,
|
||||
@@ -440,7 +440,11 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => {
|
||||
id: tenantCustId(row),
|
||||
firstname: row.firstname || '',
|
||||
lastname: row.lastname || '',
|
||||
phone: row.phone || '',
|
||||
// GET/POST /admin/tenantcustomers has a write/read asymmetry confirmed
|
||||
// live: POST accepts `phone`, but the stored/returned field is
|
||||
// `contactno`. The form field itself stays named `phone` (matching
|
||||
// what create/update accept) — only the read source differs.
|
||||
phone: row.contactno || '',
|
||||
email: row.email || ''
|
||||
});
|
||||
setFormOpen(true);
|
||||
@@ -470,7 +474,7 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => {
|
||||
{c.firstname} {c.lastname}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{[c.phone, c.email].filter(Boolean).join(' · ')}
|
||||
{[c.contactno, c.email].filter(Boolean).join(' · ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={0.5}>
|
||||
@@ -580,11 +584,9 @@ const Clients1 = () => {
|
||||
setPage(0);
|
||||
};
|
||||
const handleChange1 = (event, newValue1) => {
|
||||
console.log('newValue1', newValue1);
|
||||
setValue1(newValue1);
|
||||
};
|
||||
const handleChange2 = (event, newValue2) => {
|
||||
console.log('newValue2', newValue2);
|
||||
setValue2(newValue2);
|
||||
};
|
||||
|
||||
@@ -600,9 +602,6 @@ const Clients1 = () => {
|
||||
const handleCollapseToggle2 = (rowIndex) => {
|
||||
setOpenRowIndex2((prevIndex) => (prevIndex === rowIndex ? null : rowIndex));
|
||||
};
|
||||
useEffect(() => {
|
||||
console.log('selectedCustomer', selectedCustomer);
|
||||
}, [selectedCustomer]);
|
||||
useEffect(() => {
|
||||
setOpenRowIndex1(-1);
|
||||
setOpenRowIndex2(-1);
|
||||
@@ -700,15 +699,38 @@ const Clients1 = () => {
|
||||
queryFn: getalltenants
|
||||
});
|
||||
|
||||
// ==============================|| gettenantsummary ||============================== //
|
||||
const {
|
||||
data: summaryData = [],
|
||||
isLoading: summaryDataIsLoading,
|
||||
refetch: summaryDataRefetch
|
||||
} = useQuery({
|
||||
queryKey: ['gettenantsummary', appId, status],
|
||||
queryFn: gettenantsummary
|
||||
});
|
||||
// No tenant-tab-counts endpoint in the new API (gettenantsummary always
|
||||
// returns null) — derive the Active/Pending/Inactive counts client-side
|
||||
// from the full tenant list instead, which getalltenants already fetches.
|
||||
const tenantStatusCounts = React.useMemo(() => {
|
||||
const counts = { active: 0, pending: 0, inactive: 0 };
|
||||
(tenantList || []).forEach((t) => {
|
||||
const s = String(t.status || '').toLowerCase();
|
||||
if (s === 'active') counts.active += 1;
|
||||
else if (s === 'pending') counts.pending += 1;
|
||||
else if (s === 'inactive') counts.inactive += 1;
|
||||
});
|
||||
return counts;
|
||||
}, [tenantList]);
|
||||
|
||||
// getalltenants() takes no filter args (see api.js) — the status tabs and
|
||||
// search box only ever changed the query-cache key, never the actual
|
||||
// request, so every tab showed the exact same unfiltered list with just a
|
||||
// relabeled status badge. Filter the already-fetched list client-side
|
||||
// instead, matching the same real `status` field tenantStatusCounts uses.
|
||||
const filteredTenantList = React.useMemo(() => {
|
||||
const activeStatus = STATUS_TABS[value0]?.status;
|
||||
const q = debouncedSearch.trim().toLowerCase();
|
||||
return (tenantList || []).filter((t) => {
|
||||
if (String(t.status || '').toLowerCase() !== activeStatus) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
t.tenantname?.toLowerCase().includes(q) ||
|
||||
t.primaryemail?.toLowerCase().includes(q) ||
|
||||
t.primarycontact?.includes(debouncedSearch)
|
||||
);
|
||||
});
|
||||
}, [tenantList, value0, debouncedSearch]);
|
||||
|
||||
/* ============================================= || fetchclientpricelist || ============================================= */
|
||||
|
||||
@@ -718,7 +740,7 @@ const Clients1 = () => {
|
||||
const pricing = await getAdminPricing();
|
||||
setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// swallow
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
@@ -731,7 +753,7 @@ const Clients1 = () => {
|
||||
const pricing = await getAdminPricing();
|
||||
setTenanatPricing((pricing || []).filter((p) => p.tenantid === id));
|
||||
} catch (error) {
|
||||
console.log('fetchTenanatPricing', error);
|
||||
// swallow
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
@@ -760,10 +782,8 @@ const Clients1 = () => {
|
||||
2000
|
||||
);
|
||||
getalltenantsRefetch();
|
||||
summaryDataRefetch();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
opentoast(err.message, 'error', 1500);
|
||||
} finally {
|
||||
setisloader(false);
|
||||
@@ -776,7 +796,7 @@ const Clients1 = () => {
|
||||
const pricing = await getDoormilePricing();
|
||||
setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id));
|
||||
} catch (error) {
|
||||
console.log('appPricingRes', error);
|
||||
// swallow
|
||||
}
|
||||
};
|
||||
/* ============================================= || createpricing || ============================================= */
|
||||
@@ -793,10 +813,14 @@ const Clients1 = () => {
|
||||
priceperkg: +selectedPricing.priceperkg || 0,
|
||||
basedistance: +selectedPricing.basedistance || 0,
|
||||
handlingcharges: +selectedPricing.handlingcharges || 0,
|
||||
effectivefrom: dayjs().format('YYYY-MM-DD'),
|
||||
// Confirmed live: POST /admin/pricing rejects a bare YYYY-MM-DD date
|
||||
// for effectivefrom ("invalid request body") — needs a full ISO
|
||||
// datetime. Status also confirmed to need title case ("Active"),
|
||||
// matching every real pricing row.
|
||||
effectivefrom: dayjs().toISOString(),
|
||||
currency: 'INR',
|
||||
priority: 1,
|
||||
status: 'active'
|
||||
status: 'Active'
|
||||
});
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Price Created Successfully', {
|
||||
@@ -812,7 +836,6 @@ const Clients1 = () => {
|
||||
opentoast(res.message || 'Failed to save pricing', 'error', 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
opentoast(err.response?.data?.message || err.message || 'Failed to save pricing', 'error', 2000);
|
||||
}
|
||||
};
|
||||
@@ -826,9 +849,7 @@ const Clients1 = () => {
|
||||
opentoast(updateRes.message || 'Update Failed', 'error', 1500);
|
||||
}
|
||||
getalltenantsRefetch();
|
||||
summaryDataRefetch();
|
||||
} catch (err) {
|
||||
console.log('updateClient', err);
|
||||
opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500);
|
||||
}
|
||||
};
|
||||
@@ -837,7 +858,7 @@ const Clients1 = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{(getalltenantsIsLoading || summaryDataIsLoading || isloader) && <Loader />}
|
||||
{(getalltenantsIsLoading || isloader) && <Loader />}
|
||||
{/* ============================================= || Header | ============================================= */}
|
||||
<PageHeader
|
||||
title="Tenants"
|
||||
@@ -864,7 +885,7 @@ const Clients1 = () => {
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
{KPI_META.map((item, idx) => {
|
||||
const Icon = item.icon;
|
||||
const value = summaryData?.[item.countKey] ?? 0;
|
||||
const value = tenantStatusCounts[item.countKey] ?? 0;
|
||||
return (
|
||||
<Grid item key={item.key} xs={12} sm={4}>
|
||||
<Box
|
||||
@@ -878,7 +899,7 @@ const Clients1 = () => {
|
||||
value={value ?? 0}
|
||||
icon={<Icon size={20} />}
|
||||
color={idx === 0 ? '#662582' : item.color}
|
||||
loading={summaryDataIsLoading}
|
||||
loading={getalltenantsIsLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -925,7 +946,7 @@ const Clients1 = () => {
|
||||
const meta = STATUS_META[t.status];
|
||||
const Icon = meta.icon;
|
||||
const active = value0 === idx;
|
||||
const count = summaryData?.[t.countKey] ?? 0;
|
||||
const count = tenantStatusCounts[t.countKey] ?? 0;
|
||||
return (
|
||||
<Box
|
||||
key={t.status}
|
||||
@@ -987,7 +1008,7 @@ const Clients1 = () => {
|
||||
border: 'none'
|
||||
}}
|
||||
>
|
||||
{summaryDataIsLoading ? <Skeleton variant="text" width={14} height={10} /> : count}
|
||||
{getalltenantsIsLoading ? <Skeleton variant="text" width={14} height={10} /> : count}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -1064,14 +1085,14 @@ const Clients1 = () => {
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell>Delivery OTP</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{getalltenantsIsLoading && <OrdersTableSkeleton />}
|
||||
|
||||
{tenantList?.length == 0 && !isloader ? (
|
||||
{filteredTenantList.length == 0 && !isloader ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
@@ -1088,9 +1109,9 @@ const Clients1 = () => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
tenantList?.map((row, index) => {
|
||||
const rowStatusKey = (value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive');
|
||||
const rowStatusMeta = STATUS_META[rowStatusKey];
|
||||
filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => {
|
||||
const rowStatusKey = String(row.status || '').toLowerCase();
|
||||
const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active;
|
||||
const RowStatusIcon = rowStatusMeta.icon;
|
||||
return (
|
||||
<Fragment key={row.tenantid ?? index}>
|
||||
@@ -1162,20 +1183,9 @@ const Clients1 = () => {
|
||||
</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>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.requiredeliveryotp ? 'Yes' : 'No'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" spacing={0.75} justifyContent="flex-end">
|
||||
@@ -1217,7 +1227,7 @@ const Clients1 = () => {
|
||||
setDialogopen(true);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
getAppPricing(row.applolcationid);
|
||||
getAppPricing(row.applocationid);
|
||||
}}
|
||||
>
|
||||
<IssuesCloseOutlined style={{ fontSize: 14 }} />
|
||||
@@ -1407,7 +1417,7 @@ const Clients1 = () => {
|
||||
<Grid item xs={6}>
|
||||
<Grid container spacing={4} alignItems="center">
|
||||
<Grid item>
|
||||
<FiUser
|
||||
<FaRegCheckCircle
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
@@ -1415,9 +1425,9 @@ const Clients1 = () => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left">{row.firstname}</Typography>
|
||||
<Typography align="left">{row.status || '—'}</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Contact Person
|
||||
Status
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -1461,121 +1471,26 @@ const Clients1 = () => {
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={4} alignItems="center">
|
||||
<Grid item>
|
||||
<FaRegAddressCard
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
{row.requiredeliveryotp ? (
|
||||
<MdCheckCircle
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MdCancel
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left">{row.address}</Typography>
|
||||
<Typography align="left">{row.requiredeliveryotp ? 'Yes' : 'No'}</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Address
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* <Grid item xs={12}>
|
||||
<Grid container spacing={4} alignItems="center">
|
||||
<Grid item>
|
||||
<TbCategoryMinus
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left">{row.subcategoryname}</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Category
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<MdMyLocation
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left" variant="subtitle1">
|
||||
{row.suburb}
|
||||
</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Location
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={4} alignItems="center">
|
||||
<Grid item>
|
||||
<GiModernCity
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left">{row.city}</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
City
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<MdNumbers
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left" variant="subtitle1">
|
||||
{row.postcode}
|
||||
</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
PostCode
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={4} alignItems="center">
|
||||
<Grid item>
|
||||
<TbWorldLatitude
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left">{row.latitude}</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Latitude
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TbWorldLongitude
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'lighter'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs zeroMinWidth>
|
||||
<Typography align="left" variant="subtitle1">
|
||||
{row.longitude}
|
||||
</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Longitude
|
||||
Delivery OTP Required
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -1681,7 +1596,7 @@ const Clients1 = () => {
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
onClick={() => {
|
||||
getAppPricing(selectedCustomer.applolcationid);
|
||||
getAppPricing(selectedCustomer.applocationid);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -2105,7 +2020,7 @@ const Clients1 = () => {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{tenantList?.length == 0 && !isloader ? (
|
||||
{filteredTenantList.length == 0 && !isloader ? (
|
||||
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdGroups size={28} />
|
||||
@@ -2118,9 +2033,9 @@ const Clients1 = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
tenantList?.map((row, index) => {
|
||||
const rowStatusKey = value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive';
|
||||
const rowStatusMeta = STATUS_META[rowStatusKey];
|
||||
filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => {
|
||||
const rowStatusKey = String(row.status || '').toLowerCase();
|
||||
const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active;
|
||||
const RowStatusIcon = rowStatusMeta.icon;
|
||||
const expanded = openRowIndex1 === index;
|
||||
return (
|
||||
@@ -2199,7 +2114,7 @@ const Clients1 = () => {
|
||||
setDialogopen(true);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
getAppPricing(row.applolcationid);
|
||||
getAppPricing(row.applocationid);
|
||||
}}
|
||||
>
|
||||
<IssuesCloseOutlined style={{ fontSize: 14 }} />
|
||||
@@ -2280,7 +2195,7 @@ const Clients1 = () => {
|
||||
setSelectedCustomer(row);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
getAppPricing(row.applolcationid);
|
||||
getAppPricing(row.applocationid);
|
||||
setDialogopen(true);
|
||||
}}
|
||||
>
|
||||
@@ -2295,21 +2210,13 @@ const Clients1 = () => {
|
||||
<MobileField label="Tenant ID" value={`#${row.tenantid}`} />
|
||||
<MobileField label="Contact" value={row.primarycontact || '—'} />
|
||||
<MobileField label="Email" value={row.primaryemail || '—'} full />
|
||||
<MobileField label="Address" full>
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
|
||||
{row.address || '—'}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Status" value={row.status || '—'} />
|
||||
</MobileFieldGrid>
|
||||
|
||||
{expanded && (
|
||||
<Box sx={{ mt: 1.25, pt: 1.25, borderTop: `1px solid ${DT.divider}` }}>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="Contact Person" value={row.firstname || '—'} />
|
||||
<MobileField label="City" value={row.city || '—'} />
|
||||
<MobileField label="Postcode" value={row.postcode || '—'} />
|
||||
<MobileField label="Latitude" value={row.latitude || '—'} />
|
||||
<MobileField label="Longitude" value={row.longitude || '—'} />
|
||||
<MobileField label="Delivery OTP Required" value={row.requiredeliveryotp ? 'Yes' : 'No'} />
|
||||
</MobileFieldGrid>
|
||||
</Box>
|
||||
)}
|
||||
@@ -2321,13 +2228,13 @@ const Clients1 = () => {
|
||||
)}
|
||||
|
||||
{/* ============================================= || Pagination| ============================================= */}
|
||||
{!searchword && tenantList?.length > 0 && (
|
||||
{!searchword && filteredTenantList.length > 0 && (
|
||||
<>
|
||||
<Divider sx={{ borderColor: DT.borderSubtle }} />
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 50, 100]}
|
||||
component="div"
|
||||
count={value0 == 0 ? summaryData?.active : value0 == 1 ? summaryData?.pending : value0 == 2 ? summaryData?.inactive : 0}
|
||||
count={filteredTenantList.length ?? 0}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
|
||||
431
src/pages/nearle/clients/createCustomer.js
Normal file
431
src/pages/nearle/clients/createCustomer.js
Normal file
@@ -0,0 +1,431 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Button,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { MdPersonAddAlt1 } from 'react-icons/md';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
import { createTenantCustomer, getAdminTenants } from 'pages/api/doormileApi';
|
||||
import Loader from 'components/Loader';
|
||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { DT, tint } from 'themes/dt/tokens';
|
||||
|
||||
// Same field set as nearle_console_express's createCustomer.js (Location,
|
||||
// Choose Client, Name, Phone, Email, Door No, Address, Location/City/State/
|
||||
// Postcode, Landmark) — but POST /admin/tenantcustomers only persists
|
||||
// { firstname, lastname, phone, email }. There is no address/coordinate
|
||||
// storage anywhere on the new API for a customer record (confirmed against
|
||||
// express-console-api.md — the old jupiter /customers/create body doesn't
|
||||
// exist here at all). The address section below is collected and shown so
|
||||
// the form matches the reference 1:1, but it is NOT sent on submit — see the
|
||||
// note under that section. `tenantid` isn't in the documented POST body
|
||||
// either; it's included speculatively for staff logins (every other create
|
||||
// endpoint on this API takes tenantid explicitly) and may be silently
|
||||
// ignored server-side — unconfirmed, this endpoint isn't in the "confirmed
|
||||
// end-to-end" list.
|
||||
const Createcustomer = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const navigate = useNavigate();
|
||||
|
||||
const loggedInTenantId = localStorage.getItem('tenantid');
|
||||
const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0';
|
||||
|
||||
const [appId, setAppId] = useState(0);
|
||||
const [locaName, setLocoName] = useState('Select Location');
|
||||
const [tid, setTid] = useState(isStaffLogin ? '' : loggedInTenantId);
|
||||
|
||||
const [firstname, setFirstname] = useState('');
|
||||
const [lastname, setLastname] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [doorno, setDoorno] = useState('');
|
||||
const [inputValue2, setInputValue2] = useState('');
|
||||
const [landmark, setLandmark] = useState('');
|
||||
const [pickCust, setPickCust] = useState({});
|
||||
const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 });
|
||||
const [appLocaLat, setAppLocaLat] = useState();
|
||||
const [appLocaLng, setAppLocaLng] = useState();
|
||||
|
||||
const { data: tenantList = [], isLoading: tenantListLoading } = useQuery({
|
||||
queryKey: ['admin-tenants-picker'],
|
||||
queryFn: getAdminTenants,
|
||||
enabled: isStaffLogin
|
||||
});
|
||||
|
||||
// LocationAutocomplete only reports back applocationid/locationname — it
|
||||
// caches the full hub list (with lat/lng) in localStorage('applocations')
|
||||
// as a side effect, so pull the coordinates for the address-search bias
|
||||
// from there rather than duplicating the GET /admin/hubs call.
|
||||
useEffect(() => {
|
||||
if (!appId) return;
|
||||
try {
|
||||
const hubs = JSON.parse(localStorage.getItem('applocations') || '[]');
|
||||
const hub = hubs.find((h) => h.applocationid === appId);
|
||||
if (hub?.latitude) {
|
||||
setAppLocaLat(hub.latitude);
|
||||
setAppLocaLng(hub.longitude);
|
||||
}
|
||||
} catch (err) {
|
||||
// bias is a soft nicety — ignore a bad cache
|
||||
}
|
||||
}, [appId]);
|
||||
|
||||
const handlePlaceSelected = (place) => {
|
||||
setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() });
|
||||
const parsed = { suburb: '', city: '', state: '', postcode: '' };
|
||||
place.address_components.forEach((component) => {
|
||||
component.types.forEach((type) => {
|
||||
switch (type) {
|
||||
case 'sublocality_level_1':
|
||||
case 'sublocality':
|
||||
parsed.suburb = component.long_name;
|
||||
break;
|
||||
case 'locality':
|
||||
parsed.city = component.long_name;
|
||||
break;
|
||||
case 'administrative_area_level_1':
|
||||
parsed.state = component.long_name;
|
||||
break;
|
||||
case 'postal_code':
|
||||
parsed.postcode = component.long_name;
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
setPickCust({
|
||||
address: place.formatted_address,
|
||||
suburb: parsed.suburb,
|
||||
city: parsed.city,
|
||||
state: parsed.state,
|
||||
postcode: parsed.postcode,
|
||||
latitude: place.geometry.location.lat(),
|
||||
longitude: place.geometry.location.lng()
|
||||
});
|
||||
};
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
};
|
||||
|
||||
const createCustomerMutation = useMutation({
|
||||
mutationFn: (payload) => createTenantCustomer(payload),
|
||||
onSuccess: (res) => {
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Customer created successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/doormile/customers');
|
||||
} else {
|
||||
opentoast(res.message || 'Could not create customer');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
opentoast(err.response?.data?.message || err.message || 'Failed to create customer');
|
||||
}
|
||||
});
|
||||
|
||||
const createprofile = () => {
|
||||
if (isStaffLogin && !tid) {
|
||||
opentoast('Select Tenant');
|
||||
} else if (!firstname) {
|
||||
opentoast('Enter Name');
|
||||
} else if (!phone || phone.length !== 10) {
|
||||
opentoast('Enter a valid 10-digit Mobile Number');
|
||||
} else {
|
||||
createCustomerMutation.mutate({
|
||||
firstname,
|
||||
lastname,
|
||||
phone,
|
||||
email,
|
||||
...(isStaffLogin && tid ? { tenantid: +tid } : {})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{createCustomerMutation.isPending && <Loader />}
|
||||
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: DT.radiusCard + 'px',
|
||||
boxShadow: DT.shadowSoft,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
|
||||
<MdPersonAddAlt1 size={22} />
|
||||
</Avatar>
|
||||
<Typography variant="h3">Create Customer</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<MainCard
|
||||
contentSX={{ p: { xs: 1.5, md: 3 } }}
|
||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
||||
>
|
||||
<Grid container spacing={isMobile ? 2 : 3}>
|
||||
<Grid item xs={12}>
|
||||
<MainCard sx={{ height: '100%' }} contentSX={{ p: { xs: 1.5, md: 2.5 } }}>
|
||||
<Grid container spacing={isMobile ? 2 : 3}>
|
||||
{/* ===================================================== || Location & Client || ===================================================== */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Location</InputLabel>
|
||||
<LocationAutocomplete locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} sx={{}} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
{isStaffLogin && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Choose Client</InputLabel>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
loading={tenantListLoading}
|
||||
options={tenantList || []}
|
||||
getOptionLabel={(option) => option?.tenantname || ''}
|
||||
renderInput={(params) => <TextField {...params} placeholder="Choose Client" />}
|
||||
onChange={(e, val) => setTid(val ? val.tenantid : '')}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* ===================================================== || Name / Phone / Email || ===================================================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="customer-firstname">Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="customer-firstname"
|
||||
placeholder="First Name"
|
||||
onChange={(e) => setFirstname(e.target.value)}
|
||||
value={firstname}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="customer-lastname">Last Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="customer-lastname"
|
||||
placeholder="Last Name"
|
||||
onChange={(e) => setLastname(e.target.value)}
|
||||
value={lastname}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="customer-phone">Phone Number</InputLabel>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
||||
<MenuItem value="+1">+91</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
type="number"
|
||||
id="customer-phone"
|
||||
fullWidth
|
||||
placeholder="Phone Number"
|
||||
onChange={(e) => {
|
||||
if (e.target.value.toString().length <= 10) {
|
||||
setPhone(e.target.value);
|
||||
}
|
||||
}}
|
||||
value={phone}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="customer-email">Email</InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
id="customer-email"
|
||||
placeholder="Email Address"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
value={email}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* ===================================================== || Door No || ===================================================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="customer-doorno">Door No</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="customer-doorno"
|
||||
placeholder="Door No"
|
||||
onChange={(e) => setDoorno(e.target.value)}
|
||||
value={doorno}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* ===================================================== || Address section — collected, not persisted || ===================================================== */}
|
||||
<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>
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Address</InputLabel>
|
||||
<AddressAutocomplete
|
||||
id="customer-address"
|
||||
fullWidth
|
||||
value={inputValue2}
|
||||
onChange={(text) => setInputValue2(text)}
|
||||
onPlaceSelected={handlePlaceSelected}
|
||||
bias={{ lat: appLocaLat, lng: appLocaLng }}
|
||||
TextFieldProps={{
|
||||
variant: 'outlined',
|
||||
InputProps: {
|
||||
endAdornment: (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setInputValue2('');
|
||||
setPickCust({});
|
||||
setStartPoint({ latitude: 0, longitude: 0 });
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Location</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Location"
|
||||
onChange={(e) => setPickCust((p) => ({ ...p, suburb: e.target.value }))}
|
||||
value={pickCust.suburb || ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>City</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="City"
|
||||
onChange={(e) => setPickCust((p) => ({ ...p, city: e.target.value }))}
|
||||
value={pickCust.city || ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>State</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="State"
|
||||
onChange={(e) => setPickCust((p) => ({ ...p, state: e.target.value }))}
|
||||
value={pickCust.state || ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Post Code</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
placeholder="Postcode"
|
||||
onChange={(e) => setPickCust((p) => ({ ...p, postcode: e.target.value }))}
|
||||
value={pickCust.postcode || ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Landmark</InputLabel>
|
||||
<TextField fullWidth placeholder="Landmark" onChange={(e) => setLandmark(e.target.value)} value={landmark} autoComplete="off" />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Latitude</InputLabel>
|
||||
<TextField fullWidth disabled value={startPoint.latitude || ''} sx={{ cursor: 'not-allowed' }} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel>Longitude</InputLabel>
|
||||
<TextField fullWidth disabled value={startPoint.longitude || ''} sx={{ cursor: 'not-allowed' }} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="flex-end"
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
spacing={2}
|
||||
>
|
||||
<Button variant="contained" onClick={createprofile} fullWidth={isMobile}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Createcustomer;
|
||||
@@ -36,7 +36,11 @@ const Createclient = () => {
|
||||
const [tenantname, setTenantname] = useState('');
|
||||
const [primaryemail, setPrimaryemail] = useState('');
|
||||
const [primarycontact, setPrimarycontact] = useState('');
|
||||
const [status, setStatus] = useState('active');
|
||||
// Confirmed live: POST /admin/tenants doesn't reject lowercase status, but
|
||||
// stores it exactly as sent — every real tenant uses "Active" (title
|
||||
// case), so a lowercase one would be inconsistent with the rest of the
|
||||
// data even though the request itself succeeds.
|
||||
const [status, setStatus] = useState('Active');
|
||||
const [requiredeliveryotp, setRequiredeliveryotp] = useState(false);
|
||||
|
||||
const opentoast = (message) => {
|
||||
@@ -137,8 +141,8 @@ const Createclient = () => {
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="tenant-status">Status</InputLabel>
|
||||
<Select id="tenant-status" fullWidth value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
<MenuItem value="active">Active</MenuItem>
|
||||
<MenuItem value="inactive">Inactive</MenuItem>
|
||||
<MenuItem value="Active">Active</MenuItem>
|
||||
<MenuItem value="Inactive">Inactive</MenuItem>
|
||||
</Select>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user