Migrate console off jupiter.nearle.app to the Doormile Express API

Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across
every page (orders, deliveries, riders, tenants, pricing, profile, reports,
dispatch). Fixes several field-mapping and envelope-check bugs found along
the way, most notably that /admin/milers/:id routes (block, assign-vehicle,
edit, notify) key off milerprofileid, not userid, and that a miler's real
fields are phone/availabilitystatus/displayname, not contactno/status/
firstname+lastname (confirmed against a live read-only session).

Also fixes several silent-failure bugs uncovered during that audit: order
creation and order cancellation showed a success toast but gave no feedback
at all on failure (createorder1.js had a dead notifyadmin() call that left
the loading spinner stuck forever on every failed submit), and Tenants.js's
pricing/profile updates never surfaced a failed response to the operator.

The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and
its jupiter.nearle.app delivery-commit call remain untouched by design —
separate solver service with no equivalent in the new API.
This commit is contained in:
2026-08-06 19:26:23 +05:30
parent 0a36212ac8
commit 59f31c8adf
39 changed files with 7006 additions and 9170 deletions

View File

@@ -1,22 +1,14 @@
import { Avatar, Box, Chip, Grid, Paper, Stack, Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useQuery } from '@tanstack/react-query';
import {
MdBadge,
MdPerson,
MdLocationOn,
MdMail,
MdPhone,
MdPlace,
MdMyLocation,
MdLocationCity,
MdMap,
MdMarkunreadMailbox,
MdVerifiedUser
} from 'react-icons/md';
import { useState } from 'react';
import { Avatar, Box, Button, Chip, Grid, InputLabel, Paper, Stack, TextField, Typography } from '@mui/material';
import { useMutation, useQuery } from '@tanstack/react-query';
import { MdBadge, MdPerson, MdLocationOn, MdMail, MdShield, MdVerifiedUser, MdLock } from 'react-icons/md';
import CircularLoader from 'components/CircularLoader';
import Loader from 'components/Loader';
import { getusers } from 'pages/api/api';
import { updateProfilePassword } from 'pages/api/doormileApi';
import { enqueueSnackbar } from 'notistack';
const ROLE_LABELS = { 1: 'Admin', 3: 'Manager', 4: 'Executive' };
// ---- shared design tokens (mirrors the DT block used across the console) ----
const DT = {
@@ -99,14 +91,72 @@ const SectionCard = ({ title, icon: Icon, children }) => (
);
const ViewProfile = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const { data: userData, isLoading } = useQuery({
queryKey: ['getuser'],
queryFn: getusers
});
const fullname = userData?.fullname || userData?.firstname || 'User Profile';
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const changePasswordMutation = useMutation({
mutationFn: () => updateProfilePassword(currentPassword, newPassword),
onSuccess: (res) => {
if (res.success) {
enqueueSnackbar('Password updated successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} else {
enqueueSnackbar(res.message || 'Failed to update password', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
},
onError: (err) => {
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to update password', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
});
const handleChangePassword = () => {
if (!currentPassword || !newPassword) {
enqueueSnackbar('Fill current and new password', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
return;
}
if (newPassword !== confirmPassword) {
enqueueSnackbar('New password and confirmation do not match', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
return;
}
changePasswordMutation.mutate();
};
// GET /admin/profile has no documented schema, but the console's own login
// response ({ success, token, user: { id, name, email, role, tenantid } })
// is confirmed — /admin/profile almost certainly returns the same "user"
// shape. Fields like address/suburb/city/postcode were jupiter-era guesses
// with no basis in the new API and have been dropped rather than shown blank.
const fullname = userData?.name || userData?.fullname || 'User Profile';
const roleLabel = ROLE_LABELS[userData?.role] || ROLE_LABELS[userData?.roleid] || userData?.role || '—';
const tenantLabel = userData?.tenantid ? `Tenant #${userData.tenantid}` : 'Doormile staff (all tenants)';
return (
<>
@@ -153,11 +203,11 @@ const ViewProfile = () => {
{fullname}
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap alignItems="center">
{userData?.authname && (
{roleLabel && (
<Chip
size="small"
icon={<MdVerifiedUser size={14} style={{ color: BRAND }} />}
label={userData.authname}
label={roleLabel}
sx={{
bgcolor: soft(BRAND),
color: BRAND,
@@ -167,19 +217,17 @@ const ViewProfile = () => {
}}
/>
)}
{userData?.userid != null && userData?.userid !== '' && (
{(userData?.id != null || userData?.userid != null) && (
<Typography variant="body2" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
ID&nbsp;#{userData.userid}
ID&nbsp;#{userData.id ?? userData.userid}
</Typography>
)}
{userData?.applocation && (
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
<MdLocationOn size={15} />
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{userData.applocation}
</Typography>
</Stack>
)}
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
<MdLocationOn size={15} />
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{tenantLabel}
</Typography>
</Stack>
</Stack>
</Box>
</Stack>
@@ -190,42 +238,66 @@ const ViewProfile = () => {
<Grid item xs={12} md={6}>
<SectionCard title="Account" icon={MdBadge}>
<Stack spacing={2.5}>
<InfoField icon={MdPerson} label="User Name" value={userData?.fullname} />
<InfoField icon={MdBadge} label="User ID" value={userData?.userid} />
<InfoField icon={MdVerifiedUser} label="Auth Name" value={userData?.authname} />
<InfoField icon={MdLocationOn} label="App Location" value={userData?.applocation} />
<InfoField icon={MdPerson} label="User Name" value={userData?.name || userData?.fullname} />
<InfoField icon={MdBadge} label="User ID" value={userData?.id ?? userData?.userid} />
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
<InfoField icon={MdVerifiedUser} label="Role" value={roleLabel} />
<InfoField icon={MdLocationOn} label="Tenant" value={tenantLabel} accent="#14b8a6" />
</Stack>
</SectionCard>
</Grid>
<Grid item xs={12} md={6}>
<SectionCard title="Contact" icon={MdMail}>
<Stack spacing={2.5}>
<InfoField icon={MdPhone} label="Contact No" value={userData?.contactno} accent="#0ea5e9" />
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
<InfoField icon={MdPlace} label="Address" value={userData?.address} accent="#0ea5e9" />
<SectionCard title="Security" icon={MdShield}>
<Stack spacing={2}>
<Stack spacing={1}>
<InputLabel htmlFor="current-password">Current Password</InputLabel>
<TextField
id="current-password"
type="password"
fullWidth
placeholder="Current password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="off"
/>
</Stack>
<Stack spacing={1}>
<InputLabel htmlFor="new-password">New Password</InputLabel>
<TextField
id="new-password"
type="password"
fullWidth
placeholder="New password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="off"
/>
</Stack>
<Stack spacing={1}>
<InputLabel htmlFor="confirm-password">Confirm New Password</InputLabel>
<TextField
id="confirm-password"
type="password"
fullWidth
placeholder="Confirm new password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
autoComplete="off"
/>
</Stack>
<Button
variant="contained"
startIcon={<MdLock size={16} />}
onClick={handleChangePassword}
disabled={changePasswordMutation.isPending}
sx={{ alignSelf: 'flex-start' }}
>
Update Password
</Button>
</Stack>
</SectionCard>
</Grid>
<Grid item xs={12}>
<SectionCard title="Location" icon={MdMap}>
<Grid container spacing={isMobile ? 2.5 : 3}>
<Grid item xs={12} sm={6} md={3}>
<InfoField icon={MdMyLocation} label="Suburb" value={userData?.suburb} accent="#14b8a6" />
</Grid>
<Grid item xs={12} sm={6} md={3}>
<InfoField icon={MdLocationCity} label="City" value={userData?.city} accent="#14b8a6" />
</Grid>
<Grid item xs={12} sm={6} md={3}>
<InfoField icon={MdMap} label="State" value={userData?.state} accent="#14b8a6" />
</Grid>
<Grid item xs={12} sm={6} md={3}>
<InfoField icon={MdMarkunreadMailbox} label="Postcode" value={userData?.postcode} accent="#14b8a6" />
</Grid>
</Grid>
</SectionCard>
</Grid>
</Grid>
</Box>
</>