From 38c3c43e6bd6142b4ee1d02bb0111ba654744153 Mon Sep 17 00:00:00 2001 From: Potenz Date: Mon, 25 Aug 2025 00:19:12 +0530 Subject: [PATCH] backup page - working 1 done --- apps/Backend/.env.example | 4 + .../Backend/src/routes/database-management.ts | 116 ++++++++++ apps/Backend/src/routes/index.ts | 46 ++-- apps/Frontend/src/App.tsx | 16 +- .../src/components/layout/sidebar.tsx | 6 + .../src/pages/database-management-page.tsx | 203 ++++++++++++++++++ apps/Frontend/src/pages/payments-page.tsx | 25 --- 7 files changed, 367 insertions(+), 49 deletions(-) create mode 100644 apps/Backend/src/routes/database-management.ts create mode 100644 apps/Frontend/src/pages/database-management-page.tsx diff --git a/apps/Backend/.env.example b/apps/Backend/.env.example index 304aa3a..fec3580 100644 --- a/apps/Backend/.env.example +++ b/apps/Backend/.env.example @@ -2,4 +2,8 @@ HOST="localhost" PORT=5000 FRONTEND_URL="http://localhost:3000" JWT_SECRET = 'dentalsecret' +DB_HOST="localhost" +DB_USER="postgres" +DB_PASSWORD="mypassword" +DB_NAME="dentalapp" DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp \ No newline at end of file diff --git a/apps/Backend/src/routes/database-management.ts b/apps/Backend/src/routes/database-management.ts new file mode 100644 index 0000000..d8ac518 --- /dev/null +++ b/apps/Backend/src/routes/database-management.ts @@ -0,0 +1,116 @@ +import { Router, Request, Response } from "express"; +import { spawn } from "child_process"; +import path from "path"; +import os from "os"; +import fs from "fs"; +import { prisma } from "@repo/db/client"; + +const router = Router(); + +/** + * Create a database backup + */ + +router.post("/backup", async (req: Request, res: Response) => { + try { + const fileName = `dental_backup_${Date.now()}.dump`; + const tmpFile = path.join(os.tmpdir(), fileName); + + // Spawn pg_dump + const pgDump = spawn( + "pg_dump", + [ + "-Fc", // custom format + "--no-acl", + "--no-owner", + "-h", + process.env.DB_HOST || "localhost", + "-U", + process.env.DB_USER || "postgres", + process.env.DB_NAME || "dental_db", + "-f", + tmpFile, // write directly to temp file + ], + { + env: { + ...process.env, + PGPASSWORD: process.env.DB_PASSWORD, + }, + } + ); + let errorMessage = ""; + + pgDump.stderr.on("data", (chunk) => { + errorMessage += chunk.toString(); + }); + + pgDump.on("close", (code) => { + if (code === 0) { + // ✅ Send only if dump succeeded + res.setHeader( + "Content-Disposition", + `attachment; filename=${fileName}` + ); + res.setHeader("Content-Type", "application/octet-stream"); + + const fileStream = fs.createReadStream(tmpFile); + fileStream.pipe(res); + + fileStream.on("close", () => { + fs.unlink(tmpFile, () => {}); // cleanup temp file + }); + } else { + console.error("pg_dump failed:", errorMessage); + fs.unlink(tmpFile, () => {}); // cleanup + res.status(500).json({ + error: "Backup failed", + details: errorMessage || `pg_dump exited with code ${code}`, + }); + } + }); + + pgDump.on("error", (err) => { + console.error("Failed to start pg_dump:", err); + fs.unlink(tmpFile, () => {}); + res.status(500).json({ + error: "Failed to run pg_dump", + details: err.message, + }); + }); + } catch (err: any) { + console.error("Unexpected error:", err); + if (!res.headersSent) { + res.status(500).json({ + message: "Internal server error", + details: String(err), + }); + } + } +}); + +/** + * Get database status (connected, size, records count) + */ +router.get("/status", async (req: Request, res: Response) => { + try { + const size = await prisma.$queryRawUnsafe<{ size: string }[]>( + "SELECT pg_size_pretty(pg_database_size(current_database())) as size" + ); + + const patientsCount = await prisma.patient.count(); + + res.json({ + connected: true, + size: size[0]?.size, + patients: patientsCount, + }); + } catch (err) { + console.error("Status error:", err); + res.status(500).json({ + connected: false, + error: "Could not fetch database status", + }); + } +}); + +export default router; diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts index 2b7937d..08a787b 100644 --- a/apps/Backend/src/routes/index.ts +++ b/apps/Backend/src/routes/index.ts @@ -1,26 +1,28 @@ -import { Router } from 'express'; -import patientsRoutes from './patients'; -import appointmentsRoutes from './appointments' -import usersRoutes from './users' -import staffsRoutes from './staffs' -import pdfExtractionRoutes from './pdfExtraction'; -import claimsRoutes from './claims'; -import insuranceCredsRoutes from './insuranceCreds'; -import documentsRoutes from './documents'; -import insuranceEligibilityRoutes from './insuranceEligibility' -import paymentsRoutes from './payments' +import { Router } from "express"; +import patientsRoutes from "./patients"; +import appointmentsRoutes from "./appointments"; +import usersRoutes from "./users"; +import staffsRoutes from "./staffs"; +import pdfExtractionRoutes from "./pdfExtraction"; +import claimsRoutes from "./claims"; +import insuranceCredsRoutes from "./insuranceCreds"; +import documentsRoutes from "./documents"; +import insuranceEligibilityRoutes from "./insuranceEligibility"; +import paymentsRoutes from "./payments"; +import databaseManagementRoutes from "./database-management"; const router = Router(); -router.use('/patients', patientsRoutes); -router.use('/appointments', appointmentsRoutes); -router.use('/users', usersRoutes); -router.use('/staffs', staffsRoutes); -router.use('/pdfExtraction', pdfExtractionRoutes); -router.use('/claims', claimsRoutes); -router.use('/insuranceCreds', insuranceCredsRoutes); -router.use('/documents', documentsRoutes); -router.use('/insuranceEligibility', insuranceEligibilityRoutes); -router.use('/payments', paymentsRoutes); +router.use("/patients", patientsRoutes); +router.use("/appointments", appointmentsRoutes); +router.use("/users", usersRoutes); +router.use("/staffs", staffsRoutes); +router.use("/pdfExtraction", pdfExtractionRoutes); +router.use("/claims", claimsRoutes); +router.use("/insuranceCreds", insuranceCredsRoutes); +router.use("/documents", documentsRoutes); +router.use("/insuranceEligibility", insuranceEligibilityRoutes); +router.use("/payments", paymentsRoutes); +router.use("/database-management", databaseManagementRoutes); -export default router; \ No newline at end of file +export default router; diff --git a/apps/Frontend/src/App.tsx b/apps/Frontend/src/App.tsx index 36122bb..e4574da 100644 --- a/apps/Frontend/src/App.tsx +++ b/apps/Frontend/src/App.tsx @@ -17,8 +17,13 @@ const PatientsPage = lazy(() => import("./pages/patients-page")); const SettingsPage = lazy(() => import("./pages/settings-page")); const ClaimsPage = lazy(() => import("./pages/claims-page")); const PaymentsPage = lazy(() => import("./pages/payments-page")); -const InsuranceEligibilityPage = lazy(()=> import("./pages/insurance-eligibility-page")) +const InsuranceEligibilityPage = lazy( + () => import("./pages/insurance-eligibility-page") +); const DocumentPage = lazy(() => import("./pages/documents-page")); +const DatabaseManagementPage = lazy( + () => import("./pages/database-management-page") +); const NotFound = lazy(() => import("./pages/not-found")); function Router() { @@ -32,9 +37,16 @@ function Router() { } /> } /> } /> - }/> + } + /> } /> } /> + } + /> } /> } /> diff --git a/apps/Frontend/src/components/layout/sidebar.tsx b/apps/Frontend/src/components/layout/sidebar.tsx index b4440e8..ff3de0c 100644 --- a/apps/Frontend/src/components/layout/sidebar.tsx +++ b/apps/Frontend/src/components/layout/sidebar.tsx @@ -8,6 +8,7 @@ import { Shield, CreditCard, FolderOpen, + Database, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -56,6 +57,11 @@ export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) { path: "/documents", icon: , }, + { + name: "Backup Database", + path: "/database-management", + icon: , + }, { name: "Settings", path: "/settings", diff --git a/apps/Frontend/src/pages/database-management-page.tsx b/apps/Frontend/src/pages/database-management-page.tsx new file mode 100644 index 0000000..781f636 --- /dev/null +++ b/apps/Frontend/src/pages/database-management-page.tsx @@ -0,0 +1,203 @@ +import { useState } from "react"; +import { TopAppBar } from "@/components/layout/top-app-bar"; +import { Sidebar } from "@/components/layout/sidebar"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; +import { + Database, + FileArchive, + HardDrive, + Cloud, + RefreshCw, +} from "lucide-react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { apiRequest, queryClient } from "@/lib/queryClient"; + +export default function DatabaseManagementPage() { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const { toast } = useToast(); + + // ----- Database status query ----- + const { data: dbStatus, isLoading: isLoadingStatus } = useQuery({ + queryKey: ["/db/status"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/database-management/status"); + return res.json(); + }, + }); + + // ----- Backup mutation ----- + const backupMutation = useMutation({ + mutationFn: async () => { + const res = await apiRequest("POST", "/api/database-management/backup"); + + if (!res.ok) { + // Try to parse JSON error + let errorBody = {}; + try { + errorBody = await res.json(); + } catch {} + throw new Error((errorBody as any)?.error || "Backup failed"); + } + + // Convert response to blob (file) + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const disposition = res.headers.get("Content-Disposition"); + const fileName = + disposition?.split("filename=")[1]?.replace(/"/g, "") || + `dental_backup_${new Date().toISOString()}.dump`; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, + onSuccess: () => { + toast({ + title: "Backup Complete", + description: "Database backup downloaded successfully", + variant: "default", + }); + queryClient.invalidateQueries({ queryKey: ["/db/status"] }); + }, + onError: (error: any) => { + console.error("Backup failed:", error); + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + return ( +
+ + +
+ setIsMobileMenuOpen(!isMobileMenuOpen)} + /> + +
+
+ {/* Page Header */} +
+

+ + Database Management +

+

+ Manage your dental practice database with backup, export + capabilities +

+
+ + {/* Database Backup Section */} + + + + + Database Backup + + + +

+ Create a complete backup of your dental practice database + including patients, appointments, claims, and all related + data. +

+ +
+ + +
+ Last backup:{" "} + {dbStatus?.lastBackup + ? new Date(dbStatus.lastBackup).toLocaleString() + : "Never"} +
+
+
+
+ + {/* Database Status Section */} + + + + + Database Status + + + + {isLoadingStatus ? ( +

Loading status...

+ ) : ( +
+
+
+
+ + Status + +
+

+ {dbStatus?.connected ? "Connected" : "Disconnected"} +

+
+ +
+
+ + Size +
+

+ {dbStatus?.size ?? "Unknown"} +

+
+ +
+
+ + + Records + +
+

+ {dbStatus?.patients + ? `${dbStatus.patients} patients` + : "N/A"} +

+
+
+ )} +
+
+
+
+
+
+ ); +} diff --git a/apps/Frontend/src/pages/payments-page.tsx b/apps/Frontend/src/pages/payments-page.tsx index c5a7f5f..942dff7 100644 --- a/apps/Frontend/src/pages/payments-page.tsx +++ b/apps/Frontend/src/pages/payments-page.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; import { TopAppBar } from "@/components/layout/top-app-bar"; import { Sidebar } from "@/components/layout/sidebar"; import { @@ -7,32 +6,16 @@ import { CardHeader, CardTitle, CardContent, - CardFooter, CardDescription, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; -import { useAuth } from "@/hooks/use-auth"; import { - AlertCircle, DollarSign, - ArrowDown, Upload, Image, X, - Trash2, - Save, } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Table, - TableHeader, - TableBody, - TableRow, - TableHead, - TableCell, -} from "@/components/ui/table"; import { Select, SelectContent, @@ -40,13 +23,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog"; import PaymentsRecentTable from "@/components/payments/payments-recent-table"; import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table"; @@ -56,7 +32,6 @@ export default function PaymentsPage() { const [uploadedImage, setUploadedImage] = useState(null); const [isExtracting, setIsExtracting] = useState(false); const [isDragging, setIsDragging] = useState(false); - const [extractedPaymentData, setExtractedPaymentData] = useState([]); const [editableData, setEditableData] = useState([]); const { toast } = useToast();