major functionalities are fixed

This commit is contained in:
2025-05-14 17:12:54 +05:30
parent 53a91dd5f9
commit b03b7efcb4
34 changed files with 4434 additions and 1082 deletions

View File

@@ -0,0 +1,5 @@
HOST="localhost"
PORT=3000
FRONTEND_URL="http://localhost:5173"
JWT_SECRET = 'dentalsecret'
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp

40
apps/Backend/package.json Normal file
View File

@@ -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"
}
}

36
apps/Backend/src/app.ts Normal file
View File

@@ -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;

11
apps/Backend/src/index.ts Normal file
View File

@@ -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}`);
});

View File

@@ -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
});
}

View File

@@ -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' });
};

View File

@@ -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<string, any> | 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();
}

View File

@@ -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<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
// Get all appointments
router.get("/all", async (req: Request, res: Response): Promise<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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;

View File

@@ -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<typeof UserUncheckedCreateInputObjectSchema>;
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<any> => {
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<any> => {
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;

View File

@@ -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;

View File

@@ -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<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
// 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<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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;

View File

@@ -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<typeof StaffUncheckedCreateInputObjectSchema>;
const staffCreateSchema = StaffUncheckedCreateInputObjectSchema;
const staffUpdateSchema = (
StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).partial();
const router = Router();
router.post("/", async (req: Request, res: Response): Promise<any> => {
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<any> => {
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<any> => {
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<any> => {
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;

View File

@@ -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<typeof UserUncheckedCreateInputObjectSchema>;
// Zod validation
const userCreateSchema = UserUncheckedCreateInputObjectSchema;
const userUpdateSchema = (UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).partial();
router.get("/", async (req: Request, res: Response): Promise<any> => {
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<any> => {
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<any> => {
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<any> => {
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;

View File

@@ -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<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
//patient types
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientUncheckedCreateInputObjectSchema>;
type Patient2 = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
//user types
type User = z.infer<typeof UserUncheckedCreateInputObjectSchema>;
const insertUserSchema = (
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).pick({
username: true,
password: true,
});
const loginSchema = (insertUserSchema as unknown as z.ZodObject<any>).extend({
rememberMe: z.boolean().optional(),
});
const registerSchema = (insertUserSchema as unknown as z.ZodObject<any>)
.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<typeof insertUserSchema>;
type LoginFormValues = z.infer<typeof loginSchema>;
type RegisterFormValues = z.infer<typeof registerSchema>;
// staff types:
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export interface IStorage {
// User methods
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
updateUser(id: number, updates: Partial<User>): Promise<User | undefined>;
deleteUser(id: number): Promise<boolean>;
// Patient methods
getPatient(id: number): Promise<Patient | undefined>;
getPatientsByUserId(userId: number): Promise<Patient[]>;
createPatient(patient: InsertPatient): Promise<Patient>;
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
deletePatient(id: number): Promise<void>;
// Appointment methods
getAppointment(id: number): Promise<Appointment | undefined>;
getAllAppointments(): Promise<Appointment[]>;
getAppointmentsByUserId(userId: number): Promise<Appointment[]>;
getAppointmentsByPatientId(patientId: number): Promise<Appointment[]>;
createAppointment(appointment: InsertAppointment): Promise<Appointment>;
updateAppointment(
id: number,
appointment: UpdateAppointment
): Promise<Appointment>;
deleteAppointment(id: number): Promise<void>;
// Staff methods
getStaff(id: number): Promise<Staff | undefined>;
getAllStaff(): Promise<Staff[]>;
createStaff(staff: Staff): Promise<Staff>;
updateStaff(id: number, updates: Partial<Staff>): Promise<Staff | undefined>;
deleteStaff(id: number): Promise<boolean>;
}
export const storage: IStorage = {
// User methods
async getUser(id: number): Promise<User | undefined> {
const user = await db.user.findUnique({ where: { id } });
return user ?? undefined;
},
async getUserByUsername(username: string): Promise<User | undefined> {
const user = await db.user.findUnique({ where: { username } });
return user ?? undefined;
},
async createUser(user: InsertUser): Promise<User> {
return await db.user.create({ data: user as User });
},
async updateUser(
id: number,
updates: Partial<User>
): Promise<User | undefined> {
try {
return await db.user.update({ where: { id }, data: updates });
} catch {
return undefined;
}
},
async deleteUser(id: number): Promise<boolean> {
try {
await db.user.delete({ where: { id } });
return true;
} catch {
return false;
}
},
// Patient methods
async getPatient(id: number): Promise<Patient | undefined> {
const patient = await db.patient.findUnique({ where: { id } });
return patient ?? undefined;
},
async getPatientsByUserId(userId: number): Promise<Patient[]> {
return await db.patient.findMany({ where: { userId } });
},
async createPatient(patient: InsertPatient): Promise<Patient> {
return await db.patient.create({ data: patient as Patient });
},
async updatePatient(id: number, updateData: UpdatePatient): Promise<Patient> {
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<void> {
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<Appointment | undefined> {
const appointment = await db.appointment.findUnique({ where: { id } });
return appointment ?? undefined;
},
async getAllAppointments(): Promise<Appointment[]> {
return await db.appointment.findMany();
},
async getAppointmentsByUserId(userId: number): Promise<Appointment[]> {
return await db.appointment.findMany({ where: { userId } });
},
async getAppointmentsByPatientId(patientId: number): Promise<Appointment[]> {
return await db.appointment.findMany({ where: { patientId } });
},
async createAppointment(
appointment: InsertAppointment
): Promise<Appointment> {
return await db.appointment.create({ data: appointment as Appointment });
},
async updateAppointment(
id: number,
updateData: UpdateAppointment
): Promise<Appointment> {
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<void> {
try {
await db.appointment.delete({ where: { id } });
} catch (err) {
throw new Error(`Appointment with ID ${id} not found`);
}
},
async getStaff(id: number): Promise<Staff | undefined> {
const staff = await db.staff.findUnique({ where: { id } });
return staff ?? undefined;
},
async getAllStaff(): Promise<Staff[]> {
const staff = await db.staff.findMany();
return staff;
},
async createStaff(staff: Staff): Promise<Staff> {
const createdStaff = await db.staff.create({
data: staff,
});
return createdStaff;
},
async updateStaff(
id: number,
updates: Partial<Staff>
): Promise<Staff | undefined> {
const updatedStaff = await db.staff.update({
where: { id },
data: updates,
});
return updatedStaff ?? undefined;
},
async deleteStaff(id: number): Promise<boolean> {
try {
await db.staff.delete({ where: { id } });
return true;
} catch (error) {
console.error("Error deleting staff:", error);
return false;
}
},
};

View File

@@ -0,0 +1,10 @@
import { User } from "@repo/db/client";
declare global {
namespace Express {
interface User {
id: number;
// include any other properties
}
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"outDir":"dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,2 @@
VITE_API_BASE_URL=http://localhost:3000

View File

@@ -2,38 +2,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns"; import { format } from "date-fns";
// import { InsertAppointment, UpdateAppointment, Appointment, Patient } from "@repo/db/shared/schemas"; import { apiRequest } from "@/lib/queryClient";
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import {z} from "zod";
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
id: true,
createdAt: true,
}).partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
// 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 { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
@@ -53,29 +22,52 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Calendar } from "@/components/ui/calendar"; 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 { CalendarIcon, Clock } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
const appointmentSchema = z.object({ import {
patientId: z.coerce.number().positive(), AppointmentUncheckedCreateInputObjectSchema,
title: z.string().optional(), PatientUncheckedCreateInputObjectSchema,
date: z.date({ StaffUncheckedCreateInputObjectSchema,
required_error: "Appointment date is required", } from "@repo/db/shared/schemas";
}),
startTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { import { z } from "zod";
message: "Start time must be in format HH:MM", type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
}), type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
endTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: "End time must be in format HH:MM", const insertAppointmentSchema = (
}), AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
type: z.string().min(1, "Appointment type is required"), ).omit({
notes: z.string().optional(), id: true,
status: z.string().default("scheduled"), createdAt: true,
staff: z.string().default(staffMembers?.[0]?.id ?? "default-id"), userId: true,
}); });
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
export type AppointmentFormValues = z.infer<typeof appointmentSchema>; const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface AppointmentFormProps { interface AppointmentFormProps {
appointment?: Appointment; appointment?: Appointment;
@@ -88,51 +80,73 @@ export function AppointmentForm({
appointment, appointment,
patients, patients,
onSubmit, onSubmit,
isLoading = false isLoading = false,
}: AppointmentFormProps) { }: AppointmentFormProps) {
const { user } = useAuth();
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
useQuery<Staff[]>({
queryKey: ["/api/staffs/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/staffs/");
return res.json();
},
enabled: !!user,
});
const colorMap: Record<string, string> = {
"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 // Get the stored data from session storage
const storedDataString = sessionStorage.getItem('newAppointmentData'); const storedDataString = sessionStorage.getItem("newAppointmentData");
let parsedStoredData = null; let parsedStoredData = null;
// Try to parse it if it exists // Try to parse it if it exists
if (storedDataString) { if (storedDataString) {
try { try {
parsedStoredData = JSON.parse(storedDataString); 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) { } 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 // Format the date and times for the form
const defaultValues: Partial<AppointmentFormValues> = appointment const defaultValues: Partial<Appointment> = appointment
? { ? {
patientId: appointment.patientId, patientId: appointment.patientId,
title: appointment.title, title: appointment.title,
date: new Date(appointment.date), date: new Date(appointment.date),
startTime: typeof appointment.startTime === 'string' ? appointment.startTime.slice(0, 5) : "", startTime: appointment.startTime || "09:00", // Default "09:00"
endTime: typeof appointment.endTime === 'string' ? appointment.endTime.slice(0, 5) : "", endTime: appointment.endTime || "09:30", // Default "09:30"
type: appointment.type, type: appointment.type,
notes: appointment.notes || "", notes: appointment.notes || "",
status: appointment.status || "scheduled", status: appointment.status || "scheduled",
staffId:
typeof appointment.staffId === "number"
? appointment.staffId
: undefined,
} }
: parsedStoredData : parsedStoredData
? { ? {
patientId: parsedStoredData.patientId, patientId: Number(parsedStoredData.patientId),
date: new Date(parsedStoredData.date), date: new Date(parsedStoredData.date),
title: parsedStoredData.title || "", title: parsedStoredData.title || "",
startTime: parsedStoredData.startTime, // This should now be correctly applied startTime: parsedStoredData.startTime,
endTime: parsedStoredData.endTime, endTime: parsedStoredData.endTime,
type: parsedStoredData.type || "checkup", type: parsedStoredData.type || "checkup",
status: parsedStoredData.status || "scheduled", status: parsedStoredData.status || "scheduled",
notes: parsedStoredData.notes || "", 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(), date: new Date(),
@@ -141,195 +155,160 @@ export function AppointmentForm({
endTime: "09:30", endTime: "09:30",
type: "checkup", type: "checkup",
status: "scheduled", status: "scheduled",
staff: "doctor1", staffId: staffMembers?.[0]?.id ?? undefined,
}; };
const form = useForm<AppointmentFormValues>({ const form = useForm<InsertAppointment>({
resolver: zodResolver(appointmentSchema), resolver: zodResolver(insertAppointmentSchema),
defaultValues, defaultValues,
}); });
// Force form field values to update and clean up storage // Force form field values to update and clean up storage
useEffect(() => { useEffect(() => {
if (parsedStoredData) { if (parsedStoredData) {
// Force-update the form with the stored values
console.log("Force updating form fields with:", parsedStoredData);
// Update form field values directly // Update form field values directly
if (parsedStoredData.startTime) { if (parsedStoredData.startTime) {
form.setValue('startTime', parsedStoredData.startTime); form.setValue("startTime", parsedStoredData.startTime);
console.log(`Setting startTime to: ${parsedStoredData.startTime}`);
} }
if (parsedStoredData.endTime) { if (parsedStoredData.endTime) {
form.setValue('endTime', parsedStoredData.endTime); form.setValue("endTime", parsedStoredData.endTime);
console.log(`Setting endTime to: ${parsedStoredData.endTime}`);
} }
if (parsedStoredData.staff) { if (parsedStoredData.staff) {
form.setValue('staff', parsedStoredData.staff); form.setValue("staffId", parsedStoredData.staff);
} }
if (parsedStoredData.date) { if (parsedStoredData.date) {
form.setValue('date', new Date(parsedStoredData.date)); form.setValue("date", new Date(parsedStoredData.date));
} }
// Clean up session storage // Clean up session storage
sessionStorage.removeItem('newAppointmentData'); sessionStorage.removeItem("newAppointmentData");
} }
}, [form]); }, [form]);
const handleSubmit = (data: AppointmentFormValues) => { const handleSubmit = (data: InsertAppointment) => {
// Convert date to string format for the API and ensure patientId is properly parsed as a number
console.log("Form data before submission:", data);
// Make sure patientId is a number // Make sure patientId is a number
const patientId = typeof data.patientId === 'string' const patientId =
? parseInt(data.patientId, 10) typeof data.patientId === "string"
: data.patientId; ? parseInt(data.patientId, 10)
: data.patientId;
// Get patient name for the title // Get patient name for the title
const patient = patients.find(p => p.id === patientId); const patient = patients.find((p) => p.id === patientId);
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Patient'; const patientName = patient
? `${patient.firstName} ${patient.lastName}`
: "Patient";
// Auto-create title if it's empty // Auto-create title if it's empty
let title = data.title; let title = data.title;
if (!title || title.trim() === '') { if (!title || title.trim() === "") {
// Format: "April 19" - just the date // 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 || "";
let notes = data.notes || '';
// Get the selected staff member const selectedStaff =
const selectedStaff = staffMembers.find(staff => staff.id === data.staff) || staffMembers[0]; 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 there's no staff information in the notes, add it
if (!notes.includes('Appointment with')) { if (!notes.includes("Appointment with")) {
notes = notes ? `${notes}\nAppointment with ${selectedStaff?.name}` : `Appointment with ${selectedStaff?.name}`; 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({ onSubmit({
...data, ...data,
title, title,
notes, notes,
patientId, // Ensure patientId is a number patientId,
date: format(data.date, 'yyyy-MM-dd'), date: format(appointmentDate, "yyyy-MM-dd"),
startTime: data.startTime,
endTime: data.endTime,
}); });
}; };
return ( return (
<Form {...form}> <div className="form-container">
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> <Form {...form}>
<FormField <form
control={form.control} onSubmit={form.handleSubmit(
name="patientId" (data) => {
render={({ field }) => ( handleSubmit(data);
<FormItem> },
<FormLabel>Patient</FormLabel> (errors) => {
<Select console.error("Validation failed:", errors);
disabled={isLoading} }
onValueChange={field.onChange}
value={field.value?.toString()}
defaultValue={field.value?.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a patient" />
</SelectTrigger>
</FormControl>
<SelectContent>
{patients.map((patient) => (
<SelectItem key={patient.id} value={patient.id.toString()}>
{patient.firstName} {patient.lastName}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} )}
/> className="space-y-6"
>
<FormField <FormField
control={form.control} control={form.control}
name="title" name="patientId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Appointment Title <span className="text-muted-foreground text-xs">(optional)</span></FormLabel> <FormLabel>Patient</FormLabel>
<FormControl> <Select
<Input
placeholder="Leave blank to auto-fill with date"
{...field}
disabled={isLoading} disabled={isLoading}
/> onValueChange={(val) => field.onChange(Number(val))}
</FormControl> value={field.value?.toString()}
<FormMessage /> defaultValue={field.value?.toString()}
</FormItem> >
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl> <FormControl>
<Button <SelectTrigger>
variant={"outline"} <SelectValue placeholder="Select a patient" />
className={cn( </SelectTrigger>
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
disabled={isLoading}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl> </FormControl>
</PopoverTrigger> <SelectContent>
<PopoverContent className="w-auto p-0" align="start"> {patients.map((patient) => (
<Calendar <SelectItem
mode="single" key={patient.id}
selected={field.value} value={patient.id.toString()}
onSelect={field.onChange} >
disabled={(date) => {patient.firstName} {patient.lastName}
date < new Date(new Date().setHours(0, 0, 0, 0)) </SelectItem>
} ))}
initialFocus </SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
Appointment Title{" "}
<span className="text-muted-foreground text-xs">
(optional)
</span>
</FormLabel>
<FormControl>
<Input
placeholder="Leave blank to auto-fill with date"
{...field}
disabled={isLoading}
/> />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel>Start Time</FormLabel>
<FormControl>
<div className="relative">
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="09:00"
{...field}
disabled={isLoading}
className="pl-10"
/>
</div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -338,145 +317,222 @@ export function AppointmentForm({
<FormField <FormField
control={form.control} control={form.control}
name="endTime" name="date"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="flex flex-col">
<FormLabel>End Time</FormLabel> <FormLabel>Date</FormLabel>
<FormControl> <Popover>
<div className="relative"> <PopoverTrigger asChild>
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> <FormControl>
<Input <Button
placeholder="09:30" variant={"outline"}
{...field} className={cn(
disabled={isLoading} "w-full pl-3 text-left font-normal",
className="pl-10" !field.value && "text-muted-foreground"
)}
disabled={isLoading}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value ? new Date(field.value) : undefined}
onSelect={field.onChange}
disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0))
}
initialFocus
/> />
</div> </PopoverContent>
</FormControl> </Popover>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div>
<FormField <div className="grid grid-cols-2 gap-4">
control={form.control} <FormField
name="type" control={form.control}
render={({ field }) => ( name="startTime"
<FormItem> render={({ field }) => (
<FormLabel>Appointment Type</FormLabel> <FormItem>
<Select <FormLabel>Start Time</FormLabel>
disabled={isLoading} <FormControl>
onValueChange={field.onChange} <div className="relative">
value={field.value} <Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
defaultValue={field.value} <Input
> placeholder="09:00"
<FormControl> {...field}
<SelectTrigger> disabled={isLoading}
<SelectValue placeholder="Select a type" /> className="pl-10"
</SelectTrigger> value={
</FormControl> typeof field.value === "string" ? field.value : ""
<SelectContent> }
<SelectItem value="checkup">Checkup</SelectItem> />
<SelectItem value="cleaning">Cleaning</SelectItem> </div>
<SelectItem value="filling">Filling</SelectItem> </FormControl>
<SelectItem value="extraction">Extraction</SelectItem> <FormMessage />
<SelectItem value="root-canal">Root Canal</SelectItem> </FormItem>
<SelectItem value="crown">Crown</SelectItem> )}
<SelectItem value="dentures">Dentures</SelectItem> />
<SelectItem value="consultation">Consultation</SelectItem>
<SelectItem value="emergency">Emergency</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="status" name="endTime"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Status</FormLabel> <FormLabel>End Time</FormLabel>
<Select <FormControl>
disabled={isLoading} <div className="relative">
onValueChange={field.onChange} <Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
value={field.value} <Input
defaultValue={field.value} placeholder="09:30"
> {...field}
<FormControl> disabled={isLoading}
<SelectTrigger> className="pl-10"
<SelectValue placeholder="Select a status" /> value={
</SelectTrigger> typeof field.value === "string" ? field.value : ""
</FormControl> }
<SelectContent> />
<SelectItem value="scheduled">Scheduled</SelectItem> </div>
<SelectItem value="confirmed">Confirmed</SelectItem> </FormControl>
<SelectItem value="completed">Completed</SelectItem> <FormMessage />
<SelectItem value="cancelled">Cancelled</SelectItem> </FormItem>
<SelectItem value="no-show">No Show</SelectItem> )}
</SelectContent> />
</Select> </div>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="staff" name="type"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Doctor/Hygienist</FormLabel> <FormLabel>Appointment Type</FormLabel>
<Select <Select
disabled={isLoading}
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select staff member" />
</SelectTrigger>
</FormControl>
<SelectContent>
{staffMembers.map((staff) => (
<SelectItem key={staff.id} value={staff.id}>
{staff.name} ({staff.role})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea
placeholder="Enter any notes about the appointment"
{...field}
disabled={isLoading} disabled={isLoading}
className="min-h-24" onValueChange={field.onChange}
/> value={field.value}
</FormControl> defaultValue={field.value}
<FormMessage /> >
</FormItem> <FormControl>
)} <SelectTrigger>
/> <SelectValue placeholder="Select a type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="checkup">Checkup</SelectItem>
<SelectItem value="cleaning">Cleaning</SelectItem>
<SelectItem value="filling">Filling</SelectItem>
<SelectItem value="extraction">Extraction</SelectItem>
<SelectItem value="root-canal">Root Canal</SelectItem>
<SelectItem value="crown">Crown</SelectItem>
<SelectItem value="dentures">Dentures</SelectItem>
<SelectItem value="consultation">Consultation</SelectItem>
<SelectItem value="emergency">Emergency</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading} className="w-full"> <FormField
{appointment ? "Update Appointment" : "Create Appointment"} control={form.control}
</Button> name="status"
</form> render={({ field }) => (
</Form> <FormItem>
<FormLabel>Status</FormLabel>
<Select
disabled={isLoading}
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="scheduled">Scheduled</SelectItem>
<SelectItem value="confirmed">Confirmed</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="no-show">No Show</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="staffId"
render={({ field }) => (
<FormItem>
<FormLabel>Doctor/Hygienist</FormLabel>
<Select
disabled={isLoading}
onValueChange={(val) => field.onChange(Number(val))}
value={field.value ? String(field.value) : undefined}
defaultValue={field.value ? String(field.value) : undefined}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select staff member" />
</SelectTrigger>
</FormControl>
<SelectContent>
{staffMembers.map((staff) => (
<SelectItem
key={staff.id}
value={staff.id?.toString() || ""}
>
{staff.name} ({staff.role})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea
placeholder="Enter any notes about the appointment"
{...field}
disabled={isLoading}
className="min-h-24"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading} className="w-full">
{appointment ? "Update Appointment" : "Create Appointment"}
</Button>
</form>
</Form>
</div>
); );
} }

View File

@@ -134,14 +134,8 @@ export function AppointmentTable({
<TableCell> <TableCell>
<div className="flex items-center"> <div className="flex items-center">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" /> <Clock className="mr-2 h-4 w-4 text-muted-foreground" />
{appointment.startTime instanceof Date {appointment.startTime.slice(0, 5)} -{" "}
? appointment.startTime.toISOString().slice(11, 16) {appointment.endTime.slice(0, 5)}
: appointment.startTime.slice(0, 5)}{" "}
-
{appointment.endTime instanceof Date
? appointment.endTime.toISOString().slice(11, 16)
: appointment.endTime.slice(0, 5)}
{/* {appointment.startTime.slice(0, 5)} - {appointment.endTime.slice(0, 5)} */}
</div> </div>
</TableCell> </TableCell>
<TableCell className="capitalize"> <TableCell className="capitalize">

View File

@@ -1,4 +1,10 @@
import { useState, forwardRef, useImperativeHandle } from "react"; import {
useState,
forwardRef,
useImperativeHandle,
useEffect,
useRef,
} from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -8,38 +14,44 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { PatientForm } from "./patient-form"; import { PatientForm, PatientFormRef } from "./patient-form";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { X, Calendar } from "lucide-react"; import { X, Calendar } from "lucide-react";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
// import { InsertPatient, Patient, UpdatePatient } from "@repo/db/shared/schemas";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import {z} from "zod"; import { z } from "zod";
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true, id: true,
createdAt: true, createdAt: true,
}); });
type InsertPatient = z.infer<typeof insertPatientSchema>; type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const updatePatientSchema = (
id: true, PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, )
userId: true, .omit({
}).partial(); id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface AddPatientModalProps { interface AddPatientModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSubmit: (data: InsertPatient | UpdatePatient) => void; onSubmit: (data: InsertPatient | (UpdatePatient & { id?: number })) => void;
isLoading: boolean; isLoading: boolean;
patient?: Patient; patient?: Patient;
extractedInfo?: { extractedInfo?: {
@@ -56,35 +68,49 @@ export type AddPatientModalRef = {
navigateToSchedule: (patientId: number) => void; navigateToSchedule: (patientId: number) => void;
}; };
export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalProps>(function AddPatientModal(props, ref) { export const AddPatientModal = forwardRef<
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } = props; AddPatientModalRef,
AddPatientModalProps
>(function AddPatientModal(props, ref) {
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } =
props;
const { toast } = useToast(); const { toast } = useToast();
const [formData, setFormData] = useState<InsertPatient | UpdatePatient | null>(null); const [formData, setFormData] = useState<
InsertPatient | UpdatePatient | null
>(null);
const isEditing = !!patient; const isEditing = !!patient;
const [, navigate] = useLocation(); const [, navigate] = useLocation();
const [saveAndSchedule, setSaveAndSchedule] = useState(false); const [saveAndSchedule, setSaveAndSchedule] = useState(false);
const patientFormRef = useRef<PatientFormRef>(null); // Ref for PatientForm
// Set up the imperativeHandle to expose functionality to the parent component // Set up the imperativeHandle to expose functionality to the parent component
useEffect(() => {
if (isEditing && patient) {
const { id, userId, createdAt, ...sanitized } = patient;
setFormData(sanitized); // Update the form data with the patient data for editing
} else {
setFormData(null); // Reset form data when not editing
}
}, [isEditing, patient]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
shouldSchedule: saveAndSchedule, shouldSchedule: saveAndSchedule,
navigateToSchedule: (patientId: number) => { navigateToSchedule: (patientId: number) => {
navigate(`/appointments?newPatient=${patientId}`); navigate(`/appointments?newPatient=${patientId}`);
} },
})); }));
const handleFormSubmit = (data: InsertPatient | UpdatePatient) => { const handleFormSubmit = (data: InsertPatient | UpdatePatient) => {
setFormData(data); if (patient && patient.id) {
onSubmit(data); onSubmit({ ...data, id: patient.id });
} else {
onSubmit(data);
}
}; };
const handleSaveAndSchedule = () => { const handleSaveAndSchedule = () => {
setSaveAndSchedule(true); setSaveAndSchedule(true);
if (formData) { document.querySelector("form")?.requestSubmit();
onSubmit(formData);
} else {
// Trigger form validation by clicking the hidden submit button
document.querySelector('form')?.requestSubmit();
}
}; };
return ( return (
@@ -117,10 +143,7 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
/> />
<DialogFooter className="mt-6"> <DialogFooter className="mt-6">
<Button <Button variant="outline" onClick={() => onOpenChange(false)}>
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel Cancel
</Button> </Button>
@@ -128,7 +151,9 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
<Button <Button
variant="outline" variant="outline"
className="gap-1" className="gap-1"
onClick={handleSaveAndSchedule} onClick={() => {
handleSaveAndSchedule();
}}
disabled={isLoading} disabled={isLoading}
> >
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
@@ -138,20 +163,21 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
<Button <Button
type="submit" type="submit"
form="patient-form"
onClick={() => { onClick={() => {
if (formData) { if (patientFormRef.current) {
onSubmit(formData); patientFormRef.current.submit();
} else {
// Trigger form validation by clicking the hidden submit button
document.querySelector('form')?.requestSubmit();
} }
}} }}
disabled={isLoading} disabled={isLoading}
> >
{isLoading {isLoading
? isEditing ? "Updating..." : "Saving..." ? patient
: isEditing ? "Update Patient" : "Save Patient" ? "Updating..."
} : "Saving..."
: patient
? "Update Patient"
: "Save Patient"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -2,7 +2,6 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
// import { insertPatientSchema, InsertPatient, Patient, updatePatientSchema, UpdatePatient } from "@repo/db/shared/schemas";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { import {
Form, Form,
@@ -13,7 +12,6 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -21,27 +19,36 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { useEffect, useMemo } from "react";
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertPatientSchema = (
id: true, PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, ).omit({
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
id: true, id: true,
createdAt: true, createdAt: true,
userId: true, userId: true,
}).partial(); });
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface PatientFormProps { interface PatientFormProps {
patient?: Patient; patient?: Patient;
extractedInfo?: { extractedInfo?: {
@@ -53,50 +60,103 @@ interface PatientFormProps {
onSubmit: (data: InsertPatient | UpdatePatient) => void; onSubmit: (data: InsertPatient | UpdatePatient) => void;
} }
export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormProps) { export type PatientFormRef = {
submit: () => void;
};
export function PatientForm({
patient,
extractedInfo,
onSubmit,
}: PatientFormProps) {
const { user } = useAuth(); const { user } = useAuth();
const isEditing = !!patient; const isEditing = !!patient;
const schema = isEditing ? updatePatientSchema : insertPatientSchema.extend({ const schema = useMemo(
userId: z.number().optional(), () =>
}); isEditing
? updatePatientSchema
: insertPatientSchema.extend({ userId: z.number().optional() }),
[isEditing]
);
// Merge extracted info into default values if available const computedDefaultValues = useMemo(() => {
const defaultValues = { if (isEditing && patient) {
firstName: extractedInfo?.firstName || "", const { id, userId, createdAt, ...sanitizedPatient } = patient;
lastName: extractedInfo?.lastName || "", return {
dateOfBirth: extractedInfo?.dateOfBirth || "", ...sanitizedPatient,
gender: "", dateOfBirth: patient.dateOfBirth
phone: "", ? new Date(patient.dateOfBirth).toISOString().split("T")[0]
email: "", : "",
address: "", };
city: "", }
zipCode: "",
insuranceProvider: "", return {
insuranceId: extractedInfo?.insuranceId || "", firstName: extractedInfo?.firstName || "",
groupNumber: "", lastName: extractedInfo?.lastName || "",
policyHolder: "", dateOfBirth: extractedInfo?.dateOfBirth || "",
allergies: "", gender: "",
medicalConditions: "", phone: "",
status: "active", email: "",
userId: user?.id, address: "",
}; city: "",
zipCode: "",
insuranceProvider: "",
insuranceId: extractedInfo?.insuranceId || "",
groupNumber: "",
policyHolder: "",
allergies: "",
medicalConditions: "",
status: "active",
userId: user?.id,
};
}, [isEditing, patient, extractedInfo, user?.id]);
const form = useForm<InsertPatient | UpdatePatient>({ const form = useForm<InsertPatient | UpdatePatient>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: patient || defaultValues, defaultValues: computedDefaultValues,
}); });
const handleSubmit = (data: InsertPatient | UpdatePatient) => { // Debug form errors
useEffect(() => {
const errors = form.formState.errors;
if (Object.keys(errors).length > 0) {
console.log("❌ Form validation errors:", errors);
}
}, [form.formState.errors]);
useEffect(() => {
if (patient) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
const resetValues: Partial<Patient> = {
...sanitizedPatient,
dateOfBirth: patient.dateOfBirth
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
: "",
};
form.reset(resetValues);
}
}, [patient, computedDefaultValues, form]);
const handleSubmit2 = (data: InsertPatient | UpdatePatient) => {
onSubmit(data); onSubmit(data);
}; };
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> <form
id="patient-form"
key={patient?.id || "new"}
onSubmit={form.handleSubmit((data) => {
handleSubmit2(data);
})}
className="space-y-6"
>
{/* Personal Information */} {/* Personal Information */}
<div> <div>
<h4 className="text-md font-medium text-gray-700 mb-3">Personal Information</h4> <h4 className="text-md font-medium text-gray-700 mb-3">
Personal Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -170,7 +230,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
{/* Contact Information */} {/* Contact Information */}
<div> <div>
<h4 className="text-md font-medium text-gray-700 mb-3">Contact Information</h4> <h4 className="text-md font-medium text-gray-700 mb-3">
Contact Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -193,7 +255,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input type="email" {...field} value={field.value || ''} /> <Input type="email" {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -207,7 +269,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem className="md:col-span-2"> <FormItem className="md:col-span-2">
<FormLabel>Address</FormLabel> <FormLabel>Address</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value || ''} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -221,7 +283,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem> <FormItem>
<FormLabel>City</FormLabel> <FormLabel>City</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value || ''} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -235,7 +297,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem> <FormItem>
<FormLabel>ZIP Code</FormLabel> <FormLabel>ZIP Code</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value || ''} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -246,7 +308,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
{/* Insurance Information */} {/* Insurance Information */}
<div> <div>
<h4 className="text-md font-medium text-gray-700 mb-3">Insurance Information</h4> <h4 className="text-md font-medium text-gray-700 mb-3">
Insurance Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -256,7 +320,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormLabel>Insurance Provider</FormLabel> <FormLabel>Insurance Provider</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value as string || ''} defaultValue={(field.value as string) || ""}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@@ -264,7 +328,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="placeholder">Select provider</SelectItem> <SelectItem value="placeholder">
Select provider
</SelectItem>
<SelectItem value="delta">Delta Dental</SelectItem> <SelectItem value="delta">Delta Dental</SelectItem>
<SelectItem value="metlife">MetLife</SelectItem> <SelectItem value="metlife">MetLife</SelectItem>
<SelectItem value="cigna">Cigna</SelectItem> <SelectItem value="cigna">Cigna</SelectItem>
@@ -285,7 +351,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem> <FormItem>
<FormLabel>Insurance ID</FormLabel> <FormLabel>Insurance ID</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value || ''} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -299,7 +365,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem> <FormItem>
<FormLabel>Group Number</FormLabel> <FormLabel>Group Number</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value || ''} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -313,7 +379,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
<FormItem> <FormItem>
<FormLabel>Policy Holder (if not self)</FormLabel> <FormLabel>Policy Holder (if not self)</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value || ''} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -44,10 +44,10 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> {/* <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close> */}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ))

View File

@@ -6,24 +6,24 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
// import { insertUserSchema, User as SelectUser, InsertUser } from "@repo/db/shared/schemas"; // import { insertUserSchema, User as SelectUser, InsertUser } from "@repo/db/shared/schemas";
import { UserUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import { UserUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import {z} from "zod"; import { z } from "zod";
import { getQueryFn, apiRequest, queryClient } from "../lib/queryClient"; import { getQueryFn, apiRequest, queryClient } from "../lib/queryClient";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
type SelectUser = z.infer<typeof UserUncheckedCreateInputObjectSchema>; type SelectUser = z.infer<typeof UserUncheckedCreateInputObjectSchema>;
const insertUserSchema = (UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<{ const insertUserSchema = (
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<{
username: z.ZodString; username: z.ZodString;
password: z.ZodString; password: z.ZodString;
}>).pick({ }>
).pick({
username: true, username: true,
password: true, password: true,
}); });
type InsertUser = z.infer<typeof insertUserSchema>; type InsertUser = z.infer<typeof insertUserSchema>;
type AuthContextType = { type AuthContextType = {
user: SelectUser | null; user: SelectUser | null;
isLoading: boolean; isLoading: boolean;
@@ -40,6 +40,7 @@ type LoginData = {
}; };
export const AuthContext = createContext<AuthContextType | null>(null); export const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const { toast } = useToast(); const { toast } = useToast();
const { const {
@@ -47,17 +48,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
error, error,
isLoading, isLoading,
} = useQuery<SelectUser | undefined, Error>({ } = useQuery<SelectUser | undefined, Error>({
queryKey: ["/api/user"], queryKey: ["/api/users/"],
queryFn: getQueryFn({ on401: "returnNull" }), queryFn: getQueryFn({ on401: "returnNull" }),
}); });
const loginMutation = useMutation({ const loginMutation = useMutation({
mutationFn: async (credentials: LoginData) => { mutationFn: async (credentials: LoginData) => {
const res = await apiRequest("POST", "/api/login", credentials); const res = await apiRequest("POST", "/api/auth/login", credentials);
return await res.json();
const data = await res.json();
localStorage.setItem("token", data.token);
return data;
}, },
onSuccess: (user: SelectUser) => { onSuccess: (user: SelectUser) => {
queryClient.setQueryData(["/api/user"], user); queryClient.setQueryData(["/api/users/"], user);
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({
@@ -70,11 +75,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const registerMutation = useMutation({ const registerMutation = useMutation({
mutationFn: async (credentials: InsertUser) => { mutationFn: async (credentials: InsertUser) => {
const res = await apiRequest("POST", "/api/register", credentials); const res = await apiRequest("POST", "/api/auth/register", credentials);
return await res.json(); const data = await res.json();
localStorage.setItem("token", data.token);
return data;
}, },
onSuccess: (user: SelectUser) => { onSuccess: (user: SelectUser) => {
queryClient.setQueryData(["/api/user"], user); queryClient.setQueryData(["/api/users/"], user);
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({
@@ -87,10 +94,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const logoutMutation = useMutation({ const logoutMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await apiRequest("POST", "/api/logout"); // Remove token from localStorage when logging out
localStorage.removeItem("token");
await apiRequest("POST", "/api/auth/logout");
}, },
onSuccess: () => { onSuccess: () => {
queryClient.setQueryData(["/api/user"], null); queryClient.setQueryData(["/api/users/"], null);
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({

View File

@@ -1,3 +1,10 @@
.form-container {
max-height: 80vh; /* Set a max height (80% of the viewport height or adjust as needed) */
overflow-y: auto; /* Enable vertical scrolling when the content overflows */
padding: 20px; /* Optional, for some spacing */
}
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%; --foreground: 222.2 47.4% 11.2%;

View File

@@ -1,5 +1,7 @@
import { QueryClient, QueryFunction } from "@tanstack/react-query"; import { QueryClient, QueryFunction } from "@tanstack/react-query";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "";
async function throwIfResNotOk(res: Response) { async function throwIfResNotOk(res: Response) {
if (!res.ok) { if (!res.ok) {
const text = (await res.text()) || res.statusText; const text = (await res.text()) || res.statusText;
@@ -10,11 +12,17 @@ async function throwIfResNotOk(res: Response) {
export async function apiRequest( export async function apiRequest(
method: string, method: string,
url: string, url: string,
data?: unknown | undefined, data?: unknown | undefined
): Promise<Response> { ): Promise<Response> {
const res = await fetch(url, { const token = localStorage.getItem("token");
const res = await fetch(`${API_BASE_URL}${url}`, {
method, method,
headers: data ? { "Content-Type": "application/json" } : {}, // headers: data ? { "Content-Type": "application/json" } : {},
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}), // Include JWT token if available
},
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined,
credentials: "include", credentials: "include",
}); });
@@ -24,12 +32,20 @@ export async function apiRequest(
} }
type UnauthorizedBehavior = "returnNull" | "throw"; type UnauthorizedBehavior = "returnNull" | "throw";
export const getQueryFn: <T>(options: { export const getQueryFn: <T>(options: {
on401: UnauthorizedBehavior; on401: UnauthorizedBehavior;
}) => QueryFunction<T> = }) => QueryFunction<T> =
({ on401: unauthorizedBehavior }) => ({ on401: unauthorizedBehavior }) =>
async ({ queryKey }) => { async ({ queryKey }) => {
const res = await fetch(queryKey[0] as string, { const url = `${API_BASE_URL}${queryKey[0] as string}`;
const token = localStorage.getItem("token");
const res = await fetch(url, {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
credentials: "include", credentials: "include",
}); });

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from "react"; import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { format, addDays, parse, startOfToday, startOfDay, addMinutes, isEqual } from "date-fns"; import { format, addDays, startOfToday, addMinutes } from "date-fns";
import { TopAppBar } from "@/components/layout/top-app-bar"; import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal"; import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
@@ -9,50 +9,64 @@ import { Button } from "@/components/ui/button";
import { import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
Plus, Plus,
Users,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
RefreshCw, RefreshCw,
Move, Move,
Trash2, Trash2,
FileText FileText,
} from "lucide-react"; } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { z } from "zod"; import { z } from "zod";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import {
// import { Appointment, InsertAppointment, UpdateAppointment, Patient } from "@repo/db/shared/schemas"; AppointmentUncheckedCreateInputObjectSchema,
PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/shared/schemas";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { Card, CardContent, CardHeader, CardDescription, CardTitle } from "@/components/ui/card"; import {
import { DndProvider, useDrag, useDrop } from 'react-dnd'; Card,
import { HTML5Backend } from 'react-dnd-html5-backend'; CardContent,
import { Menu, Item, useContextMenu } from 'react-contexify'; CardHeader,
import 'react-contexify/ReactContexify.css'; CardDescription,
CardTitle,
} from "@/components/ui/card";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Menu, Item, useContextMenu } from "react-contexify";
import "react-contexify/ReactContexify.css";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
//creating types out of schema auto generated. //creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true, id: true,
createdAt: true, createdAt: true,
}); });
type InsertAppointment = z.infer<typeof insertAppointmentSchema>; type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const updateAppointmentSchema = (
id: true, AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, )
}).partial(); .omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>; type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
// Define types for scheduling // Define types for scheduling
interface TimeSlot { interface TimeSlot {
time: string; time: string;
@@ -62,7 +76,7 @@ interface TimeSlot {
interface Staff { interface Staff {
id: string; id: string;
name: string; name: string;
role: 'doctor' | 'hygienist'; role: "doctor" | "hygienist";
color: string; color: string;
} }
@@ -71,9 +85,9 @@ interface ScheduledAppointment {
patientId: number; patientId: number;
patientName: string; patientName: string;
staffId: string; staffId: string;
date: string | Date; // Allow both string and Date date: string | Date;
startTime: string | Date; // Allow both string and Date startTime: string | Date;
endTime: string | Date; // Allow both string and Date endTime: string | Date;
status: string | null; status: string | null;
type: string; type: string;
} }
@@ -86,9 +100,13 @@ export default function AppointmentsPage() {
const { user } = useAuth(); const { user } = useAuth();
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isClaimModalOpen, setIsClaimModalOpen] = useState(false); const [isClaimModalOpen, setIsClaimModalOpen] = useState(false);
const [claimAppointmentId, setClaimAppointmentId] = useState<number | null>(null); const [claimAppointmentId, setClaimAppointmentId] = useState<number | null>(
null
);
const [claimPatientId, setClaimPatientId] = useState<number | null>(null); const [claimPatientId, setClaimPatientId] = useState<number | null>(null);
const [editingAppointment, setEditingAppointment] = useState<Appointment | undefined>(undefined); const [editingAppointment, setEditingAppointment] = useState<
Appointment | undefined
>(undefined);
const [selectedDate, setSelectedDate] = useState<Date>(startOfToday()); const [selectedDate, setSelectedDate] = useState<Date>(startOfToday());
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [location] = useLocation(); const [location] = useLocation();
@@ -98,23 +116,35 @@ export default function AppointmentsPage() {
id: APPOINTMENT_CONTEXT_MENU_ID, id: APPOINTMENT_CONTEXT_MENU_ID,
}); });
// Staff members (doctors and hygienists) //Fetching staff memebers
const staffMembers: Staff[] = [ const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
{ id: "doctor1", name: "Dr. Kai Gao", role: "doctor", color: "bg-blue-600" }, useQuery<Staff[]>({
{ id: "doctor2", name: "Dr. Jane Smith", role: "doctor", color: "bg-emerald-600" }, queryKey: ["/api/staffs/"],
{ id: "hygienist1", name: "Hygienist One", role: "hygienist", color: "bg-purple-600" }, queryFn: async () => {
{ id: "hygienist2", name: "Hygienist Two", role: "hygienist", color: "bg-rose-500" }, const res = await apiRequest("GET", "/api/staffs/");
{ id: "hygienist3", name: "Hygienist Three", role: "hygienist", color: "bg-amber-500" }, return res.json();
]; },
enabled: !!user,
});
const colorMap: Record<string, string> = {
"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",
}));
// Generate time slots from 8:00 AM to 6:00 PM in 30-minute increments // Generate time slots from 8:00 AM to 6:00 PM in 30-minute increments
const timeSlots: TimeSlot[] = []; const timeSlots: TimeSlot[] = [];
for (let hour = 8; hour <= 18; hour++) { for (let hour = 8; hour <= 18; hour++) {
for (let minute = 0; minute < 60; minute += 30) { for (let minute = 0; minute < 60; minute += 30) {
const hour12 = hour > 12 ? hour - 12 : hour; const hour12 = hour > 12 ? hour - 12 : hour;
const period = hour >= 12 ? 'PM' : 'AM'; const period = hour >= 12 ? "PM" : "AM";
const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; const timeStr = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
const displayTime = `${hour12}:${minute.toString().padStart(2, '0')} ${period}`; const displayTime = `${hour12}:${minute.toString().padStart(2, "0")} ${period}`;
timeSlots.push({ time: timeStr, displayTime }); timeSlots.push({ time: timeStr, displayTime });
} }
} }
@@ -125,49 +155,55 @@ export default function AppointmentsPage() {
isLoading: isLoadingAppointments, isLoading: isLoadingAppointments,
refetch: refetchAppointments, refetch: refetchAppointments,
} = useQuery<Appointment[]>({ } = useQuery<Appointment[]>({
queryKey: ["/api/appointments"], queryKey: ["/api/appointments/all"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/appointments/all");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Fetch patients (needed for the dropdowns) // Fetch patients (needed for the dropdowns)
const { const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
data: patients = [], Patient[]
isLoading: isLoadingPatients, >({
} = useQuery<Patient[]>({ queryKey: ["/api/patients/"],
queryKey: ["/api/patients"], queryFn: async () => {
const res = await apiRequest("GET", "/api/patients/");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Handle creating a new appointment at a specific time slot and for a specific staff member // Handle creating a new appointment at a specific time slot and for a specific staff member
const handleCreateAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => { const handleCreateAppointmentAtSlot = (
timeSlot: TimeSlot,
staffId: string
) => {
// Calculate end time (30 minutes after start time) // Calculate end time (30 minutes after start time)
const startHour = parseInt(timeSlot.time.split(':')[0] as string); const startHour = parseInt(timeSlot.time.split(":")[0] as string);
const startMinute = parseInt(timeSlot.time.split(':')[1] as string); const startMinute = parseInt(timeSlot.time.split(":")[1] as string);
const startDate = new Date(selectedDate); const startDate = new Date(selectedDate);
startDate.setHours(startHour, startMinute, 0); startDate.setHours(startHour, startMinute, 0);
const endDate = addMinutes(startDate, 30); const endDate = addMinutes(startDate, 30);
const endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`; const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
// Find staff member // Find staff member
const staff = staffMembers.find(s => s.id === staffId); const staff = staffMembers.find((s) => s.id === staffId);
console.log(`Creating appointment at time slot: ${timeSlot.time} (${timeSlot.displayTime})`);
// Pre-fill appointment form with default values // Pre-fill appointment form with default values
const newAppointment = { const newAppointment = {
date: format(selectedDate, 'yyyy-MM-dd'), date: format(selectedDate, "yyyy-MM-dd"),
startTime: timeSlot.time, // This is in "HH:MM" format startTime: timeSlot.time, // This is in "HH:MM" format
endTime: endTime, endTime: endTime,
type: staff?.role === 'doctor' ? 'checkup' : 'cleaning', type: staff?.role === "doctor" ? "checkup" : "cleaning",
status: 'scheduled', status: "scheduled",
title: `Appointment with ${staff?.name}`, // Add staff name in title for easier display title: `Appointment with ${staff?.name}`, // Add staff name in title for easier display
notes: `Appointment with ${staff?.name}`, // Store staff info in notes for processing notes: `Appointment with ${staff?.name}`, // Store staff info in notes for processing
staff: staffId // This matches the 'staff' field in the appointment form schema staff: staffId, // This matches the 'staff' field in the appointment form schema
}; };
console.log('Created appointment data to pass to modal:', newAppointment);
// For new appointments, set editingAppointment to undefined // For new appointments, set editingAppointment to undefined
// This will ensure we go to the "create" branch in handleAppointmentSubmit // This will ensure we go to the "create" branch in handleAppointmentSubmit
setEditingAppointment(undefined); setEditingAppointment(undefined);
@@ -177,8 +213,11 @@ export default function AppointmentsPage() {
// Store the prefilled values in state or sessionStorage to access in the modal // Store the prefilled values in state or sessionStorage to access in the modal
// Clear any existing data first to ensure we're not using old data // Clear any existing data first to ensure we're not using old data
sessionStorage.removeItem('newAppointmentData'); sessionStorage.removeItem("newAppointmentData");
sessionStorage.setItem('newAppointmentData', JSON.stringify(newAppointment)); sessionStorage.setItem(
"newAppointmentData",
JSON.stringify(newAppointment)
);
}; };
// Check for newPatient parameter in URL // Check for newPatient parameter in URL
@@ -187,12 +226,12 @@ export default function AppointmentsPage() {
// Parse URL search params to check for newPatient // Parse URL search params to check for newPatient
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const newPatientId = params.get('newPatient'); const newPatientId = params.get("newPatient");
if (newPatientId) { if (newPatientId) {
const patientId = parseInt(newPatientId); const patientId = parseInt(newPatientId);
// Find the patient in our list // Find the patient in our list
const patient = (patients as Patient[]).find(p => p.id === patientId); const patient = (patients as Patient[]).find((p) => p.id === patientId);
if (patient) { if (patient) {
toast({ toast({
@@ -204,34 +243,38 @@ export default function AppointmentsPage() {
const staffId = staffMembers[0]!.id; const staffId = staffMembers[0]!.id;
// Find first time slot today (9:00 AM is a common starting time) // Find first time slot today (9:00 AM is a common starting time)
const defaultTimeSlot = timeSlots.find(slot => slot.time === "09:00") || timeSlots[0]; const defaultTimeSlot =
timeSlots.find((slot) => slot.time === "09:00") || timeSlots[0];
// Open appointment modal with prefilled patient // Open appointment modal with prefilled patient
handleCreateAppointmentAtSlot(defaultTimeSlot!, staffId); handleCreateAppointmentAtSlot(defaultTimeSlot!, staffId);
// Pre-select the patient in the appointment form // Pre-select the patient in the appointment form
const patientData = { const patientData = {
patientId: patient.id patientId: patient.id,
}; };
// Store info in session storage for the modal to pick up // Store info in session storage for the modal to pick up
const existingData = sessionStorage.getItem('newAppointmentData'); const existingData = sessionStorage.getItem("newAppointmentData");
if (existingData) { if (existingData) {
const parsedData = JSON.parse(existingData); const parsedData = JSON.parse(existingData);
sessionStorage.setItem('newAppointmentData', JSON.stringify({ sessionStorage.setItem(
...parsedData, "newAppointmentData",
...patientData JSON.stringify({
})); ...parsedData,
...patientData,
})
);
} }
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [patients, user, location]); }, [patients, user, location]);
// Create appointment mutation // Create appointment mutation
const createAppointmentMutation = useMutation({ const createAppointmentMutation = useMutation({
mutationFn: async (appointment: InsertAppointment) => { mutationFn: async (appointment: InsertAppointment) => {
const res = await apiRequest("POST", "/api/appointments", appointment); const res = await apiRequest("POST", "/api/appointments/", appointment);
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: () => {
@@ -239,7 +282,9 @@ export default function AppointmentsPage() {
title: "Success", title: "Success",
description: "Appointment created successfully.", description: "Appointment created successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] }); // Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
setIsAddModalOpen(false); setIsAddModalOpen(false);
}, },
onError: (error: Error) => { onError: (error: Error) => {
@@ -253,8 +298,18 @@ export default function AppointmentsPage() {
// Update appointment mutation // Update appointment mutation
const updateAppointmentMutation = useMutation({ const updateAppointmentMutation = useMutation({
mutationFn: async ({ id, appointment }: { id: number; appointment: UpdateAppointment }) => { mutationFn: async ({
const res = await apiRequest("PUT", `/api/appointments/${id}`, appointment); id,
appointment,
}: {
id: number;
appointment: UpdateAppointment;
}) => {
const res = await apiRequest(
"PUT",
`/api/appointments/${id}`,
appointment
);
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: () => {
@@ -262,7 +317,8 @@ export default function AppointmentsPage() {
title: "Success", title: "Success",
description: "Appointment updated successfully.", description: "Appointment updated successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] }); queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
setEditingAppointment(undefined); setEditingAppointment(undefined);
setIsAddModalOpen(false); setIsAddModalOpen(false);
}, },
@@ -285,7 +341,9 @@ export default function AppointmentsPage() {
title: "Success", title: "Success",
description: "Appointment deleted successfully.", description: "Appointment deleted successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] }); // Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({
@@ -297,15 +355,21 @@ export default function AppointmentsPage() {
}); });
// Handle appointment submission (create or update) // Handle appointment submission (create or update)
const handleAppointmentSubmit = (appointmentData: InsertAppointment | UpdateAppointment) => { const handleAppointmentSubmit = (
appointmentData: InsertAppointment | UpdateAppointment
) => {
// Make sure the date is for the selected date // Make sure the date is for the selected date
const updatedData = { const updatedData = {
...appointmentData, ...appointmentData,
date: format(selectedDate, 'yyyy-MM-dd') date: format(selectedDate, "yyyy-MM-dd"),
}; };
// Check if we're editing an existing appointment with a valid ID // Check if we're editing an existing appointment with a valid ID
if (editingAppointment && 'id' in editingAppointment && typeof editingAppointment.id === 'number') { if (
editingAppointment &&
"id" in editingAppointment &&
typeof editingAppointment.id === "number"
) {
updateAppointmentMutation.mutate({ updateAppointmentMutation.mutate({
id: editingAppointment.id, id: editingAppointment.id,
appointment: updatedData as unknown as UpdateAppointment, appointment: updatedData as unknown as UpdateAppointment,
@@ -314,8 +378,8 @@ export default function AppointmentsPage() {
// This is a new appointment // This is a new appointment
if (user) { if (user) {
createAppointmentMutation.mutate({ createAppointmentMutation.mutate({
...updatedData as unknown as InsertAppointment, ...(updatedData as unknown as InsertAppointment),
userId: user.id userId: user.id,
}); });
} }
} }
@@ -339,73 +403,58 @@ export default function AppointmentsPage() {
}; };
// Get formatted date string for display // Get formatted date string for display
const formattedDate = format(selectedDate, 'MMMM d, yyyy'); const formattedDate = format(selectedDate, "yyyy-MM-dd");
// Filter appointments for the selected date const selectedDateAppointments = appointments.filter((apt) => {
const selectedDateAppointments = appointments.filter(apt => // Ensure apt.date is in 'yyyy-MM-dd' format before comparison
apt.date === format(selectedDate, 'yyyy-MM-dd') const appointmentDate = format(new Date(apt.date), "yyyy-MM-dd");
); return appointmentDate === formattedDate;
});
// Add debugging logs
console.log("Selected date:", format(selectedDate, 'yyyy-MM-dd'));
console.log("All appointments:", appointments);
console.log("Filtered appointments for selected date:", selectedDateAppointments);
// Process appointments for the scheduler view // Process appointments for the scheduler view
const processedAppointments: ScheduledAppointment[] = selectedDateAppointments.map(apt => { const processedAppointments: ScheduledAppointment[] =
// Find patient name selectedDateAppointments.map((apt) => {
const patient = patients.find(p => p.id === apt.patientId); // Find patient name
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown Patient'; const patient = patients.find((p) => p.id === apt.patientId);
const patientName = patient
? `${patient.firstName} ${patient.lastName}`
: "Unknown Patient";
// Try to determine the staff from the notes or title // Try to determine the staff from the notes or title
let staffId = 'doctor1'; // Default to first doctor if we can't determine let staffId = "doctor1"; // Default to first doctor if we can't determine
console.log("Processing appointment:", { // Check notes first
id: apt.id, if (apt.notes) {
notes: apt.notes, // Look for "Appointment with Dr. X" or similar patterns
title: apt.title for (const staff of staffMembers) {
if (apt.notes.includes(staff.name)) {
staffId = staff.id;
break;
}
}
}
// If no match in notes, check title
if (staffId === "doctor1" && apt.title) {
for (const staff of staffMembers) {
if (apt.title.includes(staff.name)) {
staffId = staff.id;
break;
}
}
}
const processed = {
...apt,
patientName,
staffId,
status: apt.status ?? null, // Default to null if status is undefined
date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d
};
return processed;
}); });
// Check notes first
if (apt.notes) {
// Look for "Appointment with Dr. X" or similar patterns
console.log("Checking notes:", apt.notes);
for (const staff of staffMembers) {
console.log(`Checking if notes contains "${staff.name}":`, apt.notes.includes(staff.name));
if (apt.notes.includes(staff.name)) {
staffId = staff.id;
console.log(`Found staff in notes: ${staff.name} (${staffId})`);
break;
}
}
}
// If no match in notes, check title
if (staffId === 'doctor1' && apt.title) {
console.log("Checking title:", apt.title);
for (const staff of staffMembers) {
if (apt.title.includes(staff.name)) {
staffId = staff.id;
console.log(`Found staff in title: ${staff.name} (${staffId})`);
break;
}
}
}
console.log(`Final staffId assigned: ${staffId}`);
const processed = {
...apt,
patientName,
staffId,
status: apt.status ?? null, // Default to null if status is undefined
date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d
};
console.log("Processed appointment:", processed);
return processed;
});
// Check if appointment exists at a specific time slot and staff // Check if appointment exists at a specific time slot and staff
const getAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => { const getAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => {
if (processedAppointments.length === 0) { if (processedAppointments.length === 0) {
@@ -414,9 +463,10 @@ export default function AppointmentsPage() {
// In appointments for a given time slot, we'll just display the first one // In appointments for a given time slot, we'll just display the first one
// In a real application, you might want to show multiple or stack them // In a real application, you might want to show multiple or stack them
const appointmentsAtSlot = processedAppointments.filter(apt => { const appointmentsAtSlot = processedAppointments.filter((apt) => {
// Fix time format comparison - the database adds ":00" seconds to the time // Fix time format comparison - the database adds ":00" seconds to the time
const dbTime = typeof apt.startTime === 'string' ? apt.startTime.substring(0, 5) : ''; const dbTime =
typeof apt.startTime === "string" ? apt.startTime.substring(0, 5) : "";
const timeMatches = dbTime === timeSlot.time; const timeMatches = dbTime === timeSlot.time;
const staffMatches = apt.staffId === staffId; const staffMatches = apt.staffId === staffId;
@@ -436,35 +486,39 @@ export default function AppointmentsPage() {
// Define drag item types // Define drag item types
const ItemTypes = { const ItemTypes = {
APPOINTMENT: 'appointment', APPOINTMENT: "appointment",
}; };
// Handle moving an appointment to a new time slot and staff // Handle moving an appointment to a new time slot and staff
const handleMoveAppointment = (appointmentId: number, newTimeSlot: TimeSlot, newStaffId: string) => { const handleMoveAppointment = (
const appointment = appointments.find(a => a.id === appointmentId); appointmentId: number,
newTimeSlot: TimeSlot,
newStaffId: string
) => {
const appointment = appointments.find((a) => a.id === appointmentId);
if (!appointment) return; if (!appointment) return;
// Calculate new end time (30 minutes from start) // Calculate new end time (30 minutes from start)
const startHour = parseInt(newTimeSlot.time.split(':')[0] as string); const startHour = parseInt(newTimeSlot.time.split(":")[0] as string);
const startMinute = parseInt(newTimeSlot.time.split(':')[1] as string); const startMinute = parseInt(newTimeSlot.time.split(":")[1] as string);
const startDate = new Date(selectedDate); const startDate = new Date(selectedDate);
startDate.setHours(startHour, startMinute, 0); startDate.setHours(startHour, startMinute, 0);
const endDate = addMinutes(startDate, 30); const endDate = addMinutes(startDate, 30);
const endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`; const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
// Find staff member // Find staff member
const staff = staffMembers.find(s => s.id === newStaffId); const staff = staffMembers.find((s) => s.id === newStaffId);
// Update appointment data // Update appointment data
// Make sure we handle the time format correctly - backend expects HH:MM but stores as HH:MM:SS const { id, createdAt, ...sanitizedAppointment } = appointment;
const updatedAppointment: UpdateAppointment = { const updatedAppointment: UpdateAppointment = {
...appointment, ...sanitizedAppointment,
startTime: newTimeSlot.time, // Already in HH:MM format startTime: newTimeSlot.time, // Already in HH:MM format
endTime: endTime, // Already in HH:MM format endTime: endTime, // Already in HH:MM format
notes: `Appointment with ${staff?.name}`, notes: `Appointment with ${staff?.name}`,
staffId: newStaffId, // Update staffId
}; };
// Call update mutation // Call update mutation
updateAppointmentMutation.mutate({ updateAppointmentMutation.mutate({
id: appointmentId, id: appointmentId,
@@ -487,7 +541,13 @@ export default function AppointmentsPage() {
}; };
// Create a draggable appointment component // Create a draggable appointment component
function DraggableAppointment({ appointment, staff }: { appointment: ScheduledAppointment, staff: Staff }) { function DraggableAppointment({
appointment,
staff,
}: {
appointment: ScheduledAppointment;
staff: Staff;
}) {
const [{ isDragging }, drag] = useDrag(() => ({ const [{ isDragging }, drag] = useDrag(() => ({
type: ItemTypes.APPOINTMENT, type: ItemTypes.APPOINTMENT,
item: { id: appointment.id }, item: { id: appointment.id },
@@ -500,13 +560,15 @@ export default function AppointmentsPage() {
<div <div
ref={drag as unknown as React.RefObject<HTMLDivElement>} // Type assertion to make TypeScript happy ref={drag as unknown as React.RefObject<HTMLDivElement>} // Type assertion to make TypeScript happy
className={`${staff.color} border border-white shadow-md text-white rounded p-1 text-xs h-full overflow-hidden cursor-move relative ${ className={`${staff.color} border border-white shadow-md text-white rounded p-1 text-xs h-full overflow-hidden cursor-move relative ${
isDragging ? 'opacity-50' : 'opacity-100' isDragging ? "opacity-50" : "opacity-100"
}`} }`}
style={{ fontWeight: 500 }} style={{ fontWeight: 500 }}
onClick={(e) => { onClick={(e) => {
// Only allow edit on click if we're not dragging // Only allow edit on click if we're not dragging
if (!isDragging) { if (!isDragging) {
const fullAppointment = appointments.find(a => a.id === appointment.id); const fullAppointment = appointments.find(
(a) => a.id === appointment.id
);
if (fullAppointment) { if (fullAppointment) {
e.stopPropagation(); e.stopPropagation();
handleEditAppointment(fullAppointment); handleEditAppointment(fullAppointment);
@@ -529,12 +591,12 @@ export default function AppointmentsPage() {
timeSlot, timeSlot,
staffId, staffId,
appointment, appointment,
staff staff,
}: { }: {
timeSlot: TimeSlot, timeSlot: TimeSlot;
staffId: string, staffId: string;
appointment: ScheduledAppointment | undefined, appointment: ScheduledAppointment | undefined;
staff: Staff staff: Staff;
}) { }) {
const [{ isOver, canDrop }, drop] = useDrop(() => ({ const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: ItemTypes.APPOINTMENT, accept: ItemTypes.APPOINTMENT,
@@ -555,13 +617,13 @@ export default function AppointmentsPage() {
<td <td
ref={drop as unknown as React.RefObject<HTMLTableCellElement>} ref={drop as unknown as React.RefObject<HTMLTableCellElement>}
key={`${timeSlot.time}-${staffId}`} key={`${timeSlot.time}-${staffId}`}
className={`px-1 py-1 border relative h-14 ${isOver && canDrop ? 'bg-green-100' : ''}`} className={`px-1 py-1 border relative h-14 ${isOver && canDrop ? "bg-green-100" : ""}`}
> >
{appointment ? ( {appointment ? (
<DraggableAppointment appointment={appointment} staff={staff} /> <DraggableAppointment appointment={appointment} staff={staff} />
) : ( ) : (
<button <button
className={`w-full h-full ${isOver && canDrop ? 'bg-green-100' : 'text-gray-400 hover:bg-gray-100'} rounded flex items-center justify-center`} className={`w-full h-full ${isOver && canDrop ? "bg-green-100" : "text-gray-400 hover:bg-gray-100"} rounded flex items-center justify-center`}
onClick={() => handleCreateAppointmentAtSlot(timeSlot, staffId)} onClick={() => handleCreateAppointmentAtSlot(timeSlot, staffId)}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
@@ -573,7 +635,10 @@ export default function AppointmentsPage() {
return ( return (
<div className="flex h-screen overflow-hidden bg-gray-100"> <div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} /> <Sidebar
isMobileOpen={isMobileMenuOpen}
setIsMobileOpen={setIsMobileMenuOpen}
/>
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} /> <TopAppBar toggleMobileMenu={toggleMobileMenu} />
@@ -582,7 +647,9 @@ export default function AppointmentsPage() {
<div className="container mx-auto"> <div className="container mx-auto">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Appointment Schedule</h1> <h1 className="text-3xl font-bold tracking-tight">
Appointment Schedule
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
View and manage the dental practice schedule View and manage the dental practice schedule
</p> </p>
@@ -604,7 +671,9 @@ export default function AppointmentsPage() {
<Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade"> <Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade">
<Item <Item
onClick={({ props }) => { onClick={({ props }) => {
const fullAppointment = appointments.find(a => a.id === props.appointmentId); const fullAppointment = appointments.find(
(a) => a.id === props.appointmentId
);
if (fullAppointment) { if (fullAppointment) {
handleEditAppointment(fullAppointment); handleEditAppointment(fullAppointment);
} }
@@ -617,15 +686,21 @@ export default function AppointmentsPage() {
</Item> </Item>
<Item <Item
onClick={({ props }) => { onClick={({ props }) => {
const fullAppointment = appointments.find(a => a.id === props.appointmentId); const fullAppointment = appointments.find(
(a) => a.id === props.appointmentId
);
if (fullAppointment) { if (fullAppointment) {
// Set the appointment and patient IDs for the claim modal // Set the appointment and patient IDs for the claim modal
setClaimAppointmentId(fullAppointment.id ?? null); setClaimAppointmentId(fullAppointment.id ?? null);
setClaimPatientId(fullAppointment.patientId); setClaimPatientId(fullAppointment.patientId);
// Find the patient name for the toast notification // Find the patient name for the toast notification
const patient = patients.find(p => p.id === fullAppointment.patientId); const patient = patients.find(
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : `Patient #${fullAppointment.patientId}`; (p) => p.id === fullAppointment.patientId
);
const patientName = patient
? `${patient.firstName} ${patient.lastName}`
: `Patient #${fullAppointment.patientId}`;
// Show a toast notification // Show a toast notification
toast({ toast({
@@ -644,7 +719,9 @@ export default function AppointmentsPage() {
</span> </span>
</Item> </Item>
<Item <Item
onClick={({ props }) => handleDeleteAppointment(props.appointmentId)} onClick={({ props }) =>
handleDeleteAppointment(props.appointmentId)
}
> >
<span className="flex items-center gap-2 text-red-600"> <span className="flex items-center gap-2 text-red-600">
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
@@ -663,7 +740,9 @@ export default function AppointmentsPage() {
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => setSelectedDate(addDays(selectedDate, -1))} onClick={() =>
setSelectedDate(addDays(selectedDate, -1))
}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</Button> </Button>
@@ -671,7 +750,9 @@ export default function AppointmentsPage() {
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => setSelectedDate(addDays(selectedDate, 1))} onClick={() =>
setSelectedDate(addDays(selectedDate, 1))
}
> >
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</Button> </Button>
@@ -685,14 +766,18 @@ export default function AppointmentsPage() {
<table className="w-full border-collapse min-w-[800px]"> <table className="w-full border-collapse min-w-[800px]">
<thead> <thead>
<tr> <tr>
<th className="p-2 border bg-gray-50 w-[100px]">Time</th> <th className="p-2 border bg-gray-50 w-[100px]">
{staffMembers.map(staff => ( Time
</th>
{staffMembers.map((staff) => (
<th <th
key={staff.id} key={staff.id}
className={`p-2 border bg-gray-50 ${staff.role === 'doctor' ? 'font-bold' : ''}`} className={`p-2 border bg-gray-50 ${staff.role === "doctor" ? "font-bold" : ""}`}
> >
{staff.name} {staff.name}
<div className="text-xs text-gray-500">{staff.role}</div> <div className="text-xs text-gray-500">
{staff.role}
</div>
</th> </th>
))} ))}
</tr> </tr>
@@ -708,7 +793,10 @@ export default function AppointmentsPage() {
key={`${timeSlot.time}-${staff.id}`} key={`${timeSlot.time}-${staff.id}`}
timeSlot={timeSlot} timeSlot={timeSlot}
staffId={staff.id} staffId={staff.id}
appointment={getAppointmentAtSlot(timeSlot, staff.id)} appointment={getAppointmentAtSlot(
timeSlot,
staff.id
)}
staff={staff} staff={staff}
/> />
))} ))}
@@ -726,7 +814,9 @@ export default function AppointmentsPage() {
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle>Calendar</CardTitle> <CardTitle>Calendar</CardTitle>
<CardDescription>Select a date to view or schedule appointments</CardDescription> <CardDescription>
Select a date to view or schedule appointments
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Calendar <Calendar
@@ -745,32 +835,54 @@ export default function AppointmentsPage() {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between">
<span>Appointments</span> <span>Appointments</span>
<Button variant="ghost" size="icon" onClick={() => refetchAppointments()}> <Button
variant="ghost"
size="icon"
onClick={() => refetchAppointments()}
>
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
</Button> </Button>
</CardTitle> </CardTitle>
<CardDescription>Statistics for {formattedDate}</CardDescription> <CardDescription>
Statistics for {formattedDate}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Total appointments:</span> <span className="text-sm text-gray-500">
<span className="font-semibold">{selectedDateAppointments.length}</span> Total appointments:
</div> </span>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">With doctors:</span>
<span className="font-semibold"> <span className="font-semibold">
{processedAppointments.filter(apt => {selectedDateAppointments.length}
staffMembers.find(s => s.id === apt.staffId)?.role === 'doctor'
).length}
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-500">With hygienists:</span> <span className="text-sm text-gray-500">
With doctors:
</span>
<span className="font-semibold"> <span className="font-semibold">
{processedAppointments.filter(apt => {
staffMembers.find(s => s.id === apt.staffId)?.role === 'hygienist' processedAppointments.filter(
).length} (apt) =>
staffMembers.find((s) => s.id === apt.staffId)
?.role === "doctor"
).length
}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
With hygienists:
</span>
<span className="font-semibold">
{
processedAppointments.filter(
(apt) =>
staffMembers.find((s) => s.id === apt.staffId)
?.role === "hygienist"
).length
}
</span> </span>
</div> </div>
</div> </div>
@@ -787,7 +899,10 @@ export default function AppointmentsPage() {
open={isAddModalOpen} open={isAddModalOpen}
onOpenChange={setIsAddModalOpen} onOpenChange={setIsAddModalOpen}
onSubmit={handleAppointmentSubmit} onSubmit={handleAppointmentSubmit}
isLoading={createAppointmentMutation.isPending || updateAppointmentMutation.isPending} isLoading={
createAppointmentMutation.isPending ||
updateAppointmentMutation.isPending
}
appointment={editingAppointment} appointment={editingAppointment}
patients={patients} patients={patients}
/> />

View File

@@ -7,14 +7,23 @@ import { StatCard } from "@/components/ui/stat-card";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
import { AddPatientModal } from "@/components/patients/add-patient-modal"; import { AddPatientModal } from "@/components/patients/add-patient-modal";
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal"; import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import {
// import { InsertPatient, Patient, UpdatePatient, Appointment, InsertAppointment, UpdateAppointment } from "@repo/db/shared/schemas"; AppointmentUncheckedCreateInputObjectSchema,
import { Users, Calendar, CheckCircle, CreditCard, Plus, Clock } from "lucide-react"; PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/shared/schemas";
import {
Users,
Calendar,
CheckCircle,
CreditCard,
Plus,
Clock,
} from "lucide-react";
import { Link } from "wouter"; import { Link } from "wouter";
import { import {
Dialog, Dialog,
@@ -23,78 +32,105 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {z} from "zod"; import { z } from "zod";
//creating types out of schema auto generated. //creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true, id: true,
createdAt: true, createdAt: true,
}); });
type InsertAppointment = z.infer<typeof insertAppointmentSchema>; type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const updateAppointmentSchema = (
id: true, AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, )
}).partial(); .omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>; type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true, id: true,
createdAt: true, createdAt: true,
}); });
type InsertPatient = z.infer<typeof insertPatientSchema>; type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const updatePatientSchema = (
id: true, PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, )
userId: true, .omit({
}).partial(); id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
export default function Dashboard() { export default function Dashboard() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false); const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false); const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined); const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | undefined>(undefined); undefined
);
const [selectedAppointment, setSelectedAppointment] = useState<
Appointment | undefined
>(undefined);
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
// Fetch patients // Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({ const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
queryKey: ["/api/patients"], Patient[]
>({
queryKey: ["/api/patients/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/patients/");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Fetch appointments // Fetch appointments
const { const {
data: appointments = [] as Appointment[], data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments isLoading: isLoadingAppointments,
} = useQuery<Appointment[]>({ } = useQuery<Appointment[]>({
queryKey: ["/api/appointments"], queryKey: ["/api/appointments/all"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/appointments/all");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Add patient mutation // Add patient mutation
const addPatientMutation = useMutation({ const addPatientMutation = useMutation({
mutationFn: async (patient: InsertPatient) => { mutationFn: async (patient: InsertPatient) => {
const res = await apiRequest("POST", "/api/patients", patient); const res = await apiRequest("POST", "/api/patients/", patient);
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients"] }); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient added successfully!", description: "Patient added successfully!",
@@ -112,13 +148,19 @@ export default function Dashboard() {
// Update patient mutation // Update patient mutation
const updatePatientMutation = useMutation({ const updatePatientMutation = useMutation({
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => { mutationFn: async ({
id,
patient,
}: {
id: number;
patient: UpdatePatient;
}) => {
const res = await apiRequest("PUT", `/api/patients/${id}`, patient); const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients"] }); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient updated successfully!", description: "Patient updated successfully!",
@@ -142,14 +184,25 @@ export default function Dashboard() {
if (user) { if (user) {
addPatientMutation.mutate({ addPatientMutation.mutate({
...patient, ...patient,
userId: user.id userId: user.id,
}); });
} }
}; };
const handleUpdatePatient = (patient: UpdatePatient) => { const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
if (currentPatient) { if (currentPatient && user) {
updatePatientMutation.mutate({ id: currentPatient.id, patient }); const { id, ...sanitizedPatient } = patient;
updatePatientMutation.mutate({
id: currentPatient.id,
patient: sanitizedPatient,
});
} else {
console.error("No current patient or user found for update");
toast({
title: "Error",
description: "Cannot update patient: No patient or user found",
variant: "destructive",
});
} }
}; };
@@ -166,7 +219,7 @@ export default function Dashboard() {
// Create appointment mutation // Create appointment mutation
const createAppointmentMutation = useMutation({ const createAppointmentMutation = useMutation({
mutationFn: async (appointment: InsertAppointment) => { mutationFn: async (appointment: InsertAppointment) => {
const res = await apiRequest("POST", "/api/appointments", appointment); const res = await apiRequest("POST", "/api/appointments/", appointment);
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: () => {
@@ -175,7 +228,9 @@ export default function Dashboard() {
title: "Success", title: "Success",
description: "Appointment created successfully.", description: "Appointment created successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] }); // Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({
@@ -188,8 +243,18 @@ export default function Dashboard() {
// Update appointment mutation // Update appointment mutation
const updateAppointmentMutation = useMutation({ const updateAppointmentMutation = useMutation({
mutationFn: async ({ id, appointment }: { id: number; appointment: UpdateAppointment }) => { mutationFn: async ({
const res = await apiRequest("PUT", `/api/appointments/${id}`, appointment); id,
appointment,
}: {
id: number;
appointment: UpdateAppointment;
}) => {
const res = await apiRequest(
"PUT",
`/api/appointments/${id}`,
appointment
);
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: () => {
@@ -198,7 +263,9 @@ export default function Dashboard() {
title: "Success", title: "Success",
description: "Appointment updated successfully.", description: "Appointment updated successfully.",
}); });
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] }); // Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({
@@ -210,8 +277,10 @@ export default function Dashboard() {
}); });
// Handle appointment submission (create or update) // Handle appointment submission (create or update)
const handleAppointmentSubmit = (appointmentData: InsertAppointment | UpdateAppointment) => { const handleAppointmentSubmit = (
if (selectedAppointment && typeof selectedAppointment.id === 'number') { appointmentData: InsertAppointment | UpdateAppointment
) => {
if (selectedAppointment && typeof selectedAppointment.id === "number") {
updateAppointmentMutation.mutate({ updateAppointmentMutation.mutate({
id: selectedAppointment.id, id: selectedAppointment.id,
appointment: appointmentData as UpdateAppointment, appointment: appointmentData as UpdateAppointment,
@@ -219,7 +288,7 @@ export default function Dashboard() {
} else { } else {
if (user) { if (user) {
createAppointmentMutation.mutate({ createAppointmentMutation.mutate({
...appointmentData as InsertAppointment, ...(appointmentData as InsertAppointment),
userId: user.id, userId: user.id,
}); });
} }
@@ -228,23 +297,24 @@ export default function Dashboard() {
// Since we removed filters, just return all patients // Since we removed filters, just return all patients
const filteredPatients = patients; const filteredPatients = patients;
const today = format(new Date(), "yyyy-MM-dd");
// Get today's date in YYYY-MM-DD format const todaysAppointments = appointments.filter((appointment) => {
const today = format(new Date(), 'yyyy-MM-dd'); const appointmentDate = format(new Date(appointment.date), "yyyy-MM-dd");
return appointmentDate === today;
// Filter appointments for today });
const todaysAppointments = appointments.filter(
(appointment) => appointment.date === today
);
// Count completed appointments today // Count completed appointments today
const completedTodayCount = todaysAppointments.filter( const completedTodayCount = todaysAppointments.filter((appointment) => {
(appointment) => appointment.status === 'completed' return appointment.status === "completed";
).length; }).length;
return ( return (
<div className="flex h-screen overflow-hidden bg-gray-100"> <div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} /> <Sidebar
isMobileOpen={isMobileMenuOpen}
setIsMobileOpen={setIsMobileMenuOpen}
/>
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} /> <TopAppBar toggleMobileMenu={toggleMobileMenu} />
@@ -281,7 +351,9 @@ export default function Dashboard() {
{/* Today's Appointments Section */} {/* Today's Appointments Section */}
<div className="flex flex-col space-y-4 mb-6"> <div className="flex flex-col space-y-4 mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between"> <div className="flex flex-col md:flex-row md:items-center md:justify-between">
<h2 className="text-xl font-medium text-gray-800">Today's Appointments</h2> <h2 className="text-xl font-medium text-gray-800">
Today's Appointments
</h2>
<Button <Button
className="mt-2 md:mt-0" className="mt-2 md:mt-0"
onClick={() => { onClick={() => {
@@ -299,33 +371,70 @@ export default function Dashboard() {
{todaysAppointments.length > 0 ? ( {todaysAppointments.length > 0 ? (
<div className="divide-y"> <div className="divide-y">
{todaysAppointments.map((appointment) => { {todaysAppointments.map((appointment) => {
const patient = patients.find(p => p.id === appointment.patientId); const patient = patients.find(
(p) => p.id === appointment.patientId
);
return ( return (
<div key={appointment.id} className="p-4 flex items-center justify-between"> <div
key={appointment.id}
className="p-4 flex items-center justify-between"
>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="h-10 w-10 rounded-full bg-primary bg-opacity-10 text-primary flex items-center justify-center"> <div className="h-10 w-10 rounded-full bg-primary bg-opacity-10 text-primary flex items-center justify-center">
<Clock className="h-5 w-5" /> <Clock className="h-5 w-5" />
</div> </div>
<div> <div>
<h3 className="font-medium"> <h3 className="font-medium">
{patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown Patient'} {patient
? `${patient.firstName} ${patient.lastName}`
: "Unknown Patient"}
</h3> </h3>
<div className="text-sm text-gray-500 flex items-center space-x-2"> <div className="text-sm text-gray-500 flex items-center space-x-2">
<span>{new Date(appointment.startTime).toLocaleString()} - {new Date(appointment.endTime).toLocaleString()}</span> <span>
{new Date(
`${appointment.date.toString().slice(0, 10)}T${appointment.startTime}:00`
).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}{" "}
-{" "}
{new Date(
`${appointment.date.toString().slice(0, 10)}T${appointment.endTime}:00`
).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span>•</span> <span>•</span>
<span>{appointment.type.charAt(0).toUpperCase() + appointment.type.slice(1)}</span> <span>
{appointment.type.charAt(0).toUpperCase() +
appointment.type.slice(1)}
</span>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <span
${appointment.status === 'completed' ? 'bg-green-100 text-green-800' : className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
appointment.status === 'cancelled' ? 'bg-red-100 text-red-800' : ${
appointment.status === 'confirmed' ? 'bg-blue-100 text-blue-800' : appointment.status === "completed"
'bg-yellow-100 text-yellow-800'}`}> ? "bg-green-100 text-green-800"
{appointment.status ? appointment.status.charAt(0).toUpperCase() + appointment.status.slice(1) : 'Scheduled'} : appointment.status === "cancelled"
? "bg-red-100 text-red-800"
: appointment.status === "confirmed"
? "bg-blue-100 text-blue-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{appointment.status
? appointment.status.charAt(0).toUpperCase() +
appointment.status.slice(1)
: "Scheduled"}
</span> </span>
<Link to="/appointments" className="text-primary hover:text-primary/80 text-sm"> <Link
to="/appointments"
className="text-primary hover:text-primary/80 text-sm"
>
View All View All
</Link> </Link>
</div> </div>
@@ -336,7 +445,9 @@ export default function Dashboard() {
) : ( ) : (
<div className="p-6 text-center"> <div className="p-6 text-center">
<Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" /> <Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" />
<h3 className="text-lg font-medium text-gray-900">No appointments today</h3> <h3 className="text-lg font-medium text-gray-900">
No appointments today
</h3>
<p className="mt-1 text-gray-500"> <p className="mt-1 text-gray-500">
You don't have any appointments scheduled for today. You don't have any appointments scheduled for today.
</p> </p>
@@ -360,7 +471,9 @@ export default function Dashboard() {
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
{/* Patient Header */} {/* Patient Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between"> <div className="flex flex-col md:flex-row md:items-center md:justify-between">
<h2 className="text-xl font-medium text-gray-800">Patient Management</h2> <h2 className="text-xl font-medium text-gray-800">
Patient Management
</h2>
<Button <Button
className="mt-2 md:mt-0" className="mt-2 md:mt-0"
onClick={() => { onClick={() => {
@@ -373,8 +486,6 @@ export default function Dashboard() {
</Button> </Button>
</div> </div>
{/* Search and filters removed */}
{/* Patient Table */} {/* Patient Table */}
<PatientTable <PatientTable
patients={filteredPatients} patients={filteredPatients}
@@ -390,7 +501,9 @@ export default function Dashboard() {
open={isAddPatientOpen} open={isAddPatientOpen}
onOpenChange={setIsAddPatientOpen} onOpenChange={setIsAddPatientOpen}
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient} onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
isLoading={addPatientMutation.isPending || updatePatientMutation.isPending} isLoading={
addPatientMutation.isPending || updatePatientMutation.isPending
}
patient={currentPatient} patient={currentPatient}
/> />
@@ -408,60 +521,76 @@ export default function Dashboard() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="h-16 w-16 rounded-full bg-primary text-white flex items-center justify-center text-xl font-medium"> <div className="h-16 w-16 rounded-full bg-primary text-white flex items-center justify-center text-xl font-medium">
{currentPatient.firstName.charAt(0)}{currentPatient.lastName.charAt(0)} {currentPatient.firstName.charAt(0)}
{currentPatient.lastName.charAt(0)}
</div> </div>
<div> <div>
<h3 className="text-xl font-semibold">{currentPatient.firstName} {currentPatient.lastName}</h3> <h3 className="text-xl font-semibold">
<p className="text-gray-500">Patient ID: {currentPatient.id.toString().padStart(4, '0')}</p> {currentPatient.firstName} {currentPatient.lastName}
</h3>
<p className="text-gray-500">
Patient ID: {currentPatient.id.toString().padStart(4, "0")}
</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
<div> <div>
<h4 className="font-medium text-gray-900">Personal Information</h4> <h4 className="font-medium text-gray-900">
Personal Information
</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<p> <p>
<span className="text-gray-500">Date of Birth:</span>{' '} <span className="text-gray-500">Date of Birth:</span>{" "}
{new Date(currentPatient.dateOfBirth).toLocaleDateString()} {new Date(
currentPatient.dateOfBirth
).toLocaleDateString()}
</p> </p>
<p> <p>
<span className="text-gray-500">Gender:</span>{' '} <span className="text-gray-500">Gender:</span>{" "}
{currentPatient.gender.charAt(0).toUpperCase() + currentPatient.gender.slice(1)} {currentPatient.gender.charAt(0).toUpperCase() +
currentPatient.gender.slice(1)}
</p> </p>
<p> <p>
<span className="text-gray-500">Status:</span>{' '} <span className="text-gray-500">Status:</span>{" "}
<span className={`${ <span
currentPatient.status === 'active' className={`${
? 'text-green-600' currentPatient.status === "active"
: 'text-amber-600' ? "text-green-600"
} font-medium`}> : "text-amber-600"
{currentPatient.status.charAt(0).toUpperCase() + currentPatient.status.slice(1)} } font-medium`}
>
{currentPatient.status.charAt(0).toUpperCase() +
currentPatient.status.slice(1)}
</span> </span>
</p> </p>
</div> </div>
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900">Contact Information</h4> <h4 className="font-medium text-gray-900">
Contact Information
</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<p> <p>
<span className="text-gray-500">Phone:</span>{' '} <span className="text-gray-500">Phone:</span>{" "}
{currentPatient.phone} {currentPatient.phone}
</p> </p>
<p> <p>
<span className="text-gray-500">Email:</span>{' '} <span className="text-gray-500">Email:</span>{" "}
{currentPatient.email || 'N/A'} {currentPatient.email || "N/A"}
</p> </p>
<p> <p>
<span className="text-gray-500">Address:</span>{' '} <span className="text-gray-500">Address:</span>{" "}
{currentPatient.address ? ( {currentPatient.address ? (
<> <>
{currentPatient.address} {currentPatient.address}
{currentPatient.city && `, ${currentPatient.city}`} {currentPatient.city && `, ${currentPatient.city}`}
{currentPatient.zipCode && ` ${currentPatient.zipCode}`} {currentPatient.zipCode &&
` ${currentPatient.zipCode}`}
</> </>
) : ( ) : (
'N/A' "N/A"
)} )}
</p> </p>
</div> </div>
@@ -471,44 +600,46 @@ export default function Dashboard() {
<h4 className="font-medium text-gray-900">Insurance</h4> <h4 className="font-medium text-gray-900">Insurance</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<p> <p>
<span className="text-gray-500">Provider:</span>{' '} <span className="text-gray-500">Provider:</span>{" "}
{currentPatient.insuranceProvider {currentPatient.insuranceProvider
? currentPatient.insuranceProvider === 'delta' ? currentPatient.insuranceProvider === "delta"
? 'Delta Dental' ? "Delta Dental"
: currentPatient.insuranceProvider === 'metlife' : currentPatient.insuranceProvider === "metlife"
? 'MetLife' ? "MetLife"
: currentPatient.insuranceProvider === 'cigna' : currentPatient.insuranceProvider === "cigna"
? 'Cigna' ? "Cigna"
: currentPatient.insuranceProvider === 'aetna' : currentPatient.insuranceProvider === "aetna"
? 'Aetna' ? "Aetna"
: currentPatient.insuranceProvider : currentPatient.insuranceProvider
: 'N/A'} : "N/A"}
</p> </p>
<p> <p>
<span className="text-gray-500">ID:</span>{' '} <span className="text-gray-500">ID:</span>{" "}
{currentPatient.insuranceId || 'N/A'} {currentPatient.insuranceId || "N/A"}
</p> </p>
<p> <p>
<span className="text-gray-500">Group Number:</span>{' '} <span className="text-gray-500">Group Number:</span>{" "}
{currentPatient.groupNumber || 'N/A'} {currentPatient.groupNumber || "N/A"}
</p> </p>
<p> <p>
<span className="text-gray-500">Policy Holder:</span>{' '} <span className="text-gray-500">Policy Holder:</span>{" "}
{currentPatient.policyHolder || 'Self'} {currentPatient.policyHolder || "Self"}
</p> </p>
</div> </div>
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900">Medical Information</h4> <h4 className="font-medium text-gray-900">
Medical Information
</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
<p> <p>
<span className="text-gray-500">Allergies:</span>{' '} <span className="text-gray-500">Allergies:</span>{" "}
{currentPatient.allergies || 'None reported'} {currentPatient.allergies || "None reported"}
</p> </p>
<p> <p>
<span className="text-gray-500">Medical Conditions:</span>{' '} <span className="text-gray-500">Medical Conditions:</span>{" "}
{currentPatient.medicalConditions || 'None reported'} {currentPatient.medicalConditions || "None reported"}
</p> </p>
</div> </div>
</div> </div>
@@ -540,7 +671,10 @@ export default function Dashboard() {
open={isAddAppointmentOpen} open={isAddAppointmentOpen}
onOpenChange={setIsAddAppointmentOpen} onOpenChange={setIsAddAppointmentOpen}
onSubmit={handleAppointmentSubmit} onSubmit={handleAppointmentSubmit}
isLoading={createAppointmentMutation.isPending || updateAppointmentMutation.isPending} isLoading={
createAppointmentMutation.isPending ||
updateAppointmentMutation.isPending
}
appointment={selectedAppointment} appointment={selectedAppointment}
patients={patients} patients={patients}
/> />

View File

@@ -4,39 +4,55 @@ import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
import { AddPatientModal } from "@/components/patients/add-patient-modal"; import { AddPatientModal } from "@/components/patients/add-patient-modal";
import { PatientSearch, SearchCriteria } from "@/components/patients/patient-search"; import {
PatientSearch,
SearchCriteria,
} from "@/components/patients/patient-search";
import { FileUploadZone } from "@/components/file-upload/file-upload-zone"; import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, RefreshCw, File, FilePlus } from "lucide-react"; import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
// import { Patient, InsertPatient, UpdatePatient } from "@repo/db/shared/schemas"; // import { Patient, InsertPatient, UpdatePatient } from "@repo/db/shared/schemas";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import {z} from "zod"; import { z } from "zod";
const PatientSchema = (
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertPatientSchema = (
id: true, PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, ).omit({
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
id: true, id: true,
createdAt: true, createdAt: true,
userId: true, userId: true,
}).partial(); });
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
// Type for the ref to access modal methods // Type for the ref to access modal methods
type AddPatientModalRef = { type AddPatientModalRef = {
shouldSchedule: boolean; shouldSchedule: boolean;
@@ -48,9 +64,13 @@ export default function PatientsPage() {
const { user } = useAuth(); const { user } = useAuth();
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false); const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined); const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
undefined
);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(null); const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(
null
);
const addPatientModalRef = useRef<AddPatientModalRef | null>(null); const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
// File upload states // File upload states
@@ -65,19 +85,23 @@ export default function PatientsPage() {
isLoading: isLoadingPatients, isLoading: isLoadingPatients,
refetch: refetchPatients, refetch: refetchPatients,
} = useQuery<Patient[]>({ } = useQuery<Patient[]>({
queryKey: ["/api/patients"], queryKey: ["/api/patients/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/patients/");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Add patient mutation // Add patient mutation
const addPatientMutation = useMutation({ const addPatientMutation = useMutation({
mutationFn: async (patient: InsertPatient) => { mutationFn: async (patient: InsertPatient) => {
const res = await apiRequest("POST", "/api/patients", patient); const res = await apiRequest("POST", "/api/patients/", patient);
return res.json(); return res.json();
}, },
onSuccess: (newPatient) => { onSuccess: (newPatient) => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients"] }); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient added successfully!", description: "Patient added successfully!",
@@ -100,13 +124,19 @@ export default function PatientsPage() {
// Update patient mutation // Update patient mutation
const updatePatientMutation = useMutation({ const updatePatientMutation = useMutation({
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => { mutationFn: async ({
id,
patient,
}: {
id: number;
patient: UpdatePatient;
}) => {
const res = await apiRequest("PUT", `/api/patients/${id}`, patient); const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients"] }); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient updated successfully!", description: "Patient updated successfully!",
@@ -131,14 +161,25 @@ export default function PatientsPage() {
if (user) { if (user) {
addPatientMutation.mutate({ addPatientMutation.mutate({
...patient, ...patient,
userId: user.id userId: user.id,
}); });
} }
}; };
const handleUpdatePatient = (patient: UpdatePatient) => { const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
if (currentPatient) { if (currentPatient && user) {
updatePatientMutation.mutate({ id: currentPatient.id, patient }); const { id, ...sanitizedPatient } = patient;
updatePatientMutation.mutate({
id: currentPatient.id,
patient: sanitizedPatient,
});
} else {
console.error("No current patient or user found for update");
toast({
title: "Error",
description: "Cannot update patient: No patient or user found",
variant: "destructive",
});
} }
}; };
@@ -152,7 +193,10 @@ export default function PatientsPage() {
setIsViewPatientOpen(true); setIsViewPatientOpen(true);
}; };
const isLoading = isLoadingPatients || addPatientMutation.isPending || updatePatientMutation.isPending; const isLoading =
isLoadingPatients ||
addPatientMutation.isPending ||
updatePatientMutation.isPending;
// Search handling // Search handling
const handleSearch = (criteria: SearchCriteria) => { const handleSearch = (criteria: SearchCriteria) => {
@@ -195,15 +239,15 @@ export default function PatientsPage() {
// Set up a Promise to handle file reading // Set up a Promise to handle file reading
const fileReadPromise = new Promise<string>((resolve, reject) => { const fileReadPromise = new Promise<string>((resolve, reject) => {
reader.onload = (event) => { reader.onload = (event) => {
if (event.target && typeof event.target.result === 'string') { if (event.target && typeof event.target.result === "string") {
resolve(event.target.result); resolve(event.target.result);
} else { } else {
reject(new Error('Failed to read file as base64')); reject(new Error("Failed to read file as base64"));
} }
}; };
reader.onerror = () => { reader.onerror = () => {
reject(new Error('Error reading file')); reject(new Error("Error reading file"));
}; };
// Read the file as a data URL (base64) // Read the file as a data URL (base64)
@@ -214,20 +258,22 @@ export default function PatientsPage() {
const base64Data = await fileReadPromise; const base64Data = await fileReadPromise;
// Send file to server as base64 // Send file to server as base64
const response = await fetch('/api/upload-file', { const response = await fetch("/api/upload-file", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
pdfData: base64Data, pdfData: base64Data,
filename: uploadedFile.name filename: uploadedFile.name,
}), }),
credentials: 'include' credentials: "include",
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Server returned ${response.status}: ${response.statusText}`); throw new Error(
`Server returned ${response.status}: ${response.statusText}`
);
} }
const data = await response.json(); const data = await response.json();
@@ -238,7 +284,7 @@ export default function PatientsPage() {
firstName: data.extractedInfo.firstName, firstName: data.extractedInfo.firstName,
lastName: data.extractedInfo.lastName, lastName: data.extractedInfo.lastName,
dateOfBirth: data.extractedInfo.dateOfBirth, dateOfBirth: data.extractedInfo.dateOfBirth,
insuranceId: data.extractedInfo.insuranceId insuranceId: data.extractedInfo.insuranceId,
}; };
setExtractedInfo(simplifiedInfo); setExtractedInfo(simplifiedInfo);
@@ -246,7 +292,8 @@ export default function PatientsPage() {
// Show success message // Show success message
toast({ toast({
title: "Information Extracted", title: "Information Extracted",
description: "Basic patient information (name, DOB, ID) has been extracted successfully.", description:
"Basic patient information (name, DOB, ID) has been extracted successfully.",
variant: "default", variant: "default",
}); });
@@ -257,7 +304,6 @@ export default function PatientsPage() {
setTimeout(() => { setTimeout(() => {
setIsAddPatientOpen(true); setIsAddPatientOpen(true);
}, 500); }, 500);
} else { } else {
throw new Error(data.message || "Failed to extract information"); throw new Error(data.message || "Failed to extract information");
} }
@@ -265,7 +311,10 @@ export default function PatientsPage() {
console.error("Error extracting information:", error); console.error("Error extracting information:", error);
toast({ toast({
title: "Error", title: "Error",
description: error instanceof Error ? error.message : "Failed to extract information from file", description:
error instanceof Error
? error.message
: "Failed to extract information from file",
variant: "destructive", variant: "destructive",
}); });
} finally { } finally {
@@ -282,18 +331,18 @@ export default function PatientsPage() {
const term = searchCriteria.searchTerm.toLowerCase(); const term = searchCriteria.searchTerm.toLowerCase();
return patients.filter((patient) => { return patients.filter((patient) => {
switch (searchCriteria.searchBy) { switch (searchCriteria.searchBy) {
case 'name': case "name":
return ( return (
patient.firstName.toLowerCase().includes(term) || patient.firstName.toLowerCase().includes(term) ||
patient.lastName.toLowerCase().includes(term) patient.lastName.toLowerCase().includes(term)
); );
case 'phone': case "phone":
return patient.phone.toLowerCase().includes(term); return patient.phone.toLowerCase().includes(term);
case 'insuranceProvider': case "insuranceProvider":
return patient.insuranceProvider?.toLowerCase().includes(term); return patient.insuranceProvider?.toLowerCase().includes(term);
case 'insuranceId': case "insuranceId":
return patient.insuranceId?.toLowerCase().includes(term); return patient.insuranceId?.toLowerCase().includes(term);
case 'all': case "all":
default: default:
return ( return (
patient.firstName.toLowerCase().includes(term) || patient.firstName.toLowerCase().includes(term) ||
@@ -311,7 +360,10 @@ export default function PatientsPage() {
return ( return (
<div className="flex h-screen overflow-hidden bg-gray-100"> <div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} /> <Sidebar
isMobileOpen={isMobileMenuOpen}
setIsMobileOpen={setIsMobileMenuOpen}
/>
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} /> <TopAppBar toggleMobileMenu={toggleMobileMenu} />
@@ -409,8 +461,10 @@ export default function PatientsPage() {
<div className="flex items-center my-4 px-2 py-1 bg-muted rounded-md text-sm"> <div className="flex items-center my-4 px-2 py-1 bg-muted rounded-md text-sm">
<p> <p>
Found {filteredPatients.length} Found {filteredPatients.length}
{filteredPatients.length === 1 ? ' patient' : ' patients'} {filteredPatients.length === 1 ? " patient" : " patients"}
{searchCriteria.searchBy !== 'all' ? ` with ${searchCriteria.searchBy}` : ''} {searchCriteria.searchBy !== "all"
? ` with ${searchCriteria.searchBy}`
: ""}
matching "{searchCriteria.searchTerm}" matching "{searchCriteria.searchTerm}"
</p> </p>
</div> </div>
@@ -433,12 +487,16 @@ export default function PatientsPage() {
isLoading={isLoading} isLoading={isLoading}
patient={currentPatient} patient={currentPatient}
// Pass extracted info as a separate prop to avoid triggering edit mode // Pass extracted info as a separate prop to avoid triggering edit mode
extractedInfo={!currentPatient && extractedInfo ? { extractedInfo={
firstName: extractedInfo.firstName || "", !currentPatient && extractedInfo
lastName: extractedInfo.lastName || "", ? {
dateOfBirth: extractedInfo.dateOfBirth || "", firstName: extractedInfo.firstName || "",
insuranceId: extractedInfo.insuranceId || "" lastName: extractedInfo.lastName || "",
} : undefined} dateOfBirth: extractedInfo.dateOfBirth || "",
insuranceId: extractedInfo.insuranceId || "",
}
: undefined
}
/> />
</div> </div>
</main> </main>

1497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,13 @@
"lint": "turbo run lint", "lint": "turbo run lint",
"check-types": "turbo run check-types", "check-types": "turbo run check-types",
"format": "prettier --write \"**/*.{ts,tsx,md}\"", "format": "prettier --write \"**/*.{ts,tsx,md}\"",
"db:generate": "cd packages/db && npx prisma generate && cd ../..", "db:generate": "prisma generate --schema=packages/db/prisma/schema.prisma",
"setup:env": "shx cp packages/db/.env.example packages/db/.env" "db:migrate": "dotenv -e packages/db/.env -- prisma migrate dev --schema=packages/db/prisma/schema.prisma",
"db:seed": "prisma db seed --schema=packages/db/prisma/schema.prisma",
"setup:env": "shx cp packages/db/.env.example packages/db/.env && shx cp apps/Frontend/.env.example apps/Frontend/.env && shx cp apps/Backend/.env.example apps/Backend/.env"
},
"prisma": {
"seed": "ts-node packages/db/prisma/seed.ts"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.5.3", "prettier": "^3.5.3",
@@ -26,6 +30,8 @@
"packages/*" "packages/*"
], ],
"dependencies": { "dependencies": {
"dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0",
"shx": "^0.4.0" "shx": "^0.4.0"
} }
} }

View File

@@ -0,0 +1,17 @@
-- AlterTable
ALTER TABLE "Appointment" ADD COLUMN "staffId" INTEGER;
-- CreateTable
CREATE TABLE "Staff" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT,
"role" TEXT NOT NULL,
"phone" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Staff_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Appointment" ALTER COLUMN "startTime" SET DATA TYPE TEXT,
ALTER COLUMN "endTime" SET DATA TYPE TEXT;

View File

@@ -8,9 +8,10 @@ generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
output = "../generated/prisma" output = "../generated/prisma"
} }
generator zod { generator zod {
provider = "prisma-zod-generator" provider = "prisma-zod-generator"
output = "../shared/" // Zod schemas will be generated here inside `db/shared` output = "../shared/" // Zod schemas will be generated here inside `db/shared`
} }
datasource db { datasource db {
@@ -18,53 +19,63 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
username String @unique username String @unique
password String password String
patients Patient[] patients Patient[]
appointments Appointment[] appointments Appointment[]
} }
model Patient { model Patient {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
firstName String firstName String
lastName String lastName String
dateOfBirth DateTime @db.Date dateOfBirth DateTime @db.Date
gender String gender String
phone String phone String
email String? email String?
address String? address String?
city String? city String?
zipCode String? zipCode String?
insuranceProvider String? insuranceProvider String?
insuranceId String? insuranceId String?
groupNumber String? groupNumber String?
policyHolder String? policyHolder String?
allergies String? allergies String?
medicalConditions String? medicalConditions String?
status String @default("active") status String @default("active")
userId Int userId Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
appointments Appointment[] appointments Appointment[]
} }
model Appointment { model Appointment {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
patientId Int patientId Int
userId Int userId Int
staffId Int? // Optional: Appointment may or may not have staff assigned
title String title String
date DateTime @db.Date date DateTime @db.Date
startTime DateTime @db.Time startTime String // Store time as "hh:mm"
endTime DateTime @db.Time endTime String // Store time as "hh:mm"
type String // e.g., "checkup", "cleaning", "filling", etc. type String // e.g., "checkup", "cleaning", "filling", etc.
notes String? notes String?
status String @default("scheduled") // "scheduled", "completed", "cancelled", "no-show" status String @default("scheduled") // "scheduled", "completed", "cancelled", "no-show"
createdAt DateTime @default(now()) createdAt DateTime @default(now())
patient Patient @relation(fields: [patientId], references: [id]) patient Patient @relation(fields: [patientId], references: [id])
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
staff Staff? @relation(fields: [staffId], references: [id])
}
model Staff {
id Int @id @default(autoincrement())
name String
email String?
role String // e.g., "Dentist", "Hygienist", "Assistant"
phone String?
createdAt DateTime @default(now())
appointments Appointment[]
} }

View File

@@ -0,0 +1,93 @@
import { PrismaClient } from '../generated/prisma';
const prisma = new PrismaClient();
console.log("Here")
async function main() {
// Create multiple users
const users = await prisma.user.createMany({
data: [
{ username: 'admin2', password: '123456' },
{ username: 'bob', password: '123456' },
],
});
const createdUsers = await prisma.user.findMany();
// Creatin staff
await prisma.staff.createMany({
data: [
{ name: 'Dr. Kai Gao', role: 'Doctor' },
{ name: 'Dr. Jane Smith', role: 'Doctor' },
],
});
const staffMembers = await prisma.staff.findMany();
// Create multiple patients
const patients = await prisma.patient.createMany({
data: [
{
firstName: 'Emily',
lastName: 'Clark',
dateOfBirth: new Date('1985-06-15'),
gender: 'female',
phone: '555-0001',
email: 'emily@example.com',
address: '101 Apple Rd',
city: 'Newtown',
zipCode: '10001',
userId: createdUsers[0].id,
},
{
firstName: 'Michael',
lastName: 'Brown',
dateOfBirth: new Date('1979-09-10'),
gender: 'male',
phone: '555-0002',
email: 'michael@example.com',
address: '202 Banana Ave',
city: 'Oldtown',
zipCode: '10002',
userId: createdUsers[1].id,
},
],
});
const createdPatients = await prisma.patient.findMany();
// Create multiple appointments
await prisma.appointment.createMany({
data: [
{
patientId: createdPatients[0].id,
userId: createdUsers[0].id,
title: 'Initial Consultation',
date: new Date('2025-06-01'),
startTime: new Date('2025-06-01T10:00:00'),
endTime: new Date('2025-06-01T10:30:00'),
type: 'consultation',
},
{
patientId: createdPatients[1].id,
userId: createdUsers[1].id,
title: 'Follow-up',
date: new Date('2025-06-02'),
startTime: new Date('2025-06-02T14:00:00'),
endTime: new Date('2025-06-02T14:30:00'),
type: 'checkup',
},
],
});
console.log('✅ Seeded multiple users, patients, and appointments.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
console.log("Done seeding logged.")
await prisma.$disconnect();
});