From 8945e56f14e7e27651c0dbcd40aa2a13f3b494cc Mon Sep 17 00:00:00 2001 From: Vishnu Date: Mon, 2 Jun 2025 21:49:37 +0530 Subject: [PATCH] insuranceCred half --- apps/Backend/src/routes/index.ts | 3 +- apps/Backend/src/routes/insuranceCreds.ts | 79 +++++++++ apps/Backend/src/routes/users.ts | 19 +++ apps/Backend/src/storage/index.ts | 54 +++++- .../components/settings/InsuranceCredForm.tsx | 113 +++++++++++++ .../settings/insuranceCredTable.tsx | 57 +++++++ apps/Frontend/src/pages/settings-page.tsx | 156 +++++++++++++++++- packages/db/prisma/schema.prisma | 50 +++--- packages/db/usedSchemas/index.ts | 3 +- 9 files changed, 509 insertions(+), 25 deletions(-) create mode 100644 apps/Backend/src/routes/insuranceCreds.ts create mode 100644 apps/Frontend/src/components/settings/InsuranceCredForm.tsx create mode 100644 apps/Frontend/src/components/settings/insuranceCredTable.tsx diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts index 3add2a6..a6ab092 100644 --- a/apps/Backend/src/routes/index.ts +++ b/apps/Backend/src/routes/index.ts @@ -5,7 +5,7 @@ import userRoutes from './users' import staffRoutes from './staffs' import pdfExtractionRoutes from './pdfExtraction'; import claimsRoutes from './claims'; - +import insuranceCredsRoutes from './insuranceCreds'; const router = Router(); router.use('/patients', patientRoutes); @@ -14,5 +14,6 @@ router.use('/users', userRoutes); router.use('/staffs', staffRoutes); router.use('/pdfExtraction', pdfExtractionRoutes); router.use('/claims', claimsRoutes); +router.use('/insuranceCreds', insuranceCredsRoutes); export default router; \ No newline at end of file diff --git a/apps/Backend/src/routes/insuranceCreds.ts b/apps/Backend/src/routes/insuranceCreds.ts new file mode 100644 index 0000000..4d5c2ba --- /dev/null +++ b/apps/Backend/src/routes/insuranceCreds.ts @@ -0,0 +1,79 @@ +import express, { Request, Response, NextFunction } from "express"; +import { storage } from "../storage"; +import { InsuranceCredentialUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { z } from "zod"; + +const router = express.Router(); + +// ✅ Types +type InsuranceCredential = z.infer; + +const insertInsuranceCredentialSchema = ( + InsuranceCredentialUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ id: true }); + +type InsertInsuranceCredential = z.infer; + +// ✅ Get all credentials for a user +router.get("/", async (req: Request, res: Response):Promise => { + try { + if (!req.user || !req.user.id) { + return res.status(401).json({ message: "Unauthorized: user info missing" }); + } + const userId = req.user.id; + + const credentials = await storage.getInsuranceCredentialsByUser(userId); + return res.status(200).json(credentials); + } catch (err) { + return res.status(500).json({ error: "Failed to fetch credentials", details: String(err) }); + } +}); + +// ✅ Create credential for a user +router.post("/", async (req: Request, res: Response):Promise => { + try { + if (!req.user || !req.user.id) { + return res.status(401).json({ message: "Unauthorized: user info missing" }); + } + const userId = req.user.id; + + const parseResult = insertInsuranceCredentialSchema.safeParse({ ...req.body, userId }); + if (!parseResult.success) { + return res.status(400).json({ error: parseResult.error.flatten() }); + } + + const credential = await storage.createInsuranceCredential(parseResult.data); + return res.status(201).json(credential); + } catch (err) { + return res.status(500).json({ error: "Failed to create credential", details: String(err) }); + } +}); + +// ✅ Update credential +router.put("/:id", async (req: Request, res: Response):Promise => { + try { + const id = Number(req.params.id); + if (isNaN(id)) return res.status(400).send("Invalid credential ID"); + + const updates = req.body as Partial; + const credential = await storage.updateInsuranceCredential(id, updates); + return res.status(200).json(credential); + } catch (err) { + return res.status(500).json({ error: "Failed to update credential", details: String(err) }); + } +}); + +// ✅ Delete a credential +router.delete("/:id", async (req: Request, res: Response):Promise => { + try { + const id = Number(req.params.id); + if (isNaN(id)) return res.status(400).send("Invalid ID"); + + await storage.deleteInsuranceCredential(id); + return res.status(204).send(); + } catch (err) { + return res.status(500).json({ error: "Failed to delete credential", details: String(err) }); + } +}); + +export default router; diff --git a/apps/Backend/src/routes/users.ts b/apps/Backend/src/routes/users.ts index fde23c4..baaa7de 100644 --- a/apps/Backend/src/routes/users.ts +++ b/apps/Backend/src/routes/users.ts @@ -3,6 +3,9 @@ import type { Request, Response } from "express"; import { storage } from "../storage"; import { z } from "zod"; import { UserUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; + const router = Router(); @@ -64,6 +67,13 @@ router.post("/", async (req: Request, res: Response) => { } }); +// Function to hash password using bcrypt +async function hashPassword(password: string) { + const saltRounds = 10; // Salt rounds for bcrypt + const hashedPassword = await bcrypt.hash(password, saltRounds); + return hashedPassword; +} + // PUT: Update user router.put("/:id", async (req: Request, res: Response):Promise => { try { @@ -75,6 +85,15 @@ router.put("/:id", async (req: Request, res: Response):Promise => { const updates = userUpdateSchema.parse(req.body); + + // If password is provided and non-empty, hash it + if (updates.password && updates.password.trim() !== "") { + updates.password = await hashPassword(updates.password); + } else { + // Remove password field if empty, so it won't overwrite existing password with blank + delete updates.password; + } + const updatedUser = await storage.updateUser(id, updates); if (!updatedUser) return res.status(404).send("User not found"); diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index c16260c..7065b98 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -4,7 +4,8 @@ import { PatientUncheckedCreateInputObjectSchema, UserUncheckedCreateInputObjectSchema, StaffUncheckedCreateInputObjectSchema, - ClaimUncheckedCreateInputObjectSchema, + ClaimUncheckedCreateInputObjectSchema, + InsuranceCredentialUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; import { z } from "zod"; @@ -95,7 +96,7 @@ type RegisterFormValues = z.infer; // staff types: type Staff = z.infer; -// Claim typse: +// Claim typse: const insertClaimSchema = ( ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ @@ -118,6 +119,19 @@ type UpdateClaim = z.infer; type Claim = z.infer; +// InsuraneCreds types: +type InsuranceCredential = z.infer< + typeof InsuranceCredentialUncheckedCreateInputObjectSchema +>; + +const insertInsuranceCredentialSchema = ( + InsuranceCredentialUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ id: true }); + +type InsertInsuranceCredential = z.infer< + typeof insertInsuranceCredentialSchema +>; + export interface IStorage { // User methods getUser(id: number): Promise; @@ -135,7 +149,7 @@ export interface IStorage { // Appointment methods getAppointment(id: number): Promise; - getAllAppointments(): Promise; + getAllAppointments(): Promise; getAppointmentsByUserId(userId: number): Promise; getAppointmentsByPatientId(patientId: number): Promise; createAppointment(appointment: InsertAppointment): Promise; @@ -160,6 +174,17 @@ export interface IStorage { createClaim(claim: InsertClaim): Promise; updateClaim(id: number, updates: UpdateClaim): Promise; deleteClaim(id: number): Promise; + + // InsuranceCredential methods + getInsuranceCredentialsByUser(userId: number): Promise; + createInsuranceCredential( + data: InsertInsuranceCredential + ): Promise; + updateInsuranceCredential( + id: number, + updates: Partial + ): Promise; + deleteInsuranceCredential(id: number): Promise; } export const storage: IStorage = { @@ -356,4 +381,27 @@ export const storage: IStorage = { throw new Error(`Claim with ID ${id} not found`); } }, + + // Insurance Creds + async getInsuranceCredentialsByUser(userId: number) { + return await db.insuranceCredential.findMany({ where: { userId } }); + }, + + async createInsuranceCredential(data: InsertInsuranceCredential) { + return await db.insuranceCredential.create({ data: data as InsuranceCredential }); + }, + + async updateInsuranceCredential( + id: number, + updates: Partial + ) { + return await db.insuranceCredential.update({ + where: { id }, + data: updates, + }); + }, + + async deleteInsuranceCredential(id: number) { + await db.insuranceCredential.delete({ where: { id } }); + }, }; diff --git a/apps/Frontend/src/components/settings/InsuranceCredForm.tsx b/apps/Frontend/src/components/settings/InsuranceCredForm.tsx new file mode 100644 index 0000000..f8e4d6a --- /dev/null +++ b/apps/Frontend/src/components/settings/InsuranceCredForm.tsx @@ -0,0 +1,113 @@ +import { useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { apiRequest } from "@/lib/queryClient"; +import { toast } from "@/hooks/use-toast"; + +type CredentialFormProps = { + onClose: () => void; + userId: number; +}; + +export function CredentialForm({ onClose, userId }: CredentialFormProps) { + const [siteKey, setSiteKey] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const queryClient = useQueryClient(); + + const createCredentialMutation = useMutation({ + mutationFn: async () => { + const payload = { + siteKey: siteKey.trim(), + username: username.trim(), + password: password.trim(), + userId, + }; + + const res = await apiRequest("POST", "/api/insuranceCreds/", payload); + if (!res.ok) { + const errorData = await res.json().catch(() => null); + throw new Error(errorData?.message || "Failed to create credential"); + } + return res.json(); + }, + onSuccess: () => { + toast({ title: "Credential created." }); + queryClient.invalidateQueries({ queryKey: ["/api/insuranceCreds/"] }); + onClose(); + }, + onError: (error: any) => { + toast({ + title: "Error", + description: error.message || "Unknown error", + variant: "destructive", + }); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!siteKey || !username || !password) { + toast({ + title: "Error", + description: "All fields are required.", + variant: "destructive", + }); + return; + } + createCredentialMutation.mutate(); + }; + + return ( +
+
+

Create Credential

+
+
+ + setSiteKey(e.target.value)} + className="mt-1 p-2 border rounded w-full" + placeholder="e.g., github, slack" + /> +
+
+ + setUsername(e.target.value)} + className="mt-1 p-2 border rounded w-full" + /> +
+
+ + setPassword(e.target.value)} + className="mt-1 p-2 border rounded w-full" + /> +
+
+ + +
+
+
+
+ ); +} diff --git a/apps/Frontend/src/components/settings/insuranceCredTable.tsx b/apps/Frontend/src/components/settings/insuranceCredTable.tsx new file mode 100644 index 0000000..827eea8 --- /dev/null +++ b/apps/Frontend/src/components/settings/insuranceCredTable.tsx @@ -0,0 +1,57 @@ +// components/CredentialTable.tsx +import { useQuery } from "@tanstack/react-query"; +import { apiRequest } from "@/lib/queryClient"; +import { toast } from "@/hooks/use-toast"; + +type Credential = { + id: number; + siteKey: string; + username: string; + password: string; +}; + +export function CredentialTable() { + const { data, isLoading, error } = useQuery({ + queryKey: ["/api/credentials/"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/insuranceCreds/"); + if (!res.ok) { + throw new Error("Failed to fetch credentials"); + } + return res.json() as Promise; + }, + }); + + if (isLoading) return

Loading credentials...

; + if (error) return

Failed to load credentials.

; + if (!data || data.length === 0) + return

No credentials found.

; + + return ( +
+ + + + + + + + + + + {data.map((cred) => ( + + + + + + + ))} + +
SiteUsernamePasswordActions
{cred.siteKey}{cred.username}{cred.password} + + +
+
+ ); +} diff --git a/apps/Frontend/src/pages/settings-page.tsx b/apps/Frontend/src/pages/settings-page.tsx index 080a80f..f8f94f4 100644 --- a/apps/Frontend/src/pages/settings-page.tsx +++ b/apps/Frontend/src/pages/settings-page.tsx @@ -10,6 +10,8 @@ import { z } from "zod"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { StaffForm } from "@/components/staffs/staff-form"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; +import { CredentialForm } from "@/components/settings/InsuranceCredForm"; +import { CredentialTable } from "@/components/settings/insuranceCredTable"; // Correctly infer Staff type from zod schema type Staff = z.infer; @@ -20,6 +22,7 @@ export default function SettingsPage() { // Modal and editing staff state const [modalOpen, setModalOpen] = useState(false); + const [credentialModalOpen, setCredentialModalOpen] = useState(false); const [editingStaff, setEditingStaff] = useState(null); const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev); @@ -121,7 +124,7 @@ export default function SettingsPage() { return id; }, onSuccess: () => { - setIsDeleteStaffOpen(false); + setIsDeleteStaffOpen(false); queryClient.invalidateQueries({ queryKey: ["/api/staffs/"] }); toast({ title: "Staff Removed", @@ -217,6 +220,68 @@ export default function SettingsPage() { `Viewing staff member:\n${staff.name} (${staff.email || "No email"})` ); + // MANAGE USER + + const [usernameUser, setUsernameUser] = useState(""); + + //fetch user + const { + data: currentUser, + isLoading: isUserLoading, + isError: isUserError, + error: userError, + } = useQuery({ + queryKey: ["/api/users/"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/users/"); + if (!res.ok) throw new Error("Failed to fetch user"); + return res.json(); + }, + }); + + // Populate fields after fetch + useEffect(() => { + if (currentUser) { + setUsernameUser(currentUser.username); + } + }, [currentUser]); + + //update user mutation + const updateUserMutate = useMutation({ + mutationFn: async ( + updates: Partial<{ username: string; password: string }> + ) => { + if (!currentUser?.id) throw new Error("User not loaded"); + const res = await apiRequest( + "PUT", + `/api/users/${currentUser.id}`, + updates + ); + if (!res.ok) { + const errorData = await res.json().catch(() => null); + throw new Error(errorData?.error || "Failed to update user"); + } + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/users/"] }); + toast({ + title: "Updated", + description: "Your profile has been updated.", + variant: "default", + }); + }, + onError: (err: any) => { + toast({ + title: "Error", + description: err?.message || "Failed to update user", + variant: "destructive", + }); + }, + }); + + + return (
)} + + + +

User Settings

+ + {isUserLoading ? ( +

Loading user...

+ ) : isUserError ? ( +

{(userError as Error)?.message}

+ ) : ( +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const password = + formData.get("password")?.toString().trim() || undefined; + + updateUserMutate.mutate({ + username: usernameUser?.trim() || undefined, + password: password || undefined, + }); + }} + > +
+ + setUsernameUser(e.target.value)} + className="mt-1 p-2 border rounded w-full" + /> +
+ +
+ + +

+ Leave blank to keep current password. +

+
+ + +
+ )} +
+
+ + {/* Credential Section */} +
+ + + + +

Saved Credentials

+ +
+
+
+ + {credentialModalOpen && currentUser?.id && ( + setCredentialModalOpen(false)} + /> +)} + + diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 12ffb5f..bb52d93 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -20,12 +20,13 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - username String @unique - password String - patients Patient[] - appointments Appointment[] - claims Claim[] + id Int @id @default(autoincrement()) + username String @unique + password String + patients Patient[] + appointments Appointment[] + claims Claim[] + insuranceCredentials InsuranceCredential[] } model Patient { @@ -70,8 +71,7 @@ model Appointment { patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id]) staff Staff? @relation(fields: [staffId], references: [id]) - claims Claim[] - + claims Claim[] } model Staff { @@ -86,27 +86,27 @@ model Staff { } model Claim { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) patientId Int appointmentId Int userId Int staffId Int patientName String - memberId String - dateOfBirth DateTime @db.Date + memberId String + dateOfBirth DateTime @db.Date remarks String serviceDate DateTime insuranceProvider String // e.g., "Delta MA" - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - status String @default("pending") // "pending", "completed", "cancelled", "no-show" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + status String @default("pending") // "pending", "completed", "cancelled", "no-show" - patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) - appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade) - user User? @relation(fields: [userId], references: [id]) - staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id]) + patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) + appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) + staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id]) - serviceLines ServiceLine[] + serviceLines ServiceLine[] } model ServiceLine { @@ -120,3 +120,15 @@ model ServiceLine { billedAmount Float claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade) } + +model InsuranceCredential { + id Int @id @default(autoincrement()) + userId Int + siteKey String @unique // Unique key for site (e.g. "github", "slack") + username String + password String + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} diff --git a/packages/db/usedSchemas/index.ts b/packages/db/usedSchemas/index.ts index 6369e04..c7f9cec 100644 --- a/packages/db/usedSchemas/index.ts +++ b/packages/db/usedSchemas/index.ts @@ -3,4 +3,5 @@ export * from '../shared/schemas/objects/AppointmentUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PatientUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/StaffUncheckedCreateInput.schema' -export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema' \ No newline at end of file +export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema' +export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema' \ No newline at end of file