feat: add BackupCard component and corresponding Backup model to enhance settings functionality and data management
This commit is contained in:
437
components/settings/backup-card.tsx
Normal file
437
components/settings/backup-card.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { DataCard } from "./data-card";
|
||||
export { DangerZoneCard } from "./danger-zone-card";
|
||||
export { OFXInfoCard } from "./ofx-info-card";
|
||||
export { BackupCard } from "./backup-card";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user