From 0cad6b35563e80a275cfc78c77428957022cb232 Mon Sep 17 00:00:00 2001 From: razyd Date: Thu, 8 Oct 2026 09:42:15 +0200 Subject: [PATCH 1/2] fix(settings): fall back to the first available tab - Add resolveSettingsTab helper to validate tab parameter - Use helper in SettingsPanel to handle invalid ?tab= values - Add 5 test cases for the helper --- backend/tests/frontend/settings-tab.test.ts | 32 +++++++++++++++++++ .../app/components/profile/SettingsPanel.tsx | 10 ++++-- frontend/app/lib/helpers/settingsTab.ts | 12 +++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 backend/tests/frontend/settings-tab.test.ts create mode 100644 frontend/app/lib/helpers/settingsTab.ts diff --git a/backend/tests/frontend/settings-tab.test.ts b/backend/tests/frontend/settings-tab.test.ts new file mode 100644 index 0000000..a299209 --- /dev/null +++ b/backend/tests/frontend/settings-tab.test.ts @@ -0,0 +1,32 @@ +// tests/frontend/settings-tab.test.ts +// resolveSettingsTab returns the requested tab if available, otherwise the +// first available tab. Runs the real helper (the frontend has no test runner). + +import { describe, it, expect } from 'vitest' +import { resolveSettingsTab } from '../../../frontend/app/lib/helpers/settingsTab.js' + +describe('resolveSettingsTab', () => { + const available = ['users', 'notifications', 'security'] as const + + it('returns the requested tab when it is available', () => { + expect(resolveSettingsTab('notifications', available)).toBe('notifications') + expect(resolveSettingsTab('security', available)).toBe('security') + expect(resolveSettingsTab('users', available)).toBe('users') + }) + + it('returns available[0] when requested tab is not in available (no permission)', () => { + expect(resolveSettingsTab('users', ['notifications', 'security'])).toBe('notifications') + }) + + it('returns available[0] when requested tab does not exist', () => { + expect(resolveSettingsTab('nonexistent', available)).toBe('users') + }) + + it('returns available[0] when requested is null', () => { + expect(resolveSettingsTab(null, available)).toBe('users') + }) + + it('returns available[0] when requested is empty string', () => { + expect(resolveSettingsTab('', available)).toBe('users') + }) +}) diff --git a/frontend/app/components/profile/SettingsPanel.tsx b/frontend/app/components/profile/SettingsPanel.tsx index 1a46134..2445fbb 100644 --- a/frontend/app/components/profile/SettingsPanel.tsx +++ b/frontend/app/components/profile/SettingsPanel.tsx @@ -15,6 +15,7 @@ import { departmentsApi } from '@/app/lib/departments' import { formatDepartmentName } from '@/app/lib/logbooks/hooks/useDepartments' import { cn } from '@/app/lib/helpers/utils' import { canRunNotificationCheckRole, isAdminRole } from '@/app/lib/helpers/utils' +import { resolveSettingsTab } from '@/app/lib/helpers/settingsTab' import { toast } from 'react-hot-toast' import { FiUsers, @@ -138,8 +139,12 @@ export function SettingsPanel() { [t, showDemoTab] ) - const activeTab = - (searchParams.get('tab') as SettingsTab) || (isUserAdmin ? 'users' : 'notifications') + const availableTabs = tabs.filter((tab) => !tab.adminOnly || isUserAdmin) + + const activeTab = resolveSettingsTab( + searchParams.get('tab'), + availableTabs.map((t) => t.id) + ) as SettingsTab const [users, setUsers] = useState([]) const [loading, setLoading] = useState(false) @@ -150,7 +155,6 @@ export function SettingsPanel() { const [isDropdownOpen, setIsDropdownOpen] = useState(false) const dropdownRef = useRef(null) - const availableTabs = tabs.filter((tab) => !tab.adminOnly || isUserAdmin) const activeTabConfig = availableTabs.find((t) => t.id === activeTab) || availableTabs[0] useEffect(() => { diff --git a/frontend/app/lib/helpers/settingsTab.ts b/frontend/app/lib/helpers/settingsTab.ts new file mode 100644 index 0000000..c79abbb --- /dev/null +++ b/frontend/app/lib/helpers/settingsTab.ts @@ -0,0 +1,12 @@ +// frontend/app/lib/helpers/settingsTab.ts + +/** + * Resolves the active settings tab from URL parameter. + * Returns the requested tab if it's available, otherwise the first available tab. + */ +export function resolveSettingsTab(requested: string | null, available: readonly string[]): string { + if (requested && available.includes(requested)) { + return requested + } + return available[0] +} From 286cb5c940fea211deb0f24b5ab62f49d2a00bcc Mon Sep 17 00:00:00 2001 From: razyd Date: Sun, 11 Oct 2026 10:56:50 +0200 Subject: [PATCH 2/2] docs(todo): drop the settings tab fallback, now fixed - Remove the "?tab= of a tab without permission" entry. Why: resolveSettingsTab falls back to the first available tab. Checked in the browser: admin with ?tab=xxx and ?tab=demo opens Users, a receptionist with ?tab=users opens Notifications. --- docs/TODO.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 473ba85..ccacfe4 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -297,9 +297,6 @@ preview. volviera a ejecutar (`UPDATE users SET is_active = 0 WHERE username = 'demo'`). `setup:local` no la ejecuta (es anterior al baseline); no aplicarla a mano. _Comprobado el 2026-10-05._ -- [ ] **Configuración: `?tab=` de una pestaña sin permiso deja el contenido en - blanco** — p. ej. recepción con `?tab=users`; debería caer en la primera - pestaña disponible. _Visto el 2026-10-05._ - [ ] **Errores del backend sin traducir** — `messages/{es,en}/errors.json` tiene ~200 textos en `codes` que nadie usa: la UI muestra el `error` del backend, en español. `app/lib/helpers/errorCodeMessage.ts` ya los traduce