diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index 5750b06..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,17 +10,28 @@ import { AlertCircle, Search, MapPin, - ShieldCheck + X, + Activity, + Hash } from 'lucide-react'; +// --- 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(); @@ -30,12 +42,12 @@ 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' }; } }; - const filteredLogs = useMemo(() => { return logs.filter(log => { const searchLower = searchTerm.toLowerCase(); @@ -61,159 +73,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 +180,13 @@ const AuditLogView = ({ logs = [], loading }) => {
)}
+ + {/* MODAL RENDER - Använder nu den importerade komponenten */} + 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..21d26e3 --- /dev/null +++ b/frontend/src/components/admin/ClinicDetailsModal.jsx @@ -0,0 +1,170 @@ +import React, { useEffect } from 'react'; +import { + X, + MapPin, + Phone, + Hospital, + Calendar, + Activity, + Edit2, + 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 => + u.clinicId === clinic.id && + u.role?.includes('VET') + ); + + return ( +
+
e.stopPropagation()} + > + + {/* HEADER */} +
+ + +
+
+ +
+
+

+ {clinic.name} +

+
+
+
+ + {/* 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..ed5610e --- /dev/null +++ b/frontend/src/components/admin/LogDetailsModal.jsx @@ -0,0 +1,134 @@ +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 fullDate = new Date(log.createdAt).toLocaleString('sv-SE', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + return ( +
+
e.stopPropagation()} + > + {/* 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}

+
+
+ +
+
+
+
+ ); +}; + +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..e175b2e --- /dev/null +++ b/frontend/src/components/admin/UserDetailsModal.jsx @@ -0,0 +1,190 @@ +import React, { useState, useEffect } from '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(() => { + 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); + } + + return () => { + ignore = true; + window.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, user, onClose]); + + 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 ( +
+
e.stopPropagation()} + > + + {/* HEADER */} +
+ + +
+
+ +
+
+

+ {user.name} +

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

E-postadress

+
+ + {user.email} +
+
+ + {user.clinicId && ( +
+

Ansluten klinik

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

Vet-ID / Legitimation

+
+ + {loadingVet ? ( + Hämtar... + ) : ( + 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} + />
); };