|
|
|
@@ -1,22 +1,32 @@
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { Outlet, useNavigate } from 'react-router-dom';
|
|
|
|
|
import { Menu, Sun, Moon, Bell, LogOut, User, Globe } from 'lucide-react';
|
|
|
|
|
import { Menu, Sun, Moon, Bell, LogOut, User, Globe, KeyRound, Save } from 'lucide-react';
|
|
|
|
|
import { NavigationView } from '../components/NavigationView.jsx';
|
|
|
|
|
import { Flyout, FlyoutItem, FlyoutSeparator } from '../components/Flyout.jsx';
|
|
|
|
|
import { DetailPopup } from '../components/DetailPopup.jsx';
|
|
|
|
|
import { Field } from '../components/Field.jsx';
|
|
|
|
|
import { Button } from '../components/Button.jsx';
|
|
|
|
|
import { useAuth } from '../context/auth-context.jsx';
|
|
|
|
|
import { useUi } from '../context/ui-context.jsx';
|
|
|
|
|
import { useAsync } from '../hooks/useAsync.js';
|
|
|
|
|
import { listNotifications, markAllNotificationsRead, markNotificationRead } from '../services/notifications.js';
|
|
|
|
|
import { changePassword } from '../services/auth.js';
|
|
|
|
|
|
|
|
|
|
const EMPTY_PASSWORD_FORM = { currentPassword: '', newPassword: '', confirmPassword: '' };
|
|
|
|
|
|
|
|
|
|
export default function AppShell() {
|
|
|
|
|
const [navMode, setNavMode] = useState('expanded');
|
|
|
|
|
const { user, logout } = useAuth();
|
|
|
|
|
const { t, theme, setTheme, lang, setLang } = useUi();
|
|
|
|
|
const { t, theme, setTheme, lang, setLang, pushToast } = useUi();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
const { data: notifData, reload: reloadNotifications } = useAsync(listNotifications, []);
|
|
|
|
|
const unread = notifData?.unread || 0;
|
|
|
|
|
|
|
|
|
|
const [changingPassword, setChangingPassword] = useState(false);
|
|
|
|
|
const [passwordForm, setPasswordForm] = useState(EMPTY_PASSWORD_FORM);
|
|
|
|
|
const [passwordError, setPasswordError] = useState('');
|
|
|
|
|
|
|
|
|
|
function toggleNav() {
|
|
|
|
|
setNavMode((m) => (m === 'expanded' ? 'compact' : 'expanded'));
|
|
|
|
|
}
|
|
|
|
@@ -28,6 +38,31 @@ export default function AppShell() {
|
|
|
|
|
else if (notification.entity_type === 'activity') navigate(`/proyectos`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openChangePassword() {
|
|
|
|
|
setPasswordForm(EMPTY_PASSWORD_FORM);
|
|
|
|
|
setPasswordError('');
|
|
|
|
|
setChangingPassword(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitChangePassword() {
|
|
|
|
|
setPasswordError('');
|
|
|
|
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
|
|
|
|
setPasswordError(t('register_password_mismatch'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (passwordForm.newPassword.length < 8) {
|
|
|
|
|
setPasswordError(t('register_password_short'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
await changePassword(passwordForm.currentPassword, passwordForm.newPassword);
|
|
|
|
|
setChangingPassword(false);
|
|
|
|
|
pushToast({ tone: 'success', message: t('change_password_success') });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setPasswordError(err.message || t('common_error'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="app-shell">
|
|
|
|
|
<NavigationView mode={navMode} />
|
|
|
|
@@ -79,6 +114,9 @@ export default function AppShell() {
|
|
|
|
|
<div style={{ fontSize: '.7rem', color: 'rgb(var(--muted))' }}>{user?.roleName || user?.roleCode}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<FlyoutSeparator />
|
|
|
|
|
<FlyoutItem icon={KeyRound} onClick={openChangePassword}>
|
|
|
|
|
{t('change_password_title')}
|
|
|
|
|
</FlyoutItem>
|
|
|
|
|
<FlyoutItem icon={LogOut} danger onClick={() => { logout(); navigate('/login'); }}>
|
|
|
|
|
{t('logout')}
|
|
|
|
|
</FlyoutItem>
|
|
|
|
@@ -89,6 +127,23 @@ export default function AppShell() {
|
|
|
|
|
<Outlet />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DetailPopup open={changingPassword} title={t('change_password_title')} onClose={() => setChangingPassword(false)} width={420} height={380}>
|
|
|
|
|
{passwordError && <div className="login-error">{passwordError}</div>}
|
|
|
|
|
<Field label={t('change_password_current')}>
|
|
|
|
|
<input className="input" type="password" value={passwordForm.currentPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, currentPassword: e.target.value }))} autoFocus />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label={t('reset_password_new')}>
|
|
|
|
|
<input className="input" type="password" value={passwordForm.newPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, newPassword: e.target.value }))} />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label={t('register_password_confirm')}>
|
|
|
|
|
<input className="input" type="password" value={passwordForm.confirmPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, confirmPassword: e.target.value }))} />
|
|
|
|
|
</Field>
|
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
|
|
|
<Button variant="accent" icon={Save} onClick={submitChangePassword}>{t('common_save')}</Button>
|
|
|
|
|
<Button variant="standard" onClick={() => setChangingPassword(false)}>{t('common_cancel')}</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DetailPopup>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|