ultisuite-client/components/admin/settings/sections/audit-section.tsx
2026-06-07 21:55:42 +02:00

118 lines
3.9 KiB
TypeScript

"use client"
import { useState } from "react"
import { Download } from "lucide-react"
import { SettingsSectionHeader } from "@/components/gmail/settings/settings-section-header"
import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner"
import { useAdminAuditLogs } from "@/lib/api/hooks/use-admin-queries"
import { apiClient } from "@/lib/api/client"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function AuditSection() {
const [page, setPage] = useState(1)
const { data, isFetching, isError, refetch } = useAdminAuditLogs({ page, page_size: 50 })
const logs = data?.logs ?? []
const total = data?.pagination.total ?? 0
const pageSize = data?.pagination.page_size ?? 50
const totalPages = Math.max(1, Math.ceil(total / pageSize))
async function exportLogs(format: "csv" | "ndjson") {
const blob = await apiClient.getBlob(`/admin/audit/export?format=${format}&limit=5000`)
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `audit-export.${format === "csv" ? "csv" : "ndjson"}`
a.click()
URL.revokeObjectURL(url)
}
return (
<>
<SettingsSectionHeader
title="Journal d'audit"
description="Historique des actions administratives sur la plateforme."
/>
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
<div className="mb-4 flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={() => void exportLogs("csv")}>
<Download className="mr-2 size-4" />
Export CSV
</Button>
<Button variant="outline" size="sm" onClick={() => void exportLogs("ndjson")}>
<Download className="mr-2 size-4" />
Export NDJSON
</Button>
</div>
<div className="overflow-x-auto rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Acteur</TableHead>
<TableHead>Action</TableHead>
<TableHead className="hidden lg:table-cell">Détails</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Aucun événement.
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell className="whitespace-nowrap text-xs">
{new Date(log.created_at).toLocaleString("fr-FR")}
</TableCell>
<TableCell className="max-w-[140px] truncate font-mono text-xs">
{log.actor}
</TableCell>
<TableCell className="text-sm">{log.action}</TableCell>
<TableCell className="hidden max-w-md truncate font-mono text-xs lg:table-cell">
{typeof log.details === "string"
? log.details
: JSON.stringify(log.details)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{totalPages > 1 ? (
<div className="mt-4 flex justify-end gap-2">
<Button
variant="outline"
size="sm"
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
Précédent
</Button>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages}
onClick={() => setPage((p) => p + 1)}
>
Suivant
</Button>
</div>
) : null}
</>
)
}