From b03b7efcb404522f88987333cede19be5ca1cb08 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Wed, 14 May 2025 17:12:54 +0530 Subject: [PATCH] major functionalities are fixed --- apps/Backend/.env.example | 5 + apps/Backend/package.json | 40 + apps/Backend/src/app.ts | 36 + apps/Backend/src/index.ts | 11 + .../src/middlewares/auth.middleware.ts | 26 + .../src/middlewares/error.middleware.ts | 6 + .../src/middlewares/logger.middleware.ts | 33 + apps/Backend/src/routes/appointements.ts | 351 ++++ apps/Backend/src/routes/auth.ts | 87 + apps/Backend/src/routes/index.ts | 14 + apps/Backend/src/routes/patients.ts | 252 +++ apps/Backend/src/routes/staffs.ts | 77 + apps/Backend/src/routes/users.ts | 108 ++ apps/Backend/src/storage/index.ts | 284 ++++ apps/Backend/src/types/express.types.d.ts | 10 + apps/Backend/tsconfig.json | 10 + apps/Frontend/.env.example | 2 + .../appointments/appointment-form.tsx | 766 +++++---- .../appointments/appointment-table.tsx | 10 +- .../components/patients/add-patient-modal.tsx | 110 +- .../src/components/patients/patient-form.tsx | 206 ++- apps/Frontend/src/components/ui/dialog.tsx | 4 +- apps/Frontend/src/hooks/use-auth.tsx | 37 +- apps/Frontend/src/index.css | 7 + apps/Frontend/src/lib/queryClient.ts | 24 +- apps/Frontend/src/pages/appointments-page.tsx | 647 ++++--- apps/Frontend/src/pages/dashboard.tsx | 424 +++-- apps/Frontend/src/pages/patients-page.tsx | 232 ++- package-lock.json | 1497 ++++++++++++++++- package.json | 12 +- .../migration.sql | 17 + .../migration.sql | 3 + packages/db/prisma/schema.prisma | 75 +- packages/db/prisma/seed.ts | 93 + 34 files changed, 4434 insertions(+), 1082 deletions(-) create mode 100644 apps/Backend/.env.example create mode 100644 apps/Backend/package.json create mode 100644 apps/Backend/src/app.ts create mode 100644 apps/Backend/src/index.ts create mode 100644 apps/Backend/src/middlewares/auth.middleware.ts create mode 100644 apps/Backend/src/middlewares/error.middleware.ts create mode 100644 apps/Backend/src/middlewares/logger.middleware.ts create mode 100644 apps/Backend/src/routes/appointements.ts create mode 100644 apps/Backend/src/routes/auth.ts create mode 100644 apps/Backend/src/routes/index.ts create mode 100644 apps/Backend/src/routes/patients.ts create mode 100644 apps/Backend/src/routes/staffs.ts create mode 100644 apps/Backend/src/routes/users.ts create mode 100644 apps/Backend/src/storage/index.ts create mode 100644 apps/Backend/src/types/express.types.d.ts create mode 100644 apps/Backend/tsconfig.json create mode 100644 apps/Frontend/.env.example create mode 100644 packages/db/prisma/migrations/20250512103036_add_staff_support/migration.sql create mode 100644 packages/db/prisma/migrations/20250512134001_appointment_datatime_to_string/migration.sql create mode 100644 packages/db/prisma/seed.ts diff --git a/apps/Backend/.env.example b/apps/Backend/.env.example new file mode 100644 index 0000000..06bff2e --- /dev/null +++ b/apps/Backend/.env.example @@ -0,0 +1,5 @@ +HOST="localhost" +PORT=3000 +FRONTEND_URL="http://localhost:5173" +JWT_SECRET = 'dentalsecret' +DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp \ No newline at end of file diff --git a/apps/Backend/package.json b/apps/Backend/package.json new file mode 100644 index 0000000..5f89e61 --- /dev/null +++ b/apps/Backend/package.json @@ -0,0 +1,40 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.21.2", + "express-session": "^1.18.1", + "jsonwebtoken": "^9.0.2", + "passport": "^0.7.0", + "passport-local": "^1.0.0", + "ws": "^8.18.0", + "zod": "^3.24.2", + "zod-validation-error": "^3.4.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.1", + "@types/express-session": "^1.18.0", + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "20.16.11", + "@types/passport": "^1.0.16", + "@types/passport-local": "^1.0.38", + "@types/ws": "^8.5.13", + "ts-node-dev": "^2.0.0" + } +} diff --git a/apps/Backend/src/app.ts b/apps/Backend/src/app.ts new file mode 100644 index 0000000..472a62f --- /dev/null +++ b/apps/Backend/src/app.ts @@ -0,0 +1,36 @@ +import express from 'express'; +import cors from "cors"; +import routes from './routes'; +import { errorHandler } from './middlewares/error.middleware'; +import { apiLogger } from './middlewares/logger.middleware'; +import authRoutes from './routes/auth' +import { authenticateJWT } from './middlewares/auth.middleware'; +import dotenv from 'dotenv'; + +dotenv.config(); +const FRONTEND_URL = process.env.FRONTEND_URL; + + +const app = express(); + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); // For form data +app.use(apiLogger); + + +console.log(FRONTEND_URL); + +app.use(cors({ + origin: FRONTEND_URL, // Make sure this matches the frontend URL + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Allow these HTTP methods + allowedHeaders: ['Content-Type', 'Authorization'], // Allow necessary headers +credentials: true, +})); + + +app.use('/api/auth', authRoutes); +app.use('/api', authenticateJWT, routes); + +app.use(errorHandler); + +export default app; \ No newline at end of file diff --git a/apps/Backend/src/index.ts b/apps/Backend/src/index.ts new file mode 100644 index 0000000..78a0ebb --- /dev/null +++ b/apps/Backend/src/index.ts @@ -0,0 +1,11 @@ +import app from './app'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const HOST = process.env.HOST; +const PORT = process.env.PORT; + +app.listen(PORT, () => { + console.log(`Server running at http://${HOST}:${PORT}`); +}); \ No newline at end of file diff --git a/apps/Backend/src/middlewares/auth.middleware.ts b/apps/Backend/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..a13bdce --- /dev/null +++ b/apps/Backend/src/middlewares/auth.middleware.ts @@ -0,0 +1,26 @@ +import jwt from 'jsonwebtoken'; +import { Request, Response, NextFunction } from 'express'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; // Secret used for signing JWTs + +export function authenticateJWT(req: Request, res: Response, next: NextFunction): void{ + + // Check the Authorization header for the Bearer token + const token = req.header('Authorization')?.split(' ')[1]; // Extract token from Authorization header + + if (!token) { + res.status(401).send("Access denied. No token provided."); + return; + } + + // Verify JWT + jwt.verify(token, JWT_SECRET, (err, decoded) => { + if (err) { + return res.status(403).send("Forbidden. Invalid token."); + } + + // Attach the decoded user data to the request object + req.user = decoded as Express.User; + next(); // Proceed to the next middleware or route handler + }); +} diff --git a/apps/Backend/src/middlewares/error.middleware.ts b/apps/Backend/src/middlewares/error.middleware.ts new file mode 100644 index 0000000..d8c72d2 --- /dev/null +++ b/apps/Backend/src/middlewares/error.middleware.ts @@ -0,0 +1,6 @@ +import { Request, Response, NextFunction } from 'express'; + +export const errorHandler = (err: any, _req: Request, res: Response, _next: NextFunction) => { + console.error(err); + res.status(err.status || 500).json({ message: err.message || 'Internal Server Error' }); +}; diff --git a/apps/Backend/src/middlewares/logger.middleware.ts b/apps/Backend/src/middlewares/logger.middleware.ts new file mode 100644 index 0000000..4b6a5de --- /dev/null +++ b/apps/Backend/src/middlewares/logger.middleware.ts @@ -0,0 +1,33 @@ +import { Request, Response, NextFunction } from "express"; + +function log(message: string) { + console.log(`[${new Date().toISOString()}] ${message}`); +} + +export function apiLogger(req: Request, res: Response, next: NextFunction) { + const start = Date.now(); + const path = req.path; + let capturedJsonResponse: Record | undefined = undefined; + + const originalResJson = res.json; + res.json = function (bodyJson, ...args) { + capturedJsonResponse = bodyJson; + return originalResJson.apply(res, [bodyJson, ...args]); + }; + + res.on("finish", () => { + const duration = Date.now() - start; + if (path.startsWith("/api")) { + let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; + if (capturedJsonResponse) { + logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; + } + if (logLine.length > 80) { + logLine = logLine.slice(0, 79) + "…"; + } + log(logLine); + } + }); + + next(); +} diff --git a/apps/Backend/src/routes/appointements.ts b/apps/Backend/src/routes/appointements.ts new file mode 100644 index 0000000..0cd1df1 --- /dev/null +++ b/apps/Backend/src/routes/appointements.ts @@ -0,0 +1,351 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import { storage } from "../storage"; +import { + AppointmentUncheckedCreateInputObjectSchema, + PatientUncheckedCreateInputObjectSchema, +} from "@repo/db/shared/schemas"; +import { z } from "zod"; + +const router = Router(); + +//creating types out of schema auto generated. +type Appointment = z.infer; + +const insertAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertAppointment = z.infer; + +const updateAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + }) + .partial(); +type UpdateAppointment = z.infer; + +const PatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + appointments: true, +}); +type Patient = z.infer; + +const insertPatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertPatient = z.infer; + +const updatePatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); + +type UpdatePatient = z.infer; + + + + +// Get all appointments +router.get("/all", async (req: Request, res: Response): Promise => { + try { + const appointments = await storage.getAllAppointments(); + + res.json(appointments); + } catch (error) { + res.status(500).json({ message: "Failed to retrieve all appointments" }); + } +}); + +// Get a single appointment by ID +router.get( + "/:id", + + async (req: Request, res: Response): Promise => { + try { + const appointmentIdParam = req.params.id; + + // Ensure that patientIdParam exists and is a valid number + if (!appointmentIdParam) { + return res.status(400).json({ message: "Appointment ID is required" }); + } + + const appointmentId = parseInt(appointmentIdParam); + + const appointment = await storage.getAppointment(appointmentId); + + if (!appointment) { + return res.status(404).json({ message: "Appointment not found" }); + } + + // Ensure the appointment belongs to the logged-in user + if (appointment.userId !== req.user!.id) { + return res.status(403).json({ message: "Forbidden" }); + } + + res.json(appointment); + } catch (error) { + res.status(500).json({ message: "Failed to retrieve appointment" }); + } + } +); + + +// Create a new appointment +router.post( + "/", + + async (req: Request, res: Response): Promise => { + try { + console.log("Appointment creation request body:", req.body); + + // Validate request body + const appointmentData = insertAppointmentSchema.parse({ + ...req.body, + userId: req.user!.id, + }); + + console.log("Validated appointment data:", appointmentData); + + // Verify patient exists and belongs to user + const patient = await storage.getPatient(appointmentData.patientId); + if (!patient) { + console.log("Patient not found:", appointmentData.patientId); + return res.status(404).json({ message: "Patient not found" }); + } + + if (patient.userId !== req.user!.id) { + console.log( + "Patient belongs to another user. Patient userId:", + patient.userId, + "Request userId:", + req.user!.id + ); + return res.status(403).json({ message: "Forbidden" }); + } + + // Check if there's already an appointment at this time slot + const existingAppointments = await storage.getAppointmentsByUserId( + req.user!.id + ); + const conflictingAppointment = existingAppointments.find( + (apt) => + apt.date === appointmentData.date && + apt.startTime === appointmentData.startTime && + apt.notes?.includes( + appointmentData.notes.split("Appointment with ")[1] + ) + ); + + if (conflictingAppointment) { + console.log( + "Time slot already booked:", + appointmentData.date, + appointmentData.startTime + ); + return res.status(409).json({ + message: + "This time slot is already booked. Please select another time or staff member.", + }); + } + + // Create appointment + const appointment = await storage.createAppointment(appointmentData); + console.log("Appointment created successfully:", appointment); + res.status(201).json(appointment); + } catch (error) { + console.error("Error creating appointment:", error); + + if (error instanceof z.ZodError) { + console.log( + "Validation error details:", + JSON.stringify(error.format(), null, 2) + ); + return res.status(400).json({ + message: "Validation error", + errors: error.format(), + }); + } + + res.status(500).json({ + message: "Failed to create appointment", + error: error instanceof Error ? error.message : String(error), + }); + } + } +); + +// Update an existing appointment +router.put( + "/:id", + + async (req: Request, res: Response): Promise => { + try { + const appointmentIdParam = req.params.id; + if (!appointmentIdParam) { + return res.status(400).json({ message: "Appointment ID is required" }); + } + const appointmentId = parseInt(appointmentIdParam); + + console.log( + "Update appointment request. ID:", + appointmentId, + "Body:", + req.body + ); + + // Check if appointment exists and belongs to user + const existingAppointment = await storage.getAppointment(appointmentId); + if (!existingAppointment) { + console.log("Appointment not found:", appointmentId); + return res.status(404).json({ message: "Appointment not found" }); + } + + if (existingAppointment.userId !== req.user!.id) { + console.log( + "Appointment belongs to another user. Appointment userId:", + existingAppointment.userId, + "Request userId:", + req.user!.id + ); + return res.status(403).json({ message: "Forbidden" }); + } + + // Validate request body + const appointmentData = updateAppointmentSchema.parse(req.body); + console.log("Validated appointment update data:", appointmentData); + + // If patient ID is being updated, verify the new patient belongs to user + if ( + appointmentData.patientId && + appointmentData.patientId !== existingAppointment.patientId + ) { + const patient = await storage.getPatient(appointmentData.patientId); + if (!patient) { + console.log("New patient not found:", appointmentData.patientId); + return res.status(404).json({ message: "Patient not found" }); + } + + if (patient.userId !== req.user!.id) { + console.log( + "New patient belongs to another user. Patient userId:", + patient.userId, + "Request userId:", + req.user!.id + ); + return res.status(403).json({ message: "Forbidden" }); + } + } + + // Check if there's already an appointment at this time slot (if time is being changed) + if ( + appointmentData.date && + appointmentData.startTime && + (appointmentData.date !== existingAppointment.date || + appointmentData.startTime !== existingAppointment.startTime) + ) { + // Extract staff name from notes + const staffInfo = + appointmentData.notes?.split("Appointment with ")[1] || + existingAppointment.notes?.split("Appointment with ")[1]; + + const existingAppointments = await storage.getAppointmentsByUserId( + req.user!.id + ); + const conflictingAppointment = existingAppointments.find( + (apt) => + apt.id !== appointmentId && // Don't match with itself + apt.date === (appointmentData.date || existingAppointment.date) && + apt.startTime === + (appointmentData.startTime || existingAppointment.startTime) && + apt.notes?.includes(staffInfo) + ); + + if (conflictingAppointment) { + console.log( + "Time slot already booked:", + appointmentData.date, + appointmentData.startTime + ); + return res.status(409).json({ + message: + "This time slot is already booked. Please select another time or staff member.", + }); + } + } + + // Update appointment + const updatedAppointment = await storage.updateAppointment( + appointmentId, + appointmentData + ); + console.log("Appointment updated successfully:", updatedAppointment); + res.json(updatedAppointment); + } catch (error) { + console.error("Error updating appointment:", error); + + if (error instanceof z.ZodError) { + console.log( + "Validation error details:", + JSON.stringify(error.format(), null, 2) + ); + return res.status(400).json({ + message: "Validation error", + errors: error.format(), + }); + } + + res.status(500).json({ + message: "Failed to update appointment", + error: error instanceof Error ? error.message : String(error), + }); + } + } +); + +// Delete an appointment +router.delete( + "/:id", + + async (req: Request, res: Response): Promise => { + try { + const appointmentIdParam = req.params.id; + if (!appointmentIdParam) { + return res.status(400).json({ message: "Appointment ID is required" }); + } + const appointmentId = parseInt(appointmentIdParam); + + // Check if appointment exists and belongs to user + const existingAppointment = await storage.getAppointment(appointmentId); + if (!existingAppointment) { + return res.status(404).json({ message: "Appointment not found" }); + } + + if (existingAppointment.userId !== req.user!.id) { + return res.status(403).json({ message: "Forbidden" }); + } + + // Delete appointment + await storage.deleteAppointment(appointmentId); + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: "Failed to delete appointment" }); + } + } +); + +export default router; \ No newline at end of file diff --git a/apps/Backend/src/routes/auth.ts b/apps/Backend/src/routes/auth.ts new file mode 100644 index 0000000..0d37b96 --- /dev/null +++ b/apps/Backend/src/routes/auth.ts @@ -0,0 +1,87 @@ +import express, { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; +import { storage } from '../storage'; +import { UserUncheckedCreateInputObjectSchema } from '@repo/db/shared/schemas'; +import { z } from 'zod'; + +type SelectUser = z.infer; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; +const JWT_EXPIRATION = '24h'; // JWT expiration time (1 day) + +// 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; +} + +// Function to compare passwords using bcrypt +async function comparePasswords(supplied: string, stored: string) { + const isMatch = await bcrypt.compare(supplied, stored); + return isMatch; +} + +// Function to generate JWT +function generateToken(user: SelectUser) { + return jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { + expiresIn: JWT_EXPIRATION, + }); +} + +const router = express.Router(); + + +// User registration route +router.post("/register", async (req: Request, res: Response, next: NextFunction): Promise => { + + try { + const existingUser = await storage.getUserByUsername(req.body.username); + if (existingUser) { + return res.status(400).send("Username already exists"); + } + + const hashedPassword = await hashPassword(req.body.password); + const user = await storage.createUser({ + ...req.body, + password: hashedPassword, + }); + + // Generate a JWT token for the user after successful registration + const token = generateToken(user); + + const { password, ...safeUser } = user; + return res.status(201).json({ user: safeUser, token }); + + } catch (error) { + next(error); + } +}); + +// User login route +router.post("/login", async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const user = await storage.getUserByUsername(req.body.username); + if (!user || !(await comparePasswords(req.body.password, user.password))) { + return res.status(401).send("Invalid username or password"); + } + + // Generate a JWT token for the user after successful login + const token = generateToken(user); + const { password, ...safeUser } = user; + return res.status(200).json({ user: safeUser, token }); + + } catch (error) { + next(error); + } +}); + +// Logout route (client-side action to remove the token) +router.post("/logout", (req: Request, res: Response) => { + // For JWT-based auth, logout is handled on the client (by removing token) + res.status(200).send("Logged out successfully"); +}); + + +export default router; \ No newline at end of file diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts new file mode 100644 index 0000000..f306927 --- /dev/null +++ b/apps/Backend/src/routes/index.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import patientRoutes from './patients'; +import appointmentRoutes from './appointements' +import userRoutes from './users' +import staffRoutes from './staffs' + +const router = Router(); + +router.use('/patients', patientRoutes); +router.use('/appointments', appointmentRoutes); +router.use('/users', userRoutes); +router.use('/staffs', staffRoutes); + +export default router; \ No newline at end of file diff --git a/apps/Backend/src/routes/patients.ts b/apps/Backend/src/routes/patients.ts new file mode 100644 index 0000000..5f4ff95 --- /dev/null +++ b/apps/Backend/src/routes/patients.ts @@ -0,0 +1,252 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import { storage } from "../storage"; +import { + AppointmentUncheckedCreateInputObjectSchema, + PatientUncheckedCreateInputObjectSchema, +} from "@repo/db/shared/schemas"; +import { z } from "zod"; + +const router = Router(); + +//creating types out of schema auto generated. +type Appointment = z.infer; + +const insertAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertAppointment = z.infer; + +const updateAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + }) + .partial(); +type UpdateAppointment = z.infer; + +const PatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + appointments: true, +}); +type Patient = z.infer; + +const insertPatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertPatient = z.infer; + +const updatePatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); + +type UpdatePatient = z.infer; + +// Patient Routes +// Get all patients for the logged-in user +router.get("/", async (req, res) => { + try { + const patients = await storage.getPatientsByUserId(req.user!.id); + res.json(patients); + } catch (error) { + res.status(500).json({ message: "Failed to retrieve patients" }); + } +}); + +// Get a single patient by ID +router.get( + "/:id", + + async (req: Request, res: Response): Promise => { + try { + const patientIdParam = req.params.id; + + // Ensure that patientIdParam exists and is a valid number + if (!patientIdParam) { + return res.status(400).json({ message: "Patient ID is required" }); + } + + const patientId = parseInt(patientIdParam); + + const patient = await storage.getPatient(patientId); + + if (!patient) { + return res.status(404).json({ message: "Patient not found" }); + } + + // Ensure the patient belongs to the logged-in user + if (patient.userId !== req.user!.id) { + return res.status(403).json({ message: "Forbidden" }); + } + + res.json(patient); + } catch (error) { + res.status(500).json({ message: "Failed to retrieve patient" }); + } + } +); + +// Create a new patient +router.post("/", async (req: Request, res: Response): Promise => { + try { + // Validate request body + const patientData = insertPatientSchema.parse({ + ...req.body, + userId: req.user!.id, + }); + + // Create patient + const patient = await storage.createPatient(patientData); + res.status(201).json(patient); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: "Validation error", + errors: error.format(), + }); + } + res.status(500).json({ message: "Failed to create patient" }); + } +}); + +// Update an existing patient +router.put( + "/:id", + + async (req: Request, res: Response): Promise => { + try { + + const patientIdParam = req.params.id; + + // Ensure that patientIdParam exists and is a valid number + if (!patientIdParam) { + return res.status(400).json({ message: "Patient ID is required" }); + } + + const patientId = parseInt(patientIdParam); + + // Check if patient exists and belongs to user + const existingPatient = await storage.getPatient(patientId); + if (!existingPatient) { + return res.status(404).json({ message: "Patient not found" }); + } + + if (existingPatient.userId !== req.user!.id) { + return res.status(403).json({ message: "Forbidden" }); + } + + // Validate request body + const patientData = updatePatientSchema.parse(req.body); + + // Update patient + const updatedPatient = await storage.updatePatient( + patientId, + patientData + ); + res.json(updatedPatient); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: "Validation error", + errors: error.format(), + }); + } + res.status(500).json({ message: "Failed to update patient" }); + } + } +); + +// Delete a patient +router.delete( + "/:id", + + async (req: Request, res: Response): Promise => { + try { + const patientIdParam = req.params.id; + + // Ensure that patientIdParam exists and is a valid number + if (!patientIdParam) { + return res.status(400).json({ message: "Patient ID is required" }); + } + + const patientId = parseInt(patientIdParam); + + // Check if patient exists and belongs to user + const existingPatient = await storage.getPatient(patientId); + if (!existingPatient) { + return res.status(404).json({ message: "Patient not found" }); + } + + if (existingPatient.userId !== req.user!.id) { + return res.status(403).json({ message: "Forbidden" }); + } + + // Delete patient + await storage.deletePatient(patientId); + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: "Failed to delete patient" }); + } + } +); + +// Appointment Routes +// Get all appointments for the logged-in user +router.get("/appointments", async (req, res) => { + try { + const appointments = await storage.getAppointmentsByUserId(req.user!.id); + res.json(appointments); + } catch (error) { + res.status(500).json({ message: "Failed to retrieve appointments" }); + } +}); + +// Get appointments for a specific patient +router.get( + "/:patientId/appointments", + + async (req: Request, res: Response): Promise => { + try { + const patientIdParam = req.params.id; + + // Ensure that patientIdParam exists and is a valid number + if (!patientIdParam) { + return res.status(400).json({ message: "Patient ID is required" }); + } + + const patientId = parseInt(patientIdParam); + + // Check if patient exists and belongs to user + const patient = await storage.getPatient(patientId); + if (!patient) { + return res.status(404).json({ message: "Patient not found" }); + } + + if (patient.userId !== req.user!.id) { + return res.status(403).json({ message: "Forbidden" }); + } + + const appointments = await storage.getAppointmentsByPatientId(patientId); + res.json(appointments); + } catch (error) { + res.status(500).json({ message: "Failed to retrieve appointments" }); + } + } +); + +export default router; diff --git a/apps/Backend/src/routes/staffs.ts b/apps/Backend/src/routes/staffs.ts new file mode 100644 index 0000000..94713bc --- /dev/null +++ b/apps/Backend/src/routes/staffs.ts @@ -0,0 +1,77 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import { storage } from "../storage"; +import { z } from "zod"; +import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; + +type Staff = z.infer; + +const staffCreateSchema = StaffUncheckedCreateInputObjectSchema; +const staffUpdateSchema = ( + StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).partial(); + +const router = Router(); + +router.post("/", async (req: Request, res: Response): Promise => { + try { + const validatedData = staffCreateSchema.parse(req.body); + const newStaff = await storage.createStaff(validatedData); + res.status(201).json(newStaff); + } catch (error) { + console.error("Failed to create staff:", error); + res.status(500).send("Failed to create staff"); + } +}); + +router.get("/", async (req: Request, res: Response): Promise => { + try { + const staff = await storage.getAllStaff(); + if (!staff) return res.status(404).send("Staff not found"); + + res.status(201).json(staff); + } catch (error) { + console.error("Failed to fetch staff:", error); + res.status(500).send("Failed to fetch staff"); + } +}); + +router.put("/:id", async (req: Request, res: Response): Promise => { + try { + const parsedStaffId = Number(req.params.id); + if (isNaN(parsedStaffId)) { + return res.status(400).send("Invalid staff ID"); + } + + const validatedData = staffUpdateSchema.parse(req.body); + const updatedStaff = await storage.updateStaff( + parsedStaffId, + validatedData + ); + if (!updatedStaff) return res.status(404).send("Staff not found"); + + res.json(updatedStaff); + } catch (error) { + console.error("Failed to update staff:", error); + res.status(500).send("Failed to update staff"); + } +}); + +router.delete("/:id", async (req: Request, res: Response): Promise => { + try { + const parsedStaffId = Number(req.params.id); + if (isNaN(parsedStaffId)) { + return res.status(400).send("Invalid staff ID"); + } + + const deleted = await storage.deleteStaff(parsedStaffId); + if (!deleted) return res.status(404).send("Staff not found"); + + res.status(200).send("Staff deleted successfully"); + } catch (error) { + console.error("Failed to delete staff:", error); + res.status(500).send("Failed to delete staff"); + } +}); + +export default router; diff --git a/apps/Backend/src/routes/users.ts b/apps/Backend/src/routes/users.ts new file mode 100644 index 0000000..e896730 --- /dev/null +++ b/apps/Backend/src/routes/users.ts @@ -0,0 +1,108 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import { storage } from "../storage"; +import { z } from "zod"; +import { UserUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; + +const router = Router(); + +// Type based on shared schema +type SelectUser = z.infer; + +// Zod validation +const userCreateSchema = UserUncheckedCreateInputObjectSchema; +const userUpdateSchema = (UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject).partial(); + + +router.get("/", async (req: Request, res: Response): Promise => { + try { + const userId = req.user?.id; + if (!userId) return res.status(401).send("Unauthorized UserId"); + + const user = await storage.getUser(userId); + if (!user) return res.status(404).send("User not found"); + + + const { password, ...safeUser } = user; + res.json(safeUser); + } catch (error) { + console.error(error); + res.status(500).send("Failed to fetch user"); + } +}); + +// GET: User by ID +router.get("/:id", async (req: Request, res: Response): Promise => { + try { + const idParam = req.params.id; + if (!idParam) return res.status(400).send("User ID is required"); + + const id = parseInt(idParam); + if (isNaN(id)) return res.status(400).send("Invalid user ID"); + + const user = await storage.getUser(id); + if (!user) return res.status(404).send("User not found"); + + const { password, ...safeUser } = user; + res.json(safeUser); + } catch (error) { + console.error(error); + res.status(500).send("Failed to fetch user"); + } +}); + +// POST: Create new user +router.post("/", async (req: Request, res: Response) => { + try { + const input = userCreateSchema.parse(req.body); + const newUser = await storage.createUser(input); + const { password, ...safeUser } = newUser; + res.status(201).json(safeUser); + } catch (err) { + console.error(err); + res.status(400).json({ error: "Invalid user data", details: err }); + } +}); + +// PUT: Update user +router.put("/:id", async (req: Request, res: Response):Promise => { + try { + const idParam = req.params.id; + if (!idParam) return res.status(400).send("User ID is required"); + + const id = parseInt(idParam); + if (isNaN(id)) return res.status(400).send("Invalid user ID"); + + + const updates = userUpdateSchema.parse(req.body); + const updatedUser = await storage.updateUser(id, updates); + if (!updatedUser) return res.status(404).send("User not found"); + + const { password, ...safeUser } = updatedUser; + res.json(safeUser); + } catch (err) { + console.error(err); + res.status(400).json({ error: "Invalid update data", details: err }); + } +}); + +// DELETE: Delete user +router.delete("/:id", async (req: Request, res: Response):Promise => { + try { + const idParam = req.params.id; + if (!idParam) return res.status(400).send("User ID is required"); + + const id = parseInt(idParam); + if (isNaN(id)) return res.status(400).send("Invalid user ID"); + + const success = await storage.deleteUser(id); + if (!success) return res.status(404).send("User not found"); + + res.status(204).send(); + } catch (error) { + console.error(error); + res.status(500).send("Failed to delete user"); + } +}); + +export default router; \ No newline at end of file diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts new file mode 100644 index 0000000..93620f8 --- /dev/null +++ b/apps/Backend/src/storage/index.ts @@ -0,0 +1,284 @@ +import { prisma as db } from "@repo/db/client"; +import { + AppointmentUncheckedCreateInputObjectSchema, + PatientUncheckedCreateInputObjectSchema, + UserUncheckedCreateInputObjectSchema, + StaffUncheckedCreateInputObjectSchema, +} from "@repo/db/shared/schemas"; +import { z } from "zod"; + +//creating types out of schema auto generated. +type Appointment = z.infer; + +const insertAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertAppointment = z.infer; + +const updateAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + }) + .partial(); +type UpdateAppointment = z.infer; + +//patient types +const PatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + appointments: true, +}); +type Patient = z.infer; +type Patient2 = z.infer; + +const insertPatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertPatient = z.infer; + +const updatePatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); + +type UpdatePatient = z.infer; + +//user types +type User = z.infer; + +const insertUserSchema = ( + UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).pick({ + username: true, + password: true, +}); + +const loginSchema = (insertUserSchema as unknown as z.ZodObject).extend({ + rememberMe: z.boolean().optional(), +}); + +const registerSchema = (insertUserSchema as unknown as z.ZodObject) + .extend({ + confirmPassword: z.string().min(6, { + message: "Password must be at least 6 characters long", + }), + agreeTerms: z.literal(true, { + errorMap: () => ({ + message: "You must agree to the terms and conditions", + }), + }), + }) + .refine((data: any) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], + }); + +type InsertUser = z.infer; +type LoginFormValues = z.infer; +type RegisterFormValues = z.infer; + +// staff types: +type Staff = z.infer; + +export interface IStorage { + // User methods + getUser(id: number): Promise; + getUserByUsername(username: string): Promise; + createUser(user: InsertUser): Promise; + updateUser(id: number, updates: Partial): Promise; + deleteUser(id: number): Promise; + + // Patient methods + getPatient(id: number): Promise; + getPatientsByUserId(userId: number): Promise; + createPatient(patient: InsertPatient): Promise; + updatePatient(id: number, patient: UpdatePatient): Promise; + deletePatient(id: number): Promise; + + // Appointment methods + getAppointment(id: number): Promise; + getAllAppointments(): Promise; + getAppointmentsByUserId(userId: number): Promise; + getAppointmentsByPatientId(patientId: number): Promise; + createAppointment(appointment: InsertAppointment): Promise; + updateAppointment( + id: number, + appointment: UpdateAppointment + ): Promise; + deleteAppointment(id: number): Promise; + + // Staff methods + getStaff(id: number): Promise; + getAllStaff(): Promise; + createStaff(staff: Staff): Promise; + updateStaff(id: number, updates: Partial): Promise; + deleteStaff(id: number): Promise; +} + +export const storage: IStorage = { + // User methods + async getUser(id: number): Promise { + const user = await db.user.findUnique({ where: { id } }); + return user ?? undefined; + }, + + async getUserByUsername(username: string): Promise { + const user = await db.user.findUnique({ where: { username } }); + return user ?? undefined; + }, + + async createUser(user: InsertUser): Promise { + return await db.user.create({ data: user as User }); + }, + + async updateUser( + id: number, + updates: Partial + ): Promise { + try { + return await db.user.update({ where: { id }, data: updates }); + } catch { + return undefined; + } + }, + + async deleteUser(id: number): Promise { + try { + await db.user.delete({ where: { id } }); + return true; + } catch { + return false; + } + }, + + // Patient methods + async getPatient(id: number): Promise { + const patient = await db.patient.findUnique({ where: { id } }); + return patient ?? undefined; + }, + + async getPatientsByUserId(userId: number): Promise { + return await db.patient.findMany({ where: { userId } }); + }, + + async createPatient(patient: InsertPatient): Promise { + return await db.patient.create({ data: patient as Patient }); + }, + + async updatePatient(id: number, updateData: UpdatePatient): Promise { + try { + return await db.patient.update({ + where: { id }, + data: updateData, + }); + } catch (err) { + throw new Error(`Patient with ID ${id} not found`); + } + }, + + async deletePatient(id: number): Promise { + try { + await db.patient.delete({ where: { id } }); + } catch (err) { + throw new Error(`Patient with ID ${id} not found`); + } + }, + + // Appointment methods + async getAppointment(id: number): Promise { + const appointment = await db.appointment.findUnique({ where: { id } }); + return appointment ?? undefined; + }, + + async getAllAppointments(): Promise { + return await db.appointment.findMany(); + }, + + async getAppointmentsByUserId(userId: number): Promise { + return await db.appointment.findMany({ where: { userId } }); + }, + + async getAppointmentsByPatientId(patientId: number): Promise { + return await db.appointment.findMany({ where: { patientId } }); + }, + + async createAppointment( + appointment: InsertAppointment + ): Promise { + return await db.appointment.create({ data: appointment as Appointment }); + }, + + async updateAppointment( + id: number, + updateData: UpdateAppointment + ): Promise { + try { + return await db.appointment.update({ + where: { id }, + data: updateData, + }); + } catch (err) { + throw new Error(`Appointment with ID ${id} not found`); + } + }, + + async deleteAppointment(id: number): Promise { + try { + await db.appointment.delete({ where: { id } }); + } catch (err) { + throw new Error(`Appointment with ID ${id} not found`); + } + }, + + async getStaff(id: number): Promise { + const staff = await db.staff.findUnique({ where: { id } }); + return staff ?? undefined; + }, + + async getAllStaff(): Promise { + const staff = await db.staff.findMany(); + return staff; + }, + + async createStaff(staff: Staff): Promise { + const createdStaff = await db.staff.create({ + data: staff, + }); + return createdStaff; + }, + + async updateStaff( + id: number, + updates: Partial + ): Promise { + const updatedStaff = await db.staff.update({ + where: { id }, + data: updates, + }); + return updatedStaff ?? undefined; + }, + + async deleteStaff(id: number): Promise { + try { + await db.staff.delete({ where: { id } }); + return true; + } catch (error) { + console.error("Error deleting staff:", error); + return false; + } + }, +}; diff --git a/apps/Backend/src/types/express.types.d.ts b/apps/Backend/src/types/express.types.d.ts new file mode 100644 index 0000000..da7082b --- /dev/null +++ b/apps/Backend/src/types/express.types.d.ts @@ -0,0 +1,10 @@ +import { User } from "@repo/db/client"; + +declare global { + namespace Express { + interface User { + id: number; + // include any other properties + } + } +} diff --git a/apps/Backend/tsconfig.json b/apps/Backend/tsconfig.json new file mode 100644 index 0000000..ff2839e --- /dev/null +++ b/apps/Backend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir":"dist" + }, + + "include": ["src"], + "exclude": ["node_modules", "dist"] +} + diff --git a/apps/Frontend/.env.example b/apps/Frontend/.env.example new file mode 100644 index 0000000..0705822 --- /dev/null +++ b/apps/Frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:3000 + \ No newline at end of file diff --git a/apps/Frontend/src/components/appointments/appointment-form.tsx b/apps/Frontend/src/components/appointments/appointment-form.tsx index c0250d0..ac9a86c 100644 --- a/apps/Frontend/src/components/appointments/appointment-form.tsx +++ b/apps/Frontend/src/components/appointments/appointment-form.tsx @@ -2,38 +2,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { format } from "date-fns"; -// import { InsertAppointment, UpdateAppointment, Appointment, Patient } from "@repo/db/shared/schemas"; -import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; - -import {z} from "zod"; -type Appointment = z.infer; - -const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject).omit({ - id: true, - createdAt: true, -}); -type InsertAppointment = z.infer; - -const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject).omit({ - id: true, - createdAt: true, -}).partial(); -type UpdateAppointment = z.infer; - -const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject).omit({ - appointments: true, -}); -type Patient = z.infer; - - -// Define staff members (should match those in appointments-page.tsx) -const staffMembers = [ - { id: "doctor1", name: "Dr. Kai Gao", role: "doctor" }, - { id: "doctor2", name: "Dr. Jane Smith", role: "doctor" }, - { id: "hygienist1", name: "Hygienist One", role: "hygienist" }, - { id: "hygienist2", name: "Hygienist Two", role: "hygienist" }, - { id: "hygienist3", name: "Hygienist Three", role: "hygienist" }, -]; +import { apiRequest } from "@/lib/queryClient"; import { Button } from "@/components/ui/button"; import { Form, @@ -53,29 +22,52 @@ import { SelectValue, } from "@/components/ui/select"; import { Calendar } from "@/components/ui/calendar"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { CalendarIcon, Clock } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; +import { useAuth } from "@/hooks/use-auth"; -const appointmentSchema = z.object({ - patientId: z.coerce.number().positive(), - title: z.string().optional(), - date: z.date({ - required_error: "Appointment date is required", - }), - startTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { - message: "Start time must be in format HH:MM", - }), - endTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { - message: "End time must be in format HH:MM", - }), - type: z.string().min(1, "Appointment type is required"), - notes: z.string().optional(), - status: z.string().default("scheduled"), - staff: z.string().default(staffMembers?.[0]?.id ?? "default-id"), +import { + AppointmentUncheckedCreateInputObjectSchema, + PatientUncheckedCreateInputObjectSchema, + StaffUncheckedCreateInputObjectSchema, +} from "@repo/db/shared/schemas"; + +import { z } from "zod"; +type Appointment = z.infer; +type Staff = z.infer; + +const insertAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, + userId: true, }); +type InsertAppointment = z.infer; -export type AppointmentFormValues = z.infer; +const updateAppointmentSchema = ( + AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); +type UpdateAppointment = z.infer; + +const PatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + appointments: true, +}); +type Patient = z.infer; interface AppointmentFormProps { appointment?: Appointment; @@ -84,55 +76,77 @@ interface AppointmentFormProps { isLoading?: boolean; } -export function AppointmentForm({ - appointment, - patients, - onSubmit, - isLoading = false +export function AppointmentForm({ + appointment, + patients, + onSubmit, + isLoading = false, }: AppointmentFormProps) { + const { user } = useAuth(); + + const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } = + useQuery({ + queryKey: ["/api/staffs/"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/staffs/"); + return res.json(); + }, + enabled: !!user, + }); + + const colorMap: Record = { + "Dr. Kai Gao": "bg-blue-600", + "Dr. Jane Smith": "bg-emerald-600", + }; + + const staffMembers = staffMembersRaw.map((staff) => ({ + ...staff, + color: colorMap[staff.name] || "bg-gray-400", + })); + // Get the stored data from session storage - const storedDataString = sessionStorage.getItem('newAppointmentData'); + const storedDataString = sessionStorage.getItem("newAppointmentData"); let parsedStoredData = null; - + // Try to parse it if it exists if (storedDataString) { try { parsedStoredData = JSON.parse(storedDataString); - console.log('Initial appointment data from storage:', parsedStoredData); - - // LOG the specific time values for debugging - console.log('Time values in stored data:', { - startTime: parsedStoredData.startTime, - endTime: parsedStoredData.endTime - }); } catch (error) { - console.error('Error parsing stored appointment data:', error); + console.error("Error parsing stored appointment data:", error); } } - + // Format the date and times for the form - const defaultValues: Partial = appointment + const defaultValues: Partial = appointment ? { patientId: appointment.patientId, title: appointment.title, date: new Date(appointment.date), - startTime: typeof appointment.startTime === 'string' ? appointment.startTime.slice(0, 5) : "", - endTime: typeof appointment.endTime === 'string' ? appointment.endTime.slice(0, 5) : "", + startTime: appointment.startTime || "09:00", // Default "09:00" + endTime: appointment.endTime || "09:30", // Default "09:30" type: appointment.type, notes: appointment.notes || "", status: appointment.status || "scheduled", + staffId: + typeof appointment.staffId === "number" + ? appointment.staffId + : undefined, } : parsedStoredData ? { - patientId: parsedStoredData.patientId, + patientId: Number(parsedStoredData.patientId), date: new Date(parsedStoredData.date), title: parsedStoredData.title || "", - startTime: parsedStoredData.startTime, // This should now be correctly applied + startTime: parsedStoredData.startTime, endTime: parsedStoredData.endTime, type: parsedStoredData.type || "checkup", status: parsedStoredData.status || "scheduled", notes: parsedStoredData.notes || "", - staff: parsedStoredData.staff || (staffMembers?.[0]?.id ?? "default-id") + staffId: + typeof parsedStoredData.staff === "number" + ? parsedStoredData.staff + : (staffMembers?.[0]?.id ?? undefined), } : { date: new Date(), @@ -141,342 +155,384 @@ export function AppointmentForm({ endTime: "09:30", type: "checkup", status: "scheduled", - staff: "doctor1", + staffId: staffMembers?.[0]?.id ?? undefined, }; - const form = useForm({ - resolver: zodResolver(appointmentSchema), + const form = useForm({ + resolver: zodResolver(insertAppointmentSchema), defaultValues, }); - + // Force form field values to update and clean up storage useEffect(() => { if (parsedStoredData) { - // Force-update the form with the stored values - console.log("Force updating form fields with:", parsedStoredData); - // Update form field values directly if (parsedStoredData.startTime) { - form.setValue('startTime', parsedStoredData.startTime); - console.log(`Setting startTime to: ${parsedStoredData.startTime}`); + form.setValue("startTime", parsedStoredData.startTime); } - + if (parsedStoredData.endTime) { - form.setValue('endTime', parsedStoredData.endTime); - console.log(`Setting endTime to: ${parsedStoredData.endTime}`); + form.setValue("endTime", parsedStoredData.endTime); } - + if (parsedStoredData.staff) { - form.setValue('staff', parsedStoredData.staff); + form.setValue("staffId", parsedStoredData.staff); } - + if (parsedStoredData.date) { - form.setValue('date', new Date(parsedStoredData.date)); + form.setValue("date", new Date(parsedStoredData.date)); } - + // Clean up session storage - sessionStorage.removeItem('newAppointmentData'); + sessionStorage.removeItem("newAppointmentData"); } }, [form]); - const handleSubmit = (data: AppointmentFormValues) => { - // Convert date to string format for the API and ensure patientId is properly parsed as a number - console.log("Form data before submission:", data); - + const handleSubmit = (data: InsertAppointment) => { // Make sure patientId is a number - const patientId = typeof data.patientId === 'string' - ? parseInt(data.patientId, 10) - : data.patientId; - + const patientId = + typeof data.patientId === "string" + ? parseInt(data.patientId, 10) + : data.patientId; + // Get patient name for the title - const patient = patients.find(p => p.id === patientId); - const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Patient'; - + const patient = patients.find((p) => p.id === patientId); + const patientName = patient + ? `${patient.firstName} ${patient.lastName}` + : "Patient"; + // Auto-create title if it's empty let title = data.title; - if (!title || title.trim() === '') { + if (!title || title.trim() === "") { // Format: "April 19" - just the date - title = format(data.date, 'MMMM d'); + title = format(data.date, "MMMM d"); } - - // Make sure notes include staff information (needed for appointment display in columns) - let notes = data.notes || ''; - - // Get the selected staff member - const selectedStaff = staffMembers.find(staff => staff.id === data.staff) || staffMembers[0]; - + + let notes = data.notes || ""; + + const selectedStaff = + staffMembers.find((staff) => staff.id?.toString() === data.staffId) || + staffMembers[0]; + + if (!selectedStaff) { + console.error("No staff selected and no available staff in the list"); + return; // Handle this case as well + } + // If there's no staff information in the notes, add it - if (!notes.includes('Appointment with')) { - notes = notes ? `${notes}\nAppointment with ${selectedStaff?.name}` : `Appointment with ${selectedStaff?.name}`; + if (!notes.includes("Appointment with")) { + notes = notes + ? `${notes}\nAppointment with ${selectedStaff?.name}` + : `Appointment with ${selectedStaff?.name}`; } - + + // 👇 Use current date if none provided + const appointmentDate = data.date ? new Date(data.date) : new Date(); + + if (isNaN(appointmentDate.getTime())) { + console.error("Invalid date:", data.date); + return; + } + onSubmit({ ...data, title, notes, - patientId, // Ensure patientId is a number - date: format(data.date, 'yyyy-MM-dd'), + patientId, + date: format(appointmentDate, "yyyy-MM-dd"), + startTime: data.startTime, + endTime: data.endTime, }); }; return ( -
- - ( - - Patient - - - +
+ + { + handleSubmit(data); + }, + (errors) => { + console.error("Validation failed:", errors); + } )} - /> - - ( - - Appointment Title (optional) - - + ( + + Patient + + + + )} + /> + + ( + + + Appointment Title{" "} + + (optional) + + + + - - - - - )} - /> - -
- ( - - Start Time - -
- - -
)} /> - + ( - - End Time - -
- - + Date + + + + + + + + + date < new Date(new Date().setHours(0, 0, 0, 0)) + } + initialFocus /> -
-
+ +
)} /> -
- - ( - - Appointment Type - - - - )} - /> - - ( - - Status - - - - )} - /> - - ( - - Doctor/Hygienist - - - - )} - /> - - ( - - Notes - -