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,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>
|
||||
|
||||
Reference in New Issue
Block a user