import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ApiError, can, fetchRecycleBin, purgeRecycleItem, restoreRecycleItem, type RecycleBinItem, type RecycleKind, type SessionUser, } from "./api.js"; import { qk } from "./lib/query.js"; import { formatRelativeDateTime } from "./lib/format.js"; import { Modal } from "./ui/Modal.js"; // Recycle bin — the way back from an accidental delete. Lists everything soft-deleted // across users/roles/subscriptions/plans/tariffs; an admin can Restore (back to its // catalog) or Purge (permanent). Items auto-purge after the retention window. Gated by // recyclebin:* (read to view, update to restore, delete to purge). See // apps/server/src/recycle-bin.ts, wiki/concepts/soft-delete.md. const KIND_KEY: Record = { user: "recycleBin.kind.user", role: "recycleBin.kind.role", subscription: "recycleBin.kind.subscription", plan: "recycleBin.kind.plan", tariff: "recycleBin.kind.tariff", }; export function RecycleBin({ user }: { user: SessionUser | null }) { const { t } = useTranslation(); const qc = useQueryClient(); const binQ = useQuery({ queryKey: qk.recycleBin, queryFn: fetchRecycleBin }); const canRestore = can(user, "recyclebin:update"); const canPurge = can(user, "recyclebin:delete"); const [error, setError] = useState(null); const [purging, setPurging] = useState(null); const onError = (e: unknown) => setError(e instanceof ApiError ? e.message : (e as Error).message); const invalidate = () => { void qc.invalidateQueries({ queryKey: qk.recycleBin }); // A restore/purge can change any catalog — refresh the ones a restore touches. for (const key of [["users"], ["roles"], ["subscriptions"], ["subscription-plans"], ["tariff"]]) { void qc.invalidateQueries({ queryKey: key }); } }; const restoreM = useMutation({ mutationFn: ({ kind, id }: { kind: RecycleKind; id: string }) => restoreRecycleItem(kind, id), onSuccess: invalidate, onError, }); const purgeM = useMutation({ mutationFn: ({ kind, id }: { kind: RecycleKind; id: string }) => purgeRecycleItem(kind, id), onSuccess: () => { setPurging(null); invalidate(); }, onError: (e) => { setPurging(null); onError(e); }, }); const items = binQ.data?.items ?? []; const retentionDays = binQ.data?.retentionDays ?? 0; return (

{t("recycleBin.title")}

{retentionDays > 0 && ( {t("recycleBin.retentionNote", { days: retentionDays })} )}
{error &&

{error}

} {binQ.isLoading &&

{t("common.loading")}

} {!binQ.isLoading && items.length === 0 ? (

{t("recycleBin.empty")}

) : ( {items.map((it) => ( ))}
{t("recycleBin.col.type")} {t("recycleBin.col.item")} {t("recycleBin.col.deleted")} {t("recycleBin.col.actions")}
{t(KIND_KEY[it.kind])} {it.label} {formatRelativeDateTime(it.deletedAt, t)} {canRestore && ( )} {canPurge && ( )}
)} {purging && ( setPurging(null)} title={t("recycleBin.purgeConfirmTitle")}>

{t("recycleBin.purgeConfirmBody", { label: purging.label })}

{t("recycleBin.purgeIrreversible")}

)}
); }