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:
@@ -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 #{userData.userid}
|
||||
ID #{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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user