backup page - working 1 done
This commit is contained in:
@@ -2,4 +2,8 @@ HOST="localhost"
|
|||||||
PORT=5000
|
PORT=5000
|
||||||
FRONTEND_URL="http://localhost:3000"
|
FRONTEND_URL="http://localhost:3000"
|
||||||
JWT_SECRET = 'dentalsecret'
|
JWT_SECRET = 'dentalsecret'
|
||||||
|
DB_HOST="localhost"
|
||||||
|
DB_USER="postgres"
|
||||||
|
DB_PASSWORD="mypassword"
|
||||||
|
DB_NAME="dentalapp"
|
||||||
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp
|
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp
|
||||||
116
apps/Backend/src/routes/database-management.ts
Normal file
116
apps/Backend/src/routes/database-management.ts
Normal file
@@ -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;
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from "express";
|
||||||
import patientsRoutes from './patients';
|
import patientsRoutes from "./patients";
|
||||||
import appointmentsRoutes from './appointments'
|
import appointmentsRoutes from "./appointments";
|
||||||
import usersRoutes from './users'
|
import usersRoutes from "./users";
|
||||||
import staffsRoutes from './staffs'
|
import staffsRoutes from "./staffs";
|
||||||
import pdfExtractionRoutes from './pdfExtraction';
|
import pdfExtractionRoutes from "./pdfExtraction";
|
||||||
import claimsRoutes from './claims';
|
import claimsRoutes from "./claims";
|
||||||
import insuranceCredsRoutes from './insuranceCreds';
|
import insuranceCredsRoutes from "./insuranceCreds";
|
||||||
import documentsRoutes from './documents';
|
import documentsRoutes from "./documents";
|
||||||
import insuranceEligibilityRoutes from './insuranceEligibility'
|
import insuranceEligibilityRoutes from "./insuranceEligibility";
|
||||||
import paymentsRoutes from './payments'
|
import paymentsRoutes from "./payments";
|
||||||
|
import databaseManagementRoutes from "./database-management";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use('/patients', patientsRoutes);
|
router.use("/patients", patientsRoutes);
|
||||||
router.use('/appointments', appointmentsRoutes);
|
router.use("/appointments", appointmentsRoutes);
|
||||||
router.use('/users', usersRoutes);
|
router.use("/users", usersRoutes);
|
||||||
router.use('/staffs', staffsRoutes);
|
router.use("/staffs", staffsRoutes);
|
||||||
router.use('/pdfExtraction', pdfExtractionRoutes);
|
router.use("/pdfExtraction", pdfExtractionRoutes);
|
||||||
router.use('/claims', claimsRoutes);
|
router.use("/claims", claimsRoutes);
|
||||||
router.use('/insuranceCreds', insuranceCredsRoutes);
|
router.use("/insuranceCreds", insuranceCredsRoutes);
|
||||||
router.use('/documents', documentsRoutes);
|
router.use("/documents", documentsRoutes);
|
||||||
router.use('/insuranceEligibility', insuranceEligibilityRoutes);
|
router.use("/insuranceEligibility", insuranceEligibilityRoutes);
|
||||||
router.use('/payments', paymentsRoutes);
|
router.use("/payments", paymentsRoutes);
|
||||||
|
router.use("/database-management", databaseManagementRoutes);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ const PatientsPage = lazy(() => import("./pages/patients-page"));
|
|||||||
const SettingsPage = lazy(() => import("./pages/settings-page"));
|
const SettingsPage = lazy(() => import("./pages/settings-page"));
|
||||||
const ClaimsPage = lazy(() => import("./pages/claims-page"));
|
const ClaimsPage = lazy(() => import("./pages/claims-page"));
|
||||||
const PaymentsPage = lazy(() => import("./pages/payments-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 DocumentPage = lazy(() => import("./pages/documents-page"));
|
||||||
|
const DatabaseManagementPage = lazy(
|
||||||
|
() => import("./pages/database-management-page")
|
||||||
|
);
|
||||||
const NotFound = lazy(() => import("./pages/not-found"));
|
const NotFound = lazy(() => import("./pages/not-found"));
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
@@ -32,9 +37,16 @@ function Router() {
|
|||||||
<ProtectedRoute path="/patients" component={() => <PatientsPage />} />
|
<ProtectedRoute path="/patients" component={() => <PatientsPage />} />
|
||||||
<ProtectedRoute path="/settings" component={() => <SettingsPage />} />
|
<ProtectedRoute path="/settings" component={() => <SettingsPage />} />
|
||||||
<ProtectedRoute path="/claims" component={() => <ClaimsPage />} />
|
<ProtectedRoute path="/claims" component={() => <ClaimsPage />} />
|
||||||
<ProtectedRoute path="/insurance-eligibility" component={()=><InsuranceEligibilityPage/>}/>
|
<ProtectedRoute
|
||||||
|
path="/insurance-eligibility"
|
||||||
|
component={() => <InsuranceEligibilityPage />}
|
||||||
|
/>
|
||||||
<ProtectedRoute path="/payments" component={() => <PaymentsPage />} />
|
<ProtectedRoute path="/payments" component={() => <PaymentsPage />} />
|
||||||
<ProtectedRoute path="/documents" component={() => <DocumentPage />} />
|
<ProtectedRoute path="/documents" component={() => <DocumentPage />} />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/database-management"
|
||||||
|
component={() => <DatabaseManagementPage />}
|
||||||
|
/>
|
||||||
<Route path="/auth" component={() => <AuthPage />} />
|
<Route path="/auth" component={() => <AuthPage />} />
|
||||||
<Route component={() => <NotFound />} />
|
<Route component={() => <NotFound />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
Database,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -56,6 +57,11 @@ export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
|
|||||||
path: "/documents",
|
path: "/documents",
|
||||||
icon: <FolderOpen className="h-5 w-5" />,
|
icon: <FolderOpen className="h-5 w-5" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Backup Database",
|
||||||
|
path: "/database-management",
|
||||||
|
icon: <Database className="h-5 w-5" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|||||||
203
apps/Frontend/src/pages/database-management-page.tsx
Normal file
203
apps/Frontend/src/pages/database-management-page.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||||
|
<Sidebar
|
||||||
|
isMobileOpen={isMobileMenuOpen}
|
||||||
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<TopAppBar
|
||||||
|
toggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 p-6">
|
||||||
|
<div className="max-w-6xl mx-auto space-y-6">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6 border">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 flex items-center space-x-3">
|
||||||
|
<Database className="h-8 w-8 text-blue-600" />
|
||||||
|
<span>Database Management</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
Manage your dental practice database with backup, export
|
||||||
|
capabilities
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Database Backup Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<HardDrive className="h-5 w-5" />
|
||||||
|
<span>Database Backup</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Create a complete backup of your dental practice database
|
||||||
|
including patients, appointments, claims, and all related
|
||||||
|
data.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => backupMutation.mutate()}
|
||||||
|
disabled={backupMutation.isPending}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
{backupMutation.isPending ? (
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<FileArchive className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{backupMutation.isPending
|
||||||
|
? "Creating Backup..."
|
||||||
|
: "Create Backup"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Last backup:{" "}
|
||||||
|
{dbStatus?.lastBackup
|
||||||
|
? new Date(dbStatus.lastBackup).toLocaleString()
|
||||||
|
: "Never"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Database Status Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
<span>Database Status</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoadingStatus ? (
|
||||||
|
<p className="text-gray-500">Loading status...</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="font-medium text-green-800">
|
||||||
|
Status
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-green-600 mt-1">
|
||||||
|
{dbStatus?.connected ? "Connected" : "Disconnected"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Database className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="font-medium text-blue-800">Size</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-blue-600 mt-1">
|
||||||
|
{dbStatus?.size ?? "Unknown"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Cloud className="h-4 w-4 text-purple-500" />
|
||||||
|
<span className="font-medium text-purple-800">
|
||||||
|
Records
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-purple-600 mt-1">
|
||||||
|
{dbStatus?.patients
|
||||||
|
? `${dbStatus.patients} patients`
|
||||||
|
: "N/A"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import {
|
import {
|
||||||
@@ -7,32 +6,16 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
|
||||||
CardDescription,
|
CardDescription,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
ArrowDown,
|
|
||||||
Upload,
|
Upload,
|
||||||
Image,
|
Image,
|
||||||
X,
|
X,
|
||||||
Trash2,
|
|
||||||
Save,
|
|
||||||
} from "lucide-react";
|
} 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 {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -40,13 +23,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogFooter,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
||||||
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
||||||
|
|
||||||
@@ -56,7 +32,6 @@ export default function PaymentsPage() {
|
|||||||
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
const [isExtracting, setIsExtracting] = useState(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [extractedPaymentData, setExtractedPaymentData] = useState<any[]>([]);
|
|
||||||
const [editableData, setEditableData] = useState<any[]>([]);
|
const [editableData, setEditableData] = useState<any[]>([]);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|||||||
Reference in New Issue
Block a user