feat: purger les contenus expirés ou anciens
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m30s
Validation applicative / TypeScript, tests et build (push) Successful in 1m52s

This commit is contained in:
manus-admin
2026-10-09 15:46:52 +02:00
parent 648ff4ca75
commit 716431ede3
5 changed files with 255 additions and 18 deletions

View File

@@ -41,6 +41,7 @@ import { fr } from "date-fns/locale";
type AAPCategorie = "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
type ReadFilter = "unread" | "all" | "read";
type AapPurgeScope = "all" | "expired" | "older_than";
interface AAPItem {
id: number;
@@ -78,6 +79,7 @@ const CAT_ACCENT: Record<string, string> = {
};
const PAGE_SIZE = 24;
const PURGE_MONTH_OPTIONS = [1, 3, 6, 12, 18, 24] as const;
function formatDate(d: Date | null | undefined): string | null {
if (!d) return null;
@@ -137,12 +139,16 @@ export default function AAPDashboard() {
});
const unreadCountQuery = trpc.aap.unreadCount.useQuery();
const unreadCount = unreadCountQuery.data?.count ?? 0;
const [purgeScope, setPurgeScope] = useState<AapPurgeScope>("expired");
const [purgeMonths, setPurgeMonths] = useState<(typeof PURGE_MONTH_OPTIONS)[number]>(6);
const purgeMutation = trpc.aap.purge.useMutation({
onSuccess: (data) => {
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
utils.aap.list.invalidate();
utils.aap.filters.invalidate();
utils.aap.unreadCount.invalidate();
utils.aap.getReadIds.invalidate();
},
onError: (err) => {
toast.error(`Erreur lors de la purge : ${err.message}`);
@@ -258,11 +264,58 @@ export default function AAPDashboard() {
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle size={20} className="text-destructive" />
Purger tous les appels à projets
Purger les appels à projets
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2">
<p>Cette action va <strong>supprimer définitivement</strong> tous les appels à projets.</p>
<div className="space-y-4">
<p>Choisissez les AAP à <strong>supprimer définitivement</strong>.</p>
<div className="grid gap-2">
{([
["expired", "AAP clôturés"],
["older_than", "AAP plus anciens"],
["all", "Tous les AAP"],
] as const).map(([scope, label]) => (
<button
key={scope}
type="button"
onClick={() => setPurgeScope(scope)}
className={cn(
"rounded-lg border-2 px-3 py-2 text-left text-sm font-medium transition-colors",
purgeScope === scope
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/40 text-foreground",
)}
>
{label}
</button>
))}
</div>
{purgeScope === "older_than" && (
<div className="grid grid-cols-3 gap-2">
{PURGE_MONTH_OPTIONS.map((months) => (
<button
key={months}
type="button"
onClick={() => setPurgeMonths(months)}
className={cn(
"rounded-lg border-2 px-2 py-2 text-sm font-medium transition-colors",
purgeMonths === months
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/40 text-foreground",
)}
>
{months} mois
</button>
))}
</div>
)}
<p>
{purgeScope === "expired"
? "Seuls les AAP dont la date de clôture est dépassée seront supprimés."
: purgeScope === "all"
? "Tous les appels à projets seront supprimés."
: `Les AAP publiés, ou importés sans date de publication, il y a plus de ${purgeMonths} mois seront supprimés.`}
</p>
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
</div>
</AlertDialogDescription>
@@ -271,10 +324,14 @@ export default function AAPDashboard() {
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => purgeMutation.mutate()}
onClick={() => purgeMutation.mutate(
purgeScope === "older_than"
? { scope: "older_than", months: purgeMonths }
: { scope: purgeScope },
)}
disabled={purgeMutation.isPending}
>
{purgeMutation.isPending ? "Purge en cours..." : "Oui, purger tous les appels à projets"}
{purgeMutation.isPending ? "Purge en cours..." : "Oui, supprimer définitivement"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -48,6 +48,7 @@ import { fr } from "date-fns/locale";
type TypeVeille = "reglementaire" | "concurrentielle" | "technologique" | "informationnelle";
type ReadFilter = "unread" | "all" | "read";
type VeillePurgeScope = "all" | "older_than";
interface VeilleItem {
id: number;
@@ -97,6 +98,7 @@ const TYPE_HEADER_BG: Record<string, string> = {
};
const PAGE_SIZE = 24;
const PURGE_MONTH_OPTIONS = [1, 3, 6, 12, 18, 24] as const;
function formatDate(d: Date | null | undefined): string | null {
if (!d) return null;
@@ -254,12 +256,16 @@ export default function VeilleDashboard() {
});
const unreadCountQuery = trpc.veille.unreadCount.useQuery();
const unreadCount = unreadCountQuery.data?.count ?? 0;
const [purgeScope, setPurgeScope] = useState<VeillePurgeScope>("older_than");
const [purgeMonths, setPurgeMonths] = useState<(typeof PURGE_MONTH_OPTIONS)[number]>(6);
const purgeMutation = trpc.veille.purge.useMutation({
onSuccess: (data) => {
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
utils.veille.list.invalidate();
utils.veille.filters.invalidate();
utils.veille.unreadCount.invalidate();
utils.veille.getReadIds.invalidate();
},
onError: (err) => {
toast.error(`Erreur lors de la purge : ${err.message}`);
@@ -381,11 +387,55 @@ export default function VeilleDashboard() {
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle size={20} className="text-destructive" />
Purger toutes les données de veille
Purger la veille stratégique
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2">
<p>Cette action va <strong>supprimer définitivement</strong> toutes les entrées de la veille stratégique.</p>
<div className="space-y-4">
<p>Choisissez les articles à <strong>supprimer définitivement</strong>.</p>
<div className="grid gap-2">
{([
["older_than", "Articles plus anciens"],
["all", "Tous les articles"],
] as const).map(([scope, label]) => (
<button
key={scope}
type="button"
onClick={() => setPurgeScope(scope)}
className={cn(
"rounded-lg border-2 px-3 py-2 text-left text-sm font-medium transition-colors",
purgeScope === scope
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/40 text-foreground",
)}
>
{label}
</button>
))}
</div>
{purgeScope === "older_than" && (
<div className="grid grid-cols-3 gap-2">
{PURGE_MONTH_OPTIONS.map((months) => (
<button
key={months}
type="button"
onClick={() => setPurgeMonths(months)}
className={cn(
"rounded-lg border-2 px-2 py-2 text-sm font-medium transition-colors",
purgeMonths === months
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/40 text-foreground",
)}
>
{months} mois
</button>
))}
</div>
)}
<p>
{purgeScope === "all"
? "Tous les articles de veille seront supprimés."
: `Les articles publiés, ou importés sans date de publication, il y a plus de ${purgeMonths} mois seront supprimés.`}
</p>
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
</div>
</AlertDialogDescription>
@@ -394,10 +444,14 @@ export default function VeilleDashboard() {
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => purgeMutation.mutate()}
onClick={() => purgeMutation.mutate(
purgeScope === "all"
? { scope: "all" }
: { scope: "older_than", months: purgeMonths },
)}
disabled={purgeMutation.isPending}
>
{purgeMutation.isPending ? "Purge en cours..." : "Oui, purger toutes les données"}
{purgeMutation.isPending ? "Purge en cours..." : "Oui, supprimer définitivement"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>