Changes in dashboard
This commit is contained in:
parent
eabcc4e421
commit
dcc05fc762
@ -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 />} />
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user