feat: version 4.3.0 -- Sesiones activas y pagina Mi cuenta

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-26 16:42:19 -05:00
parent 544928d04a
commit a6cc41666e
34 changed files with 464 additions and 110 deletions
+2
View File
@@ -23,6 +23,7 @@ import NotasPage from '../pages/Notas/NotasPage.jsx';
import ContrasenasPage from '../pages/Contrasenas/ContrasenasPage.jsx';
import EquiposPage from '../pages/Equipos/EquiposPage.jsx';
import ResetVaultPinPage from '../pages/Login/ResetVaultPinPage.jsx';
import MiCuentaPage from '../pages/MiCuenta/MiCuentaPage.jsx';
// Router de datos (createBrowserRouter): necesario para useBlocker, que avisa al salir de una
// seccion con cambios sin guardar (ver components/NavigationGuard.jsx). Mismas rutas de antes.
@@ -37,6 +38,7 @@ const router = createBrowserRouter(createRoutesFromElements(
<Route path="/aceptar-invitacion" element={<AcceptInvitePage />} />
<Route path="/" element={<ProtectedRoute><AppShell /></ProtectedRoute>}>
<Route index element={<DashboardPage />} />
<Route path="mi-cuenta" element={<MiCuentaPage />} />
<Route path="proyectos" element={<ModuleRoute module="projects"><ProyectosPage /></ModuleRoute>} />
<Route path="proyectos/:id" element={<ModuleRoute module="projects"><ProyectoDetallePage /></ModuleRoute>} />
<Route path="tareas-recurrentes" element={<ModuleRoute module="recurring_tasks"><RecurringTasksPage /></ModuleRoute>} />
+5 -56
View File
@@ -1,37 +1,27 @@
import { useEffect, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { Menu, Sun, Moon, Bell, LogOut, User, Globe, KeyRound, Save } from 'lucide-react';
import { Menu, Sun, Moon, Bell, LogOut, User, Globe, UserCog } from 'lucide-react';
import { NavigationView } from '../components/NavigationView.jsx';
import { NavigationGuard } from '../components/NavigationGuard.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 { PasswordInput } from '../components/PasswordInput.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';
import { getUpdateStatus } from '../services/updater.js';
const EMPTY_PASSWORD_FORM = { currentPassword: '', newPassword: '', confirmPassword: '' };
const MOBILE_BREAKPOINT = 768;
export default function AppShell() {
const [navMode, setNavMode] = useState('expanded');
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const { user, logout } = useAuth();
const { t, theme, setTheme, lang, setLang, pushToast, openDialog } = useUi();
const { t, theme, setTheme, lang, setLang, openDialog } = 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('');
// Al entrar (una vez por sesion, no en cada navegacion): si es superadmin y hay una
// actualizacion pendiente, avisar con un popup y un acceso directo al panel. Silencioso si
@@ -80,31 +70,6 @@ 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} mobileOpen={mobileNavOpen} onNavigate={closeMobileNav} />
@@ -157,10 +122,10 @@ 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 icon={UserCog} onClick={() => navigate('/mi-cuenta')}>
{t('account_title')}
</FlyoutItem>
<FlyoutItem icon={LogOut} danger onClick={() => { logout(); navigate('/login'); }}>
<FlyoutItem icon={LogOut} danger onClick={() => { logout({ remote: true }); navigate('/login'); }}>
{t('logout')}
</FlyoutItem>
</Flyout>
@@ -177,22 +142,6 @@ export default function AppShell() {
</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')}>
<PasswordInput value={passwordForm.currentPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, currentPassword: e.target.value }))} autoFocus />
</Field>
<Field label={t('reset_password_new')}>
<PasswordInput value={passwordForm.newPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, newPassword: e.target.value }))} />
</Field>
<Field label={t('register_password_confirm')}>
<PasswordInput 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>
);
}