Files
DentalManagementMH05/apps/Frontend/src/components/database-management/backup-destination-manager.tsx
2026-05-04 00:52:42 -04:00

241 lines
7.7 KiB
TypeScript
Executable File

import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { FolderOpen, HardDrive, Trash2 } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { FolderBrowserModal } from "./folder-browser-modal";
export function BackupDestinationManager() {
const { toast } = useToast();
const [path, setPath] = useState("");
const [deleteId, setDeleteId] = useState<number | null>(null);
const [browserOpen, setBrowserOpen] = useState(false);
// ==============================
// Queries
// ==============================
const { data: destinations = [] } = useQuery({
queryKey: ["/db/destination"],
queryFn: async () => {
const res = await apiRequest(
"GET",
"/api/database-management/destination"
);
return res.json();
},
});
const { data: usbSettingData } = useQuery({
queryKey: ["/db/usb-backup-setting"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/database-management/usb-backup-setting");
return res.json();
},
});
const usbBackupEnabled = usbSettingData?.usbBackupEnabled ?? false;
const usbToggleMutation = useMutation({
mutationFn: async (enabled: boolean) => {
const res = await apiRequest("PUT", "/api/database-management/usb-backup-setting", {
usbBackupEnabled: enabled,
});
return res.json();
},
onSuccess: (data) => {
queryClient.setQueryData(["/db/usb-backup-setting"], data);
toast({
title: "Setting Saved",
description: `USB backup ${data.usbBackupEnabled ? "enabled" : "disabled"}.`,
});
},
onError: () => {
toast({
title: "Error",
description: "Failed to update USB backup setting.",
variant: "destructive",
});
},
});
// ==============================
// Mutations
// ==============================
const saveMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest(
"POST",
"/api/database-management/destination",
{ path }
);
if (!res.ok) throw new Error((await res.json()).error);
},
onSuccess: () => {
toast({ title: "Backup destination saved" });
setPath("");
queryClient.invalidateQueries({ queryKey: ["/db/destination"] });
},
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
await apiRequest("DELETE", `/api/database-management/destination/${id}`);
},
onSuccess: () => {
toast({ title: "Backup destination deleted" });
queryClient.invalidateQueries({ queryKey: ["/db/destination"] });
setDeleteId(null);
},
});
const backupNowMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("POST", "/api/database-management/backup-path");
if (!res.ok) {
const body = await res.json();
throw new Error(body.details || body.error || "Backup failed");
}
return res.json();
},
onSuccess: (data) => {
toast({ title: "Backup complete", description: `Saved: ${data.filename}` });
queryClient.invalidateQueries({ queryKey: ["/db/status"] });
},
onError: (err: any) => {
toast({ title: "Backup failed", description: err.message, variant: "destructive" });
},
});
// ==============================
// Folder browser
// ==============================
const handleFolderSelect = (selectedPath: string) => {
setPath(selectedPath);
};
// ==============================
// UI
// ==============================
return (
<Card>
<CardHeader>
<CardTitle>External Backup Destination</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-3">
<Switch
id="usb-backup-toggle"
checked={usbBackupEnabled}
onCheckedChange={(checked) => usbToggleMutation.mutate(checked)}
disabled={usbToggleMutation.isPending}
/>
<label
htmlFor="usb-backup-toggle"
className="text-sm font-medium text-gray-700 cursor-pointer select-none"
>
USB Backup
</label>
<span className="text-xs text-gray-400">
(daily at 9 PM saves to the &quot;USB Backup&quot; folder on your drive)
</span>
</div>
<p className="text-sm text-gray-500">
Enter the root path of your USB drive below. The app will automatically back up to the{" "}
<span className="font-medium text-gray-700">USB Backup</span> folder inside it every night at 9 PM when the toggle is on.
</p>
<div className="flex gap-2">
<Input
placeholder="/media/usb-drive or D:\\Backups"
value={path}
onChange={(e) => setPath(e.target.value)}
/>
<Button variant="outline" onClick={() => setBrowserOpen(true)} title="Browse folders">
<FolderOpen className="h-4 w-4" />
</Button>
</div>
<FolderBrowserModal
open={browserOpen}
onClose={() => setBrowserOpen(false)}
onSelect={handleFolderSelect}
/>
<Button
onClick={() => saveMutation.mutate()}
disabled={!path || saveMutation.isPending}
>
Save Destination
</Button>
<div className="space-y-2">
{destinations.map((d: any) => (
<div
key={d.id}
className="flex justify-between items-center border rounded p-2"
>
<span className="text-sm text-gray-700">{d.path}</span>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => backupNowMutation.mutate()}
disabled={backupNowMutation.isPending}
title="Backup now to this destination"
>
<HardDrive className="h-4 w-4 mr-1" />
{backupNowMutation.isPending ? "Backing up..." : "Backup Now"}
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => setDeleteId(d.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
{/* Confirm delete dialog */}
<AlertDialog open={deleteId !== null}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete backup destination?</AlertDialogTitle>
<AlertDialogDescription>
This will remove the destination and stop automatic backups.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteId(null)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteId && deleteMutation.mutate(deleteId)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
);
}