Changes in dashboard

This commit is contained in:
Malini 2025-10-28 10:29:12 +05:30
parent eabcc4e421
commit dcc05fc762
7 changed files with 218 additions and 37 deletions

View File

@ -12,19 +12,94 @@ import History from './pages/History/History'
import ChangePassword from './pages/ChangePassword/ChangePassword'
import ForgotPassword from './pages/ForgotPassword/ForgotPassword'
const RequireRole = ({ allowedRoles = [], children }) => {
let role
let token
try {
role = sessionStorage.getItem('user_role')
token = sessionStorage.getItem('auth_token')
} catch (error) {
role = null
token = null
}
const normalizedRole = String(role || '').toLowerCase()
const isAllowed = Boolean(token) && allowedRoles.some((allowed) => String(allowed || '').toLowerCase() === normalizedRole)
if (isAllowed) {
return children
}
return <Navigate to="/login" replace />
}
function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/history" element={<History />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin/dashboard" element={<AdminDashboard />} />
<Route path="/admin/validations" element={<Validations />} />
<Route path="/admin/validations/:id" element={<ValidationReview />} />
<Route path="/admin/configuration" element={<Configuration />} />
<Route path="/admin/users" element={<AdminUsers />} />
<Route path="/history" element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<History />
</RequireRole>
)}
/>
<Route
path="/dashboard"
element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<Dashboard />
</RequireRole>
)}
/>
<Route
path="/admin/dashboard"
element={(
<RequireRole allowedRoles={['Admin']}>
<AdminDashboard />
</RequireRole>
)}
/>
<Route
path="/admin/validations"
element={(
<RequireRole allowedRoles={['Admin']}>
<Validations />
</RequireRole>
)}
/>
<Route
path="/admin/validations/:id"
element={(
<RequireRole allowedRoles={['Admin']}>
<ValidationReview />
</RequireRole>
)}
/>
<Route
path="/admin/configuration"
element={(
<RequireRole allowedRoles={['Admin']}>
<Configuration />
</RequireRole>
)}
/>
<Route
path="/admin/users"
element={(
<RequireRole allowedRoles={['Admin']}>
<AdminUsers />
</RequireRole>
)}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/survey" element={<Survey />} />
<Route
path="/survey"
element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<Survey />
</RequireRole>
)}
/>
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="*" element={<Navigate to="/" replace />} />

View File

@ -42,7 +42,7 @@ const AdminHeader = () => {
React.useEffect(() => {
try {
const stored = localStorage.getItem('user_profile');
const stored = sessionStorage.getItem('user_profile');
if (stored) {
const parsed = JSON.parse(stored);
if (parsed?.name || parsed?.username) {
@ -59,8 +59,10 @@ const AdminHeader = () => {
const handleLogout = () => {
try {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_profile');
sessionStorage.removeItem('auth_token');
sessionStorage.removeItem('user_profile');
sessionStorage.removeItem('user_role');
sessionStorage.removeItem('establishment_id');
} catch {
// ignore storage errors
}

View File

@ -18,7 +18,7 @@ const HeaderBar = () => {
React.useEffect(() => {
let parsed;
try {
const stored = localStorage.getItem('user_profile');
const stored = sessionStorage.getItem('user_profile');
if (stored) {
parsed = JSON.parse(stored);
}
@ -37,8 +37,10 @@ const HeaderBar = () => {
const onLogout = () => {
try {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_profile');
sessionStorage.removeItem('auth_token');
sessionStorage.removeItem('user_profile');
sessionStorage.removeItem('user_role');
sessionStorage.removeItem('establishment_id');
} catch {}
setUserOpen(false);
navigate('/login');

View File

@ -39,9 +39,10 @@ const ChangePassword = () => {
throw new Error(message);
}
try {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_profile');
localStorage.removeItem('user_role');
sessionStorage.removeItem('auth_token');
sessionStorage.removeItem('user_profile');
sessionStorage.removeItem('user_role');
sessionStorage.removeItem('establishment_id');
} catch (storageError) {
console.warn('Unable to clear stored session', storageError);
}

View File

@ -11,14 +11,17 @@ const Login = () => {
const [rememberMe, setRememberMe] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState('');
const [toast, setToast] = React.useState(null);
const toastTimeoutRef = React.useRef(null);
const navigateTimeoutRef = React.useRef(null);
React.useEffect(() => {
try {
const remembered = localStorage.getItem('remember_me');
const remembered = sessionStorage.getItem('remember_me');
if (remembered === 'true') {
setRememberMe(true);
const storedEmail = localStorage.getItem('remembered_email');
const storedPassword = localStorage.getItem('remembered_password');
const storedEmail = sessionStorage.getItem('remembered_email');
const storedPassword = sessionStorage.getItem('remembered_password');
if (storedEmail) {
setEmail(storedEmail);
}
@ -31,6 +34,48 @@ const Login = () => {
}
}, []);
const closeToast = React.useCallback(() => {
if (toastTimeoutRef.current) {
window.clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
setToast(null);
}, []);
const showToast = React.useCallback((type, message) => {
if (!message) return;
if (toastTimeoutRef.current) {
window.clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
setToast({ type, message });
toastTimeoutRef.current = window.setTimeout(() => {
setToast(null);
toastTimeoutRef.current = null;
}, 4000);
}, []);
const scheduleNavigate = React.useCallback((path) => {
if (!path) return;
if (navigateTimeoutRef.current) {
window.clearTimeout(navigateTimeoutRef.current);
navigateTimeoutRef.current = null;
}
navigateTimeoutRef.current = window.setTimeout(() => {
navigate(path);
navigateTimeoutRef.current = null;
}, 500);
}, [navigate]);
React.useEffect(() => () => {
if (toastTimeoutRef.current) {
window.clearTimeout(toastTimeoutRef.current);
}
if (navigateTimeoutRef.current) {
window.clearTimeout(navigateTimeoutRef.current);
}
}, []);
const submit = async (e) => {
e.preventDefault();
setError('');
@ -43,7 +88,7 @@ const Login = () => {
const token = response.data;
try {
localStorage.setItem('auth_token', token);
sessionStorage.setItem('auth_token', token);
} catch (storageError) {
console.error('Failed to persist auth token', storageError);
}
@ -61,7 +106,7 @@ const Login = () => {
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
if (role) {
try {
localStorage.setItem('user_role', role);
sessionStorage.setItem('user_role', role);
} catch (storageError) {
console.warn('Unable to persist user role', storageError);
}
@ -72,37 +117,39 @@ const Login = () => {
name: payload?.name || payload?.username || '',
email: payload?.email || '',
};
localStorage.setItem('user_profile', JSON.stringify(profile));
sessionStorage.setItem('user_profile', JSON.stringify(profile));
} catch (storageError) {
console.warn('Unable to persist user profile', storageError);
}
const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
const isAdmin = String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
const successMessage = response?.message || 'Login successful.';
if (isAdmin) {
try {
localStorage.removeItem('establishment_id');
sessionStorage.removeItem('establishment_id');
} catch (storageError) {
console.warn('Unable to clear establishment ID', storageError);
}
navigate('/admin/dashboard');
showToast('success', successMessage);
scheduleNavigate('/admin/dashboard');
return;
}
if (rememberMe) {
try {
localStorage.setItem('remember_me', 'true');
localStorage.setItem('remembered_email', email);
localStorage.setItem('remembered_password', password);
sessionStorage.setItem('remember_me', 'true');
sessionStorage.setItem('remembered_email', email);
sessionStorage.setItem('remembered_password', password);
} catch (storageError) {
console.warn('Unable to persist remember-me preference', storageError);
}
} else {
try {
localStorage.removeItem('remember_me');
localStorage.removeItem('remembered_email');
localStorage.removeItem('remembered_password');
sessionStorage.removeItem('remember_me');
sessionStorage.removeItem('remembered_email');
sessionStorage.removeItem('remembered_password');
} catch (storageError) {
console.warn('Unable to clear remembered credentials', storageError);
}
@ -110,17 +157,20 @@ const Login = () => {
if (establishmentId) {
try {
localStorage.setItem('establishment_id', String(establishmentId));
sessionStorage.setItem('establishment_id', String(establishmentId));
} catch (storageError) {
console.warn('Unable to persist establishment ID', storageError);
}
navigate('/dashboard');
showToast('success', successMessage);
scheduleNavigate('/dashboard');
} else {
setError('No establishment associated with this account.');
}
} catch (err) {
console.error('Login failed', err);
setError(err.message || 'Unable to sign in. Please try again.');
const apiMessage = err.response?.data?.message || err.response?.data?.error;
const fallbackMessage = err.message || 'Unable to sign in. Please try again.';
setError(apiMessage || fallbackMessage);
} finally {
setLoading(false);
}
@ -128,6 +178,57 @@ const Login = () => {
return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
{loading && (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/20 backdrop-blur-sm">
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-white border-t-transparent" aria-label="Loading" />
</div>
)}
{toast && (
<div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4">
<div
className={`text-sm rounded px-3 py-2 border flex items-start justify-between gap-3 shadow ${
toast.type === 'success'
? 'bg-[#F3FAF4] border-[#C6E7D8] text-[#2F663C]'
: 'bg-[#FEF2F2] border-[#FECACA] text-[#B91C1C]'
}`}
>
<div className="flex items-start gap-2">
{toast.type === 'success' ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4 mt-0.5">
<path
fillRule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm4.28 7.28a.75.75 0 0 0-1.06-1.06l-4.22 4.22-1.22-1.22a.75.75 0 0 0-1.06 1.06l1.75 1.75c.293.293.767.293 1.06 0l4.75-4.75Z"
clipRule="evenodd"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4 mt-0.5">
<path
fillRule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.5 5.25a.75.75 0 0 1 1.5 0v4.5a.75.75 0 0 1-1.5 0v-4.5Zm.75 7.5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"
clipRule="evenodd"
/>
</svg>
)}
<span>{toast.message}</span>
</div>
<button
type="button"
aria-label="Close"
className="flex h-6 w-6 items-center justify-center rounded hover:bg-black/5"
onClick={closeToast}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4">
<path
fillRule="evenodd"
d="m12 10.94 3.97-3.97a.75.75 0 1 1 1.06 1.06L13.06 12l3.97 3.97a.75.75 0 1 1-1.06 1.06L12 13.06l-3.97 3.97a.75.75 0 1 1-1.06-1.06L10.94 12 6.97 8.03a.75.75 0 1 1 1.06-1.06L12 10.94Z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
)}
<div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF]">
@ -147,7 +248,7 @@ const Login = () => {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
placeholder="Enter Email"
className="block w-full h-10 rounded-md border-2 border-[#92722A] focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white"
required
/>

View File

@ -22,7 +22,7 @@ apiClient.interceptors.request.use((config) => {
if (!config.headers.Authorization) {
let token;
try {
token = localStorage.getItem('auth_token');
token = sessionStorage.getItem('auth_token');
} catch (error) {
token = undefined;
}

View File

@ -10,7 +10,7 @@ export const resolveEstablishmentId = (inputId) => {
}
let storedId;
try {
storedId = localStorage.getItem('establishment_id');
storedId = sessionStorage.getItem('establishment_id');
} catch (error) {
storedId = undefined;
}