updates on the api integration and the design changes on the whole website

This commit is contained in:
2026-08-11 11:18:40 +05:30
parent d144ff71a9
commit bf28249528
48 changed files with 2888 additions and 6361 deletions

View File

@@ -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}

View 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&apos;t store an address against a customer yet — these fields aren&apos;t required and won&apos;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;

View File

@@ -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>