165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
"use client"
|
|
|
|
import { SettingsSectionHeader } from "@/components/gmail/settings/settings-section-header"
|
|
import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner"
|
|
import { AdminRuntimePanel } from "@/components/admin/settings/admin-runtime-panel"
|
|
import { useAdminStats } from "@/lib/api/hooks/use-admin-queries"
|
|
import { formatBytes } from "@/lib/admin/format-bytes"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
|
|
export function OverviewSection() {
|
|
const { data, isFetching, isError, refetch } = useAdminStats()
|
|
|
|
const storage = data?.storage
|
|
|
|
return (
|
|
<>
|
|
<SettingsSectionHeader
|
|
title="Vue d'ensemble"
|
|
description="Activité de la plateforme, stockage et configuration runtime."
|
|
/>
|
|
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
|
|
<AdminRuntimePanel />
|
|
|
|
{data ? (
|
|
<div className="space-y-6">
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<StatCard label="Utilisateurs" value={data.users.total} hint={`${data.users.active} actifs`} />
|
|
<StatCard label="Invitations" value={data.users.invited} hint={`${data.users.disabled} désactivés`} />
|
|
<StatCard label="Comptes mail" value={data.services.mail_accounts_total} />
|
|
<StatCard label="Messages" value={data.services.messages_total} />
|
|
</div>
|
|
|
|
{storage ? (
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
<StorageCard
|
|
label="Mail (plateforme)"
|
|
used={storage.mail.used_bytes}
|
|
allocated={storage.mail.allocated_bytes}
|
|
/>
|
|
<StorageCard
|
|
label="Drive (plateforme)"
|
|
used={storage.drive.used_bytes}
|
|
allocated={storage.drive.allocated_bytes}
|
|
tracked={storage.drive.tracked}
|
|
/>
|
|
<StorageCard
|
|
label="Photos (plateforme)"
|
|
used={storage.photos.used_bytes}
|
|
allocated={storage.photos.allocated_bytes}
|
|
tracked={storage.photos.tracked}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Audit (24 h)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-semibold">{data.services.audit_events_24h}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">événements enregistrés</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Quotas mail</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-semibold">{data.quotas.users_near_mail_quota_90pct}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
utilisateurs à plus de 90 % du quota
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{data.audit.top_actors_7d.length > 0 ? (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Top acteurs (7 j)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{data.audit.top_actors_7d.map((item) => (
|
|
<div
|
|
key={item.actor}
|
|
className="flex items-center justify-between text-sm"
|
|
>
|
|
<span className="truncate font-mono text-xs">{item.actor}</span>
|
|
<span className="text-muted-foreground">{item.count}</span>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
) : isFetching ? (
|
|
<p className="text-sm text-muted-foreground">Chargement des statistiques…</p>
|
|
) : null}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function StatCard({
|
|
label,
|
|
value,
|
|
hint,
|
|
}: {
|
|
label: string
|
|
value: number
|
|
hint?: string
|
|
}) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-semibold">{value.toLocaleString("fr-FR")}</p>
|
|
{hint ? <p className="mt-1 text-xs text-muted-foreground">{hint}</p> : null}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function StorageCard({
|
|
label,
|
|
used,
|
|
allocated,
|
|
tracked = true,
|
|
}: {
|
|
label: string
|
|
used: number
|
|
allocated: number
|
|
tracked?: boolean
|
|
}) {
|
|
const pct = allocated > 0 ? Math.min(100, Math.round((used / allocated) * 100)) : 0
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">{label}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<p className="text-lg font-semibold">
|
|
{formatBytes(used)}
|
|
<span className="text-sm font-normal text-muted-foreground">
|
|
{" "}
|
|
/ {formatBytes(allocated)}
|
|
</span>
|
|
</p>
|
|
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
<div
|
|
className="h-full rounded-full bg-primary transition-all"
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{tracked ? `${pct} % des quotas alloués` : "Mesure non disponible"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|