From 63e4614c1c002197ce35b8605bc37b090d1ea6c7 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 23 Apr 2026 22:18:59 +0200 Subject: [PATCH 1/5] feat(admin): implement advanced filtering for audit logs(#248) --- .../src/components/admin/AuditLogView.jsx | 241 ++++++++++++++---- .../activitylog/ActivityLogResponse.java | 6 + 2 files changed, 197 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index df47c710..c8414381 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { FilePlus, RefreshCw, @@ -6,12 +6,25 @@ import { UserPlus, Clock, User as UserIcon, - AlertCircle + AlertCircle, + Search, + MapPin, + ShieldCheck } from 'lucide-react'; 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(''); + + const clinics = useMemo(() => { + const uniqueClinics = [...new Set(logs.map(log => log.clinicName))].filter(Boolean); + return uniqueClinics.sort(); + }, [logs]); - // Mappa actions till färger och ikoner const getActionStyle = (action) => { switch (action) { case 'CASE_CREATED': return { icon: , color: 'text-emerald-500', bg: 'bg-emerald-50' }; @@ -22,57 +35,185 @@ const AuditLogView = ({ logs = [], loading }) => { } }; - if (loading) return
Laddar historik...
; - if (logs.length === 0) return ( -
- -

Ingen aktivitet hittades

-
- ); + const filteredLogs = useMemo(() => { + return logs.filter(log => { + const searchLower = searchTerm.toLowerCase(); + const matchesSearch = + log.performedByName?.toLowerCase().includes(searchLower) || + log.description?.toLowerCase().includes(searchLower) || + log.clinicName?.toLowerCase().includes(searchLower) || + log.petName?.toLowerCase().includes(searchLower) || + log.recordId?.includes(searchTerm); + + const matchesAction = actionFilter === 'ALL' || log.action === actionFilter; + const matchesRole = roleFilter === 'ALL' || log.performedByRole === roleFilter; + const matchesClinic = clinicFilter === 'ALL' || log.clinicName === clinicFilter; + const matchesDate = !dateFilter || log.createdAt.startsWith(dateFilter); + + return matchesSearch && matchesAction && matchesRole && matchesDate && matchesClinic; + }); + }, [logs, searchTerm, actionFilter, roleFilter, clinicFilter, dateFilter]); + + if (loading) return
Laddar administrativ historik...
; return ( -
- {logs.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' - }); - - return ( -
- {/* Timeline line */} -
- - {/* Icon */} -
- {style.icon} -
- - {/* Content */} -
-
-

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

- - {date} - -
-

{log.description}

- -
-
- - - {log.performedByName} - +
+ {/* --- ADMIN FILTER PANEL --- */} +
+
+ +
+ + setSearchTerm(e.target.value)} + /> +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ 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" + /> +
+
+
+ + {/* --- LOG LIST --- */} +
+ {filteredLogs.length === 0 ? ( +
+ +

Inga loggar matchar administratörens 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' + }); + + return ( +
+
+ +
+ {style.icon} +
+ +
+
+
+

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

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

{log.description}

+ +
+ {/* Utförare */} +
+ + + {log.performedByName} + +
+ {/* DJURETS NAMN (NYTT) */} +
+ Patient: + + {log.petName} + +
+ + {/* Klinik */} +
+ + + {log.clinicName || 'System'} + +
+ + {/* Ärende-ID */} +
+ Ref: + + {log.recordId} + +
+
+
- ID: {log.recordId.substring(0, 8)}... -
-
+ ); + })}
- ); - })} + )} +
); }; diff --git a/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java b/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java index e966df77..a444ec05 100644 --- a/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java +++ b/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java @@ -13,7 +13,10 @@ public record ActivityLogResponse( String description, UUID performedById, String performedByName, + String performedByRole, UUID recordId, + String clinicName, + String petName, Instant createdAt ) { @@ -24,7 +27,10 @@ public static ActivityLogResponse from(ActivityLog log) { log.getDescription(), log.getPerformedBy().getId(), log.getPerformedBy().getName(), + log.getPerformedBy().getRole().name(), log.getMedicalRecord().getId(), + log.getMedicalRecord().getClinic().getName(), + log.getMedicalRecord().getPet() != null ? log.getMedicalRecord().getPet().getName() : "Okänt djur", log.getCreatedAt() ); } From cbda13cfeb14d8d0cb88d5723ff0c481129f2598 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 23 Apr 2026 22:46:54 +0200 Subject: [PATCH 2/5] feat(admin): implement advanced filtering for audit logs(#248) --- frontend/src/components/admin/AuditLogView.jsx | 1 + .../vet1177/dto/response/activitylog/ActivityLogResponse.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index c8414381..ba36a7c1 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -35,6 +35,7 @@ const AuditLogView = ({ logs = [], loading }) => { } }; + const filteredLogs = useMemo(() => { return logs.filter(log => { const searchLower = searchTerm.toLowerCase(); diff --git a/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java b/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java index a444ec05..ac43ab8c 100644 --- a/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java +++ b/src/main/java/org/example/vet1177/dto/response/activitylog/ActivityLogResponse.java @@ -27,9 +27,9 @@ public static ActivityLogResponse from(ActivityLog log) { log.getDescription(), log.getPerformedBy().getId(), log.getPerformedBy().getName(), - log.getPerformedBy().getRole().name(), + log.getPerformedBy().getRole() != null ? log.getPerformedBy().getRole().name() : "ROLE_UNKNOWN", log.getMedicalRecord().getId(), - log.getMedicalRecord().getClinic().getName(), + log.getMedicalRecord().getClinic() != null ? log.getMedicalRecord().getClinic().getName() : "Okänd klinik", log.getMedicalRecord().getPet() != null ? log.getMedicalRecord().getPet().getName() : "Okänt djur", log.getCreatedAt() ); From c39d948593299fdcd708383e0384df77e329fc7f Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 23 Apr 2026 22:48:14 +0200 Subject: [PATCH 3/5] fix(audit-log): normalize role comparison in filter logic --- frontend/src/components/admin/AuditLogView.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index ba36a7c1..ed1ea27b 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -35,7 +35,7 @@ const AuditLogView = ({ logs = [], loading }) => { } }; - + const filteredLogs = useMemo(() => { return logs.filter(log => { const searchLower = searchTerm.toLowerCase(); @@ -47,7 +47,9 @@ const AuditLogView = ({ logs = [], loading }) => { log.recordId?.includes(searchTerm); const matchesAction = actionFilter === 'ALL' || log.action === actionFilter; - const matchesRole = roleFilter === 'ALL' || log.performedByRole === roleFilter; + const matchesRole = roleFilter === 'ALL' || + log.performedByRole?.replace('ROLE_', '') === roleFilter.replace('ROLE_', ''); + const matchesClinic = clinicFilter === 'ALL' || log.clinicName === clinicFilter; const matchesDate = !dateFilter || log.createdAt.startsWith(dateFilter); From 5110d3dd4869948d1c57416e5502e951a61ae1cd Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 23 Apr 2026 22:56:45 +0200 Subject: [PATCH 4/5] fix(ui): improve role tag rendering and safety in AuditLogView --- frontend/src/components/admin/AuditLogView.jsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index ed1ea27b..70faa813 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -164,12 +164,17 @@ const AuditLogView = ({ logs = [], loading }) => { {/* Roll-tagg */} - {log.performedByRole.replace('ROLE_', '')} - + + {log.performedByRole?.replace('ROLE_', '') ?? 'SYSTEM'} +
{date} From 377b51b277aff9071c5b37305b3f07c907ddcfd6 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 23 Apr 2026 22:58:47 +0200 Subject: [PATCH 5/5] fix(audit-log): harden date filtering and prevent potential crashes --- frontend/src/components/admin/AuditLogView.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/admin/AuditLogView.jsx b/frontend/src/components/admin/AuditLogView.jsx index 70faa813..5750b066 100644 --- a/frontend/src/components/admin/AuditLogView.jsx +++ b/frontend/src/components/admin/AuditLogView.jsx @@ -51,7 +51,7 @@ const AuditLogView = ({ logs = [], loading }) => { log.performedByRole?.replace('ROLE_', '') === roleFilter.replace('ROLE_', ''); const matchesClinic = clinicFilter === 'ALL' || log.clinicName === clinicFilter; - const matchesDate = !dateFilter || log.createdAt.startsWith(dateFilter); + const matchesDate = !dateFilter || (log.createdAt?.startsWith(dateFilter) ?? false); return matchesSearch && matchesAction && matchesRole && matchesDate && matchesClinic; });