Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/AuditLog.tsx
Nathnael 6f14cf8bbf feat(freight-backoffice): add DateRangePicker, replace ad-hoc from/to date filters
Adds a shadcn Popover+Calendar range picker with presets (Today, Last 7/30
days, this/last month, YTD) and Apply/Cancel. Swaps it into every shadcn-family
from-to date filter: activity log, sector reports, incoming records (EN
branch), CombinedFilterBar (13 consumers), user records, audit log — also
fixes an uncontrolled-input bug on the audit log date fields.

Mantine-based ListControls/ReportFilters left untouched (different UI kit).
2026-08-13 08:33:59 +00:00

256 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from "react";
import { Input } from "@/shared/common/ui/input";
import { DateRangePicker, parseDay, formatDay } from "@/shared/common/ui/date-range-picker";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/shared/common/ui/select";
import { Button } from "@/shared/common/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/common/ui/table";
import { CollectionQueryDTO, fetchAuditLogs } from "@/user-management/services/api/auditService";
import { useTranslation } from "react-i18next";
import { useLocalizedName } from "@/shared/common/localizedName";
interface AuditLogItem {
id: string;
user: string | { id: string; name: string; email?: string; phone?: string; position?: string; userType?: string; employeeId?: string; employeePositionId?: string };
action: string;
message: string;
timestamp: string;
}
const AuditLog: React.FC = () => {
const {t} = useTranslation()
const [logs, setLogs] = useState<AuditLogItem[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const [sort, setSort] = useState("timestamp:DESC");
const [dateRange, setDateRange] = useState<{ start?: string; end?: string }>({});
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [status, setStatus] = useState("")
const localizedName = useLocalizedName()
const pageSize = 10;
const statuses = [
{
status: "Draft",
color: "bg-gray-200",
detail: t("landingPage.notForwarded"),
},
{
status: "Submitted",
color: "bg-purple-100",
detail: t("landingPage.awaiting"),
},
{
status: "Accepted",
color: "bg-primary-100",
detail: t("landingPage.accepted"),
},
{
status: "Approved",
color: "bg-primary-200",
detail: t("landingPage.approvedSent"),
},
{
status: "Adjustment",
color: "bg-yellow-100",
detail: t("landingPage.returned"),
},
{
status: "Rejected",
color: "bg-red-100",
detail: t("landingPage.rejected"),
},
{ status: "Sent", color: "bg-primary-300", detail: t("landingPage.sent") },
{
status: "Returned",
color: "bg-gray-400",
detail: t("landingPage.returnedByOfficer"),
},
];
const buildQuery = (): CollectionQueryDTO => {
const query: CollectionQueryDTO = {
s: "id,user,action,message,timestamp",
o: sort,
t: pageSize,
sk: (page - 1) * pageSize,
};
const where: string[] = [];
if (search) where.push(`message:ILIKE:%${search}%`);
if (status) where.push(`status:=:${status}`);
if (status && status !== "all") where.push(`status:=:${status}`);
if (dateRange.start && dateRange.end)
where.push(`timestamp:>=:${dateRange.start}|timestamp:<=:${dateRange.end}`);
if (where.length) query.w = where.join("|");
return query;
};
const fetchLogs = async () => {
setLoading(true);
try {
const result = await fetchAuditLogs(buildQuery());
setLogs(result.items);
setTotal(result.total ?? 0);
} catch (err) {
console.error(err);
setLogs([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLogs();
}, [search, sort, dateRange, page]);
return (
<div className="p-6 bg-background min-h-screen">
<h1 className="text-2xl font-semibold mb-6 text-foreground">
Audit Logs
</h1>
{/* Filter Bar */}
<div className="flex flex-wrap gap-3 mb-5 items-center">
<Input
placeholder="Search message..."
value={search}
onChange={(e: any) => setSearch(e.target.value)}
className="w-64"
/>
<DateRangePicker
value={{ from: parseDay(dateRange.start), to: parseDay(dateRange.end) }}
onChange={(range) =>
setDateRange({ start: formatDay(range.from), end: formatDay(range.to) })
}
/>
<Select value={sort} onValueChange={setSort}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="timestamp:DESC">Newest First</SelectItem>
<SelectItem value="timestamp:ASC">Oldest First</SelectItem>
<SelectItem value="user:ASC">User (AZ)</SelectItem>
<SelectItem value="user:DESC">User (ZA)</SelectItem>
</SelectContent>
</Select>
{/* ✅ Status Dropdown */}
<Select value={status} onValueChange={setStatus}>
<SelectTrigger className="w-[200px]">
<SelectValue
placeholder={
t("auditLogs.filterByStatus") || "Filter by status"
}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t("common.all") || "All"}</SelectItem>
{statuses.map((s) => (
<SelectItem key={s.status} value={s.status}>
{t(`${s.status}`) || s.status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Table */}
<div className="rounded-xl border bg-card shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Action</TableHead>
<TableHead>Message</TableHead>
<TableHead>Timestamp</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell
colSpan={4}
className="text-center py-6 text-muted-foreground">
Loading logs...
</TableCell>
</TableRow>
) : logs.length > 0 ? (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell className="font-medium">
{typeof log.user === "string"
? log.user
: typeof log.user?.name === "string"
? log.user.name
: localizedName(log.user?.name) || "Unknown"}
</TableCell>
<TableCell>{log.action}</TableCell>
<TableCell>{log.message}</TableCell>
<TableCell className="text-muted-foreground">
{log.timestamp && !isNaN(new Date(log.timestamp).getTime())
? new Date(log.timestamp).toLocaleString("sv-SE")
: "N/A"}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={4}
className="text-center py-6 text-muted-foreground">
No logs found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex justify-between items-center mt-5 text-sm">
<Button
variant="secondary"
disabled={page === 1}
onClick={() => setPage((p) => Math.max(p - 1, 1))}>
Previous
</Button>
<span className="text-muted-foreground">
Page {page} of {Math.ceil(total / pageSize) || 1}
</span>
<Button
variant="secondary"
disabled={page * pageSize >= total}
onClick={() => setPage((p) => p + 1)}>
Next
</Button>
</div>
</div>
);
};
export default AuditLog;