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
This commit is contained in:
7
.env
7
.env
@@ -6,9 +6,10 @@ REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
|||||||
|
|
||||||
## Google Map Key
|
## Google Map Key
|
||||||
|
|
||||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
REACT_APP_URL=https://api.doormile.com/api/v1
|
||||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
REACT_APP_URL2=https://api.doormile.com/api/v1
|
||||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
REACT_APP_URL3=https://api.doormile.com/api/v1
|
||||||
|
REACT_APP_INTERNAL_KEY=doormile-internal-2024
|
||||||
REACT_APP_STAFF_TOKEN=
|
REACT_APP_STAFF_TOKEN=
|
||||||
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
||||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
REACT_APP_URL=https://api.doormile.com/api/v1
|
||||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
REACT_APP_URL2=https://api.doormile.com/api/v1
|
||||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
REACT_APP_URL3=https://api.doormile.com/api/v1
|
||||||
|
REACT_APP_INTERNAL_KEY=doormile-internal-2024
|
||||||
REACT_APP_STAFF_TOKEN=
|
REACT_APP_STAFF_TOKEN=
|
||||||
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
||||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
REACT_APP_URL=https://api.doormile.com/api/v1
|
||||||
REACT_APP_URL2=
|
REACT_APP_URL2=https://api.doormile.com/api/v1
|
||||||
|
REACT_APP_INTERNAL_KEY=doormile-internal-2024
|
||||||
REACT_APP_STAFF_TOKEN=
|
REACT_APP_STAFF_TOKEN=
|
||||||
|
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -104,3 +104,11 @@ dist
|
|||||||
|
|
||||||
# wincompare file
|
# wincompare file
|
||||||
*.bak
|
*.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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
|
REACT_APP_URL='https://api.doormile.com/api/v1'
|
||||||
REACT_APP_URL2=''
|
REACT_APP_URL2='https://api.doormile.com/api/v1'
|
||||||
|
REACT_APP_INTERNAL_KEY='doormile-internal-2024'
|
||||||
REACT_APP_STAFF_TOKEN=''
|
REACT_APP_STAFF_TOKEN=''
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import logger from './utils/logger';
|
import logger from './utils/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -19,6 +20,11 @@ import { store } from 'store';
|
|||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
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 container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
|
|||||||
@@ -4,82 +4,96 @@ import dayjs from 'dayjs';
|
|||||||
const userid = localStorage.getItem('userid');
|
const userid = localStorage.getItem('userid');
|
||||||
|
|
||||||
// ==============================|| getRiderPeriodicLogs ||============================== //
|
// ==============================|| getRiderPeriodicLogs ||============================== //
|
||||||
// Returns the rider's latest periodic log entry — battery, GPS, status, current
|
// Returns the miler's latest known position/status. Doormile has no periodic-log
|
||||||
// order. Used by the Rider Info modal on the Dispatch page.
|
// 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) => {
|
export const getRiderPeriodicLogs = async (userid) => {
|
||||||
const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`;
|
if (!userid) return null;
|
||||||
const response = await axios.get(url);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${userid}`);
|
||||||
if (response.data && response.data.status) return response.data.data;
|
const m = response.data?.data;
|
||||||
return null;
|
if (!m) return null;
|
||||||
|
return {
|
||||||
|
userid: m.userid,
|
||||||
|
lat: m.currentlat,
|
||||||
|
lon: m.currentlon,
|
||||||
|
status: m.availabilitystatus,
|
||||||
|
battery: null,
|
||||||
|
speed: null
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchAppLocations||============================== //
|
// ==============================|| fetchAppLocations||============================== //
|
||||||
|
|
||||||
export const fetchAppLocations = async () => {
|
export const fetchAppLocations = async () => {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
|
||||||
const updatedLocations = [
|
const hubs = (response.data?.data || []).map((h) => ({
|
||||||
...response.data.details,
|
...h,
|
||||||
{ locationname: 'All', applocationid: 0 } // Add your new object here
|
applocationid: h.hubid,
|
||||||
];
|
locationname: h.hubname
|
||||||
|
}));
|
||||||
return updatedLocations;
|
return [...hubs, { locationname: 'All', applocationid: 0 }];
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchPercentageData (orders) ||============================== //
|
// ==============================|| fetchPercentageData (orders) ||============================== //
|
||||||
|
|
||||||
export const fetchPercentageData = async ({ queryKey }) => {
|
export const fetchPercentageData = async ({ queryKey }) => {
|
||||||
const [, appId, startdate, enddate, tenantid, locationid] = queryKey;
|
const [, appId, startdate, enddate, tenantid, locationid] = queryKey;
|
||||||
const response = await axios.get(
|
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Delivered', 'Cancelled'];
|
||||||
`${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
|
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 {
|
return {
|
||||||
created: details.created.toString(),
|
created: created.toString(),
|
||||||
uncoveredOrders: details.pending.toString(),
|
uncoveredOrders: pending.data.total.toString(),
|
||||||
coveredOrders: details.delivered.toString(),
|
coveredOrders: delivered.data.total.toString(),
|
||||||
cancelled: details.cancelled.toString(),
|
cancelled: cancelled.data.total.toString(),
|
||||||
percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(),
|
percentage1: (Math.round((created / created) * 100) || 0).toString(),
|
||||||
percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(),
|
percentage2: (Math.round((pending.data.total / created) * 100) || 0).toString(),
|
||||||
percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(),
|
percentage3: (Math.round((delivered.data.total / created) * 100) || 0).toString(),
|
||||||
percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString()
|
percentage4: (Math.round((cancelled.data.total / created) * 100) || 0).toString()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===================================================== || getTenants || =====================================================
|
// ===================================================== || getTenants || =====================================================
|
||||||
|
// appId (hub/zone) has no equivalent filter on Doormile's CRM clients endpoint —
|
||||||
export const getTenants = async (appId) => {
|
// kept as a parameter only so existing call sites (deliveries.js, orders.js,
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`);
|
// reports/*) don't need to change their queryFn wiring.
|
||||||
if (response.data.status) {
|
export const getTenants = async () => {
|
||||||
let arr = [];
|
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
|
||||||
response.data.details.map((val) => {
|
return (response.data?.data || []).map((val) => ({
|
||||||
arr.push({
|
...val,
|
||||||
...val,
|
tenantid: val.clientid,
|
||||||
label: `${val.tenantname}`
|
tenantname: val.clientname,
|
||||||
});
|
label: val.clientname
|
||||||
});
|
}));
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// ============================================= || gettenantlocations (branches) || =============================================
|
// ============================================= || gettenantlocations (branches) || =============================================
|
||||||
export const gettenantlocations = async (appId) => {
|
// No Doormile equivalent to tenant branches — call sites use this for a
|
||||||
try {
|
// branch/location dropdown that doesn't apply to Doormile's client model.
|
||||||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${appId}`);
|
export const gettenantlocations = async () => {
|
||||||
return res.data.details;
|
return [];
|
||||||
} catch (err) {
|
|
||||||
console.log('gettenantlocations', err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// ==============================|| fetchorderscount (orders) ||============================== //
|
// ==============================|| fetchorderscount (orders) ||============================== //
|
||||||
|
|
||||||
export const fetchorderscount = async ({ queryKey }) => {
|
export const fetchorderscount = async ({ queryKey }) => {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
|
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 {
|
||||||
|
created: all.data.total,
|
||||||
return response.data.details;
|
pending: pending.data.total,
|
||||||
|
delivered: delivered.data.total,
|
||||||
|
cancelled: cancelled.data.total
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchOrders (orders) ||============================== //
|
// ==============================|| fetchOrders (orders) ||============================== //
|
||||||
@@ -95,15 +109,21 @@ export const fetchorderscount = async ({ queryKey }) => {
|
|||||||
// return response.data.details.map((val, i) => ({ ...val, sno: i + 1 }));
|
// return response.data.details.map((val, i) => ({ ...val, sno: i + 1 }));
|
||||||
// };
|
// };
|
||||||
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
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 [, 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: {
|
||||||
const response = await axios.get(url);
|
status: currentStatus === 'All' ? undefined : currentStatus,
|
||||||
|
keyword: debouncedSearch,
|
||||||
|
pageno: pageParam,
|
||||||
|
pagesize: rowsPerPage
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: response.data.details,
|
rows: response.data.data,
|
||||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
nextPage: response.data.data.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,6 +157,46 @@ export const fetchRidersList = async ({ queryKey }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==============================|| 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 ||============================== //
|
// ==============================|| createOptimisationDeliveries (orders) Arrange the order ||============================== //
|
||||||
|
|
||||||
export const createOptimisationDeliveries = async (deliveryData) => {
|
export const createOptimisationDeliveries = async (deliveryData) => {
|
||||||
@@ -145,14 +205,9 @@ export const createOptimisationDeliveries = async (deliveryData) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
|
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
|
||||||
|
// No Doormile equivalent — made a no-op so CLAUDE.md's "always reconcile before
|
||||||
export const reconcileSteps = async ({ riders }) => {
|
// createdeliveries" hard constraint keeps holding trivially until Phase 3.
|
||||||
const response = await axios.post(
|
export const reconcileSteps = async (data) => data;
|
||||||
`https://routes.workolik.com/api/v1/optimization/reconcile-steps`,
|
|
||||||
{ riders }
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== //
|
// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== //
|
||||||
// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }.
|
// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }.
|
||||||
@@ -223,24 +278,9 @@ export const createAutomationDeliveries = async (variables) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| notifyRider (orders / deliveries) ||============================== //
|
// ==============================|| notifyRider (orders / deliveries) ||============================== //
|
||||||
|
// Doormile sends miler FCM notifications automatically from the Go backend on
|
||||||
export const notifyRider = async (riderToken) => {
|
// status changes, so there's no equivalent client-triggered endpoint. No-op.
|
||||||
if (!riderToken) {
|
export const notifyRider = async () => ({ success: true });
|
||||||
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: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==============================|| cancelOrder (orders) ||============================== //
|
// ==============================|| cancelOrder (orders) ||============================== //
|
||||||
|
|
||||||
@@ -270,18 +310,29 @@ export const cancelMultipleOrder = async (orderlist) => {
|
|||||||
};
|
};
|
||||||
// ==============================|| fetchDeliveries (deliveries) ||============================== //
|
// ==============================|| 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 }) => {
|
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;
|
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
|
||||||
currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
|
|
||||||
const url =
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
|
||||||
appId === 0
|
params: {
|
||||||
? `${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}`
|
status: currentStatus === 'all' || currentStatus === 'All' ? undefined : currentStatus,
|
||||||
: `${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}`;
|
keyword: searchword,
|
||||||
const response = await axios.get(url);
|
pageno: pageParam,
|
||||||
|
pagesize: rowsPerPage
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: response.data.details,
|
rows: response.data.data,
|
||||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
nextPage: response.data.data.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -370,32 +421,46 @@ export const updateDeliveryAPI = async (orderData) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| getalltenants (tenants) ||============================== //
|
// ==============================|| 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 }) => {
|
export const getalltenants = async ({ queryKey }) => {
|
||||||
const [, appId, debouncedSearch, status, page, rowsPerPage] = queryKey;
|
const [, , debouncedSearch, status, page, rowsPerPage] = queryKey;
|
||||||
try {
|
try {
|
||||||
let url = `${process.env.REACT_APP_URL
|
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
|
||||||
}/tenants/getalltenants/?status=${status}&applocationid=${appId}&keyword=${debouncedSearch}&pageno=${page + 1
|
let clients = (response.data?.data || []).map((c) => ({
|
||||||
}&pagesize=${rowsPerPage}&moduleid=6`;
|
...c,
|
||||||
const response = await axios.get(url);
|
tenantid: c.clientid,
|
||||||
return response.data.details; // return only data, keep it clean
|
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) {
|
} catch (err) {
|
||||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||||
OpenToast(message);
|
OpenToast(message);
|
||||||
return null; // return null for failure
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// ==============================|| gettenantsummary (tenants) ||============================== //
|
// ==============================|| gettenantsummary (tenants) ||============================== //
|
||||||
|
export const gettenantsummary = async () => {
|
||||||
export const gettenantsummary = async ({ queryKey }) => {
|
|
||||||
const [, appId] = queryKey;
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
|
||||||
return response.data.summary; // return only data, keep it clean
|
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) {
|
} catch (err) {
|
||||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||||
OpenToast(message);
|
OpenToast(message);
|
||||||
return null; // return null for failure
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// ==============================|| getpricinglist (tenants) ||============================== //
|
// ==============================|| getpricinglist (tenants) ||============================== //
|
||||||
@@ -467,55 +532,58 @@ export const getallcustomers = async ({ pageParam = 1, queryKey }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchAllRiders (riders) ||============================== //
|
// ==============================|| 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 {
|
try {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const [, appId, debouncedSearch, tabvalue] = queryKey;
|
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
|
if (debouncedSearch) {
|
||||||
}/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${(tabvalue == 0 || tabvalue == 2) ? '' : 'Active'
|
const kw = debouncedSearch.toLowerCase();
|
||||||
}`;
|
milers = milers.filter(
|
||||||
const res = await axios.get(url);
|
(m) => (m.displayname || '').toLowerCase().includes(kw) || (m.phone || '').includes(debouncedSearch)
|
||||||
return {
|
);
|
||||||
details: res.data.details,
|
}
|
||||||
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
|
if (tabvalue != 0 && tabvalue != 2) {
|
||||||
};
|
milers = milers.filter((m) => m.availabilitystatus === 'Available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { details: milers, nextPage: undefined };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('fetchAllRiders err', err.message);
|
console.log('fetchAllRiders err', err.message);
|
||||||
return [];
|
return { details: [], nextPage: undefined };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// ==============================|| getallridersummary (riders) ||============================== //
|
// ==============================|| getallridersummary (riders) ||============================== //
|
||||||
export const getallridersummary = async ({ queryKey }) => {
|
export const getallridersummary = async () => {
|
||||||
try {
|
try {
|
||||||
const [, appId, tabvalue] = queryKey;
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||||
const response = await axios.get(
|
const milers = response.data?.data || [];
|
||||||
`${process.env.REACT_APP_URL}/partners/getallridersummary/?applocationid=${appId}&status=${tabvalue == 0 ? '' : 'Active'}`
|
const active = milers.filter((m) => m.availabilitystatus === 'Available').length;
|
||||||
);
|
return { total: milers.length, active, inactive: milers.length - active };
|
||||||
return response.data.details;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('getallridersummary err', err.message);
|
console.log('getallridersummary err', err.message);
|
||||||
return [];
|
return { total: 0, active: 0, inactive: 0 };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchRiders (riders), active riders ||============================== //
|
// ==============================|| 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 }) => {
|
// ==============================|| fetchMilerDetail (riders) ||============================== //
|
||||||
try {
|
export const fetchMilerDetail = async (milerid) => {
|
||||||
const [, appId, debouncedSearch] = queryKey;
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${milerid}`);
|
||||||
|
return response.data.data;
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| getriderstatus (riders)||============================== //
|
// ==============================|| getriderstatus (riders)||============================== //
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1079,7 +1079,7 @@ const Dispatch = ({
|
|||||||
// • no batch is selected,
|
// • no batch is selected,
|
||||||
// • the previous request is still in flight (prevents queue
|
// • the previous request is still in flight (prevents queue
|
||||||
// stacking on slow networks),
|
// 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
|
// Loading state is tracked through a ref so the interval doesn't
|
||||||
// reset on every in-flight flip. ─────────────────────────────────
|
// reset on every in-flight flip. ─────────────────────────────────
|
||||||
const ANALYSIS_POLL_MS = 15000;
|
const ANALYSIS_POLL_MS = 15000;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// UNUSED — login1.js is the active login page (see routes/MainRoutes.js). This
|
||||||
|
// file still targets the old jupiter.nearle.app console-login flow.
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import AnimateButton from 'components/@extended/AnimateButton';
|
import AnimateButton from 'components/@extended/AnimateButton';
|
||||||
|
|||||||
@@ -28,17 +28,17 @@ const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%)
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
// import { openSnackbar } from 'store/reducers/snackbar';
|
// import { openSnackbar } from 'store/reducers/snackbar';
|
||||||
// import { useDispatch } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const fcmtoken = useSelector((state) => state.fcm);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [, setAlertmessage] = useState('');
|
const [, setAlertmessage] = useState('');
|
||||||
const [checkusername, setCheckusername] = useState(false);
|
|
||||||
// const [toast, setToast] = useState(false);
|
// const [toast, setToast] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
@@ -76,116 +76,48 @@ const Login = () => {
|
|||||||
// console.log(alertmessage)
|
// 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(() => {
|
|
||||||
setCheckusername(true);
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loginsend = async () => {
|
const loginsend = async () => {
|
||||||
// e.preventDefault();
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
if (password && username) {
|
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');
|
|
||||||
setAlertmessage('Fill All required fields');
|
setAlertmessage('Fill All required fields');
|
||||||
opentoast('Fill All required fields');
|
opentoast('Fill All required fields');
|
||||||
setLoading(false);
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,8 +248,8 @@ const Login = () => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
onChange={usernamecheck}
|
value={username}
|
||||||
error={checkusername}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,235 +1,78 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
|
|
||||||
import { Box, Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
|
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
|
// project import
|
||||||
import MainCard from 'components/MainCard';
|
import MainCard from 'components/MainCard';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
// assets
|
|
||||||
import { usePlacesWidget } from 'react-google-autocomplete';
|
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import Geocode from 'react-geocode';
|
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { fetchAppLocations } from 'pages/api/api';
|
||||||
|
|
||||||
const Createrider = () => {
|
const Createrider = () => {
|
||||||
const [mobilenumber, setMobilenumber] = useState('');
|
const [displayname, setDisplayname] = useState('');
|
||||||
const [emailaddress, setEmailaddress] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [city, setCity] = useState('');
|
const [hubid, setHubid] = useState('');
|
||||||
const [zipcode, setZipcode] = useState('');
|
const [hubs, setHubs] = 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();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
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);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
fetchAppLocations().then((locations) => {
|
||||||
// fetchprofiledetails(181);
|
setHubs((locations || []).filter((l) => l.applocationid !== 0));
|
||||||
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 opentoast = (message) => {
|
const opentoast = (message) => {
|
||||||
enqueueSnackbar(message, {
|
enqueueSnackbar(message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
autoHideDuration: 2000
|
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);
|
setLoading(true);
|
||||||
await axios
|
try {
|
||||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/milers`, {
|
||||||
.then((res) => {
|
displayname,
|
||||||
console.log(res);
|
phone,
|
||||||
if (res.data.status) {
|
hubid,
|
||||||
setTenantinfo(res.data.details);
|
availabilitystatus: 'Available'
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
};
|
if (res.data?.success) {
|
||||||
|
enqueueSnackbar('Miler created successfully', {
|
||||||
useEffect(() => {
|
variant: 'success',
|
||||||
if (selectedImage) {
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
setAvatar(URL.createObjectURL(selectedImage));
|
autoHideDuration: 2000
|
||||||
}
|
});
|
||||||
}, [selectedImage]);
|
navigate('/nearle/riders');
|
||||||
|
} else {
|
||||||
const { ref: materialRef } = usePlacesWidget({
|
opentoast(res.data?.message || 'Failed to create miler.');
|
||||||
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 (!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);
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
opentoast(err.response?.data?.message || err.message || 'Failed to create miler.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,205 +81,87 @@ const Createrider = () => {
|
|||||||
{loading && <Loader />}
|
{loading && <Loader />}
|
||||||
|
|
||||||
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
|
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||||
<Stack
|
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={1}>
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
<Typography variant="h3">Create Miler</Typography>
|
||||||
justifyContent="space-between"
|
</Stack>
|
||||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
</Grid>
|
||||||
spacing={1}
|
<MainCard>
|
||||||
>
|
<Grid container spacing={3}>
|
||||||
<Typography variant="h3">Create Rider</Typography>
|
<Grid item xs={12}>
|
||||||
</Stack>
|
<MainCard sx={{ height: '100%' }}>
|
||||||
</Grid>
|
<Grid container spacing={3}>
|
||||||
<MainCard>
|
<Grid item xs={12} sm={6}>
|
||||||
<Grid container spacing={3}>
|
<Stack spacing={1.25}>
|
||||||
<Grid item xs={12}>
|
<InputLabel htmlFor="miler-name">Name</InputLabel>
|
||||||
<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>
|
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
|
||||||
id="personal-phone"
|
|
||||||
// format="##########"
|
|
||||||
// mask="_"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
// customInput={TextField}
|
id="miler-name"
|
||||||
placeholder="Phone Number"
|
placeholder="Name"
|
||||||
// defaultValue="8654239581"
|
onChange={(e) => setDisplayname(e.target.value)}
|
||||||
// onBlur={() => { }}
|
value={displayname}
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value.toString().length <= 10) {
|
|
||||||
setMobilenumber(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={mobilenumber}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
// disabled
|
|
||||||
sx={{ cursor: 'not-allowed' }}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</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>
|
||||||
<Grid item xs={12} sm={6}>
|
</MainCard>
|
||||||
<Stack spacing={1.25}>
|
</Grid>
|
||||||
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
|
<Grid item xs={12}>
|
||||||
<TextField
|
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="flex-end" alignItems={{ xs: 'stretch', sm: 'center' }} spacing={2}>
|
||||||
type="email"
|
<Button variant="contained" onClick={() => createMiler()} fullWidth={isMobile}>
|
||||||
fullWidth
|
Create
|
||||||
// defaultValue="stebin.ben@gmail.com"
|
</Button>
|
||||||
id="personal-email"
|
</Stack>
|
||||||
placeholder="Email Address"
|
</Grid>
|
||||||
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>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
</MainCard>
|
||||||
<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>
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,12 @@ const axiosServices = axios.create({ baseURL: process.env.REACT_APP_API_URL || '
|
|||||||
|
|
||||||
// ==============================|| AXIOS - FOR MOCK SERVICES ||============================== //
|
// ==============================|| 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(
|
axiosServices.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
"others": "Others",
|
"others": "Others",
|
||||||
"roadmap": "Roadmap",
|
"roadmap": "Roadmap",
|
||||||
"MENU": "Menu",
|
"MENU": "Menu",
|
||||||
"orders": "Orders",
|
"orders": "Bookings",
|
||||||
"orderspreview": "Orders Preview",
|
"orderspreview": "Orders Preview",
|
||||||
"deliveries": "Deliveries",
|
"deliveries": "Consignments",
|
||||||
"tenants": "Tenants",
|
"tenants": "Clients",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"customers": "Customers",
|
"customers": "Customers",
|
||||||
"riders": "Riders",
|
"riders": "Milers",
|
||||||
"reports": "Reports",
|
"reports": "Reports",
|
||||||
"ordersummary": "Orders Summary",
|
"ordersummary": "Orders Summary",
|
||||||
"ordersdetails": "Orders Details",
|
"ordersdetails": "Orders Details",
|
||||||
"riderssummary": "Riders Summary",
|
"riderssummary": "Riders Summary",
|
||||||
"riderslogs": "Riders Logs",
|
"riderslogs": "Riders Logs",
|
||||||
"invoice": "Invoice",
|
"invoice": "Invoice",
|
||||||
"dispatch": "Dispatch",
|
"dispatch": "Live Operations",
|
||||||
"profitability": "Profitability",
|
"profitability": "Profitability",
|
||||||
"Doormile": "Doormile"
|
"Doormile": "Doormile"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user