mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
265 lines
7.8 KiB
TypeScript
265 lines
7.8 KiB
TypeScript
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 (A–Z)</SelectItem>
|
||
<SelectItem value="user:DESC">User (Z–A)</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;
|