LoginRoutes.js is registered before MainRoutes.js and defines its own
un-prefixed '/login' route pointing at pages/nearle/login — it wins the
route match, so login1.js (fixed in the earlier commit) was never being
rendered. login.js still hit jupiter.nearle.app/live/api/v1/users/console/login
with the old multi-step (email lookup -> setup/enter password) flow.
Collapsed to a single POST /admin/login with { email, password,
userfcmtoken }, matching Doormile's one-shot auth response. Visual
layout (branded two-panel screen, "Welcome back" copy) is unchanged.
Confirmed live: curl against api.doormile.com/api/v1/admin/login,
/admin/milers, /crm/clients, /admin/bookings all return the expected
{success,...} shapes and status codes for this code to handle.
281 lines
10 KiB
JavaScript
281 lines
10 KiB
JavaScript
// This is the active login page — matched by routes/LoginRoutes.js, which is
|
|
// registered before MainRoutes.js and wins the route match for both '/' and
|
|
// '/login'. (login1.js is dead code — MainRoutes.js's own '/login' route is
|
|
// unreachable because LoginRoutes.js's un-prefixed '/login' matches first.)
|
|
import { useState, useEffect } from 'react';
|
|
import AnimateButton from 'components/@extended/AnimateButton';
|
|
|
|
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, IconButton, InputAdornment } from '@mui/material';
|
|
import { useTheme } from '@mui/material/styles';
|
|
import axios from 'axios';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import Loader from 'components/Loader';
|
|
import doormileLogo from 'assets/images/doormile-logo.png';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import { OpenToast } from 'components/third-party/OpenToast';
|
|
import Visibility from '@mui/icons-material/Visibility';
|
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
|
import { markSessionStart } from 'utils/session';
|
|
|
|
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for light surfaces.
|
|
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
|
|
|
|
const Login = () => {
|
|
const dispatch = useDispatch();
|
|
const fcmtoken = useSelector((state) => state.fcm);
|
|
const theme = useTheme();
|
|
const [loading, setLoading] = useState(false);
|
|
let navigate = useNavigate();
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (localStorage.getItem('authname')) {
|
|
navigate('/nearle/dispatch');
|
|
}
|
|
}, []);
|
|
|
|
const loginsend = async () => {
|
|
if (!username || !password) {
|
|
opentoast('Fill All required fields');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
|
|
email: username,
|
|
password,
|
|
userfcmtoken: fcmtoken?.token
|
|
});
|
|
|
|
if (res.data.success) {
|
|
OpenToast('Login Successful', 'success', 1000);
|
|
const { token, data } = res.data;
|
|
dispatch(setLoginUser(data));
|
|
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}`;
|
|
markSessionStart();
|
|
navigate('/nearle/dispatch');
|
|
} else {
|
|
OpenToast(res.data.message || 'Invalid Data', 'error', 3000);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
OpenToast(err.response?.data?.message || err.message, 'error', 5000);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
const opentoast = (message) => {
|
|
OpenToast(message, 'error', 1500);
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ minHeight: '100vh', display: 'flex', bgcolor: '#f8fafc' }}>
|
|
{loading && <Loader />}
|
|
|
|
{/* ---- Left brand panel (hidden on small screens) ---- */}
|
|
<Box
|
|
sx={{
|
|
display: { xs: 'none', md: 'flex' },
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
flexBasis: '46%',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
p: 6,
|
|
background: 'linear-gradient(150deg, #900E1D 0%, #C01227 52%, #D35968 100%)'
|
|
}}
|
|
>
|
|
{/* Logo at the top-left corner */}
|
|
<img
|
|
src={doormileLogo}
|
|
alt="Doormile"
|
|
style={{
|
|
position: 'absolute',
|
|
top: 48,
|
|
left: 48,
|
|
maxHeight: 40
|
|
}}
|
|
/>
|
|
|
|
{/* decorative light glows */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: -120,
|
|
right: -80,
|
|
width: 360,
|
|
height: 360,
|
|
borderRadius: '50%',
|
|
background: 'radial-gradient(circle, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 70%)'
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
bottom: -150,
|
|
left: -110,
|
|
width: 440,
|
|
height: 440,
|
|
borderRadius: '50%',
|
|
background: 'radial-gradient(circle, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0) 70%)'
|
|
}}
|
|
/>
|
|
|
|
<Box sx={{ position: 'relative', maxWidth: 430 }}>
|
|
<Typography sx={{ fontSize: 40, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
|
|
Operate your dispatch,
|
|
<br />
|
|
end to end.
|
|
</Typography>
|
|
<Typography sx={{ fontSize: 17.5, color: 'rgba(255,255,255,0.85)', mb: 4, lineHeight: 1.6 }}>
|
|
Orders, AI route optimisation, live rider tracking and billing — all in the Doormile operator console.
|
|
</Typography>
|
|
|
|
<Stack spacing={1.5} sx={{ display: 'inline-flex', textAlign: 'left' }}>
|
|
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
|
|
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
|
|
<Box
|
|
sx={{
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: '50%',
|
|
bgcolor: 'rgba(255,255,255,0.18)',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: 13,
|
|
fontWeight: 700
|
|
}}
|
|
>
|
|
✓
|
|
</Box>
|
|
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
|
|
</Stack>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* ---- Right form panel ---- */}
|
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 2.5, sm: 4 } }}>
|
|
<Box sx={{ width: '100%', maxWidth: 420 }}>
|
|
<Card
|
|
sx={{
|
|
width: '100%',
|
|
borderRadius: 3,
|
|
border: `1px solid ${theme.palette.divider}`,
|
|
boxShadow: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
|
p: { xs: 2.5, sm: 4 }
|
|
}}
|
|
>
|
|
{/* Logo */}
|
|
<Stack alignItems="center" mb={2.5}>
|
|
<img src={doormileLogo} alt="Doormile" style={{ maxHeight: 40, filter: DOORMILE_RED_FILTER }} />
|
|
</Stack>
|
|
|
|
{/* Title */}
|
|
<Typography variant="h3" textAlign="center" sx={{ fontWeight: 700, mb: 0.5 }}>
|
|
Welcome back
|
|
</Typography>
|
|
<Typography variant="body2" textAlign="center" sx={{ color: '#64748b', mb: 3 }}>
|
|
Sign in to the Doormile console
|
|
</Typography>
|
|
|
|
<CardContent sx={{ p: 0 }}>
|
|
<form
|
|
noValidate
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
loginsend();
|
|
}}
|
|
>
|
|
<Stack spacing={3}>
|
|
{/* Email */}
|
|
<TextField
|
|
autoFocus
|
|
fullWidth
|
|
label="E-mail Address"
|
|
variant="outlined"
|
|
autoComplete="email"
|
|
required
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
|
|
/>
|
|
{/* Password */}
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
variant="outlined"
|
|
autoComplete="current-password"
|
|
required
|
|
type={showPassword ? 'text' : 'password'}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
{/* Submit */}
|
|
<AnimateButton>
|
|
<Button fullWidth size="large" type="submit" variant="contained" color="primary">
|
|
Continue
|
|
</Button>
|
|
</AnimateButton>
|
|
</Stack>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* footer */}
|
|
<Stack direction="row" justifyContent="center" alignItems="center" flexWrap="wrap" useFlexGap spacing={2} sx={{ mt: 3 }}>
|
|
<Typography
|
|
variant="caption"
|
|
component={Link}
|
|
href="https://nearle.in"
|
|
target="_blank"
|
|
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
|
|
>
|
|
© All rights reserved
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
component={Link}
|
|
href="https://nearle.in/terms"
|
|
target="_blank"
|
|
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
|
|
>
|
|
Terms and Conditions
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
component={Link}
|
|
href="https://nearle.in/privacy"
|
|
target="_blank"
|
|
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
|
|
>
|
|
Privacy Policy
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Login;
|