From bb501b8eb318c596da421f241009fa59edbae36f Mon Sep 17 00:00:00 2001 From: Riyadh Date: Tue, 19 May 2026 12:11:54 +0000 Subject: [PATCH] Improve audit log filtering with a searchable user list and better date formatting Introduces a combobox for filtering audit logs by actor, replaces multiple date formatting calls with a dedicated helper function, and updates locale files for new filter UI elements. --- artifacts/tx-os/src/locales/ar.json | 5 +- artifacts/tx-os/src/locales/en.json | 5 +- .../tx-os/src/pages/executive-meetings.tsx | 147 ++++++++++++++++-- 3 files changed, 138 insertions(+), 19 deletions(-) diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index b4aec812..d250e339 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -1526,7 +1526,10 @@ "filter": { "from": "من", "to": "إلى", - "actorId": "معرّف المنفّذ" + "actorId": "معرّف المنفّذ", + "actorPlaceholder": "كل المستخدمين", + "actorSearch": "ابحث بالاسم...", + "actorEmpty": "ما فيه نتائج" }, "pageInfo": "عرض", "all": "الكل", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 3956cf17..f464bc1c 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -1399,7 +1399,10 @@ "filter": { "from": "From", "to": "To", - "actorId": "Actor ID" + "actorId": "Actor ID", + "actorPlaceholder": "All users", + "actorSearch": "Search by name...", + "actorEmpty": "No matches" }, "pageInfo": "Showing", "all": "All", diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index 774b3c78..37a25963 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -117,7 +117,16 @@ import { GripVertical } from "lucide-react"; import { EditableCell } from "@/components/editable-cell"; import { Composer } from "@/components/notes/composer"; import { SendNoteDialog } from "@/components/notes/send-note-dialog"; -import { useNoteLabels } from "@/lib/notes-api"; +import { useNoteLabels, type UserSummary } from "@/lib/notes-api"; +import { cn } from "@/lib/utils"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; // #486: reuse the same dialog the upcoming-alert pops, so postponing // from the schedule row quick-actions menu shares one implementation. import { PostponeDialog } from "@/components/executive-meetings/upcoming-meeting-alert"; @@ -209,6 +218,24 @@ type AuditEntry = { } | null; }; +// #616-followup: localized 12h timestamp for the audit log + PDF archive +// listing. Format: dd/mm/yyyy h:mm <ص|م> in Arabic, or h:mm AM/PM in +// English. Seconds dropped to keep rows compact, day before month so +// Arabic readers don't have to mentally re-order the US m/d/y default. +function formatAuditDateTime(iso: string, lang: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + const dd = String(d.getDate()).padStart(2, "0"); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const yyyy = d.getFullYear(); + let h = d.getHours(); + const min = String(d.getMinutes()).padStart(2, "0"); + const isPm = h >= 12; + h = h % 12 || 12; + const suffix = lang === "ar" ? (isPm ? "م" : "ص") : isPm ? "PM" : "AM"; + return `${dd}/${mm}/${yyyy} ${h}:${min} ${suffix}`; +} + type FontPrefs = { fontFamily: string; fontSize: number; @@ -7933,11 +7960,28 @@ function AuditSection({ t: (k: string) => string; }) { const isMobile = useIsMobile(); + const lang = isRtl ? "ar" : "en"; const [date, setDate] = useState(""); const [dateTo, setDateTo] = useState(""); const [entityType, setEntityType] = useState("all"); const [actionFilter, setActionFilter] = useState("all"); - const [actorFilter, setActorFilter] = useState(""); + // #616-followup: actor filter is now a user id picked from a searchable + // combobox of active users (replaces the old free-form numeric input — + // no one memorises raw user IDs). Empty string = no filter. + const [actorFilter, setActorFilter] = useState(""); + const [actorPickerOpen, setActorPickerOpen] = useState(false); + const { data: directory } = useQuery({ + queryKey: ["users", "directory"], + queryFn: () => apiJson("/api/users/directory"), + enabled: canView, + }); + const actors = directory ?? []; + const selectedActor = + actorFilter !== "" ? actors.find((u) => u.id === actorFilter) ?? null : null; + const actorDisplay = (u: UserSummary) => + (lang === "ar" + ? u.displayNameAr ?? u.displayNameEn + : u.displayNameEn ?? u.displayNameAr) ?? u.username; const [page, setPage] = useState(0); const PAGE_SIZE = 50; useEffect(() => { @@ -7965,7 +8009,7 @@ function AuditSection({ if (dateTo) qs.set("dateTo", dateTo); if (entityType !== "all") qs.set("entityType", entityType); if (actionFilter !== "all") qs.set("action", actionFilter); - if (actorFilter.trim()) qs.set("actorId", actorFilter.trim()); + if (actorFilter !== "") qs.set("actorId", String(actorFilter)); qs.set("limit", String(PAGE_SIZE)); qs.set("offset", String(page * PAGE_SIZE)); return apiJson(`/api/executive-meetings/audit-logs?${qs}`); @@ -8003,14 +8047,86 @@ function AuditSection({ data-testid="em-audit-date-to" /> - setActorFilter(e.target.value)} - className="w-32 h-9" - data-testid="em-audit-actor" - /> + + + + + + { + if (!search) return 1; + return value.toLowerCase().includes(search.toLowerCase()) + ? 1 + : 0; + }} + > + + + + {t("executiveMeetings.audit.filter.actorEmpty")} + + + { + setActorFilter(""); + setActorPickerOpen(false); + }} + data-testid="em-audit-actor-option-any" + > + + {t("executiveMeetings.audit.filter.actorPlaceholder")} + + + {actors.map((u) => { + const display = actorDisplay(u); + return ( + { + setActorFilter(u.id); + setActorPickerOpen(false); + }} + data-testid={`em-audit-actor-option-${u.id}`} + > +
+
+ {display} +
+
+ @{u.username} +
+
+
+ ); + })} +
+
+
+
+