+
{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 */}