From 4025ca45e01eec88fab2e0d40984a45c74eeb707 Mon Sep 17 00:00:00 2001 From: ff Date: Sat, 11 Apr 2026 00:32:39 -0400 Subject: [PATCH] feat: database management - auto/USB backup toggles, folder browser, cron jobs --- .../53b0054db79f7114-turbo.log.2026-04-10 | 0 .../53b0054db79f7114-turbo.log.2026-04-11 | 1 + apps/Backend/src/cron/backupCheck.ts | 206 ++++++++------- .../Backend/src/routes/database-management.ts | 82 ++++++ .../backup-destination-manager.tsx | 93 +++++-- .../folder-browser-modal.tsx | 130 ++++++++++ .../src/components/layout/sidebar.tsx | 2 +- .../src/pages/database-management-page.tsx | 51 ++++ ...854788_202610000403500_20260410_201547.pdf | Bin 0 -> 19461 bytes packages/db/generated/prisma/edge.js | 12 +- packages/db/generated/prisma/index-browser.js | 4 +- packages/db/generated/prisma/index.d.ts | 240 +++++++++++++++--- packages/db/generated/prisma/index.js | 12 +- packages/db/generated/prisma/package.json | 2 +- packages/db/generated/prisma/schema.prisma | 2 + packages/db/prisma/schema.prisma | 2 + .../.prisma-zod-generator-manifest.json | 12 +- packages/db/shared/helpers/decimal-helpers.ts | 7 +- .../enums/UserScalarFieldEnum.schema.ts | 2 +- .../schemas/findFirstOrThrowUser.schema.ts | 4 + .../db/shared/schemas/findFirstUser.schema.ts | 4 + .../db/shared/schemas/findManyUser.schema.ts | 4 + .../AppointmentProcedureCreateInput.schema.ts | 11 +- ...cedureCreateManyAppointmentInput.schema.ts | 7 +- ...ointmentProcedureCreateManyInput.schema.ts | 7 +- ...tProcedureCreateManyPatientInput.schema.ts | 7 +- ...ureCreateWithoutAppointmentInput.schema.ts | 9 +- ...ocedureCreateWithoutPatientInput.schema.ts | 9 +- ...intmentProcedureScalarWhereInput.schema.ts | 19 +- ...reScalarWhereWithAggregatesInput.schema.ts | 19 +- ...entProcedureUncheckedCreateInput.schema.ts | 7 +- ...kedCreateWithoutAppointmentInput.schema.ts | 7 +- ...checkedCreateWithoutPatientInput.schema.ts | 7 +- ...entProcedureUncheckedUpdateInput.schema.ts | 19 +- ...rocedureUncheckedUpdateManyInput.schema.ts | 19 +- ...pdateManyWithoutAppointmentInput.schema.ts | 19 +- ...kedUpdateManyWithoutPatientInput.schema.ts | 19 +- ...kedUpdateWithoutAppointmentInput.schema.ts | 19 +- ...checkedUpdateWithoutPatientInput.schema.ts | 19 +- .../AppointmentProcedureUpdateInput.schema.ts | 21 +- ...ProcedureUpdateManyMutationInput.schema.ts | 17 +- ...ureUpdateWithoutAppointmentInput.schema.ts | 19 +- ...ocedureUpdateWithoutPatientInput.schema.ts | 19 +- .../AppointmentProcedureWhereInput.schema.ts | 27 +- ...ecimalFieldUpdateOperationsInput.schema.ts | 16 +- .../schemas/objects/DecimalFilter.schema.ts | 21 +- .../objects/DecimalNullableFilter.schema.ts | 21 +- ...imalNullableWithAggregatesFilter.schema.ts | 25 +- .../DecimalWithAggregatesFilter.schema.ts | 25 +- .../objects/NestedDecimalFilter.schema.ts | 22 +- .../NestedDecimalNullableFilter.schema.ts | 22 +- ...imalNullableWithAggregatesFilter.schema.ts | 23 +- ...estedDecimalWithAggregatesFilter.schema.ts | 23 +- ...ecimalFieldUpdateOperationsInput.schema.ts | 16 +- .../objects/PaymentCreateInput.schema.ts | 23 +- .../objects/PaymentCreateManyInput.schema.ts | 13 +- .../PaymentCreateManyPatientInput.schema.ts | 13 +- .../PaymentCreateManyUpdatedByInput.schema.ts | 13 +- .../PaymentCreateWithoutClaimInput.schema.ts | 21 +- ...PaymentCreateWithoutPatientInput.schema.ts | 21 +- ...houtServiceLineTransactionsInput.schema.ts | 21 +- ...ntCreateWithoutServiceLinesInput.schema.ts | 21 +- ...ymentCreateWithoutUpdatedByInput.schema.ts | 21 +- .../objects/PaymentScalarWhereInput.schema.ts | 25 +- ...ntScalarWhereWithAggregatesInput.schema.ts | 25 +- .../PaymentUncheckedCreateInput.schema.ts | 17 +- ...UncheckedCreateWithoutClaimInput.schema.ts | 17 +- ...checkedCreateWithoutPatientInput.schema.ts | 17 +- ...houtServiceLineTransactionsInput.schema.ts | 15 +- ...edCreateWithoutServiceLinesInput.schema.ts | 15 +- ...eckedCreateWithoutUpdatedByInput.schema.ts | 17 +- .../PaymentUncheckedUpdateInput.schema.ts | 29 +-- .../PaymentUncheckedUpdateManyInput.schema.ts | 25 +- ...kedUpdateManyWithoutPatientInput.schema.ts | 25 +- ...dUpdateManyWithoutUpdatedByInput.schema.ts | 25 +- ...UncheckedUpdateWithoutClaimInput.schema.ts | 29 +-- ...checkedUpdateWithoutPatientInput.schema.ts | 29 +-- ...houtServiceLineTransactionsInput.schema.ts | 27 +- ...edUpdateWithoutServiceLinesInput.schema.ts | 27 +- ...eckedUpdateWithoutUpdatedByInput.schema.ts | 29 +-- .../objects/PaymentUpdateInput.schema.ts | 33 ++- .../PaymentUpdateManyMutationInput.schema.ts | 23 +- .../PaymentUpdateWithoutClaimInput.schema.ts | 31 ++- ...PaymentUpdateWithoutPatientInput.schema.ts | 31 ++- ...houtServiceLineTransactionsInput.schema.ts | 31 ++- ...ntUpdateWithoutServiceLinesInput.schema.ts | 31 ++- ...ymentUpdateWithoutUpdatedByInput.schema.ts | 31 ++- .../objects/PaymentWhereInput.schema.ts | 41 ++- .../objects/ServiceLineCreateInput.schema.ts | 19 +- .../ServiceLineCreateManyClaimInput.schema.ts | 13 +- .../ServiceLineCreateManyInput.schema.ts | 13 +- ...erviceLineCreateManyPaymentInput.schema.ts | 13 +- ...rviceLineCreateWithoutClaimInput.schema.ts | 17 +- ...iceLineCreateWithoutPaymentInput.schema.ts | 17 +- ...houtServiceLineTransactionsInput.schema.ts | 17 +- .../ServiceLineScalarWhereInput.schema.ts | 27 +- ...neScalarWhereWithAggregatesInput.schema.ts | 27 +- ...erviceLineTransactionCreateInput.schema.ts | 13 +- ...ceLineTransactionCreateManyInput.schema.ts | 9 +- ...ransactionCreateManyPaymentInput.schema.ts | 9 +- ...actionCreateManyServiceLineInput.schema.ts | 9 +- ...sactionCreateWithoutPaymentInput.schema.ts | 11 +- ...ionCreateWithoutServiceLineInput.schema.ts | 11 +- ...eLineTransactionScalarWhereInput.schema.ts | 19 +- ...onScalarWhereWithAggregatesInput.schema.ts | 19 +- ...eTransactionUncheckedCreateInput.schema.ts | 9 +- ...checkedCreateWithoutPaymentInput.schema.ts | 9 +- ...kedCreateWithoutServiceLineInput.schema.ts | 9 +- ...eTransactionUncheckedUpdateInput.schema.ts | 19 +- ...nsactionUncheckedUpdateManyInput.schema.ts | 19 +- ...kedUpdateManyWithoutPaymentInput.schema.ts | 19 +- ...pdateManyWithoutServiceLineInput.schema.ts | 19 +- ...checkedUpdateWithoutPaymentInput.schema.ts | 19 +- ...kedUpdateWithoutServiceLineInput.schema.ts | 19 +- ...erviceLineTransactionUpdateInput.schema.ts | 21 +- ...ansactionUpdateManyMutationInput.schema.ts | 17 +- ...sactionUpdateWithoutPaymentInput.schema.ts | 19 +- ...ionUpdateWithoutServiceLineInput.schema.ts | 19 +- ...ServiceLineTransactionWhereInput.schema.ts | 27 +- .../ServiceLineUncheckedCreateInput.schema.ts | 15 +- ...UncheckedCreateWithoutClaimInput.schema.ts | 15 +- ...checkedCreateWithoutPaymentInput.schema.ts | 15 +- ...houtServiceLineTransactionsInput.schema.ts | 13 +- .../ServiceLineUncheckedUpdateInput.schema.ts | 29 +-- ...viceLineUncheckedUpdateManyInput.schema.ts | 27 +- ...eckedUpdateManyWithoutClaimInput.schema.ts | 27 +- ...kedUpdateManyWithoutPaymentInput.schema.ts | 27 +- ...UncheckedUpdateWithoutClaimInput.schema.ts | 29 +-- ...checkedUpdateWithoutPaymentInput.schema.ts | 29 +-- ...houtServiceLineTransactionsInput.schema.ts | 27 +- .../objects/ServiceLineUpdateInput.schema.ts | 29 +-- ...rviceLineUpdateManyMutationInput.schema.ts | 23 +- ...rviceLineUpdateWithoutClaimInput.schema.ts | 27 +- ...iceLineUpdateWithoutPaymentInput.schema.ts | 27 +- ...houtServiceLineTransactionsInput.schema.ts | 27 +- .../objects/ServiceLineWhereInput.schema.ts | 37 ++- .../objects/UserCountAggregateInput.schema.ts | 2 + .../UserCountOrderByAggregateInput.schema.ts | 4 +- .../schemas/objects/UserCreateInput.schema.ts | 2 + .../objects/UserCreateManyInput.schema.ts | 4 +- ...erCreateWithoutAppointmentsInput.schema.ts | 2 + ...teWithoutBackupDestinationsInput.schema.ts | 2 + .../UserCreateWithoutBackupsInput.schema.ts | 2 + .../UserCreateWithoutClaimsInput.schema.ts | 2 + ...UserCreateWithoutCloudFilesInput.schema.ts | 2 + ...erCreateWithoutCloudFoldersInput.schema.ts | 2 + ...CreateWithoutCommunicationsInput.schema.ts | 2 + ...WithoutInsuranceCredentialsInput.schema.ts | 2 + ...rCreateWithoutNotificationsInput.schema.ts | 2 + ...erCreateWithoutNpiProvidersInput.schema.ts | 2 + .../UserCreateWithoutPatientsInput.schema.ts | 2 + .../UserCreateWithoutStaffInput.schema.ts | 2 + ...reateWithoutUpdatedPaymentsInput.schema.ts | 2 + .../objects/UserMaxAggregateInput.schema.ts | 4 +- .../UserMaxOrderByAggregateInput.schema.ts | 4 +- .../objects/UserMinAggregateInput.schema.ts | 4 +- .../UserMinOrderByAggregateInput.schema.ts | 4 +- .../UserOrderByWithAggregationInput.schema.ts | 2 + .../UserOrderByWithRelationInput.schema.ts | 2 + ...erScalarWhereWithAggregatesInput.schema.ts | 7 +- .../schemas/objects/UserSelect.schema.ts | 2 + .../UserUncheckedCreateInput.schema.ts | 2 + ...edCreateWithoutAppointmentsInput.schema.ts | 2 + ...teWithoutBackupDestinationsInput.schema.ts | 2 + ...checkedCreateWithoutBackupsInput.schema.ts | 2 + ...ncheckedCreateWithoutClaimsInput.schema.ts | 2 + ...ckedCreateWithoutCloudFilesInput.schema.ts | 2 + ...edCreateWithoutCloudFoldersInput.schema.ts | 2 + ...CreateWithoutCommunicationsInput.schema.ts | 2 + ...WithoutInsuranceCredentialsInput.schema.ts | 2 + ...dCreateWithoutNotificationsInput.schema.ts | 2 + ...edCreateWithoutNpiProvidersInput.schema.ts | 2 + ...heckedCreateWithoutPatientsInput.schema.ts | 2 + ...UncheckedCreateWithoutStaffInput.schema.ts | 2 + ...reateWithoutUpdatedPaymentsInput.schema.ts | 2 + .../UserUncheckedUpdateInput.schema.ts | 3 + .../UserUncheckedUpdateManyInput.schema.ts | 7 +- ...edUpdateWithoutAppointmentsInput.schema.ts | 3 + ...teWithoutBackupDestinationsInput.schema.ts | 3 + ...checkedUpdateWithoutBackupsInput.schema.ts | 3 + ...ncheckedUpdateWithoutClaimsInput.schema.ts | 3 + ...ckedUpdateWithoutCloudFilesInput.schema.ts | 3 + ...edUpdateWithoutCloudFoldersInput.schema.ts | 3 + ...UpdateWithoutCommunicationsInput.schema.ts | 3 + ...WithoutInsuranceCredentialsInput.schema.ts | 3 + ...dUpdateWithoutNotificationsInput.schema.ts | 3 + ...edUpdateWithoutNpiProvidersInput.schema.ts | 3 + ...heckedUpdateWithoutPatientsInput.schema.ts | 3 + ...UncheckedUpdateWithoutStaffInput.schema.ts | 3 + ...pdateWithoutUpdatedPaymentsInput.schema.ts | 3 + .../schemas/objects/UserUpdateInput.schema.ts | 3 + .../UserUpdateManyMutationInput.schema.ts | 7 +- ...erUpdateWithoutAppointmentsInput.schema.ts | 3 + ...teWithoutBackupDestinationsInput.schema.ts | 3 + .../UserUpdateWithoutBackupsInput.schema.ts | 3 + .../UserUpdateWithoutClaimsInput.schema.ts | 3 + ...UserUpdateWithoutCloudFilesInput.schema.ts | 3 + ...erUpdateWithoutCloudFoldersInput.schema.ts | 3 + ...UpdateWithoutCommunicationsInput.schema.ts | 3 + ...WithoutInsuranceCredentialsInput.schema.ts | 3 + ...rUpdateWithoutNotificationsInput.schema.ts | 3 + ...erUpdateWithoutNpiProvidersInput.schema.ts | 3 + .../UserUpdateWithoutPatientsInput.schema.ts | 3 + .../UserUpdateWithoutStaffInput.schema.ts | 3 + ...pdateWithoutUpdatedPaymentsInput.schema.ts | 3 + .../schemas/objects/UserWhereInput.schema.ts | 3 + .../results/UserAggregateResult.schema.ts | 2 + .../results/UserCreateResult.schema.ts | 2 + .../results/UserDeleteResult.schema.ts | 2 + .../results/UserFindFirstResult.schema.ts | 2 + .../results/UserFindManyResult.schema.ts | 2 + .../results/UserFindUniqueResult.schema.ts | 2 + .../results/UserGroupByResult.schema.ts | 4 + .../results/UserUpdateResult.schema.ts | 2 + .../results/UserUpsertResult.schema.ts | 2 + .../schemas/variants/input/User.input.ts | 2 + .../shared/schemas/variants/pure/User.pure.ts | 2 + .../schemas/variants/result/User.result.ts | 2 + 218 files changed, 1995 insertions(+), 1381 deletions(-) create mode 100644 .turbo/daemon/53b0054db79f7114-turbo.log.2026-04-10 create mode 100644 .turbo/daemon/53b0054db79f7114-turbo.log.2026-04-11 create mode 100644 apps/Frontend/src/components/database-management/folder-browser-modal.tsx create mode 100644 apps/SeleniumService/downloads/claim_confirmation_100048854788_202610000403500_20260410_201547.pdf diff --git a/.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-10 b/.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-10 new file mode 100644 index 0000000..e69de29 diff --git a/.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-11 b/.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-11 new file mode 100644 index 0000000..6e94491 --- /dev/null +++ b/.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-11 @@ -0,0 +1 @@ +2026-04-11T03:16:27.115983Z WARN get_package_file_hashes{include_default_files=true telemetry=None}: turborepo_scm::package_deps: git hashing failed for AnchoredSystemPath("packages/db") with resource error: Git error on /home/ff/Desktop/DentalManagementMHAprilgg/packages/db/shared/schemas/enums/UserScalarFieldEnum.schema.ts: could not find '/home/ff/Desktop/DentalManagementMHAprilgg/packages/db/shared/schemas/enums/UserScalarFieldEnum.schema.ts' to open: No such file or directory; class=Os (2); code=NotFound (-3) diff --git a/apps/Backend/src/cron/backupCheck.ts b/apps/Backend/src/cron/backupCheck.ts index 7c35cea..41604da 100755 --- a/apps/Backend/src/cron/backupCheck.ts +++ b/apps/Backend/src/cron/backupCheck.ts @@ -1,100 +1,134 @@ import cron from "node-cron"; import fs from "fs"; +import path from "path"; import { storage } from "../storage"; import { NotificationTypes } from "@repo/db/types"; import { backupDatabaseToPath } from "../services/databaseBackupService"; -/** - * Daily cron job to check if users haven't backed up in 7 days - * Creates a backup notification if overdue - */ +// Local backup folder in the app root (apps/Backend/backups) +const LOCAL_BACKUP_DIR = path.resolve(process.cwd(), "backups"); + +// Name of the USB backup subfolder the user creates on their drive +const USB_BACKUP_FOLDER_NAME = "USB Backup"; + +function ensureLocalBackupDir() { + if (!fs.existsSync(LOCAL_BACKUP_DIR)) { + fs.mkdirSync(LOCAL_BACKUP_DIR, { recursive: true }); + } +} + +async function runForAllUsers( + handler: (user: Awaited>[number]) => Promise +) { + const batchSize = 100; + let offset = 0; + while (true) { + const users = await storage.getUsers(batchSize, offset); + if (!users || users.length === 0) break; + for (const user of users) { + if (user.id == null) continue; + try { + await handler(user); + } catch (err) { + console.error(`Error processing user ${user.id}:`, err); + } + } + offset += batchSize; + } +} + export const startBackupCron = () => { - cron.schedule("0 22 * * *", async () => { - // Every calendar days, at 10 PM - // cron.schedule("*/10 * * * * *", async () => { // Every 10 seconds (for Test) + // ============================================================ + // 8 PM โ€” Local automatic backup to apps/Backend/backups/ + // ============================================================ + cron.schedule("0 20 * * *", async () => { + console.log("๐Ÿ”„ [8 PM] Running local auto-backup..."); + ensureLocalBackupDir(); - console.log("๐Ÿ”„ Running backup check..."); - - const userBatchSize = 100; - let userOffset = 0; - - while (true) { - // Fetch a batch of users - const users = await storage.getUsers(userBatchSize, userOffset); - if (!users || users.length === 0) break; - - for (const user of users) { - try { - if (user.id == null) { - continue; - } - - const destination = await storage.getActiveBackupDestination(user.id); - const lastBackup = await storage.getLastBackup(user.id); - - // ============================== - // CASE 1: Destination exists โ†’ auto backup - // ============================== - if (destination) { - if (!fs.existsSync(destination.path)) { - await storage.createNotification( - user.id, - "BACKUP", - "โŒ Automatic backup failed: external drive not connected." - ); - continue; - } - - try { - const filename = `dental_backup_${Date.now()}.zip`; - - await backupDatabaseToPath({ - destinationPath: destination.path, - filename, - }); - - await storage.createBackup(user.id); - await storage.deleteNotificationsByType(user.id, "BACKUP"); - - console.log(`โœ… Auto backup successful for user ${user.id}`); - continue; - } catch (err) { - console.error(`Auto backup failed for user ${user.id}`, err); - - await storage.createNotification( - user.id, - "BACKUP", - "โŒ Automatic backup failed. Please check your backup destination." - ); - continue; - } - } - - // ============================== - // CASE 2: No destination โ†’ fallback to reminder - // ============================== - - const daysSince = lastBackup?.createdAt - ? (Date.now() - new Date(lastBackup.createdAt).getTime()) / - (1000 * 60 * 60 * 24) - : Infinity; - - if (daysSince >= 7) { - await storage.createNotification( - user.id, - "BACKUP" as NotificationTypes, - "โš ๏ธ It has been more than 7 days since your last backup." - ); - console.log(`Notification created for user ${user.id}`); - } - } catch (err) { - console.error(`Error processing user ${user.id}:`, err); + await runForAllUsers(async (user) => { + if (!user.autoBackupEnabled) { + // No local backup โ€” check if a 7-day reminder is needed + const lastBackup = await storage.getLastBackup(user.id); + const daysSince = lastBackup?.createdAt + ? (Date.now() - new Date(lastBackup.createdAt).getTime()) / (1000 * 60 * 60 * 24) + : Infinity; + if (daysSince >= 7) { + await storage.createNotification( + user.id, + "BACKUP" as NotificationTypes, + "โš ๏ธ It has been more than 7 days since your last backup." + ); + console.log(`Reminder notification created for user ${user.id}`); } + return; } - userOffset += userBatchSize; // next user batch - } + try { + const filename = `dental_backup_user${user.id}_${Date.now()}.zip`; + await backupDatabaseToPath({ destinationPath: LOCAL_BACKUP_DIR, filename }); + await storage.createBackup(user.id); + await storage.deleteNotificationsByType(user.id, "BACKUP"); + console.log(`โœ… Local backup done for user ${user.id} โ†’ ${filename}`); + } catch (err) { + console.error(`Local backup failed for user ${user.id}`, err); + await storage.createNotification( + user.id, + "BACKUP", + "โŒ Automatic backup failed. Please check the server backup folder." + ); + } + }); - console.log("โœ… Daily backup check completed."); + console.log("โœ… [8 PM] Local backup complete."); + }); + + // ============================================================ + // 9 PM โ€” USB backup to the "USB Backup" folder on the drive + // ============================================================ + cron.schedule("0 21 * * *", async () => { + console.log("๐Ÿ”„ [9 PM] Running USB backup..."); + + await runForAllUsers(async (user) => { + if (!user.usbBackupEnabled) return; + + const destination = await storage.getActiveBackupDestination(user.id); + if (!destination) { + await storage.createNotification( + user.id, + "BACKUP", + "โŒ USB backup failed: no backup destination configured." + ); + return; + } + + // The target is the "USB Backup" subfolder inside the configured drive path + const usbBackupPath = path.join(destination.path, USB_BACKUP_FOLDER_NAME); + + if (!fs.existsSync(usbBackupPath)) { + await storage.createNotification( + user.id, + "BACKUP", + `โŒ USB backup failed: folder "${USB_BACKUP_FOLDER_NAME}" not found on the drive. Make sure the USB drive is connected and the folder exists.` + ); + return; + } + + try { + const filename = `dental_backup_usb_${Date.now()}.zip`; + await backupDatabaseToPath({ destinationPath: usbBackupPath, filename }); + await storage.createBackup(user.id); + await storage.deleteNotificationsByType(user.id, "BACKUP"); + console.log(`โœ… USB backup done for user ${user.id} โ†’ ${usbBackupPath}/${filename}`); + } catch (err) { + console.error(`USB backup failed for user ${user.id}`, err); + await storage.createNotification( + user.id, + "BACKUP", + "โŒ USB backup failed. Please check the USB drive and try again." + ); + } + }); + + console.log("โœ… [9 PM] USB backup complete."); }); }; diff --git a/apps/Backend/src/routes/database-management.ts b/apps/Backend/src/routes/database-management.ts index 895077d..7b4b685 100755 --- a/apps/Backend/src/routes/database-management.ts +++ b/apps/Backend/src/routes/database-management.ts @@ -318,6 +318,88 @@ router.delete("/destination/:id", async (req, res) => { res.json({ success: true }); }); +// GET directory listing for folder browser +router.get("/browse", async (req, res) => { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ error: "Unauthorized" }); + + const requestedPath = (req.query.path as string) || "/"; + + // Resolve and sanitize โ€” must be absolute + const resolved = path.resolve(requestedPath); + + try { + const entries = fs.readdirSync(resolved, { withFileTypes: true }); + const dirs = entries + .filter((e) => e.isDirectory()) + .map((e) => ({ + name: e.name, + path: path.join(resolved, e.name), + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + const parent = resolved !== "/" ? path.dirname(resolved) : null; + + res.json({ current: resolved, parent, dirs }); + } catch (err: any) { + res.status(400).json({ error: err.message || "Cannot read directory" }); + } +}); + +// GET usb backup setting +router.get("/usb-backup-setting", async (req, res) => { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ error: "Unauthorized" }); + + const user = await storage.getUser(userId); + if (!user) return res.status(404).json({ error: "User not found" }); + + res.json({ usbBackupEnabled: user.usbBackupEnabled }); +}); + +// PUT usb backup setting +router.put("/usb-backup-setting", async (req, res) => { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ error: "Unauthorized" }); + + const { usbBackupEnabled } = req.body; + if (typeof usbBackupEnabled !== "boolean") { + return res.status(400).json({ error: "usbBackupEnabled must be a boolean" }); + } + + const updated = await storage.updateUser(userId, { usbBackupEnabled }); + if (!updated) return res.status(404).json({ error: "User not found" }); + + res.json({ usbBackupEnabled: updated.usbBackupEnabled }); +}); + +// GET auto backup setting +router.get("/auto-backup-setting", async (req, res) => { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ error: "Unauthorized" }); + + const user = await storage.getUser(userId); + if (!user) return res.status(404).json({ error: "User not found" }); + + res.json({ autoBackupEnabled: user.autoBackupEnabled }); +}); + +// PUT auto backup setting +router.put("/auto-backup-setting", async (req, res) => { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ error: "Unauthorized" }); + + const { autoBackupEnabled } = req.body; + if (typeof autoBackupEnabled !== "boolean") { + return res.status(400).json({ error: "autoBackupEnabled must be a boolean" }); + } + + const updated = await storage.updateUser(userId, { autoBackupEnabled }); + if (!updated) return res.status(404).json({ error: "User not found" }); + + res.json({ autoBackupEnabled: updated.autoBackupEnabled }); +}); + router.post("/backup-path", async (req, res) => { const userId = req.user?.id; if (!userId) return res.status(401).json({ error: "Unauthorized" }); diff --git a/apps/Frontend/src/components/database-management/backup-destination-manager.tsx b/apps/Frontend/src/components/database-management/backup-destination-manager.tsx index 6b520ec..acab5ee 100755 --- a/apps/Frontend/src/components/database-management/backup-destination-manager.tsx +++ b/apps/Frontend/src/components/database-management/backup-destination-manager.tsx @@ -3,6 +3,7 @@ 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, @@ -16,11 +17,13 @@ import { import { FolderOpen, 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(null); + const [browserOpen, setBrowserOpen] = useState(false); // ============================== // Queries @@ -36,6 +39,39 @@ export function BackupDestinationManager() { }, }); + 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 // ============================== @@ -67,30 +103,10 @@ export function BackupDestinationManager() { }); // ============================== - // Folder picker (browser limitation) + // Folder browser // ============================== - const openFolderPicker = async () => { - // @ts-ignore - if (!window.showDirectoryPicker) { - toast({ - title: "Not supported", - description: "Your browser does not support folder picking", - variant: "destructive", - }); - return; - } - - try { - // @ts-ignore - const dirHandle = await window.showDirectoryPicker(); - - toast({ - title: "Folder selected", - description: `Selected folder: ${dirHandle.name}. Please enter the full path manually.`, - }); - } catch { - // user cancelled - } + const handleFolderSelect = (selectedPath: string) => { + setPath(selectedPath); }; // ============================== @@ -102,17 +118,46 @@ export function BackupDestinationManager() { External Backup Destination +
+ usbToggleMutation.mutate(checked)} + disabled={usbToggleMutation.isPending} + /> + + + (daily at 9 PM โ†’ saves to the "USB Backup" folder on your drive) + +
+ +

+ Enter the root path of your USB drive below. The app will automatically back up to the{" "} + USB Backup folder inside it every night at 9 PM when the toggle is on. +

+
setPath(e.target.value)} /> -
+ setBrowserOpen(false)} + onSelect={handleFolderSelect} + /> + + )} + + {/* Directory list */} +
+ {isLoading && ( +
+ + Loading... +
+ )} + {isError && ( +

Cannot read this directory.

+ )} + {!isLoading && !isError && data?.dirs.length === 0 && ( +

No sub-folders here.

+ )} + {!isLoading && + !isError && + data?.dirs.map((dir) => ( + + ))} +
+ +

+ Single-click to select ยท Double-click to open +

+ + + + + + + + ); +} diff --git a/apps/Frontend/src/components/layout/sidebar.tsx b/apps/Frontend/src/components/layout/sidebar.tsx index e5096c4..3464738 100755 --- a/apps/Frontend/src/components/layout/sidebar.tsx +++ b/apps/Frontend/src/components/layout/sidebar.tsx @@ -77,7 +77,7 @@ export function Sidebar() { icon: , }, { - name: "Backup Database", + name: "Database Management", path: "/database-management", icon: , }, diff --git a/apps/Frontend/src/pages/database-management-page.tsx b/apps/Frontend/src/pages/database-management-page.tsx index eb70ab5..31a4dea 100755 --- a/apps/Frontend/src/pages/database-management-page.tsx +++ b/apps/Frontend/src/pages/database-management-page.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; import { useToast } from "@/hooks/use-toast"; import { Database, @@ -75,6 +76,40 @@ export default function DatabaseManagementPage() { } } + // ----- Auto backup setting query ----- + const { data: autoBackupData } = useQuery({ + queryKey: ["/db/auto-backup-setting"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/database-management/auto-backup-setting"); + return res.json(); + }, + }); + + const autoBackupEnabled = autoBackupData?.autoBackupEnabled ?? true; + + const autoBackupMutation = useMutation({ + mutationFn: async (enabled: boolean) => { + const res = await apiRequest("PUT", "/api/database-management/auto-backup-setting", { + autoBackupEnabled: enabled, + }); + return res.json(); + }, + onSuccess: (data) => { + queryClient.setQueryData(["/db/auto-backup-setting"], data); + toast({ + title: "Setting Saved", + description: `Automatic backup ${data.autoBackupEnabled ? "enabled" : "disabled"}.`, + }); + }, + onError: () => { + toast({ + title: "Error", + description: "Failed to update automatic backup setting.", + variant: "destructive", + }); + }, + }); + // ----- Backup mutation ----- const backupMutation = useMutation({ mutationFn: async () => { @@ -178,6 +213,22 @@ export default function DatabaseManagementPage() { including patients, appointments, claims, and all related data.

+
+ autoBackupMutation.mutate(checked)} + disabled={autoBackupMutation.isPending} + /> + + (daily at 8 PM to server backup folder) +
+