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 ChangePassword from './pages/ChangePassword/ChangePassword'
|
||||||
import ForgotPassword from './pages/ForgotPassword/ForgotPassword'
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/history" element={<History />} />
|
<Route path="/history" element={(
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<RequireRole allowedRoles={['EstablishmentUser']}>
|
||||||
<Route path="/admin/dashboard" element={<AdminDashboard />} />
|
<History />
|
||||||
<Route path="/admin/validations" element={<Validations />} />
|
</RequireRole>
|
||||||
<Route path="/admin/validations/:id" element={<ValidationReview />} />
|
)}
|
||||||
<Route path="/admin/configuration" element={<Configuration />} />
|
/>
|
||||||
<Route path="/admin/users" element={<AdminUsers />} />
|
<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="/" 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="/change-password" element={<ChangePassword />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
@ -42,7 +42,7 @@ const AdminHeader = () => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('user_profile');
|
const stored = sessionStorage.getItem('user_profile');
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored);
|
||||||
if (parsed?.name || parsed?.username) {
|
if (parsed?.name || parsed?.username) {
|
||||||
@ -59,8 +59,10 @@ const AdminHeader = () => {
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('auth_token');
|
sessionStorage.removeItem('auth_token');
|
||||||
localStorage.removeItem('user_profile');
|
sessionStorage.removeItem('user_profile');
|
||||||
|
sessionStorage.removeItem('user_role');
|
||||||
|
sessionStorage.removeItem('establishment_id');
|
||||||
} catch {
|
} catch {
|
||||||
// ignore storage errors
|
// ignore storage errors
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const HeaderBar = () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('user_profile');
|
const stored = sessionStorage.getItem('user_profile');
|
||||||
if (stored) {
|
if (stored) {
|
||||||
parsed = JSON.parse(stored);
|
parsed = JSON.parse(stored);
|
||||||
}
|
}
|
||||||
@ -37,8 +37,10 @@ const HeaderBar = () => {
|
|||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('auth_token');
|
sessionStorage.removeItem('auth_token');
|
||||||
localStorage.removeItem('user_profile');
|
sessionStorage.removeItem('user_profile');
|
||||||
|
sessionStorage.removeItem('user_role');
|
||||||
|
sessionStorage.removeItem('establishment_id');
|
||||||
} catch {}
|
} catch {}
|
||||||
setUserOpen(false);
|
setUserOpen(false);
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
|
|||||||
@ -39,9 +39,10 @@ const ChangePassword = () => {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('auth_token');
|
sessionStorage.removeItem('auth_token');
|
||||||
localStorage.removeItem('user_profile');
|
sessionStorage.removeItem('user_profile');
|
||||||
localStorage.removeItem('user_role');
|
sessionStorage.removeItem('user_role');
|
||||||
|
sessionStorage.removeItem('establishment_id');
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to clear stored session', storageError);
|
console.warn('Unable to clear stored session', storageError);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,14 +11,17 @@ const Login = () => {
|
|||||||
const [rememberMe, setRememberMe] = React.useState(false);
|
const [rememberMe, setRememberMe] = React.useState(false);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const [error, setError] = React.useState('');
|
const [error, setError] = React.useState('');
|
||||||
|
const [toast, setToast] = React.useState(null);
|
||||||
|
const toastTimeoutRef = React.useRef(null);
|
||||||
|
const navigateTimeoutRef = React.useRef(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const remembered = localStorage.getItem('remember_me');
|
const remembered = sessionStorage.getItem('remember_me');
|
||||||
if (remembered === 'true') {
|
if (remembered === 'true') {
|
||||||
setRememberMe(true);
|
setRememberMe(true);
|
||||||
const storedEmail = localStorage.getItem('remembered_email');
|
const storedEmail = sessionStorage.getItem('remembered_email');
|
||||||
const storedPassword = localStorage.getItem('remembered_password');
|
const storedPassword = sessionStorage.getItem('remembered_password');
|
||||||
if (storedEmail) {
|
if (storedEmail) {
|
||||||
setEmail(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) => {
|
const submit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
@ -43,7 +88,7 @@ const Login = () => {
|
|||||||
|
|
||||||
const token = response.data;
|
const token = response.data;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('auth_token', token);
|
sessionStorage.setItem('auth_token', token);
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.error('Failed to persist auth token', storageError);
|
console.error('Failed to persist auth token', storageError);
|
||||||
}
|
}
|
||||||
@ -61,7 +106,7 @@ const Login = () => {
|
|||||||
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
|
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
|
||||||
if (role) {
|
if (role) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('user_role', role);
|
sessionStorage.setItem('user_role', role);
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to persist user role', storageError);
|
console.warn('Unable to persist user role', storageError);
|
||||||
}
|
}
|
||||||
@ -72,37 +117,39 @@ const Login = () => {
|
|||||||
name: payload?.name || payload?.username || '',
|
name: payload?.name || payload?.username || '',
|
||||||
email: payload?.email || '',
|
email: payload?.email || '',
|
||||||
};
|
};
|
||||||
localStorage.setItem('user_profile', JSON.stringify(profile));
|
sessionStorage.setItem('user_profile', JSON.stringify(profile));
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to persist user profile', storageError);
|
console.warn('Unable to persist user profile', storageError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
|
const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
|
||||||
const isAdmin = String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
|
const isAdmin = String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
|
||||||
|
const successMessage = response?.message || 'Login successful.';
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('establishment_id');
|
sessionStorage.removeItem('establishment_id');
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to clear establishment ID', storageError);
|
console.warn('Unable to clear establishment ID', storageError);
|
||||||
}
|
}
|
||||||
navigate('/admin/dashboard');
|
showToast('success', successMessage);
|
||||||
|
scheduleNavigate('/admin/dashboard');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('remember_me', 'true');
|
sessionStorage.setItem('remember_me', 'true');
|
||||||
localStorage.setItem('remembered_email', email);
|
sessionStorage.setItem('remembered_email', email);
|
||||||
localStorage.setItem('remembered_password', password);
|
sessionStorage.setItem('remembered_password', password);
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to persist remember-me preference', storageError);
|
console.warn('Unable to persist remember-me preference', storageError);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('remember_me');
|
sessionStorage.removeItem('remember_me');
|
||||||
localStorage.removeItem('remembered_email');
|
sessionStorage.removeItem('remembered_email');
|
||||||
localStorage.removeItem('remembered_password');
|
sessionStorage.removeItem('remembered_password');
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to clear remembered credentials', storageError);
|
console.warn('Unable to clear remembered credentials', storageError);
|
||||||
}
|
}
|
||||||
@ -110,17 +157,20 @@ const Login = () => {
|
|||||||
|
|
||||||
if (establishmentId) {
|
if (establishmentId) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('establishment_id', String(establishmentId));
|
sessionStorage.setItem('establishment_id', String(establishmentId));
|
||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Unable to persist establishment ID', storageError);
|
console.warn('Unable to persist establishment ID', storageError);
|
||||||
}
|
}
|
||||||
navigate('/dashboard');
|
showToast('success', successMessage);
|
||||||
|
scheduleNavigate('/dashboard');
|
||||||
} else {
|
} else {
|
||||||
setError('No establishment associated with this account.');
|
setError('No establishment associated with this account.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login failed', 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -128,6 +178,57 @@ const Login = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
<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="w-full max-w-md">
|
||||||
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
<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]">
|
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF]">
|
||||||
@ -147,7 +248,7 @@ const Login = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
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"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ apiClient.interceptors.request.use((config) => {
|
|||||||
if (!config.headers.Authorization) {
|
if (!config.headers.Authorization) {
|
||||||
let token;
|
let token;
|
||||||
try {
|
try {
|
||||||
token = localStorage.getItem('auth_token');
|
token = sessionStorage.getItem('auth_token');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
token = undefined;
|
token = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const resolveEstablishmentId = (inputId) => {
|
|||||||
}
|
}
|
||||||
let storedId;
|
let storedId;
|
||||||
try {
|
try {
|
||||||
storedId = localStorage.getItem('establishment_id');
|
storedId = sessionStorage.getItem('establishment_id');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
storedId = undefined;
|
storedId = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user