Files
fintrack/components/settings/backup-card.tsx

439 lines
14 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Database, Trash2, RotateCcw, Save, Clock } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import { fr } from "date-fns/locale/fr";
import { toast } from "sonner";
interface Backup {
id: string;
filename: string;
size: number;
createdAt: Date;
}
interface BackupSettings {
enabled: boolean;
frequency: "hourly" | "daily" | "weekly" | "monthly";
lastBackup?: string;
nextBackup?: string;
}
export function BackupCard() {
const [backups, setBackups] = useState<Backup[]>([]);
const [settings, setSettings] = useState<BackupSettings>({
enabled: true,
frequency: "hourly",
});
const [loading, setLoading] = useState(true);
const [creating, setCreating] = useState(false);
const [restoring, setRestoring] = useState<string | null>(null);
const loadData = async () => {
try {
const [backupsRes, settingsRes] = await Promise.all([
fetch("/api/backups"),
fetch("/api/backups/settings"),
]);
const backupsData = await backupsRes.json();
const settingsData = await settingsRes.json();
if (backupsData.success) {
setBackups(
backupsData.data.map(
(b: {
id: string;
filename: string;
size: number;
createdAt: string;
}) => ({
...b,
createdAt: new Date(b.createdAt),
}),
),
);
}
if (settingsData.success) {
setSettings(settingsData.data);
}
} catch (error) {
console.error("Error loading backup data:", error);
toast.error("Erreur lors du chargement des données");
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const handleCreateBackup = async () => {
setCreating(true);
try {
const response = await fetch("/api/backups", {
method: "POST",
});
const data = await response.json();
if (data.success) {
if (data.data.skipped) {
toast.info(
"Aucun changement détecté. La dernière sauvegarde a été mise à jour.",
);
} else {
toast.success("Sauvegarde créée avec succès");
}
await loadData();
} else {
toast.error(data.error || "Erreur lors de la création");
}
} catch (error) {
console.error("Error creating backup:", error);
toast.error("Erreur lors de la création de la sauvegarde");
} finally {
setCreating(false);
}
};
const handleDeleteBackup = async (id: string) => {
try {
const response = await fetch(`/api/backups/${id}`, {
method: "DELETE",
});
const data = await response.json();
if (data.success) {
toast.success("Sauvegarde supprimée");
await loadData();
} else {
toast.error(data.error || "Erreur lors de la suppression");
}
} catch (error) {
console.error("Error deleting backup:", error);
toast.error("Erreur lors de la suppression");
}
};
const handleRestoreBackup = async (id: string) => {
setRestoring(id);
try {
const response = await fetch(`/api/backups/${id}/restore`, {
method: "POST",
});
const data = await response.json();
if (data.success) {
toast.success(
"Sauvegarde restaurée avec succès. Rechargement de la page...",
);
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
toast.error(data.error || "Erreur lors de la restauration");
}
} catch (error) {
console.error("Error restoring backup:", error);
toast.error("Erreur lors de la restauration");
} finally {
setRestoring(null);
}
};
const handleSettingsChange = async (updates: Partial<BackupSettings>) => {
const newSettings = { ...settings, ...updates };
setSettings(newSettings);
try {
const response = await fetch("/api/backups/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
const data = await response.json();
if (data.success) {
setSettings(data.data);
toast.success("Paramètres mis à jour");
} else {
toast.error("Erreur lors de la mise à jour");
await loadData(); // Revert on error
}
} catch (error) {
console.error("Error updating settings:", error);
toast.error("Erreur lors de la mise à jour");
await loadData(); // Revert on error
}
};
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
};
if (loading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="w-5 h-5" />
Sauvegardes automatiques
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">Chargement...</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="w-5 h-5" />
Sauvegardes automatiques
</CardTitle>
<CardDescription>
Configurez les sauvegardes périodiques de votre base de données
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Settings */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="backup-enabled">Sauvegardes automatiques</Label>
<p className="text-sm text-muted-foreground">
Activez les sauvegardes périodiques
</p>
</div>
<Switch
id="backup-enabled"
checked={settings.enabled}
onCheckedChange={(checked) =>
handleSettingsChange({ enabled: checked })
}
/>
</div>
{settings.enabled && (
<div className="space-y-2">
<Label htmlFor="backup-frequency">Fréquence</Label>
<Select
value={settings.frequency}
onValueChange={(
value: "hourly" | "daily" | "weekly" | "monthly",
) => handleSettingsChange({ frequency: value })}
>
<SelectTrigger id="backup-frequency">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hourly">Horaire</SelectItem>
<SelectItem value="daily">Quotidienne</SelectItem>
<SelectItem value="weekly">Hebdomadaire</SelectItem>
<SelectItem value="monthly">Mensuelle</SelectItem>
</SelectContent>
</Select>
</div>
)}
{settings.enabled && settings.lastBackup && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="w-4 h-4" />
<span>
Dernière sauvegarde:{" "}
{formatDistanceToNow(new Date(settings.lastBackup), {
addSuffix: true,
locale: fr,
})}
</span>
</div>
)}
{settings.enabled && settings.nextBackup && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="w-4 h-4" />
<span>
Prochaine sauvegarde:{" "}
{formatDistanceToNow(new Date(settings.nextBackup), {
addSuffix: true,
locale: fr,
})}
</span>
</div>
)}
</div>
{/* Manual backup */}
<div className="flex gap-2">
<Button
onClick={handleCreateBackup}
disabled={creating}
variant="outline"
className="flex-1"
>
<Save className="w-4 h-4 mr-2" />
{creating ? "Création..." : "Créer une sauvegarde"}
</Button>
</div>
{/* Backups list */}
{backups.length > 0 && (
<div className="space-y-2">
<Label>Sauvegardes ({backups.length}/10)</Label>
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Taille</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{backups.map((backup) => (
<TableRow key={backup.id}>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">
{backup.createdAt.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(backup.createdAt, {
addSuffix: true,
locale: fr,
})}
</span>
</div>
</TableCell>
<TableCell>{formatFileSize(backup.size)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={restoring === backup.id}
>
<RotateCcw className="w-4 h-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Restaurer cette sauvegarde ?
</AlertDialogTitle>
<AlertDialogDescription>
Cette action va remplacer votre base de
données actuelle par cette sauvegarde. Une
sauvegarde de sécurité sera créée avant la
restauration.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleRestoreBackup(backup.id)}
>
Restaurer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm">
<Trash2 className="w-4 h-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Supprimer cette sauvegarde ?
</AlertDialogTitle>
<AlertDialogDescription>
Cette action est irréversible. La sauvegarde
sera définitivement supprimée.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteBackup(backup.id)}
>
Supprimer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
{backups.length === 0 && (
<div className="text-center py-8 text-sm text-muted-foreground">
Aucune sauvegarde pour le moment
</div>
)}
</CardContent>
</Card>
);
}