From aff08f9a8a5e109c9bf88efc878230b7923aec3b Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 24 Apr 2026 08:34:55 +0200 Subject: [PATCH 1/5] feat: add details modals for users, clinics and audit logs --- .../src/components/admin/AuditLogView.jsx | 264 +++++++++++------- .../components/admin/ClinicDetailsModal.jsx | 137 +++++++++ .../src/components/admin/LogDetailsModal.jsx | 135 +++++++++ .../src/components/admin/UserDetailsModal.jsx | 146 ++++++++++ frontend/src/pages/AdminDashboard.jsx | 134 ++++++--- 5 files changed, 674 insertions(+), 142 deletions(-) create mode 100644 frontend/src/components/admin/ClinicDetailsModal.jsx create mode 100644 frontend/src/components/admin/LogDetailsModal.jsx create mode 100644 frontend/src/components/admin/UserDetailsModal.jsx diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index 5750b06..49db74d 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -9,17 +9,127 @@ import { AlertCircle, Search, MapPin, - ShieldCheck + ShieldCheck, + X, + Activity, + Hash, + Info } from 'lucide-react'; +// --- MODAL KOMPONENT --- +const LogDetailsModal = ({ isOpen, onClose, log }) => { + if (!isOpen || !log) return null; + + const fullDate = new Date(log.createdAt).toLocaleString('sv-SE', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + return ( +
+
+ {/* HEADER */} +
+ +
+
+ +
+
+

Systemlogg / Detaljer

+

+ {log.action.replace('_', ' ')} +

+
+
+
+ + {/* INNEHÅLL */} +
+
+ +

Beskrivning

+

{log.description}

+
+ +
+
+

Utförd av

+
+ + {log.performedByName} +
+
+
+

Klinik

+
+ + {log.clinicName || 'System'} +
+
+
+

Patient

+
+ + {log.petName || 'N/A'} +
+
+
+

Tidpunkt

+
+ + {fullDate} +
+
+
+ +
+
+
+
+

Referens-ID

+

{log.recordId}

+
+
+ +
+
+
+
+ ); +}; + +// --- HUVUDKOMPONENT --- const AuditLogView = ({ logs = [], loading }) => { - // 1. States för filtrering const [searchTerm, setSearchTerm] = useState(''); const [roleFilter, setRoleFilter] = useState('ALL'); const [actionFilter, setActionFilter] = useState('ALL'); const [clinicFilter, setClinicFilter] = useState('ALL'); const [dateFilter, setDateFilter] = useState(''); + // States för modal + const [selectedLog, setSelectedLog] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openLogDetails = (log) => { + setSelectedLog(log); + setIsModalOpen(true); + }; + const clinics = useMemo(() => { const uniqueClinics = [...new Set(logs.map(log => log.clinicName))].filter(Boolean); return uniqueClinics.sort(); @@ -35,7 +145,6 @@ const AuditLogView = ({ logs = [], loading }) => { } }; - const filteredLogs = useMemo(() => { return logs.filter(log => { const searchLower = searchTerm.toLowerCase(); @@ -61,159 +170,105 @@ const AuditLogView = ({ logs = [], loading }) => { return (
- {/* --- ADMIN FILTER PANEL --- */} -
-
- + {/* FILTER PANEL */} +
+
+
setSearchTerm(e.target.value)} />
-
- - setActionFilter(e.target.value)} className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold outline-none"> + - + - +
-
- - setRoleFilter(e.target.value)} className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold outline-none"> + - - + +
-
- setClinicFilter(e.target.value)} className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold outline-none"> + + {clinics.map(name => )}
-
-
- setDateFilter(e.target.value)} - className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold outline-none focus:ring-2 focus:ring-[#003f5a] text-slate-600" - /> -
+ setDateFilter(e.target.value)} className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold outline-none text-slate-600" />
- {/* --- LOG LIST --- */} + {/* LOG LIST */}
{filteredLogs.length === 0 ? (
-

Inga loggar matchar administratörens filter

+

Inga loggar matchar filter

) : ( -
+
{filteredLogs.map((log) => { const style = getActionStyle(log.action); - const date = new Date(log.createdAt).toLocaleString('sv-SE', { - month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' + const dateStr = new Date(log.createdAt).toLocaleString('sv-SE', { + month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); return ( -
-
- +
openLogDetails(log)} + className="relative flex gap-6 group border-b border-slate-50 last:border-0 cursor-pointer hover:bg-slate-50/80 p-2 rounded-2xl transition-all" + > +
{style.icon}
- -
+

{log.action.replace('_', ' ')}

- {/* Roll-tagg */} - - {log.performedByRole?.replace('ROLE_', '') ?? 'SYSTEM'} - + {log.performedByRole?.replace('ROLE_', '')} +
- - {date} - + {dateStr}
- -

{log.description}

- +

{log.description}

- {/* Utförare */}
- - {log.performedByName} - -
- {/* DJURETS NAMN (NYTT) */} -
- Patient: - - {log.petName} - -
- - {/* Klinik */} -
- - - {log.clinicName || 'System'} - -
- - {/* Ärende-ID */} -
- Ref: - - {log.recordId} - + {log.performedByName}
+ {log.petName && ( +
+ Patient: {log.petName} +
+ )}
@@ -222,6 +277,13 @@ const AuditLogView = ({ logs = [], loading }) => {
)}
+ + {/* MODAL RENDER */} + setIsModalOpen(false)} + log={selectedLog} + />
); }; diff --git a/frontend/src/components/admin/ClinicDetailsModal.jsx b/frontend/src/components/admin/ClinicDetailsModal.jsx new file mode 100644 index 0000000..7d95fa9 --- /dev/null +++ b/frontend/src/components/admin/ClinicDetailsModal.jsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { + X, + MapPin, + Phone, + Hospital, + Calendar, + Activity, + Edit2, + Users as UsersIcon, + Award +} from 'lucide-react'; + +const ClinicDetailsModal = ({ isOpen, onClose, clinic, users = [], onEdit }) => { + if (!isOpen || !clinic) return null; + + const clinicStaff = users.filter(u => + u.clinicId === clinic.id && + u.role?.includes('VET') + ); + + return ( +
+
+ + {/* HEADER */} +
+ + +
+
+ +
+
+

+ {clinic.name} +

+ {/* "Aktiv enhet" borttagen härifrån */} +
+
+
+ + {/* INNEHÅLL */} +
+ {/* KONTAKT & ADRESS */} +
+
+

Adress

+
+ + {clinic.address || 'Ingen adress angiven'} +
+
+
+

Telefonnummer

+
+ + {clinic.phoneNumber || 'Saknas'} +
+
+
+ + {/* VETERINÄR-LISTA */} +
+
+ +

Anslutna Veterinärer ({clinicStaff.length})

+
+ + {clinicStaff.length > 0 ? ( +
+ {clinicStaff.map(member => ( +
+
+

{member.name}

+

+ Legitimerad Veterinär +

+
+
+ {member.email} +
+
+ ))} +
+ ) : ( +
+

Inga veterinärer kopplade till denna klinik

+
+ )} +
+ + {/* SYSTEMDETALJER */} +
+
+
+
+
+

Registrerad

+

+ {clinic.createdAt ? new Date(clinic.createdAt).toLocaleDateString('sv-SE') : '---'} +

+
+
+
+
+
+

Klinik-ID

+

+ {clinic.id?.substring(0, 13)}... +

+
+
+
+ + +
+
+ +
+
+
+ ); +}; + +export default ClinicDetailsModal; \ No newline at end of file diff --git a/frontend/src/components/admin/LogDetailsModal.jsx b/frontend/src/components/admin/LogDetailsModal.jsx new file mode 100644 index 0000000..261582c --- /dev/null +++ b/frontend/src/components/admin/LogDetailsModal.jsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { + X, + Clock, + User as UserIcon, + MapPin, + Activity, + Hash, + Info, + ShieldCheck +} from 'lucide-react'; + +const LogDetailsModal = ({ isOpen, onClose, log }) => { + if (!isOpen || !log) return null; + + const date = new Date(log.createdAt).toLocaleString('sv-SE', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + return ( +
+
+ + {/* HEADER - Lila Audit-tema */} +
+ + +
+
+ +
+
+

Systemlogg / Händelse

+

+ {log.action.replace('_', ' ')} +

+
+
+
+ + {/* INNEHÅLL */} +
+ + {/* HUVUDBESKRIVNING */} +
+ +

Händelseförlopp

+

+ {log.description} +

+
+ + {/* DETALJERAD INFO GRID */} +
+
+

Utförd av

+
+ + {log.performedByName} + + {log.performedByRole?.replace('ROLE_', '')} + +
+
+
+

Klinik

+
+ + {log.clinicName || 'Systemnivå'} +
+
+
+

Patient (Referens)

+
+ + {log.petName || 'N/A'} +
+
+
+

Tidstämpel

+
+ + {date} +
+
+
+ + {/* TEKNISK INFO */} +
+
+
+
+
+

Ärende ID (Record)

+

+ {log.recordId} +

+
+
+
+
+

Logg-ID

+

+ {log.id} +

+
+
+
+
+ + +
+ +
+
+
+ ); +}; + +export default LogDetailsModal; \ No newline at end of file diff --git a/frontend/src/components/admin/UserDetailsModal.jsx b/frontend/src/components/admin/UserDetailsModal.jsx new file mode 100644 index 0000000..3dd8f7c --- /dev/null +++ b/frontend/src/components/admin/UserDetailsModal.jsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react'; +import { X, Mail, Shield, Hospital, Calendar, Activity, User as UserIcon, Edit2, Award, Briefcase } from 'lucide-react'; +import { vetService } from '../../services/api'; + +const UserDetailsModal = ({ isOpen, onClose, user, clinics, onEdit }) => { + const [vetDetails, setVetDetails] = useState(null); + const [loadingVet, setLoadingVet] = useState(false); + + useEffect(() => { + // Om användaren är en veterinär, hämta extrauppgifter + if (isOpen && user?.role?.includes('VET')) { + setLoadingVet(true); + vetService.getAll() + .then(res => { + const details = res.data.find(v => v.userId === user.id); + setVetDetails(details); + }) + .catch(err => console.error("Kunde inte hämta veterinärinfo:", err)) + .finally(() => setLoadingVet(false)); + } else { + setVetDetails(null); + } + }, [isOpen, user]); + + if (!isOpen || !user) return null; + + const userClinic = clinics.find(c => c.id === user.clinicId); + const roleName = user.role?.replace('ROLE_', ''); + const isVet = user.role?.includes('VET'); + + return ( +
+
+ + {/* HEADER */} +
+ + +
+
+ +
+
+

+ {user.name} +

+
+ + {roleName} + +
+
+
+
+ + {/* INNEHÅLL */} +
+ {/* BASINFO KONTAKT */} +
+
+

E-postadress

+
+ + {user.email} +
+
+ + {/* Renderas bara om användaren har en klinik kopplad */} + {user.clinicId && ( +
+

Ansluten klinik

+
+ + {userClinic?.name || 'Laddar...'} +
+
+ )} +
+ + {/* VETERINÄR-SPECIFIKA FÄLT */} + {isVet && ( +
+
+

Vet-ID / Legitimation

+
+ + {loadingVet ? '...' : vetDetails?.licenseId || 'Ej angivet'} +
+
+
+

Specialisering

+
+ + {loadingVet ? '...' : vetDetails?.specialization || 'Allmänpraktiserande'} +
+
+
+ )} + + {/* SYSTEMDETALJER */} +
+
+
+
+
+

Medlem sedan

+

+ {user.createdAt ? new Date(user.createdAt).toLocaleDateString('sv-SE') : '---'} +

+
+
+
+
+
+

Användar-ID

+

+ {user.id?.substring(0, 13)}... +

+
+
+
+ + +
+
+ +
+
+
+ ); +}; + +export default UserDetailsModal; \ No newline at end of file diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index bd103d2..b65b15c 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -2,7 +2,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { userService, clinicService, activityService } from '../services/api'; import UserModal from '../components/admin/UserModal'; import ClinicModal from '../components/admin/ClinicModal'; -import AuditLogView from '../components/admin/AuditLogView'; // Import av den nya vyn +import AuditLogView from '../components/admin/AuditLogView'; +import UserDetailsModal from '../components/admin/UserDetailsModal'; +import ClinicDetailsModal from '../components/admin/ClinicDetailsModal'; import { Users, Hospital, @@ -19,19 +21,19 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { // Data State const [users, setUsers] = useState([]); const [clinics, setClinics] = useState([]); - const [activities, setActivities] = useState([]); // Nytt state för loggar + const [activities, setActivities] = useState([]); const [loading, setLoading] = useState(true); // UI State const [activeTab, setActiveTab] = useState('USERS'); const [isUserModalOpen, setIsUserModalOpen] = useState(false); const [isClinicModalOpen, setIsClinicModalOpen] = useState(false); + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + const [isClinicDetailsOpen, setIsClinicDetailsOpen] = useState(false); // Nytt state för klinikdetaljer const [selectedItem, setSelectedItem] = useState(null); - // Avgör om vi ska visa statistik (Översiktsvyn från Hem-knappen) const showStats = initialTab === 'OVERVIEW'; - // Synka fliken när vi navigerar från sidomenyn useEffect(() => { if (initialTab === 'OVERVIEW') { setActiveTab('USERS'); @@ -43,8 +45,6 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { const fetchData = useCallback(async () => { setLoading(true); try { - // Här hämtar vi användare, kliniker och loggar parallellt - // OBS: Om du inte skapat en global endpoint än kan logRes.data vara tom const [userRes, clinicRes, logRes] = await Promise.all([ userService.getAll(), clinicService.getAll(), @@ -78,6 +78,18 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { else setIsClinicModalOpen(true); }; + // Detaljvyer + const openDetailsModal = (user) => { + setSelectedItem(user); + setIsDetailsModalOpen(true); + }; + + const openClinicDetails = (clinic) => { + setSelectedItem(clinic); + setIsClinicDetailsOpen(true); + }; + + // Sparningslogik const handleSaveUser = async (userData) => { const response = selectedItem ? await userService.update(selectedItem.id, userData) @@ -88,10 +100,14 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { }; const handleSaveClinic = async (clinicData) => { - selectedItem ? await clinicService.update(selectedItem.id, clinicData) : await clinicService.create(clinicData); - fetchData(); + const response = selectedItem + ? await clinicService.update(selectedItem.id, clinicData) + : await clinicService.create(clinicData); + await fetchData(); + return response; }; + // Raderingslogik const handleDeleteClinic = async (clinicId, name) => { if (window.confirm(`Vill du verkligen radera kliniken ${name}?`)) { try { @@ -109,6 +125,23 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { } }; + // Sömlös redigering från detaljvyer + const handleEditFromDetails = (user) => { + setIsDetailsModalOpen(false); + setTimeout(() => { + setSelectedItem(user); + setIsUserModalOpen(true); + }, 100); + }; + + const handleEditClinicFromDetails = (clinic) => { + setIsClinicDetailsOpen(false); + setTimeout(() => { + setSelectedItem(clinic); + setIsClinicModalOpen(true); + }, 100); + }; + const stats = { totalUsers: users.length, totalClinics: clinics.length, @@ -120,7 +153,7 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { {/* HEADER */}
-

+

{showStats ? : activeTab === 'USERS' ? : activeTab === 'CLINICS' ? : @@ -130,12 +163,11 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { activeTab === 'USERS' ? "Användarregister" : activeTab === 'CLINICS' ? "Kliniköversikt" : "Aktivitetslogg"}

-

+

Admin: {userName} • {showStats ? "Övergripande status" : `Hantering av ${activeTab.toLowerCase()}`}

- {/* SKAPA-KNAPP: Visas inte om vi är i Audit Log */} {!showStats && activeTab !== 'LOGS' && (
- - {activeTab !== 'LOGS' && ( - - )}
)} - {/* INNEHÅLL BASERAT PÅ FLIK */} {activeTab === 'LOGS' ? ( ) : ( @@ -240,20 +261,22 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { ) : activeTab === 'USERS' ? ( users.map(user => ( - - -
{user.name}
+ + openDetailsModal(user)}> +
+ {user.name} +
{user.email}
- - {user.role?.replace('ROLE_', '')} - + + {user.role?.replace('ROLE_', '')} + -
+
@@ -262,14 +285,14 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { )) ) : ( clinics.map(clinic => ( - - -
{clinic.name}
+ + openClinicDetails(clinic)}> +
{clinic.name}
{clinic.address}
{clinic.phoneNumber} -
+
@@ -282,8 +305,37 @@ const AdminDashboard = ({ userName, initialTab = 'USERS' }) => { )}
- setIsUserModalOpen(false)} onSave={handleSaveUser} initialData={selectedItem} clinics={clinics} /> - setIsClinicModalOpen(false)} onSave={handleSaveClinic} initialData={selectedItem} /> + {/* MODALER */} + setIsUserModalOpen(false)} + onSave={handleSaveUser} + initialData={selectedItem} + clinics={clinics} + /> + + setIsDetailsModalOpen(false)} + user={selectedItem} + clinics={clinics} + onEdit={handleEditFromDetails} + /> + + setIsClinicModalOpen(false)} + onSave={handleSaveClinic} + initialData={selectedItem} + /> + + setIsClinicDetailsOpen(false)} + clinic={selectedItem} + users={users} + onEdit={handleEditClinicFromDetails} + />
); }; From 490154601fe27d9e3479c91046c074a94916cda4 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 24 Apr 2026 09:27:56 +0200 Subject: [PATCH 2/5] refactor: move LogDetailsModal to standalone component --- .../src/components/admin/AuditLogView.jsx | 105 +----------------- .../src/components/admin/LogDetailsModal.jsx | 2 +- 2 files changed, 5 insertions(+), 102 deletions(-) diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index 49db74d..28d43f4 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -1,4 +1,5 @@ import React, { useState, useMemo } from 'react'; +import LogDetailsModal from './LogDetailsModal'; // Importeras nu som den enda källan till sanning import { FilePlus, RefreshCw, @@ -9,110 +10,11 @@ import { AlertCircle, Search, MapPin, - ShieldCheck, X, Activity, - Hash, - Info + Hash } from 'lucide-react'; -// --- MODAL KOMPONENT --- -const LogDetailsModal = ({ isOpen, onClose, log }) => { - if (!isOpen || !log) return null; - - const fullDate = new Date(log.createdAt).toLocaleString('sv-SE', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - - return ( -
-
- {/* HEADER */} -
- -
-
- -
-
-

Systemlogg / Detaljer

-

- {log.action.replace('_', ' ')} -

-
-
-
- - {/* INNEHÅLL */} -
-
- -

Beskrivning

-

{log.description}

-
- -
-
-

Utförd av

-
- - {log.performedByName} -
-
-
-

Klinik

-
- - {log.clinicName || 'System'} -
-
-
-

Patient

-
- - {log.petName || 'N/A'} -
-
-
-

Tidpunkt

-
- - {fullDate} -
-
-
- -
-
-
-
-

Referens-ID

-

{log.recordId}

-
-
- -
-
-
-
- ); -}; - // --- HUVUDKOMPONENT --- const AuditLogView = ({ logs = [], loading }) => { const [searchTerm, setSearchTerm] = useState(''); @@ -140,6 +42,7 @@ const AuditLogView = ({ logs = [], loading }) => { case 'CASE_CREATED': return { icon: , color: 'text-emerald-500', bg: 'bg-emerald-50' }; case 'STATUS_CHANGED': return { icon: , color: 'text-blue-500', bg: 'bg-blue-50' }; case 'COMMENT_ADDED': return { icon: , color: 'text-purple-500', bg: 'bg-purple-50' }; + case 'USER_REGISTERED': return { icon: , color: 'text-orange-500', bg: 'bg-orange-50' }; case 'ASSIGNED': return { icon: , color: 'text-orange-500', bg: 'bg-orange-50' }; default: return { icon: , color: 'text-slate-500', bg: 'bg-slate-50' }; } @@ -278,7 +181,7 @@ const AuditLogView = ({ logs = [], loading }) => { )}
- {/* MODAL RENDER */} + {/* MODAL RENDER - Använder nu den importerade komponenten */} setIsModalOpen(false)} diff --git a/frontend/src/components/admin/LogDetailsModal.jsx b/frontend/src/components/admin/LogDetailsModal.jsx index 261582c..e32d7f9 100644 --- a/frontend/src/components/admin/LogDetailsModal.jsx +++ b/frontend/src/components/admin/LogDetailsModal.jsx @@ -122,7 +122,7 @@ const LogDetailsModal = ({ isOpen, onClose, log }) => { onClick={onClose} className="w-full flex items-center justify-center gap-2 py-4 bg-slate-900 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl hover:bg-purple-600 transition-all shadow-xl active:scale-95 italic" > - Stäng detaljvy + Stäng
From f4059cde7da81ab3db087eb58d07ae9f76869647 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 24 Apr 2026 09:34:01 +0200 Subject: [PATCH 3/5] feat(admin): improve accessibility and UX for ClinicDetailsModal --- .../components/admin/ClinicDetailsModal.jsx | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/admin/ClinicDetailsModal.jsx b/frontend/src/components/admin/ClinicDetailsModal.jsx index 7d95fa9..21d26e3 100644 --- a/frontend/src/components/admin/ClinicDetailsModal.jsx +++ b/frontend/src/components/admin/ClinicDetailsModal.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { X, MapPin, @@ -7,11 +7,28 @@ import { Calendar, Activity, Edit2, - Users as UsersIcon, Award } from 'lucide-react'; const ClinicDetailsModal = ({ isOpen, onClose, clinic, users = [], onEdit }) => { + + // --- NYTT: Keyboard handler & Scroll-lock --- + useEffect(() => { + const handleEscape = (e) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + window.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; // Lås bakgrundsscroll + } + + return () => { + window.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; // Återställ scroll + }; + }, [isOpen, onClose]); + if (!isOpen || !clinic) return null; const clinicStaff = users.filter(u => @@ -20,14 +37,27 @@ const ClinicDetailsModal = ({ isOpen, onClose, clinic, users = [], onEdit }) => ); return ( -
-
+
+
e.stopPropagation()} + > {/* HEADER */}
@@ -37,10 +67,13 @@ const ClinicDetailsModal = ({ isOpen, onClose, clinic, users = [], onEdit }) =>
-

+

{clinic.name}

- {/* "Aktiv enhet" borttagen härifrån */}
@@ -69,7 +102,7 @@ const ClinicDetailsModal = ({ isOpen, onClose, clinic, users = [], onEdit }) =>
-

Anslutna Veterinärer ({clinicStaff.length})

+

Anslutna Veterinärer ({clinicStaff.length})

{clinicStaff.length > 0 ? ( From 1bc4bb94ceae23669cfbf2ab9cc41b2b654c0fd4 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 24 Apr 2026 09:38:07 +0200 Subject: [PATCH 4/5] accessibility: add a11y semantics and escape key support to LogDetailsModal --- .../src/components/admin/LogDetailsModal.jsx | 123 +++++++++--------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/admin/LogDetailsModal.jsx b/frontend/src/components/admin/LogDetailsModal.jsx index e32d7f9..ed5610e 100644 --- a/frontend/src/components/admin/LogDetailsModal.jsx +++ b/frontend/src/components/admin/LogDetailsModal.jsx @@ -1,19 +1,27 @@ -import React from 'react'; -import { - X, - Clock, - User as UserIcon, - MapPin, - Activity, - Hash, - Info, - ShieldCheck -} from 'lucide-react'; +import React, { useEffect } from 'react'; +import { X, ShieldCheck, Info, User as UserIcon, MapPin, Activity, Clock, Hash } from 'lucide-react'; const LogDetailsModal = ({ isOpen, onClose, log }) => { + // --- NYTT: Keyboard handler & Scroll-lock --- + useEffect(() => { + const handleEscape = (e) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + window.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; // Lås bakgrundsscroll + } + + return () => { + window.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; // Återställ scroll + }; + }, [isOpen, onClose]); + if (!isOpen || !log) return null; - const date = new Date(log.createdAt).toLocaleString('sv-SE', { + const fullDate = new Date(log.createdAt).toLocaleString('sv-SE', { year: 'numeric', month: 'long', day: 'numeric', @@ -23,25 +31,40 @@ const LogDetailsModal = ({ isOpen, onClose, log }) => { }); return ( -
-
- - {/* HEADER - Lila Audit-tema */} +
+
e.stopPropagation()} + > + {/* HEADER */}
-
-

Systemlogg / Händelse

-

+

Systemlogg / Detaljer

+

{log.action.replace('_', ' ')}

@@ -50,83 +73,59 @@ const LogDetailsModal = ({ isOpen, onClose, log }) => { {/* INNEHÅLL */}
- - {/* HUVUDBESKRIVNING */} -
+
-

Händelseförlopp

-

- {log.description} -

+

Beskrivning

+

{log.description}

- {/* DETALJERAD INFO GRID */}

Utförd av

{log.performedByName} - - {log.performedByRole?.replace('ROLE_', '')} -

Klinik

- {log.clinicName || 'Systemnivå'} + {log.clinicName || 'System'}
-

Patient (Referens)

+

Patient

{log.petName || 'N/A'}
-

Tidstämpel

+

Tidpunkt

- {date} + {fullDate}
- {/* TEKNISK INFO */} -
-
-
-
-
-

Ärende ID (Record)

-

- {log.recordId} -

-
-
-
-
-

Logg-ID

-

- {log.id} -

-
+
+
+
+
+

Referens-ID

+

{log.recordId}

+
- -
- -
); From eefc8e79584fcb42401415a45219bef109ebdb48 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 24 Apr 2026 09:42:34 +0200 Subject: [PATCH 5/5] fix: resolve race conditions and add dialog semantics to UserDetailsModal --- .../src/components/admin/UserDetailsModal.jsx | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/admin/UserDetailsModal.jsx b/frontend/src/components/admin/UserDetailsModal.jsx index 3dd8f7c..e175b2e 100644 --- a/frontend/src/components/admin/UserDetailsModal.jsx +++ b/frontend/src/components/admin/UserDetailsModal.jsx @@ -1,26 +1,50 @@ import React, { useState, useEffect } from 'react'; -import { X, Mail, Shield, Hospital, Calendar, Activity, User as UserIcon, Edit2, Award, Briefcase } from 'lucide-react'; +import { X, Mail, Shield, Hospital, Calendar, Activity, User as UserIcon, Edit2, Award } from 'lucide-react'; import { vetService } from '../../services/api'; const UserDetailsModal = ({ isOpen, onClose, user, clinics, onEdit }) => { const [vetDetails, setVetDetails] = useState(null); const [loadingVet, setLoadingVet] = useState(false); + // --- KEYBOARD & SCROLL LOCK & RACE CONDITION PROTECTION --- useEffect(() => { - // Om användaren är en veterinär, hämta extrauppgifter - if (isOpen && user?.role?.includes('VET')) { - setLoadingVet(true); - vetService.getAll() - .then(res => { - const details = res.data.find(v => v.userId === user.id); - setVetDetails(details); - }) - .catch(err => console.error("Kunde inte hämta veterinärinfo:", err)) - .finally(() => setLoadingVet(false)); + let ignore = false; + + const handleEscape = (e) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + window.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + + // Om användaren är en veterinär, hämta extrauppgifter + if (user?.role?.includes('VET')) { + setLoadingVet(true); + vetService.getAll() + .then(res => { + if (!ignore) { + const details = res.data.find(v => v.userId === user.id); + setVetDetails(details); + } + }) + .catch(err => { + if (!ignore) console.error("Kunde inte hämta veterinärinfo:", err); + }) + .finally(() => { + if (!ignore) setLoadingVet(false); + }); + } } else { setVetDetails(null); } - }, [isOpen, user]); + + return () => { + ignore = true; + window.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, user, onClose]); if (!isOpen || !user) return null; @@ -29,14 +53,24 @@ const UserDetailsModal = ({ isOpen, onClose, user, clinics, onEdit }) => { const isVet = user.role?.includes('VET'); return ( -
-
+
+
e.stopPropagation()} + > {/* HEADER */}
@@ -46,7 +80,10 @@ const UserDetailsModal = ({ isOpen, onClose, user, clinics, onEdit }) => {
-

+

{user.name}

@@ -72,7 +109,6 @@ const UserDetailsModal = ({ isOpen, onClose, user, clinics, onEdit }) => {
- {/* Renderas bara om användaren har en klinik kopplad */} {user.clinicId && (

Ansluten klinik

@@ -91,14 +127,22 @@ const UserDetailsModal = ({ isOpen, onClose, user, clinics, onEdit }) => {

Vet-ID / Legitimation

- {loadingVet ? '...' : vetDetails?.licenseId || 'Ej angivet'} + {loadingVet ? ( + Hämtar... + ) : ( + vetDetails?.licenseId || 'Ej angivet' + )}

Specialisering

- {loadingVet ? '...' : vetDetails?.specialization || 'Allmänpraktiserande'} + {loadingVet ? ( + ... + ) : ( + vetDetails?.specialization || 'Allmänpraktiserande' + )}