Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/AuditLog.tsx
Marshal 0d8c63328a add items per wagon map to cargo types and sync bulk rate units
- Implemented  in the  to manage the physical item capacity for each wagon type.
- Added a new migration to create the  column in the  table.
- Introduced  method in  to update rate units when cargo type unit of measure changes.
- Updated booking calculations to consider items per wagon for break-bulk cargo.
- Refactored various components to utilize the new items fit logic and ensure consistent date formatting across the application.
- Added tests for the new display timezone functionality to ensure consistent date/time representation across different user settings.
2026-08-01 09:55:21 +00:00

264 lines
7.7 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 {
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())
? 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;