initialisation on the doormile express console with astryx

This commit is contained in:
2026-07-29 18:16:25 +05:30
parent d5bde7d1ac
commit 7d672139b5
108 changed files with 12536 additions and 21351 deletions

View File

@@ -233,7 +233,7 @@ export const notifyRider = async (riderToken) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: riderToken,
notification: {
title: 'NearleXpress',
title: 'DoormileXpress',
body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
sound: 'ring',
image: ''
@@ -271,12 +271,9 @@ export const cancelMultipleOrder = async (orderlist) => {
// ==============================|| fetchDeliveries (deliveries) ||============================== //
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
let [, appId, , currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
const url =
appId === 0
? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
: `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const url = `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const response = await axios.get(url);
return {
@@ -311,10 +308,7 @@ export const fetchPercentageAPI = async (appId) => {
// ==============================|| fetchCountAPI (deliveries) ||============================== //
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
const url =
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const response = await axios.get(url);
const data = response.data.details;
return {
@@ -369,103 +363,6 @@ export const updateDeliveryAPI = async (orderData) => {
return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, orderData);
};
// ==============================|| getalltenants (tenants) ||============================== //
export const getalltenants = async ({ queryKey }) => {
const [, appId, debouncedSearch, status, page, rowsPerPage] = queryKey;
try {
let url = `${process.env.REACT_APP_URL
}/tenants/getalltenants/?status=${status}&applocationid=${appId}&keyword=${debouncedSearch}&pageno=${page + 1
}&pagesize=${rowsPerPage}&moduleid=6`;
const response = await axios.get(url);
return response.data.details; // return only data, keep it clean
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| gettenantsummary (tenants) ||============================== //
export const gettenantsummary = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| getpricinglist (tenants) ||============================== //
export const getpricinglist = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| getallpricing (clientPricing) ||============================== //
export const getallpricing = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
return response.data.details || [];
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return [];
}
};
// ==============================|| getcustomersummary (customers) ||============================== //
export const getcustomersummary = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getcustomersummary?applocationid=${appId}`);
return response.data.summary;
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| getallcustomers (customers) ||============================== //
export const getallcustomers = async ({ pageParam = 1, queryKey }) => {
const [, appId, debouncedSearch, rowsPerPage] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getallcustomers/`, {
params: {
applocationid: appId,
keyword: debouncedSearch,
pageno: pageParam,
pagesize: rowsPerPage
}
});
return {
data: response.data.details || [],
nextPage: response.data.details?.length === rowsPerPage ? pageParam + 1 : undefined
};
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
throw err; // IMPORTANT for React Query
}
};
// ==============================|| fetchAllRiders (riders) ||============================== //
export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
try {
@@ -554,10 +451,7 @@ export const fetchorderdetails = async ({ queryKey }) => {
const [appId, startdate, enddate, page, rowsPerPage] = queryKey;
const response = await axios.get(
appId == 0
? `${process.env.REACT_APP_URL2}/orders/getorders/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}&pageno=${page + 1
}&pagesize=${rowsPerPage}`
: `${process.env.REACT_APP_URL2}/orders/getorders/?fromdate=${startdate}&todate=${enddate}&applocationid=${appId}&pageno=${page}&pagesize=${rowsPerPage}`
`${process.env.REACT_APP_URL2}/orders/getorders/?fromdate=${startdate}&todate=${enddate}&applocationid=${appId}&pageno=${page}&pagesize=${rowsPerPage}`
);
const detailsWithSNo = response.data.details.map((item, index) => ({
...item,
@@ -621,21 +515,6 @@ export const fetchLocations = async () => {
return updatedLocations;
};
// ==============================|| fetchinvoiceinsight (Invoice)||============================== //
export const fetchinvoiceinsight = async () => {
const insightResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getinvoiceinsight`);
return insightResponse.data.details;
};
// ==============================|| fetchdeliverylist (Invoice)||============================== //
export const fetchdeliverylist = async ({ queryKey }) => {
const [billStatus] = queryKey;
const deliveyResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}`);
console.log('fetchdeliverylist', deliveyResponse.data.details);
return deliveyResponse.data.details;
};
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
export const fetchRidersLogs = async ({ queryKey }) => {

View File

@@ -1,29 +0,0 @@
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import AuthWrapper from 'sections/auth/AuthWrapper';
import AuthCodeVerification from 'sections/auth/auth-forms/AuthCodeVerification';
// ================================|| CODE VERIFICATION ||================================ //
const CodeVerification = () => (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<Typography variant="h3">Enter Verification Code</Typography>
<Typography color="secondary">We send you on mail.</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<Typography>We`ve send you code on jone. ****@company.com</Typography>
</Grid>
<Grid item xs={12}>
<AuthCodeVerification />
</Grid>
</Grid>
</AuthWrapper>
);
export default CodeVerification;

View File

@@ -1,602 +0,0 @@
import React, { useMemo, useRef, useState } from 'react';
import {
Avatar,
Box,
Grid,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdLocalOffer,
MdMyLocation,
MdAttachMoney,
MdGroups,
MdPlace,
MdSpeed,
MdPriceCheck,
MdStraighten,
MdReceiptLong,
MdOutlineLocalOffer,
MdOutlineGroups,
MdOutlineAttachMoney,
MdOutlinePlace
} from 'react-icons/md';
import { useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { getallpricing } from 'pages/api/api';
// ============================================================================
// Design tokens — shared with the deliveries / tenants / customers pages so every
// surface (header, KPI tiles, table, badges) speaks the same visual language.
// Keep this block in sync with customers.js / deliveries.js.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 14,
radiusField: 10,
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
borderHover: '#cbd5e1',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
const formatRupees = (value) =>
new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
const formatDecimal = (value) =>
new Intl.NumberFormat('en-IN', { minimumFractionDigits: 2 }).format(Number(value) || 0);
// Numeric table value — plain, strong, right-readable text. The old version
// wrapped every cell in a coloured bordered pill, which made the table read
// like a rainbow; corporate data tables keep figures as quiet typography and
// let the column header carry the meaning.
const MetricPill = ({ label }) => (
<Typography
component="span"
sx={{ fontSize: 13.5, fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}
>
{label}
</Typography>
);
// Subtle neutral category chip (zone / slab) — one quiet style, muted icon.
const CategoryChip = ({ icon, label }) => (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 8,
bgcolor: DT.surfaceAlt,
border: `1px solid ${DT.borderSubtle}`,
color: DT.textPrimary,
fontSize: 12,
fontWeight: 600,
whiteSpace: 'nowrap'
}}
>
<Box component="span" sx={{ display: 'inline-flex', color: DT.textMuted }}>
{icon}
</Box>
{label}
</Box>
);
// ==============================|| Pricing page ||============================== //
const ClientsPricing = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const containerRef = useRef();
const [appId, setAppId] = useState(0);
const [locaName, setLocoName] = useState('All');
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const {
data: pricing = [],
isLoading
} = useQuery({
queryKey: ['getallpricing', appId],
queryFn: getallpricing,
keepPreviousData: true
});
const rows = useMemo(() => {
if (!debouncedSearch) return pricing;
const q = debouncedSearch.toLowerCase().trim();
return pricing.filter((row) =>
[row.applocation, row.appname, row.slab, String(row.pricingid)]
.filter(Boolean)
.some((field) => String(field).toLowerCase().includes(q))
);
}, [pricing, debouncedSearch]);
const stats = useMemo(() => {
const total = pricing.length;
const tenants = new Set(pricing.map((r) => r.appname).filter(Boolean)).size;
const avgBase = total
? pricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total
: 0;
return { total, tenants, avgBase };
}, [pricing]);
const KPI_META = [
{ key: 'total', label: 'Total Pricing Slabs', color: BRAND, icon: MdOutlineLocalOffer, value: stats.total },
{ key: 'tenants', label: 'Tenants Priced', color: '#0ea5e9', icon: MdOutlineGroups, value: stats.tenants },
{ key: 'avg', label: 'Avg Base Price', color: '#f59e0b', icon: MdOutlineAttachMoney, value: formatRupees(stats.avgBase) },
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdOutlinePlace, value: locaName || 'All Zones' }
];
return (
<>
{isLoading && <Loader />}
{/* ============================================= || Header || ============================================= */}
<PageHeader
title="Pricing"
subtitle={`Live · ${locaName || 'All Zones'}`}
live
action={
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* ============================================= || KPI Cards || ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={6} md={3}>
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} />
</Grid>
);
})}
</Grid>
{/* ============================================= || Search Header || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent="space-between"
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<AccentAvatar color={BRAND} size={32}>
<MdLocalOffer size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Pricing Catalog
</Typography>
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
{pricing.length} total · {rows.length} shown
</Typography>
</Stack>
</Stack>
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder={`Search pricing (ctrl+k)`}
sx={{
m: 0,
width: '100%',
borderRadius: DT.radiusField + 'px',
bgcolor: DT.surface,
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
'&:hover fieldset': { borderColor: DT.borderHover },
'&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* ============================================= || Table || ============================================= */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{isMobile ? (
rows.length === 0 && !isLoading ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdLocalOffer size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No pricing to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'}
</Typography>
</Stack>
) : (
<MobileCardList scroll>
{rows.map((row, index) => (
<MobileCard
key={row.pricingid || `${row.appname}-${index}`}
accent={BRAND}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary }}
noWrap
>
{row.appname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.pricingid}
</Typography>
</Stack>
</Stack>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted, flexShrink: 0 }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</Stack>
}
>
<Stack direction="row" spacing={0.75} sx={{ mt: 1, flexWrap: 'wrap', gap: 0.75 }}>
{row.applocation ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdPlace size={12} /> {row.applocation}
</Box>
) : null}
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdSpeed size={12} /> {row.slab || '—'}
</Box>
</Stack>
<MobileFieldGrid>
<MobileField label="Base Price">
<MetricPill
color={BRAND}
icon={<MdPriceCheck size={12} />}
label={formatRupees(row.baseprice)}
/>
</MobileField>
<MobileField label="Price / KM">
<MetricPill
color="#10b981"
icon={<MdAttachMoney size={12} />}
label={formatRupees(row.priceperkm)}
/>
</MobileField>
<MobileField label="Min KM">
<MetricPill
color="#f59e0b"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.minkm)} km`}
/>
</MobileField>
<MobileField label="Max KM">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.maxkm)} km`}
/>
</MobileField>
<MobileField label="Min Orders">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdReceiptLong size={14} color={DT.textMuted} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.minorder ?? '—'}
</Typography>
</Stack>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))}
</MobileCardList>
)
) : (
<TableContainer
ref={containerRef}
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: { xs: 860, md: 1080 } }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: { xs: 10, md: 11 },
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: { xs: 1, md: 1.25 },
px: { xs: 1, md: 2 }
}
}}
>
<TableCell>#</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Zone</TableCell>
<TableCell>Slab</TableCell>
<TableCell align="center">Base Price</TableCell>
<TableCell align="center">Min KM</TableCell>
<TableCell align="center">Price / KM</TableCell>
<TableCell align="center">Max KM</TableCell>
<TableCell align="center">Min Orders</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && <OrdersTableSkeleton col={5} />}
{rows.length === 0 && !isLoading ? (
<TableRow>
<TableCell colSpan={9} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdLocalOffer size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No pricing to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'}
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
rows.map((row, index) => (
<TableRow
key={row.pricingid || `${row.appname}-${index}`}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 }
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.appname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.pricingid}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell>
{row.applocation ? (
<CategoryChip icon={<MdPlace size={12} />} label={row.applocation} />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
<TableCell>
<CategoryChip icon={<MdSpeed size={12} />} label={row.slab || '—'} />
</TableCell>
<TableCell align="center">
<MetricPill
color={BRAND}
icon={<MdPriceCheck size={12} />}
label={formatRupees(row.baseprice)}
width={110}
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#f59e0b"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.minkm)} km`}
width={90}
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#10b981"
icon={<MdAttachMoney size={12} />}
label={formatRupees(row.priceperkm)}
width={110}
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.maxkm)} km`}
width={90}
/>
</TableCell>
<TableCell align="center">
<Stack direction="row" alignItems="center" justifyContent="center" spacing={0.5}>
<MdReceiptLong size={14} color={DT.textMuted} />
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.minorder ?? '—'}
</Typography>
</Stack>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
</>
);
};
export default ClientsPricing;

File diff suppressed because it is too large Load Diff

View File

@@ -1,545 +0,0 @@
import { React, useEffect, useState, useRef } from 'react';
import { useTheme } from '@mui/material/styles';
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, IconButton, Autocomplete, useMediaQuery } from '@mui/material';
import MainCard from 'components/MainCard';
import axios from 'axios';
import Loader from 'components/Loader';
import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import CloseIcon from '@mui/icons-material/Close';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import { OpenToast } from 'components/third-party/OpenToast';
const CreateCustomer = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [appId, setAppId] = useState(0);
const locationRef = useRef(null);
const [mobilenumber, setMobilenumber] = useState('');
const [emailaddress, setEmailaddress] = useState('');
const [address, setAddress] = useState('');
const [firstname, setFirstname] = useState('');
const [doorno, setDoorno] = useState('');
const [landmark, setLandmark] = useState('');
const [inputValue2, setInputValue2] = useState('');
const [appLocaLat, setAppLocaLat] = useState();
const [appLocaLng, setAppLocaLng] = useState();
const [appLocaRadius, setAppLocaRadius] = useState();
const [locaName, setLocoName] = useState('Select Location');
const [tenantlist, setTenantlist] = useState([]);
const [tid, setTid] = useState(0);
const [pickCust, setPickCust] = useState({});
const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 });
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
useEffect(() => {
// Initialize Google Maps Autocomplete
if (inputValue2) {
const autocompleteInput = document.getElementById('addressAuto1');
const autocomplete = new window.google.maps.places.Autocomplete(autocompleteInput, {
// types: ['(cities)'], // You can adjust the types parameter based on your requirements
strictBounds: true,
bounds: new window.google.maps.Circle({
// center: new window.google.maps.LatLng(11.0050707, 76.9509083),
// radius: 100000
center: new window.google.maps.LatLng(appLocaLat, appLocaLng),
radius: appLocaRadius * 1000
}).getBounds()
});
// Event listener for autocomplete place changed
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
setInputValue2(`${place.name}, ${place.formatted_address}`);
console.log('new place', place); // Do something with the selected place
console.log(' pick (new place) lat lng', { lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }); // Do something with the selected place
// to trigger getDistance
setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() });
setAddress(`${place.name} ${place.formatted_address}`);
setPickCust({ ...pickCust, address: `${place.name} ${place.formatted_address}` });
const address = {
address: `${place.name} ${place.formatted_address}`,
street_number: '',
route: '',
locality: '',
sublocality_level_1: '',
administrative_area_level_3: '',
administrative_area_level_1: '',
country: '',
postal_code: ''
};
place.address_components.forEach((component) => {
component.types.forEach((type) => {
switch (type) {
case 'street_number':
address.street_number = component.long_name;
break;
case 'route':
address.route = component.long_name;
break;
case 'locality':
address.locality = component.long_name;
break;
case 'sublocality_level_1':
address.sublocality_level_1 = component.long_name;
break;
case 'administrative_area_level_3':
address.administrative_area_level_3 = component.long_name;
break;
case 'administrative_area_level_1':
address.administrative_area_level_1 = component.long_name;
break;
case 'country':
address.country = component.long_name;
break;
case 'postal_code':
address.postal_code = component.long_name;
break;
// Add more cases as needed for other types
}
});
});
// Use address object as per your requirements
setPickCust({
...pickCust,
address: address.address,
doorno: `${address.street_number} ${address.route}`,
suburb: address.administrative_area_level_3,
city: address.locality,
state: address.administrative_area_level_1,
postcode: address.postal_code,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
});
console.log('Pick Address:', address);
});
}
}, [inputValue2]);
// ==================================================== || getapplocations || ====================================================
const getapplocations = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
.then((res) => {
console.log('getapplocations', res);
const { latitude, longitude, radius } = res.data.details[0];
if (res.data.status) {
setAppLocaLat(latitude);
setAppLocaLng(longitude);
setAppLocaRadius(radius);
console.log('radius', radius);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (appId) {
getapplocations();
}
}, [appId]);
// ===================================================== || fetchtenantinfolist || =====================================================
const fetchtenantinfolist = async (id) => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${id}&status=active`)
.then((res) => {
console.log(res);
if (res.data.status) {
let arr = [];
res.data.details.map((val) => {
arr.push({
...val,
label: `${val.tenantname}`
});
});
setTenantlist(arr);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
appId && fetchtenantinfolist(appId);
}, [appId]);
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
console.log('gettenantlocations', res.data.details);
if (res.data.details.length == 1) {
} else {
}
} catch (err) {
console.log('gettenantlocations', err);
}
};
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
};
const createprofile = async () => {
let obj = {
applocationid: +appId,
tenantid: +tid,
customerid: 0,
configid: 1,
firstname: firstname,
dialcode: '+91',
contactno: mobilenumber,
email: emailaddress,
doorno: doorno,
address: pickCust.address,
suburb: pickCust.suburb,
city: pickCust.city,
state: pickCust.state,
postcode: pickCust.postcode,
landmark: landmark,
latitude: startPoint.latitude.toString(),
longitude: startPoint.longitude.toString(),
profileimage: '',
devicetype: '',
deviceid: '',
customertoken: '',
primaryaddress: 1
};
console.log(obj);
setLoading(true);
try {
await axios
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
.then((res) => {
console.log(res);
if (res.data.status) {
enqueueSnackbar(' Created Successfully ', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
navigate('/nearle/customers');
} else if (res.data.message == 'Customer Already available') {
enqueueSnackbar('Customer Already available', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
});
} catch (err) {
console.log(err);
setLoading(false);
}
};
return (
<>
{loading && <Loader />}
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h3">Create Customer</Typography>
</Stack>
</Grid>
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={{ xs: 2, md: 3 }}>
<Grid item xs={12}>
<Grid container spacing={{ xs: 2, md: 3 }}>
{/* ===================================================== || Choose location || ===================================================== */}
<Grid item xs={12} md={6}>
<LocationAutocomplete ref={locationRef} locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} sx={{}} />
</Grid>
{/* ===================================================== || Choose client || ===================================================== */}
<Grid item xs={12} md={6}>
<Autocomplete
fullWidth
disabled={appId == 0}
id="free-solo-demo"
sx={{}}
options={tenantlist || []}
renderInput={(params) => <TextField {...params} label="Choose Client" focused />}
onChange={(e, val, reason) => {
if (val) {
console.log('Client', val);
gettenantlocations(val.tenantid);
setTid(val.tenantid);
} else {
setClientinfo({});
setTenantid('');
}
if (reason == 'clear') {
}
}}
/>{' '}
</Grid>
{/* ===================================================== || Name|| ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-last-name">Name</InputLabel>
<TextField
fullWidth
id="personal-last-name"
placeholder="Name"
onChange={(e) => setFirstname(e.target.value)}
value={firstname}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || Phone Number || ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-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="personal-phone"
fullWidth
placeholder="Phone Number"
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setMobilenumber(e.target.value);
}
}}
value={mobilenumber}
autoComplete="off"
// disabled
sx={{ cursor: 'not-allowed' }}
/>
</Stack>
</Stack>
</Grid>
{/* ===================================================== || Email|| ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Email </InputLabel>
<TextField
type="email"
fullWidth
id="personal-email"
placeholder="Email "
onChange={(e) => setEmailaddress(e.target.value)}
value={emailaddress}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || door no || ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Door No</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Door No"
onChange={(e) => setDoorno(e.target.value)}
value={doorno}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || Address || ===================================================== */}
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email"> Address</InputLabel>
<TextField
variant="outlined"
id="addressAuto1"
fullWidth
value={inputValue2}
onChange={(e) => {
if (appId) {
appId && setInputValue2(e.target.value);
} else {
OpenToast('Select Location First', 'warning', 3000);
}
}}
InputProps={{
endAdornment: (
<IconButton
onClick={() => {
setInputValue2('');
setPickCust({
...pickCust,
doorno: '',
suburb: '',
city: '',
postcode: '',
landmark: ''
});
setStartPoint({ latitude: 0, longitude: 0 });
}}
size="small"
>
<CloseIcon />
</IconButton>
)
}}
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Location</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Location"
onChange={(e) => setPickCust({ ...pickCust, suburb: e.target.value })}
value={pickCust.suburb}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
<TextField
fullWidth
id="personal-zipcode"
placeholder="City"
onChange={(e) => setPickCust({ ...pickCust, city: e.target.value })}
value={pickCust.city}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">State</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="State"
onChange={(e) => setPickCust({ ...pickCust, state: e.target.value })}
value={pickCust.state}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
<TextField
fullWidth
// defaultValue="956754"
type="number"
id="personal-zipcode"
placeholder="Zipcode"
onChange={(e) => setPickCust({ ...pickCust, postcode: e.target.value })}
value={pickCust.postcode}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
placeholder="Landmark"
onChange={(e) => setLandmark(e.target.value)}
value={landmark}
autoComplete="off"
/>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
if (appId === '') {
opentoast('Select Applocation ');
} else if (tid === '') {
opentoast('Select Tenant');
} else if (firstname === '') {
opentoast('Enter Name');
} else if (mobilenumber === '') {
opentoast('Enter Mobile Number ');
} else if (address === '') {
opentoast('Enter Address ');
} else if (pickCust.city === '') {
opentoast('Enter City ');
} else if (pickCust.state === '') {
opentoast('Enter State ');
} else if (pickCust.suburb === '') {
opentoast('Enter location ');
} else if (pickCust.postcode === '') {
opentoast('Enter Post Code ');
} else if (landmark === '') {
opentoast('Enter Land Mark ');
} else if (pickCust.latitude === '') {
opentoast('Invalid latitude ');
} else if (pickCust.longitude === '') {
opentoast('Invaiid Longitude ');
} else {
createprofile();
}
}}
>
Create
</Button>
</Stack>
</Grid>
</Grid>
</MainCard>
</>
);
};
export default CreateCustomer;

View File

@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
import { Avatar, Box, Button, FormLabel, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
import { MdPersonAddAlt1 } from 'react-icons/md';
// third-party
// import { PatternFormat } from 'react-number-format';
@@ -15,6 +16,7 @@ import Loader from 'components/Loader';
import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import { DT, tint } from 'themes/dt/tokens';
// import { setLocationType } from 'react-geocode';
// const avatarImage = require.context('assets/images/users', true);
@@ -145,12 +147,6 @@ const Createclient = () => {
});
};
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => {
@@ -296,11 +292,28 @@ const Createclient = () => {
{loading && <Loader />}
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h3">Create Client</Typography>
</Stack>
<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 Client</Typography>
</Stack>
</Paper>
</Grid>
<MainCard contentSX={{ p: { xs: 1.5, md: 3 } }}>
<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} sm={4} >
<MainCard title="Personal Information" sx={{ height: '100%' }}>

File diff suppressed because it is too large Load Diff

View File

@@ -78,7 +78,7 @@ import {
startOfMonth,
startOfWeek
} from 'date-fns';
import { DateRangePicker } from 'mui-daterange-picker';
import { DateRangePicker } from 'components/nearle_components/DateRangePicker';
import * as React from 'react';
import Loader from 'components/Loader';
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
@@ -105,62 +105,11 @@ import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — extracted from the polished "Batch" dropdown so every
// surface on this page (filters, KPIs, tabs, status badges, dialogs) shares
// the same visual language. All helpers take a color and emit MUI sx values.
// Design tokens — shared across every DT-styled operator page so filters,
// KPIs, tabs, status badges, and dialogs stay visually consistent.
// See src/themes/dt/tokens.js for the canonical source.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 14,
radiusInner: 10,
radiusField: 10,
// Restrained, low-contrast elevation — corporate (Linear/Stripe), not flashy.
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
borderHover: '#cbd5e1',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
};
// Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown
// pattern (`${color}08`, `${color}18`, `${color}55`, `${color}26`).
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08'); // very subtle surface tint
const soft = (c) => a(c, '18'); // soft chip / avatar bg
const ring = (c) => a(c, '26'); // focus ring color
const edge = (c) => a(c, '55'); // resting border
// Pill input sx — used by every filter Autocomplete/TextField on the page.
// Accepts the accent color and returns sx for the outer TextField. Width is
// driven by parent flex/grid so this helper stays width-agnostic.
// Neutral, corporate filter field: white surface, hairline border, brand focus
// ring. Colour is no longer used to tint the whole control (that produced the
// "rainbow" filter bar) — accent now lives only in the small start-adornment
// icon, which aids scanning without flooding the surface.
const pillFieldSx = () => ({
cursor: 'pointer',
'& .MuiOutlinedInput-root': {
borderRadius: DT.radiusField + 'px',
bgcolor: DT.surface,
fontWeight: 600,
color: DT.textPrimary,
paddingRight: '8px',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
'&:hover fieldset': { borderColor: DT.borderHover },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(DT.brand)}` },
'&.Mui-focused fieldset': { borderColor: DT.brand, borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
});
import { DT, a, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens';
// Status palette — drives tab pills, row status badges, dialogs.
const STATUS_META = {
@@ -413,7 +362,7 @@ const Deliveries = () => {
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: selectedRow.userfcmtoken,
notification: {
title: 'NearleXpress',
title: 'DoormileXpress',
body: `${selectedRow.orderid} have been Cancelled`,
sound: 'ring',
image: ''
@@ -912,7 +861,7 @@ const Deliveries = () => {
setLocoName={setLocoName}
setPage={setPage}
pill
accentColor="#662582"
accentColor="#C01227"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
@@ -962,7 +911,7 @@ const Deliveries = () => {
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: { xs: 1, md: 1.5 }, color: DT.textSecondary }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#662582'), color: '#662582' }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#C01227'), color: '#C01227' }}>
<MdTune size={16} />
</Avatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
@@ -1125,8 +1074,8 @@ const Deliveries = () => {
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
}}
@@ -1344,15 +1293,15 @@ const Deliveries = () => {
flexShrink: 0,
cursor: 'pointer',
borderRadius: DT.radiusField + 'px',
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface,
border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
bgcolor: active ? '#C01227' : DT.surface,
color: active ? '#fff' : DT.textSecondary,
fontWeight: 600,
boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': {
borderColor: active ? meta.color : DT.borderHover,
bgcolor: active ? meta.color : DT.surfaceAlt
borderColor: active ? '#C01227' : DT.borderHover,
bgcolor: active ? '#C01227' : DT.surfaceAlt
}
}}
>
@@ -1442,10 +1391,10 @@ const Deliveries = () => {
overflowX: 'auto',
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'),
backgroundColor: edge('#C01227'),
borderRadius: '8px',
cursor: 'pointer',
'&:hover': { backgroundColor: '#662582' }
'&:hover': { backgroundColor: '#C01227' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
@@ -1562,7 +1511,7 @@ const Deliveries = () => {
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, row)}
sx={{ borderRadius: 999, bgcolor: tint('#662582'), color: '#662582', border: `1px solid ${edge('#662582')}`, '&:hover': { bgcolor: soft('#662582') } }}
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
>
<EditOutlined />
</IconButton>
@@ -1650,7 +1599,7 @@ const Deliveries = () => {
</MobileField>
<MobileField label="Step">
{row.step ? (
<Box sx={{ ...chipSx('#662582'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
<Box sx={{ ...chipSx('#C01227'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
@@ -1664,8 +1613,8 @@ const Deliveries = () => {
{isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color="#662582" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
@@ -2065,9 +2014,9 @@ const Deliveries = () => {
height: 24,
px: 0.875,
borderRadius: 999,
bgcolor: tint('#662582'),
border: `1px solid ${edge('#662582')}`,
color: '#662582',
bgcolor: tint('#C01227'),
border: `1px solid ${edge('#C01227')}`,
color: '#C01227',
fontWeight: 800,
fontSize: 11
}}
@@ -2133,10 +2082,10 @@ const Deliveries = () => {
onClick={(e) => handleMenuOpen(e, row)}
sx={{
borderRadius: 999,
bgcolor: tint('#662582'),
color: '#662582',
border: `1px solid ${edge('#662582')}`,
'&:hover': { bgcolor: soft('#662582') }
bgcolor: tint('#C01227'),
color: '#C01227',
border: `1px solid ${edge('#C01227')}`,
'&:hover': { bgcolor: soft('#C01227') }
}}
>
<EditOutlined />
@@ -2168,9 +2117,9 @@ const Deliveries = () => {
background: '#fff'
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#662582') }}>
<AccentAvatar color="#662582"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
@@ -2778,14 +2727,14 @@ const Deliveries = () => {
borderRadius: 2,
bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#662582' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}
}}
>
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => {
const m = STATUS_META[s] || { label: s, color: '#662582', icon: MdHistoryToggleOff };
const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff };
const Ic = m.icon;
return (
<MenuItem key={s} value={s} sx={{ gap: 1 }}>
@@ -2812,9 +2761,9 @@ const Deliveries = () => {
borderRadius: 2,
bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#662582' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}
}}
/>

View File

@@ -43,7 +43,7 @@ Dispatch.js uses `react-leaflet` for declarative tile/marker rendering, BUT a lo
## 3. The reconcile rule (re-stated because it's load-bearing)
After **any** manual edit on `/nearle/dispatch/preview` (drag-and-drop step reorder, swap rider, change delivery sequence), the page **must** call `POST routes.workolik.com/optimization/reconcile-steps` before `POST jupiter.nearle.app/deliveries/createdeliveries`.
After **any** manual edit on `/doormile/dispatch/preview` (drag-and-drop step reorder, swap rider, change delivery sequence), the page **must** call `POST routes.workolik.com/optimization/reconcile-steps` before `POST jupiter.nearle.app/deliveries/createdeliveries`.
Skipping reconcile corrupts route sequences in the database. This is the single biggest production bug to avoid in this area.
@@ -74,7 +74,7 @@ The dispatch page renders 100+ markers and polylines on every render. Watch for
- Drag-and-drop uses `react-dnd` with `react-dnd-html5-backend`. Don't swap libraries.
- After every drop, debounce a call to `reconcileSteps` from `api.js`. Don't call it synchronously on every drag tick — the optimiser will rate-limit you.
- The "Assign" button calls `finalCreatedeliveries` → triggers `notifyRider` for each rider in the payload → redirects to `/nearle/deliveries`. Don't reorder these three steps.
- The "Assign" button calls `finalCreatedeliveries` → triggers `notifyRider` for each rider in the payload → redirects to `/doormile/deliveries`. Don't reorder these three steps.
---

File diff suppressed because it is too large Load Diff

View File

@@ -80,7 +80,7 @@ import logger from '../../../utils/logger';
// emerald for the actual GPS trail (signals "live / real" data). Per-step
// distinction in Combined view is carried by the numbered drop pins, which
// keep STEP_PALETTE so the timeline link to a specific delivery survives.
const COMBINED_PLANNED_COLOR = '#662582';
const COMBINED_PLANNED_COLOR = '#C01227';
const COMBINED_ACTUAL_COLOR = '#10b981';
const toNum = (v) => {
@@ -5729,7 +5729,7 @@ const Dispatch = ({
style={{
boxShadow: 'var(--shadow-lg)',
background: compareOpen
? 'linear-gradient(135deg, #662582, #9255AB)'
? 'linear-gradient(135deg, #C01227, #D25463)'
: '#fff',
marginLeft: 8,
color: compareOpen ? '#fff' : undefined
@@ -6328,7 +6328,7 @@ const Dispatch = ({
<div className="da-section">
<div className="da-hero-row">
<div className="da-hero-card">
<div className="da-hero-icon" style={{ background: '#6625821f', color: '#662582' }}>
<div className="da-hero-icon" style={{ background: '#C012271f', color: '#C01227' }}>
<MdOutlineInventory2 />
</div>
<div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
@@ -6809,7 +6809,7 @@ const Dispatch = ({
<div className="da-pos-modal-title-wrap">
<div
className="da-pos-modal-avatar"
style={{ background: `${riderPositionModal.color || '#662582'}22`, color: riderPositionModal.color || '#662582' }}
style={{ background: `${riderPositionModal.color || '#C01227'}22`, color: riderPositionModal.color || '#C01227' }}
>
<MdTwoWheeler />
</div>

View File

@@ -297,7 +297,7 @@ const Preview = () => {
// so a later reload / back-forward also bounces instead of re-using it.
useEffect(() => {
if (!stateData.dispatchPreviewData) {
navigate('/nearle/orders', { replace: true });
navigate('/doormile/orders', { replace: true });
return;
}
if (typeof window !== 'undefined' && window.history?.state) {
@@ -414,7 +414,7 @@ const Preview = () => {
OpenToast('Delivery Created Successfully', 'success', 2000);
setIsLoading(false);
if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
navigate('/nearle/deliveries');
navigate('/doormile/deliveries');
},
onError: (error) => {
OpenToast(error.message, 'error', 4000);
@@ -566,7 +566,7 @@ const Preview = () => {
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
<IconButton
onClick={() => navigate('/nearle/orders')}
onClick={() => navigate('/doormile/orders')}
sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }}
>
<HiOutlineArrowLeft size={20} />

View File

@@ -1,859 +0,0 @@
import React, { useState, useMemo } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import {
Avatar,
Box,
Divider,
Grid,
IconButton,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Tooltip,
Typography,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
MdReceiptLong,
MdDashboard,
MdHourglassEmpty,
MdReportProblem,
MdCheckCircle,
MdGroups,
MdEventNote,
MdCurrencyRupee,
MdVisibility,
MdInventory2,
MdOutlinePendingActions,
MdOutlineCheckCircle
} from 'react-icons/md';
import { fetchinvoiceinsight, fetchdeliverylist } from 'pages/api/api';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — shared with deliveries / tenants / customers / pricing /
// orders-details / riders-summary pages.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 14,
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
// Bill status → tab visual meta (semantic colours; brand purple reserved for "All").
const STATUS_META = {
0: { key: 'all', label: 'All', color: BRAND, icon: MdDashboard, countKey: 'totalcount' },
1: { key: 'open', label: 'Open', color: '#ef4444', icon: MdHourglassEmpty, countKey: 'pendingcount' },
2: { key: 'overdue', label: 'Overdue', color: '#f59e0b', icon: MdReportProblem, countKey: 'overduecount' },
3: { key: 'paid', label: 'Paid', color: '#10b981', icon: MdCheckCircle, countKey: 'paidcount' }
};
const STATUS_TABS = [0, 1, 2, 3];
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
}
const Invoice = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [billStatus, setBillStatus] = useState(0);
const [isloader, setIsLoader] = useState(false);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const handleDebouncedSearch = React.useCallback((val) => {
setDebouncedSearch(val);
setPage(0);
}, []);
// ============================================= || fetchinvoiceinsight ||
const {
data: insightdata,
isLoading: isInsightLoading,
isError: isInsightError,
error: insightError
} = useQuery({
queryKey: ['invoiceInsight'],
queryFn: fetchinvoiceinsight,
refetchInterval: 300000
});
// ============================================= || fetchdeliverylist ||
// NOTE: queryKey shape MUST stay `[billStatus]` — `fetchdeliverylist`
// destructures `const [billStatus] = queryKey`.
const {
data: deliveryList,
isLoading: isDeliveryLoading,
isError: isDeliveryError,
error: deliveryError
} = useQuery({
queryKey: [billStatus],
queryFn: fetchdeliverylist,
refetchInterval: 300000
});
const isLoading = isInsightLoading || isDeliveryLoading;
const isError = isInsightError || isDeliveryError;
const errorMessage = insightError?.message || deliveryError?.message;
// Client-side filter across tenant name, contact person, invoice number.
const filteredList = useMemo(() => {
if (!deliveryList) return [];
if (!debouncedSearch) return deliveryList;
const q = debouncedSearch.toLowerCase().trim();
return deliveryList.filter((row) =>
[row.tenantname, row.contactperson, String(row.invoiceno)]
.filter(Boolean)
.some((field) => String(field).toLowerCase().includes(q))
);
}, [deliveryList, debouncedSearch]);
const activePage = useMemo(() => {
const maxPage = Math.max(0, Math.ceil(filteredList.length / rowsPerPage) - 1);
return Math.min(page, maxPage);
}, [filteredList.length, page, rowsPerPage]);
// Keep page state in sync when filters or data updates shrink the list below current page
React.useEffect(() => {
if (page !== activePage) {
setPage(activePage);
}
}, [page, activePage]);
const pagedList = useMemo(
() => filteredList.slice(activePage * rowsPerPage, activePage * rowsPerPage + rowsPerPage),
[filteredList, activePage, rowsPerPage]
);
const grandTotal = useMemo(
() => filteredList.reduce((sum, r) => sum + (Number(r.totalamount) || 0), 0),
[filteredList]
);
const pageTotal = useMemo(
() => pagedList.reduce((sum, r) => sum + (Number(r.totalamount) || 0), 0),
[pagedList]
);
const handleChangePage = (event, newPage) => setPage(newPage);
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(+event?.target?.value);
setPage(0);
};
if (isError) {
return errorMessage;
}
const KPI_META = [
{ idx: 0, label: 'All Invoices', color: BRAND, icon: MdDashboard, value: insightdata?.totalcount ?? 0 },
{ idx: 1, label: 'Open', color: '#ef4444', icon: MdOutlinePendingActions, value: insightdata?.pendingcount ?? 0 },
{ idx: 2, label: 'Overdue', color: '#f59e0b', icon: MdReportProblem, value: insightdata?.overduecount ?? 0 },
{ idx: 3, label: 'Paid', color: '#10b981', icon: MdOutlineCheckCircle, value: insightdata?.paidcount ?? 0 }
];
const activeMeta = STATUS_META[billStatus];
return (
<>
{(isloader || isLoading) && <Loader />}
{/* ============================================= || Header || ============================================= */}
<PageHeader
title="Invoices"
subtitle={`Live · Viewing ${activeMeta.label.toLowerCase()} invoices`}
live
action={
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 1,
px: 1.5,
py: 0.875,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1.5px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 12
}}
>
<MdCurrencyRupee size={14} />
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.4, textTransform: 'uppercase' }}>
Grand Total
</Typography>
<Typography sx={{ fontWeight: 800, color: BRAND, fontSize: 13 }}>
{formatNumberToRupees(grandTotal)}
</Typography>
</Box>
}
/>
{/* ============================================= || KPI Cards (clickable filter) || ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.idx} xs={6} sm={6} md={3}>
<Box
onClick={() => {
setBillStatus(item.idx);
setPage(0);
}}
sx={{ cursor: 'pointer', height: '100%' }}
>
<StatCard
title={item.label}
value={item.value}
icon={<Icon size={20} />}
color={item.color}
loading={isInsightLoading}
/>
</Box>
</Grid>
);
})}
</Grid>
{/* ============================================= || Status Tabs + Search || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={1.5}
sx={{ flexWrap: 'wrap-reverse' }}
>
<Stack
direction="row"
spacing={0.75}
sx={{
flex: 1,
overflowX: 'auto',
py: 0.5,
px: 0.25,
'&::-webkit-scrollbar': { height: 6 },
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
}}
>
{STATUS_TABS.map((idx) => {
const meta = STATUS_META[idx];
const Icon = meta.icon;
const active = billStatus === idx;
const count = insightdata?.[meta.countKey] ?? 0;
return (
<Box
key={idx}
onClick={() => {
setBillStatus(idx);
setPage(0);
}}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: { xs: 0.625, md: 0.875 },
pl: 0.5,
pr: { xs: 1, md: 1.25 },
py: 0.5,
flexShrink: 0,
cursor: 'pointer',
borderRadius: 999,
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface,
color: active ? '#fff' : DT.textSecondary,
fontWeight: 600,
boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': {
borderColor: active ? meta.color : '#cbd5e1',
bgcolor: active ? meta.color : DT.surfaceAlt
}
}}
>
<Avatar
sx={{
width: { xs: 20, md: 22 },
height: { xs: 20, md: 22 },
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(meta.color),
color: active ? '#fff' : meta.color
}}
>
<Icon size={12} />
</Avatar>
<Typography
variant="caption"
sx={{
fontWeight: 600,
fontSize: { xs: 11.5, md: 13 },
lineHeight: 1
}}
>
{meta.label}
</Typography>
<Box
sx={{
minWidth: { xs: 20, md: 24 },
height: { xs: 18, md: 20 },
px: 0.625,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 999,
fontSize: { xs: 10, md: 11 },
fontWeight: 700,
bgcolor: active ? 'rgba(255,255,255,0.22)' : DT.surfaceAlt,
color: active ? '#fff' : DT.textSecondary,
border: 'none'
}}
>
{count}
</Box>
</Box>
);
})}
</Stack>
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={handleDebouncedSearch}
placeholder="Search invoices (ctrl+k)"
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* ============================================= || Table || ============================================= */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{isMobile ? (
<>
{isDeliveryLoading ? (
<Box sx={{ p: 1.5 }}>
<OrdersTableSkeleton col={4} />
</Box>
) : pagedList.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdReceiptLong size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No invoices to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword
? 'Try a different keyword.'
: `No ${activeMeta.label.toLowerCase()} invoices for this filter.`}
</Typography>
</Stack>
) : (
<MobileCardList>
{pagedList.map((item, index) => {
const overdue =
billStatus === 2 ||
(item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3);
return (
<MobileCard
key={item.invoiceno || index}
accent={BRAND}
header={
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{item.tenantname || '—'}
</Typography>
{item.contactperson && (
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{item.contactperson}
</Typography>
)}
</Stack>
</Stack>
<Tooltip title="Preview invoice" placement="left">
<IconButton
size="small"
onClick={() => {
setIsLoader(true);
setTimeout(() => {
setIsLoader(false);
navigate('/nearle/invoice/preview', { state: item });
}, 500);
}}
sx={{
flexShrink: 0,
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
<MdVisibility size={16} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Invoice ID">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdReceiptLong size={12} /> {item.invoiceno || '—'}
</Box>
</MobileField>
<MobileField label="Amount" align="right">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontSize: 11,
fontWeight: 800,
justifyContent: 'center'
}}
>
<MdCurrencyRupee size={11} />
{formatNumberToRupees(item.totalamount).replace('₹', '').trim()}
</Box>
</MobileField>
<MobileField label="Invoice Date">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={DT.textMuted} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Due Date">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={overdue && billStatus !== 3 ? '#ef4444' : DT.textMuted} />
<Typography
variant="caption"
sx={{
fontWeight: 700,
color: overdue && billStatus !== 3 ? '#ef4444' : DT.textPrimary
}}
noWrap
>
{item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Items">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#14b8a6'),
border: `1px solid ${edge('#14b8a6')}`,
color: '#14b8a6',
fontSize: 11,
fontWeight: 800,
minWidth: 44,
justifyContent: 'center'
}}
>
<MdInventory2 size={11} /> {item.itemcount ?? 0}
</Box>
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)}
</>
) : (
<TableContainer
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: { xs: 880, md: 1060 } }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: { xs: 10, md: 11 },
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: { xs: 1, md: 1.25 },
px: { xs: 1, md: 2 }
}
}}
>
<TableCell>#</TableCell>
<TableCell>Client</TableCell>
<TableCell>Invoice ID</TableCell>
<TableCell>Invoice Date</TableCell>
<TableCell>Due Date</TableCell>
<TableCell align="center">Items</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isDeliveryLoading && <OrdersTableSkeleton col={4} />}
{!isDeliveryLoading && pagedList.length === 0 ? (
<TableRow>
<TableCell colSpan={8} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdReceiptLong size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No invoices to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword
? 'Try a different keyword.'
: `No ${activeMeta.label.toLowerCase()} invoices for this filter.`}
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
pagedList.map((item, index) => {
const overdue = billStatus === 2 || (item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3);
return (
<TableRow
key={item.invoiceno || index}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 },
verticalAlign: 'top'
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(activePage * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{item.tenantname || '—'}
</Typography>
{item.contactperson && (
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{item.contactperson}
</Typography>
)}
</Stack>
</Stack>
</TableCell>
<TableCell>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdReceiptLong size={12} /> {item.invoiceno || '—'}
</Box>
</TableCell>
<TableCell>
<Stack spacing={0.25}>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={DT.textMuted} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
<Typography variant="caption" sx={{ color: DT.textSecondary, pl: 2 }}>
{item.transactiondate ? dayjs(item.transactiondate).utc().format('hh:mm A') : ''}
</Typography>
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.25}>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote
size={12}
color={overdue && billStatus !== 3 ? '#ef4444' : DT.textMuted}
/>
<Typography
variant="caption"
sx={{
fontWeight: 700,
color: overdue && billStatus !== 3 ? '#ef4444' : DT.textPrimary,
whiteSpace: 'nowrap'
}}
>
{item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
<Typography variant="caption" sx={{ color: DT.textSecondary, pl: 2 }}>
{item.duedate ? dayjs(item.duedate).utc().format('hh:mm A') : ''}
</Typography>
</Stack>
</TableCell>
<TableCell align="center">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#14b8a6'),
border: `1px solid ${edge('#14b8a6')}`,
color: '#14b8a6',
fontSize: 11,
fontWeight: 800,
minWidth: 44,
justifyContent: 'center'
}}
>
<MdInventory2 size={11} /> {item.itemcount ?? 0}
</Box>
</TableCell>
<TableCell align="right">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontSize: 11,
fontWeight: 800,
minWidth: 110,
justifyContent: 'center'
}}
>
<MdCurrencyRupee size={11} />
{formatNumberToRupees(item.totalamount).replace('₹', '').trim()}
</Box>
</TableCell>
<TableCell align="center">
<Tooltip title="Preview invoice" placement="left">
<IconButton
size="small"
onClick={() => {
setIsLoader(true);
setTimeout(() => {
setIsLoader(false);
navigate('/nearle/invoice/preview', { state: item });
}, 500);
}}
sx={{
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
<MdVisibility size={16} />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
<Divider />
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'flex-start', sm: 'center' }}
justifyContent="space-between"
sx={{
px: 2,
py: 1,
background: '#ffffff'
}}
>
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}>
Page total · {formatNumberToRupees(pageTotal)}
</Typography>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
component="div"
count={filteredList.length}
rowsPerPage={rowsPerPage}
page={activePage}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
sx={{
'& .MuiTablePagination-toolbar': { minHeight: 40, px: 0 },
'& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': {
fontWeight: 700,
color: DT.textSecondary
}
}}
/>
</Stack>
</Paper>
<Outlet />
</>
);
};
export default Invoice;

View File

@@ -1,486 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
// import nearleLogo from '../../../assets/images/nearleLogo.png';
import logo_nearle1 from '../../../assets/images/logo-nearle1.png';
import axios from 'axios';
import dayjs from 'dayjs';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
import { DownloadOutlined, PrinterFilled } from '@ant-design/icons';
import ReactToPrint, { useReactToPrint } from 'react-to-print';
import { SearchOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
// import jsPDF from 'jspdf';
import { useNavigate } from 'react-router-dom';
import { FaArrowLeft } from 'react-icons/fa6';
import { FaIndianRupeeSign } from 'react-icons/fa6';
// import autoTable from 'jspdf-autotable';
import {
Grid,
Button,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Tabs,
Tab,
Typography,
Box,
OutlinedInput,
InputAdornment,
IconButton,
TextField,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Stack,
Chip
} from '@mui/material';
const InvoicePreview = () => {
const [selected, setselected] = useState({});
const location = useLocation();
const navigate = useNavigate();
console.log('previewSelect', location.state);
const componentRef = useRef(null);
const [tabletype, settabletype] = useState(true);
const [paydialog, setpaydialog] = useState(false);
const [refnumber, setRefnumber] = useState('');
const [remarks, setRemarks] = useState('');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
useEffect(() => {
setselected(location.state);
}, []);
// ================================================= || formatNumberToRupees || =================================================
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(value);
}
// ================================================= || updatePayment || =================================================
const updatePayment = async () => {
try {
const updateResponse = await axios.put(`${process.env.REACT_APP_URL}/invoice/updatestatus`, {
salesid: selected.salesid,
referenceno: refnumber,
referencedate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
billstatus: 2,
paymentremarks: remarks
});
if (updateResponse.status) {
enqueueSnackbar(' Updated Successfully ', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
}
console.log('updateResponse', updateResponse);
} catch (error) {
console.log('updateResponse', error);
}
};
return (
<>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="Space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={2}
sx={{ px: { xs: 1.5, md: 2.5 }, py: 1, bgcolor: '#eeeeee' }}
>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Tooltip title="back">
<IconButton
color="primary"
onClick={() => {
navigate('/nearle/invoice');
}}
>
<FaArrowLeft size={'large'} />
</IconButton>
</Tooltip>
<Stack alignItems={'center'}>
<Typography variant="h3" color={'primary'}>
Invoice Details
</Typography>
<Chip
size="small"
color="warning"
variant="outlined"
sx={{ bgcolor: theme.palette.warning.lighter }}
label={`Invoice No :${'\u00a0\u00a0'}${selected.invoiceno}`}
/>
</Stack>
</Stack>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ width: { xs: '100%', md: 'auto' } }}>
<Button
variant="outlined"
color="primary"
fullWidth={isMobile}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
}
}}
onClick={() => {
setpaydialog(true);
}}
>
{' '}
<FaIndianRupeeSign />
Update Payment
</Button>
<ReactToPrint
trigger={() => (
<Button
size="small"
startIcon={<PrinterFilled />}
variant="outlined"
color="primary"
fullWidth={isMobile}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
}
}}
>
Print
</Button>
)}
content={() => componentRef.current}
/>
</Stack>
</Stack>
<Box sx={{ pb: 2.5, border: '1px solid #eee', overflowX: { xs: 'auto', md: 'visible' } }}>
{/* minWidth keeps the invoice at a legible fixed layout on phones —
the parent's overflowX:auto then lets it scroll horizontally
instead of squishing the header into vertical slivers. 720px sits
within the print page width, so printing is unaffected. */}
<div ref={componentRef} style={{ width: '100%', minWidth: 720 }}>
<Box id="print" sx={{ p: 2.5 }}>
<Box sx={{ pb: 2.5 }}>
<Stack
sx={{
flexDirection: 'row',
// bgcolor: theme.palette.primary.main,
border: '1px solid #eee',
px: 3
}}
justifyContent="space-between"
>
<Box sx={{ pt: 0.5 }}>
<Stack direction="row" spacing={2}>
<img src={logo_nearle1} style={{ width: '150px', height: '50px' }} />{' '}
</Stack>
{/* <Typography
sx={{ color: theme.palette.primary.main, py: 0.5 }}
>
{`Invoice No: ${"\u00a0\u00a0\u00a0"}${selected.invoiceno}`}
</Typography> */}
<Stack direction="row" justifyContent="space-between">
<Typography
sx={{
overflow: 'hidden',
color: theme.palette.primary.main
}}
variant="subtitle1"
>
Invoice No :
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>{`${'\u00a0\u00a0'}${selected.invoiceno}`}</Typography>
</Stack>
</Box>
<Box sx={{ pt: 2.5, pb: 1.75 }}>
<Stack direction="row" justifyContent="space-between">
<Typography sx={{ pl: 4, color: theme.palette.primary.main }} variant="subtitle1">
Date :{' '}
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>
{dayjs(selected.transactiondate).format('DD-MM-YYYY')}
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography
sx={{
pr: 2,
overflow: 'hidden',
color: theme.palette.primary.main
}}
variant="subtitle1"
>
Due Date :
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>{dayjs(selected.dueDate).format('DD-MM-YYYY')}</Typography>
</Stack>
{/* <Stack direction="row" justifyContent="space-between">
<Typography
sx={{
pr: 2,
overflow: "hidden",
color: theme.palette.primary.main,
}}
variant="subtitle1"
>
Invoice No :
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>
{`${"\u00a0\u00a0\u00a0"}${selected.invoiceno}`}
</Typography>
</Stack> */}
</Box>
</Stack>
<Box sx={{ pt: 2.5 }}>
<Grid container spacing={2} justifyContent="space-between" direction="row">
<Grid item xs={12} sm={6}>
<Box
sx={{
border: 1,
minHeight: 240,
borderColor: 'grey.200',
borderRadius: 0.5,
p: 2.5
}}
>
<Grid container direction="row">
<Grid item md={8}>
<Stack spacing={2}>
<Typography variant="h5">From:</Typography>
<Stack sx={{ width: '100%' }}>
<Typography variant="subtitle1">Nearle Technology Privite Limited.</Typography>
<Typography color="secondary">
424, 4<sup>th</sup>floor,
</Typography>
<Typography color="secondary">Red rose towers,</Typography>
<Typography color="secondary">DB Road, RS Puram,</Typography>
<Typography color="secondary">641002.</Typography>
<Typography color="secondary">care@nearle.in</Typography>
<Typography color="secondary">9047968666</Typography>
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
</Grid>
<Grid item xs={12} sm={6}>
<Box
sx={{
border: 1,
minHeight: 240,
borderColor: 'grey.200',
borderRadius: 0.5,
p: 2.5
}}
>
<Grid container direction="row">
<Grid item md={8}>
<Stack spacing={2}>
<Typography variant="h5">To:</Typography>
<Stack sx={{ width: '100%' }}>
<Typography variant="subtitle1">{selected.tenantname}</Typography>
<Typography color="secondary">{selected.address}</Typography>
<Typography color="secondary">{selected.suburb}</Typography>
<Typography color="secondary">{selected.city}</Typography>
<Typography color="secondary">{selected.state}</Typography>{' '}
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
</Box>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Particulars</TableCell>
<TableCell>Unit</TableCell>
<TableCell>Quantity</TableCell>
<TableCell align="right">Rate</TableCell>
{/* {selected && selected.pricingtypeid === 73 && ( */}
<TableCell align="right">Other Charges</TableCell>
{/* )} */}
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
{selected.tenantsalesdetails && (
<TableBody>
<TableRow>
<TableCell>1</TableCell>
<TableCell>
<Typography>
{`Invoice from ${dayjs(selected.tenantsalesdetails[0].fromdate).format('DD-MM-YYYY')} to ${dayjs(
selected.tenantsalesdetails[0].todate
).format('DD-MM-YYYY')}`}
</Typography>
</TableCell>
<TableCell>
<Typography>{selected.tenantsalesdetails[0].pricingtype}</Typography>
</TableCell>
<TableCell>
<Typography>{`${selected.tenantsalesdetails[0].quantity.toFixed(2)} km`}</Typography>
</TableCell>
<TableCell>
<Typography align="right">{`${selected.tenantsalesdetails[0].baserate.toFixed(2)}`}</Typography>
</TableCell>
{/* {selected.tenantsalesdetails[0].pricingtypeid == 73 && ( */}
<TableCell align="right">
<Typography>{`${selected.tenantsalesdetails[0].othercharges}.00`}</Typography>
</TableCell>
{/* )} */}
<TableCell align="right">
<Typography>{`${selected.tenantsalesdetails[0].amount}.00`}</Typography>
</TableCell>
</TableRow>
</TableBody>
)}
</Table>
</TableContainer>
<Divider />
<Box sx={{ p: 2.5 }}>
<Grid container direction="row" justifyContent="flex-end">
<Grid item md={4}>
<Stack spacing={2}>
<Stack direction="row" justifyContent="space-between">
<Typography color="secondary">Sub Total:</Typography>
<Typography variant="h6">{formatNumberToRupees(selected.salesamount)}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography color="secondary">Discount:</Typography>
<Typography variant="h6" color={theme.palette.error.main}>
- {formatNumberToRupees(selected.discountamt)}
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography color={theme.palette.grey[500]}>Tax:</Typography>
<Typography variant="h6" color={theme.palette.success.main}>
+ {formatNumberToRupees(selected.taxamount)}
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography sx={{ pr: 2 }} variant="subtitle1">
Grand Total:
</Typography>
<Typography variant="h6">{formatNumberToRupees(Math.round(selected.totalamount))}</Typography>
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
</Box>
<Divider />
<Box sx={{ p: 2.5 }}>
<Typography>Notes: {selected.remarks}</Typography>
</Box>
<Divider />
</div>
</Box>
{/* ================================================= || updatePayment Dialog || ================================================= */}
<Dialog
open={paydialog}
onClose={() => {
setpaydialog(false);
}}
maxWidth={'sm'}
fullWidth
>
<DialogTitle sx={{ bgcolor: theme.palette.primary.main }}>
<Stack direction={'row'} spacing={1}>
<Typography variant="h2" sx={{ color: 'white' }}>
</Typography>
<Typography variant="h3" sx={{ color: 'white' }}>
Update Payment
</Typography>
</Stack>
</DialogTitle>
<DialogContent dividers>
<Stack spacing={1} sx={{ mb: 2 }}>
<Typography>Reference No</Typography>
<TextField
type="number"
placeholder="Enter Reference Number"
sx={{ width: '100%' }}
onChange={(e) => {
setRefnumber(e.target.value);
}}
/>
</Stack>
<Stack spacing={2} sx={{ mb: 2 }}>
<Typography>Remarks</Typography>
<TextField
multiline
required
placeholder="Enter Remarks"
sx={{ width: '100%' }}
onChange={(e) => {
setRemarks(e.target.value);
}}
/>
</Stack>
</DialogContent>
<DialogActions>
<Button
variant="outlined"
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
},
m: 2
}}
onClick={() => {
setpaydialog(false);
}}
>
Cancel
</Button>
<Button
variant="outlined"
disabled={refnumber == '' || remarks == ''}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
},
m: 2
}}
onClick={() => {
setpaydialog(false);
updatePayment();
navigate('/nearle/invoice');
}}
>
Update
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default InvoicePreview;

View File

@@ -1,45 +1,90 @@
import { useState, useEffect } from 'react';
import { enqueueSnackbar, closeSnackbar } from 'notistack';
import AnimateButton from 'components/@extended/AnimateButton';
import OtpInput from 'react18-input-otp';
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, FormLabel, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { enqueueSnackbar } from 'notistack';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import Loader from 'components/Loader';
import logo from 'assets/images/logo-nearle1.png';
import expressImage from 'assets/images/express.png';
import logo from 'assets/images/doormile-logo.png';
import { useSelector, useDispatch } from 'react-redux';
import { OpenToast } from 'components/third-party/OpenToast';
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { setLoginUser } from 'store/reducers/loginUserSlice';
import { markSessionStart } from 'utils/session';
import { DT } from 'themes/dt/tokens';
// Astryx design system — see themes/astryx.js for the Doormile brand theme
// and CLAUDE.md's <!-- ASTRYX:START --> block for the CLI workflow.
// NOTE: custom CSS (xstyle/stylex.create()) isn't wired up yet — see the
// comment in config-overrides.js. Everything below uses Astryx component
// props only; the brand gradient panel and the two logo images are plain
// native elements with inline `style` for that reason.
import { AppShell } from '@astryxdesign/core/AppShell';
import { Theme } from '@astryxdesign/core/theme';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Center } from '@astryxdesign/core/Center';
import { Card } from '@astryxdesign/core/Card';
import { Heading } from '@astryxdesign/core/Heading';
import { Text } from '@astryxdesign/core/Text';
import { TextInput } from '@astryxdesign/core/TextInput';
import { Button } from '@astryxdesign/core/Button';
import { Link } from '@astryxdesign/core/Link';
import { doormileTheme } from 'themes/astryx';
const brandPanelStyle = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
width: '46%',
height: '100vh',
color: '#fff',
padding: 48,
background: `linear-gradient(150deg, ${DT.brand} 0%, #D25463 100%)`
};
const logoLockupStyle = { position: 'absolute', top: 48, left: 48, maxHeight: 60 };
const bulletDotStyle = {
width: 22,
height: 22,
borderRadius: '50%',
backgroundColor: 'rgba(255, 255, 255, 0.18)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700,
flexShrink: 0
};
// doormile-logo.png is a white asset; recolour to brand red for this
// white-background card (the brand-panel logo stays white as-is).
const formLogoStyle = {
maxHeight: 48,
filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'
};
const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'];
const Login = () => {
const dispatch = useDispatch();
const fcmtoken = useSelector((state) => state.fcm);
const permission = useSelector((state) => state.fcm.permission);
const theme = useTheme();
const [loading, setLoading] = useState(false);
let navigate = useNavigate();
const [otp, setOtp] = useState('');
const [currentotp, setCurrentotp] = useState('');
const [userinfo, setUserinfo] = useState({});
const [username, setUsername] = useState('');
const [passwordStatus, setPasswordStatus] = useState(0);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isPassword, setIspassword] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [userid, setUserid] = useState(0);
useEffect(() => {
if (localStorage.getItem('firstname')) {
navigate('/nearle/dispatch');
navigate('/doormile/dispatch');
}
}, []);
@@ -100,7 +145,7 @@ const Login = () => {
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
markSessionStart();
fetchAppLocations(userinfo.userid);
navigate('/nearle/dispatch');
navigate('/doormile/dispatch');
} else {
OpenToast(res.data.message, 'error', 3000);
}
@@ -123,7 +168,7 @@ const Login = () => {
markSessionStart();
closeGlobalToast(); // to close the pin snackbar
navigate('/nearle/dispatch');
navigate('/doormile/dispatch');
};
const opentoast = (message) => {
@@ -152,320 +197,157 @@ const Login = () => {
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (passwordStatus == 0) {
loginsend();
} else if (passwordStatus == 1) {
if (!password || !confirmPassword || password != confirmPassword) {
OpenToast('Check Password', 'warning', 3000);
} else {
updateUser();
}
} else if (passwordStatus == 2) {
if (!password) {
OpenToast('Invalid Password', 'warning', 3000);
}
loginsend();
}
};
return (
<Box sx={{ minHeight: '100vh', display: 'flex', bgcolor: '#f8fafc' }}>
{loading && <Loader />}
<Theme theme={doormileTheme} mode="light">
<AppShell contentPadding={0}>
{loading && <Loader />}
<HStack gap={0} height="100vh" wrap="nowrap">
{/* ---- Left brand panel (plain element: see the note at the top of
this file about custom styling not going through Astryx yet) ---- */}
<div style={brandPanelStyle}>
<img src={logo} alt="Doormile" style={logoLockupStyle} />
{/* ---- Left brand panel (hidden on small screens) ---- */}
<Box
sx={{
display: { xs: 'none', md: 'flex' },
position: 'relative',
overflow: 'hidden',
flexBasis: '46%',
flexDirection: 'column',
justifyContent: 'center',
color: '#fff',
p: 6,
background: 'linear-gradient(150deg, #4D1C61 0%, #662582 52%, #9255AB 100%)'
}}
>
{/* Logo at the top-left corner */}
<img
src={expressImage}
alt="Operate your dispatch"
style={{
position: 'absolute',
top: 48,
left: 48,
maxHeight: 60
}}
/>
<VStack gap={2} maxWidth={430}>
<Heading level={1} color="inherit">
Operate your dispatch, end to end.
</Heading>
<Text type="large" color="inherit">
Orders, AI route optimisation, live rider tracking and billing all in the Doormile Express operator console.
</Text>
{/* decorative light glows */}
<Box
sx={{
position: 'absolute',
top: -120,
right: -80,
width: 360,
height: 360,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 70%)'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -150,
left: -110,
width: 440,
height: 440,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0) 70%)'
}}
/>
<VStack gap={1.5} padding={0}>
{BULLETS.map((t) => (
<HStack key={t} gap={1.25} vAlign="center">
<span style={bulletDotStyle}></span>
<Text color="inherit">{t}</Text>
</HStack>
))}
</VStack>
</VStack>
</div>
<Box sx={{ position: 'relative', maxWidth: 430 }}>
<Typography sx={{ fontSize: 40, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
Operate your dispatch,
<br />
end to end.
</Typography>
<Typography sx={{ fontSize: 17.5, color: 'rgba(255,255,255,0.85)', mb: 4, lineHeight: 1.6 }}>
Orders, AI route optimisation, live rider tracking and billing all in the NearlExpress operator console.
</Typography>
{/* ---- Right form panel ---- */}
<Center axis="both" width="54%" height="100vh">
<VStack width="100%" maxWidth={420} gap={3} padding={3}>
<Card padding={4} elevation="low">
<VStack gap={0.5} hAlign="center" padding={0}>
<img src={logo} alt="Doormile" style={formLogoStyle} />
<Heading level={2}>Welcome back</Heading>
<Text type="supporting">Sign in to the Doormile Express console</Text>
</VStack>
<Stack spacing={1.5} sx={{ display: 'inline-flex', textAlign: 'left' }}>
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
<Box
sx={{
width: 22,
height: 22,
borderRadius: '50%',
bgcolor: 'rgba(255,255,255,0.18)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700
}}
>
</Box>
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
</Stack>
))}
</Stack>
</Box>
</Box>
{/* ---- Right form panel ---- */}
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 2.5, sm: 4 } }}>
<Box sx={{ width: '100%', maxWidth: 420 }}>
<Card
sx={{
width: '100%',
borderRadius: 3,
border: `1px solid ${theme.palette.divider}`,
boxShadow: '0 14px 40px rgba(15, 23, 42, 0.10)',
p: { xs: 2.5, sm: 4 }
}}
>
{/* Logo */}
<Stack alignItems="center" mb={2.5}>
<img src={logo} alt="loginpagelogo" style={{ maxHeight: 48 }} />
</Stack>
{/* Title */}
<Typography variant="h3" textAlign="center" sx={{ fontWeight: 700, mb: 0.5 }}>
Welcome back
</Typography>
<Typography variant="body2" textAlign="center" sx={{ color: '#64748b', mb: 3 }}>
Sign in to the NearlExpress console
</Typography>
<CardContent sx={{ p: 0 }}>
<form
noValidate
onSubmit={(e) => {
e.preventDefault();
if (passwordStatus == 0) {
loginsend();
} else if (passwordStatus == 1) {
if (!password || !confirmPassword || password != confirmPassword) {
OpenToast('Check Password', 'warning', 3000);
} else {
updateUser();
}
} else if (passwordStatus == 2) {
if (!password) {
OpenToast('Invalid Password', 'warning', 3000);
}
loginsend();
}
// if (currentotp) {
// if (currentotp == otp) {
// loginsuccessful();
// fetchAppLocations();
// } else {
// opentoast('Invalid pin');
// }
// }
}}
>
<Stack spacing={3}>
{/* Email */}
<TextField
autoFocus
fullWidth
label="E-mail Address"
variant="outlined"
autoComplete="email"
required
value={username}
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
InputProps={{ readOnly: passwordStatus }}
/>
{/* Setup Password */}
{passwordStatus == 1 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
<Typography variant="h4" textAlign="start" mb={3}>
Setup Password
</Typography>
<TextField
autoFocus
fullWidth
label="Enter New Password"
variant="outlined"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
<form noValidate onSubmit={handleSubmit}>
<VStack gap={3} padding={0}>
<TextInput
hasAutoFocus
label="E-mail Address"
type="email"
isRequired
value={username}
onChange={(value) => setUsername(value.toLocaleLowerCase())}
isDisabled={!!passwordStatus}
/>
<TextField
error={confirmPassword !== '' && password !== confirmPassword}
fullWidth
label="Re-Enter Password"
variant="outlined"
required
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end">
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
)}
{/* Enter Password */}
{passwordStatus == 2 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
<Typography variant="h4" textAlign="start" mb={3}>
Enter Password
</Typography>
<TextField
autoFocus
fullWidth
label="Enter Password"
variant="outlined"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
)}
{/* Setup Password */}
{passwordStatus == 1 && (
<VStack gap={3} padding={0}>
<Heading level={4}>Setup Password</Heading>
<TextInput
hasAutoFocus
label="Enter New Password"
type="password"
isRequired
value={password}
onChange={(value) => setPassword(value)}
/>
<TextInput
label="Re-Enter Password"
type="password"
isRequired
value={confirmPassword}
onChange={(value) => setConfirmPassword(value)}
status={
confirmPassword !== '' && password !== confirmPassword
? { type: 'error', message: 'Passwords do not match' }
: undefined
}
/>
</VStack>
)}
{/* OTP */}
{isPassword && (
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<FormLabel>Enter Password</FormLabel>
<Link
variant="body2"
sx={{ cursor: 'pointer' }}
onClick={() => {
setOtp('');
loginsend();
}}
>
Retry
</Link>
</Stack>
{/* Enter Password */}
{passwordStatus == 2 && (
<VStack gap={3} padding={0}>
<Heading level={4}>Enter Password</Heading>
<TextInput
hasAutoFocus
label="Enter Password"
type="password"
isRequired
value={password}
onChange={(value) => setPassword(value)}
/>
</VStack>
)}
{/* <OtpInput
shouldAutoFocus
value={otp}
onChange={(otp) => setOtp(otp)}
numInputs={4}
containerStyle={{ justifyContent: 'space-between' }}
inputStyle={{
width: 48,
height: 48,
borderRadius: 8,
border: `1px solid ${borderColor}`,
fontSize: 18
}}
focusStyle={{
outline: 'none',
border: `1px solid ${theme.palette.primary.main}`,
boxShadow: theme.customShadows.primary
}}
/> */}
<TextField type="passowrd" value={password} onChange={(e) => setPassword(e.target.value)} />
</Stack>
)}
{/* Submit */}
<AnimateButton>
<Button fullWidth size="large" type="submit" variant="contained" color="primary">
Continue
</Button>
</AnimateButton>
</Stack>
</form>
</CardContent>
</Card>
{/* OTP / retry */}
{isPassword && (
<VStack gap={1.5} padding={0}>
<HStack justify="between">
<Text type="label">Enter Password</Text>
<Link
onClick={() => {
setOtp('');
loginsend();
}}
>
Retry
</Link>
</HStack>
<TextInput label="Password" type="password" value={password} onChange={(value) => setPassword(value)} isLabelHidden />
</VStack>
)}
{/* footer */}
<Stack direction="row" justifyContent="center" alignItems="center" flexWrap="wrap" useFlexGap spacing={2} sx={{ mt: 3 }}>
<Typography
variant="caption"
component={Link}
href="https://nearle.in"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
>
&copy; All rights reserved
</Typography>
<Typography
variant="caption"
component={Link}
href="https://nearle.in/terms"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
>
Terms and Conditions
</Typography>
<Typography
variant="caption"
component={Link}
href="https://nearle.in/privacy"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
>
Privacy Policy
</Typography>
</Stack>
</Box>
</Box>
</Box>
<Button label="Continue" type="submit" variant="primary" size="lg" width="100%" />
</VStack>
</form>
</Card>
{/* footer */}
<HStack justify="center" wrap="wrap" gap={2}>
<Link href="https://nearle.in" target="_blank" isExternalLink>
&copy; All rights reserved
</Link>
<Link href="https://nearle.in/terms" target="_blank" isExternalLink>
Terms and Conditions
</Link>
<Link href="https://nearle.in/privacy" target="_blank" isExternalLink>
Privacy Policy
</Link>
</HStack>
</VStack>
</Center>
</HStack>
</AppShell>
</Theme>
);
};

View File

@@ -22,7 +22,11 @@ import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import AnimateButton from 'components/@extended/AnimateButton';
import logo from 'assets/images/logo-nearle1.png';
import logo from 'assets/images/doormile-logo.png';
// doormile-logo.png is a white asset; recolour it to brand red for this page's light background.
const DOORMILE_RED_FILTER =
'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
@@ -228,7 +232,7 @@ const Login = () => {
// sx={{ ml: 3, mt: 3 }}
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }}
>
<img src={logo} alt="legendary" width={isMobile ? '160px' : '200px'} />
<img src={logo} alt="Doormile" width={isMobile ? '160px' : '200px'} style={{ height: 'auto', filter: DOORMILE_RED_FILTER }} />
</Grid>
<Grid item xs={12}>
<Grid

View File

@@ -33,8 +33,8 @@ useMutation({
After the solver returns:
1. Solver response → stored in the orders page state.
2. Operator navigates to `OrdersPreview.js` (`/nearle/orders/preview`) for a first look.
3. From there → `/nearle/dispatch/preview` (`Preview.js` in the dispatch folder) for drag-and-drop adjustment.
2. Operator navigates to `OrdersPreview.js` (`/doormile/orders/preview`) for a first look.
3. From there → `/doormile/dispatch/preview` (`Preview.js` in the dispatch folder) for drag-and-drop adjustment.
4. `Preview.js` is the one that calls `finalCreatedeliveries` to commit.
Don't try to commit from `orders.js` or `OrdersPreview.js` — they are read-only / staging steps. The reconcile-then-commit dance only happens on the dispatch preview page (see `src/pages/nearle/dispatch/CLAUDE.md`).

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import {
Autocomplete,
Button,
@@ -25,14 +26,14 @@ import { useLocation, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import MainCard from 'components/MainCard';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from 'pages/api/api';
import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../../api/api';
import { OpenToast } from 'components/third-party/OpenToast';
import { useMutation, useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import { Empty } from 'antd';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { DashboardFilled, OpenAIFilled } from '@ant-design/icons';
import { DashboardFilled } from '@ant-design/icons';
import { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6';
import { HiOutlineArrowLeft } from 'react-icons/hi';
@@ -234,9 +235,7 @@ const OrdersPreview = () => {
const {
data: paymentModes = [],
isLoading: paymentModesLoading,
isError: paymentModesError,
error: paymentModesErrorMessage
isLoading: paymentModesLoading
} = useQuery({
queryKey: ['paymentmodes'],
queryFn: fetchPaymentType
@@ -246,10 +245,7 @@ const OrdersPreview = () => {
const {
data: ridersList = [],
isLoading: ridersListLoading,
isError: ridersListError,
error: ridersListErrorMessage,
refetch: ridersListRefetch
isLoading: ridersListLoading
} = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
queryFn: fetchRidersList,
@@ -282,13 +278,13 @@ const OrdersPreview = () => {
onSuccess: (data, variables) => {
console.log('data', data);
console.log('varialbles', variables);
notifyRiderMutation.mutate(rider.userfcmtoken || riderToken); // Call notifyRider after success
notifyRiderMutation.mutate(rider?.userfcmtoken || riderToken); // Call notifyRider after success
if (data.status == 'accepted') {
OpenToast('Delivery Created Successfully', 'success', 2000);
}
setTimeout(() => {
setIsLoading(false);
navigate('/nearle/deliveries');
navigate('/nearle/orders');
}, 2000);
},
onError: (error) => {
@@ -581,11 +577,6 @@ const OrdersPreview = () => {
<TableCell>
<Typography> {index + 1}</Typography>
</TableCell>
{/* {aiMode == 1 && (
<TableCell>
<Chip color="primary" label={val.zone_name} />
</TableCell>
)} */}
<TableCell>
<Tooltip title={val.tenantaddress}>
<Typography variant="body1" noWrap>
@@ -656,12 +647,6 @@ const OrdersPreview = () => {
</Stack>
</TableCell>
<TableCell align="left">{val.ordernotes}</TableCell>
{/* {aiMode == 1 && (
<TableCell align="left">
<Typography sx={{ whiteSpace: 'nowrap' }}>{val.username}</Typography>
<Typography>ID : {val.userid}</Typography>
</TableCell>
)} */}
<TableCell align="center">
<Chip
size="small"
@@ -774,7 +759,7 @@ const OrdersPreview = () => {
disabled={aiMode === 0 && (!rider || !payment)}
onClick={handleManualCreateDelivery}
>
Assign Orders
Finalise
</Button>
</Stack>
</MainCard>

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { TableRow, TableCell, Skeleton, Stack } from '@mui/material';
export const OrdersTableSkeleton = ({ rowsPerPage = 5, col = 1 }) => {

View File

@@ -0,0 +1,79 @@
/* eslint-disable no-unused-vars */
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// distance function (same)
function distance(lat1, lng1, lat2, lng2) {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLng = (lng2 - lng1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
const center = [11.015181, 76.953682];
const riders = [
{ id: 1, lat: 11.04362, lng: 76.924667 },
{ id: 2, lat: 11.00988, lng: 76.949966 },
{ id: 3, lat: 11.020983, lng: 76.966331 }
];
export default function RidersPinPointOSM() {
const sortedRiders = riders
.map((r) => ({
...r,
distance: distance(center[0], center[1], r.lat, r.lng)
}))
.sort((a, b) => a.distance - b.distance);
// purple center marker
const centerIcon = L.icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/purple-dot.png',
iconSize: [32, 32]
});
// basic numbered marker icon
const createMarkerIcon = (number) =>
L.divIcon({
className: 'custom-marker',
html: `
<div style="
background:#007bff;
color:white;
width:28px;
height:28px;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-weight:bold;
border:2px solid white;
">
${number}
</div>
`,
iconSize: [30, 30],
iconAnchor: [15, 15]
});
return (
<MapContainer center={center} zoom={14} style={{ height: '300px', width: '100%' }}>
{/* OSM tiles */}
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{/* Purple center marker */}
<Marker position={center} icon={centerIcon} />
{/* Sorted rider markers */}
{sortedRiders.map((r, index) => (
<Marker key={r.id} position={[r.lat, r.lng]} icon={createMarkerIcon(index + 1)}>
<Popup>Rider {index + 1}</Popup>
</Marker>
))}
</MapContainer>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
/* eslint-disable no-unused-vars */
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import parse from 'autosuggest-highlight/parse';
import { debounce } from '@mui/material/utils';
// This key was created specifically for the demo in mui.com.
// You need to create a new one for your application.
const GOOGLE_MAPS_API_KEY ='AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8';
function loadScript(src, position, id) {
if (!position) {
return;
}
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
export default function GoogleMaps() {
const [value, setValue] = React.useState(null);
const [inputValue, setInputValue] = React.useState('');
const [options, setOptions] = React.useState([]);
const loaded = React.useRef(false);
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
document.querySelector('head'),
'google-maps',
);
}
loaded.current = true;
}
const fetch = React.useMemo(
() =>
debounce((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 400),
[],
);
React.useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google) {
autocompleteService.current =
new window.google.maps.places.AutocompleteService();
}
if (!autocompleteService.current) {
return undefined;
}
if (inputValue === '') {
setOptions(value ? [value] : []);
return undefined;
}
fetch({ input: inputValue }, (results) => {
if (active) {
let newOptions = [];
if (value) {
newOptions = [value];
}
if (results) {
newOptions = [...newOptions, ...results];
}
setOptions(newOptions);
}
});
return () => {
active = false;
};
}, [value, inputValue, fetch]);
return (
<Autocomplete
id="google-map-demo"
// sx={{ width: 300 }}
fullWidth
getOptionLabel={(option) =>
typeof option === 'string' ? option : option.description
}
filterOptions={(x) => x}
options={options}
autoComplete
includeInputInList
filterSelectedOptions
value={value}
noOptionsText="No locations"
onChange={(event, newValue) => {
setOptions(newValue ? [newValue, ...options] : options);
setValue(newValue);
}}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => (
<TextField {...params}
// label="Add a location"
placeholder='Address'
fullWidth />
)}
renderOption={(props, option) => {
const matches =
option.structured_formatting.main_text_matched_substrings || [];
const parts = parse(
option.structured_formatting.main_text,
matches.map((match) => [match.offset, match.offset + match.length]),
);
return (
<li {...props}>
<Grid container alignItems="center">
<Grid item sx={{ display: 'flex', width: 44 }}>
<LocationOnIcon sx={{ color: 'text.secondary' }} />
</Grid>
<Grid item sx={{ width: 'calc(100% - 44px)', wordWrap: 'break-word' }}>
{parts.map((part, index) => (
<Box
key={index}
component="span"
sx={{ fontWeight: part.highlight ? 'bold' : 'regular' }}
>
{part.text}
</Box>
))}
<Typography variant="body2" color="text.secondary">
{option.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
</li>
);
}}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,846 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import Loader from 'components/Loader';
import { useEffect, useState, Fragment } from 'react';
import { useTheme } from '@mui/material/styles';
import MainCard from 'components/MainCard';
import axios from 'axios';
import ClearIcon from '@mui/icons-material/Clear';
import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
import { Empty } from 'antd';
import MyLocationIcon from '@mui/icons-material/MyLocation';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import { GoogleMap, LoadScript, Marker } from '@react-google-maps/api';
import {
FormControl,
InputAdornment,
Grid,
Typography,
Stack,
Button,
TextField,
Autocomplete,
Divider,
Dialog,
DialogTitle,
DialogContent,
Checkbox,
DialogActions,
CircularProgress,
IconButton,
OutlinedInput,
FormGroup,
FormControlLabel,
Table,
TableContainer,
TableCell,
TableBody,
TableRow,
Paper,
TableHead,
Box
} from '@mui/material';
import CircularLoader from 'components/nearle_components/CircularLoader';
// import RidersPinPointOSM from './RidersPinPointOSM';
import RidersPinPoint from './ridersPinPoint';
const MultipleOrders = () => {
const navigate = useNavigate();
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [appId, setAppId] = useState(0);
const [tenantLocations, setTenantlocations] = useState([]);
const userid = localStorage.getItem('userid');
const tenId = localStorage.getItem('tenantid');
const [tid, setTid] = useState(0);
const [isLocation, setIsLocation] = useState(false);
const [basePrice, setBasePrice] = useState(0);
const [pricePerKm, setPricePerKm] = useState(0);
const [minKm, setMinKm] = useState(0);
const [pickCust, setPickCust] = useState(null);
const [dropCust, setDropCust] = useState([]);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
const [customerlist, setCustomerlist] = useState([]);
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
const [timeslotarr, setTimeslotarr] = useState([]);
const [starttime, setStatrttime] = useState();
const [endtime, setEndtime] = useState();
const [alertmessage, setAlertmessage] = useState('');
const [otherinstructions, setOtherinstructions] = useState('');
const [admintoken, setAdmintoken] = useState();
const [totaldist, settotaldist] = useState(0);
const [totalAmt, settotalAmt] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [showMap, setShowMap] = useState(false);
useEffect(() => {
dropCust && console.log('dropCust', dropCust);
}, [dropCust]);
// =============================================== || opentoast || ===============================================
const opentoast = (message, variant, time) => {
enqueueSnackbar(message, {
variant: variant,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time ? time : 1500
});
console.log(alertmessage);
};
// ==============================|| fetchAppLocations ||============================== //
const fetchAppLocations = async () => {
try {
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
console.log('fetchAppLocations', locationRes.data.details);
} catch (err) {
console.log('locationRes', err);
}
};
useEffect(() => {
fetchAppLocations();
}, []);
// ============================================= || fetchTenantPricing || =============================================
const fetchTenantPricing = async (id) => {
try {
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tenId}`);
console.log('pricingResponse', pricingResponse.data.details);
setBasePrice(pricingResponse.data.details.baseprice);
setPricePerKm(pricingResponse.data.details.priceperkm);
setMinKm(pricingResponse.data.details.minkm);
} catch (error) {
console.log('fetchTenantPricing error', error);
}
};
useEffect(() => {
fetchTenantPricing();
}, []);
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
console.log('gettenantlocations', res.data.details);
if (res.data.details.length == 1) {
setIsLocation(true);
setTenantlocations(res.data.details);
setPickCust(res.data.details[0]);
} else {
setTenantlocations(res.data.details);
}
} catch (err) {
console.log('gettenantlocations', err);
}
};
useEffect(() => {
gettenantlocations(tenId);
}, []);
// ========================================================= || clientdetails || =========================================================
const clientdetails = async () => {
try {
let url =
searchCustList == ''
? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenId}&pageno=1&pagesize=10`
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenId}&keyword=${searchCustList}`;
await axios
.get(url)
.then((res) => {
if (res.data.status) {
console.log('clientdetails', res.data.details);
setCustomerlist(res.data.details);
let arr = [];
res.data.details.map((val) => {
arr.push({
label: `${val.firstname} | ${val.contactno}`,
...val
});
});
}
})
.catch((err) => {
console.log(err);
opentoast('server error', 'warning');
});
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (tenId) {
clientdetails();
}
}, [searchCustList.length > 3, searchCustList == '', tenId]);
// ========================================================= || calculateTotal(dist , charge) || =========================================================
const calculateTotal = () => {
let a1 = 0;
let a2 = 0;
dropCust?.map((customer) => {
a1 += customer.distance;
a2 += customer.totalcharge;
});
settotaldist(a1);
settotalAmt(a2);
};
useEffect(() => {
dropCust && calculateTotal();
}, [dropCust]);
// ========================================================= || handleCheckboxChange || =========================================================
const handleCheckboxChange = async (event, customer) => {
setIsLoading(true);
console.log('event', event.target.checked);
console.log('customer', customer);
if (event.target.checked) {
// If the checkbox is checked, calculate the distance and add the customer
try {
const obj = await calculateDistance(customer);
console.log('return of calculateDistance', obj);
const { roundedDistance, totalcharge } = obj;
// Create a new customer object with the distance property
const updatedCustomer = {
...customer,
distance: roundedDistance,
totalcharge: totalcharge
};
// Add the updated customer object to dropCust
setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]);
// Log the rounded distance
console.log(`Rounded Distance: ${roundedDistance} km`);
} catch (error) {
console.error('Failed to calculate distance:', error);
}
setIsLoading(false);
} else {
// If the checkbox is unchecked, remove the customer from dropCust
setDropCust((prevDropCust) => {
return prevDropCust.filter((cust) => cust.customerid !== customer.customerid);
});
setIsLoading(false);
}
};
// ========================================================= || calculateDistance || =========================================================
const calculateDistance = async (customer) => {
console.log('Distance calculation starts');
try {
const roundedDistance = await calculateDrivingDistance(pickCust, customer);
const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
return { roundedDistance, totalcharge };
} catch (error) {
console.error('Error calculating distance:', error);
throw error;
}
};
// ==================================================== || fetchTiming || ====================================================
const fetchTiming = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
.then((res) => {
console.log('fetchTiming', res);
const { opentime, closetime, latitude, longitude, radius } = res.data.details[0];
if (res.data.status) {
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
let arr = [];
for (
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
j++, i = dayjs(i).add(30, 'm')
) {
arr.push(i);
}
console.log('setTimeslotarr', arr);
setTimeslotarr(arr);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (appId) {
fetchTiming();
}
}, [starttime, endtime, appId]);
const fetchAppAdminTokens = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`)
.then((res) => {
const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); // fcm => firebase cloud messaging
console.log('fetchAppAdminTokens', res);
console.log('userfcmtokemArray', userfcmtokemArray);
if (res.data.status) {
setAdmintoken(userfcmtokemArray);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (starttime && endtime) {
fetchAppAdminTokens();
}
}, [starttime, endtime]);
useEffect(() => {
console.log('pickCust', pickCust);
}, [pickCust]);
// ==================================================== || fetchtenantinfo || ====================================================
const fetchtenantinfo = async () => {
setLoading(true);
console.log('tid', tid);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => {
console.log('fetchtenantinfo', res);
if (res.data.status) {
fetchAppAdminTokens();
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (tid) {
fetchtenantinfo();
}
}, [tid]);
// ================================================== || sendnotifications || ==================================================
const sendnotifications = async () => {
setLoading(true);
await axios
.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
priority: 'high',
registration_ids: admintoken,
data: {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
},
notification: {
title: 'Nearle Merchant',
body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring'
}
})
.then((res) => {
console.log(res);
if (res.data.message == 'Success') {
enqueueSnackbar('Notification sent Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
setLoading(false);
});
};
// =============================================== || creategrouporders || ===============================================
const creategrouporders = async () => {
const arr = dropCust?.map((customer) => ({
applocationid: pickCust.applocationid,
cancellled: '',
// categoryid: +tenant.categoryid,
configid: 9,
customerid: customer.customerid,
deliveryaddress: customer.address || '',
deliverycharge: +customer.totalcharge || 0,
deliverycity: customer.city || '',
deliverycontactno: customer.contactno || '',
deliverycustomer: customer.firstname || '',
deliveryid: +customer.customerid,
deliverylandmark: customer.landmark || '',
deliverylat: customer.latitude,
deliverylocation: customer.suburb || '',
deliverylocationid: customer.deliverylocationid || 0,
deliverylong: customer.longitude,
// deliverytime: `${dayjs(startdate).format('YYYY-MM-DD HH:mm:ss')} `,
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
deliverytype: 'B',
delivered: '',
itemcount: 1,
kms: customer.distance.toString() || 0,
locationid: +pickCust.locationid,
moduleid: +pickCust.moduleid,
orderamount: +customer.totalcharge || 0,
ordercharges: 0.0,
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
orderheaderid: 0,
orderid: '', //
ordernotes: otherinstructions,
orderstatus: 'created',
ordervalue: +customer.totalcharge || 0,
partnerid: pickCust.partnerid,
partneruserid: +userid,
paymentstatus: 1,
paymenttype: 42,
pending: '',
pickupaddress: pickCust.address || '',
pickupcity: pickCust.locationcity || '',
pickupcontactno: pickCust.contactno || '',
pickupcustomer: pickCust.locationname || '',
pickuplandmark: pickCust.landmark || '',
pickuplat: pickCust.latitude,
pickuplocation: pickCust.suburb || '',
pickuplocationid: pickCust.locationid || 0,
pickuplong: pickCust.longitude,
processing: '',
ready: '',
remarks: '',
taxamount: 0.0,
tenantid: pickCust.tenantid,
tenantuserid: 0
}));
console.log('arr', arr);
if (!tenId) {
opentoast('Choose Client ', 'warning');
} else {
setLoading(true);
await axios
.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr)
.then((res) => {
if (res.data.status) {
enqueueSnackbar('Order Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
if (admintoken) {
// notifyadmin(admintoken);
sendnotifications();
}
navigate('/nearle/orders');
} else {
opentoast(res.data.message, 'warning');
}
setLoading(false);
console.log(res);
})
.catch((err) => {
console.log(err);
// opentoast(err.data.message, 'warning');
setLoading(false);
});
}
console.log(arr);
};
return (
<>
{loading && <Loader />}
{/* <RidersPinPointOSM /> */}
<Grid container sx={{ mb: 2 }}>
<Grid item xs={12} sm={3} md={6}>
<Stack>
<Typography variant="h3" whiteSpace="nowrap">
Multiple Orders
</Typography>
</Stack>
</Grid>
<Grid item xs={12} sm={9} md={6}>
<Stack
sx={{}}
width={'100%'}
direction="row"
alignItems="center"
spacing={2}
justifyContent={'flex-end'}
flexWrap={{ xs: 'wrap', custom550: 'nowrap' }}
gap={2}
>
{/* Business Location */}
<Stack sx={{ width: '100%' }}>
{tenantLocations?.length === 1 ? (
<TextField
label="Business Location"
fullWidth
focused
value={tenantLocations[0]?.locationname}
InputProps={{
style: { color: theme.palette.primary.main },
startAdornment: (
<InputAdornment position="start">
<MyLocationIcon color="primary" />
</InputAdornment>
)
}}
/>
) : (
<Autocomplete
fullWidth
options={tenantLocations || []}
getOptionLabel={(option) => `${option.locationname} (${option.suburb})`}
onChange={(event, value, reason) => {
if (value) {
setTid(value.tenantid);
setIsLocation(true);
setPickCust(value);
}
if (reason === 'clear') setIsLocation(false);
}}
renderInput={(params) => <TextField {...params} label="Select Business Location" color="primary" fullWidth />}
/>
)}
</Stack>
{/* Date Picker */}
<Stack sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
format="DD-MM-YYYY"
disablePast
value={dayjs(startdate)}
sx={{ width: 150 }}
onChange={(e) => {
let diff = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd');
if (diff <= 0) {
setStartdate(e);
let arr = [];
timeslotarr.forEach((val) => {
if (dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) {
arr.push(val);
}
});
if (arr[0]) {
setOrderarr([
{
sno: 1,
address: '',
customerid: '',
deliverytime: dayjs(arr[0]),
deliverylocationid: '',
clientname: '',
contactno: '',
latitude: '',
longitude: ''
}
]);
} else {
setOrderarr([]);
}
} else {
opentoast('choose Upcoming Date', 'warning');
setStartdate(NaN);
}
}}
/>
</LocalizationProvider>
</Stack>
</Stack>
</Grid>
</Grid>
{/* ===================================================== || Pickup || ===================================================== */}
{pickCust && (
<TableContainer component={Paper} sx={{ mb: 2 }}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Pickup Location</TableCell>
<TableCell>Address</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>{pickCust?.locationname}</TableCell>
<TableCell>{pickCust?.address}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
)}
{/* ===================================================== || Drop || ===================================================== */}
<MainCard
sx={{ height: '100%' }}
title={`Drop (${dropCust?.length || 0})`}
secondary={
<Button
variant="outlined"
size="small"
sx={{
'&:hover': {
bgcolor: theme.palette.primary.main,
color: 'white'
}
}}
onClick={() => {
if (!isLocation) {
opentoast('Select Business Location', 'warning');
} else {
setIsCustomerOpen(true);
setSearchCustList('');
}
}}
>
Select Customers
</Button>
}
>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Address</TableCell>
<TableCell>Kms</TableCell>
<TableCell align="right">Charge</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{!dropCust && (
<TableRow>
<TableCell colSpan={6}>
<Empty description={' Drop Customers Not Selected'} />
</TableCell>
</TableRow>
)}
{dropCust?.map((customer, index) => (
<TableRow key={index}>
<TableCell>{index + 1}</TableCell>
<TableCell>{customer.firstname}</TableCell>
<TableCell>{customer.address}</TableCell>
<TableCell>{customer.distance}</TableCell>
<TableCell align="right">{`${customer.totalcharge}.00`}</TableCell>
<TableCell align="center">
{
<CloseOutlined
style={{ cursor: 'pointer', color: 'red' }}
onClick={(event) => handleCheckboxChange(event, customer)}
/>
}
</TableCell>
</TableRow>
))}
{dropCust?.length != 0 && (
<TableRow>
<TableCell>
<Typography variant="h5">Total</Typography>
</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
<Typography variant="h5">{`${totaldist} `}</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="h5"> {`${totalAmt}.00`}</Typography>
</TableCell>
<TableCell></TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</MainCard>
{/* ================================================= || Riders Map || ================================================= */}
{/* {showMap && dropCust.length >= 1 && <RidersPinPoint pickCust={pickCust} dropCust={dropCust} />} */}
{/* ================================================= || Notes || ================================================= */}
{dropCust && (
<MainCard sx={{ mt: 2 }} title={'Notes'}>
<Grid container>
<Grid item xs={12}>
<TextField
focused
id="outlined-multiline-static"
sx={{ width: '100%', height: '100%', mb: 2 }}
multiline
rows={1}
placeholder="Notes"
value={otherinstructions}
onChange={(e) => setOtherinstructions(e.target.value)}
/>
</Grid>
<Stack direction="row" justifyContent={'end'} sx={{ mt: 2, width: '100%' }}>
<Button
disabled={dropCust?.length == 0}
size="medium"
variant="outlined"
onClick={() => {
setLoading(true);
setBtnLoading(true);
creategrouporders();
setTimeout(() => {
setLoading(false);
setBtnLoading(false);
}, 2000);
}}
sx={{
'&:hover': {
transform: 'scale(1.05)',
transition: 'transform 0.3s ease'
}
}}
>
{btnLoading ? <CircularProgress color="primary" size={20} thickness={10} /> : 'Create'}
</Button>
</Stack>
</Grid>
</MainCard>
)}
{/* ============================================= || saved address Dialog || ============================================= */}
<Dialog
open={isCustomerOpen}
onClose={() => {
setIsCustomerOpen(false);
}}
fullWidth
sx={{ minWidth: 'lg' }}
>
{isLoading && <CircularLoader />}
<DialogTitle sx={{ bgcolor: theme.palette.primary.main, color: 'white' }}>
<Stack>
<Typography variant="h4"> {`Select Drop Customers (${dropCust?.length || 0})`}</Typography>
<FormControl
sx={{
width: '100%',
mt: 1
}}
>
<Stack spacing={2} sx={{ py: 0.2 }}>
<OutlinedInput
fullWidth
id="input-search-header"
placeholder="Search"
value={searchCustList}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{
'& .MuiOutlinedInput-input': {
p: '10.5px 0px 12px'
},
bgcolor: 'white'
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small' }} />
</InputAdornment>
}
endAdornment={
<IconButton
sx={{ visibility: searchCustList ? 'visible' : 'hidden' }}
onClick={() => {
setSearchCustList('');
}}
>
<ClearIcon />
</IconButton>
}
autoComplete="off"
/>
</Stack>
</FormControl>
</Stack>
</DialogTitle>
<Divider />
<DialogContent sx={{ p: 2.5 }}>
{customerlist.length == 0 ? (
<Stack spacing={2} direction={'row'} alignItems={'center'} justifyContent={'center'} sx={{ minHeight: 600, maxHeight: 600 }}>
<Empty />
</Stack>
) : (
<Stack spacing={2} sx={{ minHeight: 600, maxHeight: 600 }}>
{customerlist &&
customerlist.map((customer, index) => (
<FormGroup key={index}>
<FormControlLabel
control={
<Checkbox
checked={dropCust?.some((cust) => cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust`
onChange={(event) => handleCheckboxChange(event, customer)}
/>
}
label={
<div style={{ width: '100%' }}>
<Typography variant="subtitle1" sx={{ textAlign: 'left' }}>
{`${customer.firstname} (${customer.contactno})`}
</Typography>
<Typography variant="body2" color="secondary" sx={{ textAlign: 'left' }}>
{customer.address}
</Typography>
</div>
}
/>
</FormGroup>
))}
</Stack>
)}
</DialogContent>
<Divider />
<DialogActions sx={{ p: 2.5 }}>
<Button
color={dropCust?.length !== 0 ? 'primary' : 'error'}
variant="outlined"
sx={{
'&:hover': {
bgcolor: dropCust?.length !== 0 ? theme.palette.primary.main : theme.palette.error.main,
color: 'white'
}
}}
onClick={() => {
setIsCustomerOpen(false);
{
dropCust?.length !== 0 && setShowMap(true);
}
}}
>
{dropCust?.length !== 0 ? 'Continue' : 'Close'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default MultipleOrders;

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import Loader from 'components/Loader';
import { useEffect, useState, useRef, Fragment } from 'react';
@@ -506,7 +507,7 @@ const MultipleOrders = () => {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
},
notification: {
title: 'Nearle Merchant',
title: 'Doormile Merchant',
body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring'
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import * as React from 'react';
import { useEffect, useState, useRef, Fragment } from 'react';
import {
@@ -786,7 +787,7 @@ const Createorder1 = () => {
// notifyadmin(admintoken);
sendnotifications();
}
navigate('/nearle/orders');
navigate('/doormile/orders');
} else {
opentoast('Error in creating orders', 'warning');
}
@@ -837,7 +838,7 @@ const Createorder1 = () => {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
},
notification: {
title: 'Nearle Merchant',
title: 'Doormile Merchant',
body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring'
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import {
Autocomplete,
Button,
@@ -200,7 +201,7 @@ const OptimisedOrderPreview = () => {
onSettled: () => {
setTimeout(() => {
setIsLoading(false);
navigate('/nearle/deliveries');
navigate('/doormile/deliveries');
}, 2000);
}
});
@@ -279,7 +280,7 @@ const OptimisedOrderPreview = () => {
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
<IconButton
onClick={() => navigate('/nearle/orders')}
onClick={() => navigate('/doormile/orders')}
sx={{
bgcolor: 'action.hover',
'&:hover': { bgcolor: 'action.selected' }
@@ -419,7 +420,7 @@ const OptimisedOrderPreview = () => {
<MobileCardList>
{orders.map((val, i) => {
const typeAccent =
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#662582';
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#C01227';
return (
<MobileCard key={i} accent={typeAccent}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
@@ -719,7 +720,7 @@ const OptimisedOrderPreview = () => {
color="secondary"
startIcon={<ArrowBackIcon />}
onClick={() => {
navigate('/nearle/orders');
navigate('/doormile/orders');
}}
>
Back

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
/* eslint-disable no-unused-vars */
import { LoadScriptNext, GoogleMap, Marker } from '@react-google-maps/api';
// distance function
function distance(lat1, lng1, lat2, lng2) {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLng = (lng2 - lng1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
const containerStyle = {
width: '100%',
height: '300px'
};
export default function RidersPinPoint({ pickCust, dropCust }) {
// Ensure valid lat/lng
const center = pickCust?.latitude && pickCust?.longitude ? { lat: Number(pickCust.latitude), lng: Number(pickCust.longitude) } : null;
// If center missing, don't render map
if (!center) return null;
const sortedRiders = dropCust
?.map((r) => ({
...r,
distance: distance(center.lat, center.lng, Number(r.latitude), Number(r.longitude))
}))
.sort((a, b) => a.distance - b.distance);
return (
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
<GoogleMap mapContainerStyle={containerStyle} zoom={11} center={center}>
<Marker position={center} icon={{ url: 'http://maps.google.com/mapfiles/ms/icons/purple-dot.png' }} />
{sortedRiders?.map((r, index) => (
<Marker
key={index}
position={{ lat: Number(r.latitude), lng: Number(r.longitude) }}
label={{
text: (index + 1).toString(),
color: 'white',
fontSize: '14px',
fontWeight: 'bold'
}}
/>
))}
</GoogleMap>
</LoadScriptNext>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react';
import { LoadScriptNext, GoogleMap } from '@react-google-maps/api';
import { DT } from 'themes/dt/tokens';
const containerStyle = {
width: '100%',
@@ -46,7 +47,7 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
const route = new window.google.maps.Polyline({
path: numericCoordinates,
geodesic: false,
strokeColor: '#1A73E8',
strokeColor: DT.brand,
strokeOpacity: 1.0,
strokeWeight: 4
});
@@ -69,11 +70,13 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
right: 10,
zIndex: 999,
padding: '6px 12px',
background: '#1A73E8',
background: DT.brand,
color: 'white',
borderRadius: 6,
borderRadius: DT.radiusInner,
cursor: 'pointer',
border: 'none'
border: 'none',
fontWeight: 600,
boxShadow: DT.shadowMd
}}
>
Close

View File

@@ -1,76 +0,0 @@
import { Button } from '@mui/material';
import { LoadScriptNext, GoogleMap, Marker, OverlayView } from '@react-google-maps/api';
const containerStyle = {
width: '100%',
height: 'calc(100vh - 150px)'
};
export default function RiderLocationMap({ riderLocations }) {
console.log('riderLocations', riderLocations);
const center = {
lat: Number(riderLocations?.[0]?.latitude || 11.0056),
lng: Number(riderLocations?.[0]?.longitude || 76.9661)
};
const GreenIcon = {
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
scaledSize: new window.google.maps.Size(25, 41),
anchor: new window.google.maps.Point(12, 41)
};
const RedIcon = {
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
scaledSize: new window.google.maps.Size(25, 41),
anchor: new window.google.maps.Point(12, 41)
};
return (
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
<GoogleMap mapContainerStyle={containerStyle} zoom={12} center={center}>
{riderLocations &&
riderLocations?.map((r, index) => {
const lat = Number(r.latitude);
const lng = Number(r.longitude);
return (
<div key={index}>
{/* Marker */}
<Marker
position={{ lat, lng }}
icon={r.status == 'active' ? GreenIcon : RedIcon}
label={{
fontSize: '14px',
fontWeight: 'bold'
}}
/>
<OverlayView position={{ lat, lng }} mapPaneName={OverlayView.OVERLAY_LAYER}>
<div
style={{
background: 'none',
color: 'green',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 600,
whiteSpace: 'nowrap',
transform: 'translate(-50%, -140%)',
pointerEvents: 'none',
ml: 20
}}
>
<Button variant="contained" color="primary" size="small">
{` ${r.username} `}
{/* <br /> */}
{/* {`${r.contactno || '##### ##### '} `} */}
<br />
{`(${r.orderid || ''}) `}
</Button>
</div>
</OverlayView>
</div>
);
})}
</GoogleMap>
</LoadScriptNext>
);
}

View File

@@ -107,7 +107,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
// brand purple to match the planned-route polyline below.
const stepIcon = (n, isFocused) => {
const size = isFocused ? 38 : 32;
const color = isFocused ? '#4D1C61' : '#662582';
const color = isFocused ? '#910E1D' : '#C01227';
const svg = encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="${size}" height="${size}">` +
`<circle cx="16" cy="16" r="14" fill="${color}" stroke="white" stroke-width="3"/>` +
@@ -126,7 +126,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
px: 2,
py: 1.25,
borderBottom: '1px solid rgba(15, 23, 42, 0.08)',
background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)',
background: 'linear-gradient(135deg, #C01227 0%, #D25463 100%)',
color: '#fff',
flexShrink: 0
}}
@@ -207,12 +207,12 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
{/* Translucent backdrop so the route stays legible on busy tiles. */}
<Polyline
path={routePath}
options={{ strokeColor: '#662582', strokeOpacity: 0.25, strokeWeight: 8 }}
options={{ strokeColor: '#C01227', strokeOpacity: 0.25, strokeWeight: 8 }}
/>
{/* Road-following planned route from the Directions API. */}
<Polyline
path={routePath}
options={{ strokeColor: '#662582', strokeOpacity: 0.95, strokeWeight: 4 }}
options={{ strokeColor: '#C01227', strokeOpacity: 0.95, strokeWeight: 4 }}
/>
</>
) : (
@@ -221,7 +221,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
<Polyline
path={dropPath}
options={{
strokeColor: '#662582',
strokeColor: '#C01227',
strokeOpacity: 0,
strokeWeight: 0,
icons: [
@@ -229,7 +229,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
icon: {
path: 'M 0,-1 0,1',
strokeOpacity: 0.6,
strokeColor: '#662582',
strokeColor: '#C01227',
scale: 3
},
offset: '0',

View File

@@ -5,8 +5,8 @@ import 'leaflet/dist/leaflet.css';
import dayjs from 'dayjs';
import { Chip, Stack, Typography, Box } from '@mui/material';
import { CloseCircleOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
import CircularLoader from 'components/CircularLoader';
import { DT } from 'themes/dt/tokens';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
@@ -33,7 +33,6 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
console.log('additionalProps', additionalProps);
const mapRef = useRef(null);
const theme = useTheme();
const [routePoints, setRoutePoints] = useState([]);
const [loading, setLoading] = useState(false);
@@ -145,13 +144,13 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
top: 12,
right: 12,
zIndex: 2000,
bgcolor: theme.palette.error.main,
bgcolor: DT.brand,
color: '#fff',
fontWeight: 600,
borderRadius: '12px',
borderRadius: DT.radiusInner + 'px',
px: 1.5,
py: 0.5,
boxShadow: theme.shadows[4],
boxShadow: DT.shadowMd,
cursor: 'pointer',
'& .MuiChip-icon': { color: '#fff' }
}}
@@ -183,7 +182,7 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
width: '100%',
bgcolor: 'rgba(255,255,255,0.96)',
p: 2,
boxShadow: theme.shadows[3],
boxShadow: DT.shadowPop,
zIndex: 1500
}}
>

View File

@@ -114,8 +114,8 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND_LIGHT = '#9255AB';
const BRAND = '#C01227';
const BRAND_LIGHT = '#D25463';
const SoftPaper = (props) => (
<Paper
@@ -153,7 +153,7 @@ const pillFieldSx = (color) => ({
'& 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 }
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
}
});
@@ -1104,15 +1104,15 @@ export default function OrdersDetails() {
flexShrink: 0,
cursor: 'pointer',
borderRadius: 999,
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface,
border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
bgcolor: active ? '#C01227' : DT.surface,
color: active ? '#fff' : DT.textSecondary,
fontWeight: 600,
boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': {
borderColor: active ? meta.color : '#cbd5e1',
bgcolor: active ? meta.color : DT.surfaceAlt
borderColor: active ? '#C01227' : '#cbd5e1',
bgcolor: active ? '#C01227' : DT.surfaceAlt
}
}}
>
@@ -1172,7 +1172,7 @@ export default function OrdersDetails() {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
@@ -1817,10 +1817,21 @@ export default function OrdersDetails() {
</TableBody>
</Table>
)}
<Divider />
<Divider sx={{ minWidth: { xs: '100%', md: 1600 } }} />
{rows?.length !== 0 && (
<Stack justifyContent="center" alignItems="center" sx={{ width: '100%', py: 2 }}>
<Stack ref={loadMoreRef} style={{ textAlign: 'center', width: '100%' }}>
<Box sx={{ minWidth: { xs: '100%', md: 1600 }, py: 2 }}>
<Stack
ref={loadMoreRef}
alignItems="center"
justifyContent="center"
sx={{
position: 'sticky',
left: 0,
width: '100%',
maxWidth: '100vw',
textAlign: 'center'
}}
>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
@@ -1829,7 +1840,7 @@ export default function OrdersDetails() {
</Typography>
)}
</Stack>
</Stack>
</Box>
)}
</TableContainer>
</Paper>

View File

@@ -87,8 +87,8 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND_LIGHT = '#9255AB';
const BRAND = '#C01227';
const BRAND_LIGHT = '#D25463';
const SoftPaper = (props) => (
<Paper
@@ -190,7 +190,7 @@ const pillFieldSx = (color) => ({
'& 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 }
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
}
});
@@ -588,7 +588,7 @@ export default function OrdersReport() {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>

View File

@@ -1,790 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useInfiniteQuery } from '@tanstack/react-query';
import {
Avatar,
Box,
Chip,
Grid,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdMyLocation,
MdCalendarMonth,
MdPerson,
MdOutlineCurrencyRupee,
MdStraighten,
MdPayments,
MdRoute,
MdTrendingUp,
MdTrendingDown
} from 'react-icons/md';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { fetchDeliveries } from 'pages/api/api';
import Loader from 'components/Loader';
import DateFilterDialog from 'components/DateFilterDialog';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
const DT = {
radiusPill: 999,
radiusCard: 14,
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const aColor = (c, suffix) => `${c}${suffix}`;
const soft = (c) => aColor(c, '18');
const tint = (c) => aColor(c, '08');
const edge = (c) => aColor(c, '55');
const ring = (c) => aColor(c, '26');
const BRAND = '#662582';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
SoftPaper.propTypes = {
children: PropTypes.node
};
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
AccentAvatar.propTypes = {
color: PropTypes.string.isRequired,
selected: PropTypes.bool,
size: PropTypes.number,
children: PropTypes.node
};
const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => (
<Tooltip title={tooltip || ''} placement="top">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(color)}`,
color,
fontSize: 11,
fontWeight: 800,
minWidth,
justifyContent: 'center',
whiteSpace: 'nowrap'
}}
>
{icon}
{label}
</Box>
</Tooltip>
);
MetricPill.propTypes = {
color: PropTypes.string.isRequired,
icon: PropTypes.node,
label: PropTypes.string.isRequired,
tooltip: PropTypes.string,
minWidth: PropTypes.number
};
const BATCHES = [
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
];
const getBatchForHour = (h, batches = BATCHES) => {
for (const b of batches) {
if (h >= b.startHour && h < b.endHour) return b.id;
}
return null;
};
const getRowBatch = (r, batches = BATCHES) => {
const t = r?.assigntime;
if (!t) return null;
const str = String(t).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
const d = dayjs(t);
if (!d.isValid()) return null;
return getBatchForHour(d.hour() + d.minute() / 60, batches);
};
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
}
export default function ProfitabilityReport() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [locaName, setLocoName] = useState('All');
const [open, setOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [appId, setAppId] = useState(0);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const liveUserid = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
// Load slots configuration from localStorage to match Dispatch page edits
const customBatches = useMemo(() => {
if (typeof window === 'undefined') return BATCHES;
try {
const raw = window.localStorage.getItem('dispatch.slots.v9');
if (!raw) return BATCHES;
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== BATCHES.length) return BATCHES;
return parsed.map((s, i) => {
const id = s.id || `slot-${i + 1}`;
const startHour = Number(s.startHour) || 0;
const endHour = Number(s.endHour) || 24;
return {
id,
name: s.name || BATCHES.find((b) => b.id === id)?.name || `Slot ${i + 1}`,
startHour,
endHour
};
});
} catch (e) {
return BATCHES;
}
}, []);
// Fetch all deliveries for the selected date range and zone
const {
data: deliveriesData,
isLoading: isLoadingDeliveries,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['fetchdeliveries', appId, liveUserid, 'all', startdate, enddate, 2000, '', 0, 0, 0],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
refetchOnWindowFocus: false
});
// Auto-page through all results
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
// Flatten and deduplicate deliveries by orderid
const liveRows = useMemo(() => {
const all = (deliveriesData?.pages || []).flatMap((p) => p.rows || []);
const seen = new Set();
const out = [];
for (const r of all) {
const key = r.orderid != null ? String(r.orderid) : null;
if (key && seen.has(key)) continue;
if (key) seen.add(key);
out.push(r);
}
return out;
}, [deliveriesData]);
// Group deliveries by rider
const ridersList = useMemo(() => {
const riderMap = {};
liveRows.forEach((r) => {
const key = String(r.userid || r.rider_id || '');
if (!key || key === 'unassigned' || key === '0') return;
if (!riderMap[key]) {
riderMap[key] = {
id: key,
riderName: r.ridername || r.rider_name || r.username || `Rider ${key}`,
orders: []
};
}
if (!riderMap[key].orders.some((existing) => existing.orderid === r.orderid)) {
riderMap[key].orders.push(r);
}
});
return Object.values(riderMap)
.map((r) => ({
...r,
orders: [...r.orders].sort((a, b) => {
const tA = a.trip_number || 1;
const tB = b.trip_number || 1;
if (tA !== tB) return tA - tB;
return (a.step || 0) - (b.step || 0);
})
}))
.sort((a, b) => b.orders.length - a.orders.length);
}, [liveRows]);
// Calculate profitability metrics for all riders
const stats = useMemo(() => {
let activeRiders = 0;
let profitableRiders = 0;
let lossRiders = 0;
let totalKms = 0;
let totalPlannedKms = 0;
let totalActualKms = 0;
const list = ridersList
.map((r) => {
let rRevenue = 0;
let rKms = 0;
let rPlannedKms = 0;
let rActualKms = 0;
const slotsByDate = {};
let ordersInSlots = 0;
r.orders.forEach((o) => {
const status = String(o.orderstatus || '').toLowerCase();
if (status === 'cancelled' || status === 'skipped') return;
const slot = getRowBatch(o, customBatches);
if (!slot) return;
const oKms = parseFloat(o.riderkms || 0);
rKms += oKms;
rPlannedKms += parseFloat(o.kms || 0);
rActualKms += parseFloat(o.actualkms || 0);
rRevenue += oKms <= 8 ? 30 : 30 + (oKms - 8) * 6;
const dateStr = o.assigntime
? dayjs(o.assigntime).format('YYYY-MM-DD')
: o.deliverydate
? dayjs(o.deliverydate).format('YYYY-MM-DD')
: null;
if (!dateStr) return;
if (!slotsByDate[dateStr]) {
slotsByDate[dateStr] = new Set();
}
slotsByDate[dateStr].add(slot);
ordersInSlots++;
});
if (ordersInSlots === 0) {
return null;
}
// Sum unique slots per day, capping at 3 slots max per day
let slotCount = 0;
Object.values(slotsByDate).forEach((set) => {
slotCount += Math.min(set.size, 3);
});
const rVarCost = rKms * 2.5;
const rFixedCost = slotCount * (500 / 3);
const rTotalCost = rVarCost + rFixedCost;
const rNet = rRevenue - rTotalCost;
const rMargin = rRevenue > 0 ? (rNet / rRevenue) * 100 : 0;
if (rNet >= 0) {
profitableRiders++;
} else {
lossRiders++;
}
totalKms += rKms;
totalPlannedKms += rPlannedKms;
totalActualKms += rActualKms;
activeRiders++;
return {
...r,
orderCount: ordersInSlots,
kms: rKms,
plannedKms: rPlannedKms,
actualKms: rActualKms,
revenue: rRevenue,
varCost: rVarCost,
fixedCost: rFixedCost,
totalCost: rTotalCost,
net: rNet,
margin: rMargin
};
})
.filter(Boolean);
return {
activeRiders,
profitableRiders,
lossRiders,
totalKms,
totalPlannedKms,
totalActualKms,
enrichedRiders: list
};
}, [ridersList, customBatches]);
// Filter riders by search query
const filteredRiders = useMemo(() => {
if (!stats?.enrichedRiders || !Array.isArray(stats.enrichedRiders)) return [];
const baseList = stats.enrichedRiders.filter(Boolean);
if (!debouncedSearch) return baseList;
const q = debouncedSearch.toLowerCase().trim();
return baseList.filter(
(r) => r && [r.riderName, String(r.id)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))
);
}, [stats?.enrichedRiders, debouncedSearch]);
const KPI_META = [
{
key: 'riders',
label: 'Riders Active',
color: BRAND,
icon: MdPerson,
value: stats?.activeRiders ?? 0
},
{
key: 'planned-kms',
label: 'Planned KMs',
color: '#0ea5e9',
icon: MdRoute,
value: `${(stats?.totalPlannedKms ?? 0).toFixed(1)} km`
},
{
key: 'actual-kms',
label: 'Actual KMs',
color: '#f59e0b',
icon: MdMyLocation,
value: `${(stats?.totalActualKms ?? 0).toFixed(1)} km`
},
{
key: 'rider-kms',
label: 'Rider KMs',
color: BRAND,
icon: MdStraighten,
value: `${(stats?.totalKms ?? 0).toFixed(1)} km`
},
{
key: 'total-distance',
label: 'Trip KMs',
color: '#10b981',
icon: MdStraighten,
value: `${(stats?.totalKms ?? 0).toFixed(1)} km`
}
];
return (
<>
{(isLoadingDeliveries || isFetchingNextPage) && <Loader />}
{/* Page Header */}
<PageHeader
title="Profitability Report"
subtitle={`Live · ${locaName || 'All Zones'} · ${datestatus}`}
live
action={
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* KPI Cards Grid */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={4} md={2.4}>
<StatCard
title={item.label}
value={item.value ?? 0}
icon={<Icon size={20} />}
color={item.color}
loading={isLoadingDeliveries}
/>
</Grid>
);
})}
</Grid>
{/* Filter Bar (date + search) */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent="space-between"
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1.25} flexWrap="wrap">
<AccentAvatar color={BRAND} size={32}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Profitability Overview · {datestatus}
</Typography>
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
{filteredRiders.length} riders · {stats.profitableRiders} profitable · {stats.lossRiders} at loss
</Typography>
</Stack>
<Tooltip title="Date Filter" placement="top">
<Box
onClick={() => setOpen(true)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.75,
borderRadius: 999,
cursor: 'pointer',
bgcolor: tint('#f59e0b'),
border: `1.5px solid ${edge('#f59e0b')}`,
color: '#f59e0b',
fontWeight: 800,
fontSize: 12,
ml: 1,
transition: 'all 0.18s',
'&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
}}
>
<MdCalendarMonth size={14} />
{dayjs(startdate).format('DD/MM/YY')} {dayjs(enddate).format('DD/MM/YY')}
</Box>
</Tooltip>
</Stack>
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder="Search riders"
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* Table & Mobile List Container */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{isMobile ? (
<MobileCardList scroll>
{!filteredRiders || filteredRiders.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdPerson size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
</Stack>
) : (
filteredRiders.map((row, index) => {
if (!row) return null;
const isProfit = (row.net ?? 0) >= 0;
return (
<MobileCard
key={row.id || index}
accent={isProfit ? '#10b981' : '#ef4444'}
header={
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={isProfit ? '#10b981' : '#ef4444'} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.id}
</Typography>
</Stack>
</Stack>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Orders" value={row.orderCount} />
<MobileField label="Rider KMs" value={`${Math.round(row.kms)} km`} />
<MobileField label="Revenue" value={formatNumberToRupees(row.revenue)} />
<MobileField label="Fixed Cost" value={formatNumberToRupees(row.fixedCost)} />
<MobileField label="Variable Cost" value={formatNumberToRupees(row.varCost)} />
<MobileField label="Total Cost" value={formatNumberToRupees(row.totalCost)} />
<MobileField label="Net Profit" value={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net)}`} full />
<MobileField label="Margin" value={`${Math.abs(row.margin).toFixed(0)}%`} full />
</MobileFieldGrid>
</MobileCard>
);
})
)}
</MobileCardList>
) : (
<TableContainer
sx={{
maxHeight: 'calc(100vh - 280px)',
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: 1000 }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: 11,
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: 1.25,
px: 2
}
}}
>
<TableCell>#</TableCell>
<TableCell>Rider</TableCell>
<TableCell align="center">Orders</TableCell>
<TableCell align="center">Rider KMs</TableCell>
<TableCell align="center">Revenue</TableCell>
<TableCell align="center">Fixed Cost</TableCell>
<TableCell align="center">Variable Cost</TableCell>
<TableCell align="center">Total Cost</TableCell>
<TableCell align="center">Net Profit</TableCell>
<TableCell align="center">Margin</TableCell>
</TableRow>
</TableHead>
<TableBody>
{!filteredRiders || filteredRiders.length === 0 ? (
<TableRow>
<TableCell colSpan={10} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdPerson size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
filteredRiders.map((row, index) => {
if (!row) return null;
const isProfit = (row.net ?? 0) >= 0;
return (
<TableRow
key={row.id || index}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: 1.5,
px: 2
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.id}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell align="center">
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.orderCount}
</Typography>
</TableCell>
<TableCell align="center">
<MetricPill color="#10b981" icon={<MdStraighten size={11} />} label={`${Math.round(row.kms)} km`} tooltip="KMS" />
</TableCell>
<TableCell align="center">
<MetricPill
color={BRAND}
icon={<MdOutlineCurrencyRupee size={11} />}
label={formatNumberToRupees(row.revenue).replace('₹', '').trim()}
tooltip="Revenue"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#6366f1"
icon={<MdPayments size={11} />}
label={formatNumberToRupees(row.fixedCost).replace('₹', '').trim()}
tooltip="Fixed Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#f59e0b"
icon={<MdRoute size={11} />}
label={formatNumberToRupees(row.varCost).replace('₹', '').trim()}
tooltip="Variable Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#94a3b8"
icon={<MdPayments size={11} />}
label={formatNumberToRupees(row.totalCost).replace('₹', '').trim()}
tooltip="Total Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color={isProfit ? '#10b981' : '#ef4444'}
icon={isProfit ? <MdTrendingUp size={11} /> : <MdTrendingDown size={11} />}
label={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net).replace('₹', '').trim()}`}
tooltip="Net Profit"
/>
</TableCell>
<TableCell align="center">
<Chip
label={`${Math.abs(row.margin).toFixed(0)}%`}
color={isProfit ? 'success' : 'error'}
size="small"
sx={{ fontWeight: 700, minWidth: 60 }}
/>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
{/* Date Filter Dialog */}
<DateFilterDialog
open={open}
onClose={() => setOpen(false)}
onSelect={(range) => {
setStartdate(range.startDate);
setEnddate(range.endDate);
setDatestatus(range.label);
}}
/>
</>
);
}

View File

@@ -1,355 +0,0 @@
import React, { useState, useEffect, Fragment } from 'react';
import {
Box,
Drawer,
IconButton,
Toolbar,
Typography,
AppBar,
useMediaQuery,
Divider,
List,
ListItem,
ListItemText,
useTheme,
ListItemAvatar,
Stack,
Button,
Checkbox,
Skeleton
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SearchBar from 'components/nearle_components/SearchBar';
import { useQuery } from '@tanstack/react-query';
import { fetchRidersLogs } from 'pages/api/api';
import RiderLocationMap from './RiderLocationMap';
import MainCard from 'components/MainCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import dayjs from 'dayjs';
import error500 from 'assets/images/maintenance/Error500.png';
const drawerWidth = 350;
const RidersLogs = () => {
const theme = useTheme();
const isDesktop = useMediaQuery('(min-width:900px)');
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [open, setOpen] = useState(false);
const [selectedRiders, setSelectedRiders] = useState([]);
const [riderSearch, setRiderSearch] = useState('');
const appId = 1;
const {
data: riders,
isLoading: ridersIsLoading,
isFetching: riderIsFetching,
refetch: riderLogsRefetch,
error: riderLogsError
} = useQuery({
queryKey: [appId, dayjs().format('YYYY-MM-DD'), riderSearch],
queryFn: fetchRidersLogs,
refetchInterval: 5 * 60 * 1000
});
useEffect(() => {
// const sortedRiders = riders?.sort((a, b) => a.firstname.localeCompare(b.firstname));
setSelectedRiders(riders);
}, [riders]);
useEffect(() => {
console.log('selectedRiders', selectedRiders);
}, [selectedRiders]);
useEffect(() => {
setOpen(isDesktop);
}, [isDesktop]);
return (
<MainCard content={false}>
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative' }}>
{/* Drawer */}
<Drawer
variant={isDesktop ? 'persistent' : 'temporary'}
open={open}
onClose={() => !isDesktop && setOpen(false)}
ModalProps={{ keepMounted: true }}
sx={{
'& .MuiDrawer-paper': {
width: isMobile ? '100vw' : drawerWidth,
maxWidth: isMobile ? '100vw' : drawerWidth,
position: 'absolute',
left: 0,
top: 0,
height: '100%',
overflowY: 'auto',
transition: 'transform 0.35s ease-in-out',
zIndex: 13
}
}}
>
{/* Search */}
<Box sx={{ position: 'sticky', top: 0, zIndex: 1 }}>
<SearchBar
value={riderSearch}
placeholder="Search Rider"
onChange={(e) => setRiderSearch(e.target.value)}
sx={{
height: 60,
bgcolor: 'white',
'& .MuiOutlinedInput-notchedOutline': {
borderBottom: '1px solid',
borderColor: theme.palette.secondary.light
}
}}
/>
<List>
<ListItem sx={{ cursor: 'pointer', '&:hover': { bgcolor: theme.palette.secondary.lighter }, bgcolor: 'white', mt: -1 }}>
<ListItemAvatar>
<Checkbox
checked={riders?.length == selectedRiders?.length}
onChange={(e) => {
if (e.target.checked) {
setSelectedRiders(riders);
}
}}
/>
</ListItemAvatar>
<ListItemText primary="All" />
</ListItem>
<Divider />
</List>
</Box>
{/* Rider List */}
<List>
{/* Individuals */}
{ridersIsLoading || riderIsFetching
? Array.from({ length: 10 }).map((_, index) => (
<Fragment key={index}>
<ListItem sx={{ py: 1.5, px: 2 }}>
<ListItemAvatar>
<Skeleton variant="circular" width={24} height={24} />
</ListItemAvatar>
<ListItemText
primary={<Skeleton variant="text" width="60%" height={22} />}
secondary={<Skeleton variant="text" width="40%" height={18} />}
/>
<Stack spacing={0.5} textAlign="right">
<Skeleton variant="text" width={50} height={18} />
<Skeleton variant="text" width={80} height={16} />
</Stack>
</ListItem>
<Divider />
</Fragment>
))
: !isMobile &&
riders?.map((row) => {
return (
<Fragment key={row.userid}>
<ListItem
sx={{
cursor: 'pointer',
py: 1,
px: 2,
borderRadius: 1,
'&:hover': { bgcolor: theme.palette.secondary.lighter }
}}
secondaryAction={
<Stack textAlign="right" spacing={0.5}>
<Typography variant="body2" noWrap sx={{ color: row.status == 'active' ? 'success.main' : 'error.main' }}>
{row.userid}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')}
</Typography>
</Stack>
}
>
<ListItemAvatar>
<Checkbox
sx={{
color: row.status == 'active' ? 'green' : 'red',
'&.Mui-checked': {
color: row.status == 'active' ? 'green' : 'red'
}
}}
checked={
// INDIVIDUAL CHECKED CONDITION
selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid
}
onChange={(e) => {
if (e.target.checked) {
// SELECT ONE RIDER
setSelectedRiders([row]);
} else {
// UNCHECK -> SELECT ALL
setSelectedRiders(riders);
}
}}
/>
</ListItemAvatar>
<ListItemText
primary={
<Typography noWrap>
{row.username?.slice(0, 25) || ''}
{row.username?.length > 25 && '...'}
{/* {row.status === 'active' && <TaskAltIcon fontSize="small" color="success" sx={{ ml: 1 }} />} */}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary" noWrap>
{row.contactno || '##########'}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
</List>
{/* Mobile: rider rows rendered as app-style cards (same selection behaviour) */}
{isMobile && !ridersIsLoading && !riderIsFetching && (
<MobileCardList>
{riders?.map((row) => {
const isActive = row.status == 'active';
const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid;
return (
<MobileCard
key={row.userid}
accent={isActive ? '#10b981' : '#ef4444'}
selected={isSelected}
header={
<Stack direction="row" alignItems="flex-start" spacing={1}>
<Checkbox
sx={{
p: 0.5,
color: isActive ? 'green' : 'red',
'&.Mui-checked': { color: isActive ? 'green' : 'red' }
}}
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
setSelectedRiders([row]);
} else {
setSelectedRiders(riders);
}
}}
/>
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography noWrap sx={{ fontWeight: 600 }}>
{row.username?.slice(0, 25) || ''}
{row.username?.length > 25 && '...'}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{row.contactno || '##########'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="User ID">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: isActive ? 'success.main' : 'error.main' }} noWrap>
{row.userid}
</Typography>
</MobileField>
<MobileField label="Status" value={isActive ? 'Active' : 'Inactive'} />
<MobileField label="Last Log" value={dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')} full />
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)}
</Drawer>
{/* AppBar */}
<AppBar
elevation={0}
position="absolute"
sx={{
top: 0,
left: open && isDesktop ? `${drawerWidth}px` : 0,
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
transition: 'left 0.3s ease, width 0.3s ease',
backgroundColor: 'white',
borderBottom: '1px solid',
borderColor: theme.palette.secondary.light
}}
>
<Toolbar>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
<Stack direction="row" alignItems="center">
<IconButton color="primary" onClick={() => setOpen(!open)}>
<MenuIcon />
</IconButton>
<Typography variant="h5" color="primary" sx={{ ml: 2 }}>
Riders Locations
</Typography>
</Stack>
<Button
variant="outlined"
color="primary"
onClick={() => {
riderLogsRefetch();
}}
>
Refresh
</Button>
</Stack>
</Toolbar>
</AppBar>
{/* Map */}
<Box
sx={{
flexGrow: 1,
overflow: 'auto',
pt: '64px',
pl: open && isDesktop ? `${drawerWidth}px` : 0,
transition: 'padding-left 0.3s ease',
minHeight: '80vh'
}}
>
{(ridersIsLoading || riderIsFetching) && (
<Box position="relative" width="100%" height="80vh" display="grid" placeItems="center">
{/* <CircularLoader /> */}
<Skeleton
variant="rectangular"
width="100%"
height="100%"
animation="wave"
sx={{
position: 'absolute',
top: 0,
left: 0,
borderRadius: 1,
zIndex: 1
}}
/>
</Box>
)}
{selectedRiders?.length > 0 && <RiderLocationMap riderLocations={selectedRiders} />}
{riderLogsError && (
<Box sx={{ width: '100% ', height: '100%' }}>
<img src={error500} alt="mantis" style={{ height: '100%', width: '100%' }} />
</Box>
)}
</Box>
</Box>
</MainCard>
);
};
export default RidersLogs;

View File

@@ -86,8 +86,8 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND_LIGHT = '#9255AB';
const BRAND = '#C01227';
const BRAND_LIGHT = '#D25463';
const SoftPaper = (props) => (
<Paper
@@ -265,17 +265,9 @@ export default function RidersSummary() {
const getuserdeliverylogs = async (userid) => {
setRouteLoading(true);
try {
// /deliveries/getdeliveries treats applocationid=0 differently from a
// real location id — when appId===0 ("All") the backend expects the
// logged-in operator's userid via appuserid instead. Mirrors the
// branching in api.js#fetchDeliveries.
const loggedInUserId = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
const scopeParam = appId === 0
? `appuserid=${loggedInUserId}`
: `applocationid=${appId}`;
const url =
`${process.env.REACT_APP_URL}/deliveries/getdeliveries/` +
`?${scopeParam}` +
`?applocationid=${appId}` +
`&status=all` +
`&fromdate=${startdate}` +
`&todate=${enddate}` +
@@ -456,7 +448,7 @@ export default function RidersSummary() {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>

View File

@@ -31,9 +31,12 @@ import {
FormLabel,
DialogActions,
useMediaQuery,
useTheme
useTheme,
Paper
} from '@mui/material';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { MdPayments } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
import { Autocomplete as Autocomplete1 } from '@mui/material';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
@@ -202,7 +205,7 @@ const Requests = () => {
const [currenttenantid] = useState('');
const [latlong, setLatlong] = useState({});
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
// const [alertmessage, setAlertmessage] = useState('');
const [alertmessage, setAlertmessage] = useState('');
// const [toast, setToast] = useState(false);
const [rolesarr, setRolesarr] = useState([]);
const [roleslist] = useState([]);
@@ -215,6 +218,12 @@ const Requests = () => {
const [refno, setRefno] = useState('');
const [requestor, setRequestor] = useState('');
const [bankname, setBankname] = useState('');
const [amount, setAmount] = useState('');
const [accountno, setAccountno] = useState('');
const [ifsc, setIfsc] = useState('');
const [reason, setReason] = useState('');
const [expandopen, setExpandopen] = useState('');
const [editexpandopen, setEditexpandopen] = useState('');
useEffect(() => {
setRolesarr([
@@ -902,7 +911,7 @@ const Requests = () => {
<MobileCardList scroll>
{loading &&
[0, 1, 2, 3, 4].map((item) => (
<MobileCard key={item} accent="#662582">
<MobileCard key={item} accent="#C01227">
<Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="circular" width={32} height={32} />
<Stack sx={{ flex: 1 }}>
@@ -923,13 +932,13 @@ const Requests = () => {
return (
<MobileCard
key={row.sno}
accent="#662582"
accent="#C01227"
selected={isItemSelected}
onClick={(event) => handleClick(event, row.sno)}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#66258218', color: '#662582', fontSize: 13 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#C0122718', color: '#C01227', fontSize: 13 }}>
{row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'}
</Avatar>
<Stack sx={{ minWidth: 0 }}>
@@ -942,7 +951,7 @@ const Requests = () => {
</Stack>
</Stack>
{row.amount != null && (
<Chip label={row.amount} size="small" sx={{ bgcolor: '#66258218', color: '#662582', fontWeight: 700 }} />
<Chip label={row.amount} size="small" sx={{ bgcolor: '#C0122718', color: '#C01227', fontWeight: 700 }} />
)}
</Stack>
}
@@ -1813,8 +1822,6 @@ const Requests = () => {
const [searchword, setSearchword] = useState('');
const [dialogopen, setDialogopen] = useState(false);
// const [expandopen, setExpandopen] = React.useState('');
// const setinitial = (val)=>{
// if(val){
@@ -1938,26 +1945,52 @@ const Requests = () => {
xs={12}
// sx={{ mb: -2.25 }}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }}
<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%)`
}}
>
<Typography variant="h3">Payment Requests</Typography>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
// setDialogopen(true)
}}
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }}
>
Create Request
</Button>
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdPayments size={22} />
</Avatar>
<Typography variant="h3">Payment Requests</Typography>
</Stack>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
setDialogopen(true);
}}
>
Create Request
</Button>
</Stack>
</Paper>
</Grid>
<Grid item xs={12}>
<Box sx={{ overflow: 'auto', border: 1, borderColor: 'grey.200', borderRadius: 2, backgroundColor: '#fff', minHeight: 400 }}>
<Box
sx={{
overflow: 'auto',
border: '1px solid',
borderColor: DT.borderSubtle,
borderRadius: DT.radiusCard + 'px',
boxShadow: DT.shadowSoft,
backgroundColor: DT.surface,
minHeight: 400
}}
>
{/* <Box
sx={{
p: 1,

View File

@@ -269,7 +269,7 @@ export default function RiderSubstitution({
bgcolor: BRAND,
color: '#fff',
'&:hover': {
bgcolor: '#4D1C61'
bgcolor: '#910E1D'
}
}
}
@@ -657,7 +657,7 @@ export default function RiderSubstitution({
fontWeight: 700,
textTransform: 'none',
'&:hover': {
bgcolor: '#4D1C61'
bgcolor: '#910E1D'
}
}}
>

View File

@@ -2,7 +2,9 @@ import { useEffect, useState } from 'react';
// material-ui
import { Box, Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
import { Avatar, Box, Button, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
import { MdDirectionsBike } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
// third-party
// import { PatternFormat } from 'react-number-format';
@@ -113,12 +115,6 @@ const Createrider = () => {
});
};
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => {
@@ -160,14 +156,6 @@ const Createrider = () => {
});
const createprofile = async () => {
console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode);
// if (!businessname) {
// opentoast('Fill Business name')
// } else if (!businessno) {
// opentoast('Fill Registration No')
// }
// else
if (!firstname) {
opentoast('Fill Full name');
} else if (!mobilenumber) {
@@ -260,16 +248,27 @@ const Createrider = () => {
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', sm: 'center' }}
spacing={1}
<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%)`
}}
>
<Typography variant="h3">Create Rider</Typography>
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdDirectionsBike size={22} />
</Avatar>
<Typography variant="h3">Create Rider</Typography>
</Stack>
</Paper>
</Grid>
<MainCard>
<MainCard
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
>
<Grid container spacing={3}>
<Grid item xs={12}>
<MainCard

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Avatar,
Box,
Button,
Grid,
@@ -17,6 +18,8 @@ import {
useMediaQuery,
useTheme
} from '@mui/material';
import { MdDirectionsBike } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
@@ -106,9 +109,6 @@ const EditRider = () => {
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => {
console.log(res);
if (res.data.status) {
setTenantinfo(res.data.details);
}
setLoading(false);
})
.catch((err) => {
@@ -231,12 +231,6 @@ const EditRider = () => {
});
};
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => {
@@ -265,7 +259,6 @@ const EditRider = () => {
setCity(city1 || '');
setState(state1 || '');
setZipcode(zipcode1 || '');
setSuburb(suburb1 || '');
// setAddress(place.formatted_address)
@@ -328,7 +321,7 @@ const EditRider = () => {
autoHideDuration: 2000
});
setRiderdata(null);
navigate('/nearle/riders');
navigate('/doormile/riders');
setLoading(false);
} else {
enqueueSnackbar('Update Failed', {
@@ -349,22 +342,33 @@ const EditRider = () => {
</>
)}
<MainCard
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
title={
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }}
sx={{ backgroundColor: 'secondary.lighter', width: '100%', height: '100%', p: 2 }}
sx={{
width: '100%',
height: '100%',
p: 2,
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
}}
>
<Typography variant="h3">Edit Rider </Typography>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 40, height: 40, bgcolor: DT.brand }}>
<MdDirectionsBike size={20} />
</Avatar>
<Typography variant="h3">Edit Rider</Typography>
</Stack>
<Button
startIcon={<ArrowBackIcon />}
variant="outlined"
fullWidth={isMobile}
onClick={() => {
setRiderdata(null);
navigate('/nearle/riders');
navigate('/doormile/riders');
}}
>
Back to Riders
@@ -664,16 +668,6 @@ const EditRider = () => {
starttime: val.starttime,
endtime: val.endtime
});
setBasefare(val.basefare);
setAdditionalkms(val.additionalkm);
setOthercharges(val.additionalcharges);
setShift(val);
} else {
setBasefare('');
setAdditionalkms('');
setOthercharges('');
setShift({});
}
}}
freeSolo
@@ -694,7 +688,6 @@ const EditRider = () => {
...riderdata,
basefare: e.target.value
});
setBasefare(e.target.value);
}}
value={riderdata?.basefare}
autoComplete="off"
@@ -717,7 +710,6 @@ const EditRider = () => {
...riderdata,
additionalkm: e.target.value
});
setAdditionalkms(e.target.value);
}}
value={riderdata?.additionalkm}
autoComplete="off"
@@ -740,7 +732,6 @@ const EditRider = () => {
...riderdata,
additionalcharges: e.target.value
});
setOthercharges(e.target.value);
}}
value={riderdata?.additionalcharges}
autoComplete="off"
@@ -777,7 +768,6 @@ const EditRider = () => {
...riderdata,
accountno: e.target.value
});
setAccountno(e.target.value);
}}
autoComplete="off"
/>
@@ -798,7 +788,6 @@ const EditRider = () => {
...riderdata,
accountname: e.target.value
});
setAccountname(e.target.value);
}}
autoComplete="off"
/>
@@ -823,11 +812,7 @@ const EditRider = () => {
...riderdata,
accounttype: val.label
});
setAccount(val);
setAccountType(val.label);
// fetchroles(val.tenantid);
} else {
setAccount({});
}
}}
freeSolo
@@ -849,7 +834,6 @@ const EditRider = () => {
...riderdata,
bankname: e.target.value
});
setBankname(e.target.value);
}}
autoComplete="off"
/>
@@ -870,7 +854,6 @@ const EditRider = () => {
...riderdata,
ifsccode: e.target.value
});
setIfsc(e.target.value);
}}
autoComplete="off"
/>
@@ -891,7 +874,6 @@ const EditRider = () => {
...riderdata,
branch: e.target.value
});
setBranch(e.target.value);
}}
autoComplete="off"
/>
@@ -924,7 +906,6 @@ const EditRider = () => {
onChange={(e, val) => {
if (val) {
console.log('vehi', val);
setVehicle(val);
setRiderdata({
...riderdata,
vehiclename: val.label,
@@ -932,8 +913,6 @@ const EditRider = () => {
});
// fetchroles(val.tenantid);
} else {
setVehicle({});
}
}}
freeSolo
@@ -974,7 +953,6 @@ const EditRider = () => {
...riderdata,
model: e.target.value
});
setModelyear(e.target.value);
}}
value={riderdata?.model}
autoComplete="off"
@@ -995,7 +973,6 @@ const EditRider = () => {
...riderdata,
color: e.target.value
});
setVehiclecolor(e.target.value);
}}
value={riderdata?.color}
autoComplete="off"
@@ -1054,7 +1031,6 @@ const EditRider = () => {
label="Date"
value={dayjs(riderdata?.insurancedate)}
onChange={(e) => {
setExpirydate(dayjs(e.$d).format('YYYY-MM-DD 00:00:00'));
setRiderdata({
...riderdata,
insurancedate: dayjs(e.$d).format('YYYY-MM-DD 00:00:00')
@@ -1078,16 +1054,16 @@ const EditRider = () => {
sx={{
position: 'sticky',
bottom: 0,
backgroundColor: 'secondary.lighter',
backgroundColor: DT.surfaceAlt,
p: 2,
zIndex: 10,
border: ' 1px solid ',
borderColor: '#E6EBF1',
border: '1px solid',
borderColor: DT.borderSubtle,
borderTop: 'none'
}}
>
<Stack direction={{ xs: 'column-reverse', sm: 'row' }} justifyContent="flex-end" spacing={2}>
<Button startIcon={<ArrowBackIcon />} variant="outlined" fullWidth={isMobile} onClick={() => navigate('/nearle/riders')}>
<Button startIcon={<ArrowBackIcon />} variant="outlined" fullWidth={isMobile} onClick={() => navigate('/doormile/riders')}>
Back to Riders
</Button>
<Button

View File

@@ -77,7 +77,7 @@ import RiderSubstitution from './RiderSubstitution';
// ============================================================================
// Design tokens — shared with the deliveries / tenants / customers pages so
// every surface (header, KPI tiles, table, badges, dialog) speaks the same
// visual language. Brand purple `#662582` is the canonical primary; status
// visual language. Brand purple `#C01227` is the canonical primary; status
// colours are semantic and distinct from the brand.
// ============================================================================
const DT = {
@@ -100,7 +100,7 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND = '#C01227';
const SoftPaper = (props) => (
<Paper
@@ -144,10 +144,10 @@ const STATUS_META = {
// Pill-tab definitions for the rider listing tabs. Keeps brand purple for the
// "ALL" view and emerald for "Active" so the colour matches the count's meaning.
const TAB_META = [
{ key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' },
{ key: 1, label: 'Active', color: '#10b981', icon: MdCheckCircle, countKey: 'active' },
{ key: 2, label: 'Substitutes', color: '#8b5cf6', icon: MdTwoWheeler, countKey: 'substitute' },
{ key: 3, label: 'Substitution History', color: '#f59e0b', icon: MdAccessTime, countKey: 'history' }
{ key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' },
{ key: 1, label: 'Active', color: BRAND, icon: MdCheckCircle, countKey: 'active' },
{ key: 2, label: 'Substitutes', color: BRAND, icon: MdTwoWheeler, countKey: 'substitute' },
{ key: 3, label: 'Substitution History', color: BRAND, icon: MdAccessTime, countKey: 'history' }
];
const KPI_META = (summary) => [
@@ -763,7 +763,7 @@ const Riders = () => {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
@@ -813,7 +813,7 @@ const Riders = () => {
bgcolor: BRAND,
color: '#fff',
'&:hover': {
bgcolor: '#4D1C61'
bgcolor: '#910E1D'
}
}
}
@@ -1244,7 +1244,7 @@ const Riders = () => {
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
onClick={() => {
navigate('/nearle/riders/edit', { state: { riderdata: row } });
navigate('/doormile/riders/edit', { state: { riderdata: row } });
}}
>
<MdEdit size={14} />
@@ -1696,7 +1696,7 @@ const Riders = () => {
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
onClick={() => {
navigate('/nearle/riders/edit', { state: { riderdata: row } });
navigate('/doormile/riders/edit', { state: { riderdata: row } });
}}
>
<MdEdit size={14} />
@@ -1914,7 +1914,7 @@ const Riders = () => {
fontWeight: 600,
bgcolor: BRAND,
color: '#fff',
'&:hover': { bgcolor: '#4D1C61' }
'&:hover': { bgcolor: '#910E1D' }
}}
>
Save Changes

View File

@@ -31,8 +31,8 @@ const DT = {
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const BRAND = '#662582';
const BRAND_LIGHT = '#9255AB';
const BRAND = '#C01227';
const BRAND_LIGHT = '#D25463';
const tint = (c) => `${c}08`;
const soft = (c) => `${c}18`;
@@ -143,7 +143,7 @@ const ViewProfile = () => {
color: '#fff',
fontSize: { xs: 24, md: 28 },
fontWeight: 700,
boxShadow: '0 10px 24px rgba(102, 37, 130, 0.35)'
boxShadow: '0 10px 24px rgba(192, 18, 39, 0.35)'
}}
>
{initialsOf(fullname)}