updates on the redesign page for all the pages
This commit is contained in:
113
src/pages/api/CLAUDE.md
Normal file
113
src/pages/api/CLAUDE.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# CLAUDE.md — `src/pages/api/`
|
||||
|
||||
Rules for editing `api.js`. This is the **central API layer** — every page calls into it. The root `CLAUDE.md` covers project-wide conventions; this file is the scoped rule sheet for the API layer specifically.
|
||||
|
||||
---
|
||||
|
||||
## 1. Function signature patterns
|
||||
|
||||
### TanStack `useQuery` / `useInfiniteQuery` consumers
|
||||
Destructure from `queryKey` in the order the call site declared it. The leading `_` is the query name and is intentionally discarded.
|
||||
|
||||
```js
|
||||
// Plain query — destructure { queryKey }, skip the [0] name slot
|
||||
export const fetchorderscount = async ({ queryKey }) => {
|
||||
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
|
||||
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&...`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.details;
|
||||
};
|
||||
|
||||
// Infinite query — also receives pageParam (default to 1)
|
||||
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
||||
const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
|
||||
const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&...&pageno=${pageParam}&pagesize=${rowsPerPage}`;
|
||||
const response = await axios.get(url);
|
||||
return {
|
||||
rows: response.data.details,
|
||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Hard rules:**
|
||||
- The query-key array order at the call site MUST match the destructure order here. Re-ordering one without the other silently breaks every caller.
|
||||
- Infinite queries return `{ rows, nextPage }`. `nextPage` is `undefined` when the page size wasn't filled (signals end-of-stream to `getNextPageParam`).
|
||||
- Some legacy functions return `{ data, nextPage }` instead of `{ rows, nextPage }` (e.g. `getallcustomers`). Match the existing shape rather than "fixing" it — call sites depend on the field name.
|
||||
|
||||
---
|
||||
|
||||
## 2. Direct positional-argument calls
|
||||
A few functions take plain positional arguments instead of `queryKey` — usually when they're invoked from a `useMutation` or imperatively. Example: `getTenants(appId)`, `gettenantlocations(tenantid)`.
|
||||
|
||||
Pick the signature based on how the function is called:
|
||||
- Called via `useQuery({ queryFn: fn })` → destructure `{ queryKey }`.
|
||||
- Called via `useQuery({ queryFn: () => fn(arg) })` → take positional args.
|
||||
- Called from a mutation or imperatively → take positional args.
|
||||
|
||||
---
|
||||
|
||||
## 3. Base URL selection
|
||||
|
||||
| Use base | When |
|
||||
|---|---|
|
||||
| `process.env.REACT_APP_URL` | Default for ~95% of endpoints |
|
||||
| `process.env.REACT_APP_URL2` | `/users/update`, `/tenants/update`, `/tenants/update/services`, archival `/orders/getorders`, `/partners/getriderlogs` |
|
||||
| Hardcoded `https://routes.workolik.com` | Bike solver + reconcile-steps + batch efficiency |
|
||||
| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver |
|
||||
| Hardcoded `https://jupiter.nearle.app` | Login + final `/deliveries/createdeliveries` commit |
|
||||
|
||||
When adding a new endpoint, check whether the backend actually serves it on URL or URL2 — don't guess. URL2 lives on a separate service.
|
||||
|
||||
---
|
||||
|
||||
## 4. Error handling pattern
|
||||
|
||||
```js
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/...`);
|
||||
return response.data.details; // or .summary, .data, etc — depends on backend shape
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return null; // or return [] / {} — match what the caller expects
|
||||
}
|
||||
```
|
||||
|
||||
- Toast on failure via `OpenToast` from `components/third-party/OpenToast`. Don't `throw` — TanStack Query's `onError` is rarely wired by callers.
|
||||
- Return a sensible empty default (`null`, `[]`, `{}`) so the call site's destructuring doesn't crash.
|
||||
- Don't `console.log(err)` AND toast — toast is enough. Some legacy functions do both; new functions should not.
|
||||
|
||||
---
|
||||
|
||||
## 5. When NOT to add to `api.js`
|
||||
|
||||
The user has tolerated a number of pages making direct inline `axios.put` / `axios.post` calls for mutations (e.g. `Tenants.js` calls `axios.put('/tenants/update')` inline). Match the surrounding file:
|
||||
|
||||
- **Adding a new shared GET** → put it in `api.js`.
|
||||
- **Adding a one-off mutation used in only one page** → tolerated inline in that page; doesn't need a new export here.
|
||||
- **Adding a polling endpoint used by multiple pages** → put it here as a named export so the query cache keys are coherent.
|
||||
|
||||
---
|
||||
|
||||
## 6. Response shape quirks
|
||||
|
||||
Backend response shapes are inconsistent. Don't assume `response.data.details` — check what the specific endpoint returns:
|
||||
|
||||
| Backend field | Used by |
|
||||
|---|---|
|
||||
| `response.data.details` | Most list endpoints |
|
||||
| `response.data.summary` | `getcustomersummary`, `gettenantsummary`, `getpricinglist` summary calls |
|
||||
| `response.data.data` | `getRiderPeriodicLogs`, `getallcustomers` infinite-query page payload |
|
||||
| `response.data.message` | Mutation success messages (for toast) |
|
||||
| `response.data.status` | Boolean success flag — check before reading `.details` on some endpoints |
|
||||
|
||||
When unsure, log the response once during dev and pick the matching field.
|
||||
|
||||
---
|
||||
|
||||
## 7. Things that look broken but are intentional
|
||||
|
||||
- `const userid = localStorage.getItem('userid');` at module top — read once at module load, intentional for the `fetchAppLocations` helper. Don't move it inside the function.
|
||||
- Some functions take a `pageno` 0-indexed and others 1-indexed (`pageno: pageParam + 1` vs `pageno: pageParam`). Backend inconsistency — leave it alone unless you confirm the backend side.
|
||||
- A few legacy commented-out function bodies are kept above their current implementation as historical reference. Don't delete them in a drive-by edit.
|
||||
@@ -406,6 +406,20 @@ export const getpricinglist = async ({ queryKey }) => {
|
||||
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 }) => {
|
||||
|
||||
@@ -1,130 +1,603 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Stack, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Chip } from '@mui/material';
|
||||
import MainCard from 'components/MainCard';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MdLocalOffer,
|
||||
MdMyLocation,
|
||||
MdAttachMoney,
|
||||
MdGroups,
|
||||
MdPlace,
|
||||
MdSpeed,
|
||||
MdPriceCheck,
|
||||
MdStraighten,
|
||||
MdReceiptLong
|
||||
} from 'react-icons/md';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
import TitleCard from 'components/nearle_components/TitleCard';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import { Empty } from 'antd';
|
||||
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: 16,
|
||||
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||
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 BRAND_LIGHT = '#9255AB';
|
||||
|
||||
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);
|
||||
|
||||
// Soft pill for metric cells in the table.
|
||||
const MetricPill = ({ color, icon, label, width }) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint(color),
|
||||
border: `1px solid ${edge(color)}`,
|
||||
color,
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
minWidth: width,
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
|
||||
// ==============================|| Pricing page ||============================== //
|
||||
|
||||
// ==============================|| Starts here ||============================== //
|
||||
const ClientsPricing = () => {
|
||||
const containerRef = useRef();
|
||||
const [appId, setAppId] = useState(0);
|
||||
const [locaName, setLocoName] = useState('');
|
||||
const [pricing, setpricing] = useState([]);
|
||||
const [isLoader, setIsloader] = useState([]);
|
||||
const [locaName, setLocoName] = useState('All');
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
|
||||
// ==============================|| formatNumberToRupees ||============================== //
|
||||
function formatNumberToRupees(value) {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 2
|
||||
}).format(value);
|
||||
}
|
||||
function dotzerozero(value) {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
minimumFractionDigits: 2
|
||||
}).format(value);
|
||||
}
|
||||
const {
|
||||
data: pricing = [],
|
||||
isLoading
|
||||
} = useQuery({
|
||||
queryKey: ['getallpricing', appId],
|
||||
queryFn: getallpricing,
|
||||
keepPreviousData: true
|
||||
});
|
||||
|
||||
// ==============================|| getAllPricing ||============================== //
|
||||
const getAllPricing = async () => {
|
||||
setIsloader(true);
|
||||
try {
|
||||
const pricingres = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
|
||||
console.log('pricingres', pricingres.data.details);
|
||||
setpricing(pricingres.data.details);
|
||||
setIsloader(false);
|
||||
} catch (err) {
|
||||
console.log('pricingres', err);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
getAllPricing();
|
||||
}, [appId]);
|
||||
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: MdLocalOffer, value: stats.total },
|
||||
{ key: 'tenants', label: 'Tenants Priced', color: '#0ea5e9', icon: MdGroups, value: stats.tenants },
|
||||
{ key: 'avg', label: 'Avg Base Price', color: '#f59e0b', icon: MdAttachMoney, value: formatRupees(stats.avgBase) },
|
||||
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdPlace, value: locaName || 'All Zones' }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoader && <Loader />}
|
||||
{/* ============================================= || Title Card || ============================================= */}
|
||||
<Stack direction={'row'} justifyContent="space-between" alignItems="center" sx={{ my: 1 }}>
|
||||
<TitleCard title="Pricing" />
|
||||
<Stack sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{isLoading && <Loader />}
|
||||
|
||||
{/* ============================================= || Header || ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mb: { xs: 1.5, md: 2 },
|
||||
p: { xs: 1.5, sm: 2, md: 2.5 },
|
||||
borderRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||
boxShadow: DT.shadowMd
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={{ xs: 1.5, sm: 2 }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={{ xs: 1.25, sm: 1.75 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 40, sm: 48 },
|
||||
height: { xs: 40, sm: 48 },
|
||||
bgcolor: BRAND,
|
||||
color: '#fff',
|
||||
boxShadow: `0 6px 18px ${ring(BRAND)}`
|
||||
}}
|
||||
>
|
||||
<MdLocalOffer size={22} />
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
|
||||
}}
|
||||
>
|
||||
Pricing
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#10b981',
|
||||
boxShadow: '0 0 0 4px rgba(16,185,129,0.18)'
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||
Live · {locaName || 'All Zones'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<LocationAutocomplete
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
sx={{ width: { xs: '100%', custom450: 300 }, zIndex: '100' }}
|
||||
pill
|
||||
accentColor={BRAND}
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
paperComponent={SoftPaper}
|
||||
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/* ============================================= || table || ============================================= */}
|
||||
<MainCard content={false} sx={{ mt: 2 }}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead sx={{ bgcolor: '#e1bee7' }}>
|
||||
<TableRow>
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell align="center">Pricing Id</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
</Paper>
|
||||
|
||||
{/* ============================================= || KPI Cards || ============================================= */}
|
||||
<Grid container spacing={{ xs: 1.25, sm: 1.5, md: 2 }} sx={{ mt: '1px' }}>
|
||||
{KPI_META.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Grid item key={item.key} xs={12} sm={6} md={3}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
p: { xs: 1.25, sm: 1.75, md: 2.25 },
|
||||
borderRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: '#fff',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: DT.shadowMd,
|
||||
borderColor: edge(item.color)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)`
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
|
||||
<Stack spacing={0.5} sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: DT.textSecondary,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.4,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: { xs: 10, sm: 11 },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 36, sm: 42, md: 48 },
|
||||
height: { xs: 36, sm: 42, md: 48 },
|
||||
bgcolor: soft(item.color),
|
||||
color: item.color,
|
||||
boxShadow: `inset 0 0 0 1px ${edge(item.color)}`,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</Avatar>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</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: 999,
|
||||
bgcolor: tint(BRAND),
|
||||
'& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: BRAND },
|
||||
'&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 },
|
||||
'&.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'
|
||||
}}
|
||||
>
|
||||
<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">MinKm</TableCell>
|
||||
<TableCell align="center">Price/Km</TableCell>
|
||||
<TableCell align="center">MaxKm</TableCell>
|
||||
<TableCell>Min Orders</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>
|
||||
{isLoader && <OrdersTableSkeleton col={5} />}
|
||||
{pricing?.length === 0 && !isLoader ? (
|
||||
{isLoading && <OrdersTableSkeleton col={5} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={10} align="center">
|
||||
<Empty description={'No Pricing List'} />
|
||||
<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>
|
||||
) : (
|
||||
pricing?.map((data, index) => (
|
||||
<TableRow key={data.pricingid || index}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
|
||||
<TableCell>{data.applocation}</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Chip size="small" color="info" label={data.pricingid} sx={{ width: 50 }} />
|
||||
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 sx={{ whiteSpace: 'nowrap' }}>{data.appname}</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>{data.slab}</TableCell>
|
||||
<TableCell>
|
||||
{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>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Chip size="small" color="primary" label={formatNumberToRupees(data.baseprice)} sx={{ width: 100 }} />
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<MdSpeed size={12} /> {row.slab || '—'}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Chip size="small" color="warning" label={dotzerozero(data.minkm)} sx={{ width: 100 }} />
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
icon={<MdPriceCheck size={12} />}
|
||||
label={formatRupees(row.baseprice)}
|
||||
width={110}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Chip size="small" color="success" label={formatNumberToRupees(data.priceperkm)} sx={{ width: 100 }} />
|
||||
<MetricPill
|
||||
color="#f59e0b"
|
||||
icon={<MdStraighten size={12} />}
|
||||
label={`${formatDecimal(row.minkm)} km`}
|
||||
width={90}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Chip size="small" color="error" label={dotzerozero(data.maxkm)} sx={{ width: 100 }} />
|
||||
<MetricPill
|
||||
color="#10b981"
|
||||
icon={<MdAttachMoney size={12} />}
|
||||
label={formatRupees(row.priceperkm)}
|
||||
width={110}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{data.minorder}</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>
|
||||
</MainCard>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import TitleCard from 'components/nearle_components/TitleCard';
|
||||
import React, { useEffect, useState, useRef, useMemo, Fragment } from 'react';
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
@@ -40,26 +39,32 @@ import {
|
||||
DialogTitle,
|
||||
FormLabel,
|
||||
TablePagination,
|
||||
Chip,
|
||||
Skeleton
|
||||
Skeleton,
|
||||
Avatar,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EditOutlined,
|
||||
IssuesCloseOutlined,
|
||||
StopOutlined
|
||||
StopOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { PopupTransition } from 'components/@extended/Transitions';
|
||||
import { FaRegCheckCircle } from 'react-icons/fa';
|
||||
import { TbBrandDatabricks } from 'react-icons/tb';
|
||||
import { GiMoneyStack } from 'react-icons/gi';
|
||||
import { FiUser } from 'react-icons/fi';
|
||||
import { FaRegAddressCard } from 'react-icons/fa';
|
||||
import { GiModernCity } from 'react-icons/gi';
|
||||
import { MdNumbers } from 'react-icons/md';
|
||||
import { TbWorldLongitude } from 'react-icons/tb';
|
||||
import { FiPhoneCall } from 'react-icons/fi';
|
||||
import { FaRegCheckCircle, FaRegAddressCard } from 'react-icons/fa';
|
||||
import { TbBrandDatabricks, TbWorldLongitude, TbWorldLatitude } from 'react-icons/tb';
|
||||
import { GiMoneyStack, GiModernCity } from 'react-icons/gi';
|
||||
import { FiUser, FiPhoneCall } from 'react-icons/fi';
|
||||
import {
|
||||
MdNumbers,
|
||||
MdGroups,
|
||||
MdCheckCircle,
|
||||
MdHourglassEmpty,
|
||||
MdCancel,
|
||||
MdMyLocation,
|
||||
MdPersonPin
|
||||
} from 'react-icons/md';
|
||||
import { LuMail } from 'react-icons/lu';
|
||||
import { BiUser } from 'react-icons/bi';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
@@ -68,6 +73,97 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { getalltenants, gettenantsummary } from 'pages/api/api';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
|
||||
// ============================================================================
|
||||
// Design tokens — shared with the deliveries page so every surface (header,
|
||||
// filters, KPI tiles, tabs, badges, dialogs) speaks the same visual language.
|
||||
// ============================================================================
|
||||
const DT = {
|
||||
radiusPill: 999,
|
||||
radiusCard: 16,
|
||||
radiusInner: 12,
|
||||
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||
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 pillFieldSx = (color) => ({
|
||||
cursor: 'pointer',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: DT.radiusPill + 'px',
|
||||
bgcolor: tint(color),
|
||||
fontWeight: 600,
|
||||
color: DT.textPrimary,
|
||||
paddingRight: '8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s',
|
||||
'& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: color },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||
'&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
|
||||
},
|
||||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: color }
|
||||
});
|
||||
|
||||
// Status palette — drives the pill tabs and per-row badges.
|
||||
const STATUS_META = {
|
||||
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, statusKey: 'active' },
|
||||
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, statusKey: 'pending' },
|
||||
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel, statusKey: 'inactive' }
|
||||
};
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ status: 'active', countKey: 'active', tabStatus: 'Active' },
|
||||
{ status: 'pending', countKey: 'pending', tabStatus: 'Pending' },
|
||||
{ status: 'inactive', countKey: 'inactive', tabStatus: 'InActive' }
|
||||
];
|
||||
|
||||
const KPI_META = [
|
||||
{ key: 'active', label: 'Active Tenants', color: '#10b981', icon: MdCheckCircle, countKey: 'active' },
|
||||
{ key: 'pending', label: 'Pending Approval', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pending' },
|
||||
{ key: 'inactive', label: 'Inactive Tenants', color: '#ef4444', icon: MdCancel, countKey: 'inactive' }
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
// ==============================|| Starts here||============================== //
|
||||
const Clients1 = () => {
|
||||
const textFieldRef = useRef(null);
|
||||
@@ -474,114 +570,356 @@ const Clients1 = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const activeTabMeta = STATUS_META[STATUS_TABS[value0]?.status] || STATUS_META.active;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{(getalltenantsIsLoading || summaryDataIsLoading || isloader) && <Loader />}
|
||||
{/* ========================================================== || Titlecard || ========================================================== */}
|
||||
<TitleCard title="Tenants ">
|
||||
<LocationAutocomplete
|
||||
locations={locations}
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
setPage={setPage}
|
||||
sx={{ width: { xs: '100%', custom450: 300 }, zIndex: '100' }}
|
||||
/>
|
||||
</TitleCard>
|
||||
{/* ========================================================== || MainCard (searchword) || ========================================================== */}
|
||||
<MainCard
|
||||
content={false}
|
||||
sx={{ mt: 2 }}
|
||||
title={
|
||||
{/* ============================================= || Header | ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mb: { xs: 1.5, md: 2 },
|
||||
p: { xs: 1.5, sm: 2, md: 2.5 },
|
||||
borderRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: `linear-gradient(135deg, ${tint('#6366f1')} 0%, ${tint('#0ea5e9')} 100%)`,
|
||||
boxShadow: DT.shadowMd
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={{ xs: 1.5, sm: 2 }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={{ xs: 1.25, sm: 1.75 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 40, sm: 48 },
|
||||
height: { xs: 40, sm: 48 },
|
||||
bgcolor: '#6366f1',
|
||||
color: '#fff',
|
||||
boxShadow: '0 6px 18px rgba(99,102,241,0.32)'
|
||||
}}
|
||||
>
|
||||
<MdGroups size={22} />
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
|
||||
}}
|
||||
>
|
||||
Tenants
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#10b981',
|
||||
boxShadow: '0 0 0 4px rgba(16,185,129,0.18)'
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||
Live · {locaName || 'All Zones'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<LocationAutocomplete
|
||||
locations={locations}
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
setPage={setPage}
|
||||
pill
|
||||
accentColor="#6366f1"
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
paperComponent={SoftPaper}
|
||||
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* ============================================= || KPI Cards | ============================================= */}
|
||||
<Grid container spacing={{ xs: 1.25, sm: 1.5, md: 2 }} sx={{ mt: '1px' }}>
|
||||
{KPI_META.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const value = summaryData?.[item.countKey] ?? 0;
|
||||
return (
|
||||
<Grid item key={item.key} xs={12} sm={4}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
p: { xs: 1.25, sm: 1.75, md: 2.25 },
|
||||
borderRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: '#fff',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: DT.shadowMd,
|
||||
borderColor: edge(item.color)
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
const idx = STATUS_TABS.findIndex((t) => t.countKey === item.countKey);
|
||||
if (idx >= 0) handleChange(null, idx);
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)`
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
|
||||
<Stack spacing={0.5} sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: DT.textSecondary,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.4,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: { xs: 10, sm: 11 },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Typography>
|
||||
{summaryDataIsLoading ? (
|
||||
<Skeleton sx={{ width: 70, height: { xs: 28, md: 36 } }} animation="wave" />
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.5rem', sm: '1.75rem', md: '2rem' }
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 36, sm: 42, md: 48 },
|
||||
height: { xs: 36, sm: 42, md: 48 },
|
||||
bgcolor: soft(item.color),
|
||||
color: item.color,
|
||||
boxShadow: `inset 0 0 0 1px ${edge(item.color)}`,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</Avatar>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</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
|
||||
minWidth={'100%'}
|
||||
flexDirection={{ xs: 'row', sm: 'row' }}
|
||||
direction="row"
|
||||
spacing={0.75}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap-reverse',
|
||||
gap: 1
|
||||
flex: 1,
|
||||
overflowX: 'auto',
|
||||
py: 0.5,
|
||||
px: 0.25,
|
||||
'&::-webkit-scrollbar': { height: 6 },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
|
||||
}}
|
||||
>
|
||||
{/* ============================================= || Tabs || ================================================= */}
|
||||
|
||||
<Tabs value={value0} onChange={handleChange} aria-label="basic tabs example">
|
||||
<Tab
|
||||
label="Active"
|
||||
sx={{ ml: -1 }}
|
||||
onClick={() => {
|
||||
setstatus('active');
|
||||
}}
|
||||
icon={
|
||||
<Chip
|
||||
label={summaryDataIsLoading ? <Skeleton variant="text" width={15} height={10} /> : summaryData?.active || 0}
|
||||
color="primary"
|
||||
variant="light"
|
||||
sx={{ minWidth: 32, justifyContent: 'center' }}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
iconPosition="end"
|
||||
/>
|
||||
<Tab
|
||||
label="Pending"
|
||||
onClick={() => {
|
||||
setstatus('pending');
|
||||
}}
|
||||
icon={
|
||||
<Chip
|
||||
label={summaryDataIsLoading ? <Skeleton variant="text" width={15} height={10} /> : summaryData?.pending || 0}
|
||||
color="primary"
|
||||
variant="light"
|
||||
sx={{ minWidth: 32, justifyContent: 'center' }}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
iconPosition="end"
|
||||
/>
|
||||
<Tab
|
||||
label="Inactive"
|
||||
onClick={() => {
|
||||
setstatus('inactive');
|
||||
}}
|
||||
icon={
|
||||
<Chip
|
||||
label={summaryDataIsLoading ? <Skeleton variant="text" width={15} height={10} /> : summaryData?.inactive || 0}
|
||||
color="primary"
|
||||
variant="light"
|
||||
sx={{ minWidth: 32, justifyContent: 'center' }}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
iconPosition="end"
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
{/* ============================================= || SearchOutlined || ================================================= */}
|
||||
{STATUS_TABS.map((t, idx) => {
|
||||
const meta = STATUS_META[t.status];
|
||||
const Icon = meta.icon;
|
||||
const active = value0 === idx;
|
||||
const count = summaryData?.[t.countKey] ?? 0;
|
||||
return (
|
||||
<Box
|
||||
key={t.status}
|
||||
onClick={() => {
|
||||
setstatus(t.status);
|
||||
handleChange(null, idx);
|
||||
}}
|
||||
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: `1.5px solid ${active ? meta.color : edge(meta.color)}`,
|
||||
bgcolor: active ? meta.color : tint(meta.color),
|
||||
color: active ? '#fff' : meta.color,
|
||||
fontWeight: 700,
|
||||
boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : 'none',
|
||||
transition: 'all 0.18s',
|
||||
'&:hover': {
|
||||
borderColor: meta.color,
|
||||
boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : `0 0 0 3px ${ring(meta.color)}`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 22, md: 26 },
|
||||
height: { xs: 22, md: 26 },
|
||||
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(meta.color),
|
||||
color: active ? '#fff' : meta.color
|
||||
}}
|
||||
>
|
||||
<Icon size={13} />
|
||||
</Avatar>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 800, fontSize: { xs: 11.5, md: 13 }, lineHeight: 1 }}
|
||||
>
|
||||
{meta.label}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: { xs: 22, md: 26 },
|
||||
height: { xs: 18, md: 22 },
|
||||
px: 0.625,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 999,
|
||||
fontSize: { xs: 10, md: 11 },
|
||||
fontWeight: 800,
|
||||
bgcolor: active ? 'rgba(255,255,255,0.22)' : '#fff',
|
||||
color: active ? '#fff' : meta.color,
|
||||
border: active ? 'none' : `1px solid ${edge(meta.color)}`
|
||||
}}
|
||||
>
|
||||
{summaryDataIsLoading ? <Skeleton variant="text" width={14} height={10} /> : count}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
{/* Search */}
|
||||
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
sx={{ width: { xs: '100%', custom600: 275 }, m: 0 }}
|
||||
sx={{
|
||||
m: 0,
|
||||
width: '100%',
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#6366f1'),
|
||||
'& fieldset': { borderColor: edge('#6366f1'), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: '#6366f1' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#6366f1', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#6366f1')}` }
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
</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'
|
||||
}}
|
||||
>
|
||||
{/* ============================================= || TableContainer| ============================================= */}
|
||||
<TableContainer sx={{ maxHeight: 'calc(100vh - 300px)' }}>
|
||||
<Table stickyHeader>
|
||||
<TableContainer
|
||||
sx={{
|
||||
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
|
||||
'&::-webkit-scrollbar': { width: '10px', height: '10px' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge('#6366f1'),
|
||||
borderRadius: '8px',
|
||||
'&:hover': { backgroundColor: '#6366f1' }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader sx={{ minWidth: { xs: 720, md: 960 } }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ position: 'sticky !important', bgcolor: '#e1bee7!important' }}>S.No</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important', bgcolor: '#e1bee7!important' }}>Client</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important', bgcolor: '#e1bee7!important' }}>Contact</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important', bgcolor: '#e1bee7!important' }}>Address</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important', bgcolor: '#e1bee7!important' }} align="center">
|
||||
Actions
|
||||
</TableCell>
|
||||
<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>Status</TableCell>
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -589,111 +927,220 @@ const Clients1 = () => {
|
||||
|
||||
{tenantList?.length == 0 && !isloader ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<Stack width={'100%'} direction={'row'} justifyContent={'center'}>
|
||||
<Empty description={`No ${tabStatus} Tenants`} styles={{ description: { color: theme.palette.error.main } }} />
|
||||
<TableCell colSpan={6} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdGroups size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
No tenants to show
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{`No ${activeTabMeta.label.toLowerCase()} tenants for this filter.`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
tenantList?.map((row, index) => (
|
||||
<>
|
||||
tenantList?.map((row, index) => {
|
||||
const rowStatusKey = (value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive');
|
||||
const rowStatusMeta = STATUS_META[rowStatusKey];
|
||||
const RowStatusIcon = rowStatusMeta.icon;
|
||||
return (
|
||||
<Fragment key={row.tenantid ?? index}>
|
||||
{/* ============================================ || tablerow 1 || ============================================ */}
|
||||
<TableRow key={index}>
|
||||
<TableCell>{index + 1 + page * rowsPerPage}</TableCell>
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
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>
|
||||
<Stack>
|
||||
<Typography variant="subtitle1" sx={{ whiteSpace: 'nowrap' }}>
|
||||
{row.tenantname}
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||||
{String(index + 1 + page * rowsPerPage).padStart(2, '0')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
pl: 0.5,
|
||||
pr: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint(rowStatusMeta.color),
|
||||
border: `1px solid ${edge(rowStatusMeta.color)}`,
|
||||
color: rowStatusMeta.color
|
||||
}}
|
||||
>
|
||||
<AccentAvatar color={rowStatusMeta.color} size={20}>
|
||||
<RowStatusIcon size={12} />
|
||||
</AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 11, lineHeight: 1 }}>
|
||||
{rowStatusMeta.label}
|
||||
</Typography>
|
||||
<Typography variant="body2">Id : {row.tenantid}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography>{row.primarycontact}</Typography>
|
||||
<Typography color="secondary">{row.primaryemail}</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color="#6366f1" size={32}>
|
||||
<MdPersonPin size={16} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.tenantname}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.tenantid}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell sx={{ whiteSpace: 'wrap' }}>{row.address}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Stack direction={'row'}>
|
||||
<IconButton
|
||||
<TableCell>
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.primarycontact || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.primaryemail || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 280 }}>
|
||||
<Tooltip title={row.address || ''} placement="top">
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: value0 == 1 ? theme.palette.primary.main : theme.palette.success.main
|
||||
color: DT.textSecondary,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Tooltip title={value0 == 0 ? 'Inactive' : value == 1 ? 'Approve' : 'Active'} placement="top">
|
||||
{value0 == 0 ? (
|
||||
<StopOutlined
|
||||
style={{ color: 'red' }}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
setClientstatus(row.approved);
|
||||
setTimeout(() => {
|
||||
tenantupdate(row.tenantid);
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
) : value0 == 1 ? (
|
||||
<IssuesCloseOutlined
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setDialogopen(true);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
getAppPricing(row.applolcationid);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FaRegCheckCircle
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
setTimeout(() => {
|
||||
tenantupdate(row.tenantid);
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{row.address || '—'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" spacing={0.75} justifyContent="flex-end">
|
||||
{value0 == 0 && (
|
||||
<Tooltip title="Inactivate" placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: soft('#ef4444'),
|
||||
color: '#ef4444',
|
||||
border: `1px solid ${edge('#ef4444')}`,
|
||||
'&:hover': { bgcolor: '#ef4444', color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
setTimeout(() => {
|
||||
tenantupdate(row.tenantid);
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
<StopOutlined style={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
handleCollapseToggle1(index); // makes 1st collpase open
|
||||
setOpenRowIndex2(-1); //makes 2nd collapse close , if open
|
||||
setSelectedtenid(row.tenantid);
|
||||
// setAppId(row.applocationid);
|
||||
}}
|
||||
sx={{
|
||||
color: openRowIndex1 === index ? theme.palette.error.main : theme.palette.primary.main
|
||||
}}
|
||||
>
|
||||
{openRowIndex1 === index ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
</IconButton>
|
||||
{value0 !== 1 && (
|
||||
)}
|
||||
{value0 == 1 && (
|
||||
<Tooltip title="Approve" placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: soft('#f59e0b'),
|
||||
color: '#f59e0b',
|
||||
border: `1px solid ${edge('#f59e0b')}`,
|
||||
'&:hover': { bgcolor: '#f59e0b', color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setDialogopen(true);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
getAppPricing(row.applolcationid);
|
||||
}}
|
||||
>
|
||||
<IssuesCloseOutlined style={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{value0 == 2 && (
|
||||
<Tooltip title="Activate" placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: soft('#10b981'),
|
||||
color: '#10b981',
|
||||
border: `1px solid ${edge('#10b981')}`,
|
||||
'&:hover': { bgcolor: '#10b981', color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
setTimeout(() => {
|
||||
tenantupdate(row.tenantid);
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
<FaRegCheckCircle style={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={openRowIndex1 === index ? 'Hide details' : 'View details'} placement="top">
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="medium"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: openRowIndex1 === index ? '#6366f1' : soft('#6366f1'),
|
||||
color: openRowIndex1 === index ? '#fff' : '#6366f1',
|
||||
border: `1px solid ${edge('#6366f1')}`,
|
||||
'&:hover': { bgcolor: '#6366f1', color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
handleCollapseToggle2(index); // makse 2nd collpase open
|
||||
setOpenRowIndex1(-1); // makse 1st collpse close if open
|
||||
handleCollapseToggle1(index);
|
||||
setOpenRowIndex2(-1);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
}}
|
||||
sx={{
|
||||
color: openRowIndex2 === index ? theme.palette.error.main : theme.palette.primary.main
|
||||
}}
|
||||
>
|
||||
<Tooltip title={openRowIndex2 === index ? 'Close' : 'Edit'} placement="top">
|
||||
{openRowIndex2 === index ? <CloseOutlined /> : <EditOutlined />}
|
||||
</Tooltip>
|
||||
{openRowIndex1 === index ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{value0 !== 1 && (
|
||||
<Tooltip title={openRowIndex2 === index ? 'Close' : 'Edit'} placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: openRowIndex2 === index ? '#8b5cf6' : soft('#8b5cf6'),
|
||||
color: openRowIndex2 === index ? '#fff' : '#8b5cf6',
|
||||
border: `1px solid ${edge('#8b5cf6')}`,
|
||||
'&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
handleCollapseToggle2(index);
|
||||
setOpenRowIndex1(-1);
|
||||
setSelectedtenid(row.tenantid);
|
||||
setAppId(row.applocationid);
|
||||
}}
|
||||
>
|
||||
{openRowIndex2 === index ? <CloseOutlined style={{ fontSize: 14 }} /> : <EditOutlined style={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
@@ -1532,21 +1979,20 @@ const Clients1 = () => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Divider />
|
||||
{/* ============================================= || Pagination| ============================================= */}
|
||||
{!searchword && tenantList?.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Divider sx={{ borderColor: DT.borderSubtle }} />
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 50, 100]}
|
||||
component="div"
|
||||
// count={pageno}
|
||||
count={value0 == 0 ? summaryData?.active : value0 == 1 ? summaryData?.pending : value0 == 2 ? summaryData?.inactive : 0}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
@@ -1555,7 +2001,7 @@ const Clients1 = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MainCard>
|
||||
</Paper>
|
||||
{/* // ==============================||( Client Pricing ) dialog (dialogopen) ||============================== // */}
|
||||
<Dialog fullWidth={true} open={dialogopen} onClose={dialogclose} scroll={'paper'} maxWidth="sm" TransitionComponent={PopupTransition}>
|
||||
<DialogTitle
|
||||
@@ -1710,7 +2156,7 @@ const Clients1 = () => {
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { React, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Empty } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
import { RiEdit2Fill } from 'react-icons/ri';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -27,14 +23,26 @@ import {
|
||||
DialogContent,
|
||||
Button,
|
||||
TextField,
|
||||
Autocomplete
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MdPeopleAlt,
|
||||
MdMyLocation,
|
||||
MdPersonPin,
|
||||
MdPhone,
|
||||
MdLocationOn,
|
||||
MdEdit,
|
||||
MdGroups,
|
||||
MdHowToReg,
|
||||
MdPlace
|
||||
} from 'react-icons/md';
|
||||
import Geocode from 'react-geocode';
|
||||
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||
import parse from 'autosuggest-highlight/parse';
|
||||
import { debounce } from '@mui/material/utils';
|
||||
// project imports
|
||||
import MainCard from 'components/MainCard';
|
||||
import Loader from 'components/Loader';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
@@ -43,6 +51,59 @@ import { getallcustomers, getcustomersummary } from 'pages/api/api';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
|
||||
// ============================================================================
|
||||
// Design tokens — shared with the deliveries / tenants / pricing pages so every
|
||||
// surface (header, KPI tiles, table, badges, dialog) speaks the same visual
|
||||
// language. Keep this block in sync with deliveries.js:109–162.
|
||||
// ============================================================================
|
||||
const DT = {
|
||||
radiusPill: 999,
|
||||
radiusCard: 16,
|
||||
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||
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 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>
|
||||
);
|
||||
|
||||
// ==============================|| google address ||============================== //
|
||||
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
|
||||
|
||||
@@ -67,7 +128,6 @@ export default function Customers() {
|
||||
const loadMoreRef = useRef();
|
||||
const [rowsPerPage] = useState(50);
|
||||
const [page] = useState(0);
|
||||
const theme = useTheme();
|
||||
const [appId, setAppId] = useState(0);
|
||||
const [locaName, setLocoName] = useState('All');
|
||||
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
|
||||
@@ -326,131 +386,433 @@ export default function Customers() {
|
||||
}
|
||||
}
|
||||
};
|
||||
const KPI_META = [
|
||||
{ key: 'total', label: 'Total Customers', color: '#662582', icon: MdGroups, value: pageCount?.Total ?? 0 },
|
||||
{ key: 'loaded', label: 'Loaded in View', color: '#0ea5e9', icon: MdHowToReg, value: rows.length },
|
||||
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdPlace, value: locaName || 'All Zones' }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{(customerSummaryIsLoading || customersIsLoading) && (
|
||||
<>
|
||||
{/* <CircularLoader /> */}
|
||||
<Loader />
|
||||
</>
|
||||
)}
|
||||
{/* <TitleCard title={'Customers'} /> */}
|
||||
<MainCard
|
||||
content={false}
|
||||
sx={{}}
|
||||
title={
|
||||
<Stack sx={{ m: 2 }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={2}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{/* Left: Title */}
|
||||
<Typography variant="h3">Customers</Typography>
|
||||
{/* Right: Controls */}
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="center" width={{ xs: '100%', sm: 'auto' }}>
|
||||
<LocationAutocomplete
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 320 },
|
||||
zIndex: 100
|
||||
}}
|
||||
/>
|
||||
{(customerSummaryIsLoading || customersIsLoading) && <Loader />}
|
||||
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder={`Search ${pageCount?.Total || ''} Customer (ctrl+k)`}
|
||||
{/* ============================================= || Header | ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mb: { xs: 1.5, md: 2 },
|
||||
p: { xs: 1.5, sm: 2, md: 2.5 },
|
||||
borderRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)`,
|
||||
boxShadow: DT.shadowMd
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={{ xs: 1.5, sm: 2 }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={{ xs: 1.25, sm: 1.75 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 40, sm: 48 },
|
||||
height: { xs: 40, sm: 48 },
|
||||
bgcolor: '#662582',
|
||||
color: '#fff',
|
||||
boxShadow: '0 6px 18px rgba(99,102,241,0.32)'
|
||||
}}
|
||||
>
|
||||
<MdPeopleAlt size={22} />
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
|
||||
}}
|
||||
>
|
||||
Customers
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 320 }
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#10b981',
|
||||
boxShadow: '0 0 0 4px rgba(16,185,129,0.18)'
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||
Live · {locaName || 'All Zones'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
<LocationAutocomplete
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
pill
|
||||
accentColor="#662582"
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
paperComponent={SoftPaper}
|
||||
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* ============================================= || KPI Cards | ============================================= */}
|
||||
<Grid container spacing={{ xs: 1.25, sm: 1.5, md: 2 }} sx={{ mt: '1px' }}>
|
||||
{KPI_META.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Grid item key={item.key} xs={12} sm={4}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
p: { xs: 1.25, sm: 1.75, md: 2.25 },
|
||||
borderRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: '#fff',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: DT.shadowMd,
|
||||
borderColor: edge(item.color)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
background: `linear-gradient(90deg, ${item.color} 0%, ${soft(item.color)} 100%)`
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
|
||||
<Stack spacing={0.5} sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: DT.textSecondary,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.4,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: { xs: 10, sm: 11 },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' },
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: { xs: 36, sm: 42, md: 48 },
|
||||
height: { xs: 36, sm: 42, md: 48 },
|
||||
bgcolor: soft(item.color),
|
||||
color: item.color,
|
||||
boxShadow: `inset 0 0 0 1px ${edge(item.color)}`,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</Avatar>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</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'
|
||||
}}
|
||||
>
|
||||
<TableContainer ref={containerRef} onScroll={handleScroll} sx={{ maxHeight: 'calc(100vh - 180px)' }}>
|
||||
<Table stickyHeader>
|
||||
<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="#662582" size={32}>
|
||||
<MdGroups size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||||
>
|
||||
Directory
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{pageCount?.Total ?? 0} total · {rows.length} loaded
|
||||
</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 customers (ctrl+k)`}
|
||||
sx={{
|
||||
m: 0,
|
||||
width: '100%',
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#662582'),
|
||||
'& fieldset': { borderColor: edge('#662582'), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: '#662582' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
|
||||
}}
|
||||
/>
|
||||
</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'
|
||||
}}
|
||||
>
|
||||
<TableContainer
|
||||
ref={containerRef}
|
||||
onScroll={handleScroll}
|
||||
sx={{
|
||||
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
|
||||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge('#662582'),
|
||||
borderRadius: 8,
|
||||
'&:hover': { backgroundColor: '#662582' }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader sx={{ minWidth: { xs: 720, md: 960 } }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ position: 'sticky !important' }}>#</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important' }}>Name</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important' }}>Contact</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important' }}>Address</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important' }}>Location</TableCell>
|
||||
<TableCell sx={{ position: 'sticky !important' }}>Action</TableCell>
|
||||
<TableRow
|
||||
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>Customer</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell align="right">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{customersIsLoading && <OrdersTableSkeleton />}
|
||||
{rows?.length == 0 && !customersIsLoading ? (
|
||||
{rows?.length === 0 && !customersIsLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<Stack width={'100%'} direction={'row'} justifyContent={'center'}>
|
||||
<Empty />
|
||||
<TableCell colSpan={6} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdGroups size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
No customers to show
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the directory.'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows?.map((row, index) => {
|
||||
return (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell padding="none">{index + 1 + page * rowsPerPage}</TableCell>
|
||||
<TableCell align="left">
|
||||
<Typography align="left" variant="subtitle1">
|
||||
{row.firstname}
|
||||
</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
Id : {row.customerid}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
<Typography align="left" variant="subtitle1">
|
||||
{row.contactno}
|
||||
</Typography>
|
||||
<Typography align="left" variant="caption" color="secondary">
|
||||
{row.email}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="left">{row.address}</TableCell>
|
||||
<TableCell align="left">{row.suburb}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title={'To Edit'}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
console.log('row', row);
|
||||
setSelectedCustomer(row);
|
||||
setTimeout(() => {
|
||||
setOpen(true);
|
||||
}, 0);
|
||||
}}
|
||||
rows?.map((row, index) => (
|
||||
<TableRow
|
||||
key={row.customerid || `${row.firstname}-${index}`}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
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 + page * rowsPerPage).padStart(2, '0')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color="#662582" size={36}>
|
||||
<MdPersonPin size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<RiEdit2Fill
|
||||
fontSize={'large'}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.primary.main
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
{row.firstname || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.customerid}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdPhone size={12} color={DT.textMuted} />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{row.contactno || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{row.email && (
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.email}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 280 }}>
|
||||
<Tooltip title={row.address || ''} placement="top">
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: DT.textSecondary,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{row.address || '—'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{row.suburb ? (
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<MdLocationOn size={12} /> {row.suburb}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Edit customer" placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedCustomer(row);
|
||||
setTimeout(() => setOpen(true), 0);
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: soft('#8b5cf6'),
|
||||
color: '#8b5cf6',
|
||||
border: `1px solid ${edge('#8b5cf6')}`,
|
||||
'&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
|
||||
}}
|
||||
>
|
||||
<MdEdit size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
{rows?.length != 0 && (
|
||||
{rows?.length !== 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={15} rowSpan={3}>
|
||||
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage ? <LoaderWithImage /> : hasNextPage ? <LoaderWithImage /> : 'No More Orders'}
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||||
No more customers
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -458,8 +820,7 @@ export default function Customers() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Divider />
|
||||
</MainCard>
|
||||
</Paper>
|
||||
{/* ======================================== || Edit Dialog || ======================================== */}
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -469,9 +830,26 @@ export default function Customers() {
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title" sx={{ bgcolor: theme.palette.primary.main, color: 'white' }}>
|
||||
<Stack direction={'row'} alignItems={'center'} spacing={1}>
|
||||
<FaRegEdit style={{ fontSize: 25 }} /> <Typography variant="h3">Edit Customer</Typography>
|
||||
<DialogTitle
|
||||
id="alert-dialog-title"
|
||||
sx={{
|
||||
background: `linear-gradient(135deg, #662582 0%, #9255AB 100%)`,
|
||||
color: '#fff',
|
||||
py: 2
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 36, height: 36, bgcolor: 'rgba(255,255,255,0.2)', color: '#fff' }}>
|
||||
<FaRegEdit style={{ fontSize: 18 }} />
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography sx={{ fontSize: 11, fontWeight: 700, opacity: 0.85, letterSpacing: 0.6, textTransform: 'uppercase', lineHeight: 1 }}>
|
||||
Customer
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 800, fontSize: { xs: '1.05rem', sm: '1.2rem' }, lineHeight: 1.2, mt: 0.25 }}>
|
||||
Edit {selectedCustomer?.firstname || 'Customer'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
85
src/pages/nearle/dispatch/CLAUDE.md
Normal file
85
src/pages/nearle/dispatch/CLAUDE.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# CLAUDE.md — `src/pages/nearle/dispatch/`
|
||||
|
||||
Rules for editing `Dispatch.js`, `Preview.js`, `CompareDataPanel.js`, and `dispatchShared.js`.
|
||||
**This is the most complex area of the codebase.** Dispatch.js alone is ~2500 lines, mixes Leaflet imperative APIs with React state, and shares its batch / time-bucket model with `deliveries.js`. Read this before touching anything here.
|
||||
|
||||
---
|
||||
|
||||
## 1. Batch / wave model
|
||||
|
||||
Dispatch.js defines the canonical batch hour ranges. `deliveries.js` mirrors them — **the two pages must agree on which batch a given row belongs to**, otherwise the same delivery shows up in one batch on one page and a different batch on the other.
|
||||
|
||||
```js
|
||||
// BATCH_OPTIONS — half-open [startHour, endHour) in LOCAL time, not UTC
|
||||
[
|
||||
{ id: 'morning', startHour: 0, endHour: 8 }, // 12 AM – 8 AM
|
||||
{ id: 'afternoon', startHour: 9, endHour: 12 }, // 9 AM – 12 PM
|
||||
{ id: 'evening', startHour: 16, endHour: 19 } // 4 PM – 7 PM
|
||||
]
|
||||
```
|
||||
|
||||
**Gaps are intentional** (8–9 AM, 12 PM–4 PM, after 7 PM). Rows that fall in a gap belong to no batch — *not* to the nearest one.
|
||||
|
||||
### Time-field selection (`selectedTimeField`)
|
||||
Default `'assigned'` → bucket key is `['assigntime']`. Other options use other timestamp fields (`pickedtime`, `deliverytime`). If you add a new time field option, make sure `deliveries.js` is updated too — they read each other's bucketing.
|
||||
|
||||
### Don'ts
|
||||
- Don't bucket in UTC. Use `dayjs(t)` (local), not `dayjs(t).utc()`. The original deliveries page had a UTC bucketing bug that hid orders mid-day; the multi-line comment above `getRowBatchId` in `deliveries.js` (search for `getRowBatchId`) explains it. Don't reintroduce.
|
||||
- Don't bucket bare `YYYY-MM-DD` strings — they parse to midnight and mis-bucket into Morning. Skip them.
|
||||
- Don't add a 4th batch without updating both pages and confirming with the backend what the new boundary means for assignments.
|
||||
|
||||
---
|
||||
|
||||
## 2. Leaflet integration
|
||||
|
||||
Dispatch.js uses `react-leaflet` for declarative tile/marker rendering, BUT a lot of marker behaviour is imperative:
|
||||
|
||||
- **Marker icons:** The default leaflet marker icons are loaded from a CDN at module top (line ~454) via `L.Icon.Default.mergeOptions`. Webpack can't bundle the default sprite, so don't remove this shim.
|
||||
- **Polyline offset:** `import '../../../utils/leafletPolylineOffset'` adds a polyline-offset method to leaflet. Required for showing two riders on the same road segment with parallel polylines. Don't remove the import; the symbol it adds is used at the prototype level.
|
||||
- **Imperative marker registry:** A `useRef` map holds Leaflet marker instances keyed by `orderid`. Opening/closing popups is done by calling `marker.openPopup()` / `closePopup()` directly — *not* by setting React state. This is intentional; flipping state caused full-list re-renders and dropped frames.
|
||||
- **Popup overlay:** The "centered" popup the operator sees on hover is rendered outside Leaflet (it's a plain MUI dialog absolutely-positioned over the map). Leaflet's `<Popup>` attached to the marker is for click context only. Don't try to consolidate them.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
|
||||
Skipping reconcile corrupts route sequences in the database. This is the single biggest production bug to avoid in this area.
|
||||
|
||||
---
|
||||
|
||||
## 4. State that drives the live map
|
||||
|
||||
- `riders` — array of rider objects with latest GPS. Updated by polling `/utils/getriderperiodiclogs`.
|
||||
- `selectedRider` — currently focused rider. Triggers polyline highlight + popup reveal.
|
||||
- `batchCounts` — derived (via `useMemo`) from the loaded order list filtered by the active batch. Mirror this shape on `deliveries.js`.
|
||||
- `selectedTimeField` — which timestamp drives bucketing. See §1.
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance constraints
|
||||
|
||||
The dispatch page renders 100+ markers and polylines on every render. Watch for these regressions:
|
||||
|
||||
- Don't pass new object literals to memoised child components (`{ size: 12 }`-style props re-trigger re-render). Lift to refs or `useMemo`.
|
||||
- Don't put `setSelectedRider` inside a `useEffect` that depends on `riders` — that causes polling-loop re-renders.
|
||||
- Don't `console.log` inside a marker click handler in production paths.
|
||||
|
||||
---
|
||||
|
||||
## 6. Editing Preview.js specifically
|
||||
|
||||
`Preview.js` is the post-solver staging page. Receives the solver output, lets the operator drag-and-drop, then commits.
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 7. File expectations for new work
|
||||
|
||||
- New solver mode? Add a new constant in `dispatchShared.js`, wire it through `orders.js` (the mode selector), and add the corresponding endpoint to `api.js`. Don't hardcode a new URL inside Dispatch.js or Preview.js.
|
||||
- New rider attribute on the live map? Add it to the rider-popup component in `Dispatch.js`, not to a new component — Leaflet popup mounting is fragile across React tree changes.
|
||||
- Don't break `CompareDataPanel.js` — it's used by the QA team to A/B different solver runs.
|
||||
File diff suppressed because it is too large
Load Diff
78
src/pages/nearle/orders/CLAUDE.md
Normal file
78
src/pages/nearle/orders/CLAUDE.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# CLAUDE.md — `src/pages/nearle/orders/`
|
||||
|
||||
Rules for editing `orders.js`, `OrdersPreview.js`, `createorder1.js`, `newcreateOrder.js`, `multipleOrders.js`, `details.js`, and the optimised preview.
|
||||
|
||||
This is the **revenue-critical area** of the console — the path from "order received" to "rider assigned" runs through here. The optimiser hand-off and the assign-and-notify sequence are the two flows you must not break.
|
||||
|
||||
---
|
||||
|
||||
## 1. The three dispatch modes (Mode 0 / 1 / 2)
|
||||
|
||||
The orders page tracks the chosen mode in `aiModeRef` (a `useRef`, not state — it's set just before the mutation fires).
|
||||
|
||||
| Mode | Solver | Endpoint | When operator picks it |
|
||||
|---|---|---|---|
|
||||
| **0 · Manual** | `routes.workolik.com` | `POST /optimization/createdeliveries` | "Optimise selected orders" — gives a tentative route, operator can rearrange in preview |
|
||||
| **1 · Bike** | `routes.workolik.com` | `POST /optimization/riderassign?hypertuning_params={...}` | Bike fleet with hyper-tuning (Balanced / Fuel Saver / Aggressive / Strict Zone) |
|
||||
| **2 · Auto** | `routemate.workolik.com` | `POST /optimization/riderassign?strategy=multi_trip` | Auto-rickshaw fleet, hourly multi-trip |
|
||||
|
||||
The mutation function gets picked by mode:
|
||||
|
||||
```js
|
||||
useMutation({
|
||||
mutationFn: aiModeRef.current == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
**`createAutomationDeliveries` covers both Mode 1 and Mode 2** — the difference is the URL it constructs and the `hypertuning_params` query string. Don't split them into separate functions.
|
||||
|
||||
---
|
||||
|
||||
## 2. The hand-off to `/orders/preview` (and then `/dispatch/preview`)
|
||||
|
||||
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.
|
||||
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`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Selection state
|
||||
|
||||
Multi-order optimisation uses a checkbox column. The selection lives in component state as an array of order objects (not just IDs) because the solver payload needs full order data (`pickup_lat`, `pickup_lng`, `drop_lat`, `drop_lng`, `weight`, `expecteddeliverytime`, etc.).
|
||||
|
||||
- "Select all" is implemented per visible page — not across all loaded infinite pages — to avoid accidental multi-thousand-order solver runs.
|
||||
- Selection clears when `currentStatus`, `appId`, or date range changes. This is intentional — solver runs are scoped to a single tab.
|
||||
- Don't add a "select across pages" affordance without confirming the optimiser's payload limits.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cancellation paths
|
||||
|
||||
| Function | Use |
|
||||
|---|---|
|
||||
| `cancelOrder` (`PUT /orders/updateorder`) | Single-order cancel (per-row icon) |
|
||||
| `cancelMultipleOrder` (`PUT /orders/updatemultipleorders`) | Bulk cancel of selected orders (toolbar button) |
|
||||
|
||||
Both record a cancel timestamp on the order. Neither triggers a rider FCM notification because the order was never assigned. **Do not call `notifyRider` after these.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Filters & data
|
||||
|
||||
The orders list is a `useInfiniteQuery` keyed by `['fetchorders', appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid]`. The corresponding `api.js` function (`fetchOrders`) destructures in this same order — see `src/pages/api/CLAUDE.md` §1.
|
||||
|
||||
The summary endpoint (`fetchorderscount`) uses a slightly different key — `currentStatus` comes after the date range. Match the call site, don't normalise.
|
||||
|
||||
---
|
||||
|
||||
## 6. Don'ts specific to this folder
|
||||
|
||||
- **Don't introduce a 4th dispatch mode** without coordinating with the workolik backend team. The solver URL and `?hypertuning_params=` query are versioned.
|
||||
- **Don't move solver URLs into env vars.** They are deliberately hardcoded because the optimiser is a separate, versioned service — pinning the URL is the version pin.
|
||||
- **Don't fold `createorder1.js`, `newcreateOrder.js`, and `multipleOrders.js` into one component.** They serve different operator flows (existing customer vs new customer vs CSV bulk import).
|
||||
- **Don't reorder `OrdersTableSkeleton`'s column count** without checking every page that imports it. It's used as a shared skeleton across orders, deliveries, tenants, and customers.
|
||||
@@ -157,9 +157,9 @@ const OrderMap = ({ startPoint, endPoint, appLocaLat, appLocaLng }) => {
|
||||
{hasPick && <Marker position={pickCoords} icon={pickupIcon} />}
|
||||
{hasDrop && <Marker position={dropCoords} icon={dropoffIcon} />}
|
||||
{routePoints.length > 0 ? (
|
||||
<Polyline positions={routePoints} color="#1890ff" weight={4} />
|
||||
<Polyline positions={routePoints} color="#662582" weight={4} />
|
||||
) : (
|
||||
hasPick && hasDrop && <Polyline positions={[pickCoords, dropCoords]} color="#1890ff" weight={4} />
|
||||
hasPick && hasDrop && <Polyline positions={[pickCoords, dropCoords]} color="#662582" weight={4} />
|
||||
)}
|
||||
<MapBoundsController startPoint={startPoint} endPoint={endPoint} />
|
||||
</MapContainer>
|
||||
@@ -1490,7 +1490,7 @@ const Createorder1 = () => {
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchOutlined style={{ fontSize: 15, color: '#1890ff' }} />
|
||||
<SearchOutlined style={{ fontSize: 15, color: '#662582' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
@@ -1526,7 +1526,7 @@ const Createorder1 = () => {
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<FaLocationDot style={{ fontSize: 14, color: '#1890ff' }} />
|
||||
<FaLocationDot style={{ fontSize: 14, color: '#662582' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
@@ -2268,7 +2268,7 @@ const Createorder1 = () => {
|
||||
{/* Map Card */}
|
||||
<Card className="orders-card" sx={{ p: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography sx={{ fontWeight: 700, mb: 1.25, display: 'flex', alignItems: 'center', gap: 0.75, color: '#1e293b', fontSize: '14px', letterSpacing: '-0.01em' }}>
|
||||
<MyLocationIcon sx={{ color: '#1890ff', fontSize: 16 }} />
|
||||
<MyLocationIcon sx={{ color: '#662582', fontSize: 16 }} />
|
||||
Live Route Preview
|
||||
</Typography>
|
||||
<div className="map-preview-wrapper">
|
||||
@@ -2446,7 +2446,7 @@ const Createorder1 = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ bgcolor: '#1890ff', color: 'white', py: 2.5 }}>
|
||||
<DialogTitle sx={{ background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)', color: 'white', py: 2.5 }}>
|
||||
<Stack spacing={1.5}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 600, color: 'white' }}>
|
||||
{`Select Saved Address (${pickordrop === 1 ? 'Pickup' : 'Drop'})`}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user