Compare commits

...

14 Commits

Author SHA1 Message Date
49e86a4428 feat: console fully wired to Doormile backend
- customers.js: edit dialog calls PATCH /admin/customers/:id (live)
- createclient.js: fixed ReferenceError, posts to /crm/clients
- createCustomer.js: redirects to customers list (no B2C create flow)

Console status:
 Login, Dashboard, Hubs, Bookings, Consignments
 Milers (CRUD), Clients (CRUD), Customers (view + edit)
 Cancel booking, Auto-assign, Status updates
Phase 3 pending: Dispatch/Live Operations

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:22:45 +05:30
18d35a0ad2 fix: customer edit dialog field names + safe stub
- pre-fill from confirmed fields (name/phone/email)
- submit stubbed to toast until PATCH /admin/customers/:id exists
- flagged in comment for Phase 3 backend work

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:10:32 +05:30
709a06274a feat: console Phase 2 final - live cancel and customer endpoints
- cancelOrder/cancelDeliveryAPI: real POST /admin/bookings/:id/cancel
- getallcustomers/getcustomersummary: real /admin/customers, aliasing
  appcustomerid -> userid for existing call sites
- BookingDetail: confirmed field names from live backend, cancel button
  now hits the live endpoint
- customers.js: table columns show Name/Phone/Email/Total Bookings/Joined
  using appcustomerid/name/phone/email/totalbookings/createdat

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 11:48:45 +05:30
1a71732396 feat: Doormile console Phase 2 complete
Part A: remaining api.js legacy functions replaced
  - fetchRidersList, cancelOrder/Delivery, changeRiderAPI
  - fetchPercentageAPI, fetchCountAPI → Doormile status counts
  - getpricinglist, fetchLocations, fetchRidersLogs, getusers
  - fetchPaymentType, getorderdetails

Part B: deliveries.js status tabs → Doormile statuses

Part C: 3 new pages
  - pages/nearle/hubs/Hubs.js (hub CRUD + miler counts)
  - pages/nearle/bookings/BookingDetail.js (booking detail + timeline)
  - pages/nearle/dashboard/Dashboard.js (live KPIs + city breakdown)

Part D: routes + menu updated
2026-07-08 19:12:28 +05:30
b8f1cd5f1d fix: point customers page at /admin/customers instead of legacy path
getallcustomers/getcustomersummary still called the old
/customers/getallcustomers and /customers/getcustomersummary paths,
which 403 on api.doormile.com (auth was accepted, route wasn't valid).
Repointed both at /admin/customers per the original conversion plan.

No Doormile customer response shape has been confirmed anywhere in this
work (unlike bookings/clients/milers, which had example shapes given
up front) -- field names in customers.js (firstname/contactno/address/
customerid/etc) are still the old NearlExpress shape and left as-is
rather than guessed. Won't crash, just shows blanks for any field that
doesn't exist on a real Doormile customer. Need the actual response
body to do a real field-mapping pass, same as the login.js episode.

updateCustomer (PUT /customers/update) is still unmapped -- flagged in
a comment rather than guessed at, consistent with cancelOrder.
2026-07-08 17:54:36 +05:30
283c24942d fix: stop the notification-permission toast from stacking
AppContent was declared inline inside App(), making it a new component
reference on every App render -- React would unmount/remount it each
time, re-running its useEffect (generateToken + FCM listener setup) and
re-showing the "Enable notifications" toast, which never auto-dismisses
and has no dedup, so they piled up. Moved AppContent to a stable
top-level component and added a one-per-tab guard on the toast itself
as a second line of defense.
2026-07-08 17:45:32 +05:30
6143644039 remove: invoice, reports (orders/riders summary+logs, profitability), legacy APIs
Dropped entirely rather than adapted to Doormile, per direction: none of
these are needed for the Doormile console right now.

Deleted:
  - src/pages/nearle/invoice/ (invoice.js, invoicePreview.js)
  - src/pages/nearle/reports/ (ordersSummary, ordersDetails, ridersSummary,
    ridersLogs, profitability, plus their only consumers: mapWithRoute.js,
    RiderLocationMap.js, RidersRoutes.js)

Removed the corresponding routes (MainRoutes.js) and sidebar entries
(menu-items/nearle.js: the whole "reports" collapse + "invoice" item),
and their locale keys from en.json.

Removed now-orphaned api.js functions that only those pages called:
getreportsummary, getreportlocationsummary, getriderbydelivery, fetchCount,
fetchRidersSummary, fetchinvoiceinsight, fetchdeliverylist, fetchOrders1,
getallriders. Verified each had zero remaining importers before removing.

Kept fetchorderdetails (still used by orders/details.js) and fetchRidersLogs
(still used by Dispatch.js) -- same name, different consumer than the
deleted reports pages.
2026-07-08 17:40:02 +05:30
c22daee1de feat: rebuild orders.js around Doormile bookings, drop AI-optimiser pipeline
orders.js was the entry point of the whole NearlExpress optimiser flow
(checkbox multi-select -> createOptimisationDeliveries/createAutomationDeliveries
-> navigate to dispatch/Preview.js -> finalCreatedeliveries), none of which
maps to Doormile (assignment happens server-side; there's no manual
route-sequencing/preview step per the original conversion plan).

Stripped: SpeedDial (AI/manual assign, bulk delete), checkbox multi-select,
the two full-screen assign/preview dialogs, tenant/location filters, date
range filter, transport-mode + hyper-tuning selectors, absent-riders picker,
product-line collapse, CSV export, and the embedded <Dispatch> render --
all NearlExpress-only concepts with no Doormile equivalent. This is what
was crashing (.toFixed() on undefined collectionamt/deliverycharge/etc,
fields that don't exist on a Doormile booking).

Rebuilt around the real booking shape (bookingid, bookingreference,
pickupaddress, deliveryaddress, status, createdat, assignedmileruserid)
with the 9 real Doormile statuses, and replaced the whole multi-step
assign flow with a single per-row "auto-assign" action calling
autoAssignBooking(bookingId).

Consequence: OrdersPreview.js and dispatch/Preview.js are no longer
reachable via navigation from this page (still routed directly by URL).
OrdersPreview.js was already unreachable before this change -- its only
navigate() call was commented out. dispatch/Preview.js's own file was not
rewritten; that's Dispatch.js's live-map ecosystem, a separate and much
larger undertaking.

Cancel booking still calls the old /orders/updateorder endpoint -- no
Doormile cancel-booking endpoint has been specified anywhere in this
conversion work, so it's left pointing at the unmapped legacy path
rather than guessed at.
2026-07-08 17:32:31 +05:30
a6e62a70f0 fix: tolerate flat (non-nested) success response shape from /admin/login
The live backend returns success:true + token but doesn't nest user
fields under a data object the way the spec described, causing
'Cannot read properties of undefined (reading email)'. Falls back to
reading fields off data.data, data.user, or the top-level response.
2026-07-08 17:20:25 +05:30
ab59421861 fix: point the actual active login page (login.js) at Doormile auth
LoginRoutes.js is registered before MainRoutes.js and defines its own
un-prefixed '/login' route pointing at pages/nearle/login — it wins the
route match, so login1.js (fixed in the earlier commit) was never being
rendered. login.js still hit jupiter.nearle.app/live/api/v1/users/console/login
with the old multi-step (email lookup -> setup/enter password) flow.

Collapsed to a single POST /admin/login with { email, password,
userfcmtoken }, matching Doormile's one-shot auth response. Visual
layout (branded two-panel screen, "Welcome back" copy) is unchanged.

Confirmed live: curl against api.doormile.com/api/v1/admin/login,
/admin/milers, /crm/clients, /admin/bookings all return the expected
{success,...} shapes and status codes for this code to handle.
2026-07-08 17:06:01 +05:30
c52350df0f feat: convert NearlExpress console to Doormile admin
Phase 1: env config + login auth (Doormile JWT)
Phase 2: full API layer rewrite in api.js
  - All endpoints now point at api.doormile.com/api/v1
  - Removed all jupiter.nearle.app references
  - Removed all routes.workolik.com references (Phase 3)
  - Miler CRUD: GET/POST/PATCH /admin/milers
  - Clients: GET/POST/PATCH /crm/clients
  - Bookings: GET /admin/bookings
  - Auto-assign: POST /hub/bookings/:id/auto-assign

Pages rewritten:
  - riders.js - Doormile miler fields, status mapping
  - createrider.js - fixed broken form, real miler creation
  - editRider.js - stripped bank/vehicle/insurance, hub mapping
  - Tenants.js - stripped pricing dialog, CRM client fields

Menu: renamed Orders/Deliveries/Tenants/Riders/Dispatch
  to Bookings/Consignments/Clients/Milers/Live Operations

Pending Phase 3: dispatch pages, reports, consignments
2026-07-08 15:47:01 +05:30
ed7640ad1e updates on the build regarding the dockerfile and nginx config file 2026-07-08 11:41:56 +05:30
8224f45974 updates and removed on the dead codes 2026-07-07 12:38:20 +05:30
d90609f5a0 updates on the readme file regarding the namechange 2026-07-07 11:19:13 +05:30
94 changed files with 3098 additions and 23515 deletions

7
.env
View File

@@ -6,9 +6,10 @@ REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
## Google Map Key
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
REACT_APP_URL=https://api.doormile.com/api/v1
REACT_APP_URL2=https://api.doormile.com/api/v1
REACT_APP_URL3=https://api.doormile.com/api/v1
REACT_APP_INTERNAL_KEY=doormile-internal-2024
REACT_APP_STAFF_TOKEN=
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk

View File

@@ -1,6 +1,7 @@
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
REACT_APP_URL=https://api.doormile.com/api/v1
REACT_APP_URL2=https://api.doormile.com/api/v1
REACT_APP_URL3=https://api.doormile.com/api/v1
REACT_APP_INTERNAL_KEY=doormile-internal-2024
REACT_APP_STAFF_TOKEN=
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk

View File

@@ -1,5 +1,6 @@
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
REACT_APP_URL2=
REACT_APP_URL=https://api.doormile.com/api/v1
REACT_APP_URL2=https://api.doormile.com/api/v1
REACT_APP_INTERNAL_KEY=doormile-internal-2024
REACT_APP_STAFF_TOKEN=
GENERATE_SOURCEMAP=false

8
.gitignore vendored
View File

@@ -104,3 +104,11 @@ dist
# wincompare file
*.bak
# Env files — only env.staging (no leading dot) is the agreed-shared
# baseline and gets committed. Everything else holds live API keys.
.env
.env.development
.env.staging
.env.production
.env.local

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM nginx:alpine
# Move to Nginx's public folder
WORKDIR /usr/share/nginx/html
# 1. CRUCIAL: Remove Nginx's default "Welcome" page files completely
RUN rm -rf ./*
# 2. Copy your compiled static assets into the root folder.
# NOTE: If your folder is named "dist" instead of "build", change "build/" to "dist/"
COPY build/ .
# 3. Copy your custom Nginx configuration (which you already have in your log)
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,4 +1,4 @@
# NearleXpress - Operator Dispatch Console & Deliveries Portal
# Doormile - Operator Dispatch Console & Deliveries Portal
A high-fidelity, real-time dispatcher command center and order-delivery console built using React, React-Leaflet, TanStack Query, and Material UI.

View File

@@ -1,3 +1,4 @@
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
REACT_APP_URL2=''
REACT_APP_URL='https://api.doormile.com/api/v1'
REACT_APP_URL2='https://api.doormile.com/api/v1'
REACT_APP_INTERNAL_KEY='doormile-internal-2024'
REACT_APP_STAFF_TOKEN=''

18
nginx.conf Normal file
View File

@@ -0,0 +1,18 @@
events {}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# This line forces Nginx to pass routing back to React Router
try_files $uri $uri/ /index.html;
}
}
}

14
package-lock.json generated
View File

@@ -21417,20 +21417,6 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.40",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",

View File

@@ -2,7 +2,6 @@
import Routes from 'routes';
import ThemeCustomization from 'themes';
import Locales from 'components/Locales';
// import RTLLayout from 'components/RTLLayout';
import ScrollTop from 'components/ScrollTop';
import Snackbar from 'components/@extended/Snackbar';
import Notistack from 'components/third-party/Notistack';
@@ -12,11 +11,28 @@ import { generateToken, initFirebaseNotificationListener } from 'firebase_notifi
import InternetStatus from 'components/updateNetworkStatus';
import useInactivityLogout from 'hooks/useInactivityLogout';
// auth-provider
// import { JWTProvider as AuthProvider } from 'contexts/JWTContext';
// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== //
// Was previously declared inline inside App(), which made it a fresh
// component reference on every App render — React would unmount/remount it
// each time, re-firing its useEffect (and the FCM permission toast) on
// every App re-render instead of once per app load.
const AppContent = () => {
useInactivityLogout();
useEffect(() => {
generateToken();
initFirebaseNotificationListener();
}, []);
return (
<>
<Routes />
<Snackbar />
</>
);
};
const App = () => {
const navigate = useNavigate();
useEffect(() => {
@@ -25,41 +41,17 @@ const App = () => {
}
}, [navigate]);
const AppContent = () => {
useInactivityLogout();
useEffect(() => {
generateToken();
initFirebaseNotificationListener();
}, []);
return (
<>
<Routes />
<Snackbar />
</>
);
};
return (
<>
<ThemeCustomization>
<InternetStatus />
{/* <RTLLayout> */}
<Locales>
<ScrollTop>
{/* <AuthProvider> */}
<>
<Notistack>
{/* <Routes />
<Snackbar /> */}
<AppContent />
</Notistack>
</>
{/* </AuthProvider> */}
<Notistack>
<AppContent />
</Notistack>
</ScrollTop>
</Locales>
{/* </RTLLayout> */}
</ThemeCustomization>
</>
);

View File

@@ -1,32 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, CircularProgress, Typography } from '@mui/material';
// ==============================|| PROGRESS - CIRCULAR LABEL ||============================== //
export default function CircularWithLabel({ value, ...others }) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress variant="determinate" value={value} {...others} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography variant="caption" component="div" color="text.secondary">{`${Math.round(value)}%`}</Typography>
</Box>
</Box>
);
}
CircularWithLabel.propTypes = {
value: PropTypes.number
};

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, CircularProgress, Typography, circularProgressClasses } from '@mui/material';
// ==============================|| PROGRESS - CIRCULAR PATH ||============================== //
export default function CircularWithPath({ value, size, variant, thickness, showLabel, pathColor, sx, ...others }) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress
variant="determinate"
sx={{ color: pathColor ? pathColor : 'grey.200' }}
size={size}
thickness={thickness}
{...others}
value={100}
/>
{showLabel && (
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography variant="caption" component="div" color="text.secondary">
{value ? `${Math.round(value)}%` : '0%'}
</Typography>
</Box>
)}
<CircularProgress
variant={variant}
sx={{
...sx,
position: 'absolute',
left: 0,
[`& .${circularProgressClasses.circle}`]: {
strokeLinecap: 'round'
}
}}
size={size}
thickness={thickness}
value={value}
{...others}
/>
</Box>
);
}
CircularWithPath.propTypes = {
value: PropTypes.number,
size: PropTypes.number,
variant: PropTypes.string,
thickness: PropTypes.number,
showLabel: PropTypes.bool,
pathColor: PropTypes.string,
sx: PropTypes.array,
others: PropTypes.array
};

View File

@@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, LinearProgress } from '@mui/material';
// ==============================|| PROGRESS - LINEAR ICON ||============================== //
export default function LinearWithIcon({ icon, value, ...others }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={value} {...others} />
</Box>
<Box sx={{ minWidth: 35 }}>{icon}</Box>
</Box>
);
}
LinearWithIcon.propTypes = {
icon: PropTypes.node,
value: PropTypes.number
};

View File

@@ -1,23 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, LinearProgress, Typography } from '@mui/material';
// ==============================|| PROGRESS - LINEAR WITH LABEL ||============================== //
export default function LinearWithLabel({ value, ...others }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={value} {...others} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
</Box>
</Box>
);
}
LinearWithLabel.propTypes = {
value: PropTypes.number
};

View File

@@ -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 <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
};
RTLLayout.propTypes = {
children: PropTypes.node
};
export default RTLLayout;

View File

@@ -1,10 +0,0 @@
// material-ui
import { styled } from '@mui/material/styles';
const ScrollX = styled('div')({
width: '100%',
overflowX: 'auto',
display: 'block'
});
export default ScrollX;

View File

@@ -1,49 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Grid, Link, Stack, Typography } from '@mui/material';
// assets
import { GlobalOutlined, NodeExpandOutlined } from '@ant-design/icons';
// ==============================|| COMPONENTS - BREADCRUMBS ||============================== //
const ComponentHeader = ({ title, caption, directory, link }) => (
<Box sx={{ pl: 3 }}>
<Stack spacing={1.25}>
<Typography variant="h2">{title}</Typography>
{caption && (
<Typography variant="h6" color="textSecondary">
{caption}
</Typography>
)}
</Stack>
<Grid container spacing={0.75} sx={{ mt: 1.75 }}>
{directory && (
<Grid item xs={12}>
<Typography variant="caption" color="textSecondary">
<NodeExpandOutlined style={{ marginRight: 10 }} />
{directory}
</Typography>
</Grid>
)}
{link && (
<Grid item xs={12}>
<Link variant="caption" color="primary" href={link} target="_blank">
<GlobalOutlined style={{ marginRight: 10 }} />
{link}
</Link>
</Grid>
)}
</Grid>
</Box>
);
ComponentHeader.propTypes = {
title: PropTypes.string,
caption: PropTypes.string,
directory: PropTypes.string,
link: PropTypes.string
};
export default ComponentHeader;

View File

@@ -1,52 +0,0 @@
// material-ui
import { useTheme } from '@mui/material/styles';
import { Fab, Badge } from '@mui/material';
// project import
// assets
// ==============================|| CART ITEMS - FLOATING BUTTON ||============================== //
const FloatingCart = ({ element, count = 0, onClick, sx }) => {
const theme = useTheme();
// const cart = useSelector((state) => state.cart);
// const totalQuantity = sum(cart.checkout.products.map((item) => item.quantity));
return (
<Fab
// component={Link}
// to="/apps/e-commerce/checkout"
onClick={onClick} // ← important
size="large"
sx={{
top: '75%',
position: 'fixed',
right: 0,
zIndex: theme.zIndex.speedDial,
boxShadow: theme.customShadows.primary,
bgcolor: 'primary.lighter',
color: 'primary.main',
borderRadius: '25%',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
'&:hover': {
bgcolor: 'primary.100',
boxShadow: theme.customShadows.primary
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.dark}`,
outlineOffset: 2
},
...sx
}}
>
<Badge showZero badgeContent={count} color="error">
{element}
</Badge>
</Fab>
);
};
export default FloatingCart;

View File

@@ -1,176 +0,0 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, CardContent, CardMedia, Chip, Divider, Grid, Rating, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import IconButton from 'components/@extended/IconButton';
import SkeletonProductPlaceholder from 'components/cards/skeleton/ProductPlaceholder';
import { useDispatch, useSelector } from 'store';
import { addProduct } from 'store/reducers/cart';
import { openSnackbar } from 'store/reducers/snackbar';
// assets
import { HeartOutlined, HeartFilled } from '@ant-design/icons';
const prodImage = require.context('assets/images/e-commerce', true);
// ==============================|| PRODUCT CARD ||============================== //
const ProductCard = ({ id, color, name, brand, offer, isStock, image, description, offerPrice, salePrice, rating }) => {
const theme = useTheme();
const dispatch = useDispatch();
const prodProfile = image && prodImage(`./${image}`);
const [productRating] = useState(rating);
const [wishlisted, setWishlisted] = useState(false);
const cart = useSelector((state) => state.cart);
const addCart = () => {
dispatch(addProduct({ id, name, image, salePrice, offerPrice, color, size: 8, quantity: 1, description }, cart.checkout.products));
dispatch(
openSnackbar({
open: true,
message: 'Add To Cart Success',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
};
const addToFavourite = () => {
setWishlisted(!wishlisted);
dispatch(
openSnackbar({
open: true,
message: 'Added to favourites',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
};
const [isLoading, setLoading] = useState(true);
useEffect(() => {
setLoading(false);
}, []);
return (
<>
{isLoading ? (
<SkeletonProductPlaceholder />
) : (
<MainCard
content={false}
boxShadow
sx={{
'&:hover': {
transform: 'scale3d(1.02, 1.02, 1)',
transition: 'all .4s ease-in-out'
}
}}
>
<Box sx={{ width: 250, m: 'auto' }}>
<CardMedia
sx={{ height: 250, textDecoration: 'none', opacity: isStock ? 1 : 0.25 }}
image={prodProfile}
component={Link}
to={`/apps/e-commerce/product-details/${id}`}
/>
</Box>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ width: '100%', position: 'absolute', top: 0, pt: 1.75, pl: 2, pr: 1 }}
>
{!isStock && <Chip variant="light" color="error" size="small" label="Sold out" />}
{offer && <Chip label={offer} variant="combined" color="success" size="small" />}
<IconButton color="secondary" sx={{ ml: 'auto', '&:hover': { background: 'transparent' } }} onClick={addToFavourite}>
{wishlisted ? (
<HeartFilled style={{ fontSize: '1.15rem', color: theme.palette.error.main }} />
) : (
<HeartOutlined style={{ fontSize: '1.15rem' }} />
)}
</IconButton>
</Stack>
<Divider />
<CardContent sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack>
<Typography
component={Link}
to={`/apps/e-commerce/product-details/${id}`}
color="textPrimary"
variant="h5"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
textDecoration: 'none'
}}
>
{name}
</Typography>
<Typography variant="h6" color="textSecondary">
{brand}
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-end" flexWrap="wrap" rowGap={1.75}>
<Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="h5">${offerPrice}</Typography>
{salePrice && (
<Typography variant="h6" color="textSecondary" sx={{ textDecoration: 'line-through' }}>
${salePrice}
</Typography>
)}
</Stack>
<Stack direction="row" alignItems="flex-start">
<Rating precision={0.5} name="size-small" value={productRating} size="small" readOnly />
<Typography variant="caption">({productRating?.toFixed(1)})</Typography>
</Stack>
</Stack>
<Button variant="contained" onClick={addCart} disabled={!isStock}>
{!isStock ? 'Sold Out' : 'Add to Cart'}
</Button>
</Stack>
</Grid>
</Grid>
</CardContent>
</MainCard>
)}
</>
);
};
ProductCard.propTypes = {
id: PropTypes.number,
color: PropTypes.string,
name: PropTypes.string,
brand: PropTypes.string,
isStock: PropTypes.bool,
image: PropTypes.string,
description: PropTypes.string,
offerPrice: PropTypes.number,
salePrice: PropTypes.number,
offer: PropTypes.string,
rating: PropTypes.number
};
export default ProductCard;

View File

@@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Grid, Rating, Stack, Typography } from '@mui/material';
// project imports
import Avatar from 'components/@extended/Avatar';
// assets
import { StarFilled, StarOutlined } from '@ant-design/icons';
const avatarImage = require.context('assets/images/users', true);
// ==============================|| PRODUCT DETAILS - REVIEW ||============================== //
const ProductReview = ({ avatar, date, name, rating, review }) => (
<Grid item xs={12}>
<Stack direction="row" spacing={1}>
<Avatar alt={name} src={avatar && avatarImage(`./${avatar}`)} />
<Stack spacing={2}>
<Stack>
<Typography variant="subtitle1" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>
{name}
</Typography>
<Typography variant="caption" color="textSecondary">
{date}
</Typography>
<Rating
size="small"
name="simple-controlled"
value={rating < 4 ? rating + 1 : rating}
icon={<StarFilled style={{ fontSize: 'inherit' }} />}
emptyIcon={<StarOutlined style={{ fontSize: 'inherit' }} />}
precision={0.1}
readOnly
/>
</Stack>
<Typography variant="body2">{review}</Typography>
</Stack>
</Stack>
</Grid>
);
ProductReview.propTypes = {
avatar: PropTypes.string,
date: PropTypes.string,
name: PropTypes.string,
rating: PropTypes.number,
review: PropTypes.string
};
export default ProductReview;

View File

@@ -1,44 +0,0 @@
// material-ui
import { CardContent, Grid, Skeleton, Stack } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// ===========================|| SKELETON - PRODUCT CARD ||=========================== //
const ProductPlaceholder = () => (
<MainCard content={false} boxShadow>
<Skeleton variant="rectangular" height={220} />
<CardContent sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Skeleton variant="rectangular" height={20} />
</Grid>
<Grid item xs={12}>
<Skeleton variant="rectangular" height={45} />
</Grid>
<Grid item xs={12} sx={{ pt: '8px !important' }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="rectangular" height={20} width={90} />
<Skeleton variant="rectangular" height={20} width={38} />
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Grid container spacing={1}>
<Grid item>
<Skeleton variant="rectangular" height={20} width={40} />
</Grid>
<Grid item>
<Skeleton variant="rectangular" height={17} width={20} />
</Grid>
</Grid>
<Skeleton variant="rectangular" height={32} width={47} />
</Stack>
</Grid>
</Grid>
</CardContent>
</MainCard>
);
export default ProductPlaceholder;

View File

@@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Chip, Grid, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { FallOutlined, RiseOutlined } from '@ant-design/icons';
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
const AnalyticEcommerce = ({ color = 'primary', title, count, percentage, isLoss, extra }) => (
<MainCard contentSX={{ p: 2.25 }}>
<Stack spacing={0.5}>
<Typography variant="h6" color="textSecondary">
{title}
</Typography>
<Grid container alignItems="center">
<Grid item>
<Typography variant="h4" color="inherit">
{count}
</Typography>
</Grid>
{percentage && (
<Grid item>
<Chip
variant="combined"
color={color}
icon={
<>
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
</>
}
label={`${percentage}%`}
sx={{ ml: 1.25, pl: 1 }}
size="small"
/>
</Grid>
)}
</Grid>
</Stack>
<Box sx={{ pt: 2.25 }}>
<Typography variant="caption" color="textSecondary">
You made an extra{' '}
<Typography component="span" variant="caption" sx={{ color: `${color || 'primary'}.main` }}>
{extra}
</Typography>{' '}
this year
</Typography>
</Box>
</MainCard>
);
AnalyticEcommerce.propTypes = {
title: PropTypes.string,
count: PropTypes.string,
percentage: PropTypes.number,
isLoss: PropTypes.bool,
color: PropTypes.string,
extra: PropTypes.string
};
export default AnalyticEcommerce;

View File

@@ -1,56 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Chip, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { RiseOutlined, FallOutlined } from '@ant-design/icons';
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
const AnalyticsDataCard = ({ color = 'primary', title, count, percentage, isLoss, children }) => (
<MainCard content={false}>
<Box sx={{ p: 2.25 }}>
<Stack spacing={0.5}>
<Typography variant="h6" color="textSecondary">
{title}
</Typography>
<Stack direction="row" alignItems="center">
<Typography variant="h4" color="inherit">
{count}
</Typography>
{percentage && (
<Chip
variant="combined"
color={color}
icon={
<>
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
</>
}
label={`${percentage}%`}
sx={{ ml: 1.25, pl: 1 }}
size="small"
/>
)}
</Stack>
</Stack>
</Box>
{children}
</MainCard>
);
AnalyticsDataCard.propTypes = {
title: PropTypes.string,
count: PropTypes.string,
percentage: PropTypes.number,
isLoss: PropTypes.bool,
color: PropTypes.string,
children: PropTypes.node
};
export default AnalyticsDataCard;

View File

@@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import { Chip } from '@mui/material';
// ==============================|| STATUS CHIP (Doormile-style soft badge) ||============================== //
// One consistent soft-filled chip for every lifecycle status across orders,
// deliveries, riders, tenants, invoices. Colour encodes meaning; the chip stays
// quiet (soft bg + readable dark text) so tables don't turn into a rainbow.
// Soft background + readable foreground per semantic tone.
const TONE = {
amber: { bg: '#FEF3C7', fg: '#92400E' },
indigo: { bg: '#E0E7FF', fg: '#3730A3' },
cyan: { bg: '#CFFAFE', fg: '#155E75' },
violet: { bg: '#EDE9FE', fg: '#5B21B6' },
teal: { bg: '#CCFBF1', fg: '#115E59' },
emerald: { bg: '#D1FAE5', fg: '#065F46' },
red: { bg: '#FEE2E2', fg: '#991B1B' },
orange: { bg: '#FFEDD5', fg: '#9A3412' },
sky: { bg: '#E0F2FE', fg: '#075985' },
slate: { bg: '#F1F5F9', fg: '#475569' },
brand: { bg: '#F1E6F7', fg: '#5B1F73' }
};
// Status keyword -> tone + display label.
const MAP = {
pending: { tone: 'amber', label: 'Pending' },
created: { tone: 'sky', label: 'Created' },
assigned: { tone: 'indigo', label: 'Assigned' },
accepted: { tone: 'indigo', label: 'Accepted' },
arrived: { tone: 'cyan', label: 'Arrived' },
picked: { tone: 'violet', label: 'Picked' },
'picked-up':{ tone: 'violet', label: 'Picked Up' },
started: { tone: 'cyan', label: 'Started' },
active: { tone: 'teal', label: 'Active' },
'in-transit': { tone: 'teal', label: 'In Transit' },
delivered: { tone: 'emerald', label: 'Delivered' },
completed: { tone: 'emerald', label: 'Completed' },
skipped: { tone: 'orange', label: 'Skipped' },
failed: { tone: 'red', label: 'Failed' },
cancelled: { tone: 'red', label: 'Cancelled' },
// riders / tenants
online: { tone: 'emerald', label: 'Online' },
offline: { tone: 'slate', label: 'Offline' },
inactive: { tone: 'slate', label: 'Inactive' },
idle: { tone: 'amber', label: 'Idle' },
unknown: { tone: 'slate', label: 'Unknown' },
// invoices
paid: { tone: 'emerald', label: 'Paid' },
unpaid: { tone: 'amber', label: 'Unpaid' },
open: { tone: 'sky', label: 'Open' },
overdue: { tone: 'red', label: 'Overdue' },
prepaid: { tone: 'emerald', label: 'Prepaid' },
cod: { tone: 'amber', label: 'COD' }
};
export default function StatusChip({ status, label, size = 'small', sx }) {
const key = String(status || '').toLowerCase().trim().replace(/\s+/g, '-');
const cfg = MAP[key] || { tone: 'slate', label: status || '—' };
const tone = TONE[cfg.tone] || TONE.slate;
return (
<Chip
size={size}
label={label || cfg.label}
sx={{ bgcolor: tone.bg, color: tone.fg, border: 'none', fontWeight: 600, ...sx }}
/>
);
}
StatusChip.propTypes = {
status: PropTypes.string,
label: PropTypes.node,
size: PropTypes.string,
sx: PropTypes.object
};

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Stack, Typography, Box } from '@mui/material';
const TitleCard = ({ sx, title, children, starticon }) => {
return (
<Box
sx={{
...sx
}}
>
<Stack direction="row" flexWrap={'wrap'} alignItems="center" justifyContent="space-between" gap={1}>
<Stack>
{starticon && starticon}
<Typography variant="h3">{title}</Typography>
</Stack>
{children}
</Stack>
</Box>
);
};
export default TitleCard;

View File

@@ -1,147 +0,0 @@
import PropTypes from 'prop-types';
import { createContext, useEffect, useReducer } from 'react';
// third-party
import { Chance } from 'chance';
import jwtDecode from 'jwt-decode';
// reducer - state management
import { LOGIN, LOGOUT } from 'store/reducers/actions';
import authReducer from 'store/reducers/auth';
// project import
import Loader from 'components/Loader';
import axios from 'utils/axios';
const chance = new Chance();
// constant
const initialState = {
isLoggedIn: false,
isInitialized: false,
user: null
};
const verifyToken = (serviceToken) => {
if (!serviceToken) {
return false;
}
const decoded = jwtDecode(serviceToken);
/**
* Property 'exp' does not exist on type '<T = unknown>(token: string, options?: JwtDecodeOptions | undefined) => T'.
*/
return decoded.exp > Date.now() / 1000;
};
const setSession = (serviceToken) => {
if (serviceToken) {
localStorage.setItem('serviceToken', serviceToken);
axios.defaults.headers.common.Authorization = `Bearer ${serviceToken}`;
} else {
localStorage.removeItem('serviceToken');
delete axios.defaults.headers.common.Authorization;
}
};
// ==============================|| JWT CONTEXT & PROVIDER ||============================== //
const JWTContext = createContext(null);
export const JWTProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
useEffect(() => {
const init = async () => {
console.log(verifyToken)
// try {
// const serviceToken = window.localStorage.getItem('serviceToken');
// if (serviceToken && verifyToken(serviceToken)) {
// setSession(serviceToken);
// const response = await axios.get('/api/account/me');
// const { user } = response.data;
// dispatch({
// type: LOGIN,
// payload: {
// isLoggedIn: true,
// user
// }
// });
// } else {
// dispatch({
// type: LOGOUT
// });
// }
// } catch (err) {
// console.error(err);
// dispatch({
// type: LOGOUT
// });
// }
};
init();
}, []);
const login = async (email, password) => {
const response = await axios.post('/api/account/login', { email, password });
const { serviceToken, user } = response.data;
setSession(serviceToken);
dispatch({
type: LOGIN,
payload: {
isLoggedIn: true,
user
}
});
};
const register = async (email, password, firstName, lastName) => {
// todo: this flow need to be recode as it not verified
const id = chance.bb_pin();
const response = await axios.post('/api/account/register', {
id,
email,
password,
firstName,
lastName
});
let users = response.data;
if (window.localStorage.getItem('users') !== undefined && window.localStorage.getItem('users') !== null) {
const localUsers = window.localStorage.getItem('users');
users = [
...JSON.parse(localUsers),
{
id,
email,
password,
name: `${firstName} ${lastName}`
}
];
}
window.localStorage.setItem('users', JSON.stringify(users));
};
const logout = () => {
setSession(null);
dispatch({ type: LOGOUT });
};
const resetPassword = async () => {};
const updateProfile = () => {};
if (state.isInitialized !== undefined && !state.isInitialized) {
return <Loader />;
}
return <JWTContext.Provider value={{ ...state, login, logout, register, resetPassword, updateProfile }}>{children}</JWTContext.Provider>;
};
JWTProvider.propTypes = {
children: PropTypes.node
};
export default JWTContext;

View File

@@ -22,6 +22,12 @@ const opentoast = (message, color, vertical = 'bottom') => {
});
};
// Notification.requestPermission() re-resolves with the browser's current
// (often already-decided) permission every time this runs. Without a guard,
// any repeat call — e.g. from a remount — re-shows the same permanent,
// non-auto-dismissing toast and they stack up. Only warn once per tab.
let notificationWarningShown = false;
// ===================== Generate FCM Token =====================
export const generateToken = async () => {
try {
@@ -29,7 +35,10 @@ export const generateToken = async () => {
dispatch(setFcmPermission(permission));
if (permission !== 'granted') {
opentoast('Enable notifications to receive OTP, alerts, and updates', 'error');
if (!notificationWarningShown) {
notificationWarningShown = true;
opentoast('Enable notifications to receive OTP, alerts, and updates', 'error');
}
return;
}

View File

@@ -1,16 +0,0 @@
import { useContext } from 'react';
// auth provider
import AuthContext from 'contexts/JWTContext';
// ==============================|| AUTH HOOKS ||============================== //
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('context must be use inside provider');
return context;
};
export default useAuth;

View File

@@ -1,29 +0,0 @@
import { useState } from 'react';
// ==============================|| CARD - PAGINATION ||============================== //
export default function usePagination(data, itemsPerPage) {
const [currentPage, setCurrentPage] = useState(1);
const maxPage = Math.ceil(data.length / itemsPerPage);
function currentData() {
const begin = (currentPage - 1) * itemsPerPage;
const end = begin + itemsPerPage;
return data.slice(begin, end);
}
function next() {
setCurrentPage((currentPage) => Math.min(currentPage + 1, maxPage));
}
function prev() {
setCurrentPage((currentPage) => Math.max(currentPage - 1, 1));
}
function jump(page) {
const pageNumber = Math.max(1, page);
setCurrentPage(() => Math.min(pageNumber, maxPage));
}
return { next, prev, jump, currentData, currentPage, maxPage };
}

View File

@@ -1,18 +0,0 @@
import { useEffect, useRef } from 'react';
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
const useScriptRef = () => {
const scripted = useRef(true);
useEffect(
() => () => {
scripted.current = false;
},
[]
);
return scripted;
};
export default useScriptRef;

View File

@@ -1,4 +1,5 @@
import logger from './utils/logger';
import axios from 'axios';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
@@ -19,6 +20,11 @@ import { store } from 'store';
import reportWebVitals from './reportWebVitals';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const storedToken = localStorage.getItem('token');
if (storedToken) {
axios.defaults.headers.common.Authorization = `Bearer ${storedToken}`;
}
const container = document.getElementById('root');
const root = createRoot(container);
const queryClient = new QueryClient({

View File

@@ -27,7 +27,7 @@ import Transitions from 'components/@extended/Transitions';
import useConfig from 'hooks/useConfig';
import { dispatch, useSelector } from 'store';
import { activeItem } from 'store/reducers/menu';
import { MenuOrientation, ThemeMode } from 'config';
import { MenuOrientation } from 'config';
// assets
import { BorderOutlined, DownOutlined, UpOutlined, RightOutlined } from '@ant-design/icons';

View File

@@ -1,99 +0,0 @@
import PropTypes from 'prop-types';
import { forwardRef, useEffect } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { useMediaQuery, Avatar, Chip, ListItemButton, ListItemText, Typography } from '@mui/material';
// project imports
import { ThemeMode } from 'config';
import { dispatch, useSelector } from 'store';
import { activeComponent, openComponentDrawer } from 'store/reducers/menu';
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
const NavItem = ({ item }) => {
const theme = useTheme();
const matchesMD = useMediaQuery(theme.breakpoints.down('md'));
const menu = useSelector((state) => state.menu);
const { openComponent } = menu;
let itemTarget = '_self';
if (item.target) {
itemTarget = '_blank';
}
let listItemProps = { component: forwardRef((props, ref) => <Link {...props} to={item.url} target={itemTarget} ref={ref} />) };
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget };
}
const itemHandler = (id) => {
dispatch(activeComponent({ openComponent: id }));
if (matchesMD) dispatch(openComponentDrawer({ componentDrawerOpen: false }));
};
// active menu item on page load
useEffect(() => {
const currentIndex = document.location.pathname
.toString()
.split('/')
.findIndex((id) => id === item.id);
if (currentIndex > -1) {
dispatch(activeComponent({ openComponent: item.id }));
}
// eslint-disable-next-line
}, []);
const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : 'text.primary';
const iconSelectedColor = theme.palette.mode === ThemeMode.DARK ? 'text.primary' : 'primary.main';
return (
<ListItemButton
{...listItemProps}
disabled={item.disabled}
onClick={() => itemHandler(item.id)}
selected={openComponent === item.id}
sx={{
pl: 4,
py: 1,
mb: 0.5,
'&:hover': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
},
'&.Mui-selected': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
borderRight: `2px solid ${theme.palette.primary.main}`,
'&:hover': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
}
}
}}
>
<ListItemText
primary={
<Typography variant="h6" sx={{ color: openComponent === item.id ? iconSelectedColor : textColor }}>
{item.title}
</Typography>
}
/>
{item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
</ListItemButton>
);
};
NavItem.propTypes = {
item: PropTypes.object
};
export default NavItem;

View File

@@ -13,9 +13,6 @@ import useConfig from 'hooks/useConfig';
import logo from 'assets/images/doormile-logo.png'
import logo1 from 'assets/images/doormile-mark.png'
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for the light sidebar background.
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(14%) sepia(85%) saturate(4649%) hue-rotate(345deg) brightness(97%) contrast(92%)';
// ==============================|| DRAWER HEADER ||============================== //
const DrawerHeader = ({ open }) => {
@@ -37,8 +34,6 @@ const DrawerHeader = ({ open }) => {
paddingLeft: isHorizontal ? { xs: '24px', lg: '0' } : open ? '24px' : 0
}}
>
{/* <Logo isIcon={!open} sx={{ width: open ? 'auto' : 35, height: 35 }} /> */}
{(open) &&
<img src={logo}
style={{ height: '29px', width: 'auto' }}

View File

@@ -1,10 +1,8 @@
// third-party
import { FormattedMessage } from 'react-intl';
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined';
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
import NearMeOutlinedIcon from '@mui/icons-material/NearMeOutlined';
import { TbListDetails } from 'react-icons/tb';
// assets
import {
@@ -24,7 +22,6 @@ import {
TeamOutlined,
MailOutlined,
ImportOutlined,
BarChartOutlined,
MoneyCollectOutlined,
FileDoneOutlined
} from '@ant-design/icons';
@@ -47,8 +44,6 @@ const icons = {
TeamOutlined,
MailOutlined,
ImportOutlined,
BarChartOutlined,
ReceiptOutlinedIcon,
NearMeOutlinedIcon,
DirectionsBikeOutlinedIcon,
MopedOutlinedIcon,
@@ -63,6 +58,13 @@ const nearle = {
icon: icons.FileDoneOutlined,
type: 'group',
children: [
{
id: 'dashboard',
title: <FormattedMessage id="dashboard" />,
type: 'item',
url: '/nearle/dashboard',
icon: icons.DashboardOutlined
},
{
id: 'dispatch',
title: <FormattedMessage id="dispatch" />,
@@ -70,6 +72,13 @@ const nearle = {
url: '/nearle/dispatch',
icon: icons.DirectionsBikeOutlinedIcon
},
{
id: 'hubs',
title: <FormattedMessage id="hubs" />,
type: 'item',
url: '/nearle/hubs',
icon: icons.DeploymentUnitOutlined
},
{
id: 'orders',
title: <FormattedMessage id="orders" />,
@@ -112,59 +121,6 @@ const nearle = {
type: 'item',
url: '/nearle/riders',
icon: DirectionsBikeOutlinedIcon
},
{
id: 'reports',
title: <FormattedMessage id="reports" />,
type: 'collapse',
icon: icons.BarChartOutlined,
children: [
{
id: 'reports',
title: <FormattedMessage id="ordersummary" />,
type: 'item',
url: '/nearle/reports/orderssummary',
icon: TbListDetails
},
{
id: 'ordersdetails',
title: <FormattedMessage id="ordersdetails" />,
type: 'item',
url: '/nearle/reports/ordersdetails',
icon: icons.DashboardOutlined
// target: true
},
{
id: 'riderssummary',
title: <FormattedMessage id="riderssummary" />,
type: 'item',
url: '/nearle/reports/riderssummary',
icon: DirectionsBikeOutlinedIcon
// target: true
},
{
id: 'riderslogs',
title: <FormattedMessage id="riderslogs" />,
type: 'item',
url: '/nearle/reports/riderslogs',
icon: DirectionsBikeOutlinedIcon
// target: true
},
{
id: 'profitability',
title: <FormattedMessage id="profitability" />,
type: 'item',
url: '/nearle/reports/profitability',
icon: icons.BarChartOutlined
}
]
},
{
id: 'invoice',
title: <FormattedMessage id="invoice" />,
type: 'item',
url: '/nearle/invoice',
icon: icons.ReceiptOutlinedIcon
}
]
};

View File

@@ -4,82 +4,111 @@ import dayjs from 'dayjs';
const userid = localStorage.getItem('userid');
// ==============================|| getRiderPeriodicLogs ||============================== //
// Returns the rider's latest periodic log entry — battery, GPS, status, current
// order. Used by the Rider Info modal on the Dispatch page.
// Returns the miler's latest known position/status. Doormile has no periodic-log
// stream yet (that lands with EMQX) — this reads the live snapshot off /admin/milers/:id
// instead. battery/speed have no Doormile equivalent yet, so they're always null.
export const getRiderPeriodicLogs = async (userid) => {
const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`;
const response = await axios.get(url);
if (response.data && response.data.status) return response.data.data;
return null;
if (!userid) return null;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${userid}`);
const m = response.data?.data;
if (!m) return null;
return {
userid: m.userid,
lat: m.currentlat,
lon: m.currentlon,
status: m.availabilitystatus,
battery: null,
speed: null
};
};
// ==============================|| fetchAppLocations||============================== //
export const fetchAppLocations = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
const updatedLocations = [
...response.data.details,
{ locationname: 'All', applocationid: 0 } // Add your new object here
];
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
const hubs = (response.data?.data || []).map((h) => ({
...h,
applocationid: h.hubid,
locationname: h.hubname
}));
return [...hubs, { locationname: 'All', applocationid: 0 }];
};
return updatedLocations;
// ==============================|| fetchHubs / createHub / updateHub (hubs) ||============================== //
export const fetchHubs = async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
return res.data?.data || [];
};
export const createHub = async (body) => {
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/hubs`, body);
return res.data;
};
export const updateHub = async (id, body) => {
const res = await axios.patch(`${process.env.REACT_APP_URL}/admin/hubs/${id}`, body);
return res.data;
};
// ==============================|| fetchPercentageData (orders) ||============================== //
export const fetchPercentageData = async ({ queryKey }) => {
const [, appId, startdate, enddate, tenantid, locationid] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Delivered', 'Cancelled'];
const [pending, assigned, delivered, cancelled] = await Promise.all(
statuses.map((status) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status } }))
);
const details = response.data.details;
const created = pending.data.total + assigned.data.total + delivered.data.total + cancelled.data.total;
return {
created: details.created.toString(),
uncoveredOrders: details.pending.toString(),
coveredOrders: details.delivered.toString(),
cancelled: details.cancelled.toString(),
percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(),
percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(),
percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(),
percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString()
created: created.toString(),
uncoveredOrders: pending.data.total.toString(),
coveredOrders: delivered.data.total.toString(),
cancelled: cancelled.data.total.toString(),
percentage1: (Math.round((created / created) * 100) || 0).toString(),
percentage2: (Math.round((pending.data.total / created) * 100) || 0).toString(),
percentage3: (Math.round((delivered.data.total / created) * 100) || 0).toString(),
percentage4: (Math.round((cancelled.data.total / created) * 100) || 0).toString()
};
};
// ===================================================== || getTenants || =====================================================
export const getTenants = async (appId) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`);
if (response.data.status) {
let arr = [];
response.data.details.map((val) => {
arr.push({
...val,
label: `${val.tenantname}`
});
});
return arr;
}
// appId (hub/zone) has no equivalent filter on Doormile's CRM clients endpoint —
// kept as a parameter only so existing call sites (deliveries.js, orders.js,
// reports/*) don't need to change their queryFn wiring.
export const getTenants = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
return (response.data?.data || []).map((val) => ({
...val,
tenantid: val.clientid,
tenantname: val.clientname,
label: val.clientname
}));
};
// ============================================= || gettenantlocations (branches) || =============================================
export const gettenantlocations = async (appId) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${appId}`);
return res.data.details;
} catch (err) {
console.log('gettenantlocations', err);
}
// No Doormile equivalent to tenant branches — call sites use this for a
// branch/location dropdown that doesn't apply to Doormile's client model.
export const gettenantlocations = async () => {
return [];
};
// ==============================|| fetchorderscount (orders) ||============================== //
export const fetchorderscount = async ({ queryKey }) => {
// eslint-disable-next-line no-unused-vars
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&status=${currentStatus}`;
const [all, pending, delivered, cancelled] = await Promise.all([
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`),
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Pending_Pickup' } }),
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Delivered' } }),
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Cancelled' } })
]);
const response = await axios.get(url);
return response.data.details;
return {
created: all.data.total,
pending: pending.data.total,
delivered: delivered.data.total,
cancelled: cancelled.data.total
};
};
// ==============================|| fetchOrders (orders) ||============================== //
@@ -95,46 +124,93 @@ export const fetchorderscount = async ({ queryKey }) => {
// return response.data.details.map((val, i) => ({ ...val, sno: i + 1 }));
// };
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
// eslint-disable-next-line no-unused-vars
const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&keyword=${debouncedSearch}&pageno=${pageParam}&pagesize=${rowsPerPage}`;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
params: {
status: currentStatus === 'All' ? undefined : currentStatus,
keyword: debouncedSearch,
pageno: pageParam,
pagesize: rowsPerPage
}
});
const response = await axios.get(url);
// TEMPORARY: backend fix in progress — pageno/pagesize are currently
// ignored server-side and it returns every matching record on every call.
// Slice client-side so infinite scroll doesn't dump the whole dataset on
// page 1. Safe to remove once the backend honours pagination.
const all = response.data.data || [];
const size = Number(rowsPerPage);
const start = (pageParam - 1) * size;
const rows = all.slice(start, start + size);
return {
rows: response.data.details,
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
rows,
nextPage: start + size < all.length ? pageParam + 1 : undefined
};
};
// ==============================|| fetchPaymentType (orders) ||============================== //
export const fetchPaymentType = async () => {
const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`);
return data.details.map((val) => ({
...val,
label: val.typename
}));
};
// No Doormile payment-types endpoint exists yet — static defaults.
export const fetchPaymentType = async () => [
{ apptypeid: 1, typename: 'Cash', label: 'Cash' },
{ apptypeid: 2, typename: 'Online', label: 'Online' },
{ apptypeid: 3, typename: 'COD', label: 'COD' }
];
// ==============================|| fetchRidersList (orders) ||============================== //
export const fetchRidersList = async ({ queryKey }) => {
try {
const [, appId] = queryKey; // Extract appId from queryKey
const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
console.log('data', data);
const response = data?.details
? data?.details.map((val) => ({
...val,
label: `${val.firstname} ${val.lastname} | ${val.contactno}`
}))
: [];
return response;
} catch (err) {
OpenToast(err.message, 'error', 2000);
throw err; // 🔥 REQUIRED
}
export const fetchRidersList = async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
return (res.data?.data || []).map((m) => ({
...m,
userid: m.userid,
label: `${m.displayname} | ${m.phone}`,
firstname: m.displayname,
lastname: '',
contactno: m.phone
}));
};
// ==============================|| Doormile assignment (new) ||============================== //
// NOTE: createOptimisationDeliveries / reconcileSteps / fetchBatchEfficiency /
// finalCreatedeliveries / createAutomationDeliveries below still call the legacy
// NearlExpress optimiser (routes.workolik.com / routemate.workolik.com /
// jupiter.nearle.app) and are wired into Dispatch.js, Preview.js, OrdersPreview.js,
// orders.js and deliveries.js. Per the "don't touch Dispatch/Preview internals —
// Phase 3" instruction, they're left as-is rather than renamed/repointed, since
// doing so would silently break those pages' running mutations without a
// corresponding page rewrite. The Doormile-native replacements are added here
// instead, so Phase 3 can cut individual call sites over one at a time.
// assignBooking — triggers Doormile's AI assignment engine for one booking via
// the internal-key-gated endpoint. SECURITY: X-Internal-Key is a static secret
// baked into this client bundle — anyone can extract it from the shipped JS and
// call this endpoint directly. Prefer routing this through the already-
// authenticated admin JWT (see autoAssignBooking below) once the backend
// supports it; keep this only if /internal/bookings/:id/reassign truly must
// stay key-gated rather than JWT-gated.
export const assignBooking = async (bookingId) => {
const response = await axios.post(
`${process.env.REACT_APP_URL}/internal/bookings/${bookingId}/reassign`,
{},
{ headers: { 'X-Internal-Key': process.env.REACT_APP_INTERNAL_KEY } }
);
return response.data;
};
// autoAssignBooking — same intent as assignBooking but authenticated with the
// operator's own admin JWT instead of a shared static key.
export const autoAssignBooking = async (bookingId) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${bookingId}/auto-assign`, {});
return response.data;
};
// fetchDashboardStats — hub-level stats, replaces the workolik batch-efficiency call.
export const fetchDashboardStats = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/hub/dashboard`);
return response.data;
};
// ==============================|| createOptimisationDeliveries (orders) Arrange the order ||============================== //
@@ -145,14 +221,9 @@ export const createOptimisationDeliveries = async (deliveryData) => {
return response.data;
};
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
export const reconcileSteps = async ({ riders }) => {
const response = await axios.post(
`https://routes.workolik.com/api/v1/optimization/reconcile-steps`,
{ riders }
);
return response.data;
};
// No Doormile equivalent — made a no-op so CLAUDE.md's "always reconcile before
// createdeliveries" hard constraint keeps holding trivially until Phase 3.
export const reconcileSteps = async (data) => data;
// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== //
// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }.
@@ -223,34 +294,19 @@ export const createAutomationDeliveries = async (variables) => {
};
// ==============================|| notifyRider (orders / deliveries) ||============================== //
// Doormile sends miler FCM notifications automatically from the Go backend on
// status changes, so there's no equivalent client-triggered endpoint. No-op.
export const notifyRider = async () => ({ success: true });
export const notifyRider = async (riderToken) => {
if (!riderToken) {
throw new Error('Invalid rider token');
}
console.log('notify rider called');
console.log('riderToken', riderToken);
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: riderToken,
notification: {
title: 'NearleXpress',
body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
sound: 'ring',
image: ''
}
});
// ==============================|| cancelOrder (orders) ||============================== //
export const cancelOrder = async (bookingid) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/cancel`);
return response.data;
};
// ==============================|| cancelOrder (orders) ||============================== //
export const cancelOrder = async (orderheaderid) => {
const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
orderheaderid: orderheaderid,
orderstatus: 'cancelled',
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
});
// ==============================|| updateBookingStatus (bookings) ||============================== //
export const updateBookingStatus = async (bookingid, status) => {
const response = await axios.put(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/status`, { status });
return response.data;
};
// ==============================|| cancelMultipleOrder (orders) ||============================== //
@@ -270,97 +326,105 @@ export const cancelMultipleOrder = async (orderlist) => {
};
// ==============================|| fetchDeliveries (deliveries) ||============================== //
// NOTE: NearlExpress "deliveries" tabs are keyed off statuses
// (pending/accepted/arrived/picked/active/delivered/cancelled/skipped) that
// don't exist in Doormile's booking status vocabulary (Pending_Pickup,
// Miler_Assigned, Pickup_Scheduled, At_Customer, Picked_Up, At_Hub, Delivered,
// Cancelled, Assignment_Failed). This passes currentStatus straight through —
// deliveries.js's own tab definitions still need updating to Doormile statuses
// (flagged, not guessed).
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
// eslint-disable-next-line no-unused-vars
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
const url =
appId === 0
? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
: `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const response = await axios.get(url);
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
params: {
status: currentStatus === 'all' || currentStatus === 'All' ? undefined : currentStatus,
keyword: searchword,
pageno: pageParam,
pagesize: rowsPerPage
}
});
// TEMPORARY: same backend pagination bug as fetchOrders — slice client-side
// until pageno/pagesize are honoured server-side.
const all = response.data.data || [];
const size = Number(rowsPerPage);
const start = (pageParam - 1) * size;
const rows = all.slice(start, start + size);
return {
rows: response.data.details,
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
rows,
nextPage: start + size < all.length ? pageParam + 1 : undefined
};
};
// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
export const fetchPercentageAPI = async (appId) => {
const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}`;
const response = await axios.get(url);
const data = response.data.details;
// UNVERIFIED: status param + pagesize=1-for-count-only pattern hasn't been
// confirmed against the real backend the way bookings/clients/milers were —
// implemented per explicit instruction, flag if it 404s/403s like
// /admin/customers did.
export const fetchPercentageAPI = async () => {
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Picked_Up', 'Delivered', 'Cancelled'];
const results = await Promise.all(
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
);
const [pending, assigned, picked, delivered, cancelled] = results.map((r) => r.data?.total || 0);
const total = pending + assigned + picked + delivered + cancelled;
return {
coveredOrders: data.delivered.toString(),
cancelledOrders: data.cancelled.toString(),
uncoveredOrders: data.pending.toString(),
assignedOrders: data.accepted.toString(),
createdOrders: data.created.toString(),
closedOrders: data.delivered.toString(),
pickedOrders: data.picked.toString(),
percentage1: (Math.round((data.pending / data.total) * 100) || 0).toString(),
percentage2: (Math.round((data.accepted / data.total) * 100) || 0).toString(),
percentage3: (Math.round((data.picked / data.total) * 100) || 0).toString(),
percentage4: (Math.round((data.delivered / data.total) * 100) || 0).toString()
coveredOrders: delivered.toString(),
cancelledOrders: cancelled.toString(),
uncoveredOrders: pending.toString(),
assignedOrders: assigned.toString(),
createdOrders: total.toString(),
closedOrders: delivered.toString(),
pickedOrders: picked.toString(),
total: total.toString(),
percentage1: (Math.round((pending / total) * 100) || 0).toString(),
percentage2: (Math.round((assigned / total) * 100) || 0).toString(),
percentage3: (Math.round((picked / total) * 100) || 0).toString(),
percentage4: (Math.round((delivered / total) * 100) || 0).toString()
};
};
// ==============================|| fetchCountAPI (deliveries) ||============================== //
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
const url =
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const response = await axios.get(url);
const data = response.data.details;
export const fetchCountAPI = async () => {
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered', 'Cancelled'];
const results = await Promise.all(
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
);
const [pending, assigned, scheduled, atcustomer, picked, athub, delivered, cancelled] = results.map((r) => r.data?.total || 0);
return {
total: data.total,
uncoveredLength: data.pending,
assignedLength: data.accepted,
arrivedLength: data.arrived,
pickedLength: data.picked,
activeLength: data.active,
coveredLength: data.delivered,
cancelLength: data.cancelled,
skippedLength: data.skipped
total: pending + assigned + scheduled + atcustomer + picked + athub + delivered + cancelled,
uncoveredLength: pending,
assignedLength: assigned,
arrivedLength: scheduled,
pickedLength: picked,
activeLength: atcustomer + athub,
coveredLength: delivered,
cancelLength: cancelled,
skippedLength: 0
};
};
// ==============================|| cancelDeliveryAPI (deliveries) ||============================== //
export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => {
const payload = {
deliveryid: selectedRow.deliveryid,
orderheaderid: selectedRow.orderheaderid,
orderstatus: 'cancelled',
canceltime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
feedback: cancelFeed
};
const response = await axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, payload);
export const cancelDeliveryAPI = async (selectedRow) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/admin/bookings/${selectedRow.bookingid}/cancel`);
return response.data;
};
// ==============================|| getorderdetails (deliveries) ||============================== //
export const getorderdetails = async (orderHeaderid) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getorderdetails?orderheaderid=${orderHeaderid}`);
export const getorderdetails = async (bookingid) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}`);
return response.data;
};
// ==============================|| changeRiderAPI (deliveries) ||============================== //
export const changeRiderAPI = async (selectedRider, selectedRow) => {
console.log('selectedRider', selectedRider);
console.log('selectedRow', selectedRow);
return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, {
userid: selectedRider.userid,
deliveryid: selectedRow.deliveryid,
orderheaderid: selectedRow.orderheaderid,
orderstatus: 'pending',
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss')
return axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${selectedRow.bookingid}/assign-miler`, {
mileruserid: selectedRider.userid
});
};
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
@@ -370,152 +434,158 @@ export const updateDeliveryAPI = async (orderData) => {
};
// ==============================|| getalltenants (tenants) ||============================== //
// Doormile's /crm/clients has no documented status/keyword/page filter params
// (unlike the old /tenants/getalltenants), so this fetches the full client
// list and filters + paginates client-side.
export const getalltenants = async ({ queryKey }) => {
const [, appId, debouncedSearch, status, page, rowsPerPage] = queryKey;
const [, , debouncedSearch, status, page, rowsPerPage] = queryKey;
try {
let url = `${process.env.REACT_APP_URL
}/tenants/getalltenants/?status=${status}&applocationid=${appId}&keyword=${debouncedSearch}&pageno=${page + 1
}&pagesize=${rowsPerPage}&moduleid=6`;
const response = await axios.get(url);
return response.data.details; // return only data, keep it clean
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
let clients = (response.data?.data || []).map((c) => ({
...c,
tenantid: c.clientid,
tenantname: c.clientname
}));
if (status) {
clients = clients.filter((c) => (c.status || '').toLowerCase() === status.toLowerCase());
}
if (debouncedSearch) {
const kw = debouncedSearch.toLowerCase();
clients = clients.filter((c) => (c.clientname || '').toLowerCase().includes(kw) || (c.email || '').toLowerCase().includes(kw));
}
const start = page * rowsPerPage;
return clients.slice(start, start + rowsPerPage);
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
return null;
}
};
// ==============================|| gettenantsummary (tenants) ||============================== //
export const gettenantsummary = async ({ queryKey }) => {
const [, appId] = queryKey;
export const gettenantsummary = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
const clients = response.data?.data || [];
const countOf = (s) => clients.filter((c) => (c.status || '').toLowerCase() === s).length;
return { active: countOf('active'), pending: countOf('pending'), inactive: countOf('inactive') };
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
return null;
}
};
// ==============================|| getpricinglist (tenants) ||============================== //
export const getpricinglist = async ({ queryKey }) => {
const [, appId] = queryKey;
export const getpricinglist = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/pricing`);
return response.data?.data || [];
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
OpenToast(err.message, 'error', 2000);
return [];
}
};
// ==============================|| 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 [];
}
};
export const getallpricing = getpricinglist;
// ==============================|| getcustomersummary (customers) ||============================== //
export const getcustomersummary = async ({ queryKey }) => {
const [, appId] = queryKey;
export const getcustomersummary = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getcustomersummary?applocationid=${appId}`);
return response.data.summary;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/customers`);
const customers = response.data?.data || [];
const total = response.data?.total || customers.length;
return { Total: total };
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
OpenToast(err.message, 'error', 2000);
return null;
}
};
// ==============================|| getallcustomers (customers) ||============================== //
// Backend rows key the customer id as `appcustomerid` — aliased to `userid`
// here so any call site still expecting the old field name keeps working.
export const getallcustomers = async ({ pageParam = 1, queryKey }) => {
const [, appId, debouncedSearch, rowsPerPage] = queryKey;
const [, , debouncedSearch, rowsPerPage] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getallcustomers/`, {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/customers`, {
params: {
applocationid: appId,
keyword: debouncedSearch,
keyword: debouncedSearch || undefined,
pageno: pageParam,
pagesize: rowsPerPage
pagesize: rowsPerPage || 20
}
});
const customers = (response.data?.data || []).map((c) => ({
...c,
userid: c.appcustomerid
}));
return {
data: response.data.details || [],
nextPage: response.data.details?.length === rowsPerPage ? pageParam + 1 : undefined
data: customers,
nextPage: customers.length === Number(rowsPerPage) ? pageParam + 1 : undefined
};
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
OpenToast(err.message, 'error', 2000);
throw err; // IMPORTANT for React Query
}
};
// ==============================|| fetchAllRiders (riders) ||============================== //
export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
// Doormile's /admin/milers has no documented pagination or search/status
// filter params, unlike the old /partners/getallriders. Fetches the full miler
// list and filters client-side; single page (nextPage always undefined) until
// the backend adds server-side paging.
export const fetchAllRiders = async ({ queryKey }) => {
try {
// eslint-disable-next-line no-unused-vars
const [, appId, debouncedSearch, tabvalue] = queryKey;
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
let milers = res.data?.data || [];
const url = `${process.env.REACT_APP_URL
}/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${(tabvalue == 0 || tabvalue == 2) ? '' : 'Active'
}`;
const res = await axios.get(url);
return {
details: res.data.details,
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
};
if (debouncedSearch) {
const kw = debouncedSearch.toLowerCase();
milers = milers.filter(
(m) => (m.displayname || '').toLowerCase().includes(kw) || (m.phone || '').includes(debouncedSearch)
);
}
if (tabvalue != 0 && tabvalue != 2) {
milers = milers.filter((m) => m.availabilitystatus === 'Available');
}
return { details: milers, nextPage: undefined };
} catch (err) {
console.log('fetchAllRiders err', err.message);
return [];
return { details: [], nextPage: undefined };
}
};
// ==============================|| getallridersummary (riders) ||============================== //
export const getallridersummary = async ({ queryKey }) => {
export const getallridersummary = async () => {
try {
const [, appId, tabvalue] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/partners/getallridersummary/?applocationid=${appId}&status=${tabvalue == 0 ? '' : 'Active'}`
);
return response.data.details;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
const milers = response.data?.data || [];
const active = milers.filter((m) => m.availabilitystatus === 'Available').length;
return { total: milers.length, active, inactive: milers.length - active };
} catch (err) {
console.log('getallridersummary err', err.message);
return [];
return { total: 0, active: 0, inactive: 0 };
}
};
// ==============================|| fetchRiders (riders), active riders ||============================== //
// Not currently imported anywhere (riders.js uses fetchAllRiders) — updated for
// consistency in case a future page picks it up.
export const fetchRiders = async (appId) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
return response.data?.data || [];
};
export const fetchRiders = async ({ pageParam = 1, queryKey }) => {
try {
const [, appId, debouncedSearch] = queryKey;
const url = `${process.env.REACT_APP_URL
}/partners/getriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}`;
const res = await axios.get(url);
return {
details: res.data.details,
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
};
} catch (err) {
console.log('fetchRiders err', err.message);
return [];
}
// ==============================|| fetchMilerDetail (riders) ||============================== //
export const fetchMilerDetail = async (milerid) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${milerid}`);
return response.data.data;
};
// ==============================|| getriderstatus (riders)||============================== //
@@ -524,29 +594,6 @@ export const getriderstatus = async () => {
return response.data.data;
};
// ==============================|| getreportsummary (orders summary)||============================== //
export const getreportsummary = async ({ queryKey }) => {
console.log('queryKey for getreportsummary', queryKey);
const [appId, tenantid, locationid, startdate, enddate] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getreportsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
console.log('getreportsummary', response.data.details);
return response.data.details;
};
// ==============================|| getreportlocationsummary (orders summary)||============================== //
export const getreportlocationsummary = async ({ queryKey }) => {
console.log('queryKey for getreportlocationsummary', queryKey);
const [appId, tenantid, locationid, startdate, enddate] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
console.log('getreportlocationsummary', response.data.details);
return response.data.details;
};
// ==============================|| fetchorderdetails (orders detail)||============================== //
export const fetchorderdetails = async ({ queryKey }) => {
console.log('queryKey of fetchorderdetails', queryKey);
@@ -567,119 +614,27 @@ export const fetchorderdetails = async ({ queryKey }) => {
return detailsWithSNo;
};
// ==============================|| getriderbydelivery (orders detail)||============================== //
export const getriderbydelivery = async (startdate, enddate, appId = 0, tenantid = 0, locationid = 0) => {
// const [, startdate, enddate] = queryKey;
try {
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getriderbydelivery/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
return response.data.details || [];
} catch (err) {
console.log('getriderbydelivery', err.message);
return [];
}
};
// ==============================|| fetchCount (orders detail)||============================== //
export const fetchCount = async ({ queryKey }) => {
console.log('queryKey of fetchCount', queryKey);
const [appId, startdate, enddate] = queryKey;
let url =
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`;
const response = await axios.get(url);
return response.data.details;
};
// ==============================|| fetchRidersSummary (riders summary)||============================== //
export const fetchRidersSummary = async ({ queryKey }) => {
console.log('queryKey for fetchRidersSummary', queryKey);
const [, appId, startdate, enddate] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getridersummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`
);
console.log('fetchRidersSummary', response.data.details);
return response.data.details;
};
// ==============================|| fetchLocations (orders summary))||============================== //
// Not needed as a separate concept in Doormile — returns the hub list instead.
export const fetchLocations = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`);
const updatedLocations = [
...response.data.details,
{ partnername: 'All', partnerid: -1 } // Add your new object here
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
return [
...(response.data?.data || []).map((h) => ({ ...h, partnername: h.hubname, partnerid: h.hubid })),
{ partnername: 'All', partnerid: -1 }
];
console.log('fetchLocations', updatedLocations);
return updatedLocations;
};
// ==============================|| fetchinvoiceinsight (Invoice)||============================== //
export const fetchinvoiceinsight = async () => {
const insightResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getinvoiceinsight`);
return insightResponse.data.details;
};
// ==============================|| fetchdeliverylist (Invoice)||============================== //
export const fetchdeliverylist = async ({ queryKey }) => {
const [billStatus] = queryKey;
const deliveyResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}`);
console.log('fetchdeliverylist', deliveyResponse.data.details);
return deliveyResponse.data.details;
};
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
// No Doormile equivalent yet (real-time miler location stream lands with EMQX).
export const fetchRidersLogs = async () => [];
export const fetchRidersLogs = async ({ queryKey }) => {
const [appId, startdate, riderSearch = ''] = queryKey;
const riderLogsResponse = await axios.get(
`${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${startdate}&keyword=${riderSearch || ''
} `
);
console.log('fetchRidersLogs', riderLogsResponse.data.details);
return riderLogsResponse.data.details;
};
// ==============================|| getorders (Locations)||============================== //
// fetchOrders.js
export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => {
const [, tenantid, locationid, status, startdate, enddate, searchword, rowsPerPage] = queryKey;
const res = await axios.get(
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tenantid}&locationid=${locationid}&status=${status}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}`
);
return {
details: res.data.details,
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
};
};
// ==============================|| getusers (viewProfile)||============================== //
export const getusers = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/users/getusers/?configid=9&userid=${userid}`);
return res.data.details;
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/users`);
return res.data?.data || [];
} catch (err) {
console.log('getusers', err.message);
}
};
// ==============================|| getallriders (order)||============================== //
export const getallriders = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getallriders?partnerid=64`);
return res.data.details;
} catch (err) {
console.log('getallriders', err.message);
return [];
}
};

View File

View File

@@ -0,0 +1,308 @@
import { useParams, useNavigate } from 'react-router-dom';
import { Avatar, Box, Button, Chip, Grid, Paper, Stack, Step, StepLabel, Stepper, Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
MdArrowBack,
MdLocalShipping,
MdPerson,
MdPhone,
MdLocationOn,
MdTwoWheeler,
MdStar,
MdOutlineSmartToy,
MdOutlineAssignmentInd,
MdOutlineCancel
} from 'react-icons/md';
import Loader from 'components/Loader';
import { OpenToast } from 'components/third-party/OpenToast';
import { getorderdetails, autoAssignBooking, cancelOrder } from 'pages/api/api';
const DT = {
radiusCard: 16,
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 edge = (c) => a(c, '55');
const BRAND = '#C01227';
// Ordered status progression used for the stepper. Doormile's real status
// vocabulary — Assignment_Failed and Cancelled are terminal/off-path states
// shown separately rather than as stepper nodes.
const STATUS_STEPS = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered'];
const STEP_LABELS = {
Pending_Pickup: 'Pending Pickup',
Miler_Assigned: 'Miler Assigned',
Pickup_Scheduled: 'Pickup Scheduled',
At_Customer: 'At Customer',
Picked_Up: 'Picked Up',
At_Hub: 'At Hub',
Delivered: 'Delivered'
};
const InfoRow = ({ icon: Icon, label, value, color = BRAND }) => (
<Stack direction="row" alignItems="flex-start" spacing={1.5}>
<Avatar sx={{ width: 32, height: 32, bgcolor: soft(color), color }}>
<Icon size={16} />
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4 }}>
{label}
</Typography>
<Typography sx={{ fontWeight: 600, color: DT.textPrimary, wordBreak: 'break-word' }}>{value || '—'}</Typography>
</Box>
</Stack>
);
const BookingDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const queryClient = useQueryClient();
const { data, isLoading, isError } = useQuery({
queryKey: ['bookingDetail', id],
queryFn: () => getorderdetails(id),
enabled: Boolean(id)
});
// Confirmed shape (booking id 5): bookingid, bookingreference, status,
// pickupaddress, deliveryaddress, assignedmileruserid, createdat,
// bookingparcels (nested parcels), serviceoptions, payments.
const booking = data?.data || data || {};
const reassignMutation = useMutation({
mutationFn: () => autoAssignBooking(id),
onSuccess: () => {
OpenToast('Reassignment triggered', 'success', 2000);
queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
const cancelMutation = useMutation({
mutationFn: () => cancelOrder(id),
onSuccess: () => {
OpenToast('Booking cancelled', 'success', 2000);
queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
if (isLoading) return <Loader />;
const status = booking.status || 'Pending_Pickup';
const isTerminal = ['cancelled', 'assignment_failed'].includes(String(status).toLowerCase());
const activeStepIdx = STATUS_STEPS.indexOf(status);
const parcels = booking.bookingparcels || booking.parcels || [];
const miler = booking.miler || booking.assignedmiler || null;
const agentDecision = booking.agent_decision_id ? booking.agentdecision || booking.agent_decision : null;
return (
<>
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 2 }}>
<Button startIcon={<MdArrowBack size={16} />} onClick={() => navigate('/nearle/orders')} sx={{ color: DT.textSecondary, textTransform: 'none', fontWeight: 700 }}>
Back to Bookings
</Button>
</Stack>
<Paper
elevation={0}
sx={{
p: { xs: 2, md: 3 },
borderRadius: `${DT.radiusCard}px`,
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint('#D35968')} 100%)`,
border: '1px solid',
borderColor: DT.borderSubtle,
mb: 2
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
<MdLocalShipping size={24} />
</Avatar>
<Box>
<Typography variant="h3">{booking.bookingreference || `Booking #${id}`}</Typography>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
{booking.createdat ? new Date(booking.createdat).toLocaleString() : '—'}
</Typography>
</Box>
</Stack>
<Chip
label={STEP_LABELS[status] || status}
sx={{ fontWeight: 800, bgcolor: isTerminal ? soft('#ef4444') : soft(BRAND), color: isTerminal ? '#ef4444' : BRAND, border: `1px solid ${edge(isTerminal ? '#ef4444' : BRAND)}` }}
/>
</Stack>
</Paper>
{isError && (
<Paper elevation={0} sx={{ p: 2, mb: 2, borderRadius: 2, border: `1px solid ${edge('#ef4444')}`, bgcolor: tint('#ef4444') }}>
<Typography sx={{ color: '#ef4444', fontWeight: 600 }}>Could not load full booking details showing whatever came back.</Typography>
</Paper>
)}
{/* Status timeline */}
{!isTerminal && (
<Paper elevation={0} sx={{ p: { xs: 2, md: 3 }, mb: 2, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff' }}>
<Stepper activeStep={activeStepIdx} alternativeLabel={!isMobile} orientation={isMobile ? 'vertical' : 'horizontal'}>
{STATUS_STEPS.map((s) => (
<Step key={s}>
<StepLabel
sx={{
'& .MuiStepIcon-root.Mui-active': { color: BRAND },
'& .MuiStepIcon-root.Mui-completed': { color: BRAND }
}}
>
{STEP_LABELS[s]}
</StepLabel>
</Step>
))}
</Stepper>
</Paper>
)}
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Customer & Pickup
</Typography>
<Stack spacing={2}>
<InfoRow icon={MdPerson} label="Customer" value={booking.customername || booking.customer?.name} />
<InfoRow icon={MdPhone} label="Phone" value={booking.customerphone || booking.customer?.phone} />
<InfoRow icon={MdLocationOn} label="Pickup Address" value={booking.pickupaddress} color="#0ea5e9" />
</Stack>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Delivery
</Typography>
<Stack spacing={2}>
<InfoRow icon={MdLocationOn} label="Delivery Address" value={booking.deliveryaddress} color="#10b981" />
</Stack>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Assigned Miler
</Typography>
{booking.assignedmileruserid || miler ? (
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#8b5cf6'), color: '#8b5cf6' }}>
<MdTwoWheeler size={22} />
</Avatar>
<Box>
<Typography sx={{ fontWeight: 700, color: DT.textPrimary }}>
{miler?.displayname || `Miler #${booking.assignedmileruserid}`}
</Typography>
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mt: 0.25 }}>
{miler?.phone && (
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{miler.phone}
</Typography>
)}
{miler?.rating != null && (
<Stack direction="row" alignItems="center" spacing={0.25}>
<MdStar size={13} style={{ color: '#f59e0b' }} />
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{miler.rating}
</Typography>
</Stack>
)}
</Stack>
</Box>
</Stack>
) : (
<Typography variant="body2" sx={{ color: DT.textMuted }}>
No miler assigned yet.
</Typography>
)}
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Parcel Details
</Typography>
{parcels.length === 0 ? (
<Typography variant="body2" sx={{ color: DT.textMuted }}>
No parcel details available.
</Typography>
) : (
<Stack spacing={1}>
{parcels.map((p, i) => (
<Stack key={i} direction="row" justifyContent="space-between" sx={{ p: 1, borderRadius: 1.5, bgcolor: DT.surfaceAlt }}>
<Typography variant="body2">{p.description || p.name || `Parcel ${i + 1}`}</Typography>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
{p.weight ? `${p.weight}kg` : ''}
</Typography>
</Stack>
))}
</Stack>
)}
</Paper>
</Grid>
{agentDecision && (
<Grid item xs={12}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: `1px solid ${edge('#6366f1')}`, background: tint('#6366f1') }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
<MdOutlineSmartToy size={16} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: '#6366f1' }}>
AI Assignment Reasoning
</Typography>
</Stack>
<Typography variant="body2" sx={{ color: DT.textPrimary }}>
{agentDecision.reasoning || agentDecision.reason || JSON.stringify(agentDecision)}
</Typography>
</Paper>
</Grid>
)}
</Grid>
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
{!isTerminal && (
<Button
variant="contained"
startIcon={<MdOutlineAssignmentInd size={16} />}
disabled={reassignMutation.isLoading}
onClick={() => reassignMutation.mutate()}
sx={{ bgcolor: '#6366f1', '&:hover': { bgcolor: '#4f46e5' } }}
>
Reassign Miler
</Button>
)}
{!isTerminal && (
<Button
variant="outlined"
color="error"
startIcon={<MdOutlineCancel size={16} />}
disabled={cancelMutation.isLoading}
onClick={() => cancelMutation.mutate()}
>
Cancel Booking
</Button>
)}
</Stack>
</>
);
};
export default BookingDetail;

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,88 +1,26 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
// third-party
// import { PatternFormat } from 'react-number-format';
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import axios from 'axios';
import { usePlacesWidget } from 'react-google-autocomplete';
import Loader from 'components/Loader';
import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
// import { setLocationType } from 'react-geocode';
// const avatarImage = require.context('assets/images/users', true);
// styles & constant
// const ITEM_HEIGHT = 48;
// const ITEM_PADDING_TOP = 8;
// const MenuProps = {
// PaperProps: {
// style: {
// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP
// }
// }
// };
const Createclient = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// const [role, setRole] = useState('');
const [mobilenumber, setMobilenumber] = useState('');
const [emailaddress, setEmailaddress] = useState('');
const [city, setCity] = useState('');
const [zipcode, setZipcode] = useState('');
const [address, setAddress] = useState('');
const [state, setState] = useState('');
const [suburb, setSuburb] = useState('');
const [latlong, setLatlong] = useState({});
const [firstname, setFirstname] = useState('');
const [doorno, setDoorno] = useState('');
const [landmark, setLandmark] = useState('');
const [tenantinfo, setTenantinfo] = useState({});
const navigate = useNavigate();
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
// Geocode.setApiKey('AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8');
const [clientname, setClientname] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
// fetchprofiledetails(localStorage.getItem('appuserid'));
// fetchprofiledetails(181);
if (localStorage.getItem('tenantid')) {
fetchtenantinfo(localStorage.getItem('tenantid'));
}
}, []);
useEffect(() => {
try {
Geocode.fromAddress(address).then(
(response) => {
if (response.status == 'OK') {
const { lat, lng } = response.results[0].geometry.location;
setLatlong({
lat,
lng
});
console.log(response);
}
},
(error) => {
console.log(error);
}
);
} catch (err) {
console.log(err);
}
}, [address]);
const navigate = useNavigate();
const opentoast = (message) => {
enqueueSnackbar(message, {
@@ -90,207 +28,45 @@ const Createclient = () => {
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
// console.log(alertmessage)
};
const fetchprofiledetails = async (userid) => {
if (userid) {
setLoading(true);
try {
await axios
.get(`${process.env.REACT_APP_URL2}/tenants/getclient?id=${userid}`)
.then((res) => {
console.log(res);
if (res.data.message === 'Successful') {
let res1 = res.data.details;
setMobilenumber(res1.contactno);
setEmailaddress(res1.primaryemail);
setAddress(res1.address);
setCity(res1.city);
setZipcode(res1.postcode);
setState(res1.state);
setSuburb(res1.suburb);
setLatlong({
lat: res1.latitude,
lng: res1.longitude
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
} catch (err) {
console.log(err);
setLoading(false);
}
}
};
const fetchtenantinfo = async (tid) => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => {
console.log(res);
if (res.data.status) {
setTenantinfo(res.data.details);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => {
console.log(place);
setAddress(place.formatted_address);
let city1, zipcode1, state1, suburb1;
for (let i = 0; i < place.address_components.length; i++) {
for (let j = 0; j < place.address_components[i].types.length; j++) {
switch (place.address_components[i].types[j]) {
case 'locality':
city1 = place.address_components[i].long_name;
break;
case 'administrative_area_level_1':
state1 = place.address_components[i].long_name;
break;
case 'postal_code':
zipcode1 = place.address_components[i].long_name;
break;
case 'sublocality':
suburb1 = place.address_components[i].long_name;
break;
}
}
}
setCity(city1 || '');
setState(state1 || '');
setZipcode(zipcode1 || '');
setSuburb(suburb1 || '');
// setAddress(place.formatted_address)
},
// inputAutocompleteValue: "country",
options: {
// componentRestrictions: 'us',
// types: ["establishment"]
types: ['address' || 'geocode']
}
});
const createprofile = async () => {
console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode);
// if (!businessname) {
// opentoast('Fill Business name')
// } else if (!businessno) {
// opentoast('Fill Registration No')
// }
// else
if (!firstname) {
opentoast('Fill Full name');
if (!clientname) {
opentoast('Fill Client Name');
} else if (!mobilenumber) {
opentoast('Fill Mobile Number');
} else if (!emailaddress) {
opentoast('Fill emailaddress');
} else if (!address) {
opentoast('Fill Address');
opentoast('Fill Email Address');
} else if (!city) {
opentoast('Fill City');
} else if (!zipcode) {
opentoast('Fill post code');
} else if (!suburb) {
opentoast('Fill suburb');
} else if (!latlong.lat || !latlong.lng) {
opentoast('Choose valid address');
} else {
let obj = {
customerid: 0,
configid: 1,
firstname: firstname,
applocationid: tenantinfo.applolcationid,
profileimage: '',
dialcode: '+91',
contactno: mobilenumber,
devicetype: '',
deviceid: '',
customertoken: '',
address: address,
suburb: suburb,
city: city,
state: state,
postcode: zipcode,
landmark: landmark,
doorno: doorno,
latitude: latlong.lat.toString(),
longitude: latlong.lng.toString(),
tenantid: parseInt(localStorage.getItem('tenantid')),
email: emailaddress
};
console.log(obj);
setLoading(true);
try {
await axios
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
.then((res) => {
console.log(res);
if (res.data.status) {
enqueueSnackbar(' Created Successfully ', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
navigate('/clients');
// setTimeout(()=>{
// fetchprofiledetails(localStorage.getItem('appuserid'));
// },2000)
} else if (res.data.message == 'Customer Already available') {
enqueueSnackbar('Customer Already available', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
});
await axios.post(`${process.env.REACT_APP_URL}/crm/clients`, {
clientname,
email: emailaddress,
phone: mobilenumber,
city,
status: 'pending'
});
enqueueSnackbar('Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
navigate('/nearle/tenants');
} catch (err) {
console.log(err);
enqueueSnackbar(err.response?.data?.message || err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
} finally {
setLoading(false);
}
}
};
// const [experience, setExperience] = useState('0');
// const handleChange = (event) => {
// setExperience(event.target.value);
// };
return (
<>
{loading && <Loader />}
@@ -302,158 +78,34 @@ const Createclient = () => {
</Grid>
<MainCard contentSX={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={isMobile ? 2 : 3}>
{/* <Grid item xs={12} sm={4} >
<MainCard title="Personal Information" sx={{ height: '100%' }}>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={2.5} alignItems="center" sx={{ m: 3 }}>
<FormLabel
htmlFor="change-avtar"
sx={{
position: 'relative',
borderRadius: '50%',
overflow: 'hidden',
'&:hover .MuiBox-root': { opacity: 1 },
cursor: 'pointer'
}}
>
<Avatar alt="Avatar 1"
src={avatar}
sx={{ width: 76, height: 76 }} />
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
backgroundColor: theme.palette.mode === ThemeMode.DARK ? 'rgba(255, 255, 255, .75)' : 'rgba(0,0,0,.65)',
width: '100%',
height: '100%',
opacity: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Stack spacing={0.5} alignItems="center">
<CameraOutlined style={{ color: theme.palette.secondary.lighter, fontSize: '1.5rem' }} />
<Typography sx={{ color: 'secondary.lighter' }} variant="caption">
Upload
</Typography>
</Stack>
</Box>
</FormLabel>
<TextField
type="file"
accept="image/*"
id="change-avtar"
placeholder="Outlined"
variant="outlined"
sx={{ display: 'none' }}
onChange={(e) => setSelectedImage(e.target.files?.[0])}
/>
</Stack>
</Grid>
<Grid item xs={12}
>
</Grid>
<Grid item xs={12}
>
</Grid>
<Grid item xs={12}
>
</Grid>
<Grid item xs={12}
>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-role-name">Role</InputLabel>
<TextField fullWidth
id="personal-role-name" placeholder="Role Name" autoFocus
onChange={(e) => setRole(e.target.value)}
value={role}
autoComplete='off'
/>
</Stack>
</Grid>
</Grid>
</MainCard>
</Grid> */}
<Grid
item
xs={12}
// sm={8}
>
<MainCard
// title="Contact Information"
sx={{ height: '100%' }}
contentSX={{ p: { xs: 1.5, md: 2.5 } }}
>
<Grid item xs={12}>
<MainCard sx={{ height: '100%' }} contentSX={{ p: { xs: 1.5, md: 2.5 } }}>
<Grid container spacing={isMobile ? 2 : 3}>
{/* <Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-first-name">Business Name</InputLabel>
<TextField fullWidth
id="personal-first-name" placeholder="Business Name" autoFocus
onChange={(e) => setBusinessname(e.target.value)}
value={businessname}
autoComplete='off'
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-last-name">Registration No</InputLabel>
<TextField fullWidth
id="personal-last-name" placeholder="Registration No"
onChange={(e) => setBusinessno(e.target.value)}
value={businessno}
autoComplete='off'
/>
</Stack>
</Grid> */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-last-name">Admin Name</InputLabel>
<InputLabel htmlFor="client-name">Client Name</InputLabel>
<TextField
fullWidth
id="personal-last-name"
placeholder="Name"
onChange={(e) => setFirstname(e.target.value)}
value={firstname}
id="client-name"
placeholder="Client Name"
onChange={(e) => setClientname(e.target.value)}
value={clientname}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}></Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
<InputLabel htmlFor="client-phone">Phone Number</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
<MenuItem value="+1">+91</MenuItem>
</Select>
<TextField
type="number"
id="personal-phone"
// format="##########"
// mask="_"
id="client-phone"
fullWidth
// customInput={TextField}
placeholder="Phone Number"
// defaultValue="8654239581"
// onBlur={() => { }}
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setMobilenumber(e.target.value);
@@ -461,20 +113,17 @@ const Createclient = () => {
}}
value={mobilenumber}
autoComplete="off"
// disabled
sx={{ cursor: 'not-allowed' }}
/>
</Stack>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
<InputLabel htmlFor="client-email">Email Address</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
id="client-email"
placeholder="Email Address"
onChange={(e) => setEmailaddress(e.target.value)}
value={emailaddress}
@@ -482,44 +131,12 @@ const Createclient = () => {
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-address">Address</InputLabel>
<TextField
fullWidth
// defaultValue="Street 110-B Kalians Bag, Dewan, M.P. New York"
id="personal-address"
placeholder="Address"
value={address}
onChange={(e) => setAddress(e.target.value)}
inputRef={materialRef}
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Suburb</InputLabel>
<InputLabel htmlFor="client-city">City</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Location"
onChange={(e) => setSuburb(e.target.value)}
value={suburb}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
<TextField
fullWidth
// defaultValue="956754"
// type='number'
id="personal-zipcode"
id="client-city"
placeholder="City"
onChange={(e) => setCity(e.target.value)}
value={city}
@@ -527,66 +144,6 @@ const Createclient = () => {
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">State</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="State"
onChange={(e) => setState(e.target.value)}
value={state}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
<TextField
fullWidth
// defaultValue="956754"
type="number"
id="personal-zipcode"
placeholder="Zipcode"
onChange={(e) => setZipcode(e.target.value)}
value={zipcode}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Door No</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Door No"
onChange={(e) => setDoorno(e.target.value)}
value={doorno}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
placeholder="Landmark"
onChange={(e) => setLandmark(e.target.value)}
value={landmark}
autoComplete="off"
/>
</Stack>
</Grid>
</Grid>
</MainCard>
</Grid>

View File

@@ -1,4 +1,4 @@
import { React, useState, useEffect, useRef, useMemo } from 'react';
import { React, useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { FaRegEdit } from 'react-icons/fa';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
@@ -23,7 +23,6 @@ import {
DialogContent,
Button,
TextField,
Autocomplete,
Avatar,
Paper,
useMediaQuery,
@@ -33,19 +32,12 @@ import {
MdMyLocation,
MdPersonPin,
MdPhone,
MdLocationOn,
MdEdit,
MdGroups,
MdHowToReg,
MdPlace,
MdOutlineGroups,
MdOutlineHowToReg,
MdOutlinePlace
} 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 Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
@@ -111,23 +103,6 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
</Avatar>
);
// ==============================|| google address ||============================== //
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
function loadScript(src, position, id) {
if (!position) {
return;
}
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
// ==============================|| MUI TABLE - ENHANCED ||============================== //
export default function Customers() {
@@ -141,146 +116,9 @@ export default function Customers() {
const [locaName, setLocoName] = useState('All');
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
const [open, setOpen] = useState(false);
const [address, setAddress] = useState('');
const [latlong, setLatlong] = useState({});
const [city, setCity] = useState('');
const [postcode, setPostcode] = useState('');
const [state, setState] = useState('');
const [suburb, setSuburb] = useState('');
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
// ==============================|| for google address ||============================== //
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState('');
const [options, setOptions] = useState([]);
const loaded = useRef(false);
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
document.querySelector('head'),
'google-maps'
);
}
loaded.current = true;
}
const fetch = useMemo(
() =>
debounce((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 400),
[]
);
useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google) {
autocompleteService.current = new window.google.maps.places.AutocompleteService();
}
if (!autocompleteService.current) {
return undefined;
}
if (inputValue === '') {
setOptions(value ? [value] : []);
return undefined;
}
fetch({ input: inputValue }, (results) => {
if (active) {
let newOptions = [];
if (value) {
newOptions = [value];
}
if (results) {
newOptions = [...newOptions, ...results];
}
setOptions(newOptions);
}
});
return () => {
active = false;
};
}, [value, inputValue, fetch]);
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
useEffect(() => {
try {
console.log('selected address =>', address);
Geocode.fromAddress(address).then(
(response) => {
console.log('lat long response =>', response.results[0]);
if (response.status == 'OK') {
const { lat, lng } = response.results[0].geometry.location;
setLatlong({
lat,
lng
});
// setSelectedCustomer({
// ...selectedCustomer,
// latitude: lat,
// longitude: lng
// });
if (response.results[0].address_components) {
let place = response.results[0];
let city1, zipcode1, state1, suburb1;
for (let i = 0; i < place.address_components.length; i++) {
for (let j = 0; j < place.address_components[i].types.length; j++) {
switch (place.address_components[i].types[j]) {
case 'locality':
city1 = place.address_components[i].long_name;
break;
case 'administrative_area_level_1':
state1 = place.address_components[i].long_name;
break;
case 'postal_code':
zipcode1 = place.address_components[i].long_name;
break;
case 'sublocality':
suburb1 = place.address_components[i].long_name;
break;
}
}
}
setCity(city1 || '');
setState(state1 || '');
setPostcode(zipcode1 || '');
setSuburb(suburb1 || '');
setSelectedCustomer((prev) => ({
...prev,
city: city1 || '',
state: state1 || '',
postcode: zipcode1 || '',
suburb: suburb1 || '',
latitude: lat || '',
longitude: lng || ''
}));
}
}
},
(error) => {
console.log(error);
}
);
} catch (err) {
console.log(err);
}
}, [address]);
// useEffect(() => {
// selectedCustomer &&
// setLatlong({
// lat: selectedCustomer.latitude,
// lng: selectedCustomer.longitude
// });
// }, [selectedCustomer]);
// ==============================|| getallcustomers (customers) ||============================== //
const {
@@ -336,63 +174,19 @@ export default function Customers() {
useEffect(() => {
console.log('pageCount', pageCount);
}, [pageCount]);
// ==============================|| updateCustomer (post)||============================== //
// ==============================|| updateCustomer (PATCH) ||============================== //
const updateCustomer = async () => {
console.log('selectedCustomer', selectedCustomer);
if (!selectedCustomer.firstname) {
OpenToast('Enter Door NO', 'warning', 1500);
} else if (!selectedCustomer.contactno) {
OpenToast('Enter Contact Number ', 'warning', 1500);
} else if (!selectedCustomer.address) {
OpenToast('Enter Valid Address', 'warning', 1500);
} else if (!selectedCustomer.suburb) {
OpenToast('Enter Suburb', 'warning', 1500);
} else if (!selectedCustomer.city) {
OpenToast('Enter City ', 'warning', 1500);
} else if (!selectedCustomer.state) {
OpenToast('Enter State', 'warning', 1500);
} else if (!selectedCustomer.postcode) {
OpenToast('Enter PostCode', 'warning', 1500);
} else if (!selectedCustomer.landmark) {
OpenToast('Enter Landmark', 'warning', 1500);
} else if (!selectedCustomer.latitude) {
OpenToast('Enter Latitude', 'warning', 1500);
} else if (!selectedCustomer.longitude) {
OpenToast('Enter Longitude', 'warning', 1500);
} else {
try {
const postUpdateResponse = await axios.put(`${process.env.REACT_APP_URL}/customers/update`, {
customerid: selectedCustomer.customerid,
configid: 1,
firstname: selectedCustomer.firstname,
applocationid: selectedCustomer.applocationid,
profileimage: '',
dialcode: '+91',
contactno: selectedCustomer.contactno,
devicetype: '',
deviceid: '',
customertoken: '123',
address: selectedCustomer.address,
suburb: suburb,
city: city,
state: state,
postcode: postcode,
landmark: selectedCustomer.landmark,
doorno: selectedCustomer.doorno,
latitude: selectedCustomer.latitude.toString(),
longitude: selectedCustomer.longitude.toString()
});
console.log('postUpdateResponse', postUpdateResponse);
if (postUpdateResponse.data.status) {
OpenToast(postUpdateResponse.data.message, 'success', 1500);
setOpen(false);
getallcustomersRefetch();
}
} catch (error) {
console.log('postUpdate error', error);
}
try {
await axios.patch(`${process.env.REACT_APP_URL}/admin/customers/${selectedCustomer.appcustomerid}`, {
name: selectedCustomer.name,
phone: selectedCustomer.phone,
email: selectedCustomer.email
});
OpenToast('Customer updated successfully', 'success', 2000);
setOpen(false);
getallcustomersRefetch();
} catch (err) {
OpenToast(err.response?.data?.message || 'Update failed', 'error', 2000);
}
};
const KPI_META = [
@@ -534,7 +328,7 @@ export default function Customers() {
) : (
rows?.map((row, index) => (
<MobileCard
key={row.customerid || `${row.firstname}-${index}`}
key={row.appcustomerid || `${row.name}-${index}`}
accent="#C01227"
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
@@ -544,10 +338,10 @@ export default function Customers() {
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.firstname || '—'}
{row.name || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
ID #{row.appcustomerid}
</Typography>
</Stack>
</Stack>
@@ -573,35 +367,10 @@ export default function Customers() {
}
>
<MobileFieldGrid>
<MobileField label="Contact" value={row.contactno || '—'} />
<MobileField label="Location">
{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 sx={{ fontSize: 13, fontWeight: 600, color: DT.textMuted }}></Typography>
)}
</MobileField>
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
{row.address || '—'}
</Typography>
</MobileField>
<MobileField label="Phone" value={row.phone || '—'} />
<MobileField label="Email" value={row.email || '—'} />
<MobileField label="Total Bookings" value={row.totalbookings ?? 0} />
<MobileField label="Joined" value={row.createdat ? new Date(row.createdat).toLocaleDateString() : '—'} />
</MobileFieldGrid>
</MobileCard>
))
@@ -652,10 +421,11 @@ export default function Customers() {
}}
>
<TableCell>#</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Location</TableCell>
<TableCell>Name</TableCell>
<TableCell>Phone</TableCell>
<TableCell>Email</TableCell>
<TableCell>Total Bookings</TableCell>
<TableCell>Joined</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
@@ -664,7 +434,7 @@ export default function Customers() {
{customersIsLoading && <OrdersTableSkeleton />}
{rows?.length === 0 && !customersIsLoading ? (
<TableRow>
<TableCell colSpan={6} sx={{ py: 6 }}>
<TableCell colSpan={7} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdGroups size={28} />
@@ -681,7 +451,7 @@ export default function Customers() {
) : (
rows?.map((row, index) => (
<TableRow
key={row.customerid || `${row.firstname}-${index}`}
key={row.appcustomerid || `${row.name}-${index}`}
sx={{
cursor: 'pointer',
transition: 'background-color 0.15s',
@@ -708,70 +478,39 @@ export default function Customers() {
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.firstname || '—'}
{row.name || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
ID #{row.appcustomerid}
</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">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdPhone size={12} color={DT.textMuted} />
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.address || '—'}
{row.phone || '—'}
</Typography>
</Tooltip>
</Stack>
</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>
)}
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.email || '—'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.totalbookings ?? 0}
</Typography>
</TableCell>
<TableCell>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.createdat ? new Date(row.createdat).toLocaleDateString() : '—'}
</Typography>
</TableCell>
<TableCell align="right">
<Tooltip title="Edit customer" placement="top">
@@ -797,7 +536,7 @@ export default function Customers() {
)}
{rows?.length !== 0 && (
<TableRow>
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
<TableCell colSpan={7} sx={{ borderBottom: 'none' }}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
@@ -843,36 +582,36 @@ export default function Customers() {
Customer
</Typography>
<Typography sx={{ fontWeight: 800, fontSize: { xs: '1.05rem', sm: '1.2rem' }, lineHeight: 1.2, mt: 0.25 }}>
Edit {selectedCustomer?.firstname || 'Customer'}
Edit {selectedCustomer?.name || 'Customer'}
</Typography>
</Stack>
</Stack>
</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 2 }}>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Customer Name</Typography>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Name</Typography>
<TextField
variant="outlined"
fullWidth
defaultValue={selectedCustomer?.firstname}
value={selectedCustomer?.name || ''}
onChange={(e) => {
setSelectedCustomer({
...selectedCustomer,
firstname: e.target.value
});
setSelectedCustomer((prev) => ({
...prev,
name: e.target.value
}));
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Contact Number</Typography>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Phone</Typography>
<Stack direction={'row'} spacing={1}>
<TextField readonly variant="outlined" value={'+91'} sx={{ width: 60 }} />
<TextField
variant="outlined"
fullWidth
type="text"
value={selectedCustomer?.contactno || ''}
value={selectedCustomer?.phone || ''}
inputProps={{
maxLength: 10,
inputMode: 'numeric', // mobile numeric keypad
@@ -883,168 +622,40 @@ export default function Customers() {
setSelectedCustomer((prev) => ({
...prev,
contactno: value
phone: value
}));
}}
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Address</Typography>
<Autocomplete
id="google-map-demo"
sx={{}}
fullWidth
getOptionLabel={(option) => (typeof option === 'string' ? option : option?.description || '')}
filterOptions={(x) => x}
options={options}
autoComplete
includeInputInList
filterSelectedOptions
value={selectedCustomer?.address}
noOptionsText="No locations"
onChange={(event, newValue) => {
setOptions(newValue ? [newValue, ...options] : options);
setValue(newValue);
console.log('newValue', newValue || '');
setAddress(newValue?.description);
setSelectedCustomer({
...selectedCustomer,
address: newValue?.description
});
}}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => <TextField {...params} fullWidth />}
renderOption={(props, option) => {
const matches = option.structured_formatting.main_text_matched_substrings || [];
const parts = parse(
option.structured_formatting.main_text,
matches.map((match) => [match.offset, match.offset + match.length])
);
return (
<li {...props}>
<Grid container alignItems="center">
<Grid item sx={{ display: 'flex', width: 44 }}>
<LocationOnIcon sx={{ color: 'text.secondary' }} />
</Grid>
<Grid
item
sx={{
width: 'calc(100% - 44px)',
wordWrap: 'break-word'
}}
>
{parts?.map((part, index) => (
<Box
key={index}
component="span"
sx={{
fontWeight: part.highlight ? 'bold' : 'regular'
}}
>
{part.text}
</Box>
))}
<Typography variant="body2" color="text.secondary">
{option?.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
</li>
);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Location</Typography>
<Typography sx={{ mb: 1 }}>Email</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer?.suburb}
type="email"
value={selectedCustomer?.email || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
suburb: value
email: e.target.value
}));
// setSuburb(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>City</Typography>
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Total Bookings</Typography>
<TextField variant="outlined" fullWidth disabled value={selectedCustomer?.totalbookings ?? 0} />
</Grid>
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Joined</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer.city || city || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
city: value
}));
// setCity(e.target.value);
}}
disabled
value={selectedCustomer?.createdat ? new Date(selectedCustomer.createdat).toLocaleDateString() : '—'}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>State</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer.state || state || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
state: value
}));
// setState(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Postcode</Typography>
<TextField
variant="outlined"
fullWidth
value={postcode == '' ? selectedCustomer.postcode : postcode}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
postcode: value
}));
// setPostcode(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Landmark</Typography>
<TextField
variant="outlined"
fullWidth
defaultValue={selectedCustomer.landmark}
onChange={(e) => {
setSelectedCustomer({
...selectedCustomer,
landmark: e.target.value
});
}}
/>
</Grid>
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Latitude</Typography>
<TextField variant="outlined" fullWidth value={latlong.lat} sx={{ cursor: 'not-allowed' }} />
</Grid>
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Longitude</Typography>
<TextField readonly variant="outlined" fullWidth value={latlong.lng} />
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ mr: 2, mb: 2 }}>

View File

@@ -0,0 +1,317 @@
import { useNavigate } from 'react-router-dom';
import { Avatar, Box, Chip, Grid, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, Button } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import dayjs from 'dayjs';
import {
MdOutlineLocalShipping,
MdOutlinePendingActions,
MdOutlineCheckCircle,
MdTwoWheeler,
MdOutlineSmartToy,
MdLocationCity,
MdArrowForward,
MdCircle
} from 'react-icons/md';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { fetchHubs } from 'pages/api/api';
const DT = {
radiusCard: 16,
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 edge = (c) => a(c, '55');
const BRAND = '#C01227';
// Static list — the actual multi-agent pipeline (decide-assignment etc.) has
// no status endpoint yet, so this is presentational only, per spec.
const AGENTS = [
'Intake Agent',
'Geocoding Agent',
'Pricing Agent',
'Miler Matching Agent',
'Route Optimisation Agent',
'Notification Agent',
'Fraud Detection Agent',
'Reconciliation Agent'
];
const fetchBookingsByStatus = async (status, pagesize = 1) => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status, pagesize } });
return res.data;
};
const Dashboard = () => {
const navigate = useNavigate();
const { data: activeStatusCounts, isLoading: activeLoading } = useQuery({
queryKey: ['dashboardActiveCounts'],
queryFn: async () => {
const [assigned, scheduled, atCustomer] = await Promise.all([
fetchBookingsByStatus('Miler_Assigned'),
fetchBookingsByStatus('Pickup_Scheduled'),
fetchBookingsByStatus('At_Customer')
]);
return (assigned.total || 0) + (scheduled.total || 0) + (atCustomer.total || 0);
}
});
const { data: pendingCount, isLoading: pendingLoading } = useQuery({
queryKey: ['dashboardPendingCount'],
queryFn: async () => (await fetchBookingsByStatus('Pending_Pickup')).total || 0
});
const { data: deliveredTodayCount, isLoading: deliveredLoading } = useQuery({
queryKey: ['dashboardDeliveredToday'],
queryFn: async () => (await fetchBookingsByStatus('Delivered')).total || 0
});
const { data: milers = [], isLoading: milersLoading } = useQuery({
queryKey: ['dashboardMilers'],
queryFn: async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
return res.data?.data || [];
}
});
const availableMilers = milers.filter((m) => m.availabilitystatus === 'Available').length;
const { data: hubs = [] } = useQuery({ queryKey: ['fetchHubs'], queryFn: fetchHubs });
const { data: recentBookings = [], isLoading: recentLoading } = useQuery({
queryKey: ['dashboardRecentBookings'],
queryFn: async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { pagesize: 10 } });
return res.data?.data || [];
}
});
// No `city` field is confirmed on a booking (only pickupaddress/deliveryaddress
// were ever specified) -- derives a best-effort city breakdown from hubs
// instead, since hubs do have a confirmed `city` field. Flagged rather than
// guessed at a booking-level city grouping that may not exist.
const cityRows = Object.values(
hubs.reduce((acc, h) => {
const city = h.city || 'Unknown';
if (!acc[city]) acc[city] = { city, milers: 0, hubs: 0 };
acc[city].hubs += 1;
acc[city].milers += milers.filter((m) => m.hubid === h.hubid).length;
return acc;
}, {})
);
const kpis = [
{ key: 'active', label: 'Active Bookings', color: BRAND, icon: MdOutlineLocalShipping, value: activeStatusCounts ?? 0, loading: activeLoading },
{ key: 'pending', label: 'Pending Assignment', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingCount ?? 0, loading: pendingLoading },
{ key: 'delivered', label: 'Delivered Today', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredTodayCount ?? 0, loading: deliveredLoading },
{ key: 'milers', label: 'Milers Available', color: '#0ea5e9', icon: MdTwoWheeler, value: availableMilers, loading: milersLoading }
];
return (
<>
<PageHeader title="Dashboard" subtitle="Live · Operations overview" live />
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{kpis.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={3}>
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} loading={item.loading} />
</Grid>
);
})}
</Grid>
<Grid container spacing={2.5}>
<Grid item xs={12} md={7}>
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
<MdLocationCity size={18} color={BRAND} />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
City Breakdown
</Typography>
</Stack>
<TableContainer>
<Table>
<TableHead>
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
<TableCell>City</TableCell>
<TableCell align="center">Hubs</TableCell>
<TableCell align="center">Milers</TableCell>
<TableCell align="center">Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{cityRows.length === 0 && (
<TableRow>
<TableCell colSpan={4} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
No hub data yet.
</TableCell>
</TableRow>
)}
{cityRows.map((row) => (
<TableRow key={row.city} sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` } }}>
<TableCell sx={{ fontWeight: 700 }}>{row.city}</TableCell>
<TableCell align="center">{row.hubs}</TableCell>
<TableCell align="center">{row.milers}</TableCell>
<TableCell align="center">
<Chip
size="small"
label={row.milers > 0 ? 'Active' : 'Idle'}
sx={{
bgcolor: row.milers > 0 ? tint('#10b981') : tint('#94a3b8'),
color: row.milers > 0 ? '#10b981' : '#94a3b8',
border: `1px solid ${edge(row.milers > 0 ? '#10b981' : '#94a3b8')}`,
fontWeight: 700
}}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
<Paper elevation={0} sx={{ mt: 2.5, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1}>
<MdOutlineLocalShipping size={18} color={BRAND} />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
Recent Bookings
</Typography>
</Stack>
<Button size="small" endIcon={<MdArrowForward size={14} />} onClick={() => navigate('/nearle/orders')} sx={{ textTransform: 'none', fontWeight: 700 }}>
View all
</Button>
</Stack>
<TableContainer>
<Table>
<TableHead>
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
<TableCell>Booking</TableCell>
<TableCell>Status</TableCell>
<TableCell>Created</TableCell>
</TableRow>
</TableHead>
<TableBody>
{recentLoading && (
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
Loading
</TableCell>
</TableRow>
)}
{!recentLoading && recentBookings.length === 0 && (
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
No bookings yet.
</TableCell>
</TableRow>
)}
{recentBookings.map((b) => (
<TableRow
key={b.bookingid}
hover
sx={{ cursor: 'pointer', '& td': { borderBottom: `1px solid ${DT.divider}` } }}
onClick={() => navigate(`/nearle/bookings/${b.bookingid}`)}
>
<TableCell sx={{ fontWeight: 700 }}>{b.bookingreference || `#${b.bookingid}`}</TableCell>
<TableCell>
<Chip size="small" label={b.status || '—'} sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700 }} />
</TableCell>
<TableCell sx={{ color: DT.textSecondary, fontSize: 13 }}>
{b.createdat ? dayjs(b.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Grid>
<Grid item xs={12} md={5}>
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
<MdOutlineSmartToy size={18} color="#6366f1" />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
Agent System Status
</Typography>
</Stack>
<Stack spacing={0} sx={{ p: 1 }}>
{AGENTS.map((agent) => (
<Stack
key={agent}
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ px: 1.5, py: 1.25, borderRadius: 2, '&:hover': { bgcolor: DT.surfaceAlt } }}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<Avatar sx={{ width: 30, height: 30, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
<MdOutlineSmartToy size={15} />
</Avatar>
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }}>
{agent}
</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdCircle size={8} color="#10b981" />
<Typography variant="caption" sx={{ color: '#10b981', fontWeight: 700 }}>
Running
</Typography>
</Stack>
</Stack>
))}
</Stack>
</Paper>
<Paper
elevation={0}
component="button"
onClick={() => navigate('/nearle/hubs')}
sx={{
mt: 2.5,
width: '100%',
p: 2,
borderRadius: `${DT.radiusCard}px`,
border: `1px solid ${edge(BRAND)}`,
background: tint(BRAND),
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
textAlign: 'left',
font: 'inherit'
}}
>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ bgcolor: BRAND, color: '#fff' }}>
<MdLocationCity size={18} />
</Avatar>
<Box>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary }}>Manage Hubs</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{hubs.length} hub{hubs.length === 1 ? '' : 's'} configured
</Typography>
</Box>
</Stack>
<MdArrowForward size={18} color={BRAND} />
</Paper>
</Grid>
</Grid>
</>
);
};
export default Dashboard;

View File

@@ -18,7 +18,6 @@ import {
MdStorefront,
MdLocationOn,
MdDirectionsBike,
MdLocalShipping,
MdNotificationsActive,
MdPersonPin,
MdHistoryToggleOff,
@@ -26,8 +25,6 @@ import {
MdCancel,
MdInventory2,
MdHourglassEmpty,
MdRoute,
MdSkipNext,
MdTune,
MdMyLocation,
MdOutlineLocalShipping,
@@ -162,30 +159,34 @@ const pillFieldSx = () => ({
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
});
// Status palette — drives tab pills, row status badges, dialogs.
// Status palette — drives tab pills, row status badges, dialogs. Keys are
// Doormile's real booking statuses, lowercased (row.status is compared
// lowercased everywhere below so exact backend casing doesn't matter).
const STATUS_META = {
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin },
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn },
picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 },
active: { label: 'Active', color: '#14b8a6', icon: MdRoute },
skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
picked_up: { label: 'Picked Up', color: '#14b8a6', icon: MdInventory2 },
at_hub: { label: 'At Hub', color: '#0ea5e9', icon: MdStorefront },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
};
// Ordered status list driving the tabs row (left → right). Each entry binds
// the visual meta above to the `currentStatus` key the queries use AND to the
// `batchCounts` key (so the chip count for the tab is one lookup).
// Ordered status list driving the tabs row (left → right). `status` is the
// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
// casing Doormile's /admin/bookings status filter expects.
const STATUS_TABS = [
{ status: 'pending', countKey: 'uncoveredLength' },
{ status: 'accepted', countKey: 'assignedLength' },
{ status: 'arrived', countKey: 'arrivedLength' },
{ status: 'picked', countKey: 'pickedLength' },
{ status: 'active', countKey: 'activeLength' },
{ status: 'skipped', countKey: 'skippedLength' },
{ status: 'delivered', countKey: 'coveredLength' },
{ status: 'cancelled', countKey: 'cancelLength' }
{ status: 'all', apiStatus: 'all', countKey: 'all' },
{ status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
{ status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
{ status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
{ status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
{ status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
{ status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
{ status: 'delivered', apiStatus: 'Delivered', countKey: 'delivered' },
{ status: 'cancelled', apiStatus: 'Cancelled', countKey: 'cancelled' }
];
// KPI palette + icons — mirrors the four cards across the top of the page.
@@ -322,10 +323,9 @@ const Deliveries = () => {
const [appId, setAppId] = useState(0);
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [tabstatus, setTabstatus] = useState('Pending');
const [tabstatus, setTabstatus] = useState('All');
const [tabvalue, setTabvalue] = useState(0);
const [open, setOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [kms, setKms] = useState('');
const [cumulativekms, setCumulativeKms] = useState();
const [deliveryamount, setDeliveryamount] = useState();
@@ -333,13 +333,12 @@ const Deliveries = () => {
const [currentorder, setCurrentorder] = useState({});
const [deliverylat, setDeliverylat] = useState('');
const [deliverylong, setDeliverylong] = useState('');
const [currentStatus, setCurrentStatus] = useState('pending');
const [currentStatus, setCurrentStatus] = useState('all');
const [updateStatus, setUpdateStatus] = useState('delivered');
const locationRef = useRef(null);
const tenantRef = useRef(null);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(50);
const [totalCount, setTotalCount] = React.useState();
const [productCollapse, setProductCollapse] = useState(null);
const [orderHeaderid, setOrderHeaderId] = useState(null);
const [searchword, setSearchword] = useState('');
@@ -347,8 +346,6 @@ const Deliveries = () => {
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
const [selectedRow, setSelectedRow] = useState(null);
const [loading1, setLoading1] = useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const [open2, setOpen2] = useState('');
const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false);
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
const [cancelFeed, setCancelFeed] = useState('');
@@ -488,7 +485,7 @@ const Deliveries = () => {
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
},
onError: (err, { selectedRider, selectedRow }) => {
onError: (err, { selectedRow }) => {
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
opentoast(err.message, 'error');
setLoading1(false);
@@ -521,48 +518,9 @@ const Deliveries = () => {
setPage(0);
setTabvalue(i);
setRowsPerPage(50);
if (i === 0) {
setTabstatus('Pending');
setCurrentStatus('pending');
setTotalCount(countData?.uncoveredLength);
}
if (i === 1) {
setTabstatus('Assigned');
setCurrentStatus('accepted');
setTotalCount(countData?.assignedLength);
}
if (i === 2) {
setTabstatus('Arrived');
setCurrentStatus('arrived');
setTotalCount(countData?.arrivedLength);
}
if (i === 3) {
setTabstatus('Picked');
setCurrentStatus('picked');
setTotalCount(countData?.pickedLength);
}
if (i === 4) {
setTabstatus('Active');
setCurrentStatus('active');
setTotalCount(countData?.activeLength);
}
if (i === 5) {
setTabstatus('Skipped');
setCurrentStatus('skipped');
setTotalCount(countData?.skippedLength);
}
if (i === 6) {
setTabstatus('Delivered');
setCurrentStatus('delivered');
setTotalCount(countData?.coveredLength);
}
if (i === 7) {
setTabstatus('Cancelled');
setCurrentStatus('cancelled');
setTotalCount(countData?.cancelLength);
}
console.log(i);
const tab = STATUS_TABS[i];
setTabstatus(STATUS_META[tab.status]?.label || tab.status);
setCurrentStatus(tab.apiStatus);
setSearchword('');
};
@@ -660,21 +618,10 @@ const Deliveries = () => {
const q = String(debouncedSearch || '').trim().toLowerCase();
return countSourceRows.filter((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
const s = String(r.orderstatus || '').toLowerCase();
if (wantStatus && s !== wantStatus) return false;
const s = String(r.status || '').toLowerCase();
if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
if (q) {
const hay = [
r.deliverycustomer,
r.deliveryaddress,
r.deliverysuburb,
r.pickupcustomer,
r.pickupaddress,
r.pickupsuburb,
r.orderid,
r.tenantname,
r.ridername,
r.username
]
const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
.map((v) => String(v || '').toLowerCase())
.join(' ');
if (!hay.includes(q)) return false;
@@ -687,49 +634,12 @@ const Deliveries = () => {
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
const batchCounts = useMemo(() => {
const c = {
uncoveredLength: 0,
assignedLength: 0,
arrivedLength: 0,
pickedLength: 0,
activeLength: 0,
skippedLength: 0,
coveredLength: 0,
cancelLength: 0
};
const c = {};
countSourceRows.forEach((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
const s = String(r.orderstatus || '').toLowerCase();
switch (s) {
case 'pending':
c.uncoveredLength += 1;
break;
case 'accepted':
case 'assigned':
c.assignedLength += 1;
break;
case 'arrived':
c.arrivedLength += 1;
break;
case 'picked':
c.pickedLength += 1;
break;
case 'active':
c.activeLength += 1;
break;
case 'skipped':
c.skippedLength += 1;
break;
case 'delivered':
c.coveredLength += 1;
break;
case 'cancelled':
case 'canceled':
c.cancelLength += 1;
break;
default:
break;
}
const s = String(r.status || '').toLowerCase();
c[s] = (c[s] || 0) + 1;
c.all = (c.all || 0) + 1;
});
return c;
}, [countSourceRows, selectedBatch]);
@@ -807,13 +717,6 @@ const Deliveries = () => {
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
});
useEffect(() => {
console.log('countData', countData);
if (tabvalue === 0 && countData) {
setTotalCount(countData.uncoveredLength);
}
}, [countData]);
// ==============================|| fetchRidersList ||============================== //
const {
@@ -1453,7 +1356,7 @@ const Deliveries = () => {
{(() => {
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
const showSelect = tabstatus == 'Created';
const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
return isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
@@ -1481,15 +1384,14 @@ const Deliveries = () => {
</Stack>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
label: row.orderstatus || '—',
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
label: row.status || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
const RowStatusIcon = rowStatusMeta.icon;
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
const isOpen = productCollapse?.orderid === row?.orderid;
const chipSx = (c) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: c, fontWeight: 700, fontSize: 12, whiteSpace: 'nowrap' });
return (
<MobileCard
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
@@ -1571,54 +1473,35 @@ const Deliveries = () => {
</Stack>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.tenantname}
{row.bookingreference || `#${row.bookingid}`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Order / Location" full>
<MobileField label="Pickup" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.orderid} · {row.deliveryid}
{row.pickupaddress || '—'}
</Typography>
</MobileField>
<MobileField label="Pickup">
<MobileField label="Delivery" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.pickupcustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupcontactno}
{row.deliveryaddress || '—'}
</Typography>
</MobileField>
<MobileField label="Drop">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.deliverycustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverycontactno}
</Typography>
</MobileField>
<MobileField label="Rider" full>
{row.ridername ? (
<MobileField label="Miler" full>
{row.assignedmileruserid ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={24}>
<MdDirectionsBike size={13} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.ridername}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
ID #{row.userid} · {row.ridercontact || '—'}
</Typography>
</Stack>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
#{row.assignedmileruserid}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
@@ -1626,40 +1509,6 @@ const Deliveries = () => {
</Typography>
)}
</MobileField>
<MobileField label="ETA" value={row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} />
<MobileField label="Transit">
<Box sx={chipSx('#06b6d4')}>{row.transitminutes || 0}m</Box>
</MobileField>
<MobileField label="Kms · plan / act">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}>{row.kms || 0} km</Box>
<Box sx={chipSx('#10b981')}>{row.cumulativekms || 0} km</Box>
</Stack>
</MobileField>
<MobileField label="Amount · chg / amt">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}> {row.deliverycharges?.toFixed(2) ?? '0.00'}</Box>
<Box sx={chipSx('#10b981')}> {row.deliveryamt?.toFixed(2) ?? '0.00'}</Box>
</Stack>
</MobileField>
<MobileField label="Qty" value={row.Quantity || '—'} />
<MobileField label="COD">
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
{row.collectionamt ? `${row.collectionamt.toFixed(2)}` : '—'}
</Typography>
</MobileField>
<MobileField label="Step">
{row.step ? (
<Box sx={{ ...chipSx('#C01227'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</MobileField>
{row.notes && (
<MobileField label="Notes" full>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.notes}</Typography>
</MobileField>
)}
</MobileFieldGrid>
{isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
@@ -1746,19 +1595,11 @@ const Deliveries = () => {
)}
<TableCell>#</TableCell>
<TableCell>Status</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Order / Location</TableCell>
<TableCell>Booking</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell>Rider</TableCell>
<TableCell>ETA</TableCell>
<TableCell>Transit</TableCell>
<TableCell>Kms</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Step</TableCell>
<TableCell>Qty</TableCell>
<TableCell>COD</TableCell>
<TableCell>Delivery</TableCell>
<TableCell>Miler</TableCell>
<TableCell>Created</TableCell>
{showAction && <TableCell align="right">Action</TableCell>}
</TableRow>
</TableHead>
@@ -1794,8 +1635,8 @@ const Deliveries = () => {
</TableRow>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
label: row.orderstatus || '—',
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
label: row.status || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
@@ -1876,108 +1717,38 @@ const Deliveries = () => {
</Typography>
</Stack>
</TableCell>
{/* Tenants */}
{/* Booking */}
<TableCell>
<Tooltip title={row.tenantadress}>
<Stack>
<Typography noWrap variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.tenantname}
</Typography>
<Typography noWrap variant="caption" sx={{ color: DT.textSecondary }}>
{row.tenantsuburb}
</Typography>
<Typography noWrap variant="caption" sx={{ color: DT.textMuted }}>
{row.applocation}
</Typography>
</Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.bookingreference || `#${row.bookingid}`}
</Typography>
</TableCell>
{/* Pickup */}
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.pickupaddress || ''} placement="top">
<Typography variant="body2" noWrap>
{row.pickupaddress || '—'}
</Typography>
</Tooltip>
</TableCell>
{/* order details */}
<TableCell align="left">
<Tooltip title="Location Name-Suburb" placement="top">
<Typography variant="subtitle1" noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
{/* Delivery */}
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.deliveryaddress || ''} placement="top">
<Typography variant="body2" noWrap>
{row.deliveryaddress || '—'}
</Typography>
</Tooltip>
<Stack display={'flex'} flexDirection={'row'} gap={3}>
<Stack>
<Tooltip title="Order Id" placement="top">
<Typography variant="body2" noWrap>
{row.orderid}
</Typography>
</Tooltip>
<Tooltip title="Ordered date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.orderdate).utc().format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
-
<Stack>
<Tooltip title="Delivery Id" placement="top">
<Typography variant="body2" noWrap>
{row.deliveryid}
</Typography>
</Tooltip>
<Tooltip title="Delivery date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.deliverydate).utc().format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
{/* pickup */}
<TableCell align="left">
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.pickupcustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.pickupcontactno}</Typography>
<Tooltip title={row.Pickupaddress} sx={{ whiteSpace: 'nowrap' }}>
<Typography variant="caption" sx={{ color: DT.textMuted }}>
{row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')}
</Typography>
</Tooltip>
</Stack>
</TableCell>
{/* drop */}
<TableCell align="left">
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.deliverycustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.deliverycontactno}</Typography>
<Tooltip title={row.deliveryaddress}>
<Typography variant="caption" sx={{ color: DT.textMuted, whiteSpace: 'nowrap' }}>
{row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')}
</Typography>
</Tooltip>
</Stack>
</TableCell>
{/* rider */}
{/* Miler */}
<TableCell>
{row.ridername ? (
{row.assignedmileruserid ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={28}>
<MdDirectionsBike size={14} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.ridername}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.userid} · {row.ridercontact || '—'}
</Typography>
</Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
#{row.assignedmileruserid}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
@@ -1985,116 +1756,10 @@ const Deliveries = () => {
</Typography>
)}
</TableCell>
{/* Estimated Delivery Time */}
<TableCell align="left">
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'}
</Typography>
</TableCell>
{/* Transit Minutes */}
<TableCell align="left">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.5,
borderRadius: 999,
bgcolor: tint('#06b6d4'),
color: '#06b6d4',
fontWeight: 700,
fontSize: 12,
border: `1px solid ${edge('#06b6d4')}`
}}
>
{row.transitminutes || 0}m
</Box>
</TableCell>
{/* kms */}
{/* Created */}
<TableCell>
<Stack direction="column" spacing={0.5} alignItems="flex-start">
<Tooltip title="Planned KMS" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
{row.kms || 0} km
</Box>
</Tooltip>
<Tooltip title="Actual KMS" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
{row.cumulativekms || 0} km
</Box>
</Tooltip>
</Stack>
</TableCell>
{/* amount */}
<TableCell align="left">
<Stack direction="column" spacing={0.5} alignItems="flex-start">
<Tooltip title="Delivery Charge" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
{row.deliverycharges?.toFixed(2) ?? '0.00'}
</Box>
</Tooltip>
<Tooltip title="Delivery Amount" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
{row.deliveryamt?.toFixed(2) ?? '0.00'}
</Box>
</Tooltip>
</Stack>
</TableCell>
{/* notes */}
<TableCell>
{row.notes ? (
<Tooltip title={row.notes}>
<Typography variant="caption" sx={{ color: DT.textSecondary, maxWidth: 160, display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{row.notes}
</Typography>
</Tooltip>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
{/* step */}
<TableCell align="center">
{row.step ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 30,
height: 24,
px: 0.875,
borderRadius: 999,
bgcolor: tint('#C01227'),
border: `1px solid ${edge('#C01227')}`,
color: '#C01227',
fontWeight: 800,
fontSize: 11
}}
>
{row.step}
</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
{/* qty */}
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: row.Quantity ? DT.textPrimary : DT.textMuted, whiteSpace: 'nowrap' }}>
{row.Quantity || '—'}
</Typography>
</TableCell>
{/* COD */}
<TableCell>
<Typography
variant="subtitle2"
sx={{
fontWeight: 800,
color: row.collectionamt ? '#ef4444' : DT.textMuted,
whiteSpace: 'nowrap'
}}
>
{row.collectionamt ? `${row.collectionamt.toFixed(2)}` : '—'}
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</Typography>
</TableCell>
{/* Action */}
@@ -2298,7 +1963,7 @@ const Deliveries = () => {
}
}}
>
{selectedRow?.orderstatus !== 'delivered' && (
{String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
<MenuItem
onClick={() => {
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
@@ -2309,7 +1974,9 @@ const Deliveries = () => {
Notify Rider
</MenuItem>
)}
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
{['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
String(selectedRow?.status || '').toLowerCase()
) && (
<MenuItem
onClick={() => {
if (!appId) {
@@ -2334,7 +2001,7 @@ const Deliveries = () => {
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setUpdateStatus(selectedRow.status || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
@@ -2344,7 +2011,7 @@ const Deliveries = () => {
Update Status
</MenuItem>
)}
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
{!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (
<MenuItem
sx={{ color: '#ef4444 !important' }}
onClick={() => {
@@ -2597,11 +2264,6 @@ const Deliveries = () => {
} else {
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
if (range.label) {
setDatestatus(range.label);
} else {
setDatestatus('');
}
}
console.log(range);
}}
@@ -2646,11 +2308,6 @@ const Deliveries = () => {
startDate: startOfMonth(addMonths(new Date(), -1)),
endDate: endOfMonth(addMonths(new Date(), -1))
}
// {
// label: 'All',
// startDate: new Date(),
// endDate: addDays(new Date(), -1),
// },
]}
/>
</DialogContent>

View File

@@ -96,7 +96,7 @@ const ActiveSection = ({
);
}
const renderActiveDeliveryCard = (o, i) => {
const renderActiveDeliveryCard = (o) => {
const rid = o.rider_id || o.userid;
const rider = riders.find((r) => String(r.id) === String(rid));
const color = getRiderColor(rid);

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, useMapEvents, ZoomControl } from 'react-leaflet';
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, ZoomControl } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Side-effect import: patches L.Polyline so pathOptions.offset (in screen px)
@@ -22,7 +22,6 @@ import {
MdStraighten,
MdLocationOn,
MdMarkunreadMailbox,
MdMoveToInbox,
MdPlace,
MdTwoWheeler,
MdNotes,
@@ -59,7 +58,6 @@ import ProfitabilitySection from './ProfitabilitySection';
import ActiveSection from './ActiveSection';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
import {
STATUS_STYLES,
getStatusStyle,
FINAL_STATUSES,
SKIPPED_STATUSES,
@@ -232,15 +230,6 @@ const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => {
return getBatchForHour(d.hour() + d.minute() / 60, batches);
};
// Sits inside the Compare MapContainer and unpins any pinned popup whenever
// the operator clicks empty map space. Markers' click events do NOT bubble
// to the map, so this only fires on background clicks (which is what we
// want — clicking elsewhere should release the pin).
function CompareMapClickUnpin({ onUnpin }) {
useMapEvents({ click: () => onUnpin() });
return null;
}
// Captures the Leaflet map instance for the parent component via a ref. Kept
// available even after the two-map Compare layout was unified into one map,
// since future per-step imperative zoom logic still needs a handle on the
@@ -983,12 +972,6 @@ const ANALYSIS_BATCH_WINDOWS = [
// Tolerant field-name lookup so the Analysis card still renders cleanly even
// if the API response uses slightly different keys than expected.
const analysisPick = (obj, keys) => {
for (const k of keys) {
if (obj && obj[k] != null && obj[k] !== '') return obj[k];
}
return null;
};
const analysisFormatNum = (v) => {
if (v == null) return '—';
if (typeof v === 'number') return v.toLocaleString('en-IN');
@@ -996,14 +979,6 @@ const analysisFormatNum = (v) => {
if (Number.isFinite(n)) return n.toLocaleString('en-IN');
return String(v);
};
const analysisFormatKm = (v) => (v == null ? '—' : `${parseFloat(v).toFixed(1)} km`);
const analysisFormatRupees = (v) => (v == null ? '—' : `${parseFloat(v).toFixed(0)}`);
const analysisFormatPct = (v) => {
if (v == null) return '—';
const n = parseFloat(v);
if (!Number.isFinite(n)) return '—';
return `${n > 1 ? n.toFixed(1) : (n * 100).toFixed(1)}%`;
};
// Parse "HH:mm:ss" or "HH:mm" → seconds since midnight. Returns null when the
// string is missing or malformed. Used to compute gantt percentages for the
// rider timelines on the Analysis page — the API ships those fields as bare
@@ -1104,7 +1079,7 @@ const Dispatch = ({
// • no batch is selected,
// • the previous request is still in flight (prevents queue
// stacking on slow networks),
// • the browser tab is hidden (saves API quota on routes.workolik).
// • the browser tab is hidden (saves API quota on the Doormile AI layer).
// Loading state is tracked through a ref so the interval doesn't
// reset on every in-flight flip. ─────────────────────────────────
const ANALYSIS_POLL_MS = 15000;
@@ -1163,7 +1138,6 @@ const Dispatch = ({
// Short-lived close timer for the general map order/marker popups.
// Gives the cursor a ~200ms window to travel from the marker onto the popup
// or vice versa without immediately triggering a close.
const activePopupMarkerRef = useRef(null);
const popupHoverTimerRef = useRef(null);
// Order shown in the centered popup overlay. Rendered outside the leaflet
// map (see `dispatch-popup-center` overlay near the bottom of the JSX) so
@@ -3150,7 +3124,7 @@ const Dispatch = ({
? new Map(riderActualTracks.map((t) => [String(t.deliveryid), t.sequenceStep]))
: null;
return ordersToRender.map((o, idx) => {
return ordersToRender.map((o) => {
const rid = o.rider_id;
const active = rid ? activeRiders.has(rid) : true;
let color = getRiderColor(rid);
@@ -3477,13 +3451,6 @@ const Dispatch = ({
return routes;
};
const toggleRider = (rid) => {
const newActive = new Set(activeRiders);
if (newActive.has(rid)) newActive.delete(rid);
else newActive.add(rid);
setActiveRiders(newActive);
};
return (
<div className={`dispatch-container${embedded ? ' embedded' : ''}${compareOpen ? ' compare-open' : ''}`}>
{!embedded && (
@@ -5373,15 +5340,6 @@ const Dispatch = ({
(o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
);
const statusStyle = getStatusStyle(t.orderstatus);
const flagSvg = t.orderstatus
? `<svg class="cmark-flag" viewBox="0 0 18 22" xmlns="http://www.w3.org/2000/svg">
<line x1="1.5" y1="0" x2="1.5" y2="22" stroke="#0f172a" stroke-width="1.6" stroke-linecap="round"/>
<polygon points="2,1 17,1 13.5,5.5 17,10 2,10" fill="${statusStyle.bg}" stroke="#0f172a" stroke-width="0.6" stroke-linejoin="round"/>
${isDelivered ? '<polyline points="5,5.5 7,7.5 11,3.5" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' : ''}
</svg>`
: '';
const dropClasses = ['compare-step-pin'];
if (isFocusedStep) dropClasses.push('is-focused');
if (isDelivered) dropClasses.push('is-delivered');

View File

@@ -0,0 +1,533 @@
import { useState, Fragment } from 'react';
import axios from 'axios';
import {
Avatar,
Box,
Button,
Chip,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton,
MenuItem,
Paper,
Select,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography
} from '@mui/material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { DeploymentUnitOutlined } from '@ant-design/icons';
import { MdAdd, MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp, MdOutlineWarehouse, MdTwoWheeler, MdStar } from 'react-icons/md';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { OpenToast } from 'components/third-party/OpenToast';
import { fetchHubs, createHub, updateHub } from 'pages/api/api';
// ============================================================================
// DT token block — CLAUDE.md §6, copied verbatim.
// ============================================================================
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)',
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 edge = (c) => a(c, '55');
const BRAND = '#C01227';
// Confirmed from the backend test: hubs carry `applocationid` (14), not a
// literal city string. Everything else here (contact/address/pincode as
// editable fields) is inferred from the create-body shape, not confirmed
// present on the GET response — flagged in the summary, not guessed silently.
const CITY_MAP = {
1: { name: 'Coimbatore', code: 'CBE', color: '#f59e0b' },
2: { name: 'Hyderabad', code: 'HYD', color: '#06b6d4' },
3: { name: 'Bangalore', code: 'BLR', color: '#8b5cf6' },
4: { name: 'Chennai', code: 'CHN', color: '#10b981' }
};
const CITY_NAME_TO_ID = Object.fromEntries(Object.entries(CITY_MAP).map(([id, c]) => [c.name, Number(id)]));
const HUB_TYPES = [
{ value: 'sorting_center', label: 'Sorting Center' },
{ value: 'spoke', label: 'Spoke' },
{ value: 'pickup_point', label: 'Pickup Point' }
];
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 EMPTY_FORM = { hubname: '', hubtype: 'sorting_center', city: 'Coimbatore', capacity: '', contact: '', address: '', pincode: '' };
const fetchAllMilers = () =>
axios.get(`${process.env.REACT_APP_URL}/admin/milers`).then((r) => r.data?.data || []);
const Hubs = () => {
const queryClient = useQueryClient();
const [selectedCity, setSelectedCity] = useState('All');
const [dialogOpen, setDialogOpen] = useState(false);
const [editingHub, setEditingHub] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
const [expandedHubId, setExpandedHubId] = useState(null);
const { data: hubs = [], isLoading: hubsLoading } = useQuery({
queryKey: ['fetchHubs'],
queryFn: fetchHubs
});
const { data: milers = [] } = useQuery({
queryKey: ['fetchAllMilers'],
queryFn: fetchAllMilers
});
const createMutation = useMutation({
mutationFn: createHub,
onSuccess: () => {
OpenToast('Hub created', 'success', 2000);
setDialogOpen(false);
setForm(EMPTY_FORM);
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
const updateMutation = useMutation({
mutationFn: ({ id, body }) => updateHub(id, body),
onSuccess: () => {
OpenToast('Hub updated', 'success', 2000);
setDialogOpen(false);
setEditingHub(null);
setForm(EMPTY_FORM);
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
const filteredHubs =
selectedCity === 'All' ? hubs : hubs.filter((h) => CITY_MAP[h.applocationid]?.name === selectedCity);
const kpis = [
{ key: 'total', label: 'Total Hubs', color: BRAND, value: hubs.length },
{ key: 'sorting', label: 'Sorting Centers', color: '#3b82f6', value: hubs.filter((h) => h.hubtype === 'sorting_center').length },
{ key: 'spokes', label: 'Spokes', color: '#14b8a6', value: hubs.filter((h) => h.hubtype === 'spoke').length },
{ key: 'cities', label: 'Cities Covered', color: '#10b981', value: new Set(hubs.map((h) => h.applocationid).filter(Boolean)).size }
];
const openCreate = () => {
setEditingHub(null);
setForm(EMPTY_FORM);
setDialogOpen(true);
};
const openEdit = (hub) => {
setEditingHub(hub);
setForm({
hubname: hub.hubname || '',
hubtype: hub.hubtype || 'sorting_center',
city: CITY_MAP[hub.applocationid]?.name || 'Coimbatore',
capacity: hub.capacity ?? '',
contact: hub.contact || '',
address: hub.address || '',
pincode: hub.pincode || ''
});
setDialogOpen(true);
};
const handleSubmit = () => {
if (!form.hubname) {
OpenToast('Enter a hub name', 'warning', 2000);
return;
}
const body = {
hubname: form.hubname,
hubtype: form.hubtype,
applocationid: CITY_NAME_TO_ID[form.city],
capacity: form.capacity === '' ? undefined : Number(form.capacity),
contact: form.contact,
address: form.address,
pincode: form.pincode
};
if (editingHub) {
updateMutation.mutate({ id: editingHub.hubid, body });
} else {
createMutation.mutate(body);
}
};
return (
<>
{/* ============================================= || Header || ============================================= */}
<Paper
elevation={0}
sx={{
p: { xs: 2, md: 3 },
borderRadius: `${DT.radiusCard}px`,
background: 'linear-gradient(135deg, #C012270A 0%, #D359680A 100%)',
border: '1px solid',
borderColor: DT.borderSubtle,
mb: { xs: 1.5, md: 2 }
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
<DeploymentUnitOutlined style={{ fontSize: 24 }} />
</Avatar>
<Box>
<Typography variant="h3">Hubs</Typography>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.25 }}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#10b981', animation: 'pulse 1.6s infinite' }} />
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
Live · {selectedCity}
</Typography>
</Stack>
</Box>
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap">
{['All', ...Object.values(CITY_MAP).map((c) => c.name)].map((c) => (
<Chip
key={c}
label={c}
onClick={() => setSelectedCity(c)}
sx={{
fontWeight: 700,
bgcolor: selectedCity === c ? BRAND : '#fff',
color: selectedCity === c ? '#fff' : DT.textSecondary,
border: `1px solid ${selectedCity === c ? BRAND : DT.borderSubtle}`,
'&:hover': { bgcolor: selectedCity === c ? BRAND : DT.surfaceAlt }
}}
/>
))}
</Stack>
</Stack>
</Paper>
{/* ============================================= || KPI Cards || ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{kpis.map((item) => (
<Grid item key={item.key} xs={6} sm={3}>
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: `${DT.radiusInner}px`,
border: '1px solid',
borderColor: DT.borderSubtle,
borderTop: `3px solid ${item.color}`,
background: '#fff',
boxShadow: DT.shadowSoft
}}
>
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textMuted, textTransform: 'uppercase', letterSpacing: 0.5 }}>
{item.label}
</Typography>
<Typography variant="h3" sx={{ mt: 0.5, color: DT.textPrimary }}>
{hubsLoading ? '—' : item.value}
</Typography>
</Paper>
</Grid>
))}
</Grid>
{/* ============================================= || New Hub || ============================================= */}
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
<Button
variant="contained"
startIcon={<MdAdd size={16} />}
onClick={openCreate}
sx={{ borderRadius: DT.radiusPill, textTransform: 'none', fontWeight: 700, bgcolor: BRAND, boxShadow: 'none', '&:hover': { bgcolor: '#900E1D', boxShadow: 'none' } }}
>
New Hub
</Button>
</Stack>
{/* ============================================= || Table || ============================================= */}
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, overflow: 'hidden', background: '#fff' }}>
<TableContainer sx={{ maxHeight: 'calc(100vh - 190px)' }}>
<Table stickyHeader sx={{ minWidth: 900 }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: 11,
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: 1.25,
px: 2
}
}}
>
<TableCell>Hub Name</TableCell>
<TableCell>City</TableCell>
<TableCell>Type</TableCell>
<TableCell>Capacity</TableCell>
<TableCell align="center">Milers</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{hubsLoading && <OrdersTableSkeleton col={5} />}
{!hubsLoading && filteredHubs.length === 0 && (
<TableRow>
<TableCell colSpan={7} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdOutlineWarehouse size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No hubs to show
</Typography>
</Stack>
</TableCell>
</TableRow>
)}
{filteredHubs.map((hub) => {
const hubMilers = milers.filter((m) => m.hubid === hub.hubid);
const expanded = expandedHubId === hub.hubid;
const city = CITY_MAP[hub.applocationid];
const isActive = (hub.status || 'active').toLowerCase() === 'active';
return (
<Fragment key={hub.hubid}>
<TableRow sx={{ '& td': { borderBottom: `1px solid ${DT.divider}`, py: 1.5, px: 2 }, '&:hover': { backgroundColor: DT.surfaceAlt } }}>
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: BRAND }}>
{hub.hubname || '—'}
</Typography>
</TableCell>
<TableCell>
{city ? (
<Chip size="small" label={city.name} sx={{ bgcolor: tint(city.color), color: city.color, border: `1px solid ${edge(city.color)}`, fontWeight: 700 }} />
) : (
<Typography variant="body2" sx={{ color: DT.textMuted }}>
</Typography>
)}
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ color: DT.textSecondary, textTransform: 'capitalize' }}>
{(hub.hubtype || '—').replace(/_/g, ' ')}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">{hub.capacity ?? '—'}</Typography>
</TableCell>
<TableCell align="center">
<Tooltip title="View milers at this hub">
<Chip
size="small"
icon={<MdTwoWheeler size={14} />}
label={hubMilers.length}
onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}
sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700, cursor: 'pointer' }}
/>
</Tooltip>
</TableCell>
<TableCell>
<Chip
size="small"
label={isActive ? 'Active' : 'Inactive'}
sx={{
bgcolor: tint(isActive ? '#10b981' : '#ef4444'),
color: isActive ? '#10b981' : '#ef4444',
border: `1px solid ${edge(isActive ? '#10b981' : '#ef4444')}`,
fontWeight: 700
}}
/>
</TableCell>
<TableCell align="right">
<Stack direction="row" justifyContent="flex-end" spacing={0.75}>
<Tooltip title="Edit hub">
<IconButton
size="small"
onClick={() => openEdit(hub)}
sx={{ bgcolor: soft(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, '&:hover': { bgcolor: BRAND, color: '#fff' } }}
>
<MdEdit size={14} />
</IconButton>
</Tooltip>
<IconButton size="small" onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}>
{expanded ? <MdKeyboardArrowUp size={16} /> : <MdKeyboardArrowDown size={16} />}
</IconButton>
</Stack>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={7} sx={{ p: 0, border: 0 }}>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Box sx={{ p: 2, bgcolor: DT.surfaceAlt }}>
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, textTransform: 'uppercase', letterSpacing: 0.5 }}>
Milers at {hub.hubname}
</Typography>
{hubMilers.length === 0 ? (
<Typography variant="body2" sx={{ color: DT.textMuted, mt: 1 }}>
No milers assigned to this hub.
</Typography>
) : (
<TableContainer sx={{ mt: 1, borderRadius: 2, border: `1px solid ${DT.borderSubtle}`, background: '#fff' }}>
<Table size="small">
<TableHead>
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 10.5, textTransform: 'uppercase' } }}>
<TableCell>Miler</TableCell>
<TableCell>Phone</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Rating</TableCell>
</TableRow>
</TableHead>
<TableBody>
{hubMilers.map((m) => {
const availColor = m.availabilitystatus === 'Available' ? '#10b981' : m.availabilitystatus === 'On_Break' ? '#f59e0b' : '#94a3b8';
return (
<TableRow key={m.userid}>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={26}>
<MdTwoWheeler size={13} />
</AccentAvatar>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{m.displayname || `Miler #${m.userid}`}
</Typography>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
{m.phone || '—'}
</Typography>
</TableCell>
<TableCell>
<Chip
size="small"
label={m.availabilitystatus || '—'}
sx={{ bgcolor: tint(availColor), color: availColor, border: `1px solid ${edge(availColor)}`, fontWeight: 700 }}
/>
</TableCell>
<TableCell align="right">
<Stack direction="row" alignItems="center" justifyContent="flex-end" spacing={0.5}>
<MdStar size={13} style={{ color: '#f59e0b' }} />
<Typography variant="body2">{m.rating ?? '—'}</Typography>
</Stack>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
</Paper>
{/* ============================================= || Create / Edit dialog || ============================================= */}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
<DialogTitle sx={{ background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)', color: '#fff' }}>
{editingHub ? `Edit ${editingHub.hubname}` : 'Create New Hub'}
</DialogTitle>
<DialogContent sx={{ mt: 2 }}>
<Grid container spacing={2.5} sx={{ mt: 0.5 }}>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Hub Name</Typography>
<TextField fullWidth value={form.hubname} onChange={(e) => setForm({ ...form, hubname: e.target.value })} />
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Hub Type</Typography>
<Select fullWidth value={form.hubtype} onChange={(e) => setForm({ ...form, hubtype: e.target.value })}>
{HUB_TYPES.map((t) => (
<MenuItem key={t.value} value={t.value}>
{t.label}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>City</Typography>
<Select fullWidth value={form.city} onChange={(e) => setForm({ ...form, city: e.target.value })}>
{Object.values(CITY_MAP).map((c) => (
<MenuItem key={c.name} value={c.name}>
{c.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Capacity</Typography>
<TextField fullWidth type="number" value={form.capacity} onChange={(e) => setForm({ ...form, capacity: e.target.value })} />
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Contact</Typography>
<TextField fullWidth value={form.contact} onChange={(e) => setForm({ ...form, contact: e.target.value })} />
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Pincode</Typography>
<TextField fullWidth inputProps={{ maxLength: 6 }} value={form.pincode} onChange={(e) => setForm({ ...form, pincode: e.target.value.replace(/\D/g, '') })} />
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Address</Typography>
<TextField fullWidth multiline minRows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ p: 2.5 }}>
<Button onClick={() => setDialogOpen(false)} color="secondary" variant="outlined">
Cancel
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={createMutation.isLoading || updateMutation.isLoading}
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: '#900E1D' } }}
>
{editingHub ? 'Save Changes' : 'Create Hub'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default Hubs;

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
// This is the active login page — matched by routes/LoginRoutes.js, which is
// registered before MainRoutes.js and wins the route match for both '/' and
// '/login'. (login1.js is dead code — MainRoutes.js's own '/login' route is
// unreachable because LoginRoutes.js's un-prefixed '/login' matches first.)
import { useState, useEffect } from 'react';
import { enqueueSnackbar, closeSnackbar } from 'notistack';
import AnimateButton from 'components/@extended/AnimateButton';
import OtpInput from 'react18-input-otp';
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, FormLabel, IconButton, InputAdornment } from '@mui/material';
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
@@ -11,7 +13,6 @@ import Loader from 'components/Loader';
import doormileLogo from 'assets/images/doormile-logo.png';
import { useSelector, useDispatch } from 'react-redux';
import { OpenToast } from 'components/third-party/OpenToast';
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { setLoginUser } from 'store/reducers/loginUserSlice';
@@ -23,135 +24,60 @@ const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%)
const Login = () => {
const dispatch = useDispatch();
const fcmtoken = useSelector((state) => state.fcm);
const permission = useSelector((state) => state.fcm.permission);
const theme = useTheme();
const [loading, setLoading] = useState(false);
let navigate = useNavigate();
const [otp, setOtp] = useState('');
const [currentotp, setCurrentotp] = useState('');
const [userinfo, setUserinfo] = useState({});
const [username, setUsername] = useState('');
const [passwordStatus, setPasswordStatus] = useState(0);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isPassword, setIspassword] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [userid, setUserid] = useState(0);
useEffect(() => {
if (localStorage.getItem('firstname')) {
if (localStorage.getItem('authname')) {
navigate('/nearle/dispatch');
}
}, []);
const loginsend = async () => {
setLoading(true);
if (!username) {
if (!username || !password) {
opentoast('Fill All required fields');
setLoading(false);
return;
}
setLoading(true);
try {
const res = await axios.post(`https://jupiter.nearle.app/live/api/v1/users/console/login`, {
authname: username,
configid: 9, // 9 -> config id for nearle console admin
userfcmtoken: fcmtoken?.token,
password
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
email: username,
password,
userfcmtoken: fcmtoken?.token
});
// user not found
if (res.data.code == 409 && !res.data.status) {
OpenToast(res.data.message, 'error', 3000);
}
// user not activated
else if (res.data.code == 403) {
OpenToast(res.data.message, 'warning', 3000);
}
//user found, no password, setup password
else if (res.data.code == 409 && res.data.status) {
setPasswordStatus(1); // for password and confirm password ui
setUserid(res.data.details.userid);
OpenToast('User Found', 'success', 3000);
OpenToast(res.data.message, 'success', 3000);
}
//user found, incorrect password
else if (res.data.code == 401 && !res.data.status) {
OpenToast(res.data.message, 'error', 3000);
}
//user found, enter password
else if (res.data.code == 401 && res.data.status) {
OpenToast(res.data.message, 'success', 3000);
fetchAppLocations(res.data.userid);
setPasswordStatus(2);
}
// user found, correct password
else if (res.data.code == 200 && res.data.status) {
OpenToast(res.data.message, 'success', 1000);
setUserinfo(res.data.details);
const userinfo = res.data.details;
dispatch(setLoginUser(userinfo));
localStorage.setItem('firstname', userinfo.firstname);
localStorage.setItem('authname', userinfo.authname);
localStorage.setItem('roleid', userinfo.roleid);
localStorage.setItem('tenantid', userinfo.tenantid);
localStorage.setItem('partnerid', userinfo.partnerid);
localStorage.setItem('applocationid', userinfo.applocationid);
localStorage.setItem('userid', userinfo.userid);
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
if (res.data.success) {
OpenToast('Login Successful', 'success', 1000);
// The live backend's success response doesn't nest user fields under
// `data` the way the original spec described — fall back to reading
// them off the top-level response if `data` isn't there.
const token = res.data.token;
const data = res.data.data || res.data.user || res.data;
dispatch(setLoginUser(data));
localStorage.setItem('authname', data.email || data.firstname || username);
localStorage.setItem('userid', data.userid ?? data.id ?? '');
localStorage.setItem('roleid', data.roleid ?? data.role ?? '');
localStorage.setItem('token', token);
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
markSessionStart();
fetchAppLocations(userinfo.userid);
navigate('/nearle/dispatch');
} else {
OpenToast(res.data.message, 'error', 3000);
OpenToast(res.data.message || 'Invalid Data', 'error', 3000);
}
} catch (err) {
console.error(err);
OpenToast(err.message, 'error', 5000);
OpenToast(err.response?.data?.message || err.message, 'error', 5000);
} finally {
setLoading(false);
}
};
const loginsuccessful = () => {
localStorage.setItem('firstname', userinfo.firstname);
localStorage.setItem('authname', userinfo.authname);
localStorage.setItem('roleid', userinfo.roleid);
localStorage.setItem('tenantid', userinfo.tenantid);
localStorage.setItem('partnerid', userinfo.partnerid);
localStorage.setItem('applocationid', userinfo.applocationid);
localStorage.setItem('userid', userinfo.userid);
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
markSessionStart();
closeGlobalToast(); // to close the pin snackbar
navigate('/nearle/dispatch');
};
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1500
});
};
const fetchAppLocations = async (id) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${id}`);
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
};
const updateUser = async () => {
const response = await axios.put(`${process.env.REACT_APP_URL2}/users/update`, {
userid,
password
});
if (response.data.status) {
OpenToast(response.data.message, 'success', 3000);
OpenToast('Enter Password to Login', 'success', 3000);
setPasswordStatus(2);
setPassword('');
}
OpenToast(message, 'error', 1500);
};
return (
@@ -273,28 +199,7 @@ const Login = () => {
noValidate
onSubmit={(e) => {
e.preventDefault();
if (passwordStatus == 0) {
loginsend();
} else if (passwordStatus == 1) {
if (!password || !confirmPassword || password != confirmPassword) {
OpenToast('Check Password', 'warning', 3000);
} else {
updateUser();
}
} else if (passwordStatus == 2) {
if (!password) {
OpenToast('Invalid Password', 'warning', 3000);
}
loginsend();
}
// if (currentotp) {
// if (currentotp == otp) {
// loginsuccessful();
// fetchAppLocations();
// } else {
// opentoast('Invalid pin');
// }
// }
loginsend();
}}
>
<Stack spacing={3}>
@@ -308,122 +213,27 @@ const Login = () => {
required
value={username}
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
InputProps={{ readOnly: passwordStatus }}
/>
{/* Setup Password */}
{passwordStatus == 1 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
<Typography variant="h4" textAlign="start" mb={3}>
Setup Password
</Typography>
<TextField
autoFocus
fullWidth
label="Enter New Password"
variant="outlined"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
<TextField
error={confirmPassword !== '' && password !== confirmPassword}
fullWidth
label="Re-Enter Password"
variant="outlined"
required
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end">
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
)}
{/* Enter Password */}
{passwordStatus == 2 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
<Typography variant="h4" textAlign="start" mb={3}>
Enter Password
</Typography>
<TextField
autoFocus
fullWidth
label="Enter Password"
variant="outlined"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
)}
{/* OTP */}
{isPassword && (
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<FormLabel>Enter Password</FormLabel>
<Link
variant="body2"
sx={{ cursor: 'pointer' }}
onClick={() => {
setOtp('');
loginsend();
}}
>
Retry
</Link>
</Stack>
{/* <OtpInput
shouldAutoFocus
value={otp}
onChange={(otp) => setOtp(otp)}
numInputs={4}
containerStyle={{ justifyContent: 'space-between' }}
inputStyle={{
width: 48,
height: 48,
borderRadius: 8,
border: `1px solid ${borderColor}`,
fontSize: 18
}}
focusStyle={{
outline: 'none',
border: `1px solid ${theme.palette.primary.main}`,
boxShadow: theme.customShadows.primary
}}
/> */}
<TextField type="passowrd" value={password} onChange={(e) => setPassword(e.target.value)} />
</Stack>
)}
{/* Password */}
<TextField
fullWidth
label="Password"
variant="outlined"
autoComplete="current-password"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
{/* Submit */}
<AnimateButton>
<Button fullWidth size="large" type="submit" variant="contained" color="primary">

View File

@@ -1,6 +1,9 @@
// UNUSED — routes/LoginRoutes.js is registered before MainRoutes.js and
// defines its own un-prefixed '/login' route pointing at pages/nearle/login,
// which wins the match. This file's '/login' route inside MainRoutes.js is
// unreachable. Kept updated to Doormile endpoints anyway (harmless), but
// pages/nearle/login.js is the one that actually renders.
import { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
// import AuthWrapper from 'sections/auth/AuthWrapper';
import {
Box,
Grid,
@@ -30,17 +33,17 @@ const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%)
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
// import { openSnackbar } from 'store/reducers/snackbar';
// import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
const Login = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const fcmtoken = useSelector((state) => state.fcm);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [alertmessage, setAlertmessage] = useState('');
const [checkusername, setCheckusername] = useState(false);
const [, setAlertmessage] = useState('');
// const [toast, setToast] = useState(false);
const [loading, setLoading] = useState(false);
let navigate = useNavigate();
@@ -78,119 +81,48 @@ const Login = () => {
// console.log(alertmessage)
}, []);
const usernamecheck = async (e) => {
e.preventDefault();
setUsername(e.target.value);
if (e.target.value) {
try {
// await axios.post(`${process.env.REACT_APP_URL}/auth/login`, {
// "authname": e.target.value
// })
await axios
.post(`${process.env.REACT_APP_URL}/users/login`, {
authname: e.target.value,
configid: 1
// "contactno": e.target.value,
// "password": 'admin'
})
.then((res) => {
console.log(res.data);
if (res.data.details.authname === e.target.value) {
setUsername(e.target.value);
setCheckusername(false);
} else {
setCheckusername(true);
}
// if (res.data.authname === e.target.value) {
// setUsername(e.target.value);
// setCheckusername(false);
// }
})
.catch((err) => {
// if (err.response.data.message === 'No user found') {
setCheckusername(true);
// }
});
} catch (err) {
console.log(err);
}
}
};
const loginsend = async () => {
// e.preventDefault();
setLoading(true);
if (password && username) {
if (password == 'admin') {
setSubmitting(true);
try {
await axios
.post(`${process.env.REACT_APP_URL}/users/partner/login`, {
// "authname": username,
configid: 1,
contactno: username
// "password": password
})
.then((res) => {
console.log(res.data);
if (res.data.status) {
if (res.data.details.contactno === username) {
enqueueSnackbar('login Successfull', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 3000
});
setUsername('');
setPassword('');
localStorage.setItem('firstname', res.data.details.tenantname);
localStorage.setItem('authname', res.data.details.authname);
// localStorage.setItem("appuserid", res.data.details.userid);
localStorage.setItem('roleid', res.data.details.roleid);
localStorage.setItem('tenantid', res.data.details.tenantid);
localStorage.setItem('partnerid', res.data.details.partnerid);
navigate('/orders');
setSubmitting(false);
}
}
console.log(res.data.message);
setLoading(false);
})
.catch((err) => {
console.log(err);
// setAlertmessage('Invalid Data');
// if(err.message == 'Network Error'){
opentoast(err.message);
// }else{
// opentoast('Invalid Data');
// }
setLoading(false);
setSubmitting(false);
console.log(err.message);
});
} catch (err) {
console.log(err);
setLoading(false);
setSubmitting(false);
}
} else {
opentoast('Password is Incorrect');
setLoading(false);
}
} else {
// let el2 = document.getElementById('toastid');
// el2.classList.add('d-block');
// el2.classList.remove('d-none');
if (!password || !username) {
setAlertmessage('Fill All required fields');
opentoast('Fill All required fields');
setLoading(false);
return;
}
setSubmitting(true);
try {
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
email: username,
password,
userfcmtoken: fcmtoken?.token
});
if (res.data.success) {
const { token, data } = res.data;
enqueueSnackbar('Login Successful', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 3000
});
setUsername('');
setPassword('');
localStorage.setItem('authname', data.email || data.firstname);
localStorage.setItem('userid', data.userid);
localStorage.setItem('roleid', data.roleid);
localStorage.setItem('token', token);
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
navigate('/orders');
} else {
opentoast(res.data.message || 'Invalid Data');
}
} catch (err) {
opentoast(err.response?.data?.message || err.message);
} finally {
setLoading(false);
setSubmitting(false);
}
};
@@ -278,10 +210,6 @@ const Login = () => {
<CardHeader title={<Typography variant="h3">Login</Typography>} />
</Stack>
</Grid>
{/* <Grid item xs={12}>
<AuthLogin isDemo={isLoggedIn} />
</Grid> */}
</Grid>
<CardContent>
<form
@@ -325,8 +253,8 @@ const Login = () => {
variant="outlined"
autoComplete="email"
required
onChange={usernamecheck}
error={checkusername}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
margin="normal"
@@ -516,4 +444,4 @@ const Login = () => {
);
};
export default Login1;
export default Login;

View File

@@ -17,7 +17,7 @@ import {
Backdrop,
IconButton
} from '@mui/material';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import React, { Fragment, useEffect, useState } from 'react';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
@@ -32,7 +32,7 @@ import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import { Empty } from 'antd';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { DashboardFilled, OpenAIFilled } from '@ant-design/icons';
import { DashboardFilled } from '@ant-design/icons';
import { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6';
import { HiOutlineArrowLeft } from 'react-icons/hi';
@@ -234,9 +234,7 @@ const OrdersPreview = () => {
const {
data: paymentModes = [],
isLoading: paymentModesLoading,
isError: paymentModesError,
error: paymentModesErrorMessage
isLoading: paymentModesLoading
} = useQuery({
queryKey: ['paymentmodes'],
queryFn: fetchPaymentType
@@ -246,24 +244,13 @@ const OrdersPreview = () => {
const {
data: ridersList = [],
isLoading: ridersListLoading,
isError: ridersListError,
error: ridersListErrorMessage,
refetch: ridersListRefetch
isLoading: ridersListLoading
} = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
queryFn: fetchRidersList,
enabled: appId !== 0 // Ensures query runs only when appId is valid
});
const getRiderName = async (userid) => {
await ridersList.map((rider) => {
if (rider.userid == userid) {
return rider.firstname;
}
});
};
// ======================================================= || notifyRiderMutation || =======================================================
const notifyRiderMutation = useMutation({

View File

@@ -9,7 +9,6 @@ import {
Button,
TextField,
Autocomplete,
Chip,
Divider,
DialogTitle,
DialogContent,
@@ -19,7 +18,6 @@ import {
IconButton,
Switch,
OutlinedInput,
FormGroup,
FormControlLabel,
Box,
Card,
@@ -28,7 +26,7 @@ import {
import CloseIcon from '@mui/icons-material/Close';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import { Empty } from 'antd';
import { FaPhoneAlt, FaBox, FaBoxes, FaTruck, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa';
import { FaPhoneAlt, FaBox, FaBoxes, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa';
import { GiDoorHandle } from 'react-icons/gi';
import { FaLandmarkDome } from 'react-icons/fa6';
import ClearIcon from '@mui/icons-material/Clear';
@@ -39,8 +37,6 @@ import axios from 'axios';
import { useTheme } from '@mui/material/styles';
import Geocode from 'react-geocode';
import Loader from 'components/Loader';
import * as geolib from 'geolib';
import MainCard from 'components/MainCard';
import { FaUser } from 'react-icons/fa6';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
@@ -50,7 +46,7 @@ import dayjs from 'dayjs';
import { enqueueSnackbar } from 'notistack';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { SearchOutlined, CloseOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons';
import { SearchOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons';
import MyLocationIcon from '@mui/icons-material/MyLocation';
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
import { OpenToast } from 'components/third-party/OpenToast';
@@ -58,7 +54,6 @@ import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import './OrdersRedesign.css';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import AnimateButton from 'components/@extended/AnimateButton';
const pickupIcon = typeof window !== 'undefined' ? new L.Icon({
@@ -192,14 +187,12 @@ const Createorder1 = () => {
const tenantRef = useRef(null);
const [inputValue1, setInputValue1] = React.useState('');
const [inputValue2, setInputValue2] = React.useState('');
const [tenanatLocoId, setTenanatLocoId] = useState(localStorage.getItem('locationid'));
const [isLocation, setIsLocation] = useState(false);
const textFieldRef1 = useRef(null);
const textFieldRef1a = useRef(null);
const textFieldRef2 = useRef(null);
const [appId, setAppId] = useState(0);
const [open, setOpen] = useState(false);
const [clientdetail, setClientdetail] = useState([]);
const [, setClientdetail] = useState([]);
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
// const [starttime, setStatrttime] = useState(`${dayjs().format('MM-DD-YYYY')} 08:00:00`);
const [starttime, setStatrttime] = useState();
@@ -224,16 +217,15 @@ const Createorder1 = () => {
const [minKm, setMinKm] = useState(0);
const [totalCharge, setTotalCharge] = useState(0);
const [subCat, setSubCat] = useState([]);
const [subCatName, setSubCatName] = useState('Select ');
const [, setSubCatName] = useState('Select ');
const [subCatId, setSubCatId] = useState(0);
const [weight, setWeight] = useState('');
const [weight] = useState('');
const [tenantid, setTenantid] = useState(0);
const [locationid, setLocationid] = useState(0);
const [selectedCatChip, setSelectedCatChip] = useState(null);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
const [customerlist, setCustomerlist] = useState([]);
const [defaultPickup, setDefaultPickup] = useState(null);
const [, setDefaultPickup] = useState(null);
const [pickCust, setPickCust] = useState(null);
const [dropCust, setDropCust] = useState(null);
const [pickordrop, setpickordrop] = useState(0); // 1 ->pick 2 -> drop
@@ -330,20 +322,6 @@ const Createorder1 = () => {
appId && fetchtenantinfolist();
}, [appId]);
const handleChipClick = (chipLabel) => {
setSelectedCatChip(chipLabel);
};
const chipStyle = (chipLabel) => ({
cursor: 'pointer',
backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : 'default',
color: selectedCatChip === chipLabel ? '#fff' : '',
'&:hover': {
backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : theme.palette.primary.light,
color: '#fff'
}
});
const fetchTenantPricing = async (id) => {
try {
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
@@ -473,9 +451,9 @@ const Createorder1 = () => {
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
let arr = [];
for (
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
j++, i = dayjs(i).add(30, 'm')
i = dayjs(i).add(30, 'm')
) {
arr.push(i);
}
@@ -859,7 +837,6 @@ const Createorder1 = () => {
// radius: 100000 //km to m
}).getBounds()
});
let arr = [];
// Event listener for autocomplete place changed
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();

View File

@@ -1,39 +1,15 @@
import {
useEffect,
useState,
Fragment
// useReducer
} from 'react';
import { useEffect, useState, Fragment } from 'react';
import BorderColorIcon from '@mui/icons-material/BorderColor';
import {
// Navigate,
// useSearchParams,
useLocation,
useNavigate
} from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import axios from 'axios';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import {
// UserOutlined,
EnvironmentOutlined,
EditTwoTone
// DeleteTwoTone
} from '@ant-design/icons';
// import WomanIcon from '@mui/icons-material/Woman';
// import { Link } from 'react-router-dom';
// import SoupKitchenIcon from '@mui/icons-material/SoupKitchen';
import { EnvironmentOutlined, EditTwoTone } from '@ant-design/icons';
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
import { KeyboardArrowUp, KeyboardArrowDown } from '@mui/icons-material';
// import { PopupTransition } from 'components/@extended/Transitions';
// import { useDispatch } from 'react-redux';
// import { openSnackbar } from 'store/reducers/snackbar';
// assets
import { DeleteFilled, NotificationOutlined } from '@ant-design/icons';
var utc = require('dayjs/plugin/utc');
// import { groupBy } from "core-js/actual/array/group-by";
// import "lodash.chunk";
// var chunk = require('lodash.chunk');
import {
Grid,
Typography,
@@ -79,13 +55,8 @@ import { PopupTransition } from 'components/@extended/Transitions';
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
import MainCard from 'components/MainCard';
import Loader from 'components/Loader';
// import AlertCustomerDelete from 'sections/apps/customer/AlertCustomerDelete';
import dayjs from 'dayjs';
dayjs.extend(utc);
// import { Link as RouterLink } from 'react-router-dom';
// import PlayCircleFilled from '@mui/icons-material/PlayCircleFilled';
// import SmileFilled from '@mui/icons-material/Mood';
// import HeartFilled from '@mui/icons-material/Favorite';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
@@ -181,23 +152,6 @@ const Details = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// const fetchorderdetails = async () => {
// setLoading(true);
// await axios
// .get(`${process.env.REACT_APP_URL}/orders/orderbyid/?orderheaderid=${orderheaderid}`)
// .then((res) => {
// console.log(res);
// setLoading(false);
// })
// .catch((err) => {
// console.log(err);
// setLoading(false);
// });
// };
const fetchorderaddons = async () => {
setLoading(true);
await axios
@@ -249,21 +203,6 @@ const Details = () => {
console.log('res');
console.log(res);
setOrderarr(res.data.Details || []);
// let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id'))
// orderheaderid
// console.log(result)
// setOrderaddons(result.orderaddons);
// setVenuetype(result.venuetype)
// setOtherinstructions(result.remarks)
// console.log("res");
// let result = _.chain(res.data.Details)
// .groupBy("shiftid")
// .map((value, key) => ({shiftid:key, locationaddress: value[0].locationaddress, roles: value }))
// .value()
// setcategoryarr(result);
console.log('categoryarr');
setcategoryarr(res.data.Details);
console.log(res.data.Details);
@@ -305,15 +244,7 @@ const Details = () => {
const cancelorder = async () => {
await axios
.put(`${process.env.REACT_APP_URL2}/orders/cancel`, {
// "Orderheaderid": parseInt(orderheaderid),
// "Tenantid": parseInt(tenantid),
// "Orderstatus": "cancelled",
// "Currentdatetime": dayjs().format('YYYY-MM-DD HH:mm:ss'),
// "Cod": false,
// "Remarks": "",
orderheaderid: parseInt(orderheaderid),
// "orderdetailid":78,
// "shiftid":788,
orderstatus: 'cancelled',
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss'),
unserviceable: invoiceeligible ? 0 : 1
@@ -443,20 +374,10 @@ const Details = () => {
fetchorderaddons();
fetchorderattires();
fetchassignedcount();
// fetchuserdetails();
console.log(location.state || '');
// setOrderid(location.state.orderid || '');
// setEventlocation(location.state.eventlocation || '');
// setEventlocation(address || []);
// setOrderdate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '');
// setDuedate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '')
// setEventname(location.state.eventname || '');
// setClientname(location.state.tenantname || '')
} else {
setLoading(false);
}
// fetchorderdetails();
console.log(orderheaderid, tenantid);
}, [orderheaderid, tenantid, assignedpendingcount]);
@@ -522,12 +443,6 @@ const Details = () => {
.then((res) => {
console.log(res);
if (res.data.message === 'Successfully created') {
// if (orderheaderid && tenantid) {
// fetchorderdetails();
// fetchorderaddons();
// fetchorderattires();
// }
enqueueSnackbar('Roles assigned successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
@@ -631,32 +546,6 @@ const Details = () => {
});
};
// const updateorderstatus = async () => {
// await axios.put(`${process.env.REACT_APP_URL2}/orders/updateorderstatus`,{
// "orderheaderid":orderheaderid,
// "tenantid":tenantid,
// "orderstatus":"processing",
// "pending":"",
// "processing":dayjs().format('YYYY-MM-DD HH:mm:ss'),
// "completed":""
// })
// .then((res) => {
// console.log(res)
// fetchorderdetails();
// fetchorderaddons();
// fetchorderattires();
// })
// .catch((err) => {
// console.log(err)
// fetchorderdetails();
// fetchorderaddons();
// fetchorderattires();
// })
// }
const fetchassignedcount = async () => {
// console.log(obj1)
await axios

View File

@@ -55,9 +55,7 @@ import {
CalendarOutlined,
ClockCircleOutlined,
FileTextOutlined,
InboxOutlined,
LockOutlined,
CheckCircleFilled
InboxOutlined
} from '@ant-design/icons';
import { Empty } from 'antd';
import { FaUser, FaTruck, FaUsers, FaPaperPlane, FaRoute, FaMoneyBillWave, FaBoxes, FaReceipt } from 'react-icons/fa';
@@ -66,7 +64,6 @@ import { MdOutlineCloudUpload } from 'react-icons/md';
import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import AnimateButton from 'components/@extended/AnimateButton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import './OrdersRedesign.css';

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { LoadScriptNext, GoogleMap } from '@react-google-maps/api';
const containerStyle = {
width: '100%',
height: '90vh'
};
const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
const mapRef = useRef(null);
/** Convert coordinates to numbers */
const numericCoordinates = coordinates
.map((c) => {
const lat = Number(c.lat);
const lng = Number(c.lng);
return isNaN(lat) || isNaN(lng) ? null : { lat, lng };
})
.filter(Boolean);
if (numericCoordinates.length < 2) {
return <div>No route data available</div>;
}
const start = numericCoordinates[0];
const end = numericCoordinates[numericCoordinates.length - 1];
/** Map loaded callback */
const onMapLoad = (map) => {
// draw markers
new window.google.maps.Marker({
position: start,
map,
label: 'S',
title: `Start: ${additionalProps?.riderStart}`
});
new window.google.maps.Marker({
position: end,
map,
label: 'E',
title: `End: ${additionalProps?.riderEnd}`
});
// draw rider route (point-to-point)
const route = new window.google.maps.Polyline({
path: numericCoordinates,
geodesic: false,
strokeColor: '#1A73E8',
strokeOpacity: 1.0,
strokeWeight: 4
});
route.setMap(map);
// auto fit
const bounds = new window.google.maps.LatLngBounds();
numericCoordinates.forEach((p) => bounds.extend(p));
map.fitBounds(bounds);
};
return (
<>
<button
onClick={() => setMapOpen(false)}
style={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 999,
padding: '6px 12px',
background: '#1A73E8',
color: 'white',
borderRadius: 6,
cursor: 'pointer',
border: 'none'
}}
>
Close
</button>
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
<GoogleMap mapContainerStyle={containerStyle} center={start} zoom={14} onLoad={onMapLoad}>
{/* Polyline and markers added via onLoad */}
</GoogleMap>
</LoadScriptNext>
</>
);
};
export default MapWithRouteGoogle;

View File

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

View File

@@ -1,285 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { GoogleMap, Polyline, Marker, InfoWindow, useJsApiLoader } from '@react-google-maps/api';
import { Box, IconButton, Stack, Typography, CircularProgress } from '@mui/material';
import { MdClose, MdRoute } from 'react-icons/md';
const containerStyle = { width: '100%', height: '100%' };
// Renders a single rider's PLANNED route for the date range chosen on the
// Riders Summary page. `details` is an ordered array of waypoints (sorted by
// the planning step number) shaped as:
// { step, orderid, deliveryid, customer, address,
// dropLat, dropLng, pickLat, pickLng, expectedTime }
// `dropLat/dropLng` are required; pickup coords are optional and rendered as
// faded pre-stops if present.
export default function RidersRoutes({ details, loading, riderName, dateRange, onClose }) {
const mapRef = useRef(null);
const [focusedStep, setFocusedStep] = useState(null);
const [routePath, setRoutePath] = useState([]);
const [routeLoading, setRouteLoading] = useState(false);
const { isLoaded } = useJsApiLoader({
googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY
});
// Step-pin coordinates in planning order — what the polyline connects.
const dropPath = useMemo(
() => (details || []).map((d) => ({ lat: d.dropLat, lng: d.dropLng })),
[details]
);
// Auto-fit map bounds to the full planned path once the map and data are
// both ready. Re-runs whenever the route changes (different rider / date).
useEffect(() => {
if (!isLoaded || !mapRef.current || dropPath.length === 0) return;
const bounds = new window.google.maps.LatLngBounds();
dropPath.forEach((p) => bounds.extend(p));
mapRef.current.fitBounds(bounds, 48);
}, [isLoaded, dropPath]);
// Resolve the rider's planned waypoints into an actual road-following path
// via the Directions API. Without this, the polyline would cut across
// buildings / aerial lines — operators have no way to read the real route.
// Directions has a 25-waypoint limit per request, so we chunk and stitch.
useEffect(() => {
if (!isLoaded || dropPath.length < 2) {
setRoutePath([]);
return;
}
let cancelled = false;
const ds = new window.google.maps.DirectionsService();
const MAX_WPS = 23; // origin + 23 waypoints + destination = 25 stops/chunk
const fetchSegment = (origin, destination, waypoints) =>
new Promise((resolve, reject) => {
ds.route(
{
origin,
destination,
waypoints: waypoints.map((p) => ({ location: p, stopover: true })),
travelMode: window.google.maps.TravelMode.DRIVING
},
(result, status) => {
if (status === 'OK') resolve(result);
else reject(new Error(status));
}
);
});
(async () => {
setRouteLoading(true);
try {
const points = dropPath;
const all = [];
let i = 0;
while (i < points.length - 1) {
const remaining = points.length - 1 - i;
const take = Math.min(remaining, MAX_WPS + 1);
const origin = points[i];
const destination = points[i + take];
const waypoints = points.slice(i + 1, i + take);
const res = await fetchSegment(origin, destination, waypoints);
const seg = res.routes[0].overview_path.map((ll) => ({
lat: ll.lat(),
lng: ll.lng()
}));
// Avoid duplicating the join point between adjacent chunks.
if (all.length > 0 && seg.length > 0) seg.shift();
all.push(...seg);
i += take;
}
if (!cancelled) setRoutePath(all);
} catch {
// Fall back to the straight-line skeleton on failure (quota, no route, etc.).
if (!cancelled) setRoutePath([]);
} finally {
if (!cancelled) setRouteLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [isLoaded, dropPath]);
// Numbered step icon as a data URL — drawn fresh per render so we can pass
// the step number into the SVG without juggling external assets. Color is
// brand purple to match the planned-route polyline below.
const stepIcon = (n, isFocused) => {
const size = isFocused ? 38 : 32;
const color = isFocused ? '#900E1D' : '#C01227';
const svg = encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="${size}" height="${size}">` +
`<circle cx="16" cy="16" r="14" fill="${color}" stroke="white" stroke-width="3"/>` +
`<text x="16" y="21" text-anchor="middle" font-family="Arial,sans-serif" font-size="14" font-weight="700" fill="white">${n}</text>` +
`</svg>`
);
return `data:image/svg+xml;charset=UTF-8,${svg}`;
};
const headerBar = (
<Stack
direction="row"
alignItems="center"
spacing={1.5}
sx={{
px: 2,
py: 1.25,
borderBottom: '1px solid rgba(15, 23, 42, 0.08)',
background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)',
color: '#fff',
flexShrink: 0
}}
>
<MdRoute size={20} />
<Stack sx={{ flex: 1, minWidth: 0 }}>
<Typography sx={{ fontWeight: 700, fontSize: 15, lineHeight: 1.2 }}>
Planned route{riderName ? `${riderName}` : ''}
</Typography>
{dateRange && (
<Typography sx={{ fontSize: 12, opacity: 0.85 }}>{dateRange}</Typography>
)}
</Stack>
{details && details.length > 0 && (
<Typography sx={{ fontSize: 12, opacity: 0.9, fontWeight: 600 }}>
{details.length} {details.length === 1 ? 'stop' : 'stops'}
{routeLoading ? ' · resolving route…' : ''}
</Typography>
)}
{onClose && (
<IconButton size="small" onClick={onClose} sx={{ color: '#fff' }} aria-label="Close">
<MdClose />
</IconButton>
)}
</Stack>
);
// Loading state — route fetch in flight OR Google Maps script not ready yet.
if (loading || !isLoaded) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{headerBar}
<Stack alignItems="center" justifyContent="center" sx={{ flex: 1, gap: 1.5 }}>
<CircularProgress size={32} />
<Typography sx={{ color: '#64748b', fontSize: 13 }}>
{loading ? 'Loading planned route…' : 'Loading map…'}
</Typography>
</Stack>
</Box>
);
}
// Empty state — fetched but rider has no deliveries with drop coords in the
// selected window.
if (!details || details.length === 0) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{headerBar}
<Stack alignItems="center" justifyContent="center" sx={{ flex: 1, gap: 1, p: 3 }}>
<Typography sx={{ color: '#1e293b', fontWeight: 700, fontSize: 16 }}>
No planned route for this rider
</Typography>
<Typography sx={{ color: '#64748b', fontSize: 13, textAlign: 'center', maxWidth: 360 }}>
There are no deliveries with drop coordinates assigned to this rider for the selected date range.
</Typography>
</Stack>
</Box>
);
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{headerBar}
<Box sx={{ flex: 1, minHeight: 0 }}>
<GoogleMap
mapContainerStyle={containerStyle}
onLoad={(map) => (mapRef.current = map)}
center={dropPath[0]}
zoom={14}
options={{
streetViewControl: false,
mapTypeControl: false,
fullscreenControl: false
}}
>
{routePath.length > 0 ? (
<>
{/* Translucent backdrop so the route stays legible on busy tiles. */}
<Polyline
path={routePath}
options={{ strokeColor: '#C01227', strokeOpacity: 0.25, strokeWeight: 8 }}
/>
{/* Road-following planned route from the Directions API. */}
<Polyline
path={routePath}
options={{ strokeColor: '#C01227', strokeOpacity: 0.95, strokeWeight: 4 }}
/>
</>
) : (
// Fallback while Directions is in flight (or if it fails) — dashed
// straight-line skeleton between drop pins in step order.
<Polyline
path={dropPath}
options={{
strokeColor: '#C01227',
strokeOpacity: 0,
strokeWeight: 0,
icons: [
{
icon: {
path: 'M 0,-1 0,1',
strokeOpacity: 0.6,
strokeColor: '#C01227',
scale: 3
},
offset: '0',
repeat: '14px'
}
]
}}
/>
)}
{details.map((d, i) => {
const stepNum = d.step || i + 1;
const isFocused = focusedStep === d.deliveryid;
return (
<Marker
key={`step-${d.deliveryid || d.orderid || i}`}
position={{ lat: d.dropLat, lng: d.dropLng }}
icon={{ url: stepIcon(stepNum, isFocused) }}
onClick={() => setFocusedStep(isFocused ? null : d.deliveryid)}
zIndex={isFocused ? 1000 : stepNum}
>
{isFocused && (
<InfoWindow onCloseClick={() => setFocusedStep(null)}>
<Box sx={{ minWidth: 180, fontFamily: 'inherit' }}>
<Typography sx={{ fontWeight: 800, fontSize: 13, color: '#0f172a' }}>
Step {stepNum} · {d.customer}
</Typography>
{d.address && (
<Typography sx={{ fontSize: 12, color: '#475569', mt: 0.5 }}>
{d.address}
</Typography>
)}
{d.expectedTime && (
<Typography sx={{ fontSize: 12, color: '#64748b', mt: 0.5 }}>
ETA {String(d.expectedTime).slice(11, 16) || d.expectedTime}
</Typography>
)}
{d.orderid && (
<Typography sx={{ fontSize: 11, color: '#94a3b8', mt: 0.5 }}>
Order #{d.orderid}
</Typography>
)}
</Box>
</InfoWindow>
)}
</Marker>
);
})}
</GoogleMap>
</Box>
</Box>
);
}

View File

@@ -1,204 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { MapContainer, TileLayer, Marker, Polyline, Tooltip } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import dayjs from 'dayjs';
import { Chip, Stack, Typography, Box } from '@mui/material';
import { CloseCircleOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
import CircularLoader from 'components/CircularLoader';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
// Start marker
const startIcon = new L.Icon({
iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
shadowSize: [41, 41]
});
// End marker
const endIcon = new L.Icon({
iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
shadowSize: [41, 41]
});
const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
console.log('additionalProps', additionalProps);
const mapRef = useRef(null);
const theme = useTheme();
const [routePoints, setRoutePoints] = useState([]);
const [loading, setLoading] = useState(false);
// Fit the map to bounds
useEffect(() => {
if (mapRef.current && coordinates.length > 0) {
const bounds = [
[Math.min(...coordinates.map((c) => c.lat)), Math.min(...coordinates.map((c) => c.lng))],
[Math.max(...coordinates.map((c) => c.lat)), Math.max(...coordinates.map((c) => c.lng))]
];
mapRef.current.fitBounds(bounds);
}
}, [coordinates]);
// Fetch OSRM Route → REAL ROAD ROUTE
useEffect(() => {
const getOSRMRoute = async () => {
setLoading(true);
// FIX: If only one coordinate, stop loader and exit
if (coordinates.length < 2) {
setRoutePoints([]); // no route
setLoading(false);
return;
}
const subsample = (arr, max) => {
if (arr.length <= max) return arr;
const step = Math.ceil(arr.length / max);
const out = arr.filter((_, i) => i % step === 0);
const last = arr[arr.length - 1];
if (out[out.length - 1] !== last) out.push(last);
return out;
};
// Attempt 1 — map-matching (best fidelity for dense traces).
try {
const ptsM = subsample(coordinates, 90);
const coordsM = ptsM.map((p) => `${p.lng},${p.lat}`).join(';');
const matchUrl = `https://router.project-osrm.org/match/v1/driving/${coordsM}?overview=full&geometries=geojson&gaps=ignore&tidy=true`;
const resM = await fetch(matchUrl);
const jsonM = await resM.json();
if (jsonM.matchings && jsonM.matchings.length > 0) {
const poly = jsonM.matchings.flatMap((m) =>
(m.geometry?.coordinates || []).map(([lng, lat]) => ({ lat, lng }))
);
if (poly.length >= 2) {
setRoutePoints(poly);
setLoading(false);
return;
}
}
} catch (e) {
console.warn('OSRM Match error, trying route fallback:', e);
}
// Attempt 2 — waypoint routing through a coarser subsample.
try {
const ptsR = subsample(coordinates, 25);
const coordsR = ptsR.map((p) => `${p.lng},${p.lat}`).join(';');
const routeUrl = `https://router.project-osrm.org/route/v1/driving/${coordsR}?overview=full&geometries=geojson`;
const resR = await fetch(routeUrl);
const jsonR = await resR.json();
if (jsonR.routes && jsonR.routes[0]) {
const poly = jsonR.routes[0].geometry.coordinates.map(([lng, lat]) => ({ lat, lng }));
setRoutePoints(poly);
} else {
// Fallback to drawing direct lines between coordinates
setRoutePoints(coordinates);
}
} catch (err) {
console.error('OSRM Route fallback error:', err);
setRoutePoints(coordinates);
} finally {
setLoading(false);
}
};
getOSRMRoute();
}, [coordinates]);
if (!coordinates || coordinates.length === 0) return null;
const start = coordinates[0];
const end = coordinates[coordinates.length - 1];
const center = coordinates[Math.floor(coordinates.length / 2)];
const InfoItem = ({ label, value }) => (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{label}:
</Typography>
<Chip label={value || '0.00'} color="primary" sx={{ fontWeight: 700 }} />
</Stack>
);
return (
<Box sx={{ width: '100%', height: '100vh', position: 'relative', overflow: 'hidden' }}>
{loading && <CircularLoader />}
{/* CLOSE BUTTON */}
<Chip
label="Close"
icon={<CloseCircleOutlined style={{ fontSize: 18 }} />}
onClick={() => setMapOpen(false)}
sx={{
position: 'absolute',
top: 12,
right: 12,
zIndex: 2000,
bgcolor: theme.palette.error.main,
color: '#fff',
fontWeight: 600,
borderRadius: '12px',
px: 1.5,
py: 0.5,
boxShadow: theme.shadows[4],
cursor: 'pointer',
'& .MuiChip-icon': { color: '#fff' }
}}
/>
{/* MAP */}
<MapContainer center={center} zoom={14} scrollWheelZoom style={{ height: '100%', width: '100%' }} ref={mapRef}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{/* START MARKER */}
<Marker position={start} icon={startIcon}>
<Tooltip direction="bottom">{`Pickup: ${dayjs(additionalProps.riderStart).format('DD-MM-YYYY hh:mm A')}`}</Tooltip>
</Marker>
{/* END MARKER */}
<Marker position={end} icon={endIcon}>
<Tooltip direction="bottom">{`Drop: ${dayjs(additionalProps.riderEnd).format('DD-MM-YYYY hh:mm A')}`}</Tooltip>
</Marker>
{/* REAL OSRM ROUTE */}
{routePoints.length > 0 && <Polyline positions={routePoints} pathOptions={{ color: 'blue', weight: 5 }} />}
</MapContainer>
{/* BOTTOM DETAILS */}
<Box
sx={{
position: 'absolute',
bottom: 0,
width: '100%',
bgcolor: 'rgba(255,255,255,0.96)',
p: 2,
boxShadow: theme.shadows[3],
zIndex: 1500
}}
>
<Stack direction="row" flexWrap="wrap" rowGap={1.5} columnGap={3} alignItems="center">
<InfoItem label="Tenant" value={order?.tenantname} />
<InfoItem label="Rider" value={order?.ridername} />
<InfoItem label="Pickup" value={order?.pickupcustomer} />
<InfoItem label="Drop" value={order?.deliverycustomer} />
<InfoItem label="Kms" value={order?.kms} />
<InfoItem label="Actual Kms" value={order?.actualkms} />
<InfoItem label="Rider Kms" value={order?.riderkms} />
</Stack>
</Box>
</Box>
);
};
export default MapWithRoute;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,671 +0,0 @@
import React, { useState } from 'react';
import {
Paper,
Stack,
Typography,
Table,
TableCell,
TableBody,
TableHead,
TableRow,
TableContainer,
Avatar,
Box,
ToggleButtonGroup,
ToggleButton,
Autocomplete,
TextField,
useMediaQuery,
Button
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
MdCheckCircle,
MdCancel,
MdAccessTime,
MdInventory2,
MdTwoWheeler,
MdArrowForward
} from 'react-icons/md';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import dayjs from 'dayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import axios from 'axios';
import { OpenToast } from 'components/third-party/OpenToast';
const STATUS_META = {
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel },
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
};
export default function RiderSubstitution({
appId,
rows,
allRidersList,
substituteRidersList,
substituteAssignments,
handleAssignSubstitute,
onFinalizeSuccess,
selectedDate,
setSelectedDate,
DT,
BRAND,
edge,
tint,
soft
}) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [selectedBatch, setSelectedBatch] = useState('all');
const [isFinalizing, setIsFinalizing] = useState(false);
const getBatchForTime = React.useCallback((timeStr) => {
if (!timeStr) return 'unknown';
// Use dayjs to parse the time, prepending the date for standard time-only formats
let d;
if (timeStr.includes('-') || timeStr.includes('/')) {
d = dayjs(timeStr);
} else {
d = dayjs(`${dayjs().format('MM-DD-YYYY')} ${timeStr}`);
}
if (!d.isValid()) {
// Fallback to simple split if dayjs fails
const parts = timeStr.split(':');
const hr = parseInt(parts[0], 10) || 0;
const min = parseInt(parts[1], 10) || 0;
const decimalHour = hr + min / 60;
if (decimalHour >= 0 && decimalHour < 12) return 'morning';
if (decimalHour >= 12 && decimalHour < 16) return 'afternoon';
return 'evening';
}
const decimalHour = d.hour() + d.minute() / 60;
if (decimalHour >= 0 && decimalHour < 8.5) {
return 'morning';
} else if (decimalHour >= 8.5 && decimalHour < 13.5) {
return 'afternoon';
} else {
return 'evening';
}
}, []);
const isBatchDisabled = React.useCallback((batch) => {
if (!selectedDate) return false;
const isToday = selectedDate.isSame(dayjs(), 'day');
const isPast = selectedDate.isBefore(dayjs(), 'day');
if (isPast) return true;
if (!isToday) return false;
const now = dayjs();
const currentHour = now.hour();
const currentMinute = now.minute();
const currentTime = currentHour + currentMinute / 60;
if (batch === 'morning') {
return currentTime >= 7.0; // After 7:00 AM
}
if (batch === 'afternoon') {
return currentTime >= 9.0; // After 9:00 AM
}
if (batch === 'evening') {
return currentTime >= 16.0; // After 4:00 PM
}
return false;
}, [selectedDate]);
const filteredRows = React.useMemo(() => {
return rows;
}, [rows]);
const hasAssignments = Object.values(substituteAssignments || {}).some(
(val) => val !== null && val !== undefined
);
const handleFinalize = async () => {
setIsFinalizing(true);
try {
let partnerId = '';
try {
const rawLocs = localStorage.getItem('applocations');
if (rawLocs) {
const locs = JSON.parse(rawLocs);
const currentLoc = locs.find((l) => l.applocationid === appId);
if (currentLoc && currentLoc.partnerid) {
partnerId = currentLoc.partnerid;
} else {
const firstValidLoc = locs.find((l) => l.partnerid);
if (firstValidLoc && firstValidLoc.partnerid) {
partnerId = firstValidLoc.partnerid;
}
}
}
} catch (e) {
console.error('Error parsing applocations', e);
}
if (!partnerId) {
const savedPartnerId = localStorage.getItem('partnerid');
if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') {
partnerId = savedPartnerId;
}
}
if (!partnerId || partnerId === '0' || partnerId === 0) {
partnerId = 44;
}
const tenantId = parseInt(partnerId, 10);
const substitutions = Object.entries(substituteAssignments || {})
.filter((entry) => entry[1] !== null && entry[1] !== undefined)
.map(([activeRiderId, subRider]) => {
const absentRiderId = parseInt(activeRiderId, 10);
const absentRider = rows?.find((r) => r.userid === absentRiderId);
return {
sub_date: selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'),
absent_rider_id: absentRiderId,
absent_rider_name: absentRider?.username || absentRider?.fullname || `Rider #${absentRiderId}`,
sub_rider_id: parseInt(subRider.userid, 10),
sub_rider_name: subRider.username || subRider.fullname || `Rider #${subRider.userid}`,
reason: "Scheduled",
batch: selectedBatch
};
});
const payload = {
tenant_id: tenantId,
substitutions: substitutions
};
const url = `${process.env.REACT_APP_URL}/substitutions`;
const response = await axios.post(url, payload);
if (response.data && response.data.status) {
OpenToast('Substitutions finalized successfully!', 'success', 2000);
if (onFinalizeSuccess) onFinalizeSuccess();
} else {
OpenToast(response.data?.message || 'Substitutions saved successfully!', 'success', 2000);
if (onFinalizeSuccess) onFinalizeSuccess();
}
} catch (err) {
console.error('Finalize error:', err);
// Fallback/simulation
OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000);
if (onFinalizeSuccess) onFinalizeSuccess();
} finally {
setIsFinalizing(false);
}
};
const getRowStatusMeta = (row) => {
const key = (row?.status || '').toLowerCase() === 'active' ? 'active' : 'inactive';
return STATUS_META[key] || STATUS_META.unknown;
};
const totalCols = 6;
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>
);
return (
<>
{/* Filter Row inside its own Paper */}
<Paper
elevation={0}
sx={{
mb: 2,
p: 2,
borderRadius: `${DT.radiusCard / 8}px`,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff'
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems="center"
justifyContent="space-between"
gap={2}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ flexWrap: 'wrap' }}>
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textSecondary, mr: 1 }}>
Shift Batch:
</Typography>
<ToggleButtonGroup
value={selectedBatch}
exclusive
onChange={(e, val) => val && setSelectedBatch(val)}
aria-label="batch selection"
size="small"
sx={{
'& .MuiToggleButton-root': {
borderRadius: '8px',
mx: 0.5,
border: `1px solid ${DT.borderSubtle}`,
color: DT.textSecondary,
fontWeight: 600,
textTransform: 'capitalize',
px: 2,
py: 0.5,
'&.Mui-selected': {
bgcolor: BRAND,
color: '#fff',
'&:hover': {
bgcolor: '#900E1D'
}
}
}
}}
>
<ToggleButton value="all">All Batches</ToggleButton>
<ToggleButton value="morning">Morning</ToggleButton>
<ToggleButton value="afternoon">Afternoon</ToggleButton>
<ToggleButton value="evening">Evening</ToggleButton>
</ToggleButtonGroup>
</Stack>
<Box>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Select Date"
value={selectedDate}
onChange={(newValue) => newValue && setSelectedDate(newValue)}
slotProps={{
textField: {
size: 'small',
sx: {
width: 180,
'& .MuiOutlinedInput-root': {
borderRadius: '20px',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: BRAND },
'&.Mui-focused fieldset': { borderColor: BRAND }
}
}
}
}}
/>
</LocalizationProvider>
</Box>
</Stack>
</Paper>
{/* Results Table/Cards */}
<Paper
elevation={0}
sx={{
borderRadius: `${DT.radiusCard / 8}px`,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
<TableContainer
sx={{
maxHeight: 'calc(100vh - 240px)',
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
{isMobile ? (
<MobileCardList sx={{ p: 1.25 }}>
{filteredRows?.length === 0 && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdTwoWheeler size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No active riders to show
</Typography>
</Stack>
)}
{filteredRows?.length !== 0 &&
filteredRows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
const riderProfile = allRidersList?.find((r) => r.userid === row.userid);
const starttime = riderProfile?.starttime || row.starttime;
return (
<MobileCard
key={row.userid ?? index}
accent={statusMeta.color}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 11
}}
>
#{row?.userid}
</Box>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
pl: 0.5,
pr: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(statusMeta.color),
border: `1px solid ${edge(statusMeta.color)}`,
color: statusMeta.color
}}
>
<AccentAvatar color={statusMeta.color} size={18}>
<StatusIcon size={11} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
{statusMeta.label}
</Typography>
</Stack>
</Stack>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: soft(BRAND),
color: BRAND,
fontWeight: 800,
fontSize: 16,
border: `1px solid ${edge(BRAND)}`,
flexShrink: 0
}}
>
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.username || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.contactno || '—'}
</Typography>
</Box>
</Stack>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Substitute" full>
<Autocomplete
size="small"
disabled={isBatchDisabled(getBatchForTime(starttime))}
options={substituteRidersList || []}
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
value={substituteAssignments[row.userid] || null}
onChange={(event, newValue) => handleAssignSubstitute(row.userid, newValue)}
renderInput={(params) => (
<TextField
{...params}
placeholder="Assign Substitute"
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
bgcolor: '#f8fafc',
'& fieldset': { borderColor: DT.borderSubtle }
}
}}
/>
)}
/>
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: 1200 }}>
<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>ID</TableCell>
<TableCell>Rider</TableCell>
<TableCell align="center"></TableCell>
<TableCell>Substitute Rider</TableCell>
<TableCell align="center">Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRows?.length === 0 && (
<TableRow>
<TableCell colSpan={totalCols} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdTwoWheeler size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No active riders to show
</Typography>
</Stack>
</TableCell>
</TableRow>
)}
{filteredRows?.length !== 0 &&
filteredRows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
const riderProfile = allRidersList?.find((r) => r.userid === row.userid);
const starttime = riderProfile?.starttime || row.starttime;
return (
<TableRow
key={row.userid ?? 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>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 11,
minWidth: 56
}}
>
#{row?.userid}
</Box>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: soft(BRAND),
color: BRAND,
fontWeight: 800,
fontSize: 16,
border: `1px solid ${edge(BRAND)}`
}}
>
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
</Avatar>
<Stack sx={{ minWidth: 0 }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.username || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.contactno || '—'}
</Typography>
</Stack>
</Stack>
</TableCell>
{/* Substitution Arrow & Autocomplete */}
<TableCell align="center">
<MdArrowForward size={18} style={{ color: DT.textSecondary }} />
</TableCell>
<TableCell sx={{ minWidth: 220 }}>
<Autocomplete
size="small"
disabled={isBatchDisabled(getBatchForTime(starttime))}
options={substituteRidersList || []}
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
value={substituteAssignments[row.userid] || null}
onChange={(event, newValue) => handleAssignSubstitute(row.userid, newValue)}
renderInput={(params) => (
<TextField
{...params}
placeholder="Assign Substitute"
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
bgcolor: '#f8fafc',
'& fieldset': { borderColor: DT.borderSubtle }
}
}}
/>
)}
/>
</TableCell>
<TableCell align="center">
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
pl: 0.5,
pr: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(statusMeta.color),
border: `1px solid ${edge(statusMeta.color)}`,
color: statusMeta.color
}}
>
<AccentAvatar color={statusMeta.color} size={20}>
<StatusIcon size={12} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 11, lineHeight: 1 }}>
{statusMeta.label}
</Typography>
</Stack>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</TableContainer>
{hasAssignments && (
<Box
sx={{
p: 2,
borderTop: `1px solid ${DT.borderSubtle}`,
display: 'flex',
justifyContent: 'flex-end',
bgcolor: DT.surfaceAlt
}}
>
<Button
variant="contained"
disabled={isFinalizing}
onClick={handleFinalize}
sx={{
bgcolor: BRAND,
color: '#fff',
borderRadius: '8px',
px: 4,
py: 1,
fontWeight: 700,
textTransform: 'none',
'&:hover': {
bgcolor: '#900E1D'
}
}}
>
{isFinalizing ? 'Saving...' : 'Finalize Substitutions'}
</Button>
</Box>
)}
</Paper>
</>
);
}

View File

@@ -1,256 +1,78 @@
import { useEffect, useState } from 'react';
// material-ui
import { Box, Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
// third-party
// import { PatternFormat } from 'react-number-format';
// project import
import MainCard from 'components/MainCard';
import axios from 'axios';
// assets
import { usePlacesWidget } from 'react-google-autocomplete';
import Loader from 'components/Loader';
import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
// import { setLocationType } from 'react-geocode';
// const avatarImage = require.context('assets/images/users', true);
// styles & constant
// const ITEM_HEIGHT = 48;
// const ITEM_PADDING_TOP = 8;
// const MenuProps = {
// PaperProps: {
// style: {
// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP
// }
// }
// };
import { fetchAppLocations } from 'pages/api/api';
const Createrider = () => {
// const [role, setRole] = useState('');
const [mobilenumber, setMobilenumber] = useState('');
const [emailaddress, setEmailaddress] = useState('');
const [city, setCity] = useState('');
const [zipcode, setZipcode] = useState('');
const [address, setAddress] = useState('');
const [state, setState] = useState('');
const [suburb, setSuburb] = useState('');
const [latlong, setLatlong] = useState({});
const [firstname, setFirstname] = useState('');
const [doorno, setDoorno] = useState('');
const [landmark, setLandmark] = useState('');
const [tenantinfo, setTenantinfo] = useState({});
const [displayname, setDisplayname] = useState('');
const [phone, setPhone] = useState('');
const [hubid, setHubid] = useState('');
const [hubs, setHubs] = useState([]);
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
// Geocode.setApiKey('AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8');
const [loading, setLoading] = useState(false);
useEffect(() => {
// fetchprofiledetails(localStorage.getItem('appuserid'));
// fetchprofiledetails(181);
if (localStorage.getItem('tenantid')) {
fetchtenantinfo(localStorage.getItem('tenantid'));
}
fetchAppLocations().then((locations) => {
setHubs((locations || []).filter((l) => l.applocationid !== 0));
});
}, []);
useEffect(() => {
try {
Geocode.fromAddress(address).then(
(response) => {
if (response.status == 'OK') {
const { lat, lng } = response.results[0].geometry.location;
setLatlong({
lat,
lng
});
console.log(response);
}
},
(error) => {
console.log(error);
}
);
} catch (err) {
console.log(err);
}
}, [address]);
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
// console.log(alertmessage)
};
const createMiler = async () => {
if (!displayname) {
opentoast('Fill miler name');
return;
}
if (!phone) {
opentoast('Fill phone number');
return;
}
if (!hubid) {
opentoast('Choose a hub');
return;
}
const fetchtenantinfo = async (tid) => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => {
console.log(res);
if (res.data.status) {
setTenantinfo(res.data.details);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
try {
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/milers`, {
displayname,
phone,
hubid,
availabilitystatus: 'Available'
});
};
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => {
console.log(place);
setAddress(place.formatted_address);
let city1, zipcode1, state1, suburb1;
for (let i = 0; i < place.address_components.length; i++) {
for (let j = 0; j < place.address_components[i].types.length; j++) {
switch (place.address_components[i].types[j]) {
case 'locality':
city1 = place.address_components[i].long_name;
break;
case 'administrative_area_level_1':
state1 = place.address_components[i].long_name;
break;
case 'postal_code':
zipcode1 = place.address_components[i].long_name;
break;
case 'sublocality':
suburb1 = place.address_components[i].long_name;
break;
}
}
}
setCity(city1 || '');
setState(state1 || '');
setZipcode(zipcode1 || '');
setSuburb(suburb1 || '');
// setAddress(place.formatted_address)
},
// inputAutocompleteValue: "country",
options: {
// componentRestrictions: 'us',
// types: ["establishment"]
types: ['address' || 'geocode']
}
});
const createprofile = async () => {
console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode);
// if (!businessname) {
// opentoast('Fill Business name')
// } else if (!businessno) {
// opentoast('Fill Registration No')
// }
// else
if (!firstname) {
opentoast('Fill Full name');
} else if (!mobilenumber) {
opentoast('Fill Mobile Number');
} else if (!emailaddress) {
opentoast('Fill emailaddress');
} else if (!address) {
opentoast('Fill Address');
} else if (!city) {
opentoast('Fill City');
} else if (!zipcode) {
opentoast('Fill post code');
} else if (!suburb) {
opentoast('Fill suburb');
} else if (!latlong.lat || !latlong.lng) {
opentoast('Choose valid address');
} else {
let obj = {
customerid: 0,
configid: 1,
firstname: firstname,
applocationid: tenantinfo.applolcationid,
profileimage: '',
dialcode: '+91',
contactno: mobilenumber,
devicetype: '',
deviceid: '',
customertoken: '',
address: address,
suburb: suburb,
city: city,
state: state,
postcode: zipcode,
landmark: landmark,
doorno: doorno,
latitude: latlong.lat.toString(),
longitude: latlong.lng.toString(),
tenantid: parseInt(localStorage.getItem('tenantid')),
email: emailaddress
};
console.log(obj);
setLoading(true);
try {
await axios
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
.then((res) => {
console.log(res);
if (res.data.status) {
enqueueSnackbar(' Created Successfully ', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
navigate('/clients');
// setTimeout(()=>{
// fetchprofiledetails(localStorage.getItem('appuserid'));
// },2000)
} else if (res.data.message == 'Customer Already available') {
enqueueSnackbar('Customer Already available', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
});
} catch (err) {
console.log(err);
setLoading(false);
if (res.data?.success) {
enqueueSnackbar('Miler created successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
navigate('/nearle/riders');
} else {
opentoast(res.data?.message || 'Failed to create miler.');
}
} catch (err) {
opentoast(err.response?.data?.message || err.message || 'Failed to create miler.');
} finally {
setLoading(false);
}
};
@@ -259,205 +81,87 @@ const Createrider = () => {
{loading && <Loader />}
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', sm: 'center' }}
spacing={1}
>
<Typography variant="h3">Create Rider</Typography>
</Stack>
</Grid>
<MainCard>
<Grid container spacing={3}>
<Grid item xs={12}>
<MainCard
// title="Contact Information"
sx={{ height: '100%' }}
>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-last-name">Admin Name</InputLabel>
<TextField
fullWidth
id="personal-last-name"
placeholder="Name"
onChange={(e) => setFirstname(e.target.value)}
value={firstname}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}></Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
<MenuItem value="+1">+91</MenuItem>
</Select>
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={1}>
<Typography variant="h3">Create Miler</Typography>
</Stack>
</Grid>
<MainCard>
<Grid container spacing={3}>
<Grid item xs={12}>
<MainCard sx={{ height: '100%' }}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="miler-name">Name</InputLabel>
<TextField
type="number"
id="personal-phone"
// format="##########"
// mask="_"
fullWidth
// customInput={TextField}
placeholder="Phone Number"
// defaultValue="8654239581"
// onBlur={() => { }}
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setMobilenumber(e.target.value);
}
}}
value={mobilenumber}
id="miler-name"
placeholder="Name"
onChange={(e) => setDisplayname(e.target.value)}
value={displayname}
autoComplete="off"
// disabled
sx={{ cursor: 'not-allowed' }}
/>
</Stack>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="miler-phone">Phone Number</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Select defaultValue="+91" disabled sx={{ cursor: 'not-allowed' }}>
<MenuItem value="+91">+91</MenuItem>
</Select>
<TextField
type="tel"
id="miler-phone"
fullWidth
placeholder="Phone Number"
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setPhone(e.target.value);
}
}}
value={phone}
autoComplete="off"
/>
</Stack>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="miler-hub">Hub</InputLabel>
<Select
id="miler-hub"
fullWidth
displayEmpty
value={hubid}
onChange={(e) => setHubid(e.target.value)}
renderValue={(selected) => {
if (!selected) return <em>Select hub</em>;
const hub = hubs.find((h) => h.applocationid === selected);
return hub?.locationname || selected;
}}
>
{hubs.map((h) => (
<MenuItem key={h.applocationid} value={h.applocationid}>
{h.locationname}
</MenuItem>
))}
</Select>
</Stack>
</Grid>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
placeholder="Email Address"
onChange={(e) => setEmailaddress(e.target.value)}
value={emailaddress}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-address">Address</InputLabel>
<TextField
fullWidth
// defaultValue="Street 110-B Kalians Bag, Dewan, M.P. New York"
id="personal-address"
placeholder="Address"
value={address}
onChange={(e) => setAddress(e.target.value)}
inputRef={materialRef}
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Suburb</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Location"
onChange={(e) => setSuburb(e.target.value)}
value={suburb}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
<TextField
fullWidth
// defaultValue="956754"
// type='number'
id="personal-zipcode"
placeholder="City"
onChange={(e) => setCity(e.target.value)}
value={city}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">State</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="State"
onChange={(e) => setState(e.target.value)}
value={state}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
<TextField
fullWidth
// defaultValue="956754"
type="number"
id="personal-zipcode"
placeholder="Zipcode"
onChange={(e) => setZipcode(e.target.value)}
value={zipcode}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Door No</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Door No"
onChange={(e) => setDoorno(e.target.value)}
value={doorno}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
placeholder="Landmark"
onChange={(e) => setLandmark(e.target.value)}
value={landmark}
autoComplete="off"
/>
</Stack>
</Grid>
</Grid>
</MainCard>
</MainCard>
</Grid>
<Grid item xs={12}>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="flex-end" alignItems={{ xs: 'stretch', sm: 'center' }} spacing={2}>
<Button variant="contained" onClick={() => createMiler()} fullWidth={isMobile}>
Create
</Button>
</Stack>
</Grid>
</Grid>
<Grid item xs={12}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button variant="contained" onClick={() => createprofile()} fullWidth={isMobile}>
Create
</Button>
</Stack>
</Grid>
</Grid>
</MainCard>
</MainCard>
</Box>
</>
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,17 +1,10 @@
import { lazy } from 'react';
// project import
// import GuestGuard from 'utils/route-guard/GuestGuard';
import CommonLayout from 'layout/CommonLayout';
import Loadable from 'components/Loadable';
// render - login
// const AuthLogin = Loadable(lazy(() => import('pages/auth/login')));
// const AuthRegister = Loadable(lazy(() => import('pages/auth/register')));
// const AuthForgotPassword = Loadable(lazy(() => import('pages/auth/forgot-password')));
// const AuthCheckMail = Loadable(lazy(() => import('pages/auth/check-mail')));
// const AuthResetPassword = Loadable(lazy(() => import('pages/auth/reset-password')));
// const AuthCodeVerification = Loadable(lazy(() => import('pages/auth/code-verification')));
const Login = Loadable(lazy(() => import('pages/nearle/login')));
// ==============================|| AUTH ROUTING ||============================== //
@@ -21,11 +14,7 @@ const LoginRoutes = {
children: [
{
path: '/',
element: (
// <GuestGuard>
<CommonLayout />
// </GuestGuard>
),
element: <CommonLayout />,
children: [
{
path: '/',
@@ -35,35 +24,6 @@ const LoginRoutes = {
path: 'login',
element: <Login />
}
// {
// path: '/',
// element: <AuthLogin />
// },
// {
// path: 'login',
// element: <AuthLogin />
// },
// {
// path: 'register',
// element: <AuthRegister />
// },
// {
// path: 'forgot-password',
// element: <AuthForgotPassword />
// },
// {
// path: 'check-mail',
// element: <AuthCheckMail />
// },
// {
// path: 'reset-password',
// element: <AuthResetPassword />
// },
// {
// path: 'code-verification',
// element: <AuthCodeVerification />
// }
]
}
]

View File

@@ -4,7 +4,6 @@ import { lazy } from 'react';
import MainLayout from 'layout/MainLayout';
import CommonLayout from 'layout/CommonLayout';
import Loadable from 'components/Loadable';
// import AuthGuard from 'utils/route-guard/AuthGuard';
// pages routing
const MaintenanceError = Loadable(lazy(() => import('pages/maintenance/404')));
@@ -12,10 +11,7 @@ const MaintenanceError500 = Loadable(lazy(() => import('pages/maintenance/500'))
const MaintenanceUnderConstruction = Loadable(lazy(() => import('pages/maintenance/under-construction')));
const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/coming-soon')));
// render - sample page
// const SamplePage = Loadable(lazy(() => import('pages/extra-pages/sample-page')));
const Login = Loadable(lazy(() => import('pages/nearle/login1')));
// const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard')));
const Tenants = Loadable(lazy(() => import('pages/nearle/clients/Tenants')));
const ClientsPricing = Loadable(lazy(() => import('pages/nearle/clientPricing/clientPricing')));
@@ -26,9 +22,6 @@ const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPrev
const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliveries')));
const Customers = Loadable(lazy(() => import('pages/nearle/customers/customers')));
const Invoice = Loadable(lazy(() => import('pages/nearle/invoice/invoice')));
const InvoicePreview = Loadable(lazy(() => import('../pages/nearle/invoice/invoicePreview')));
const Details = Loadable(lazy(() => import('pages/nearle/orders/details')));
const ViewProfile = Loadable(lazy(() => import('pages/nearle/viewProfile')));
@@ -40,11 +33,6 @@ const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclie
const CreateCustomer = Loadable(lazy(() => import('pages/nearle/clients/createCustomer')));
const Requests = Loadable(lazy(() => import('pages/nearle/requests/requests')));
const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary')));
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
const RidersLogs = Loadable(lazy(() => import('pages/nearle/reports/ridersLogs')));
const Profitability = Loadable(lazy(() => import('pages/nearle/reports/profitability')));
const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders')));
const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider')));
@@ -52,6 +40,10 @@ const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider')));
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
const Hubs = Loadable(lazy(() => import('pages/nearle/hubs/Hubs')));
const BookingDetail = Loadable(lazy(() => import('pages/nearle/bookings/BookingDetail')));
const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard/Dashboard')));
// ==============================|| MAIN ROUTING ||============================== //
@@ -60,11 +52,7 @@ const MainRoutes = {
children: [
{
path: '/',
element: (
// <AuthGuard>
<MainLayout />
// </AuthGuard>
),
element: <MainLayout />,
children: [
{
path: 'nearle',
@@ -93,24 +81,6 @@ const MainRoutes = {
path: 'customers',
element: <Customers />
},
{
path: 'invoice',
children: [
{
index: true,
element: <Invoice />
},
{
path: 'preview',
element: <InvoicePreview />
}
]
},
{
path: 'invoice/preview',
element: <InvoicePreview />
},
{
path: 'requests',
element: <Requests />
@@ -148,31 +118,6 @@ const MainRoutes = {
path: 'customer/create',
element: <CreateCustomer />
},
{
path: 'reports',
children: [
{
path: 'orderssummary',
element: <OrdersSummary />
},
{
path: 'ordersdetails',
element: <OrdersDetails />
},
{
path: 'riderssummary',
element: <RidersSummary />
},
{
path: 'riderslogs',
element: <RidersLogs />
},
{
path: 'profitability',
element: <Profitability />
}
]
},
{
path: 'dispatch',
element: <Dispatch />
@@ -180,6 +125,18 @@ const MainRoutes = {
{
path: 'dispatch/preview',
element: <DispatchPreview />
},
{
path: 'hubs',
element: <Hubs />
},
{
path: 'bookings/:id',
element: <BookingDetail />
},
{
path: 'dashboard',
element: <Dashboard />
}
]
},
@@ -188,11 +145,6 @@ const MainRoutes = {
path: 'viewprofile',
element: <ViewProfile />
}
// {
// path: 'orders/create',
// element: <Createorder />
// },
]
},

View File

@@ -1,275 +0,0 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import {
Box,
Chip,
Drawer,
Grid,
InputAdornment,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
OutlinedInput,
Stack,
Typography,
useMediaQuery
} from '@mui/material';
// project imports
import UserAvatar from './UserAvatar';
// import UserList from './UserList';
import MainCard from 'components/MainCard';
import IconButton from 'components/@extended/IconButton';
import SimpleBar from 'components/third-party/SimpleBar';
import { ThemeMode } from 'config';
import useAuth from 'hooks/useAuth';
// assets
import {
CheckCircleFilled,
ClockCircleFilled,
LogoutOutlined,
MinusCircleFilled,
RightOutlined,
SearchOutlined,
SettingOutlined
} from '@ant-design/icons';
// ==============================|| CHAT DRAWER ||============================== //
function ChatDrawer({ handleDrawerOpen, openChatDrawer, setUser }) {
const theme = useTheme();
const { user } = useAuth();
const matchDownLG = useMediaQuery(theme.breakpoints.down('lg'));
const drawerBG = theme.palette.mode === ThemeMode.DARK ? 'dark.main' : 'white';
// show menu to set current user status
const [anchorEl, setAnchorEl] = useState();
const handleClickRightMenu = (event) => {
setAnchorEl(event?.currentTarget);
};
const handleCloseRightMenu = () => {
setAnchorEl(null);
};
// set user status on status menu click
const [status, setStatus] = useState('available');
const handleRightMenuItemClick = (userStatus) => () => {
setStatus(userStatus);
handleCloseRightMenu();
};
const [search, setSearch] = useState('');
const handleSearch = async (event) => {
const newString = event?.target.value;
setSearch(newString);
};
return (
<Drawer
sx={{
width: 320,
flexShrink: 0,
zIndex: { xs: 1100, lg: 0 },
'& .MuiDrawer-paper': {
height: matchDownLG ? '100%' : 'auto',
width: 320,
boxSizing: 'border-box',
position: 'relative',
border: 'none'
}
}}
variant={matchDownLG ? 'temporary' : 'persistent'}
anchor="left"
open={openChatDrawer}
ModalProps={{ keepMounted: true }}
onClose={handleDrawerOpen}
>
<MainCard
sx={{
bgcolor: matchDownLG ? 'transparent' : drawerBG,
borderRadius: '4px 0 0 4px',
borderRight: 'none'
}}
border={!matchDownLG}
content={false}
>
<Box sx={{ p: 3, pb: 1 }}>
<Stack spacing={2}>
<Stack direction="row" spacing={0.5} alignItems="center">
<Typography variant="h5" color="inherit">
Messages
</Typography>
<Chip
label="9"
component="span"
color="secondary"
sx={{
width: 20,
height: 20,
borderRadius: '50%',
'& .MuiChip-label': {
px: 0.5
}
}}
/>
</Stack>
<OutlinedInput
fullWidth
id="input-search-header"
placeholder="Search"
value={search}
onChange={handleSearch}
sx={{
'& .MuiOutlinedInput-input': {
p: '10.5px 0px 12px'
}
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small' }} />
</InputAdornment>
}
/>
</Stack>
</Box>
<SimpleBar
sx={{
overflowX: 'hidden',
height: matchDownLG ? 'calc(100vh - 120px)' : 'calc(100vh - 428px)',
minHeight: matchDownLG ? 0 : 420
}}
>
<Box sx={{ p: 3, pt: 0 }}>
{/* <UserList setUser={setUser} search={search} /> */}
</Box>
</SimpleBar>
<Box sx={{ p: 3, pb: 0 }}>
<List component="nav">
<ListItemButton divider>
<ListItemIcon>
<LogoutOutlined />
</ListItemIcon>
<ListItemText primary="LogOut" />
</ListItemButton>
<ListItemButton divider>
<ListItemIcon>
<SettingOutlined />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
</List>
</Box>
<Box sx={{ p: 3, pt: 1, pl: 5 }}>
<Grid container>
<Grid item xs={12}>
<Grid container spacing={1} alignItems="center" sx={{ flexWrap: 'nowrap' }}>
<Grid item>
<UserAvatar user={{ online_status: status, avatar: 'avatar-1.png', name: 'User 1' }} />
</Grid>
<Grid item xs zeroMinWidth>
<Stack sx={{ cursor: 'pointer', textDecoration: 'none' }} component={Link} to="/apps/profiles/user/personal">
<Typography align="left" variant="h5" color="textPrimary">
{user?.name}
</Typography>
<Typography align="left" variant="caption" color="textSecondary">
{user?.role}
</Typography>
</Stack>
</Grid>
<Grid item>
<IconButton onClick={handleClickRightMenu} size="small" color="secondary">
<RightOutlined />
</IconButton>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseRightMenu}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
sx={{
'& .MuiMenu-list': {
p: 0
},
'& .MuiMenuItem-root': {
pl: '6px',
py: '3px'
}
}}
>
<MenuItem onClick={handleRightMenuItemClick('available')}>
<IconButton
size="small"
sx={{
color: theme.palette.success.main,
'&:hover': { color: theme.palette.success.main, bgcolor: 'transparent', transition: 'none', padding: 0 }
}}
>
<CheckCircleFilled />
</IconButton>
<Typography>Active</Typography>
</MenuItem>
<MenuItem onClick={handleRightMenuItemClick('offline')}>
<IconButton
size="small"
sx={{
color: theme.palette.warning.main,
'&:hover': { color: theme.palette.warning.main, bgcolor: 'transparent', transition: 'none', padding: 0 }
}}
>
<ClockCircleFilled />
</IconButton>
<Typography>Away</Typography>
</MenuItem>
<MenuItem onClick={handleRightMenuItemClick('do_not_disturb')}>
<IconButton
size="small"
sx={{
color: theme.palette.grey[400],
'&:hover': { color: theme.palette.grey[400], bgcolor: 'transparent', transition: 'none', padding: 0 }
}}
>
<MinusCircleFilled />
</IconButton>
<Typography>Do not disturb</Typography>
</MenuItem>
</Menu>
</Grid>
</Grid>
</Grid>
</Grid>
</Box>
</MainCard>
</Drawer>
);
}
ChatDrawer.propTypes = {
handleDrawerOpen: PropTypes.func,
openChatDrawer: PropTypes.bool,
setUser: PropTypes.func
};
export default ChatDrawer;

View File

@@ -1,119 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback, useEffect, useRef } from 'react';
// material-ui
import { Card, CardContent, Grid, Stack, Typography } from '@mui/material';
// project imports
import UserAvatar from './UserAvatar';
// import ChatMessageAction from './ChatMessageAction';
import IconButton from 'components/@extended/IconButton';
import { ThemeMode } from 'config';
// assets
import { EditOutlined } from '@ant-design/icons';
// ==============================|| CHAT MESSAGE HISTORY ||============================== //
const ChatHistory = ({ data, theme, user }) => {
// scroll to bottom when new message is sent or received
const wrapper = useRef(document.createElement('div'));
const el = wrapper.current;
const scrollToBottom = useCallback(() => {
el.scrollIntoView(false);
}, [el]);
useEffect(() => {
scrollToBottom();
}, [data.length, scrollToBottom]);
return (
<Grid container spacing={2.5} ref={wrapper}>
{data.map((history, index) => (
<Grid item xs={12} key={index}>
{history.from !== user.name ? (
<Stack spacing={1.25} direction="row">
<Grid container spacing={1} justifyContent="flex-end">
<Grid item xs={2} md={3} xl={4} />
<Grid item xs={10} md={9} xl={8}>
<Stack direction="row" justifyContent="flex-end" alignItems="flex-start">
{/* <ChatMessageAction index={index} /> */}
<IconButton size="small" color="secondary">
<EditOutlined />
</IconButton>
<Card
sx={{
display: 'inline-block',
float: 'right',
bgcolor: theme.palette.primary.main,
boxShadow: 'none',
ml: 1
}}
>
<CardContent sx={{ p: 1, pb: '8px !important', width: 'fit-content', ml: 'auto' }}>
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography variant="h6" color={theme.palette.grey[0]} sx={{ overflowWrap: 'anywhere' }}>
{history.text}
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
</Stack>
</Grid>
<Grid item xs={12}>
<Typography align="right" variant="subtitle2" color="textSecondary">
{history.time}
</Typography>
</Grid>
</Grid>
<UserAvatar user={{ online_status: 'available', avatar: 'avatar-1.png', name: 'User 1' }} />
</Stack>
) : (
<Stack direction="row" spacing={1.25} alignItems="flext-start">
<UserAvatar user={{ online_status: user.online_status, avatar: user.avatar, name: user.name }} />
<Grid container>
<Grid item xs={12} sm={7}>
<Card
sx={{
display: 'inline-block',
float: 'left',
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'background.background' : 'grey.0',
boxShadow: 'none'
}}
>
<CardContent sx={{ p: 1, pb: '8px !important' }}>
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography variant="h6" color="textPrimary" sx={{ overflowWrap: 'anywhere' }}>
{history.text}
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sx={{ mt: 1 }}>
<Typography align="left" variant="subtitle2" color="textSecondary">
{history.time}
</Typography>
</Grid>
</Grid>
</Stack>
)}
</Grid>
))}
</Grid>
);
};
ChatHistory.propTypes = {
data: PropTypes.array,
theme: PropTypes.object,
user: PropTypes.object
};
export default ChatHistory;

View File

@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Badge } from '@mui/material';
// project imports
// import AvatarStatus from './AvatarStatus';
import Avatar from 'components/@extended/Avatar';
// assets
const avatarImage = require.context('assets/images/users', true);
// ==============================|| CHAT USER AVATAR WITH STATUS ICON ||============================== //
const UserAvatar = ({ user }) => (
<Badge
overlap="circular"
// badgeContent={<AvatarStatus status={user.online_status} />}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
sx={{ '& .MuiBox-root': { width: 6, height: 6 }, padding: 0, minWidth: 12, '& svg': { background: '#fff', borderRadius: '50%' } }}
>
<Avatar alt={user.name} src={user.avatar && avatarImage(`./${user.avatar}`)} />
</Badge>
);
UserAvatar.propTypes = {
user: PropTypes.object
};
export default UserAvatar;

View File

@@ -1,303 +0,0 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { useMediaQuery, Box, Chip, Collapse, Divider, Grid, Stack, Switch, Typography } from '@mui/material';
// project imports
// import AvatarStatus from './AvatarStatus';
import MainCard from 'components/MainCard';
import Avatar from 'components/@extended/Avatar';
import IconButton from 'components/@extended/IconButton';
import SimpleBar from 'components/third-party/SimpleBar';
import { ThemeMode } from 'config';
// assets
import {
CloseOutlined,
DownOutlined,
FileDoneOutlined,
FileSyncOutlined,
FolderOpenOutlined,
LinkOutlined,
MessageOutlined,
MoreOutlined,
PhoneOutlined,
PictureOutlined,
RightOutlined,
VideoCameraOutlined
} from '@ant-design/icons';
const avatarImage = require.context('assets/images/users', true);
// ==============================|| USER PROFILE / DETAILS ||============================== //
const UserDetails = ({ user, onClose }) => {
const theme = useTheme();
const matchDownLG = useMediaQuery(theme.breakpoints.down('md'));
const [checked, setChecked] = useState(true);
if (Object.keys(user).length === 0) return <Typography>...Loading</Typography>;
let statusBGColor;
let statusColor;
if (user.online_status === 'available') {
statusBGColor = theme.palette.success.lighter;
statusColor = theme.palette.success.main;
} else if (user.online_status === 'do_not_disturb') {
statusBGColor = theme.palette.grey.A100;
statusColor = theme.palette.grey.A200;
} else {
statusBGColor = theme.palette.warning.lighter;
statusColor = theme.palette.warning.main;
}
return (
<MainCard
sx={{
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'dark.main' : 'grey.0',
borderRadius: '0 4px 4px 0',
borderLeft: 'none'
}}
content={false}
>
<Box sx={{ p: 3 }}>
{onClose && (
<IconButton size="small" sx={{ position: 'absolute', right: 8, top: 8 }} onClick={onClose} color="error">
<CloseOutlined />
</IconButton>
)}
<Grid container spacing={1} justifyContent="center">
<Grid item xs={12}>
<Stack>
<Avatar
alt={user.name}
src={user.avatar && avatarImage(`./${user.avatar}`)}
size="xl"
sx={{
m: '8px auto',
width: 88,
height: 88,
border: '1px solid',
borderColor: theme.palette.primary.main,
p: 1,
bgcolor: 'transparent',
'& .MuiAvatar-img ': {
height: '88px',
width: '88px',
borderRadius: '50%'
}
}}
/>
<Typography variant="h5" align="center" component="div">
{user.name}
</Typography>
<Typography variant="body2" align="center" color="textSecondary">
{user.role}
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack
direction="row"
alignItems="center"
spacing={1}
justifyContent="center"
sx={{ mt: 0.75, '& .MuiChip-root': { height: '24px' } }}
>
{/* <AvatarStatus status={user?.online_status} /> */}
<Chip
label={user?.online_status.replaceAll('_', ' ')}
sx={{
bgcolor: statusBGColor,
textTransform: 'capitalize',
color: statusColor,
'& .MuiChip-label': { px: 1 }
}}
/>
</Stack>
</Grid>
</Grid>
<Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 3 }}>
<IconButton size="medium" color="secondary" sx={{ boxShadow: '0px 8px 25px rgba(0, 0, 0, 0.05)' }}>
<PhoneOutlined />
</IconButton>
<IconButton size="medium" color="secondary" sx={{ boxShadow: '0px 8px 25px rgba(0, 0, 0, 0.05)' }}>
<MessageOutlined />
</IconButton>
<IconButton size="medium" color="secondary" sx={{ boxShadow: '0px 8px 25px rgba(0, 0, 0, 0.05)' }}>
<VideoCameraOutlined />
</IconButton>
</Stack>
</Box>
<Box>
<SimpleBar
sx={{
overflowX: 'hidden',
height: matchDownLG ? 'auto' : 'calc(100vh - 397px)',
minHeight: matchDownLG ? 0 : 420
}}
>
<Stack spacing={3}>
<Stack direction="row" spacing={1.5} justifyContent="center" sx={{ px: 3 }}>
<Box sx={{ bgcolor: 'primary.lighter', p: 2, width: '50%', borderRadius: 2 }}>
<Typography color="primary">All File</Typography>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mt: 0.5 }}>
<FolderOpenOutlined style={{ color: theme.palette.primary.main, fontSize: '1.15em' }} />
<Typography variant="h4">231</Typography>
</Stack>
</Box>
<Box sx={{ bgcolor: 'secondary.lighter', p: 2, width: '50%', borderRadius: 2 }}>
<Typography>All Link</Typography>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mt: 0.5 }}>
<LinkOutlined style={{ fontSize: '1.15em' }} />
<Typography variant="h4">231</Typography>
</Stack>
</Box>
</Stack>
<Box sx={{ px: 3, pb: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ cursor: 'pointer' }}
onClick={() => setChecked(!checked)}
>
<Typography variant="h5" component="div">
Information
</Typography>
<IconButton size="small" color="secondary">
<DownOutlined />
</IconButton>
</Stack>
</Grid>
<Grid item xs={12} sx={{ mt: -1 }}>
<Divider />
</Grid>
<Grid item xs={12}>
<Collapse in={checked}>
<Stack direction="row" justifyContent="space-between" sx={{ mt: 1, mb: 2 }}>
<Typography>Address</Typography>
<Typography color="textSecondary">{user.location}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" sx={{ mt: 2 }}>
<Typography>Email</Typography>
<Typography color="textSecondary">{user.personal_email}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" sx={{ mt: 2 }}>
<Typography>Phone</Typography>
<Typography color="textSecondary">{user.personal_phone}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" sx={{ mt: 2, mb: 2 }}>
<Typography>Last visited</Typography>
<Typography color="textSecondary">{user.lastMessage}</Typography>
</Stack>
</Collapse>
</Grid>
<Grid item xs={12}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Typography variant="h5">Notification</Typography>
<Switch defaultChecked />
</Stack>
</Grid>
<Grid item xs={12} sx={{ mt: -1 }}>
<Divider />
</Grid>
<Grid item xs={12} sx={{ mt: -1 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Typography variant="h5">File type</Typography>
<IconButton size="medium" color="secondary">
<MoreOutlined />
</IconButton>
</Stack>
</Grid>
<Grid item xs={12} sx={{ mt: -1 }}>
<Divider />
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar
sx={{
color: theme.palette.success.dark,
bgcolor: theme.palette.success.light,
borderRadius: 1
}}
>
<FileDoneOutlined />
</Avatar>
<Stack>
<Typography>Document</Typography>
<Typography color="textSecondary">123 files, 193MB</Typography>
</Stack>
</Stack>
<IconButton size="small" color="secondary">
<RightOutlined />
</IconButton>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar
sx={{
color: theme.palette.warning.main,
bgcolor: theme.palette.warning.lighter,
borderRadius: 1
}}
>
<PictureOutlined />
</Avatar>
<Stack>
<Typography>Photos</Typography>
<Typography color="textSecondary">53 files, 321MB</Typography>
</Stack>
</Stack>
<IconButton size="small" color="secondary">
<RightOutlined />
</IconButton>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar
sx={{
color: theme.palette.primary.main,
bgcolor: theme.palette.primary.lighter,
borderRadius: 1
}}
>
<FileSyncOutlined />
</Avatar>
<Stack>
<Typography>Other</Typography>
<Typography color="textSecondary">49 files, 193MB</Typography>
</Stack>
</Stack>
<IconButton size="small" color="secondary">
<RightOutlined />
</IconButton>
</Stack>
</Grid>
</Grid>
</Box>
</Stack>
</SimpleBar>
</Box>
</MainCard>
);
};
UserDetails.propTypes = {
user: PropTypes.object,
onClose: PropTypes.func
};
export default UserDetails;

View File

@@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// ==============================|| AUTHENTICATION - CARD WRAPPER ||============================== //
const AuthCard = ({ children, ...other }) => {
const theme = useTheme();
return (
<MainCard
sx={{
maxWidth: { xs: 400, lg: 475 },
margin: { xs: 2.5, md: 3 },
'& > *': {
flexGrow: 1,
flexBasis: '50%'
}
}}
content={false}
{...other}
border={false}
boxShadow
shadow={theme.customShadows.z1}
>
<Box sx={{ p: { xs: 2, sm: 3, md: 4, xl: 5 } }}>{children}</Box>
</MainCard>
);
};
AuthCard.propTypes = {
children: PropTypes.node
};
export default AuthCard;

View File

@@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Grid } from '@mui/material';
// project import
import AuthFooter from 'components/cards/AuthFooter';
import Logo from 'components/logo';
import AuthCard from './AuthCard';
// assets
import AuthBackground from 'assets/images/auth/AuthBackground';
// ==============================|| AUTHENTICATION - WRAPPER ||============================== //
const AuthWrapper = ({ children }) => (
<Box sx={{ minHeight: '100vh' }}>
<AuthBackground />
<Grid
container
direction="column"
justifyContent="flex-end"
sx={{
minHeight: '100vh'
}}
>
<Grid item xs={12} sx={{ ml: 3, mt: 3 }}>
<Logo />
</Grid>
<Grid item xs={12}>
<Grid
item
xs={12}
container
justifyContent="center"
alignItems="center"
sx={{ minHeight: { xs: 'calc(100vh - 210px)', sm: 'calc(100vh - 134px)', md: 'calc(100vh - 112px)' } }}
>
<Grid item>
<AuthCard>{children}</AuthCard>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sx={{ m: 3, mt: 1 }}>
<AuthFooter />
</Grid>
</Grid>
</Box>
);
AuthWrapper.propTypes = {
children: PropTypes.node
};
export default AuthWrapper;

View File

@@ -1,66 +0,0 @@
import { useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Button, Grid, Stack, Typography } from '@mui/material';
// third-party
import OtpInput from 'react18-input-otp';
// project import
import AnimateButton from 'components/@extended/AnimateButton';
import { ThemeMode } from 'config';
// ============================|| STATIC - CODE VERIFICATION ||============================ //
const AuthCodeVerification = () => {
const theme = useTheme();
const [otp, setOtp] = useState();
const borderColor = theme.palette.mode === ThemeMode.DARK ? theme.palette.grey[200] : theme.palette.grey[300];
return (
<Grid container spacing={3}>
<Grid item xs={12}>
<OtpInput
value={otp}
onChange={(otp) => setOtp(otp)}
numInputs={4}
containerStyle={{ justifyContent: 'space-between' }}
inputStyle={{
width: '100%',
margin: '8px',
padding: '10px',
border: `1px solid ${borderColor}`,
borderRadius: 4,
':hover': {
borderColor: theme.palette.primary.main
}
}}
focusStyle={{
outline: 'none',
boxShadow: theme.customShadows.primary,
border: `1px solid ${theme.palette.primary.main}`
}}
/>
</Grid>
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation fullWidth size="large" type="submit" variant="contained">
Continue
</Button>
</AnimateButton>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline">
<Typography>Did not receive the email? Check your spam filter, or</Typography>
<Typography variant="body1" sx={{ minWidth: 85, ml: 2, textDecoration: 'none', cursor: 'pointer' }} color="primary">
Resend code
</Typography>
</Stack>
</Grid>
</Grid>
);
};
export default AuthCodeVerification;

View File

@@ -1,126 +0,0 @@
import { useNavigate } from 'react-router-dom';
// material-ui
import { Button, FormHelperText, Grid, InputLabel, OutlinedInput, Stack, Typography } from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
// project import
import AnimateButton from 'components/@extended/AnimateButton';
import useAuth from 'hooks/useAuth';
import useScriptRef from 'hooks/useScriptRef';
import { dispatch } from 'store';
import { openSnackbar } from 'store/reducers/snackbar';
// ============================|| FIREBASE - FORGOT PASSWORD ||============================ //
const AuthForgotPassword = () => {
const scriptedRef = useScriptRef();
const navigate = useNavigate();
const { isLoggedIn, resetPassword } = useAuth();
return (
<>
<Formik
initialValues={{
email: '',
submit: null
}}
validationSchema={Yup.object().shape({
email: Yup.string().email('Must be a valid email').max(255).required('Email is required')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
await resetPassword(values.email).then(
() => {
setStatus({ success: true });
setSubmitting(false);
dispatch(
openSnackbar({
open: true,
message: 'Check mail for reset password link',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
setTimeout(() => {
navigate(isLoggedIn ? '/auth/check-mail' : '/check-mail', { replace: true });
}, 1500);
// WARNING: do not set any formik state here as formik might be already destroyed here. You may get following error by doing so.
// Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.
// To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
// github issue: https://github.com/formium/formik/issues/2430
},
(err) => {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
);
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="email-forgot">Email Address</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.email && errors.email)}
id="email-forgot"
type="email"
value={values.email}
name="email"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Enter email address"
inputProps={{}}
/>
{touched.email && errors.email && (
<FormHelperText error id="helper-text-email-forgot">
{errors.email}
</FormHelperText>
)}
</Stack>
</Grid>
{errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12} sx={{ mb: -2 }}>
<Typography variant="caption">Do not forgot to check SPAM box.</Typography>
</Grid>
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Send Password Reset Email
</Button>
</AnimateButton>
</Grid>
</Grid>
</form>
)}
</Formik>
</>
);
};
export default AuthForgotPassword;

View File

@@ -1,176 +0,0 @@
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
// material-ui
import {
Button,
Checkbox,
FormControlLabel,
FormHelperText,
Grid,
Link,
InputAdornment,
InputLabel,
OutlinedInput,
Stack,
Typography
} from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
// project import
import useAuth from 'hooks/useAuth';
import useScriptRef from 'hooks/useScriptRef';
import IconButton from 'components/@extended/IconButton';
import AnimateButton from 'components/@extended/AnimateButton';
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
// ============================|| JWT - LOGIN ||============================ //
const AuthLogin = () => {
const [checked, setChecked] = React.useState(false);
const { login } = useAuth();
const scriptedRef = useScriptRef();
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
return (
<>
<Formik
initialValues={{
email: 'info@codedthemes.com',
password: '123456',
submit: null
}}
validationSchema={Yup.object().shape({
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
password: Yup.string().max(255).required('Password is required')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
await login(values.email, values.password);
if (scriptedRef.current) {
setStatus({ success: true });
setSubmitting(false);
}
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="email-login">Email Address</InputLabel>
<OutlinedInput
id="email-login"
type="email"
value={values.email}
name="email"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Enter email address"
fullWidth
error={Boolean(touched.email && errors.email)}
/>
{touched.email && errors.email && (
<FormHelperText error id="standard-weight-helper-text-email-login">
{errors.email}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="password-login">Password</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.password && errors.password)}
id="-password-login"
type={showPassword ? 'text' : 'password'}
value={values.password}
name="password"
onBlur={handleBlur}
onChange={handleChange}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
color="secondary"
>
{showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
</IconButton>
</InputAdornment>
}
placeholder="Enter password"
/>
{touched.password && errors.password && (
<FormHelperText error id="standard-weight-helper-text-password-login">
{errors.password}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12} sx={{ mt: -1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<FormControlLabel
control={
<Checkbox
checked={checked}
onChange={(event) => setChecked(event.target.checked)}
name="checked"
color="primary"
size="small"
/>
}
label={<Typography variant="h6">Keep me sign in</Typography>}
/>
<Link variant="h6" component={RouterLink} to="/forgot-password" color="text.primary">
Forgot Password?
</Link>
</Stack>
</Grid>
{errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Login
</Button>
</AnimateButton>
</Grid>
</Grid>
</form>
)}
</Formik>
</>
);
};
export default AuthLogin;

View File

@@ -1,282 +0,0 @@
import { useEffect, useState } from 'react';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
// material-ui
import {
Box,
Button,
FormControl,
FormHelperText,
Grid,
Link,
InputAdornment,
InputLabel,
OutlinedInput,
Stack,
Typography
} from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
// project import
import IconButton from 'components/@extended/IconButton';
import AnimateButton from 'components/@extended/AnimateButton';
import useAuth from 'hooks/useAuth';
import useScriptRef from 'hooks/useScriptRef';
import { dispatch } from 'store';
import { openSnackbar } from 'store/reducers/snackbar';
import { strengthColor, strengthIndicator } from 'utils/password-strength';
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
// ============================|| JWT - REGISTER ||============================ //
const AuthRegister = () => {
const { register } = useAuth();
const scriptedRef = useScriptRef();
const navigate = useNavigate();
const [level, setLevel] = useState();
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
const changePassword = (value) => {
const temp = strengthIndicator(value);
setLevel(strengthColor(temp));
};
useEffect(() => {
changePassword('');
}, []);
return (
<>
<Formik
initialValues={{
firstname: '',
lastname: '',
email: '',
company: '',
password: '',
submit: null
}}
validationSchema={Yup.object().shape({
firstname: Yup.string().max(255).required('First Name is required'),
lastname: Yup.string().max(255).required('Last Name is required'),
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
password: Yup.string().max(255).required('Password is required')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
await register(values.email, values.password, values.firstname, values.lastname);
if (scriptedRef.current) {
setStatus({ success: true });
setSubmitting(false);
dispatch(
openSnackbar({
open: true,
message: 'Your registration has been successfully completed.',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
setTimeout(() => {
navigate('/login', { replace: true });
}, 1500);
}
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Stack spacing={1}>
<InputLabel htmlFor="firstname-signup">First Name*</InputLabel>
<OutlinedInput
id="firstname-login"
type="firstname"
value={values.firstname}
name="firstname"
onBlur={handleBlur}
onChange={handleChange}
placeholder="John"
fullWidth
error={Boolean(touched.firstname && errors.firstname)}
/>
{touched.firstname && errors.firstname && (
<FormHelperText error id="helper-text-firstname-signup">
{errors.firstname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12} md={6}>
<Stack spacing={1}>
<InputLabel htmlFor="lastname-signup">Last Name*</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.lastname && errors.lastname)}
id="lastname-signup"
type="lastname"
value={values.lastname}
name="lastname"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Doe"
inputProps={{}}
/>
{touched.lastname && errors.lastname && (
<FormHelperText error id="helper-text-lastname-signup">
{errors.lastname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="company-signup">Company</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.company && errors.company)}
id="company-signup"
value={values.company}
name="company"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Demo Inc."
inputProps={{}}
/>
{touched.company && errors.company && (
<FormHelperText error id="helper-text-company-signup">
{errors.company}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="email-signup">Email Address*</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.email && errors.email)}
id="email-login"
type="email"
value={values.email}
name="email"
onBlur={handleBlur}
onChange={handleChange}
placeholder="demo@company.com"
inputProps={{}}
/>
{touched.email && errors.email && (
<FormHelperText error id="helper-text-email-signup">
{errors.email}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="password-signup">Password</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.password && errors.password)}
id="password-signup"
type={showPassword ? 'text' : 'password'}
value={values.password}
name="password"
onBlur={handleBlur}
onChange={(e) => {
handleChange(e);
changePassword(e.target.value);
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
color="secondary"
>
{showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
</IconButton>
</InputAdornment>
}
placeholder="******"
inputProps={{}}
/>
{touched.password && errors.password && (
<FormHelperText error id="helper-text-password-signup">
{errors.password}
</FormHelperText>
)}
</Stack>
<FormControl fullWidth sx={{ mt: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
</Grid>
<Grid item>
<Typography variant="subtitle1" fontSize="0.75rem">
{level?.label}
</Typography>
</Grid>
</Grid>
</FormControl>
</Grid>
<Grid item xs={12}>
<Typography variant="body2">
By Signing up, you agree to our &nbsp;
<Link variant="subtitle2" component={RouterLink} to="#">
Terms of Service
</Link>
&nbsp; and &nbsp;
<Link variant="subtitle2" component={RouterLink} to="#">
Privacy Policy
</Link>
</Typography>
</Grid>
{errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Create Account
</Button>
</AnimateButton>
</Grid>
</Grid>
</form>
)}
</Formik>
</>
);
};
export default AuthRegister;

View File

@@ -1,201 +0,0 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
// material-ui
import {
Box,
Button,
FormControl,
FormHelperText,
Grid,
InputAdornment,
InputLabel,
OutlinedInput,
Stack,
Typography
} from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
// project import
import IconButton from 'components/@extended/IconButton';
import AnimateButton from 'components/@extended/AnimateButton';
import useAuth from 'hooks/useAuth';
import useScriptRef from 'hooks/useScriptRef';
import { dispatch } from 'store';
import { openSnackbar } from 'store/reducers/snackbar';
import { strengthColor, strengthIndicator } from 'utils/password-strength';
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
// ============================|| STATIC - RESET PASSWORD ||============================ //
const AuthResetPassword = () => {
const scriptedRef = useScriptRef();
const navigate = useNavigate();
const { isLoggedIn } = useAuth();
const [level, setLevel] = useState();
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
const changePassword = (value) => {
const temp = strengthIndicator(value);
setLevel(strengthColor(temp));
};
useEffect(() => {
changePassword('');
}, []);
return (
<Formik
initialValues={{
password: '',
confirmPassword: '',
submit: null
}}
validationSchema={Yup.object().shape({
password: Yup.string().max(255).required('Password is required'),
confirmPassword: Yup.string()
.required('Confirm Password is required')
.test('confirmPassword', 'Both Password must be match!', (confirmPassword, yup) => yup.parent.password === confirmPassword)
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
// password reset
if (scriptedRef.current) {
setStatus({ success: true });
setSubmitting(false);
dispatch(
openSnackbar({
open: true,
message: 'Successfuly reset password.',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
setTimeout(() => {
navigate(isLoggedIn ? '/auth/login' : '/login', { replace: true });
}, 1500);
}
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="password-reset">Password</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.password && errors.password)}
id="password-reset"
type={showPassword ? 'text' : 'password'}
value={values.password}
name="password"
onBlur={handleBlur}
onChange={(e) => {
handleChange(e);
changePassword(e.target.value);
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
color="secondary"
>
{showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
</IconButton>
</InputAdornment>
}
placeholder="Enter password"
/>
{touched.password && errors.password && (
<FormHelperText error id="helper-text-password-reset">
{errors.password}
</FormHelperText>
)}
</Stack>
<FormControl fullWidth sx={{ mt: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
</Grid>
<Grid item>
<Typography variant="subtitle1" fontSize="0.75rem">
{level?.label}
</Typography>
</Grid>
</Grid>
</FormControl>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="confirm-password-reset">Confirm Password</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.confirmPassword && errors.confirmPassword)}
id="confirm-password-reset"
type="password"
value={values.confirmPassword}
name="confirmPassword"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Enter confirm password"
/>
{touched.confirmPassword && errors.confirmPassword && (
<FormHelperText error id="helper-text-confirm-password-reset">
{errors.confirmPassword}
</FormHelperText>
)}
</Stack>
</Grid>
{errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Reset Password
</Button>
</AnimateButton>
</Grid>
</Grid>
</form>
)}
</Formik>
);
};
export default AuthResetPassword;

View File

@@ -1,82 +0,0 @@
// material-ui
import { useTheme } from '@mui/material/styles';
import { useMediaQuery, Button, Stack } from '@mui/material';
// project import
import useAuth from 'hooks/useAuth';
// assets
import Google from 'assets/images/icons/google.svg';
import Twitter from 'assets/images/icons/twitter.svg';
import Facebook from 'assets/images/icons/facebook.svg';
// ==============================|| FIREBASE - SOCIAL BUTTON ||============================== //
const FirebaseSocial = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('sm'));
const { firebaseFacebookSignIn, firebaseGoogleSignIn, firebaseTwitterSignIn } = useAuth();
const googleHandler = async () => {
try {
await firebaseGoogleSignIn();
} catch (err) {
console.error(err);
}
};
const twitterHandler = async () => {
try {
await firebaseTwitterSignIn();
} catch (err) {
console.error(err);
}
};
const facebookHandler = async () => {
try {
await firebaseFacebookSignIn();
} catch (err) {
console.error(err);
}
};
return (
<Stack
direction="row"
spacing={matchDownSM ? 1 : 2}
justifyContent={matchDownSM ? 'space-around' : 'space-between'}
sx={{ '& .MuiButton-startIcon': { mr: matchDownSM ? 0 : 1, ml: matchDownSM ? 0 : -0.5 } }}
>
<Button
variant="outlined"
color="secondary"
fullWidth={!matchDownSM}
startIcon={<img src={Google} alt="Google" />}
onClick={googleHandler}
>
{!matchDownSM && 'Google'}
</Button>
<Button
variant="outlined"
color="secondary"
fullWidth={!matchDownSM}
startIcon={<img src={Twitter} alt="Twitter" />}
onClick={twitterHandler}
>
{!matchDownSM && 'Twitter'}
</Button>
<Button
variant="outlined"
color="secondary"
fullWidth={!matchDownSM}
startIcon={<img src={Facebook} alt="Facebook" />}
onClick={facebookHandler}
>
{!matchDownSM && 'Facebook'}
</Button>
</Stack>
);
};
export default FirebaseSocial;

View File

@@ -1,68 +0,0 @@
import { Link as RouterLink } from 'react-router-dom';
// material-ui
import { Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
// project imports
import MainCard from 'components/MainCard';
// table data
function createData(name, designation, product, date, badgeText, badgeType) {
return { name, designation, product, date, badgeText, badgeType };
}
const rows = [
createData('Materially', 'Powerful Admin Theme', '16,300', '$53', '$15,652'),
createData('Photoshop', 'Design Software', '26,421', '$35', '$8,785'),
createData('Guruable', 'Best Admin Template', '8,265', '$98', '$9,652'),
createData('Flatable', 'Admin App', '10,652', '$20', '$7,856')
];
// =========================|| DATA WIDGET - APPLICATION SALES ||========================= //
const ApplicationSales = () => (
<MainCard
title="Application Sales"
content={false}
secondary={
<Link component={RouterLink} to="#" color="primary">
View all
</Link>
}
>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell sx={{ pl: 3 }}>Application</TableCell>
<TableCell align="right">Sales</TableCell>
<TableCell align="right">Avg. Price</TableCell>
<TableCell align="right" sx={{ pr: 3 }}>
Total
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<TableRow hover key={index}>
{/* <TableCell sx={{ pl: 3 }}>
<Typography align="left" variant="subtitle1">
{row.name}
</Typography>
<Typography align="left" variant="caption" color="secondary">
{row.designation}
</Typography>
</TableCell>
<TableCell align="right">{row.product}</TableCell>
<TableCell align="right">{row.date}</TableCell>
<TableCell align="right" sx={{ pr: 3 }}>
<span>{row.badgeText}</span>
</TableCell> */}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</MainCard>
);
export default ApplicationSales;

View File

@@ -1,175 +0,0 @@
import { Link as RouterLink } from 'react-router-dom';
// material-ui
import { CardContent, Grid, Link, Typography } from '@mui/material';
// project imports
import MainCard from 'components/MainCard';
import Avatar from 'components/@extended/Avatar';
// assets
import { TwitterCircleFilled, ClockCircleFilled, BugFilled, MobileFilled, WarningFilled } from '@ant-design/icons';
// ==============================|| DATA WIDGET - TASKS CARD ||============================== //
const TasksCard = () => (
<MainCard
title="Tasks"
content={false}
secondary={
<Link component={RouterLink} to="#" color="primary">
View all
</Link>
}
>
<CardContent>
<Grid
container
spacing={2.75}
alignItems="center"
sx={{
position: 'relative',
'&>*': {
position: 'relative',
zIndex: '5'
},
'&:after': {
content: '""',
position: 'absolute',
top: 10,
left: 38,
width: 2,
height: '100%',
background: '#ebebeb',
zIndex: '1'
}
}}
>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item>
<Avatar type="filled" color="success" size="sm" sx={{ top: 10 }}>
<TwitterCircleFilled />
</Avatar>
</Grid>
<Grid item xs zeroMinWidth>
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography align="left" variant="caption" color="secondary">
8:50
</Typography>
</Grid>
<Grid item xs={12}>
<Typography align="left" variant="body2">
Youre getting more and more followers, keep it up!
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item>
<Avatar type="filled" color="primary" size="sm" sx={{ top: 10 }}>
<ClockCircleFilled />
</Avatar>
</Grid>
<Grid item xs zeroMinWidth>
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography align="left" variant="caption" color="secondary">
Sat, 5 Mar
</Typography>
</Grid>
<Grid item xs={12}>
<Typography align="left" variant="body2">
Design mobile Application
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item>
<Avatar type="filled" color="error" size="sm" sx={{ top: 10 }}>
<BugFilled />
</Avatar>
</Grid>
<Grid item xs zeroMinWidth>
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography align="left" variant="caption" color="secondary">
Sun, 17 Feb
</Typography>
</Grid>
<Grid item xs={12}>
<Typography align="left" variant="body2">
<Link component={RouterLink} to="#" underline="hover">
Jenny
</Link>{' '}
assign you a task{' '}
<Link component={RouterLink} to="#" underline="hover">
Mockup Design
</Link>
.
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item>
<Avatar type="filled" color="warning" size="sm" sx={{ top: 10 }}>
<WarningFilled />
</Avatar>
</Grid>
<Grid item xs zeroMinWidth>
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography align="left" variant="caption" color="secondary">
Sat, 18 Mar
</Typography>
</Grid>
<Grid item xs={12}>
<Typography align="left" variant="body2">
Design logo
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item>
<Avatar type="filled" color="success" size="sm" sx={{ top: 10 }}>
<MobileFilled />
</Avatar>
</Grid>
<Grid item xs zeroMinWidth>
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography align="left" variant="caption" color="secondary">
Sat, 22 Mar
</Typography>
</Grid>
<Grid item xs={12}>
<Typography align="left" variant="body2">
Design mobile Application
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</CardContent>
</MainCard>
);
export default TasksCard;

View File

@@ -4,6 +4,12 @@ const axiosServices = axios.create({ baseURL: process.env.REACT_APP_API_URL || '
// ==============================|| AXIOS - FOR MOCK SERVICES ||============================== //
axiosServices.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
axiosServices.interceptors.response.use(
(response) => response,
(error) => {

View File

@@ -4,20 +4,15 @@
"others": "Others",
"roadmap": "Roadmap",
"MENU": "Menu",
"orders": "Orders",
"orders": "Bookings",
"orderspreview": "Orders Preview",
"deliveries": "Deliveries",
"tenants": "Tenants",
"deliveries": "Consignments",
"tenants": "Clients",
"pricing": "Pricing",
"customers": "Customers",
"riders": "Riders",
"reports": "Reports",
"ordersummary": "Orders Summary",
"ordersdetails": "Orders Details",
"riderssummary": "Riders Summary",
"riderslogs": "Riders Logs",
"invoice": "Invoice",
"dispatch": "Dispatch",
"profitability": "Profitability",
"riders": "Milers",
"dispatch": "Live Operations",
"dashboard": "Dashboard",
"hubs": "Hubs",
"Doormile": "Doormile"
}

View File

@@ -1,33 +0,0 @@
/**
* Password validator for login pages
*/
// has number
const hasNumber = (number) => new RegExp(/[0-9]/).test(number);
// has mix of small and capitals
const hasMixed = (number) => new RegExp(/[a-z]/).test(number) && new RegExp(/[A-Z]/).test(number);
// has special chars
const hasSpecial = (number) => new RegExp(/[!#@$%^&*)(+=._-]/).test(number);
// set color based on password strength
export const strengthColor = (count) => {
if (count < 2) return { label: 'Poor', color: 'error.main' };
if (count < 3) return { label: 'Weak', color: 'warning.main' };
if (count < 4) return { label: 'Normal', color: 'warning.dark' };
if (count < 5) return { label: 'Good', color: 'success.main' };
if (count < 6) return { label: 'Strong', color: 'success.dark' };
return { label: 'Poor', color: 'error.main' };
};
// password strength indicator
export const strengthIndicator = (number) => {
let strengths = 0;
if (number.length > 5) strengths += 1;
if (number.length > 7) strengths += 1;
if (hasNumber(number)) strengths += 1;
if (hasSpecial(number)) strengths += 1;
if (hasMixed(number)) strengths += 1;
return strengths;
};

View File

@@ -1,21 +0,0 @@
function isNumber(value) {
return new RegExp('^(?=.*[0-9]).+$').test(value);
}
function isLowercaseChar(value) {
return new RegExp('^(?=.*[a-z]).+$').test(value);
}
function isUppercaseChar(value) {
return new RegExp('^(?=.*[A-Z]).+$').test(value);
}
function isSpecialChar(value) {
return new RegExp('^(?=.*[-+_!@#$%^&*.,?]).+$').test(value);
}
function minLength(value) {
return value.length > 7;
}
export { isNumber, isLowercaseChar, isUppercaseChar, isSpecialChar, minLength };

View File

@@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
// project import
import useAuth from 'hooks/useAuth';
// ==============================|| AUTH GUARD ||============================== //
const AuthGuard = ({ children }) => {
const { isLoggedIn } = useAuth();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (!isLoggedIn) {
navigate('login', {
state: {
from: location.pathname
},
replace: true
});
navigate('login', { replace: true });
}
}, [isLoggedIn, navigate, location]);
return children;
};
AuthGuard.propTypes = {
children: PropTypes.node
};
export default AuthGuard;

View File

@@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
// project import
import { APP_DEFAULT_PATH } from 'config';
import useAuth from 'hooks/useAuth';
// ==============================|| GUEST GUARD ||============================== //
const GuestGuard = ({ children }) => {
const { isLoggedIn } = useAuth();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (isLoggedIn) {
navigate(location?.state?.from ? location?.state?.from : APP_DEFAULT_PATH, {
state: {
from: ''
},
replace: true
});
}
}, [isLoggedIn, navigate, location]);
return children;
};
GuestGuard.propTypes = {
children: PropTypes.node
};
export default GuestGuard;

135
yarn.lock
View File

@@ -101,7 +101,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.16.0", "@babel/core@^7.21.3", "@babel/core@^7.21.4", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.4.0-0", "@babel/core@^7.7.2", "@babel/core@^7.8.0", "@babel/core@>=7.11.0":
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.21.3", "@babel/core@^7.21.4", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz"
integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==
@@ -462,7 +462,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-flow@^7.14.5", "@babel/plugin-syntax-flow@^7.16.7":
"@babel/plugin-syntax-flow@^7.16.7":
version "7.26.0"
resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz"
integrity sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==
@@ -912,7 +912,7 @@
dependencies:
"@babel/plugin-transform-react-jsx" "^7.27.1"
"@babel/plugin-transform-react-jsx@^7.14.9", "@babel/plugin-transform-react-jsx@^7.27.1":
"@babel/plugin-transform-react-jsx@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz"
integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==
@@ -1370,7 +1370,7 @@
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.10.6", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.7.1", "@emotion/react@^11.9.0":
"@emotion/react@^11.10.6":
version "11.10.6"
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz"
integrity sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==
@@ -1400,7 +1400,7 @@
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
"@emotion/styled@^11.10.6", "@emotion/styled@^11.3.0", "@emotion/styled@^11.6.0", "@emotion/styled@^11.8.1":
"@emotion/styled@^11.10.6":
version "11.10.6"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz"
integrity sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==
@@ -1533,7 +1533,7 @@
"@firebase/util" "1.10.0"
tslib "^2.1.0"
"@firebase/app-compat@0.2.43", "@firebase/app-compat@0.x":
"@firebase/app-compat@0.2.43":
version "0.2.43"
resolved "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.43.tgz"
integrity sha512-HM96ZyIblXjAC7TzE8wIk2QhHlSvksYkQ4Ukh1GmEenzkucSNUmUX4QvoKrqeWsLEQ8hdcojABeCV8ybVyZmeg==
@@ -1544,12 +1544,12 @@
"@firebase/util" "1.10.0"
tslib "^2.1.0"
"@firebase/app-types@0.9.2", "@firebase/app-types@0.x":
"@firebase/app-types@0.9.2":
version "0.9.2"
resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz"
integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==
"@firebase/app@0.10.13", "@firebase/app@0.x":
"@firebase/app@0.10.13":
version "0.10.13"
resolved "https://registry.npmjs.org/@firebase/app/-/app-0.10.13.tgz"
integrity sha512-OZiDAEK/lDB6xy/XzYAyJJkaDqmQ+BCtOEPLqFvxWKUz5JbBmej7IiiRHdtiIOD/twW7O5AxVsfaaGA/V1bNsA==
@@ -1846,7 +1846,7 @@
tslib "^2.1.0"
undici "6.19.7"
"@firebase/util@1.10.0", "@firebase/util@1.x":
"@firebase/util@1.10.0":
version "1.10.0"
resolved "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz"
integrity sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==
@@ -2356,7 +2356,7 @@
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.14.tgz"
integrity sha512-sbjXW+BBSvmzn61XyTMun899E7nGPTXwqD9drm1jBUAvWEhJpPFIRxwQQiATWZnd9rvdxtnhhdsDxEGWI0jxqA==
"@mui/icons-material@^5.0.4", "@mui/icons-material@^5.14.19":
"@mui/icons-material@^5.14.19":
version "5.16.14"
resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.14.tgz"
integrity sha512-heL4S+EawrP61xMXBm59QH6HODsu0gxtZi5JtnXF2r+rghzyU/3Uftlt1ij8rmJh+cFdKTQug1L9KkZB5JgpMQ==
@@ -2376,7 +2376,7 @@
clsx "^2.1.0"
prop-types "^15.8.1"
"@mui/material@^5.0.0", "@mui/material@^5.12.1", "@mui/material@^5.2.6", "@mui/material@^5.8.6", "@mui/material@>=5.15.0":
"@mui/material@^5.12.1":
version "5.16.14"
resolved "https://registry.npmjs.org/@mui/material/-/material-5.16.14.tgz"
integrity sha512-eSXQVCMKU2xc7EcTxe/X/rC9QsV2jUe8eLM3MUCPYbo6V52eCE436akRIvELq/AqZpxx2bwkq7HC0cRhLB+yaw==
@@ -2413,7 +2413,7 @@
csstype "^3.1.3"
prop-types "^15.8.1"
"@mui/system@^5.0.6", "@mui/system@^5.16.12", "@mui/system@^5.16.14", "@mui/system@^5.8.0":
"@mui/system@^5.16.12", "@mui/system@^5.16.14":
version "5.16.14"
resolved "https://registry.npmjs.org/@mui/system/-/system-5.16.14.tgz"
integrity sha512-KBxMwCb8mSIABnKvoGbvM33XHyT+sN0BzEBG+rsSc0lLQGzs7127KWkCA6/H8h6LZ00XpBEME5MAj8mZLiQ1tw==
@@ -2922,16 +2922,6 @@
"@svgr/babel-plugin-transform-react-native-svg" "^7.0.0"
"@svgr/babel-plugin-transform-svg-component" "^7.0.0"
"@svgr/core@*", "@svgr/core@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/@svgr/core/-/core-7.0.0.tgz"
integrity sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==
dependencies:
"@babel/core" "^7.21.3"
"@svgr/babel-preset" "^7.0.0"
camelcase "^6.2.0"
cosmiconfig "^8.1.3"
"@svgr/core@^5.5.0":
version "5.5.0"
resolved "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz"
@@ -2941,6 +2931,16 @@
camelcase "^6.2.0"
cosmiconfig "^7.0.0"
"@svgr/core@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/@svgr/core/-/core-7.0.0.tgz"
integrity sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==
dependencies:
"@babel/core" "^7.21.3"
"@svgr/babel-preset" "^7.0.0"
camelcase "^6.2.0"
cosmiconfig "^8.1.3"
"@svgr/hast-util-to-babel-ast@^5.5.0":
version "5.5.0"
resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz"
@@ -3044,7 +3044,7 @@
resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.9":
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
version "7.1.19"
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz"
integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==
@@ -3164,7 +3164,7 @@
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.1", "@types/hoist-non-react-statics@>= 3.3.1", "@types/hoist-non-react-statics@3":
"@types/hoist-non-react-statics@^3.3.1", "@types/hoist-non-react-statics@3":
version "3.3.1"
resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@@ -3230,7 +3230,7 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@>= 12", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
version "22.13.5"
resolved "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz"
integrity sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==
@@ -3272,7 +3272,7 @@
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
"@types/react@*", "@types/react@^16.8 || ^17.0 || ^18.0", "@types/react@^17.0.0 || ^18.0.0", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@>= 16", "@types/react@16 || 17 || 18":
"@types/react@*", "@types/react@16 || 17 || 18":
version "18.3.18"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz"
integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==
@@ -3369,7 +3369,7 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.0.0 || ^5.0.0", "@typescript-eslint/eslint-plugin@^5.5.0":
"@typescript-eslint/eslint-plugin@^5.5.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz"
integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
@@ -3392,7 +3392,7 @@
dependencies:
"@typescript-eslint/utils" "5.62.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.5.0":
"@typescript-eslint/parser@^5.5.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz"
integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
@@ -3627,16 +3627,16 @@ acorn-walk@^7.1.1:
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.0, acorn@^8.2.4, acorn@^8.8.2, acorn@^8.9.0:
version "8.14.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
acorn@^7.1.1:
version "7.4.1"
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.14.0, acorn@^8.2.4, acorn@^8.8.2, acorn@^8.9.0:
version "8.14.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
address@^1.0.1, address@^1.1.2:
version "1.2.2"
resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz"
@@ -3681,7 +3681,7 @@ ajv-keywords@^5.1.0:
dependencies:
fast-deep-equal "^3.1.3"
ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1:
ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -3701,7 +3701,7 @@ ajv@^8.0.0:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
ajv@^8.6.0, ajv@>=8:
ajv@^8.6.0:
version "8.17.1"
resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
@@ -3711,7 +3711,7 @@ ajv@^8.6.0, ajv@>=8:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
ajv@^8.8.2, ajv@^8.9.0:
ajv@^8.9.0:
version "8.17.1"
resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
@@ -4396,7 +4396,7 @@ browserify-sign@^4.2.3:
readable-stream "^2.3.8"
safe-buffer "^5.2.1"
browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.25.0, "browserslist@>= 4", "browserslist@>= 4.21.0", browserslist@>=4:
browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.25.0:
version "4.25.1"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz"
integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==
@@ -5175,7 +5175,7 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
csstype@^3.0.10, csstype@^3.0.2, csstype@^3.1.3:
csstype@^3.0.2, csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@@ -5221,14 +5221,14 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
"date-fns@^2.25.0 || ^3.2.0", date-fns@^2.28.0, date-fns@^2.30.0, "date-fns@>= 2.x":
date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@^1.10.7, dayjs@^1.11.10, dayjs@^1.11.11, "dayjs@>= 1.x":
dayjs@^1.11.10, dayjs@^1.11.11:
version "1.11.13"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
@@ -6042,7 +6042,7 @@ eslint-webpack-plugin@^3.1.1:
normalize-path "^3.0.0"
schema-utils "^4.0.0"
eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", "eslint@^7.5.0 || ^8.0.0", eslint@^8.0.0, eslint@^8.1.0, eslint@^8.3.0, eslint@^8.38.0, "eslint@>= 6", eslint@>=7.0.0, eslint@>=7.28.0:
eslint@^8.3.0, eslint@^8.38.0:
version "8.57.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@@ -7752,7 +7752,7 @@ jest-resolve-dependencies@^27.5.1:
jest-regex-util "^27.5.1"
jest-snapshot "^27.5.1"
jest-resolve@*, jest-resolve@^27.4.2, jest-resolve@^27.5.1:
jest-resolve@^27.4.2, jest-resolve@^27.5.1:
version "27.5.1"
resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz"
integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==
@@ -7962,7 +7962,7 @@ jest-worker@^28.0.2:
merge-stream "^2.0.0"
supports-color "^8.0.0"
"jest@^27.0.0 || ^28.0.0", jest@^27.4.3:
jest@^27.4.3:
version "27.5.1"
resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz"
integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==
@@ -8206,7 +8206,7 @@ launch-editor@^2.6.0:
picocolors "^1.0.0"
shell-quote "^1.8.1"
leaflet@^1.9.0, leaflet@^1.9.4:
leaflet@^1.9.4:
version "1.9.4"
resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz"
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
@@ -9645,15 +9645,6 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
"postcss@^7.0.0 || ^8.0.1", postcss@^8, postcss@^8.0.0, postcss@^8.0.3, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.1.4, postcss@^8.2, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3, postcss@^8.3.5, postcss@^8.4, postcss@^8.4.21, postcss@^8.4.33, postcss@^8.4.4, postcss@^8.4.47, postcss@^8.4.6, "postcss@>= 8", postcss@>=8, postcss@>=8.0.9:
version "8.5.3"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz"
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
dependencies:
nanoid "^3.3.8"
picocolors "^1.1.1"
source-map-js "^1.2.1"
postcss@^7.0.35:
version "7.0.39"
resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz"
@@ -9662,6 +9653,15 @@ postcss@^7.0.35:
picocolors "^0.2.1"
source-map "^0.6.1"
postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.4, postcss@^8.4.47:
version "8.5.3"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz"
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
dependencies:
nanoid "^3.3.8"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
@@ -9679,7 +9679,7 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^2.8.7, prettier@>=2.0.0:
prettier@^2.8.7:
version "2.8.8"
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
@@ -10299,7 +10299,7 @@ react-dnd@^16.0.1:
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@*, "react-dom@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8 || ^17 || ^18 || ^19", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", react-dom@^17.0.2, react-dom@^18.0.0, react-dom@^18.2.0, "react-dom@>= 0.14.0", react-dom@>=16.0.0, react-dom@>=16.11.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0, react-dom@>=16.9.0, "react-dom@16.2.0 - 18":
react-dom@^18.2.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@@ -10395,7 +10395,7 @@ react-loading-icons@^1.1.0:
resolved "https://registry.npmjs.org/react-loading-icons/-/react-loading-icons-1.1.0.tgz"
integrity sha512-Y9eZ6HAufmUd8DIQd6rFrx5Bt/oDlTM9Nsjvf8YpajTa3dI8cLNU8jUN5z7KTANU+Yd6/KJuBjxVlrU2dMw33g==
"react-redux@^7.2.1 || ^8.0.2", react-redux@^8.0.5:
react-redux@^8.0.5:
version "8.1.3"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz"
integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==
@@ -10407,7 +10407,7 @@ react-loading-icons@^1.1.0:
react-is "^18.0.0"
use-sync-external-store "^1.0.0"
react-refresh@^0.11.0, "react-refresh@>=0.10.0 <1.0.0":
react-refresh@^0.11.0:
version "0.11.0"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
@@ -10427,7 +10427,7 @@ react-router@^6.10.0, react-router@6.29.0:
dependencies:
"@remix-run/router" "1.22.0"
react-scripts@^5.0.1, react-scripts@>=2.1.3:
react-scripts@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz"
integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==
@@ -10502,7 +10502,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@*, "react@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.6.0 || 17 || 18", "react@^16.8 || ^17 || ^18 || ^19", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.9.0 || ^17.0.0 || ^18", "react@^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", react@^17.0.2, react@^18.0.0, react@^18.2.0, react@^18.3.1, "react@>= 0.14.0", "react@>= 16", "react@>= 16.14", react@>=16.0.0, react@>=16.11.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.9.0, "react@16.2.0 - 18":
react@^18.2.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@@ -10571,7 +10571,7 @@ redux-thunk@^2.4.2:
resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
redux@^4, "redux@^4 || ^5.0.0-beta.0", redux@^4.2.0, redux@^4.2.1:
redux@^4.2.0, redux@^4.2.1:
version "4.2.1"
resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
@@ -10792,7 +10792,7 @@ rollup-plugin-terser@^7.0.0:
serialize-javascript "^4.0.0"
terser "^5.0.0"
"rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.0.0, rollup@^2.43.1:
rollup@^2.43.1:
version "2.79.2"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz"
integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
@@ -11554,7 +11554,7 @@ stylis-plugin-rtl@^2.1.1:
dependencies:
cssjanus "^2.0.1"
stylis@^4.3.4, stylis@4.x:
stylis@^4.3.4:
version "4.3.6"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz"
integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
@@ -11930,7 +11930,7 @@ type-fest@^0.20.2:
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.21.3, "type-fest@>=0.17.0 <4.0.0":
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
@@ -12000,11 +12000,6 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
"typescript@^3.2.1 || ^4", "typescript@^4.7 || 5", "typescript@>= 2.7", "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=4.9.5:
version "4.9.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
ua-parser-js@^1.0.33:
version "1.0.40"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz"
@@ -12245,7 +12240,7 @@ webpack-dev-middleware@^5.3.4:
range-parser "^1.2.1"
schema-utils "^4.0.0"
webpack-dev-server@^4.6.0, "webpack-dev-server@3.x || 4.x":
webpack-dev-server@^4.6.0:
version "4.15.2"
resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz"
integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==
@@ -12310,7 +12305,7 @@ webpack-sources@^3.2.3:
resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", "webpack@^4.4.0 || ^5.9.0", "webpack@^4.44.2 || ^5.47.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.64.4, "webpack@>= 4", webpack@>=2, "webpack@>=4.43.0 <6.0.0":
webpack@^5.64.4:
version "5.98.0"
resolved "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz"
integrity sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==