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;
@@ -84,55 +76,77 @@ interface AppointmentFormProps {
isLoading?: boolean; isLoading?: boolean;
} }
export function AppointmentForm({ 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,342 +155,384 @@ export function AppointmentForm({
endTime: "09:30", endTime: "09:30",
type: "checkup", type: "checkup",
status: "scheduled", status: "scheduled",
staff: "doctor1", staffId: staffMembers?.[0]?.id ?? undefined,
}; };
const form = useForm<AppointmentFormValues>({ const form = useForm<InsertAppointment>({
resolver: zodResolver(appointmentSchema), resolver: zodResolver(insertAppointmentSchema),
defaultValues, defaultValues,
}); });
// Force form field values to update and clean up storage // Force form field values to update and clean up storage
useEffect(() => { useEffect(() => {
if (parsedStoredData) { if (parsedStoredData) {
// Force-update the form with the stored values
console.log("Force updating form fields with:", parsedStoredData);
// Update form field values directly // Update form field values directly
if (parsedStoredData.startTime) { if (parsedStoredData.startTime) {
form.setValue('startTime', parsedStoredData.startTime); form.setValue("startTime", parsedStoredData.startTime);
console.log(`Setting startTime to: ${parsedStoredData.startTime}`);
} }
if (parsedStoredData.endTime) { if (parsedStoredData.endTime) {
form.setValue('endTime', parsedStoredData.endTime); form.setValue("endTime", parsedStoredData.endTime);
console.log(`Setting endTime to: ${parsedStoredData.endTime}`);
} }
if (parsedStoredData.staff) { if (parsedStoredData.staff) {
form.setValue('staff', parsedStoredData.staff); form.setValue("staffId", parsedStoredData.staff);
} }
if (parsedStoredData.date) { if (parsedStoredData.date) {
form.setValue('date', new Date(parsedStoredData.date)); form.setValue("date", new Date(parsedStoredData.date));
} }
// Clean up session storage // Clean up session storage
sessionStorage.removeItem('newAppointmentData'); sessionStorage.removeItem("newAppointmentData");
} }
}, [form]); }, [form]);
const handleSubmit = (data: AppointmentFormValues) => { const handleSubmit = (data: InsertAppointment) => {
// Convert date to string format for the API and ensure patientId is properly parsed as a number
console.log("Form data before submission:", data);
// Make sure patientId is a number // Make sure patientId is a number
const patientId = typeof data.patientId === 'string' const patientId =
? parseInt(data.patientId, 10) typeof data.patientId === "string"
: data.patientId; ? parseInt(data.patientId, 10)
: data.patientId;
// Get patient name for the title // Get patient name for the title
const patient = patients.find(p => p.id === patientId); const patient = patients.find((p) => p.id === patientId);
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Patient'; const patientName = patient
? `${patient.firstName} ${patient.lastName}`
: "Patient";
// Auto-create title if it's empty // Auto-create title if it's empty
let title = data.title; let title = data.title;
if (!title || title.trim() === '') { if (!title || title.trim() === "") {
// Format: "April 19" - just the date // Format: "April 19" - just the date
title = format(data.date, 'MMMM d'); title = format(data.date, "MMMM d");
} }
// Make sure notes include staff information (needed for appointment display in columns) let notes = data.notes || "";
let notes = data.notes || '';
const selectedStaff =
// Get the selected staff member staffMembers.find((staff) => staff.id?.toString() === data.staffId) ||
const selectedStaff = staffMembers.find(staff => staff.id === data.staff) || staffMembers[0]; staffMembers[0];
if (!selectedStaff) {
console.error("No staff selected and no available staff in the list");
return; // Handle this case as well
}
// If there's no staff information in the notes, add it // If there's no staff information in the notes, add it
if (!notes.includes('Appointment with')) { if (!notes.includes("Appointment with")) {
notes = notes ? `${notes}\nAppointment with ${selectedStaff?.name}` : `Appointment with ${selectedStaff?.name}`; notes = notes
? `${notes}\nAppointment with ${selectedStaff?.name}`
: `Appointment with ${selectedStaff?.name}`;
} }
// 👇 Use current date if none provided
const appointmentDate = data.date ? new Date(data.date) : new Date();
if (isNaN(appointmentDate.getTime())) {
console.error("Invalid date:", data.date);
return;
}
onSubmit({ onSubmit({
...data, ...data,
title, title,
notes, notes,
patientId, // Ensure patientId is a number patientId,
date: format(data.date, 'yyyy-MM-dd'), date: format(appointmentDate, "yyyy-MM-dd"),
startTime: data.startTime,
endTime: data.endTime,
}); });
}; };
return ( return (
<Form {...form}> <div className="form-container">
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> <Form {...form}>
<FormField <form
control={form.control} onSubmit={form.handleSubmit(
name="patientId" (data) => {
render={({ field }) => ( handleSubmit(data);
<FormItem> },
<FormLabel>Patient</FormLabel> (errors) => {
<Select console.error("Validation failed:", errors);
disabled={isLoading} }
onValueChange={field.onChange}
value={field.value?.toString()}
defaultValue={field.value?.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a patient" />
</SelectTrigger>
</FormControl>
<SelectContent>
{patients.map((patient) => (
<SelectItem key={patient.id} value={patient.id.toString()}>
{patient.firstName} {patient.lastName}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} )}
/> className="space-y-6"
>
<FormField <FormField
control={form.control} control={form.control}
name="title" name="patientId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Appointment Title <span className="text-muted-foreground text-xs">(optional)</span></FormLabel> <FormLabel>Patient</FormLabel>
<FormControl> <Select
<Input
placeholder="Leave blank to auto-fill with date"
{...field}
disabled={isLoading} disabled={isLoading}
/> onValueChange={(val) => field.onChange(Number(val))}
</FormControl> value={field.value?.toString()}
<FormMessage /> defaultValue={field.value?.toString()}
</FormItem> >
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl> <FormControl>
<Button <SelectTrigger>
variant={"outline"} <SelectValue placeholder="Select a patient" />
className={cn( </SelectTrigger>
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
disabled={isLoading}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl> </FormControl>
</PopoverTrigger> <SelectContent>
<PopoverContent className="w-auto p-0" align="start"> {patients.map((patient) => (
<Calendar <SelectItem
mode="single" key={patient.id}
selected={field.value} value={patient.id.toString()}
onSelect={field.onChange} >
disabled={(date) => {patient.firstName} {patient.lastName}
date < new Date(new Date().setHours(0, 0, 0, 0)) </SelectItem>
} ))}
initialFocus </SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
Appointment Title{" "}
<span className="text-muted-foreground text-xs">
(optional)
</span>
</FormLabel>
<FormControl>
<Input
placeholder="Leave blank to auto-fill with date"
{...field}
disabled={isLoading}
/> />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel>Start Time</FormLabel>
<FormControl>
<div className="relative">
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="09:00"
{...field}
disabled={isLoading}
className="pl-10"
/>
</div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="endTime" name="date"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="flex flex-col">
<FormLabel>End Time</FormLabel> <FormLabel>Date</FormLabel>
<FormControl> <Popover>
<div className="relative"> <PopoverTrigger asChild>
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> <FormControl>
<Input <Button
placeholder="09:30" variant={"outline"}
{...field} className={cn(
disabled={isLoading} "w-full pl-3 text-left font-normal",
className="pl-10" !field.value && "text-muted-foreground"
)}
disabled={isLoading}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value ? new Date(field.value) : undefined}
onSelect={field.onChange}
disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0))
}
initialFocus
/> />
</div> </PopoverContent>
</FormControl> </Popover>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div>
<div className="grid grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
name="type" name="startTime"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Appointment Type</FormLabel> <FormLabel>Start Time</FormLabel>
<Select <FormControl>
disabled={isLoading} <div className="relative">
onValueChange={field.onChange} <Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
value={field.value} <Input
defaultValue={field.value} placeholder="09:00"
> {...field}
<FormControl> disabled={isLoading}
<SelectTrigger> className="pl-10"
<SelectValue placeholder="Select a type" /> value={
</SelectTrigger> typeof field.value === "string" ? field.value : ""
</FormControl> }
<SelectContent> />
<SelectItem value="checkup">Checkup</SelectItem> </div>
<SelectItem value="cleaning">Cleaning</SelectItem> </FormControl>
<SelectItem value="filling">Filling</SelectItem> <FormMessage />
<SelectItem value="extraction">Extraction</SelectItem> </FormItem>
<SelectItem value="root-canal">Root Canal</SelectItem> )}
<SelectItem value="crown">Crown</SelectItem> />
<SelectItem value="dentures">Dentures</SelectItem>
<SelectItem value="consultation">Consultation</SelectItem> <FormField
<SelectItem value="emergency">Emergency</SelectItem> control={form.control}
<SelectItem value="other">Other</SelectItem> name="endTime"
</SelectContent> render={({ field }) => (
</Select> <FormItem>
<FormMessage /> <FormLabel>End Time</FormLabel>
</FormItem> <FormControl>
)} <div className="relative">
/> <Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
<FormField placeholder="09:30"
control={form.control} {...field}
name="status" disabled={isLoading}
render={({ field }) => ( className="pl-10"
<FormItem> value={
<FormLabel>Status</FormLabel> typeof field.value === "string" ? field.value : ""
<Select }
disabled={isLoading} />
onValueChange={field.onChange} </div>
value={field.value} </FormControl>
defaultValue={field.value} <FormMessage />
> </FormItem>
<FormControl> )}
<SelectTrigger> />
<SelectValue placeholder="Select a status" /> </div>
</SelectTrigger>
</FormControl> <FormField
<SelectContent> control={form.control}
<SelectItem value="scheduled">Scheduled</SelectItem> name="type"
<SelectItem value="confirmed">Confirmed</SelectItem> render={({ field }) => (
<SelectItem value="completed">Completed</SelectItem> <FormItem>
<SelectItem value="cancelled">Cancelled</SelectItem> <FormLabel>Appointment Type</FormLabel>
<SelectItem value="no-show">No Show</SelectItem> <Select
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="staff"
render={({ field }) => (
<FormItem>
<FormLabel>Doctor/Hygienist</FormLabel>
<Select
disabled={isLoading}
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select staff member" />
</SelectTrigger>
</FormControl>
<SelectContent>
{staffMembers.map((staff) => (
<SelectItem key={staff.id} value={staff.id}>
{staff.name} ({staff.role})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea
placeholder="Enter any notes about the appointment"
{...field}
disabled={isLoading} disabled={isLoading}
className="min-h-24" onValueChange={field.onChange}
/> value={field.value}
</FormControl> defaultValue={field.value}
<FormMessage /> >
</FormItem> <FormControl>
)} <SelectTrigger>
/> <SelectValue placeholder="Select a type" />
</SelectTrigger>
<Button type="submit" disabled={isLoading} className="w-full"> </FormControl>
{appointment ? "Update Appointment" : "Create Appointment"} <SelectContent>
</Button> <SelectItem value="checkup">Checkup</SelectItem>
</form> <SelectItem value="cleaning">Cleaning</SelectItem>
</Form> <SelectItem value="filling">Filling</SelectItem>
<SelectItem value="extraction">Extraction</SelectItem>
<SelectItem value="root-canal">Root Canal</SelectItem>
<SelectItem value="crown">Crown</SelectItem>
<SelectItem value="dentures">Dentures</SelectItem>
<SelectItem value="consultation">Consultation</SelectItem>
<SelectItem value="emergency">Emergency</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select
disabled={isLoading}
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="scheduled">Scheduled</SelectItem>
<SelectItem value="confirmed">Confirmed</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="no-show">No Show</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="staffId"
render={({ field }) => (
<FormItem>
<FormLabel>Doctor/Hygienist</FormLabel>
<Select
disabled={isLoading}
onValueChange={(val) => field.onChange(Number(val))}
value={field.value ? String(field.value) : undefined}
defaultValue={field.value ? String(field.value) : undefined}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select staff member" />
</SelectTrigger>
</FormControl>
<SelectContent>
{staffMembers.map((staff) => (
<SelectItem
key={staff.id}
value={staff.id?.toString() || ""}
>
{staff.name} ({staff.role})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea
placeholder="Enter any notes about the appointment"
{...field}
disabled={isLoading}
className="min-h-24"
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading} className="w-full">
{appointment ? "Update Appointment" : "Create Appointment"}
</Button>
</form>
</Form>
</div>
); );
} }

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
// import { insertPatientSchema, InsertPatient, Patient, updatePatientSchema, UpdatePatient } from "@repo/db/shared/schemas";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { import {
Form, Form,
@@ -13,7 +12,6 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -21,27 +19,36 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { useEffect, useMemo } from "react";
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertPatientSchema = (
id: true, PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, ).omit({
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
id: true, id: true,
createdAt: true, createdAt: true,
userId: true, userId: true,
}).partial(); });
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface PatientFormProps { interface PatientFormProps {
patient?: Patient; patient?: Patient;
extractedInfo?: { extractedInfo?: {
@@ -53,50 +60,103 @@ interface PatientFormProps {
onSubmit: (data: InsertPatient | UpdatePatient) => void; onSubmit: (data: InsertPatient | UpdatePatient) => void;
} }
export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormProps) { export type PatientFormRef = {
submit: () => void;
};
export function PatientForm({
patient,
extractedInfo,
onSubmit,
}: PatientFormProps) {
const { user } = useAuth(); const { user } = useAuth();
const isEditing = !!patient; const isEditing = !!patient;
const schema = isEditing ? updatePatientSchema : insertPatientSchema.extend({
userId: z.number().optional(),
});
// Merge extracted info into default values if available const schema = useMemo(
const defaultValues = { () =>
firstName: extractedInfo?.firstName || "", isEditing
lastName: extractedInfo?.lastName || "", ? updatePatientSchema
dateOfBirth: extractedInfo?.dateOfBirth || "", : insertPatientSchema.extend({ userId: z.number().optional() }),
gender: "", [isEditing]
phone: "", );
email: "",
address: "", const computedDefaultValues = useMemo(() => {
city: "", if (isEditing && patient) {
zipCode: "", const { id, userId, createdAt, ...sanitizedPatient } = patient;
insuranceProvider: "", return {
insuranceId: extractedInfo?.insuranceId || "", ...sanitizedPatient,
groupNumber: "", dateOfBirth: patient.dateOfBirth
policyHolder: "", ? new Date(patient.dateOfBirth).toISOString().split("T")[0]
allergies: "", : "",
medicalConditions: "", };
status: "active", }
userId: user?.id,
}; return {
firstName: extractedInfo?.firstName || "",
lastName: extractedInfo?.lastName || "",
dateOfBirth: extractedInfo?.dateOfBirth || "",
gender: "",
phone: "",
email: "",
address: "",
city: "",
zipCode: "",
insuranceProvider: "",
insuranceId: extractedInfo?.insuranceId || "",
groupNumber: "",
policyHolder: "",
allergies: "",
medicalConditions: "",
status: "active",
userId: user?.id,
};
}, [isEditing, patient, extractedInfo, user?.id]);
const form = useForm<InsertPatient | UpdatePatient>({ const form = useForm<InsertPatient | UpdatePatient>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: patient || defaultValues, defaultValues: computedDefaultValues,
}); });
const handleSubmit = (data: InsertPatient | UpdatePatient) => { // Debug form errors
useEffect(() => {
const errors = form.formState.errors;
if (Object.keys(errors).length > 0) {
console.log("❌ Form validation errors:", errors);
}
}, [form.formState.errors]);
useEffect(() => {
if (patient) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
const resetValues: Partial<Patient> = {
...sanitizedPatient,
dateOfBirth: patient.dateOfBirth
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
: "",
};
form.reset(resetValues);
}
}, [patient, computedDefaultValues, form]);
const handleSubmit2 = (data: InsertPatient | UpdatePatient) => {
onSubmit(data); onSubmit(data);
}; };
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> <form
id="patient-form"
key={patient?.id || "new"}
onSubmit={form.handleSubmit((data) => {
handleSubmit2(data);
})}
className="space-y-6"
>
{/* Personal Information */} {/* Personal Information */}
<div> <div>
<h4 className="text-md font-medium text-gray-700 mb-3">Personal Information</h4> <h4 className="text-md font-medium text-gray-700 mb-3">
Personal Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -111,7 +171,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="lastName" name="lastName"
@@ -125,7 +185,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="dateOfBirth" name="dateOfBirth"
@@ -139,15 +199,15 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="gender" name="gender"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Gender *</FormLabel> <FormLabel>Gender *</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value as string} defaultValue={field.value as string}
> >
<FormControl> <FormControl>
@@ -167,10 +227,12 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
/> />
</div> </div>
</div> </div>
{/* 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}
@@ -185,7 +247,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
@@ -193,13 +255,13 @@ 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>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="address" name="address"
@@ -207,13 +269,13 @@ 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>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="city" name="city"
@@ -221,13 +283,13 @@ 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>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="zipCode" name="zipCode"
@@ -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>
@@ -243,10 +305,12 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
/> />
</div> </div>
</div> </div>
{/* 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}
@@ -254,9 +318,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<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>
@@ -277,7 +343,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="insuranceId" name="insuranceId"
@@ -285,13 +351,13 @@ 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>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="groupNumber" name="groupNumber"
@@ -299,13 +365,13 @@ 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>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="policyHolder" name="policyHolder"
@@ -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>
@@ -321,7 +387,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
/> />
</div> </div>
</div> </div>
{/* Hidden submit button for form validation */} {/* Hidden submit button for form validation */}
<button type="submit" className="hidden" aria-hidden="true"></button> <button type="submit" className="hidden" aria-hidden="true"></button>
</form> </form>

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7,14 +7,23 @@ import { StatCard } from "@/components/ui/stat-card";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
import { AddPatientModal } from "@/components/patients/add-patient-modal"; import { AddPatientModal } from "@/components/patients/add-patient-modal";
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal"; import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; import {
// import { InsertPatient, Patient, UpdatePatient, Appointment, InsertAppointment, UpdateAppointment } from "@repo/db/shared/schemas"; AppointmentUncheckedCreateInputObjectSchema,
import { Users, Calendar, CheckCircle, CreditCard, Plus, Clock } from "lucide-react"; PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/shared/schemas";
import {
Users,
Calendar,
CheckCircle,
CreditCard,
Plus,
Clock,
} from "lucide-react";
import { Link } from "wouter"; import { Link } from "wouter";
import { import {
Dialog, Dialog,
@@ -23,78 +32,105 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {z} from "zod"; import { z } from "zod";
//creating types out of schema auto generated. //creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true, id: true,
createdAt: true, createdAt: true,
}); });
type InsertAppointment = z.infer<typeof insertAppointmentSchema>; type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const updateAppointmentSchema = (
id: true, AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, )
}).partial(); .omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>; type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true, appointments: true,
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true, id: true,
createdAt: true, createdAt: true,
}); });
type InsertPatient = z.infer<typeof insertPatientSchema>; type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ const updatePatientSchema = (
id: true, PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
createdAt: true, )
userId: true, .omit({
}).partial(); id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
export default function Dashboard() { export default function Dashboard() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false); const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false); const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined); const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | undefined>(undefined); undefined
);
const [selectedAppointment, setSelectedAppointment] = useState<
Appointment | undefined
>(undefined);
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
// Fetch patients // Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({ const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
queryKey: ["/api/patients"], Patient[]
>({
queryKey: ["/api/patients/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/patients/");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Fetch appointments // Fetch appointments
const { const {
data: appointments = [] as Appointment[], data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments isLoading: isLoadingAppointments,
} = useQuery<Appointment[]>({ } = useQuery<Appointment[]>({
queryKey: ["/api/appointments"], queryKey: ["/api/appointments/all"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/appointments/all");
return res.json();
},
enabled: !!user, enabled: !!user,
}); });
// Add patient mutation // Add patient mutation
const addPatientMutation = useMutation({ const addPatientMutation = useMutation({
mutationFn: async (patient: InsertPatient) => { mutationFn: async (patient: InsertPatient) => {
const res = await apiRequest("POST", "/api/patients", patient); const res = await apiRequest("POST", "/api/patients/", patient);
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients"] }); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient added successfully!", description: "Patient added successfully!",
@@ -112,13 +148,19 @@ export default function Dashboard() {
// Update patient mutation // Update patient mutation
const updatePatientMutation = useMutation({ const updatePatientMutation = useMutation({
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => { mutationFn: async ({
id,
patient,
}: {
id: number;
patient: UpdatePatient;
}) => {
const res = await apiRequest("PUT", `/api/patients/${id}`, patient); const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients"] }); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient updated successfully!", description: "Patient updated successfully!",
@@ -142,14 +184,25 @@ export default function Dashboard() {
if (user) { if (user) {
addPatientMutation.mutate({ addPatientMutation.mutate({
...patient, ...patient,
userId: user.id userId: user.id,
}); });
} }
}; };
const handleUpdatePatient = (patient: UpdatePatient) => { const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
if (currentPatient) { if (currentPatient && user) {
updatePatientMutation.mutate({ id: currentPatient.id, patient }); const { id, ...sanitizedPatient } = patient;
updatePatientMutation.mutate({
id: currentPatient.id,
patient: sanitizedPatient,
});
} else {
console.error("No current patient or user found for update");
toast({
title: "Error",
description: "Cannot update patient: No patient or user found",
variant: "destructive",
});
} }
}; };
@@ -162,11 +215,11 @@ export default function Dashboard() {
setCurrentPatient(patient); setCurrentPatient(patient);
setIsViewPatientOpen(true); setIsViewPatientOpen(true);
}; };
// 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({
@@ -185,11 +240,21 @@ 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({
@@ -208,10 +275,12 @@ 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,27 +297,28 @@ 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 today = format(new Date(), 'yyyy-MM-dd'); const todaysAppointments = appointments.filter((appointment) => {
const appointmentDate = format(new Date(appointment.date), "yyyy-MM-dd");
// Filter appointments for today return appointmentDate === 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} />
<main className="flex-1 overflow-y-auto p-4"> <main className="flex-1 overflow-y-auto p-4">
{/* Quick Stats */} {/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
@@ -281,9 +351,11 @@ 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">
<Button Today's Appointments
className="mt-2 md:mt-0" </h2>
<Button
className="mt-2 md:mt-0"
onClick={() => { onClick={() => {
setSelectedAppointment(undefined); setSelectedAppointment(undefined);
setIsAddAppointmentOpen(true); setIsAddAppointmentOpen(true);
@@ -293,39 +365,76 @@ export default function Dashboard() {
New Appointment New Appointment
</Button> </Button>
</div> </div>
<Card> <Card>
<CardContent className="p-0"> <CardContent className="p-0">
{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>
@@ -355,14 +466,16 @@ export default function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Patient Management Section */} {/* Patient Management Section */}
<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">
<Button Patient Management
className="mt-2 md:mt-0" </h2>
<Button
className="mt-2 md:mt-0"
onClick={() => { onClick={() => {
setCurrentPatient(undefined); setCurrentPatient(undefined);
setIsAddPatientOpen(true); setIsAddPatientOpen(true);
@@ -373,13 +486,11 @@ export default function Dashboard() {
</Button> </Button>
</div> </div>
{/* Search and filters removed */}
{/* Patient Table */} {/* Patient Table */}
<PatientTable <PatientTable
patients={filteredPatients} patients={filteredPatients}
onEdit={handleEditPatient} onEdit={handleEditPatient}
onView={handleViewPatient} onView={handleViewPatient}
/> />
</div> </div>
</main> </main>
@@ -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}
/> />
@@ -403,120 +516,138 @@ export default function Dashboard() {
Complete information about the patient. Complete information about the patient.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{currentPatient && ( {currentPatient && (
<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>
</div> </div>
<div> <div>
<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>
</div> </div>
<div className="flex justify-end space-x-2 pt-4"> <div className="flex justify-end space-x-2 pt-4">
<Button <Button
variant="outline" variant="outline"
onClick={() => setIsViewPatientOpen(false)} onClick={() => setIsViewPatientOpen(false)}
> >
Close Close
@@ -534,13 +665,16 @@ export default function Dashboard() {
)} )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Add/Edit Appointment Modal */} {/* Add/Edit Appointment Modal */}
<AddAppointmentModal <AddAppointmentModal
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,11 +64,15 @@ 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
const [uploadedFile, setUploadedFile] = useState<File | null>(null); const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
@@ -65,25 +85,29 @@ 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!",
variant: "default", variant: "default",
}); });
// If the add patient modal wants to proceed to scheduling, redirect to appointments page // If the add patient modal wants to proceed to scheduling, redirect to appointments page
if (addPatientModalRef.current?.shouldSchedule) { if (addPatientModalRef.current?.shouldSchedule) {
addPatientModalRef.current.navigateToSchedule(newPatient.id); addPatientModalRef.current.navigateToSchedule(newPatient.id);
@@ -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,29 +193,32 @@ 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) => {
setSearchCriteria(criteria); setSearchCriteria(criteria);
}; };
const handleClearSearch = () => { const handleClearSearch = () => {
setSearchCriteria(null); setSearchCriteria(null);
}; };
// File upload handling // File upload handling
const handleFileUpload = (file: File) => { const handleFileUpload = (file: File) => {
setUploadedFile(file); setUploadedFile(file);
setIsUploading(false); // In a real implementation, this would be set to true during upload setIsUploading(false); // In a real implementation, this would be set to true during upload
toast({ toast({
title: "File Selected", title: "File Selected",
description: `${file.name} is ready for processing.`, description: `${file.name} is ready for processing.`,
variant: "default", variant: "default",
}); });
}; };
// Process file and extract patient information // Process file and extract patient information
const handleExtractInfo = async () => { const handleExtractInfo = async () => {
if (!uploadedFile) { if (!uploadedFile) {
@@ -185,79 +229,81 @@ export default function PatientsPage() {
}); });
return; return;
} }
setIsExtracting(true); setIsExtracting(true);
try { try {
// Read the file as base64 // Read the file as base64
const reader = new FileReader(); const reader = new FileReader();
// 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)
reader.readAsDataURL(uploadedFile); reader.readAsDataURL(uploadedFile);
}); });
// Get the base64 data // Get the base64 data
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();
if (data.success) { if (data.success) {
// Only keep firstName, lastName, dateOfBirth, and insuranceId from the extracted info // Only keep firstName, lastName, dateOfBirth, and insuranceId from the extracted info
const simplifiedInfo = { const simplifiedInfo = {
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);
// 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",
}); });
// Open patient form pre-filled with extracted data // Open patient form pre-filled with extracted data
setCurrentPatient(undefined); setCurrentPatient(undefined);
// Pre-fill the form by opening the modal with the extracted information // Pre-fill the form by opening the modal with the extracted information
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,35 +311,38 @@ 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 {
setIsExtracting(false); setIsExtracting(false);
} }
}; };
// Filter patients based on search criteria // Filter patients based on search criteria
const filteredPatients = useMemo(() => { const filteredPatients = useMemo(() => {
if (!searchCriteria || !searchCriteria.searchTerm) { if (!searchCriteria || !searchCriteria.searchTerm) {
return patients; return patients;
} }
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,11 +360,14 @@ 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} />
<main className="flex-1 overflow-y-auto p-4"> <main className="flex-1 overflow-y-auto p-4">
<div className="container mx-auto space-y-6"> <div className="container mx-auto space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -361,7 +413,7 @@ export default function PatientsPage() {
<File className="h-4 w-4 text-muted-foreground" /> <File className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<FileUploadZone <FileUploadZone
onFileUpload={handleFileUpload} onFileUpload={handleFileUpload}
isUploading={isUploading} isUploading={isUploading}
acceptedFileTypes="application/pdf" acceptedFileTypes="application/pdf"
@@ -370,9 +422,9 @@ export default function PatientsPage() {
</Card> </Card>
</div> </div>
<div className="md:col-span-1 flex items-end"> <div className="md:col-span-1 flex items-end">
<Button <Button
className="w-full h-12 gap-2" className="w-full h-12 gap-2"
onClick={handleExtractInfo} onClick={handleExtractInfo}
disabled={!uploadedFile || isExtracting} disabled={!uploadedFile || isExtracting}
> >
{isExtracting ? ( {isExtracting ? (
@@ -399,23 +451,25 @@ export default function PatientsPage() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PatientSearch <PatientSearch
onSearch={handleSearch} onSearch={handleSearch}
onClearSearch={handleClearSearch} onClearSearch={handleClearSearch}
isSearchActive={!!searchCriteria} isSearchActive={!!searchCriteria}
/> />
{searchCriteria && ( {searchCriteria && (
<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>
)} )}
<PatientTable <PatientTable
patients={filteredPatients} patients={filteredPatients}
onEdit={handleEditPatient} onEdit={handleEditPatient}
@@ -433,16 +487,20 @@ export default function PatientsPage() {
isLoading={isLoading} isLoading={isLoading}
patient={currentPatient} patient={currentPatient}
// Pass extracted info as a separate prop to avoid triggering edit mode // Pass extracted info as a separate prop to avoid triggering edit mode
extractedInfo={!currentPatient && extractedInfo ? { extractedInfo={
firstName: extractedInfo.firstName || "", !currentPatient && extractedInfo
lastName: extractedInfo.lastName || "", ? {
dateOfBirth: extractedInfo.dateOfBirth || "", firstName: extractedInfo.firstName || "",
insuranceId: extractedInfo.insuranceId || "" lastName: extractedInfo.lastName || "",
} : undefined} dateOfBirth: extractedInfo.dateOfBirth || "",
insuranceId: extractedInfo.insuranceId || "",
}
: undefined
}
/> />
</div> </div>
</main> </main>
</div> </div>
</div> </div>
); );
} }

1497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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