Migrate console off jupiter.nearle.app to the Doormile Express API

Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across
every page (orders, deliveries, riders, tenants, pricing, profile, reports,
dispatch). Fixes several field-mapping and envelope-check bugs found along
the way, most notably that /admin/milers/:id routes (block, assign-vehicle,
edit, notify) key off milerprofileid, not userid, and that a miler's real
fields are phone/availabilitystatus/displayname, not contactno/status/
firstname+lastname (confirmed against a live read-only session).

Also fixes several silent-failure bugs uncovered during that audit: order
creation and order cancellation showed a success toast but gave no feedback
at all on failure (createorder1.js had a dead notifyadmin() call that left
the loading spinner stuck forever on every failed submit), and Tenants.js's
pricing/profile updates never surfaced a failed response to the operator.

The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and
its jupiter.nearle.app delivery-commit call remain untouched by design —
separate solver service with no equivalent in the new API.
This commit is contained in:
2026-08-06 19:26:23 +05:30
parent 0a36212ac8
commit 59f31c8adf
39 changed files with 7006 additions and 9170 deletions

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState, useRef, Fragment } from 'react';
import MainCard from 'components/MainCard';
import axios from 'axios';
import { useTheme } from '@mui/material/styles';
import Loader from 'components/Loader';
import { Empty } from 'antd';
@@ -19,7 +18,6 @@ import {
IconButton,
Box,
Grid,
Autocomplete,
TextField,
Tooltip,
Collapse,
@@ -39,6 +37,8 @@ import {
Skeleton,
Avatar,
Paper,
Select,
MenuItem,
useMediaQuery
} from '@mui/material';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
@@ -65,7 +65,12 @@ import {
MdOutlinePendingActions,
MdOutlineCancel,
MdMyLocation,
MdPersonPin
MdPersonPin,
MdPlace,
MdAdd,
MdEdit,
MdDeleteOutline,
MdPeopleAlt
} from 'react-icons/md';
import { LuMail } from 'react-icons/lu';
import { BiUser } from 'react-icons/bi';
@@ -73,8 +78,21 @@ import LocationAutocomplete from 'components/nearle_components/LocationAutocompl
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getalltenants, gettenantsummary } from 'pages/api/api';
import {
getAdminPricing,
getDoormilePricing,
createAdminPricing,
updateAdminTenant,
getTenantLocations,
createTenantLocation,
updateTenantLocation,
getTenantCustomers,
createTenantCustomer,
updateTenantCustomer,
deleteTenantCustomer
} from 'pages/api/doormileApi';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
// ============================================================================
@@ -103,24 +121,6 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const pillFieldSx = (color) => ({
cursor: 'pointer',
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
bgcolor: '#ffffff',
fontWeight: 600,
color: DT.textPrimary,
paddingRight: '8px',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: color }
});
// Status palette — drives the pill tabs and per-row badges.
const STATUS_META = {
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, statusKey: 'active' },
@@ -168,6 +168,370 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
</Avatar>
);
// ==============================|| Tenant locations (branches/sites) dialog ||============================== //
// Self-contained — deliberately does not touch the existing openRowIndex1/2 +
// Tabs collapse machinery above, which is fragile legacy state. GET/POST/PUT
// /admin/tenants/:id/locations + /admin/tenantlocations/:id per
// jupiter2doormile.md §2.4 — no location UI existed anywhere before this.
const emptyLocationForm = {
locationid: null,
locationname: '',
address: '',
city: '',
state: '',
pincode: '',
isprimary: false,
status: 'active'
};
const TenantLocationsDialog = ({ tenant, open, onClose }) => {
const queryClient = useQueryClient();
const [form, setForm] = useState(emptyLocationForm);
const [formOpen, setFormOpen] = useState(false);
const { data: locations = [], isLoading } = useQuery({
queryKey: ['tenant-locations', tenant?.tenantid],
queryFn: () => getTenantLocations(tenant.tenantid),
enabled: !!tenant?.tenantid && open
});
const saveMutation = useMutation({
mutationFn: (payload) =>
payload.locationid ? updateTenantLocation(payload.locationid, payload.data) : createTenantLocation(tenant.tenantid, payload.data),
onSuccess: (res) => {
if (res.success) {
enqueueSnackbar(form.locationid ? 'Location updated' : 'Location added', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
queryClient.invalidateQueries({ queryKey: ['tenant-locations', tenant?.tenantid] });
setForm(emptyLocationForm);
setFormOpen(false);
} else {
enqueueSnackbar(res.message || 'Failed to save location', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
},
onError: (err) => {
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to save location', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
});
const handleSave = () => {
if (!form.locationname || !form.address) {
enqueueSnackbar('Fill Location Name and Address', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
return;
}
const { locationid, ...data } = form;
saveMutation.mutate({ locationid, data });
};
const openEdit = (loc) => {
setForm({
locationid: loc.locationid,
locationname: loc.locationname || '',
address: loc.address || '',
city: loc.city || '',
state: loc.state || '',
pincode: loc.pincode || '',
isprimary: !!loc.isprimary,
status: loc.status || 'active'
});
setFormOpen(true);
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>
Locations · {tenant?.tenantname}
</DialogTitle>
<DialogContent dividers>
{isLoading ? (
<Typography variant="body2" sx={{ color: DT.textSecondary, py: 2 }}>
Loading locations…
</Typography>
) : locations.length === 0 && !formOpen ? (
<Empty description="No locations added yet" />
) : (
<Stack spacing={1.5}>
{locations.map((loc) => (
<Paper key={loc.locationid} variant="outlined" sx={{ p: 1.5, borderRadius: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Box>
<Typography sx={{ fontWeight: 700 }}>
{loc.locationname} {loc.isprimary && '· Primary'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{[loc.address, loc.city, loc.state, loc.pincode].filter(Boolean).join(', ')}
</Typography>
</Box>
<IconButton size="small" onClick={() => openEdit(loc)}>
<MdEdit size={14} />
</IconButton>
</Stack>
</Paper>
))}
</Stack>
)}
{formOpen && (
<Grid container spacing={1.5} sx={{ mt: 1.5, pt: 1.5, borderTop: `1px solid ${DT.divider}` }}>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Location Name"
value={form.locationname}
onChange={(e) => setForm((f) => ({ ...f, locationname: e.target.value }))}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Pincode" value={form.pincode} onChange={(e) => setForm((f) => ({ ...f, pincode: e.target.value }))} />
</Grid>
<Grid item xs={12}>
<TextField fullWidth label="Address" value={form.address} onChange={(e) => setForm((f) => ({ ...f, address: e.target.value }))} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="City" value={form.city} onChange={(e) => setForm((f) => ({ ...f, city: e.target.value }))} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="State" value={form.state} onChange={(e) => setForm((f) => ({ ...f, state: e.target.value }))} />
</Grid>
</Grid>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
{formOpen ? (
<>
<Button
onClick={() => {
setFormOpen(false);
setForm(emptyLocationForm);
}}
>
Cancel
</Button>
<Button variant="contained" onClick={handleSave}>
{form.locationid ? 'Save Changes' : 'Add Location'}
</Button>
</>
) : (
<>
<Button onClick={onClose}>Close</Button>
<Button variant="contained" startIcon={<MdAdd size={16} />} onClick={() => setFormOpen(true)}>
Add Location
</Button>
</>
)}
</DialogActions>
</Dialog>
);
};
// ==============================|| Tenant customers dialog ||============================== //
// GET /admin/tenantcustomers has no documented tenantid filter (unlike
// locations, which are nested under /admin/tenants/:id/locations) — filtered
// client-side here the same way fetchclientpricelist already filters
// /admin/pricing client-side, since neither endpoint accepts the param.
// createclient.js used to (incorrectly) create tenant customers instead of
// tenants; now that it correctly creates tenants, this dialog is the only
// place left that can create one.
const emptyCustomerForm = { id: null, firstname: '', lastname: '', phone: '', email: '' };
// GET /admin/tenantcustomers doesn't document its id field name either — same
// defensive fallback as customers.js's custId() rather than assuming `id`.
const tenantCustId = (row) => row.id ?? row.tenantcustomerid ?? row.customerid;
const TenantCustomersDialog = ({ tenant, open, onClose }) => {
const queryClient = useQueryClient();
const [form, setForm] = useState(emptyCustomerForm);
const [formOpen, setFormOpen] = useState(false);
const { data: allCustomers = [], isLoading } = useQuery({
queryKey: ['tenant-customers-all'],
queryFn: getTenantCustomers,
enabled: open
});
const customers = (allCustomers || []).filter((c) => !tenant?.tenantid || c.tenantid === tenant.tenantid);
const saveMutation = useMutation({
mutationFn: (payload) => (payload.id ? updateTenantCustomer(payload.id, payload.data) : createTenantCustomer(payload.data)),
onSuccess: (res) => {
if (res.success) {
enqueueSnackbar(form.id ? 'Customer updated' : 'Customer added', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
queryClient.invalidateQueries({ queryKey: ['tenant-customers-all'] });
setForm(emptyCustomerForm);
setFormOpen(false);
} else {
enqueueSnackbar(res.message || 'Failed to save customer', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
},
onError: (err) => {
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to save customer', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
});
const deleteMutation = useMutation({
mutationFn: (id) => deleteTenantCustomer(id),
onSuccess: (res) => {
if (res.success) {
enqueueSnackbar('Customer deleted', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
queryClient.invalidateQueries({ queryKey: ['tenant-customers-all'] });
} else {
enqueueSnackbar(res.message || 'Failed to delete customer', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
},
onError: (err) => {
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to delete customer', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
});
const handleSave = () => {
if (!form.firstname || !form.phone) {
enqueueSnackbar('Fill Name and Phone', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
return;
}
const { id, ...data } = form;
saveMutation.mutate({ id, data });
};
const openEdit = (row) => {
setForm({
id: tenantCustId(row),
firstname: row.firstname || '',
lastname: row.lastname || '',
phone: row.phone || '',
email: row.email || ''
});
setFormOpen(true);
};
const handleDelete = (row) => {
if (window.confirm(`Delete customer "${row.firstname}"?`)) deleteMutation.mutate(tenantCustId(row));
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>Customers · {tenant?.tenantname}</DialogTitle>
<DialogContent dividers>
{isLoading ? (
<Typography variant="body2" sx={{ color: DT.textSecondary, py: 2 }}>
Loading customers…
</Typography>
) : customers.length === 0 && !formOpen ? (
<Empty description="No customers yet" />
) : (
<Stack spacing={1.5}>
{customers.map((c) => (
<Paper key={tenantCustId(c)} variant="outlined" sx={{ p: 1.5, borderRadius: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Box>
<Typography sx={{ fontWeight: 700 }}>
{c.firstname} {c.lastname}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{[c.phone, c.email].filter(Boolean).join(' · ')}
</Typography>
</Box>
<Stack direction="row" spacing={0.5}>
<IconButton size="small" onClick={() => openEdit(c)}>
<MdEdit size={14} />
</IconButton>
<IconButton size="small" onClick={() => handleDelete(c)} sx={{ color: '#ef4444' }}>
<MdDeleteOutline size={14} />
</IconButton>
</Stack>
</Stack>
</Paper>
))}
</Stack>
)}
{formOpen && (
<Grid container spacing={1.5} sx={{ mt: 1.5, pt: 1.5, borderTop: `1px solid ${DT.divider}` }}>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="First Name" value={form.firstname} onChange={(e) => setForm((f) => ({ ...f, firstname: e.target.value }))} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Last Name" value={form.lastname} onChange={(e) => setForm((f) => ({ ...f, lastname: e.target.value }))} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Phone" value={form.phone} onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Email" value={form.email} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
</Grid>
</Grid>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
{formOpen ? (
<>
<Button
onClick={() => {
setFormOpen(false);
setForm(emptyCustomerForm);
}}
>
Cancel
</Button>
<Button variant="contained" onClick={handleSave}>
{form.id ? 'Save Changes' : 'Add Customer'}
</Button>
</>
) : (
<>
<Button onClick={onClose}>Close</Button>
<Button variant="contained" startIcon={<MdAdd size={16} />} onClick={() => setFormOpen(true)}>
Add Customer
</Button>
</>
)}
</DialogActions>
</Dialog>
);
};
// ==============================|| Starts here||============================== //
const Clients1 = () => {
const textFieldRef = useRef(null);
@@ -189,7 +553,7 @@ const Clients1 = () => {
const [status, setstatus] = useState('active');
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
const [dialogopen, setDialogopen] = useState(false);
const [appPricing, setAppPricing] = useState([]);
const [, setAppPricing] = useState([]);
const [selectedPricing, setSelectedPricing] = useState({});
const [isPrice, setIsprice] = useState(true);
const [city, setCity] = useState('');
@@ -197,7 +561,6 @@ const Clients1 = () => {
const [latlong, setLatlong] = useState({});
const [editClient, setEditClient] = useState({});
const [page, setPage] = useState(0);
const [tabStatus, setTabStatus] = useState('Active');
const handleChangePage = (event, newPage) => {
setPage(newPage);
@@ -212,7 +575,6 @@ const Clients1 = () => {
};
const handleChange = (event, newValue) => {
setTabStatus(newValue == 0 ? 'Active' : newValue == 1 ? 'Pending' : 'InActive');
setValue0(newValue);
setSearchword('');
setPage(0);
@@ -228,6 +590,8 @@ const Clients1 = () => {
const [openRowIndex1, setOpenRowIndex1] = useState(null); // Initially no row is open for collapsible section 1
const [openRowIndex2, setOpenRowIndex2] = useState(null); // Initially no row is open for collapsible section 2
const [locationsDialogTenant, setLocationsDialogTenant] = useState(null);
const [customersDialogTenant, setCustomersDialogTenant] = useState(null);
// Handle toggling for collapsible section 1
const handleCollapseToggle1 = (rowIndex) => {
setOpenRowIndex1((prevIndex) => (prevIndex === rowIndex ? null : rowIndex));
@@ -348,18 +712,14 @@ const Clients1 = () => {
/* ============================================= || fetchclientpricelist || ============================================= */
// /admin/pricing has no tenantid query param documented — filter client-side.
const fetchclientpricelist = async () => {
await axios
.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?tenantid=${selectedTenid}`)
.then((res) => {
console.log('getpricinglist', res);
if (res.data.status) {
setClientpricelist(res.data.details);
}
})
.catch((err) => {
console.log(err);
});
try {
const pricing = await getAdminPricing();
setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid));
} catch (err) {
console.log(err);
}
};
useEffect(() => {
selectedTenid && fetchclientpricelist();
@@ -368,9 +728,8 @@ const Clients1 = () => {
/* ============================================= || fetchTenanatPricing || ============================================= */
const fetchTenanatPricing = async (id) => {
try {
let tenantPricing = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
console.log('tenantPricing', tenantPricing.data.details);
setTenanatPricing(tenantPricing.data.details);
const pricing = await getAdminPricing();
setTenanatPricing((pricing || []).filter((p) => p.tenantid === id));
} catch (error) {
console.log('fetchTenanatPricing', error);
}
@@ -382,44 +741,40 @@ const Clients1 = () => {
const tenantupdate = async (tenid) => {
setisloader(true);
let targetId;
let updateData;
if (tenid == -1) {
updateData = {
tenantid: selectedTenid,
approved: 1
};
targetId = selectedTenid;
updateData = { approved: 1 };
} else {
updateData = { tenantid: tenid, status: status === 'active' ? 'InActive' : 'Active' };
targetId = tenid;
updateData = { status: status === 'active' ? 'InActive' : 'Active' };
}
await axios
.put(`${process.env.REACT_APP_URL}/tenants/update`, updateData)
.then((res) => {
if (res.data.status) {
opentoast(
value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully',
'success',
2000
);
getalltenantsRefetch();
summaryDataRefetch();
setisloader(false);
}
})
.catch((err) => {
console.log(err);
opentoast(err.message, 'error', 1500);
setisloader(false);
});
// }
try {
const res = await updateAdminTenant(targetId, updateData);
if (res.success) {
opentoast(
value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully',
'success',
2000
);
getalltenantsRefetch();
summaryDataRefetch();
}
} catch (err) {
console.log(err);
opentoast(err.message, 'error', 1500);
} finally {
setisloader(false);
}
};
/* ============================================= || getAppPricing || ============================================= */
// /admin/doormile-pricing has no zone query param documented — filter client-side.
const getAppPricing = async (id) => {
console.log('id', id);
try {
let appPricingRes = await axios.get(`${process.env.REACT_APP_URL}/utils/getapppricing/?applocationid=${id}`);
console.log('appPricingRes', appPricingRes.data.details);
setAppPricing(appPricingRes.data.details);
const pricing = await getDoormilePricing();
setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id));
} catch (error) {
console.log('appPricingRes', error);
}
@@ -427,55 +782,54 @@ const Clients1 = () => {
/* ============================================= || createpricing || ============================================= */
const createpricing = async () => {
setisloader(true);
await axios
.post(`${process.env.REACT_APP_URL}/tenants/createpricing`, {
tenantpricingid: 0,
try {
const res = await createAdminPricing({
applocationid: appId,
pricingid: selectedPricing.pricingid,
tenantid: selectedTenid,
pricingdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
configid: selectedPricing.configid,
pricingtypeid: selectedPricing.pricingtypeid,
slab: selectedPricing.slab,
baseprice: +selectedPricing.baseprice,
priceperkm: +selectedPricing.priceperkm,
minkm: +selectedPricing.minkm,
maxkm: +selectedPricing.maxkm,
orders: +selectedPricing.minorder,
othercharges: 0
})
.then((res) => {
if (res.data.status) {
enqueueSnackbar('Price Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
dialogclose();
setSelectedPricing({});
tenantupdate(-1);
fetchclientpricelist();
}
})
.catch((err) => {
console.log(err);
vehicletype: selectedPricing.vehicletype || 'Bike',
baseprice: +selectedPricing.baseprice || 0,
baseweight: +selectedPricing.baseweight || 0,
priceperkm: +selectedPricing.priceperkm || 0,
priceperkg: +selectedPricing.priceperkg || 0,
basedistance: +selectedPricing.basedistance || 0,
handlingcharges: +selectedPricing.handlingcharges || 0,
effectivefrom: dayjs().format('YYYY-MM-DD'),
currency: 'INR',
priority: 1,
status: 'active'
});
if (res.success) {
enqueueSnackbar('Price Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
dialogclose();
setSelectedPricing({});
tenantupdate(-1);
fetchclientpricelist();
} else {
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);
}
};
/* ============================================= || updateClient || ============================================= */
const updateClient = async () => {
try {
const arr = { ...editClient, tenantid: selectedTenid };
console.log('updateClient', arr);
const updateRes = await axios.put(`${process.env.REACT_APP_URL}/tenants/update`, arr);
console.log('updateClient', updateRes.data.message);
if (updateRes.data.status) {
opentoast(updateRes.data.message, 'success', 1500);
const updateRes = await updateAdminTenant(selectedTenid, editClient);
if (updateRes.success) {
opentoast(updateRes.message || 'Updated Successfully', 'success', 1500);
} else {
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);
}
};
@@ -912,6 +1266,34 @@ const Clients1 = () => {
{openRowIndex1 === index ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
</IconButton>
</Tooltip>
<Tooltip title="Locations" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft('#0ea5e9'),
color: '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => setLocationsDialogTenant(row)}
>
<MdPlace size={14} />
</IconButton>
</Tooltip>
<Tooltip title="Customers" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft('#14b8a6'),
color: '#14b8a6',
border: `1px solid ${edge('#14b8a6')}`,
'&:hover': { bgcolor: '#14b8a6', color: '#fff' }
}}
onClick={() => setCustomersDialogTenant(row)}
>
<MdPeopleAlt size={14} />
</IconButton>
</Tooltip>
{value0 !== 1 && (
<Tooltip title={openRowIndex2 === index ? 'Close' : 'Edit'} placement="top">
<IconButton
@@ -1208,12 +1590,12 @@ const Clients1 = () => {
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>Date</TableCell>
<TableCell>Slab</TableCell>
<TableCell>Vehicle</TableCell>
<TableCell>Base Price</TableCell>
<TableCell>Min Kms</TableCell>
<TableCell>Price/Km</TableCell>
<TableCell>Other Charges</TableCell>
<TableCell>Price/Kg</TableCell>
<TableCell>Handling</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -1225,14 +1607,14 @@ const Clients1 = () => {
</TableRow>
) : (
clientpricelist.map((val, i) => (
<TableRow key={val.id || i} sx={{ bgcolor: 'white' }}>
<TableRow key={val.pricingid || i} sx={{ bgcolor: 'white' }}>
<TableCell>{i + 1}</TableCell>
<TableCell>{dayjs(val.pricingdate).format('DD-MM-YYYY')}</TableCell>
<TableCell>{val.slab}</TableCell>
<TableCell>{val.baseprice}</TableCell>
<TableCell>{val.minkm}</TableCell>
<TableCell>{val.priceperkm}</TableCell>
<TableCell>{val.othercharges}</TableCell>
<TableCell>{val.vehicletype || '—'}</TableCell>
<TableCell>{val.baseprice ?? '—'}</TableCell>
<TableCell>{val.priceperkm ?? '—'}</TableCell>
<TableCell>{val.priceperkg ?? '—'}</TableCell>
<TableCell>{val.handlingcharges ?? '—'}</TableCell>
<TableCell>{val.status || '—'}</TableCell>
</TableRow>
))
)}
@@ -1667,12 +2049,12 @@ const Clients1 = () => {
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>Date</TableCell>
<TableCell>Slab</TableCell>
<TableCell>Vehicle</TableCell>
<TableCell>Base Price</TableCell>
<TableCell>Min Kms</TableCell>
<TableCell>Price/Km</TableCell>
<TableCell>Other Charges</TableCell>
<TableCell>Price/Kg</TableCell>
<TableCell>Handling</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -1684,14 +2066,14 @@ const Clients1 = () => {
</TableRow>
) : (
clientpricelist.map((val, i) => (
<TableRow key={val.id || i} sx={{ bgcolor: 'white' }}>
<TableRow key={val.pricingid || i} sx={{ bgcolor: 'white' }}>
<TableCell>{i + 1}</TableCell>
<TableCell>{dayjs(val.pricingdate).format('DD-MM-YYYY')}</TableCell>
<TableCell>{val.slab}</TableCell>
<TableCell>{val.baseprice}</TableCell>
<TableCell>{val.minkm}</TableCell>
<TableCell>{val.priceperkm}</TableCell>
<TableCell>{val.othercharges}</TableCell>
<TableCell>{val.vehicletype || '—'}</TableCell>
<TableCell>{val.baseprice ?? '—'}</TableCell>
<TableCell>{val.priceperkm ?? '—'}</TableCell>
<TableCell>{val.priceperkg ?? '—'}</TableCell>
<TableCell>{val.handlingcharges ?? '—'}</TableCell>
<TableCell>{val.status || '—'}</TableCell>
</TableRow>
))
)}
@@ -1861,6 +2243,30 @@ const Clients1 = () => {
>
{expanded ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
</IconButton>
<IconButton
size="small"
sx={{
bgcolor: soft('#0ea5e9'),
color: '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => setLocationsDialogTenant(row)}
>
<MdPlace size={14} />
</IconButton>
<IconButton
size="small"
sx={{
bgcolor: soft('#14b8a6'),
color: '#14b8a6',
border: `1px solid ${edge('#14b8a6')}`,
'&:hover': { bgcolor: '#14b8a6', color: '#fff' }
}}
onClick={() => setCustomersDialogTenant(row)}
>
<MdPeopleAlt size={14} />
</IconButton>
{value0 !== 1 && (
<IconButton
size="small"
@@ -1955,24 +2361,24 @@ const Clients1 = () => {
<Grid container spacing={2.5} alignItems={'center'}>
<Grid item xs={12} sm={6}>
<FormLabel>Select Slab</FormLabel>
<Autocomplete
disablePortal
id="combo-box-demo"
options={appPricing}
getOptionLabel={(option) => `${option.slab}`}
<Select
fullWidth
selectOnFocus
renderInput={(params) => <TextField {...params} label="Slab" />}
onChange={(event, value, reason) => {
setSelectedPricing(value);
console.log('pricing', value);
displayEmpty
value={selectedPricing.vehicletype || ''}
onChange={(e) => {
setSelectedPricing({ ...selectedPricing, vehicletype: e.target.value });
setIsprice(false);
if (reason === 'clear') {
setIsprice(true);
}
}}
/>
>
<MenuItem value="" disabled>
Choose vehicle type
</MenuItem>
{['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => (
<MenuItem key={v} value={v}>
{v}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
@@ -2005,43 +2411,57 @@ const Clients1 = () => {
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Min Kms</FormLabel>
<FormLabel>Base Weight (kg)</FormLabel>
<TextField
type="number"
fullWidth
value={!isPrice ? selectedPricing.minkm : 0}
value={!isPrice ? selectedPricing.baseweight : 0}
onChange={(e) => {
setSelectedPricing({
...selectedPricing,
minkm: e.target.value
baseweight: e.target.value
});
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Max Kms</FormLabel>
<FormLabel>Price/Kg</FormLabel>
<TextField
type="number"
fullWidth
value={!isPrice ? selectedPricing.maxkm : 0}
value={!isPrice ? selectedPricing.priceperkg : 0}
onChange={(e) => {
setSelectedPricing({
...selectedPricing,
maxkm: e.target.value
priceperkg: e.target.value
});
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Min Orders</FormLabel>
<FormLabel>Base Distance (km)</FormLabel>
<TextField
type="number"
fullWidth
value={!isPrice ? selectedPricing.minorder : 0}
value={!isPrice ? selectedPricing.basedistance : 0}
onChange={(e) => {
setSelectedPricing({
...selectedPricing,
minorder: e.target.value
basedistance: e.target.value
});
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormLabel>Handling Charges</FormLabel>
<TextField
type="number"
fullWidth
value={!isPrice ? selectedPricing.handlingcharges : 0}
onChange={(e) => {
setSelectedPricing({
...selectedPricing,
handlingcharges: e.target.value
});
}}
/>
@@ -2093,6 +2513,18 @@ const Clients1 = () => {
</Grid>
</DialogActions>
</Dialog>
<TenantLocationsDialog
tenant={locationsDialogTenant}
open={!!locationsDialogTenant}
onClose={() => setLocationsDialogTenant(null)}
/>
<TenantCustomersDialog
tenant={customersDialogTenant}
open={!!customersDialogTenant}
onClose={() => setCustomersDialogTenant(null)}
/>
</>
);
};