diff --git a/package.json b/package.json index 314d08c..9f97bb2 100644 --- a/package.json +++ b/package.json @@ -3,24 +3,13 @@ "version": "2.1.0", "private": true, "dependencies": { - "@ant-design/colors": "^7.0.0", "@ant-design/icons": "^5.0.1", "@astryxdesign/core": "^0.1.9", "@astryxdesign/theme-neutral": "^0.1.9", - "@emotion/cache": "^11.10.7", - "@emotion/react": "^11.10.6", - "@emotion/styled": "^11.10.6", - "@mui/base": "^5.0.0-beta.70", - "@mui/icons-material": "^5.14.19", - "@mui/lab": "^5.0.0-alpha.127", - "@mui/material": "^5.12.1", - "@mui/x-date-pickers": "^7.29.4", "@reduxjs/toolkit": "^2.12.0", "@stylexjs/stylex": "^0.19.0", "@svgr/webpack": "^7.0.0", "@tanstack/react-query": "^5.101.4", - "antd": "^5.11.5", - "autosuggest-highlight": "^3.3.4", "axios": "^1.3.5", "buffer": "^6.0.3", "chance": "^1.1.11", @@ -60,7 +49,6 @@ "simplebar": "^6.2.5", "simplebar-react": "^3.2.4", "stream-browserify": "^3.0.0", - "stylis-plugin-rtl": "^2.1.1", "use-debounce": "^10.0.5", "util": "^0.12.5", "web-vitals": "^3.3.1", @@ -122,4 +110,4 @@ "react-app-rewired": "^2.2.1", "react-error-overlay": "6.0.11" } -} +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 15d2f8a..6115e69 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,6 @@ // project import import Routes from 'routes'; -import ThemeCustomization from 'themes'; import Locales from 'components/Locales'; -// import RTLLayout from 'components/RTLLayout'; import ScrollTop from 'components/ScrollTop'; import Notistack from 'components/third-party/Notistack'; import { useNavigate } from 'react-router'; @@ -46,24 +44,20 @@ const App = () => { return ( <> - - - {/* */} - - - {/* */} - <> - - {/* - */} - - - - {/* */} - - - {/* */} - + + + + {/* */} + <> + + {/* + */} + + + + {/* */} + + ); }; diff --git a/src/components/RTLLayout.js b/src/components/RTLLayout.js deleted file mode 100644 index 60e7bbd..0000000 --- a/src/components/RTLLayout.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import { useEffect } from 'react'; - -// material-ui -import { CacheProvider } from '@emotion/react'; -import createCache from '@emotion/cache'; - -// third-party -import rtlPlugin from 'stylis-plugin-rtl'; - -// project import -import { ThemeDirection } from 'config'; -import useConfig from 'hooks/useConfig'; - -// ==============================|| RTL LAYOUT ||============================== // - -const RTLLayout = ({ children }) => { - const { themeDirection } = useConfig(); - - useEffect(() => { - document.dir = themeDirection; - }, [themeDirection]); - - const cacheRtl = createCache({ - key: themeDirection === ThemeDirection.RTL ? 'rtl' : 'css', - prepend: true, - stylisPlugins: themeDirection === ThemeDirection.RTL ? [rtlPlugin] : [] - }); - - return {children}; -}; - -RTLLayout.propTypes = { - children: PropTypes.node -}; - -export default RTLLayout; diff --git a/src/components/nearle_components/StatCard.js b/src/components/nearle_components/StatCard.js index 4fe5674..347e915 100644 --- a/src/components/nearle_components/StatCard.js +++ b/src/components/nearle_components/StatCard.js @@ -8,9 +8,12 @@ import { Text } from '@astryxdesign/core/Text'; import { Skeleton } from '@astryxdesign/core/Skeleton'; // ==============================|| STAT / KPI CARD (Doormile-style) ||============================== // -// Clean metric card: muted label, large value, a single soft-tinted rounded -// tile holding the icon. No coloured top-stripe, no rainbow — colour appears -// only in the small tile so a row of cards reads calm and corporate. +// Clean metric card: muted eyebrow label, large tabular value, a single +// soft-tinted rounded tile holding the icon (with a faint matching ring for +// depth). A low resting elevation lifts the tile off the page instead of +// reading as a flat bordered box. No coloured top-stripe, no rainbow — +// colour appears only in the icon tile so a row of cards reads calm and +// corporate. // // `icon` is a rendered node, e.g. icon={}. // `color` is a hex accent (defaults to brand red); `caption` is the small @@ -24,16 +27,16 @@ import { Skeleton } from '@astryxdesign/core/Skeleton'; export default function StatCard({ title, value, icon, color = '#000000', caption, loading = false }) { return ( - - + + - + {title} {loading ? ( - + ) : ( - + {value} )} @@ -45,9 +48,15 @@ export default function StatCard({ title, value, icon, color = '#000000', captio {icon && (
{icon}
diff --git a/src/globalPolish.css b/src/globalPolish.css new file mode 100644 index 0000000..79c7767 --- /dev/null +++ b/src/globalPolish.css @@ -0,0 +1,41 @@ +/* Global visual polish that used to come from MUI's / + in themes/index.js (ThemeCustomization). Kept as plain CSS now that every + page runs on Astryx — see root CLAUDE.md §6.6. Astryx's own reset + (@astryxdesign/core/reset.css, imported in index.js) covers the base + element resets; this file only carries the two enterprise-polish touches + that reset doesn't: crisp font rendering and a thin, unobtrusive scrollbar. */ + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +* { + scrollbar-width: thin; /* Firefox */ + scrollbar-color: rgba(0, 0, 0, 0.28) transparent; +} + +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +*::-webkit-scrollbar-track { + background-color: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.26); + border-radius: 8px; + border: 2px solid transparent; + background-clip: padding-box; +} + +*::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, 0.45); +} + +*::-webkit-scrollbar-corner { + background-color: transparent; +} diff --git a/src/hooks/useIsNarrowViewport.js b/src/hooks/useIsNarrowViewport.js new file mode 100644 index 0000000..8fc318e --- /dev/null +++ b/src/hooks/useIsNarrowViewport.js @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react'; + +// Replaces MUI's `useMediaQuery(theme.breakpoints.down('md'))` across the +// Astryx-converted pages. MUI's `md` breakpoint is 900px and `down()` is +// exclusive, so this is the identical `max-width: 899.95px` query it used to +// generate. First introduced in `pages/nearle/dispatch/dispatchShared.js`; +// lifted here once more than one feature folder needed it. +const NARROW_VIEWPORT_QUERY = '(max-width: 899.95px)'; + +export function useIsNarrowViewport() { + const [isNarrow, setIsNarrow] = useState( + () => typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(NARROW_VIEWPORT_QUERY).matches + ); + + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) return undefined; + const mql = window.matchMedia(NARROW_VIEWPORT_QUERY); + const onChange = (e) => setIsNarrow(e.matches); + setIsNarrow(mql.matches); + mql.addEventListener('change', onChange); + return () => mql.removeEventListener('change', onChange); + }, []); + + return isNarrow; +} + +export default useIsNarrowViewport; diff --git a/src/index.js b/src/index.js index 6f0ec0b..2e0a4ad 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import 'simplebar/dist/simplebar.css'; // Doormile brand theme applied per-page during migration). import '@astryxdesign/core/reset.css'; import '@astryxdesign/core/astryx.css'; +import './globalPolish.css'; import { LinkProvider } from '@astryxdesign/core/Link'; // apex-chart diff --git a/src/pages/api/api.js b/src/pages/api/api.js index e8c2e24..52c51c4 100644 --- a/src/pages/api/api.js +++ b/src/pages/api/api.js @@ -1,6 +1,7 @@ import axios from 'axios'; import { OpenToast } from 'components/third-party/OpenToast'; import logger from 'utils/logger'; +import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import { getMilers, getMiler, @@ -518,7 +519,20 @@ export const notifyRider = async ( // deliveries.js itself doesn't need a full rewrite — same compatibility-shim // approach as fetchAllRiders' `{details}` wrapper. export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { - const [, , , , , , rowsPerPage] = queryKey; + // Every caller builds the key as + // [name, appId, userid, status, startdate, enddate, rowsPerPage, …] + // (Dispatch.js, deliveries.js, reports/ordersDetails.js, + // reports/profitability.js — all four verified). Only rowsPerPage used to be + // read here, which meant startdate/enddate were accepted and then ignored: + // changing the date on the Dispatch page rebuilt the query key and refetched, + // but issued an identical request, so the row set — and every batch count + // derived from it — was byte-identical for today, yesterday and last week. + // + // GET /admin/bookings takes only pageno/pagesize (doormileApi.js; no date + // parameter is documented in express-console-api.md and guessing one risks a + // silent 400 or, worse, a silently-ignored filter), so the range is applied + // client-side below, after the rows are normalised. + const [, , , , startdate, enddate, rowsPerPage] = queryKey; // Unlike the 3 joins below (customers/milers/tenants — each individually // guarded so a failed join just degrades a display field, not the whole // page), a failed bookings call is the one thing this function can't @@ -632,8 +646,30 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { }; }); + // Apply the requested date range to the booking's CREATION day. This has to + // agree with what the batch bucketing reads (Dispatch.js's + // BATCH_TIME_FIELD / deliveries.js's BATCH_TIME_KEYS, both `orderdate`) — + // filtering on one field while bucketing on another is how you get a row + // that is counted for the day but belongs to no batch in it. + // + // parseDoormileTimestamp, not bare dayjs(): some Doormile timestamps carry a + // false trailing Z, and near midnight an unstripped one shifts the row +5:30 + // into the next calendar day, dropping it from the selected date. + // + // A missing/blank bound means "unbounded on that side", which preserves the + // old behaviour for any caller that doesn't pass real dates. + const inRange = (row) => { + if (!startdate && !enddate) return true; + const t = parseDoormileTimestamp(row.orderdate); + if (!t.isValid()) return false; + const day = t.format('YYYY-MM-DD'); + if (startdate && day < String(startdate)) return false; + if (enddate && day > String(enddate)) return false; + return true; + }; + return { - rows, + rows: rows.filter(inRange), // Whether to fetch another page must be based on the RAW bookings page // (bookings.length), not the post-filter `rows.length` — most bookings on // any given page are still pending, not dispatched, so the filtered count @@ -764,12 +800,21 @@ export const getallridersummary = async () => { // A miler has no `status` field — availabilitystatus is the real one // (confirmed live: Available/Assigned/On_Pickup/Offline/Blocked). // "Active" here means not Offline and not Blocked, matching riders.js's - // own tab filter. + // own tab filter. `available`/`onDelivery` are a KPI-card-friendly + // breakdown of that same "active" bucket — Available is free/idle, + // On Delivery covers both Assigned (accepted, not yet picked up) and + // On_Pickup (en route) — computed separately so an unrecognised status + // value still falls into "active" (unchanged behaviour) without also + // silently inflating one of the two new buckets. const active = milers.filter((m) => !['offline', 'blocked'].includes(String(m.availabilitystatus || '').toLowerCase())).length; - return { total: milers.length, active, inactive: milers.length - active }; + const available = milers.filter((m) => String(m.availabilitystatus || '').toLowerCase() === 'available').length; + const onDelivery = milers.filter((m) => + ['assigned', 'on_pickup'].includes(String(m.availabilitystatus || '').toLowerCase()) + ).length; + return { total: milers.length, active, inactive: milers.length - active, available, onDelivery }; } catch (err) { OpenToast(err.response?.data?.message || err.message || 'Failed to load rider summary', 'error', 2000); - return { total: 0, active: 0, inactive: 0 }; + return { total: 0, active: 0, inactive: 0, available: 0, onDelivery: 0 }; } }; diff --git a/src/pages/nearle/appUsers/appUsers.js b/src/pages/nearle/appUsers/appUsers.js index 1da8994..cfbf76d 100644 --- a/src/pages/nearle/appUsers/appUsers.js +++ b/src/pages/nearle/appUsers/appUsers.js @@ -1,42 +1,30 @@ -import React, { useMemo, useState } from 'react'; -import { - Avatar, - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - IconButton, - InputLabel, - MenuItem, - Paper, - Select, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; +import { useMemo, useState } from 'react'; import { MdOutlineAdminPanelSettings, MdEdit, MdDeleteOutline, MdAdd } from 'react-icons/md'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { enqueueSnackbar } from 'notistack'; + +import { Grid } from '@astryxdesign/core/Grid'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Card } from '@astryxdesign/core/Card'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table'; import Loader from 'components/Loader'; +import TableLoader from 'components/nearle_components/TableLoader'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { getAppUsers, createAppUser, updateAppUser, deleteAppUser } from 'pages/api/doormileApi'; -import { enqueueSnackbar } from 'notistack'; -import { DT, soft, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens'; +import { DT } from 'themes/dt/tokens'; +import { TableScroll, AccentAvatar } from 'themes/dt/primitives'; // ============================================================================ // GET /admin/users confirmed live: { id, email, first_name, phone, role, @@ -48,15 +36,18 @@ import { DT, soft, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/toke // to match the GET shape. // ============================================================================ -const BRAND = '#000000'; +const BRAND = DT.brand; const ROLES = ['admin', 'manager', 'executive', 'rep']; +const ROLE_OPTIONS = ROLES.map((r) => ({ value: r, label: r.charAt(0).toUpperCase() + r.slice(1) })); +const STATUS_OPTIONS = [ + { value: 'Active', label: 'Active' }, + { value: 'Inactive', label: 'Inactive' } +]; const roleLabel = (role) => (role ? role.charAt(0).toUpperCase() + role.slice(1) : '—'); const emptyForm = { id: null, first_name: '', email: '', phone: '', password: '', role: 'admin', status: 'Active' }; const AppUsers = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); const queryClient = useQueryClient(); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); @@ -139,6 +130,8 @@ const AppUsers = () => { saveMutation.mutate({ id, data }); }; + const set = (key) => (value) => setForm((f) => ({ ...f, [key]: value })); + return ( <> {(isLoading || saveMutation.isPending || deleteMutation.isPending) && } @@ -147,162 +140,146 @@ const AppUsers = () => { title="App Users" subtitle="Live · Staff Logins" live - action={ - - } + action={ - - + !next && setDialogOpen(false)} width={480} purpose="form"> + !next && setDialogOpen(false)} + startContent={ + + + + } + /> + } + content={ + + + + + + + + + + + + + } + footer={ + + + ); diff --git a/src/pages/nearle/clientPricing/clientPricing.js b/src/pages/nearle/clientPricing/clientPricing.js index 4bd6185..02a0689 100644 --- a/src/pages/nearle/clientPricing/clientPricing.js +++ b/src/pages/nearle/clientPricing/clientPricing.js @@ -1,32 +1,4 @@ -import React, { useMemo, useRef, useState } from 'react'; -import { - Autocomplete, - Avatar, - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - IconButton, - InputLabel, - MenuItem, - Paper, - Select, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; +import { useMemo, useState } from 'react'; import { MdLocalOffer, MdGroups, @@ -44,22 +16,44 @@ import { MdCalculate } from 'react-icons/md'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { enqueueSnackbar } from 'notistack'; + +import { Grid } from '@astryxdesign/core/Grid'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Card } from '@astryxdesign/core/Card'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { Tooltip } from '@astryxdesign/core/Tooltip'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { NumberInput } from '@astryxdesign/core/NumberInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table'; import Loader from 'components/Loader'; +import TableLoader from 'components/nearle_components/TableLoader'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { getallpricing, getTenants, fetchAppLocations } from 'pages/api/api'; import { createAdminPricing, updateAdminPricing, deleteAdminPricing, simulatePricing } from 'pages/api/doormileApi'; -import { enqueueSnackbar } from 'notistack'; -import { DT, tint, soft, edge } from 'themes/dt/tokens'; -import { AccentAvatar } from 'themes/dt/primitives'; +import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport'; +import { DT } from 'themes/dt/tokens'; +import { TableScroll, AccentAvatar } from 'themes/dt/primitives'; -const BRAND = '#000000'; +const BRAND = DT.brand; const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van']; +const VEHICLE_TYPE_OPTIONS = VEHICLE_TYPES.map((v) => ({ value: v, label: v })); +const STATUS_OPTIONS = [ + { value: 'Active', label: 'Active' }, + { value: 'Inactive', label: 'Inactive' } +]; const formatRupees = (value) => new Intl.NumberFormat('en-IN', { @@ -69,42 +63,17 @@ const formatRupees = (value) => }).format(Number(value) || 0); const MetricPill = ({ label }) => ( - + {label} - + ); -const CategoryChip = ({ icon, label }) => ( - - - {icon} - - {label} - -); +const CategoryChip = ({ icon, label }) => ; const emptyForm = { pricingid: null, - tenant: null, - location: null, + tenant: '', + location: '', vehicletype: 'Bike', baseprice: '', baseweight: '', @@ -120,9 +89,7 @@ const emptyForm = { // ==============================|| Pricing page ||============================== // const ClientsPricing = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const containerRef = useRef(); + const isMobile = useIsNarrowViewport(); const queryClient = useQueryClient(); const [appId, setAppId] = useState(0); const [locaName, setLocoName] = useState('All'); @@ -131,7 +98,7 @@ const ClientsPricing = () => { const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(emptyForm); const [quoteOpen, setQuoteOpen] = useState(false); - const [quoteForm, setQuoteForm] = useState({ tenant: null, location: null, vehicletype: 'Bike', weight: '', distance: '' }); + const [quoteForm, setQuoteForm] = useState({ tenant: '', location: '', vehicletype: 'Bike', weight: '', distance: '' }); const { data: pricing = [], isLoading } = useQuery({ queryKey: ['getallpricing', appId], @@ -148,6 +115,11 @@ const ClientsPricing = () => { [locations] ); const locationOptions = useMemo(() => (locations || []).filter((l) => l.applocationid), [locations]); + const tenantSelectOptions = useMemo(() => (tenants || []).map((t) => ({ value: String(t.tenantid), label: t.tenantname || `Tenant #${t.tenantid}` })), [tenants]); + const locationSelectOptions = useMemo( + () => locationOptions.map((l) => ({ value: String(l.applocationid), label: l.locationname || `Zone #${l.applocationid}` })), + [locationOptions] + ); const tenantName = (row) => row.tenantname || tenantMap.get(row.tenantid)?.tenantname || (row.tenantid ? `Tenant #${row.tenantid}` : '—'); const zoneName = (row) => @@ -195,9 +167,8 @@ const ClientsPricing = () => { { key: 'active', label: 'Active Rules', color: '#10b981', icon: MdOutlinePlace, value: stats.activeCount } ]; - const opentoast = (message, variant = 'error') => { + const opentoast = (message, variant = 'error') => enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); - }; const savePricingMutation = useMutation({ mutationFn: (payload) => (payload.pricingid ? updateAdminPricing(payload.pricingid, payload.data) : createAdminPricing(payload.data)), @@ -236,13 +207,13 @@ const ClientsPricing = () => { }); const handleQuote = () => { - if (!quoteForm.tenant?.tenantid || !quoteForm.distance) { + if (!quoteForm.tenant || !quoteForm.distance) { opentoast('Choose Tenant and fill Distance'); return; } quoteMutation.mutate({ - tenantid: quoteForm.tenant.tenantid, - applocationid: quoteForm.location?.applocationid, + tenantid: Number(quoteForm.tenant), + applocationid: quoteForm.location ? Number(quoteForm.location) : undefined, vehicletype: quoteForm.vehicletype, weight: Number(quoteForm.weight) || 0, distance: Number(quoteForm.distance) || 0 @@ -257,8 +228,8 @@ const ClientsPricing = () => { const openEditDialog = (row) => { setForm({ pricingid: row.pricingid, - tenant: tenantMap.get(row.tenantid) || (row.tenantid ? { tenantid: row.tenantid, tenantname: tenantName(row) } : null), - location: locationMap.get(row.applocationid) || null, + tenant: row.tenantid ? String(row.tenantid) : '', + location: row.applocationid ? String(row.applocationid) : '', vehicletype: row.vehicletype || 'Bike', baseprice: row.baseprice ?? '', baseweight: row.baseweight ?? '', @@ -279,7 +250,7 @@ const ClientsPricing = () => { }; const handleSave = () => { - if (!form.tenant?.tenantid) { + if (!form.tenant) { opentoast('Choose Tenant'); return; } @@ -291,8 +262,8 @@ const ClientsPricing = () => { // effectivefrom is required and confirmed live to reject a bare // YYYY-MM-DD date ("invalid request body") — needs a full ISO datetime. const data = { - tenantid: form.tenant.tenantid, - applocationid: form.location?.applocationid, + tenantid: Number(form.tenant), + applocationid: form.location ? Number(form.location) : undefined, vehicletype: form.vehicletype, baseprice: Number(form.baseprice), baseweight: Number(form.baseweight) || 0, @@ -308,592 +279,375 @@ const ClientsPricing = () => { savePricingMutation.mutate({ pricingid: form.pricingid, data }); }; + const set = (key) => (value) => setForm((f) => ({ ...f, [key]: value })); + const setQuote = (key) => (value) => setQuoteForm((f) => ({ ...f, [key]: value })); + return ( <> {(isLoading || savePricingMutation.isPending || deletePricingMutation.isPending) && } - {/* ============================================= || Header || ============================================= */} - - - - + + + - - + + + } + /> - {/* ============================================= || Quote price dialog || ============================================= */} - setQuoteOpen(false)} maxWidth="xs" fullWidth> - Quote Price - - - - - Tenant - option.tenantname || ''} - value={quoteForm.tenant} - onChange={(e, value) => setQuoteForm((f) => ({ ...f, tenant: value }))} - renderInput={(params) => } - /> - - - - - Zone (optional) - option.locationname || ''} - value={quoteForm.location} - onChange={(e, value) => setQuoteForm((f) => ({ ...f, location: value }))} - renderInput={(params) => } - /> - - - - - Vehicle Type - - - - - - Weight (kg) - setQuoteForm((f) => ({ ...f, weight: e.target.value }))} - /> - - - - - Distance (km) - setQuoteForm((f) => ({ ...f, distance: e.target.value }))} - /> - - + { + if (!next) { + setQuoteOpen(false); + quoteMutation.reset(); + setQuoteForm({ tenant: '', location: '', vehicletype: 'Bike', weight: '', distance: '' }); + } + }} + width={480} + purpose="form" + > + { + if (!next) { + setQuoteOpen(false); + quoteMutation.reset(); + setQuoteForm({ tenant: '', location: '', vehicletype: 'Bike', weight: '', distance: '' }); + } + }} + startContent={ + + + + } + /> + } + content={ + + + + + + + setQuote('weight')(v ?? '')} isOptional /> + setQuote('distance')(v ?? '')} isRequired /> + - {quoteMutation.isPending && ( - - - Calculating… - - - )} + {quoteMutation.isPending && ( + Calculating… + )} - {quoteMutation.data && ( - - - {quoteMutation.data.success === false ? ( - - {quoteMutation.data.message || 'No quote available'} - - ) : ( - - {Object.entries(quoteMutation.data.data || quoteMutation.data).map(([key, value]) => ( - - - {key} - - - {typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')} - - - ))} - - )} - - - )} - - - - - - + {quoteMutation.data && ( + + {quoteMutation.data.success === false ? ( + {quoteMutation.data.message || 'No quote available'} + ) : ( + + {Object.entries(quoteMutation.data.data || quoteMutation.data).map(([key, value]) => ( + + + {key} + + {typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')} + + ))} + + )} + + )} + + + } + footer={ + + + ); diff --git a/src/pages/nearle/clients/Tenants.js b/src/pages/nearle/clients/Tenants.js index 303236c..65f50d9 100644 --- a/src/pages/nearle/clients/Tenants.js +++ b/src/pages/nearle/clients/Tenants.js @@ -1,56 +1,11 @@ -import React, { useEffect, useState, useRef, Fragment } from 'react'; +import { useEffect, useState, useRef, useMemo, Fragment } from 'react'; import MainCard from 'components/MainCard'; -import { useTheme } from '@mui/material/styles'; import Loader from 'components/Loader'; -import { Empty } from 'antd'; import dayjs from 'dayjs'; import { enqueueSnackbar } from 'notistack'; import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; -import { - Stack, - Table, - TableCell, - TableContainer, - TableHead, - TableRow, - TableBody, - Typography, - IconButton, - Box, - Grid, - TextField, - Tooltip, - Collapse, - Tab, - Tabs, - Divider, - Card, - CardContent, - CardActions, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormLabel, - TablePagination, - Skeleton, - Avatar, - Paper, - Select, - MenuItem, - useMediaQuery -} from '@mui/material'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { - EyeOutlined, - EyeInvisibleOutlined, - EditOutlined, - IssuesCloseOutlined, - StopOutlined, - CloseOutlined -} from '@ant-design/icons'; -import { FaRegCheckCircle, FaRegAddressCard } from 'react-icons/fa'; +import { FaRegCheckCircle } 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'; @@ -68,14 +23,39 @@ import { MdAdd, MdEdit, MdDeleteOutline, - MdPeopleAlt + MdPeopleAlt, + MdVisibility, + MdVisibilityOff, + MdBlock, + MdClose } from 'react-icons/md'; import { LuMail } from 'react-icons/lu'; import { BiUser } from 'react-icons/bi'; + +import { Grid } from '@astryxdesign/core/Grid'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Card } from '@astryxdesign/core/Card'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Divider } from '@astryxdesign/core/Divider'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { NumberInput } from '@astryxdesign/core/NumberInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { TabList, Tab } from '@astryxdesign/core/TabList'; +import { Pagination } from '@astryxdesign/core/Pagination'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table'; + import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; +import TableLoader from 'components/nearle_components/TableLoader'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getalltenants } from 'pages/api/api'; import { @@ -91,29 +71,37 @@ import { updateTenantCustomer, deleteTenantCustomer } from 'pages/api/doormileApi'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import { DT, tint, soft, edge } from 'themes/dt/tokens'; -import { AccentAvatar } from 'themes/dt/primitives'; +import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport'; +import { DT } from 'themes/dt/tokens'; +import { TableScroll, AccentAvatar } from 'themes/dt/primitives'; -// Status palette — drives the pill tabs and per-row badges. +const BRAND = DT.brand; + +// Status palette — drives the pill tabs and per-row badges. Tenant status is +// a business-entity state (active/pending approval/inactive), a different +// domain from the order-lifecycle registry in themes/dt/status.js, so it +// stays local here the same way riders.js's rider-presence STATUS_META does. 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' } + active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, badge: 'green' }, + pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, badge: 'yellow' }, + inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel, badge: 'red' } }; const STATUS_TABS = [ - { status: 'active', countKey: 'active', tabStatus: 'Active' }, - { status: 'pending', countKey: 'pending', tabStatus: 'Pending' }, + { 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: MdOutlineCheckCircle, countKey: 'active' }, - { key: 'pending', label: 'Pending Approval', color: '#f59e0b', icon: MdOutlinePendingActions, countKey: 'pending' }, - { key: 'inactive', label: 'Inactive Tenants', color: '#ef4444', icon: MdOutlineCancel, countKey: 'inactive' } + { key: 'total', label: 'Total Tenants', color: BRAND, icon: MdGroups, countKey: 'total' }, + { key: 'active', label: 'Active Tenants', color: '#10b981', icon: MdOutlineCheckCircle, countKey: 'active' }, + { key: 'pending', label: 'Pending Approval', color: '#f59e0b', icon: MdOutlinePendingActions, countKey: 'pending' }, + { key: 'inactive', label: 'Inactive Tenants', color: '#ef4444', icon: MdOutlineCancel, countKey: 'inactive' } ]; +const VEHICLE_TYPE_OPTIONS = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => ({ value: v, label: v })); + // ==============================|| Tenant locations (branches/sites) dialog ||============================== // // Self-contained — deliberately does not touch the existing openRowIndex1/2 + // Tabs collapse machinery above, which is fragile legacy state. GET/POST/PUT @@ -199,88 +187,69 @@ const TenantLocationsDialog = ({ tenant, open, onClose }) => { }; return ( - - - Locations · {tenant?.tenantname} - - - {isLoading ? ( - - Loading locations… - - ) : locations.length === 0 && !formOpen ? ( - - ) : ( - - {locations.map((loc) => ( - - - - - {loc.locationname} {loc.isprimary && '· Primary'} - - - {[loc.address, loc.city, loc.state, loc.pincode].filter(Boolean).join(', ')} - - - openEdit(loc)}> - - - - - ))} - - )} + !next && onClose()} width={560} purpose="form"> + !next && onClose()} />} + content={ + + {isLoading ? ( + Loading locations… + ) : locations.length === 0 && !formOpen ? ( + } /> + ) : ( + + {locations.map((loc) => ( + + + + + {loc.locationname} {loc.isprimary && '· Primary'} + + + {[loc.address, loc.city, loc.state, loc.pincode].filter(Boolean).join(', ')} + + + } onClick={() => openEdit(loc)} /> + + + ))} + + )} - {formOpen && ( - - - setForm((f) => ({ ...f, locationname: e.target.value }))} - /> - - - setForm((f) => ({ ...f, pincode: e.target.value }))} /> - - - setForm((f) => ({ ...f, address: e.target.value }))} /> - - - setForm((f) => ({ ...f, city: e.target.value }))} /> - - - setForm((f) => ({ ...f, state: e.target.value }))} /> - - - )} - - - {formOpen ? ( - <> - - - - ) : ( - <> - - - - )} - + {formOpen && ( + + setForm((f) => ({ ...f, locationname: v }))} /> + setForm((f) => ({ ...f, pincode: v }))} /> + setForm((f) => ({ ...f, address: v }))} /> + setForm((f) => ({ ...f, city: v }))} /> + setForm((f) => ({ ...f, state: v }))} /> + + )} + + } + footer={ + + {formOpen ? ( + + ); }; @@ -401,102 +370,147 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => { }; return ( - - Customers · {tenant?.tenantname} - - {isLoading ? ( - - Loading customers… - - ) : customers.length === 0 && !formOpen ? ( - - ) : ( - - {customers.map((c) => ( - - - - - {c.firstname} {c.lastname} - - - {[c.contactno, c.email].filter(Boolean).join(' · ')} - - - - openEdit(c)}> - - - handleDelete(c)} sx={{ color: '#ef4444' }}> - - - - - - ))} - - )} + !next && onClose()} width={560} purpose="form"> + !next && onClose()} />} + content={ + + {isLoading ? ( + Loading customers… + ) : customers.length === 0 && !formOpen ? ( + } /> + ) : ( + + {customers.map((c) => ( + + + + + {c.firstname} {c.lastname} + + + {[c.contactno, c.email].filter(Boolean).join(' · ')} + + + + } onClick={() => openEdit(c)} /> + } + onClick={() => handleDelete(c)} + /> + + + + ))} + + )} - {formOpen && ( - - - setForm((f) => ({ ...f, firstname: e.target.value }))} /> - - - setForm((f) => ({ ...f, lastname: e.target.value }))} /> - - - setForm((f) => ({ ...f, phone: e.target.value }))} /> - - - setForm((f) => ({ ...f, email: e.target.value }))} /> - - - )} - - - {formOpen ? ( - <> - - - - ) : ( - <> - - - - )} - + {formOpen && ( + + setForm((f) => ({ ...f, firstname: v }))} /> + setForm((f) => ({ ...f, lastname: v }))} /> + setForm((f) => ({ ...f, phone: v }))} /> + setForm((f) => ({ ...f, email: v }))} /> + + )} + + } + footer={ + + {formOpen ? ( + + ); }; +// A single icon + value + caption row used throughout the tenant detail/edit panels. +const InfoRow = ({ icon: Icon, value, caption }) => ( + + + + {value || '—'} + + {caption} + + + +); + +const PricingTable = ({ list }) => ( + + + + + # + Vehicle + Base Price + Price/Km + Price/Kg + Handling + Status + + + + {list?.length === 0 ? ( + + + } /> + + + ) : ( + list.map((val, i) => ( + + {i + 1} + {val.vehicletype || '—'} + {val.baseprice ?? '—'} + {val.priceperkm ?? '—'} + {val.priceperkg ?? '—'} + {val.handlingcharges ?? '—'} + {val.status || '—'} + + )) + )} + +
+
+); + // ==============================|| Starts here||============================== // const Clients1 = () => { const textFieldRef = useRef(null); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); - const [rowsPerPage, setRowsPerPage] = React.useState(10); - // const [tenantList, settenantList] = useState([]); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const [rowsPerPage, setRowsPerPage] = useState(10); + const isMobile = useIsNarrowViewport(); const [isloader, setisloader] = useState(false); const [appId, setAppId] = useState(0); const [locaName, setLocoName] = useState(''); const [value0, setValue0] = useState(0); - const [value1, setValue1] = useState(0); - const [value2, setValue2] = useState(0); + const [value1, setValue1] = useState('0'); + const [value2, setValue2] = useState('0'); const [selectedTenid, setSelectedtenid] = useState(null); const [clientpricelist, setClientpricelist] = useState([]); const [status, setstatus] = useState('active'); @@ -511,29 +525,15 @@ const Clients1 = () => { const [editClient, setEditClient] = useState({}); const [page, setPage] = useState(0); - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; const dialogclose = () => { setDialogopen(false); }; - const handleChange = (event, newValue) => { + const handleChange = (newValue) => { setValue0(newValue); setSearchword(''); setPage(0); }; - const handleChange1 = (event, newValue1) => { - setValue1(newValue1); - }; - const handleChange2 = (event, newValue2) => { - setValue2(newValue2); - }; const [openRowIndex1, setOpenRowIndex1] = useState(null); // Initially no row is open for collapsible section 1 const [openRowIndex2, setOpenRowIndex2] = useState(null); // Initially no row is open for collapsible section 2 @@ -647,7 +647,7 @@ const Clients1 = () => { // No tenant-tab-counts endpoint in the new API (gettenantsummary always // returns null) — derive the Active/Pending/Inactive counts client-side // from the full tenant list instead, which getalltenants already fetches. - const tenantStatusCounts = React.useMemo(() => { + const tenantStatusCounts = useMemo(() => { const counts = { active: 0, pending: 0, inactive: 0 }; (tenantList || []).forEach((t) => { const s = String(t.status || '').toLowerCase(); @@ -655,7 +655,7 @@ const Clients1 = () => { else if (s === 'pending') counts.pending += 1; else if (s === 'inactive') counts.inactive += 1; }); - return counts; + return { ...counts, total: (tenantList || []).length }; }, [tenantList]); // getalltenants() takes no filter args (see api.js) — the status tabs and @@ -663,7 +663,7 @@ const Clients1 = () => { // request, so every tab showed the exact same unfiltered list with just a // relabeled status badge. Filter the already-fetched list client-side // instead, matching the same real `status` field tenantStatusCounts uses. - const filteredTenantList = React.useMemo(() => { + const filteredTenantList = useMemo(() => { const activeStatus = STATUS_TABS[value0]?.status; const q = debouncedSearch.trim().toLowerCase(); return (tenantList || []).filter((t) => { @@ -801,1539 +801,537 @@ const Clients1 = () => { const activeTabMeta = STATUS_META[STATUS_TABS[value0]?.status] || STATUS_META.active; + const renderRowActions = (row, index, expanded, size = 'sm') => ( + + {value0 == 0 && ( + } + onClick={() => { + setSelectedCustomer(row); + setSelectedtenid(row.tenantid); + setAppId(row.applocationid); + setTimeout(() => { + tenantupdate(row.tenantid); + }, 100); + }} + /> + )} + {value0 == 1 && ( + } + onClick={() => { + setSelectedCustomer(row); + setDialogopen(true); + setSelectedtenid(row.tenantid); + setAppId(row.applocationid); + getAppPricing(row.applocationid); + }} + /> + )} + {value0 == 2 && ( + } + onClick={() => { + setSelectedCustomer(row); + setSelectedtenid(row.tenantid); + setAppId(row.applocationid); + setTimeout(() => { + tenantupdate(row.tenantid); + }, 100); + }} + /> + )} + : } + onClick={() => { + setSelectedCustomer(row); + handleCollapseToggle1(index); + setOpenRowIndex2(-1); + setSelectedtenid(row.tenantid); + }} + /> + } + onClick={() => setLocationsDialogTenant(row)} + /> + } + onClick={() => setCustomersDialogTenant(row)} + /> + {value0 !== 1 && ( + : } + onClick={() => { + setSelectedCustomer(row); + handleCollapseToggle2(index); + setOpenRowIndex1(-1); + setSelectedtenid(row.tenantid); + setAppId(row.applocationid); + }} + /> + )} + + ); + + const renderDetailsPanel = (row) => ( + + } /> + } /> + + } + > + {value1 == '0' && ( + + + + + + + + + + )} + {value1 == '1' && ( + + + + )} + + ); + + const renderEditPanel = (row) => ( + + { + setValue2(v); + if (v === '1') getAppPricing(selectedCustomer.applocationid); + }} + size="sm" + > + } /> + {value0 !== 1 && } />} + + {value2 == '1' && ( + - )} - - } - > - {/* ============================================= || Edit Details || ============================================= */} - {value2 == 0 && ( - - - {/* =========================|| Edit Details (right) || =========================*/} - - - - - {/* =========================|| Tenant || =========================*/} - - - - - - - { - setEditClient({ - ...editClient, - tenantname: e.target.value - }); - }} - sx={{ mt: 2 }} - /> - - - {' '} - {/* =========================|| Contact Person || =========================*/} - - - - - - - { - setEditClient({ - ...editClient, - firstname: e.target.value - }); - }} - sx={{ mt: 2 }} - /> - - - - {/* =========================|| Contact Number || =========================*/} - - - - - - - { - setEditClient({ - ...editClient, - primarycontact: e.target.value - }); - }} - sx={{ mt: 2 }} - /> - - - - {/* =========================|| E-Mail || =========================*/} - - - - - - - { - setEditClient({ - ...editClient, - primaryemail: e.target.value - }); - }} - sx={{ mt: 2 }} - /> - - - - {/* =========================|| Address || =========================*/} - - - - - - - - setEditClient((prev) => ({ - ...prev, - address: text - })) - } - onPlaceSelected={handleAddressPlaceSelected} - /> - - - - {/* =========================|| category name || =========================*/} - {/* - - - - - - { - setEditClient({ - ...editClient, - subcategoryname: e.target.value - }); - }} - /> - - - - - - { - setEditClient({ - ...editClient, - suburb: e.target.value - }); - }} - value={selectedCustomer.suburb} - /> - - - */} - {/* =========================|| city || =========================*/} - - - - - - - { - setEditClient({ - ...editClient, - city: e.target.value - }); - }} - value={city == '' ? selectedCustomer.city : city} - /> - - - - - - { - { - setEditClient({ - ...editClient, - postcode: e.target.value - }); - } - }} - value={zipcode == '' ? selectedCustomer.postcode : zipcode} - /> - - - - {/* =========================|| latitude || =========================*/} - - - - - - - - - - - - - { - // setLongi(e.target.value.toString()) - // setEditClient({ - // ...editClient, - // longitude: - // e.target.value.toString(), - // }); - // }} - /> - - - - - - {/* =========================|| update || =========================*/} - - - - - - - - - )} - {/* ============================================= || Edit Pricing || ============================================= */} - {value2 == 1 && ( - - - - - - # - Vehicle - Base Price - Price/Km - Price/Kg - Handling - Status - - - - {clientpricelist?.length === 0 ? ( - - - - - - ) : ( - clientpricelist.map((val, i) => ( - - {i + 1} - {val.vehicletype || '—'} - {val.baseprice ?? '—'} - {val.priceperkm ?? '—'} - {val.priceperkg ?? '—'} - {val.handlingcharges ?? '—'} - {val.status || '—'} - - )) - )} - -
-
-
- )} -
- {/* */} - - - )} - + {expanded && ( +
+ + + +
+ )} + ); }) )} - - - + + )} - {/* ============================================= || Mobile cards (xs only) || ============================================= */} - {isMobile && ( - - {getalltenantsIsLoading && ( - - - - - )} + {!searchword && filteredTenantList.length > 0 && ( + <> + + + setPage(p - 1)} + totalItems={filteredTenantList.length} + pageSize={rowsPerPage} + pageSizeOptions={[5, 10, 25, 50, 100]} + onPageSizeChange={(size) => { + setRowsPerPage(size); + setPage(0); + }} + variant="count" + /> + + + )} +
+ - {filteredTenantList.length == 0 && !isloader ? ( - - - - - - No tenants to show - - - {`No ${activeTabMeta.label.toLowerCase()} tenants for this filter.`} - - - ) : ( - filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => { - const rowStatusKey = String(row.status || '').toLowerCase(); - const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active; - const RowStatusIcon = rowStatusMeta.icon; - const expanded = openRowIndex1 === index; - return ( - - - - - - - - {row.tenantname} - - - - - - - {rowStatusMeta.label} - - - - - - {value0 == 0 && ( - { - setSelectedCustomer(row); - setSelectedtenid(row.tenantid); - setAppId(row.applocationid); - setTimeout(() => { - tenantupdate(row.tenantid); - }, 100); - }} - > - - - )} - {value0 == 1 && ( - { - setSelectedCustomer(row); - setDialogopen(true); - setSelectedtenid(row.tenantid); - setAppId(row.applocationid); - getAppPricing(row.applocationid); - }} - > - - - )} - {value0 == 2 && ( - { - setSelectedCustomer(row); - setSelectedtenid(row.tenantid); - setAppId(row.applocationid); - setTimeout(() => { - tenantupdate(row.tenantid); - }, 100); - }} - > - - - )} - { - setSelectedCustomer(row); - handleCollapseToggle1(index); - setOpenRowIndex2(-1); - setSelectedtenid(row.tenantid); - }} - > - {expanded ? : } - - setLocationsDialogTenant(row)} - > - - - setCustomersDialogTenant(row)} - > - - - {value0 !== 1 && ( - { - setSelectedCustomer(row); - setSelectedtenid(row.tenantid); - setAppId(row.applocationid); - getAppPricing(row.applocationid); - setDialogopen(true); - }} - > - - - )} - - - } - > - - - - - - - - {expanded && ( - - - - - - )} - - ); - }) - )} - - )} - - {/* ============================================= || Pagination| ============================================= */} - {!searchword && filteredTenantList.length > 0 && ( - <> - - - - )} - - {/* // ==============================||( Client Pricing ) dialog (dialogopen) ||============================== // */} - - - {`Client Pricing - (${selectedCustomer.tenantname})`} - - - - - - Select Slab - - - - - Base Price - { - setSelectedPricing({ - ...selectedPricing, - baseprice: e.target.value - }); - }} + placeholder="Choose vehicle type" + /> + setSelectedPricing({ ...selectedPricing, baseprice: v ?? 0 })} + /> + setSelectedPricing({ ...selectedPricing, priceperkm: v ?? 0 })} + /> + setSelectedPricing({ ...selectedPricing, baseweight: v ?? 0 })} + /> + setSelectedPricing({ ...selectedPricing, priceperkg: v ?? 0 })} + /> + setSelectedPricing({ ...selectedPricing, basedistance: v ?? 0 })} + /> + setSelectedPricing({ ...selectedPricing, handlingcharges: v ?? 0 })} /> - - Price/Km - { - setSelectedPricing({ - ...selectedPricing, - priceperkm: e.target.value - }); - }} - /> - - - - Base Weight (kg) - { - setSelectedPricing({ - ...selectedPricing, - baseweight: e.target.value - }); - }} - /> - - - Price/Kg - { - setSelectedPricing({ - ...selectedPricing, - priceperkg: e.target.value - }); - }} - /> - - - Base Distance (km) - { - setSelectedPricing({ - ...selectedPricing, - basedistance: e.target.value - }); - }} - /> - - - Handling Charges - { - setSelectedPricing({ - ...selectedPricing, - handlingcharges: e.target.value - }); - }} - /> - - - - - - - - - {value2 == 1 && ( - - )} - {value0 == 1 && ( - - )} + + } + footer={ + + + {value2 == '1' && - - - - + /> +
+ + } + /> - setLocationsDialogTenant(null)} - /> + setLocationsDialogTenant(null)} /> - setCustomersDialogTenant(null)} - /> + setCustomersDialogTenant(null)} /> ); }; diff --git a/src/pages/nearle/clients/createCustomer.js b/src/pages/nearle/clients/createCustomer.js index c6e06d2..328e0b9 100644 --- a/src/pages/nearle/clients/createCustomer.js +++ b/src/pages/nearle/clients/createCustomer.js @@ -1,33 +1,28 @@ import { useEffect, useState } from 'react'; - -// material-ui -import { - Autocomplete, - Avatar, - Button, - Grid, - InputLabel, - MenuItem, - Paper, - Select, - Stack, - TextField, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; -import { MdPersonAddAlt1 } from 'react-icons/md'; +import { useNavigate } from 'react-router'; import { useMutation, useQuery } from '@tanstack/react-query'; +import { enqueueSnackbar } from 'notistack'; +import { MdPersonAddAlt1 } from 'react-icons/md'; + +import { Grid } from '@astryxdesign/core/Grid'; +import { HStack } from '@astryxdesign/core/HStack'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Card } from '@astryxdesign/core/Card'; +import { Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { Divider } from '@astryxdesign/core/Divider'; -// project import -import MainCard from 'components/MainCard'; -import { createTenantCustomer, getAdminTenants } from 'pages/api/doormileApi'; import Loader from 'components/Loader'; +import PageHeader from 'components/nearle_components/PageHeader'; import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import { enqueueSnackbar } from 'notistack'; -import { useNavigate } from 'react-router'; -import { DT, tint } from 'themes/dt/tokens'; +import { createTenantCustomer, getAdminTenants } from 'pages/api/doormileApi'; +import { DT } from 'themes/dt/tokens'; +import { AccentAvatar } from 'themes/dt/primitives'; + +const BRAND = DT.brand; // Same field set as nearle_console_express's createCustomer.js (Location, // Choose Client, Name, Phone, Email, Door No, Address, Location/City/State/ @@ -43,8 +38,6 @@ import { DT, tint } from 'themes/dt/tokens'; // ignored server-side — unconfirmed, this endpoint isn't in the "confirmed // end-to-end" list. const Createcustomer = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); const navigate = useNavigate(); const loggedInTenantId = localStorage.getItem('tenantid'); @@ -66,11 +59,12 @@ const Createcustomer = () => { const [appLocaLat, setAppLocaLat] = useState(); const [appLocaLng, setAppLocaLng] = useState(); - const { data: tenantList = [], isLoading: tenantListLoading } = useQuery({ + const { data: tenantList = [] } = useQuery({ queryKey: ['admin-tenants-picker'], queryFn: getAdminTenants, enabled: isStaffLogin }); + const tenantOptions = (tenantList || []).map((t) => ({ value: String(t.tenantid), label: t.tenantname || `Tenant #${t.tenantid}` })); // LocationAutocomplete only reports back applocationid/locationname — it // caches the full hub list (with lat/lng) in localStorage under a @@ -125,13 +119,8 @@ const Createcustomer = () => { }); }; - const opentoast = (message) => { - enqueueSnackbar(message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }; + const opentoast = (message) => + enqueueSnackbar(message, { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); const createCustomerMutation = useMutation({ mutationFn: (payload) => createTenantCustomer(payload), @@ -147,9 +136,7 @@ const Createcustomer = () => { opentoast(res.message || 'Could not create customer'); } }, - onError: (err) => { - opentoast(err.response?.data?.message || err.message || 'Failed to create customer'); - } + onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to create customer') }); const createprofile = () => { @@ -174,238 +161,86 @@ const Createcustomer = () => { <> {createCustomerMutation.isPending && } - - - - - - - Create Customer - - - - - - - - - {/* ===================================================== || Location & Client || ===================================================== */} - - - Location - - - - {isStaffLogin && ( - - - Choose Client - option?.tenantname || ''} - renderInput={(params) => } - onChange={(e, val) => setTid(val ? val.tenantid : '')} - /> - - - )} + + + + } + /> - {/* ===================================================== || Name / Phone / Email || ===================================================== */} - - - Name - setFirstname(e.target.value)} - value={firstname} - autoComplete="off" - /> - - - - - Last Name - setLastname(e.target.value)} - value={lastname} - autoComplete="off" - /> - - - - - Phone Number - - - { - if (e.target.value.toString().length <= 10) { - setPhone(e.target.value); - } - }} - value={phone} - autoComplete="off" - /> - - - - - - Email - setEmail(e.target.value)} - value={email} - autoComplete="off" - /> - - - - {/* ===================================================== || Door No || ===================================================== */} - - - Door No - setDoorno(e.target.value)} - value={doorno} - autoComplete="off" - /> - - - - {/* ===================================================== || Address section — collected, not persisted || ===================================================== */} - - - The backend doesn't store an address against a customer yet — these fields aren't required and won't be saved on submit. - - - - - Address - setInputValue2(text)} - onPlaceSelected={handlePlaceSelected} - bias={{ lat: appLocaLat, lng: appLocaLng }} - /> - - - - - Location - setPickCust((p) => ({ ...p, suburb: e.target.value }))} - value={pickCust.suburb || ''} - autoComplete="off" - /> - - - - - City - setPickCust((p) => ({ ...p, city: e.target.value }))} - value={pickCust.city || ''} - autoComplete="off" - /> - - - - - State - setPickCust((p) => ({ ...p, state: e.target.value }))} - value={pickCust.state || ''} - autoComplete="off" - /> - - - - - Post Code - setPickCust((p) => ({ ...p, postcode: e.target.value }))} - value={pickCust.postcode || ''} - autoComplete="off" - /> - - - - - Landmark - setLandmark(e.target.value)} value={landmark} autoComplete="off" /> - - - - - Latitude - - - - - - Longitude - - - - - + + + + + + Location + + + + {isStaffLogin && ( + setTid(v || '')} + placeholder="Choose Client" + hasSearch + /> + )} + + + { + const digits = v.replace(/\D/g, ''); + if (digits.length <= 10) setPhone(digits); + }} + isRequired + /> + + - - - - + + + + + The backend doesn't store an address against a customer yet — these fields aren't required and won't be saved on submit. + + + + + + setPickCust((p) => ({ ...p, suburb: v }))} /> + setPickCust((p) => ({ ...p, city: v }))} /> + setPickCust((p) => ({ ...p, state: v }))} /> + setPickCust((p) => ({ ...p, postcode: v.replace(/\D/g, '') }))} + /> + + + - - + + + - - - - + + + + + + - - + + + + + + ) : ( + + + + + + + + + )} + + } + footer={ + + + + + - - + + The backend doesn't store an address against a customer yet — these fields aren't required and won't be saved on submit. + + + + + + setPickAddress((p) => ({ ...p, suburb: v }))} /> + setPickAddress((p) => ({ ...p, city: v }))} /> + setPickAddress((p) => ({ ...p, state: v }))} /> + setPickAddress((p) => ({ ...p, postcode: v }))} /> + setPickAddress((p) => ({ ...p, landmark: v }))} /> + + + + + + } + footer={ + + + - - - - - - {/* =============================== || change rider dialog || =============================== */} - setChangeDialogOpen(false)} - maxWidth="sm" - fullWidth - PaperProps={{ - elevation: 0, - sx: { - borderRadius: 3, - border: '1px solid', - borderColor: DT.borderSubtle, - boxShadow: DT.shadowPop, - overflow: 'hidden' } - }} - > - - - - - - - - - Change Rider - - - Assign this delivery to a different rider - - - - - - { - const name = option?.displayname || option?.authname || ''; - return option?.contactno ? `${name} (${option.contactno})` : name; - }} - onChange={(e, value) => { - setSelectedRider(value); - logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None'); - }} - renderInput={(params) => ( - - - - - - ) + content={ + +