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,15 +1,13 @@
import { useState, useEffect } from 'react';
import { enqueueSnackbar } from 'notistack';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import Loader from 'components/Loader';
import logo from 'assets/images/doormile-logo.png';
import { useSelector, useDispatch } from 'react-redux';
import { useDispatch } from 'react-redux';
import { OpenToast } from 'components/third-party/OpenToast';
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
import { setLoginUser } from 'store/reducers/loginUserSlice';
import { markSessionStart } from 'utils/session';
import { loginAdmin } from 'pages/api/doormileApi';
import { DT } from 'themes/dt/tokens';
// Astryx design system — see themes/astryx.js for the Doormile brand theme
@@ -71,150 +69,56 @@ const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', '
const Login = () => {
const dispatch = useDispatch();
const fcmtoken = useSelector((state) => state.fcm);
const [loading, setLoading] = useState(false);
let navigate = useNavigate();
const [otp, setOtp] = useState('');
const [userinfo, setUserinfo] = useState({});
const [username, setUsername] = useState('');
const [passwordStatus, setPasswordStatus] = useState(0);
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isPassword, setIspassword] = useState(false);
const [userid, setUserid] = useState(0);
useEffect(() => {
if (localStorage.getItem('firstname')) {
// 'authname' is the AUTH_PRESENCE_KEY (utils/session.js) — App.js and the
// inactivity/cross-tab logout machinery all key off it, unrelated to which
// backend actually authenticated the session.
if (localStorage.getItem('authname')) {
navigate('/doormile/dispatch');
}
}, []);
const loginsend = async () => {
setLoading(true);
const opentoast = (message, variant = 'error') => OpenToast(message, variant, 2000);
if (!username) {
opentoast('Fill All required fields');
setLoading(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!email || !password) {
opentoast('Enter your email and password');
return;
}
setLoading(true);
try {
const res = await axios.post(`https://jupiter.nearle.app/live/api/v1/users/console/login`, {
authname: username,
configid: 9, // 9 -> config id for nearle console admin
userfcmtoken: fcmtoken?.token,
password
});
// user not found
if (res.data.code == 409 && !res.data.status) {
OpenToast(res.data.message, 'error', 3000);
}
// user not activated
else if (res.data.code == 403) {
OpenToast(res.data.message, 'warning', 3000);
}
//user found, no password, setup password
else if (res.data.code == 409 && res.data.status) {
setPasswordStatus(1); // for password and confirm password ui
setUserid(res.data.details.userid);
OpenToast('User Found', 'success', 3000);
OpenToast(res.data.message, 'success', 3000);
}
//user found, incorrect password
else if (res.data.code == 401 && !res.data.status) {
OpenToast(res.data.message, 'error', 3000);
}
//user found, enter password
else if (res.data.code == 401 && res.data.status) {
OpenToast(res.data.message, 'success', 3000);
fetchAppLocations(res.data.userid);
setPasswordStatus(2);
}
// user found, correct password
else if (res.data.code == 200 && res.data.status) {
OpenToast(res.data.message, 'success', 1000);
setUserinfo(res.data.details);
const userinfo = res.data.details;
dispatch(setLoginUser(userinfo));
localStorage.setItem('firstname', userinfo.firstname);
localStorage.setItem('authname', userinfo.authname);
localStorage.setItem('roleid', userinfo.roleid);
localStorage.setItem('tenantid', userinfo.tenantid);
localStorage.setItem('partnerid', userinfo.partnerid);
localStorage.setItem('applocationid', userinfo.applocationid);
localStorage.setItem('userid', userinfo.userid);
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
const res = await loginAdmin(email, password);
if (res.success) {
const user = res.user || {};
// Compatibility keys: existing pages/api.js read userid/roleid/tenantid
// straight from localStorage rather than through a context. 'authname'
// stays the AUTH_PRESENCE_KEY so the rest of the session contract
// (useInactivityLogout, cross-tab logout, App.js gate) needs no changes.
localStorage.setItem('authname', user.email || user.name || email);
localStorage.setItem('firstname', user.name || '');
localStorage.setItem('userid', user.id ?? '');
localStorage.setItem('roleid', user.role ?? '');
localStorage.setItem('tenantid', user.tenantid ?? '');
dispatch(setLoginUser(user));
markSessionStart();
fetchAppLocations(userinfo.userid);
opentoast('Login successful', 'success');
navigate('/doormile/dispatch');
} else {
OpenToast(res.data.message, 'error', 3000);
opentoast(res.message || 'Login failed');
}
} catch (err) {
console.error(err);
OpenToast(err.message, 'error', 5000);
opentoast(err?.message || 'Login failed');
} finally {
setLoading(false);
}
};
const loginsuccessful = () => {
localStorage.setItem('firstname', userinfo.firstname);
localStorage.setItem('authname', userinfo.authname);
localStorage.setItem('roleid', userinfo.roleid);
localStorage.setItem('tenantid', userinfo.tenantid);
localStorage.setItem('partnerid', userinfo.partnerid);
localStorage.setItem('applocationid', userinfo.applocationid);
localStorage.setItem('userid', userinfo.userid);
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
markSessionStart();
closeGlobalToast(); // to close the pin snackbar
navigate('/doormile/dispatch');
};
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1500
});
};
const fetchAppLocations = async (id) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${id}`);
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
};
const updateUser = async () => {
const response = await axios.put(`${process.env.REACT_APP_URL2}/users/update`, {
userid,
password
});
if (response.data.status) {
OpenToast(response.data.message, 'success', 3000);
OpenToast('Enter Password to Login', 'success', 3000);
setPasswordStatus(2);
setPassword('');
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (passwordStatus == 0) {
loginsend();
} else if (passwordStatus == 1) {
if (!password || !confirmPassword || password != confirmPassword) {
OpenToast('Check Password', 'warning', 3000);
} else {
updateUser();
}
} else if (passwordStatus == 2) {
if (!password) {
OpenToast('Invalid Password', 'warning', 3000);
}
loginsend();
}
};
return (
<Theme theme={doormileTheme} mode="light">
@@ -266,75 +170,18 @@ const Login = () => {
type="email"
size="lg"
isRequired
value={username}
onChange={(value) => setUsername(value.toLocaleLowerCase())}
isDisabled={!!passwordStatus}
value={email}
onChange={(value) => setEmail(value.toLocaleLowerCase())}
/>
{/* Setup Password */}
{passwordStatus == 1 && (
<VStack gap={4} padding={0}>
<Divider label="Setup Password" />
<TextInput
hasAutoFocus
label="Enter New Password"
type="password"
size="lg"
isRequired
value={password}
onChange={(value) => setPassword(value)}
/>
<TextInput
label="Re-Enter Password"
type="password"
size="lg"
isRequired
value={confirmPassword}
onChange={(value) => setConfirmPassword(value)}
status={
confirmPassword !== '' && password !== confirmPassword
? { type: 'error', message: 'Passwords do not match' }
: undefined
}
/>
</VStack>
)}
{/* Enter Password */}
{passwordStatus == 2 && (
<VStack gap={4} padding={0}>
<Divider label="Enter Password" />
<TextInput
hasAutoFocus
label="Enter Password"
type="password"
size="lg"
isRequired
value={password}
onChange={(value) => setPassword(value)}
/>
</VStack>
)}
{/* OTP / retry */}
{isPassword && (
<VStack gap={1.5} padding={0}>
<HStack justify="between">
<Text type="label">Enter Password</Text>
<Link
onClick={() => {
setOtp('');
loginsend();
}}
>
Retry
</Link>
</HStack>
<TextInput label="Password" type="password" value={password} onChange={(value) => setPassword(value)} isLabelHidden />
</VStack>
)}
<Button label="Continue" type="submit" variant="primary" size="lg" width="100%" />
<TextInput
label="Password"
type="password"
size="lg"
isRequired
value={password}
onChange={(value) => setPassword(value)}
/>
<Button label="Sign in" type="submit" variant="primary" size="lg" width="100%" />
</VStack>
</form>
</VStack>