Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/AuditLog.tsx
yaschalew 697799b00d fix
2026-07-10 14:24:58 +03:00

265 lines
7.8 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 { format } from "date-fns";
import { Input } from "@/shared/common/ui/input";
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"
/>
<div className="flex gap-2 items-center">
<Input
type="date"
onChange={(e) =>
setDateRange({ ...dateRange, start: e.target.value })
}
/>
<span className="text-muted-foreground text-sm">to</span>
<Input
type="date"
onChange={(e) =>
setDateRange({ ...dateRange, end: e.target.value })
}
/>
</div>
<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())
? format(new Date(log.timestamp), "yyyy-MM-dd HH:mm:ss")
: "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;