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,87 +155,110 @@ 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 =
typeof data.patientId === "string"
? parseInt(data.patientId, 10) ? parseInt(data.patientId, 10)
: data.patientId; : 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 (
<div className="form-container">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> <form
onSubmit={form.handleSubmit(
(data) => {
handleSubmit(data);
},
(errors) => {
console.error("Validation failed:", errors);
}
)}
className="space-y-6"
>
<FormField <FormField
control={form.control} control={form.control}
name="patientId" name="patientId"
@@ -230,7 +267,7 @@ export function AppointmentForm({
<FormLabel>Patient</FormLabel> <FormLabel>Patient</FormLabel>
<Select <Select
disabled={isLoading} disabled={isLoading}
onValueChange={field.onChange} onValueChange={(val) => field.onChange(Number(val))}
value={field.value?.toString()} value={field.value?.toString()}
defaultValue={field.value?.toString()} defaultValue={field.value?.toString()}
> >
@@ -241,7 +278,10 @@ export function AppointmentForm({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{patients.map((patient) => ( {patients.map((patient) => (
<SelectItem key={patient.id} value={patient.id.toString()}> <SelectItem
key={patient.id}
value={patient.id.toString()}
>
{patient.firstName} {patient.lastName} {patient.firstName} {patient.lastName}
</SelectItem> </SelectItem>
))} ))}
@@ -257,7 +297,12 @@ export function AppointmentForm({
name="title" name="title"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Appointment Title <span className="text-muted-foreground text-xs">(optional)</span></FormLabel> <FormLabel>
Appointment Title{" "}
<span className="text-muted-foreground text-xs">
(optional)
</span>
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="Leave blank to auto-fill with date" placeholder="Leave blank to auto-fill with date"
@@ -299,7 +344,7 @@ export function AppointmentForm({
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="start">
<Calendar <Calendar
mode="single" mode="single"
selected={field.value} selected={field.value ? new Date(field.value) : undefined}
onSelect={field.onChange} onSelect={field.onChange}
disabled={(date) => disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0)) date < new Date(new Date().setHours(0, 0, 0, 0))
@@ -328,6 +373,9 @@ export function AppointmentForm({
{...field} {...field}
disabled={isLoading} disabled={isLoading}
className="pl-10" className="pl-10"
value={
typeof field.value === "string" ? field.value : ""
}
/> />
</div> </div>
</FormControl> </FormControl>
@@ -350,6 +398,9 @@ export function AppointmentForm({
{...field} {...field}
disabled={isLoading} disabled={isLoading}
className="pl-10" className="pl-10"
value={
typeof field.value === "string" ? field.value : ""
}
/> />
</div> </div>
</FormControl> </FormControl>
@@ -426,15 +477,15 @@ export function AppointmentForm({
<FormField <FormField
control={form.control} control={form.control}
name="staff" name="staffId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Doctor/Hygienist</FormLabel> <FormLabel>Doctor/Hygienist</FormLabel>
<Select <Select
disabled={isLoading} disabled={isLoading}
onValueChange={field.onChange} onValueChange={(val) => field.onChange(Number(val))}
value={field.value} value={field.value ? String(field.value) : undefined}
defaultValue={field.value} defaultValue={field.value ? String(field.value) : undefined}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@@ -443,7 +494,10 @@ export function AppointmentForm({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{staffMembers.map((staff) => ( {staffMembers.map((staff) => (
<SelectItem key={staff.id} value={staff.id}> <SelectItem
key={staff.id}
value={staff.id?.toString() || ""}
>
{staff.name} ({staff.role}) {staff.name} ({staff.role})
</SelectItem> </SelectItem>
))} ))}
@@ -466,6 +520,7 @@ export function AppointmentForm({
{...field} {...field}
disabled={isLoading} disabled={isLoading}
className="min-h-24" className="min-h-24"
value={field.value ?? ""}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -478,5 +533,6 @@ export function AppointmentForm({
</Button> </Button>
</form> </form>
</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 = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true, id: true,
createdAt: true, createdAt: true,
userId: true, userId: true,
}).partial(); })
.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, id: patient.id });
} else {
onSubmit(data); 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,16 +60,38 @@ 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) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
return {
...sanitizedPatient,
dateOfBirth: patient.dateOfBirth
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
: "",
};
}
return {
firstName: extractedInfo?.firstName || "", firstName: extractedInfo?.firstName || "",
lastName: extractedInfo?.lastName || "", lastName: extractedInfo?.lastName || "",
dateOfBirth: extractedInfo?.dateOfBirth || "", dateOfBirth: extractedInfo?.dateOfBirth || "",
@@ -81,22 +110,53 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
status: "active", status: "active",
userId: user?.id, 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

@@ -12,18 +12,18 @@ 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 = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true, id: true,
createdAt: true, createdAt: true,
}).partial(); 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,24 +243,28 @@ 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(
"newAppointmentData",
JSON.stringify({
...parsedData, ...parsedData,
...patientData ...patientData,
})); })
);
} }
} }
} }
@@ -231,7 +274,7 @@ export default function AppointmentsPage() {
// 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,61 +403,47 @@ 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[] =
selectedDateAppointments.map((apt) => {
// Find patient name // Find patient name
const patient = patients.find(p => p.id === apt.patientId); const patient = patients.find((p) => p.id === apt.patientId);
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown Patient'; 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:", {
id: apt.id,
notes: apt.notes,
title: apt.title
});
// Check notes first // Check notes first
if (apt.notes) { if (apt.notes) {
// Look for "Appointment with Dr. X" or similar patterns // Look for "Appointment with Dr. X" or similar patterns
console.log("Checking notes:", apt.notes);
for (const staff of staffMembers) { for (const staff of staffMembers) {
console.log(`Checking if notes contains "${staff.name}":`, apt.notes.includes(staff.name));
if (apt.notes.includes(staff.name)) { if (apt.notes.includes(staff.name)) {
staffId = staff.id; staffId = staff.id;
console.log(`Found staff in notes: ${staff.name} (${staffId})`);
break; break;
} }
} }
} }
// If no match in notes, check title // If no match in notes, check title
if (staffId === 'doctor1' && apt.title) { if (staffId === "doctor1" && apt.title) {
console.log("Checking title:", apt.title);
for (const staff of staffMembers) { for (const staff of staffMembers) {
if (apt.title.includes(staff.name)) { if (apt.title.includes(staff.name)) {
staffId = staff.id; staffId = staff.id;
console.log(`Found staff in title: ${staff.name} (${staffId})`);
break; break;
} }
} }
} }
console.log(`Final staffId assigned: ${staffId}`);
const processed = { const processed = {
...apt, ...apt,
patientName, patientName,
@@ -402,7 +452,6 @@ export default function AppointmentsPage() {
date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d
}; };
console.log("Processed appointment:", processed);
return processed; return processed;
}); });
@@ -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,
@@ -28,73 +37,100 @@ 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 = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true, id: true,
createdAt: true, createdAt: true,
}).partial(); })
.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 = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true, id: true,
createdAt: true, createdAt: true,
userId: true, userId: true,
}).partial(); })
.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={
!currentPatient && extractedInfo
? {
firstName: extractedInfo.firstName || "", firstName: extractedInfo.firstName || "",
lastName: extractedInfo.lastName || "", lastName: extractedInfo.lastName || "",
dateOfBirth: extractedInfo.dateOfBirth || "", dateOfBirth: extractedInfo.dateOfBirth || "",
insuranceId: extractedInfo.insuranceId || "" insuranceId: extractedInfo.insuranceId || "",
} : undefined} }
: 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,6 +8,7 @@ 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`
@@ -18,8 +19,6 @@ 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
@@ -56,10 +55,11 @@ 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"
@@ -67,4 +67,15 @@ model Appointment {
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();
});