major functionalities are fixed
This commit is contained in:
5
apps/Backend/.env.example
Normal file
5
apps/Backend/.env.example
Normal 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
40
apps/Backend/package.json
Normal 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
36
apps/Backend/src/app.ts
Normal 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
11
apps/Backend/src/index.ts
Normal 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}`);
|
||||||
|
});
|
||||||
26
apps/Backend/src/middlewares/auth.middleware.ts
Normal file
26
apps/Backend/src/middlewares/auth.middleware.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
6
apps/Backend/src/middlewares/error.middleware.ts
Normal file
6
apps/Backend/src/middlewares/error.middleware.ts
Normal 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' });
|
||||||
|
};
|
||||||
33
apps/Backend/src/middlewares/logger.middleware.ts
Normal file
33
apps/Backend/src/middlewares/logger.middleware.ts
Normal 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();
|
||||||
|
}
|
||||||
351
apps/Backend/src/routes/appointements.ts
Normal file
351
apps/Backend/src/routes/appointements.ts
Normal 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;
|
||||||
87
apps/Backend/src/routes/auth.ts
Normal file
87
apps/Backend/src/routes/auth.ts
Normal 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;
|
||||||
14
apps/Backend/src/routes/index.ts
Normal file
14
apps/Backend/src/routes/index.ts
Normal 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;
|
||||||
252
apps/Backend/src/routes/patients.ts
Normal file
252
apps/Backend/src/routes/patients.ts
Normal 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;
|
||||||
77
apps/Backend/src/routes/staffs.ts
Normal file
77
apps/Backend/src/routes/staffs.ts
Normal 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;
|
||||||
108
apps/Backend/src/routes/users.ts
Normal file
108
apps/Backend/src/routes/users.ts
Normal 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;
|
||||||
284
apps/Backend/src/storage/index.ts
Normal file
284
apps/Backend/src/storage/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
10
apps/Backend/src/types/express.types.d.ts
vendored
Normal file
10
apps/Backend/src/types/express.types.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { User } from "@repo/db/client";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
// include any other properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/Backend/tsconfig.json
Normal file
10
apps/Backend/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir":"dist"
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
||||||
2
apps/Frontend/.env.example
Normal file
2
apps/Frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
|
|
||||||
@@ -2,38 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
// import { InsertAppointment, UpdateAppointment, Appointment, Patient } from "@repo/db/shared/schemas";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
|
||||||
|
|
||||||
import {z} from "zod";
|
|
||||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
|
||||||
|
|
||||||
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
});
|
|
||||||
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
|
||||||
|
|
||||||
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
}).partial();
|
|
||||||
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
|
||||||
|
|
||||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
|
||||||
appointments: true,
|
|
||||||
});
|
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
|
||||||
|
|
||||||
|
|
||||||
// Define staff members (should match those in appointments-page.tsx)
|
|
||||||
const staffMembers = [
|
|
||||||
{ id: "doctor1", name: "Dr. Kai Gao", role: "doctor" },
|
|
||||||
{ id: "doctor2", name: "Dr. Jane Smith", role: "doctor" },
|
|
||||||
{ id: "hygienist1", name: "Hygienist One", role: "hygienist" },
|
|
||||||
{ id: "hygienist2", name: "Hygienist Two", role: "hygienist" },
|
|
||||||
{ id: "hygienist3", name: "Hygienist Three", role: "hygienist" },
|
|
||||||
];
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -53,29 +22,52 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { CalendarIcon, Clock } from "lucide-react";
|
import { CalendarIcon, Clock } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
|
||||||
const appointmentSchema = z.object({
|
import {
|
||||||
patientId: z.coerce.number().positive(),
|
AppointmentUncheckedCreateInputObjectSchema,
|
||||||
title: z.string().optional(),
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
date: z.date({
|
StaffUncheckedCreateInputObjectSchema,
|
||||||
required_error: "Appointment date is required",
|
} from "@repo/db/shared/schemas";
|
||||||
}),
|
|
||||||
startTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
|
import { z } from "zod";
|
||||||
message: "Start time must be in format HH:MM",
|
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||||
}),
|
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
|
||||||
endTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
|
|
||||||
message: "End time must be in format HH:MM",
|
const insertAppointmentSchema = (
|
||||||
}),
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
type: z.string().min(1, "Appointment type is required"),
|
).omit({
|
||||||
notes: z.string().optional(),
|
id: true,
|
||||||
status: z.string().default("scheduled"),
|
createdAt: true,
|
||||||
staff: z.string().default(staffMembers?.[0]?.id ?? "default-id"),
|
userId: true,
|
||||||
});
|
});
|
||||||
|
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
||||||
|
|
||||||
export type AppointmentFormValues = z.infer<typeof appointmentSchema>;
|
const updateAppointmentSchema = (
|
||||||
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
)
|
||||||
|
.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
||||||
|
|
||||||
|
const PatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
|
appointments: true,
|
||||||
|
});
|
||||||
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
interface AppointmentFormProps {
|
interface AppointmentFormProps {
|
||||||
appointment?: Appointment;
|
appointment?: Appointment;
|
||||||
@@ -88,51 +80,73 @@ export function AppointmentForm({
|
|||||||
appointment,
|
appointment,
|
||||||
patients,
|
patients,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isLoading = false
|
isLoading = false,
|
||||||
}: AppointmentFormProps) {
|
}: AppointmentFormProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
|
||||||
|
useQuery<Staff[]>({
|
||||||
|
queryKey: ["/api/staffs/"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/staffs/");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
"Dr. Kai Gao": "bg-blue-600",
|
||||||
|
"Dr. Jane Smith": "bg-emerald-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
const staffMembers = staffMembersRaw.map((staff) => ({
|
||||||
|
...staff,
|
||||||
|
color: colorMap[staff.name] || "bg-gray-400",
|
||||||
|
}));
|
||||||
|
|
||||||
// Get the stored data from session storage
|
// Get the stored data from session storage
|
||||||
const storedDataString = sessionStorage.getItem('newAppointmentData');
|
const storedDataString = sessionStorage.getItem("newAppointmentData");
|
||||||
let parsedStoredData = null;
|
let parsedStoredData = null;
|
||||||
|
|
||||||
// Try to parse it if it exists
|
// Try to parse it if it exists
|
||||||
if (storedDataString) {
|
if (storedDataString) {
|
||||||
try {
|
try {
|
||||||
parsedStoredData = JSON.parse(storedDataString);
|
parsedStoredData = JSON.parse(storedDataString);
|
||||||
console.log('Initial appointment data from storage:', parsedStoredData);
|
|
||||||
|
|
||||||
// LOG the specific time values for debugging
|
|
||||||
console.log('Time values in stored data:', {
|
|
||||||
startTime: parsedStoredData.startTime,
|
|
||||||
endTime: parsedStoredData.endTime
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing stored appointment data:', error);
|
console.error("Error parsing stored appointment data:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the date and times for the form
|
// Format the date and times for the form
|
||||||
const defaultValues: Partial<AppointmentFormValues> = appointment
|
const defaultValues: Partial<Appointment> = appointment
|
||||||
? {
|
? {
|
||||||
patientId: appointment.patientId,
|
patientId: appointment.patientId,
|
||||||
title: appointment.title,
|
title: appointment.title,
|
||||||
date: new Date(appointment.date),
|
date: new Date(appointment.date),
|
||||||
startTime: typeof appointment.startTime === 'string' ? appointment.startTime.slice(0, 5) : "",
|
startTime: appointment.startTime || "09:00", // Default "09:00"
|
||||||
endTime: typeof appointment.endTime === 'string' ? appointment.endTime.slice(0, 5) : "",
|
endTime: appointment.endTime || "09:30", // Default "09:30"
|
||||||
type: appointment.type,
|
type: appointment.type,
|
||||||
notes: appointment.notes || "",
|
notes: appointment.notes || "",
|
||||||
status: appointment.status || "scheduled",
|
status: appointment.status || "scheduled",
|
||||||
|
staffId:
|
||||||
|
typeof appointment.staffId === "number"
|
||||||
|
? appointment.staffId
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
: parsedStoredData
|
: parsedStoredData
|
||||||
? {
|
? {
|
||||||
patientId: parsedStoredData.patientId,
|
patientId: Number(parsedStoredData.patientId),
|
||||||
date: new Date(parsedStoredData.date),
|
date: new Date(parsedStoredData.date),
|
||||||
title: parsedStoredData.title || "",
|
title: parsedStoredData.title || "",
|
||||||
startTime: parsedStoredData.startTime, // This should now be correctly applied
|
startTime: parsedStoredData.startTime,
|
||||||
endTime: parsedStoredData.endTime,
|
endTime: parsedStoredData.endTime,
|
||||||
type: parsedStoredData.type || "checkup",
|
type: parsedStoredData.type || "checkup",
|
||||||
status: parsedStoredData.status || "scheduled",
|
status: parsedStoredData.status || "scheduled",
|
||||||
notes: parsedStoredData.notes || "",
|
notes: parsedStoredData.notes || "",
|
||||||
staff: parsedStoredData.staff || (staffMembers?.[0]?.id ?? "default-id")
|
staffId:
|
||||||
|
typeof parsedStoredData.staff === "number"
|
||||||
|
? parsedStoredData.staff
|
||||||
|
: (staffMembers?.[0]?.id ?? undefined),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
@@ -141,195 +155,160 @@ export function AppointmentForm({
|
|||||||
endTime: "09:30",
|
endTime: "09:30",
|
||||||
type: "checkup",
|
type: "checkup",
|
||||||
status: "scheduled",
|
status: "scheduled",
|
||||||
staff: "doctor1",
|
staffId: staffMembers?.[0]?.id ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = useForm<AppointmentFormValues>({
|
const form = useForm<InsertAppointment>({
|
||||||
resolver: zodResolver(appointmentSchema),
|
resolver: zodResolver(insertAppointmentSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force form field values to update and clean up storage
|
// Force form field values to update and clean up storage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parsedStoredData) {
|
if (parsedStoredData) {
|
||||||
// Force-update the form with the stored values
|
|
||||||
console.log("Force updating form fields with:", parsedStoredData);
|
|
||||||
|
|
||||||
// Update form field values directly
|
// Update form field values directly
|
||||||
if (parsedStoredData.startTime) {
|
if (parsedStoredData.startTime) {
|
||||||
form.setValue('startTime', parsedStoredData.startTime);
|
form.setValue("startTime", parsedStoredData.startTime);
|
||||||
console.log(`Setting startTime to: ${parsedStoredData.startTime}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedStoredData.endTime) {
|
if (parsedStoredData.endTime) {
|
||||||
form.setValue('endTime', parsedStoredData.endTime);
|
form.setValue("endTime", parsedStoredData.endTime);
|
||||||
console.log(`Setting endTime to: ${parsedStoredData.endTime}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedStoredData.staff) {
|
if (parsedStoredData.staff) {
|
||||||
form.setValue('staff', parsedStoredData.staff);
|
form.setValue("staffId", parsedStoredData.staff);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedStoredData.date) {
|
if (parsedStoredData.date) {
|
||||||
form.setValue('date', new Date(parsedStoredData.date));
|
form.setValue("date", new Date(parsedStoredData.date));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up session storage
|
// Clean up session storage
|
||||||
sessionStorage.removeItem('newAppointmentData');
|
sessionStorage.removeItem("newAppointmentData");
|
||||||
}
|
}
|
||||||
}, [form]);
|
}, [form]);
|
||||||
|
|
||||||
const handleSubmit = (data: AppointmentFormValues) => {
|
const handleSubmit = (data: InsertAppointment) => {
|
||||||
// Convert date to string format for the API and ensure patientId is properly parsed as a number
|
|
||||||
console.log("Form data before submission:", data);
|
|
||||||
|
|
||||||
// Make sure patientId is a number
|
// Make sure patientId is a number
|
||||||
const patientId = typeof data.patientId === 'string'
|
const patientId =
|
||||||
? parseInt(data.patientId, 10)
|
typeof data.patientId === "string"
|
||||||
: data.patientId;
|
? parseInt(data.patientId, 10)
|
||||||
|
: data.patientId;
|
||||||
|
|
||||||
// Get patient name for the title
|
// Get patient name for the title
|
||||||
const patient = patients.find(p => p.id === patientId);
|
const patient = patients.find((p) => p.id === patientId);
|
||||||
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Patient';
|
const patientName = patient
|
||||||
|
? `${patient.firstName} ${patient.lastName}`
|
||||||
|
: "Patient";
|
||||||
|
|
||||||
// Auto-create title if it's empty
|
// Auto-create title if it's empty
|
||||||
let title = data.title;
|
let title = data.title;
|
||||||
if (!title || title.trim() === '') {
|
if (!title || title.trim() === "") {
|
||||||
// Format: "April 19" - just the date
|
// Format: "April 19" - just the date
|
||||||
title = format(data.date, 'MMMM d');
|
title = format(data.date, "MMMM d");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure notes include staff information (needed for appointment display in columns)
|
let notes = data.notes || "";
|
||||||
let notes = data.notes || '';
|
|
||||||
|
|
||||||
// Get the selected staff member
|
const selectedStaff =
|
||||||
const selectedStaff = staffMembers.find(staff => staff.id === data.staff) || staffMembers[0];
|
staffMembers.find((staff) => staff.id?.toString() === data.staffId) ||
|
||||||
|
staffMembers[0];
|
||||||
|
|
||||||
|
if (!selectedStaff) {
|
||||||
|
console.error("No staff selected and no available staff in the list");
|
||||||
|
return; // Handle this case as well
|
||||||
|
}
|
||||||
|
|
||||||
// If there's no staff information in the notes, add it
|
// If there's no staff information in the notes, add it
|
||||||
if (!notes.includes('Appointment with')) {
|
if (!notes.includes("Appointment with")) {
|
||||||
notes = notes ? `${notes}\nAppointment with ${selectedStaff?.name}` : `Appointment with ${selectedStaff?.name}`;
|
notes = notes
|
||||||
|
? `${notes}\nAppointment with ${selectedStaff?.name}`
|
||||||
|
: `Appointment with ${selectedStaff?.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 Use current date if none provided
|
||||||
|
const appointmentDate = data.date ? new Date(data.date) : new Date();
|
||||||
|
|
||||||
|
if (isNaN(appointmentDate.getTime())) {
|
||||||
|
console.error("Invalid date:", data.date);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
...data,
|
...data,
|
||||||
title,
|
title,
|
||||||
notes,
|
notes,
|
||||||
patientId, // Ensure patientId is a number
|
patientId,
|
||||||
date: format(data.date, 'yyyy-MM-dd'),
|
date: format(appointmentDate, "yyyy-MM-dd"),
|
||||||
|
startTime: data.startTime,
|
||||||
|
endTime: data.endTime,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<div className="form-container">
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
<Form {...form}>
|
||||||
<FormField
|
<form
|
||||||
control={form.control}
|
onSubmit={form.handleSubmit(
|
||||||
name="patientId"
|
(data) => {
|
||||||
render={({ field }) => (
|
handleSubmit(data);
|
||||||
<FormItem>
|
},
|
||||||
<FormLabel>Patient</FormLabel>
|
(errors) => {
|
||||||
<Select
|
console.error("Validation failed:", errors);
|
||||||
disabled={isLoading}
|
}
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value?.toString()}
|
|
||||||
defaultValue={field.value?.toString()}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a patient" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{patients.map((patient) => (
|
|
||||||
<SelectItem key={patient.id} value={patient.id.toString()}>
|
|
||||||
{patient.firstName} {patient.lastName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-6"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="patientId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Appointment Title <span className="text-muted-foreground text-xs">(optional)</span></FormLabel>
|
<FormLabel>Patient</FormLabel>
|
||||||
<FormControl>
|
<Select
|
||||||
<Input
|
|
||||||
placeholder="Leave blank to auto-fill with date"
|
|
||||||
{...field}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
onValueChange={(val) => field.onChange(Number(val))}
|
||||||
</FormControl>
|
value={field.value?.toString()}
|
||||||
<FormMessage />
|
defaultValue={field.value?.toString()}
|
||||||
</FormItem>
|
>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="date"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col">
|
|
||||||
<FormLabel>Date</FormLabel>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<SelectTrigger>
|
||||||
variant={"outline"}
|
<SelectValue placeholder="Select a patient" />
|
||||||
className={cn(
|
</SelectTrigger>
|
||||||
"w-full pl-3 text-left font-normal",
|
|
||||||
!field.value && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{field.value ? (
|
|
||||||
format(field.value, "PPP")
|
|
||||||
) : (
|
|
||||||
<span>Pick a date</span>
|
|
||||||
)}
|
|
||||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
<SelectContent>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
{patients.map((patient) => (
|
||||||
<Calendar
|
<SelectItem
|
||||||
mode="single"
|
key={patient.id}
|
||||||
selected={field.value}
|
value={patient.id.toString()}
|
||||||
onSelect={field.onChange}
|
>
|
||||||
disabled={(date) =>
|
{patient.firstName} {patient.lastName}
|
||||||
date < new Date(new Date().setHours(0, 0, 0, 0))
|
</SelectItem>
|
||||||
}
|
))}
|
||||||
initialFocus
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Appointment Title{" "}
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
(optional)
|
||||||
|
</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Leave blank to auto-fill with date"
|
||||||
|
{...field}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="startTime"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Start Time</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="09:00"
|
|
||||||
{...field}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -338,145 +317,222 @@ export function AppointmentForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="endTime"
|
name="date"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>End Time</FormLabel>
|
<FormLabel>Date</FormLabel>
|
||||||
<FormControl>
|
<Popover>
|
||||||
<div className="relative">
|
<PopoverTrigger asChild>
|
||||||
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
<FormControl>
|
||||||
<Input
|
<Button
|
||||||
placeholder="09:30"
|
variant={"outline"}
|
||||||
{...field}
|
className={cn(
|
||||||
disabled={isLoading}
|
"w-full pl-3 text-left font-normal",
|
||||||
className="pl-10"
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{field.value ? (
|
||||||
|
format(field.value, "PPP")
|
||||||
|
) : (
|
||||||
|
<span>Pick a date</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={field.value ? new Date(field.value) : undefined}
|
||||||
|
onSelect={field.onChange}
|
||||||
|
disabled={(date) =>
|
||||||
|
date < new Date(new Date().setHours(0, 0, 0, 0))
|
||||||
|
}
|
||||||
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</PopoverContent>
|
||||||
</FormControl>
|
</Popover>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
<div className="grid grid-cols-2 gap-4">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="type"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="startTime"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Appointment Type</FormLabel>
|
<FormItem>
|
||||||
<Select
|
<FormLabel>Start Time</FormLabel>
|
||||||
disabled={isLoading}
|
<FormControl>
|
||||||
onValueChange={field.onChange}
|
<div className="relative">
|
||||||
value={field.value}
|
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
defaultValue={field.value}
|
<Input
|
||||||
>
|
placeholder="09:00"
|
||||||
<FormControl>
|
{...field}
|
||||||
<SelectTrigger>
|
disabled={isLoading}
|
||||||
<SelectValue placeholder="Select a type" />
|
className="pl-10"
|
||||||
</SelectTrigger>
|
value={
|
||||||
</FormControl>
|
typeof field.value === "string" ? field.value : ""
|
||||||
<SelectContent>
|
}
|
||||||
<SelectItem value="checkup">Checkup</SelectItem>
|
/>
|
||||||
<SelectItem value="cleaning">Cleaning</SelectItem>
|
</div>
|
||||||
<SelectItem value="filling">Filling</SelectItem>
|
</FormControl>
|
||||||
<SelectItem value="extraction">Extraction</SelectItem>
|
<FormMessage />
|
||||||
<SelectItem value="root-canal">Root Canal</SelectItem>
|
</FormItem>
|
||||||
<SelectItem value="crown">Crown</SelectItem>
|
)}
|
||||||
<SelectItem value="dentures">Dentures</SelectItem>
|
/>
|
||||||
<SelectItem value="consultation">Consultation</SelectItem>
|
|
||||||
<SelectItem value="emergency">Emergency</SelectItem>
|
|
||||||
<SelectItem value="other">Other</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="status"
|
name="endTime"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Status</FormLabel>
|
<FormLabel>End Time</FormLabel>
|
||||||
<Select
|
<FormControl>
|
||||||
disabled={isLoading}
|
<div className="relative">
|
||||||
onValueChange={field.onChange}
|
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
value={field.value}
|
<Input
|
||||||
defaultValue={field.value}
|
placeholder="09:30"
|
||||||
>
|
{...field}
|
||||||
<FormControl>
|
disabled={isLoading}
|
||||||
<SelectTrigger>
|
className="pl-10"
|
||||||
<SelectValue placeholder="Select a status" />
|
value={
|
||||||
</SelectTrigger>
|
typeof field.value === "string" ? field.value : ""
|
||||||
</FormControl>
|
}
|
||||||
<SelectContent>
|
/>
|
||||||
<SelectItem value="scheduled">Scheduled</SelectItem>
|
</div>
|
||||||
<SelectItem value="confirmed">Confirmed</SelectItem>
|
</FormControl>
|
||||||
<SelectItem value="completed">Completed</SelectItem>
|
<FormMessage />
|
||||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
</FormItem>
|
||||||
<SelectItem value="no-show">No Show</SelectItem>
|
)}
|
||||||
</SelectContent>
|
/>
|
||||||
</Select>
|
</div>
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="staff"
|
name="type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Doctor/Hygienist</FormLabel>
|
<FormLabel>Appointment Type</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
disabled={isLoading}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select staff member" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{staffMembers.map((staff) => (
|
|
||||||
<SelectItem key={staff.id} value={staff.id}>
|
|
||||||
{staff.name} ({staff.role})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="notes"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Notes</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Enter any notes about the appointment"
|
|
||||||
{...field}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="min-h-24"
|
onValueChange={field.onChange}
|
||||||
/>
|
value={field.value}
|
||||||
</FormControl>
|
defaultValue={field.value}
|
||||||
<FormMessage />
|
>
|
||||||
</FormItem>
|
<FormControl>
|
||||||
)}
|
<SelectTrigger>
|
||||||
/>
|
<SelectValue placeholder="Select a type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="checkup">Checkup</SelectItem>
|
||||||
|
<SelectItem value="cleaning">Cleaning</SelectItem>
|
||||||
|
<SelectItem value="filling">Filling</SelectItem>
|
||||||
|
<SelectItem value="extraction">Extraction</SelectItem>
|
||||||
|
<SelectItem value="root-canal">Root Canal</SelectItem>
|
||||||
|
<SelectItem value="crown">Crown</SelectItem>
|
||||||
|
<SelectItem value="dentures">Dentures</SelectItem>
|
||||||
|
<SelectItem value="consultation">Consultation</SelectItem>
|
||||||
|
<SelectItem value="emergency">Emergency</SelectItem>
|
||||||
|
<SelectItem value="other">Other</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button type="submit" disabled={isLoading} className="w-full">
|
<FormField
|
||||||
{appointment ? "Update Appointment" : "Create Appointment"}
|
control={form.control}
|
||||||
</Button>
|
name="status"
|
||||||
</form>
|
render={({ field }) => (
|
||||||
</Form>
|
<FormItem>
|
||||||
|
<FormLabel>Status</FormLabel>
|
||||||
|
<Select
|
||||||
|
disabled={isLoading}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="scheduled">Scheduled</SelectItem>
|
||||||
|
<SelectItem value="confirmed">Confirmed</SelectItem>
|
||||||
|
<SelectItem value="completed">Completed</SelectItem>
|
||||||
|
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||||
|
<SelectItem value="no-show">No Show</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="staffId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Doctor/Hygienist</FormLabel>
|
||||||
|
<Select
|
||||||
|
disabled={isLoading}
|
||||||
|
onValueChange={(val) => field.onChange(Number(val))}
|
||||||
|
value={field.value ? String(field.value) : undefined}
|
||||||
|
defaultValue={field.value ? String(field.value) : undefined}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select staff member" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{staffMembers.map((staff) => (
|
||||||
|
<SelectItem
|
||||||
|
key={staff.id}
|
||||||
|
value={staff.id?.toString() || ""}
|
||||||
|
>
|
||||||
|
{staff.name} ({staff.role})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Notes</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Enter any notes about the appointment"
|
||||||
|
{...field}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="min-h-24"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" disabled={isLoading} className="w-full">
|
||||||
|
{appointment ? "Update Appointment" : "Create Appointment"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { useState, forwardRef, useImperativeHandle } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -8,38 +14,44 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { PatientForm } from "./patient-form";
|
import { PatientForm, PatientFormRef } from "./patient-form";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { X, Calendar } from "lucide-react";
|
import { X, Calendar } from "lucide-react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
// import { InsertPatient, Patient, UpdatePatient } from "@repo/db/shared/schemas";
|
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const PatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
appointments: true,
|
appointments: true,
|
||||||
});
|
});
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const insertPatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
});
|
});
|
||||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||||
|
|
||||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const updatePatientSchema = (
|
||||||
id: true,
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
createdAt: true,
|
)
|
||||||
userId: true,
|
.omit({
|
||||||
}).partial();
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||||
|
|
||||||
|
|
||||||
interface AddPatientModalProps {
|
interface AddPatientModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSubmit: (data: InsertPatient | UpdatePatient) => void;
|
onSubmit: (data: InsertPatient | (UpdatePatient & { id?: number })) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
patient?: Patient;
|
patient?: Patient;
|
||||||
extractedInfo?: {
|
extractedInfo?: {
|
||||||
@@ -56,35 +68,49 @@ export type AddPatientModalRef = {
|
|||||||
navigateToSchedule: (patientId: number) => void;
|
navigateToSchedule: (patientId: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalProps>(function AddPatientModal(props, ref) {
|
export const AddPatientModal = forwardRef<
|
||||||
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } = props;
|
AddPatientModalRef,
|
||||||
|
AddPatientModalProps
|
||||||
|
>(function AddPatientModal(props, ref) {
|
||||||
|
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } =
|
||||||
|
props;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [formData, setFormData] = useState<InsertPatient | UpdatePatient | null>(null);
|
const [formData, setFormData] = useState<
|
||||||
|
InsertPatient | UpdatePatient | null
|
||||||
|
>(null);
|
||||||
const isEditing = !!patient;
|
const isEditing = !!patient;
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const [saveAndSchedule, setSaveAndSchedule] = useState(false);
|
const [saveAndSchedule, setSaveAndSchedule] = useState(false);
|
||||||
|
const patientFormRef = useRef<PatientFormRef>(null); // Ref for PatientForm
|
||||||
|
|
||||||
// Set up the imperativeHandle to expose functionality to the parent component
|
// Set up the imperativeHandle to expose functionality to the parent component
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && patient) {
|
||||||
|
const { id, userId, createdAt, ...sanitized } = patient;
|
||||||
|
setFormData(sanitized); // Update the form data with the patient data for editing
|
||||||
|
} else {
|
||||||
|
setFormData(null); // Reset form data when not editing
|
||||||
|
}
|
||||||
|
}, [isEditing, patient]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
shouldSchedule: saveAndSchedule,
|
shouldSchedule: saveAndSchedule,
|
||||||
navigateToSchedule: (patientId: number) => {
|
navigateToSchedule: (patientId: number) => {
|
||||||
navigate(`/appointments?newPatient=${patientId}`);
|
navigate(`/appointments?newPatient=${patientId}`);
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleFormSubmit = (data: InsertPatient | UpdatePatient) => {
|
const handleFormSubmit = (data: InsertPatient | UpdatePatient) => {
|
||||||
setFormData(data);
|
if (patient && patient.id) {
|
||||||
onSubmit(data);
|
onSubmit({ ...data, id: patient.id });
|
||||||
|
} else {
|
||||||
|
onSubmit(data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAndSchedule = () => {
|
const handleSaveAndSchedule = () => {
|
||||||
setSaveAndSchedule(true);
|
setSaveAndSchedule(true);
|
||||||
if (formData) {
|
document.querySelector("form")?.requestSubmit();
|
||||||
onSubmit(formData);
|
|
||||||
} else {
|
|
||||||
// Trigger form validation by clicking the hidden submit button
|
|
||||||
document.querySelector('form')?.requestSubmit();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,10 +143,7 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogFooter className="mt-6">
|
<DialogFooter className="mt-6">
|
||||||
<Button
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -128,7 +151,9 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-1"
|
className="gap-1"
|
||||||
onClick={handleSaveAndSchedule}
|
onClick={() => {
|
||||||
|
handleSaveAndSchedule();
|
||||||
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
@@ -138,20 +163,21 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
form="patient-form"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (formData) {
|
if (patientFormRef.current) {
|
||||||
onSubmit(formData);
|
patientFormRef.current.submit();
|
||||||
} else {
|
|
||||||
// Trigger form validation by clicking the hidden submit button
|
|
||||||
document.querySelector('form')?.requestSubmit();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading
|
{isLoading
|
||||||
? isEditing ? "Updating..." : "Saving..."
|
? patient
|
||||||
: isEditing ? "Update Patient" : "Save Patient"
|
? "Updating..."
|
||||||
}
|
: "Saving..."
|
||||||
|
: patient
|
||||||
|
? "Update Patient"
|
||||||
|
: "Save Patient"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||||
// import { insertPatientSchema, InsertPatient, Patient, updatePatientSchema, UpdatePatient } from "@repo/db/shared/schemas";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -13,7 +12,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -21,27 +19,36 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const PatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
appointments: true,
|
appointments: true,
|
||||||
});
|
});
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const insertPatientSchema = (
|
||||||
id: true,
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
createdAt: true,
|
).omit({
|
||||||
});
|
|
||||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
|
||||||
|
|
||||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
}).partial();
|
});
|
||||||
|
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||||
|
|
||||||
|
const updatePatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
)
|
||||||
|
.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||||
|
|
||||||
|
|
||||||
interface PatientFormProps {
|
interface PatientFormProps {
|
||||||
patient?: Patient;
|
patient?: Patient;
|
||||||
extractedInfo?: {
|
extractedInfo?: {
|
||||||
@@ -53,50 +60,103 @@ interface PatientFormProps {
|
|||||||
onSubmit: (data: InsertPatient | UpdatePatient) => void;
|
onSubmit: (data: InsertPatient | UpdatePatient) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormProps) {
|
export type PatientFormRef = {
|
||||||
|
submit: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PatientForm({
|
||||||
|
patient,
|
||||||
|
extractedInfo,
|
||||||
|
onSubmit,
|
||||||
|
}: PatientFormProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const isEditing = !!patient;
|
const isEditing = !!patient;
|
||||||
|
|
||||||
const schema = isEditing ? updatePatientSchema : insertPatientSchema.extend({
|
const schema = useMemo(
|
||||||
userId: z.number().optional(),
|
() =>
|
||||||
});
|
isEditing
|
||||||
|
? updatePatientSchema
|
||||||
|
: insertPatientSchema.extend({ userId: z.number().optional() }),
|
||||||
|
[isEditing]
|
||||||
|
);
|
||||||
|
|
||||||
// Merge extracted info into default values if available
|
const computedDefaultValues = useMemo(() => {
|
||||||
const defaultValues = {
|
if (isEditing && patient) {
|
||||||
firstName: extractedInfo?.firstName || "",
|
const { id, userId, createdAt, ...sanitizedPatient } = patient;
|
||||||
lastName: extractedInfo?.lastName || "",
|
return {
|
||||||
dateOfBirth: extractedInfo?.dateOfBirth || "",
|
...sanitizedPatient,
|
||||||
gender: "",
|
dateOfBirth: patient.dateOfBirth
|
||||||
phone: "",
|
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
|
||||||
email: "",
|
: "",
|
||||||
address: "",
|
};
|
||||||
city: "",
|
}
|
||||||
zipCode: "",
|
|
||||||
insuranceProvider: "",
|
return {
|
||||||
insuranceId: extractedInfo?.insuranceId || "",
|
firstName: extractedInfo?.firstName || "",
|
||||||
groupNumber: "",
|
lastName: extractedInfo?.lastName || "",
|
||||||
policyHolder: "",
|
dateOfBirth: extractedInfo?.dateOfBirth || "",
|
||||||
allergies: "",
|
gender: "",
|
||||||
medicalConditions: "",
|
phone: "",
|
||||||
status: "active",
|
email: "",
|
||||||
userId: user?.id,
|
address: "",
|
||||||
};
|
city: "",
|
||||||
|
zipCode: "",
|
||||||
|
insuranceProvider: "",
|
||||||
|
insuranceId: extractedInfo?.insuranceId || "",
|
||||||
|
groupNumber: "",
|
||||||
|
policyHolder: "",
|
||||||
|
allergies: "",
|
||||||
|
medicalConditions: "",
|
||||||
|
status: "active",
|
||||||
|
userId: user?.id,
|
||||||
|
};
|
||||||
|
}, [isEditing, patient, extractedInfo, user?.id]);
|
||||||
|
|
||||||
const form = useForm<InsertPatient | UpdatePatient>({
|
const form = useForm<InsertPatient | UpdatePatient>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: patient || defaultValues,
|
defaultValues: computedDefaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (data: InsertPatient | UpdatePatient) => {
|
// Debug form errors
|
||||||
|
useEffect(() => {
|
||||||
|
const errors = form.formState.errors;
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
console.log("❌ Form validation errors:", errors);
|
||||||
|
}
|
||||||
|
}, [form.formState.errors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (patient) {
|
||||||
|
const { id, userId, createdAt, ...sanitizedPatient } = patient;
|
||||||
|
const resetValues: Partial<Patient> = {
|
||||||
|
...sanitizedPatient,
|
||||||
|
dateOfBirth: patient.dateOfBirth
|
||||||
|
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
form.reset(resetValues);
|
||||||
|
}
|
||||||
|
}, [patient, computedDefaultValues, form]);
|
||||||
|
|
||||||
|
const handleSubmit2 = (data: InsertPatient | UpdatePatient) => {
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
<form
|
||||||
|
id="patient-form"
|
||||||
|
key={patient?.id || "new"}
|
||||||
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
handleSubmit2(data);
|
||||||
|
})}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
{/* Personal Information */}
|
{/* Personal Information */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-md font-medium text-gray-700 mb-3">Personal Information</h4>
|
<h4 className="text-md font-medium text-gray-700 mb-3">
|
||||||
|
Personal Information
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -170,7 +230,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
|
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-md font-medium text-gray-700 mb-3">Contact Information</h4>
|
<h4 className="text-md font-medium text-gray-700 mb-3">
|
||||||
|
Contact Information
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -193,7 +255,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="email" {...field} value={field.value || ''} />
|
<Input type="email" {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -207,7 +269,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem className="md:col-span-2">
|
<FormItem className="md:col-span-2">
|
||||||
<FormLabel>Address</FormLabel>
|
<FormLabel>Address</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value || ''} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -221,7 +283,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>City</FormLabel>
|
<FormLabel>City</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value || ''} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -235,7 +297,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>ZIP Code</FormLabel>
|
<FormLabel>ZIP Code</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value || ''} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -246,7 +308,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
|
|
||||||
{/* Insurance Information */}
|
{/* Insurance Information */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-md font-medium text-gray-700 mb-3">Insurance Information</h4>
|
<h4 className="text-md font-medium text-gray-700 mb-3">
|
||||||
|
Insurance Information
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -256,7 +320,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormLabel>Insurance Provider</FormLabel>
|
<FormLabel>Insurance Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value as string || ''}
|
defaultValue={(field.value as string) || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -264,7 +328,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="placeholder">Select provider</SelectItem>
|
<SelectItem value="placeholder">
|
||||||
|
Select provider
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="delta">Delta Dental</SelectItem>
|
<SelectItem value="delta">Delta Dental</SelectItem>
|
||||||
<SelectItem value="metlife">MetLife</SelectItem>
|
<SelectItem value="metlife">MetLife</SelectItem>
|
||||||
<SelectItem value="cigna">Cigna</SelectItem>
|
<SelectItem value="cigna">Cigna</SelectItem>
|
||||||
@@ -285,7 +351,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Insurance ID</FormLabel>
|
<FormLabel>Insurance ID</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value || ''} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -299,7 +365,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Group Number</FormLabel>
|
<FormLabel>Group Number</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value || ''} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -313,7 +379,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Policy Holder (if not self)</FormLabel>
|
<FormLabel>Policy Holder (if not self)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value || ''} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -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>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { format, addDays, parse, startOfToday, startOfDay, addMinutes, isEqual } from "date-fns";
|
import { format, addDays, startOfToday, addMinutes } from "date-fns";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
||||||
@@ -9,50 +9,64 @@ import { Button } from "@/components/ui/button";
|
|||||||
import {
|
import {
|
||||||
Calendar as CalendarIcon,
|
Calendar as CalendarIcon,
|
||||||
Plus,
|
Plus,
|
||||||
Users,
|
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Move,
|
Move,
|
||||||
Trash2,
|
Trash2,
|
||||||
FileText
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
import {
|
||||||
// import { Appointment, InsertAppointment, UpdateAppointment, Patient } from "@repo/db/shared/schemas";
|
AppointmentUncheckedCreateInputObjectSchema,
|
||||||
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
|
} from "@repo/db/shared/schemas";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { Card, CardContent, CardHeader, CardDescription, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
Card,
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
CardContent,
|
||||||
import { Menu, Item, useContextMenu } from 'react-contexify';
|
CardHeader,
|
||||||
import 'react-contexify/ReactContexify.css';
|
CardDescription,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||||
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
|
import { Menu, Item, useContextMenu } from "react-contexify";
|
||||||
|
import "react-contexify/ReactContexify.css";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
//creating types out of schema auto generated.
|
//creating types out of schema auto generated.
|
||||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||||
|
|
||||||
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const insertAppointmentSchema = (
|
||||||
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
});
|
});
|
||||||
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
||||||
|
|
||||||
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const updateAppointmentSchema = (
|
||||||
id: true,
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
createdAt: true,
|
)
|
||||||
}).partial();
|
.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
||||||
|
|
||||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const PatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
appointments: true,
|
appointments: true,
|
||||||
});
|
});
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Define types for scheduling
|
// Define types for scheduling
|
||||||
interface TimeSlot {
|
interface TimeSlot {
|
||||||
time: string;
|
time: string;
|
||||||
@@ -62,7 +76,7 @@ interface TimeSlot {
|
|||||||
interface Staff {
|
interface Staff {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: 'doctor' | 'hygienist';
|
role: "doctor" | "hygienist";
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +85,9 @@ interface ScheduledAppointment {
|
|||||||
patientId: number;
|
patientId: number;
|
||||||
patientName: string;
|
patientName: string;
|
||||||
staffId: string;
|
staffId: string;
|
||||||
date: string | Date; // Allow both string and Date
|
date: string | Date;
|
||||||
startTime: string | Date; // Allow both string and Date
|
startTime: string | Date;
|
||||||
endTime: string | Date; // Allow both string and Date
|
endTime: string | Date;
|
||||||
status: string | null;
|
status: string | null;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
@@ -86,9 +100,13 @@ export default function AppointmentsPage() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [isClaimModalOpen, setIsClaimModalOpen] = useState(false);
|
const [isClaimModalOpen, setIsClaimModalOpen] = useState(false);
|
||||||
const [claimAppointmentId, setClaimAppointmentId] = useState<number | null>(null);
|
const [claimAppointmentId, setClaimAppointmentId] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [claimPatientId, setClaimPatientId] = useState<number | null>(null);
|
const [claimPatientId, setClaimPatientId] = useState<number | null>(null);
|
||||||
const [editingAppointment, setEditingAppointment] = useState<Appointment | undefined>(undefined);
|
const [editingAppointment, setEditingAppointment] = useState<
|
||||||
|
Appointment | undefined
|
||||||
|
>(undefined);
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(startOfToday());
|
const [selectedDate, setSelectedDate] = useState<Date>(startOfToday());
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [location] = useLocation();
|
const [location] = useLocation();
|
||||||
@@ -98,23 +116,35 @@ export default function AppointmentsPage() {
|
|||||||
id: APPOINTMENT_CONTEXT_MENU_ID,
|
id: APPOINTMENT_CONTEXT_MENU_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Staff members (doctors and hygienists)
|
//Fetching staff memebers
|
||||||
const staffMembers: Staff[] = [
|
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
|
||||||
{ id: "doctor1", name: "Dr. Kai Gao", role: "doctor", color: "bg-blue-600" },
|
useQuery<Staff[]>({
|
||||||
{ id: "doctor2", name: "Dr. Jane Smith", role: "doctor", color: "bg-emerald-600" },
|
queryKey: ["/api/staffs/"],
|
||||||
{ id: "hygienist1", name: "Hygienist One", role: "hygienist", color: "bg-purple-600" },
|
queryFn: async () => {
|
||||||
{ id: "hygienist2", name: "Hygienist Two", role: "hygienist", color: "bg-rose-500" },
|
const res = await apiRequest("GET", "/api/staffs/");
|
||||||
{ id: "hygienist3", name: "Hygienist Three", role: "hygienist", color: "bg-amber-500" },
|
return res.json();
|
||||||
];
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
"Dr. Kai Gao": "bg-blue-600",
|
||||||
|
"Dr. Jane Smith": "bg-emerald-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
const staffMembers = staffMembersRaw.map((staff) => ({
|
||||||
|
...staff,
|
||||||
|
color: colorMap[staff.name] || "bg-gray-400",
|
||||||
|
}));
|
||||||
|
|
||||||
// Generate time slots from 8:00 AM to 6:00 PM in 30-minute increments
|
// Generate time slots from 8:00 AM to 6:00 PM in 30-minute increments
|
||||||
const timeSlots: TimeSlot[] = [];
|
const timeSlots: TimeSlot[] = [];
|
||||||
for (let hour = 8; hour <= 18; hour++) {
|
for (let hour = 8; hour <= 18; hour++) {
|
||||||
for (let minute = 0; minute < 60; minute += 30) {
|
for (let minute = 0; minute < 60; minute += 30) {
|
||||||
const hour12 = hour > 12 ? hour - 12 : hour;
|
const hour12 = hour > 12 ? hour - 12 : hour;
|
||||||
const period = hour >= 12 ? 'PM' : 'AM';
|
const period = hour >= 12 ? "PM" : "AM";
|
||||||
const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
const timeStr = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
|
||||||
const displayTime = `${hour12}:${minute.toString().padStart(2, '0')} ${period}`;
|
const displayTime = `${hour12}:${minute.toString().padStart(2, "0")} ${period}`;
|
||||||
timeSlots.push({ time: timeStr, displayTime });
|
timeSlots.push({ time: timeStr, displayTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,49 +155,55 @@ export default function AppointmentsPage() {
|
|||||||
isLoading: isLoadingAppointments,
|
isLoading: isLoadingAppointments,
|
||||||
refetch: refetchAppointments,
|
refetch: refetchAppointments,
|
||||||
} = useQuery<Appointment[]>({
|
} = useQuery<Appointment[]>({
|
||||||
queryKey: ["/api/appointments"],
|
queryKey: ["/api/appointments/all"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/appointments/all");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch patients (needed for the dropdowns)
|
// Fetch patients (needed for the dropdowns)
|
||||||
const {
|
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||||
data: patients = [],
|
Patient[]
|
||||||
isLoading: isLoadingPatients,
|
>({
|
||||||
} = useQuery<Patient[]>({
|
queryKey: ["/api/patients/"],
|
||||||
queryKey: ["/api/patients"],
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/patients/");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle creating a new appointment at a specific time slot and for a specific staff member
|
// Handle creating a new appointment at a specific time slot and for a specific staff member
|
||||||
const handleCreateAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => {
|
const handleCreateAppointmentAtSlot = (
|
||||||
|
timeSlot: TimeSlot,
|
||||||
|
staffId: string
|
||||||
|
) => {
|
||||||
// Calculate end time (30 minutes after start time)
|
// Calculate end time (30 minutes after start time)
|
||||||
const startHour = parseInt(timeSlot.time.split(':')[0] as string);
|
const startHour = parseInt(timeSlot.time.split(":")[0] as string);
|
||||||
const startMinute = parseInt(timeSlot.time.split(':')[1] as string);
|
const startMinute = parseInt(timeSlot.time.split(":")[1] as string);
|
||||||
const startDate = new Date(selectedDate);
|
const startDate = new Date(selectedDate);
|
||||||
startDate.setHours(startHour, startMinute, 0);
|
startDate.setHours(startHour, startMinute, 0);
|
||||||
|
|
||||||
const endDate = addMinutes(startDate, 30);
|
const endDate = addMinutes(startDate, 30);
|
||||||
const endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`;
|
const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
|
||||||
|
|
||||||
// Find staff member
|
// Find staff member
|
||||||
const staff = staffMembers.find(s => s.id === staffId);
|
const staff = staffMembers.find((s) => s.id === staffId);
|
||||||
|
|
||||||
console.log(`Creating appointment at time slot: ${timeSlot.time} (${timeSlot.displayTime})`);
|
|
||||||
|
|
||||||
// Pre-fill appointment form with default values
|
// Pre-fill appointment form with default values
|
||||||
const newAppointment = {
|
const newAppointment = {
|
||||||
date: format(selectedDate, 'yyyy-MM-dd'),
|
date: format(selectedDate, "yyyy-MM-dd"),
|
||||||
startTime: timeSlot.time, // This is in "HH:MM" format
|
startTime: timeSlot.time, // This is in "HH:MM" format
|
||||||
endTime: endTime,
|
endTime: endTime,
|
||||||
type: staff?.role === 'doctor' ? 'checkup' : 'cleaning',
|
type: staff?.role === "doctor" ? "checkup" : "cleaning",
|
||||||
status: 'scheduled',
|
status: "scheduled",
|
||||||
title: `Appointment with ${staff?.name}`, // Add staff name in title for easier display
|
title: `Appointment with ${staff?.name}`, // Add staff name in title for easier display
|
||||||
notes: `Appointment with ${staff?.name}`, // Store staff info in notes for processing
|
notes: `Appointment with ${staff?.name}`, // Store staff info in notes for processing
|
||||||
staff: staffId // This matches the 'staff' field in the appointment form schema
|
staff: staffId, // This matches the 'staff' field in the appointment form schema
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Created appointment data to pass to modal:', newAppointment);
|
|
||||||
|
|
||||||
// For new appointments, set editingAppointment to undefined
|
// For new appointments, set editingAppointment to undefined
|
||||||
// This will ensure we go to the "create" branch in handleAppointmentSubmit
|
// This will ensure we go to the "create" branch in handleAppointmentSubmit
|
||||||
setEditingAppointment(undefined);
|
setEditingAppointment(undefined);
|
||||||
@@ -177,8 +213,11 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
// Store the prefilled values in state or sessionStorage to access in the modal
|
// Store the prefilled values in state or sessionStorage to access in the modal
|
||||||
// Clear any existing data first to ensure we're not using old data
|
// Clear any existing data first to ensure we're not using old data
|
||||||
sessionStorage.removeItem('newAppointmentData');
|
sessionStorage.removeItem("newAppointmentData");
|
||||||
sessionStorage.setItem('newAppointmentData', JSON.stringify(newAppointment));
|
sessionStorage.setItem(
|
||||||
|
"newAppointmentData",
|
||||||
|
JSON.stringify(newAppointment)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for newPatient parameter in URL
|
// Check for newPatient parameter in URL
|
||||||
@@ -187,12 +226,12 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
// Parse URL search params to check for newPatient
|
// Parse URL search params to check for newPatient
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const newPatientId = params.get('newPatient');
|
const newPatientId = params.get("newPatient");
|
||||||
|
|
||||||
if (newPatientId) {
|
if (newPatientId) {
|
||||||
const patientId = parseInt(newPatientId);
|
const patientId = parseInt(newPatientId);
|
||||||
// Find the patient in our list
|
// Find the patient in our list
|
||||||
const patient = (patients as Patient[]).find(p => p.id === patientId);
|
const patient = (patients as Patient[]).find((p) => p.id === patientId);
|
||||||
|
|
||||||
if (patient) {
|
if (patient) {
|
||||||
toast({
|
toast({
|
||||||
@@ -204,34 +243,38 @@ export default function AppointmentsPage() {
|
|||||||
const staffId = staffMembers[0]!.id;
|
const staffId = staffMembers[0]!.id;
|
||||||
|
|
||||||
// Find first time slot today (9:00 AM is a common starting time)
|
// Find first time slot today (9:00 AM is a common starting time)
|
||||||
const defaultTimeSlot = timeSlots.find(slot => slot.time === "09:00") || timeSlots[0];
|
const defaultTimeSlot =
|
||||||
|
timeSlots.find((slot) => slot.time === "09:00") || timeSlots[0];
|
||||||
|
|
||||||
// Open appointment modal with prefilled patient
|
// Open appointment modal with prefilled patient
|
||||||
handleCreateAppointmentAtSlot(defaultTimeSlot!, staffId);
|
handleCreateAppointmentAtSlot(defaultTimeSlot!, staffId);
|
||||||
|
|
||||||
// Pre-select the patient in the appointment form
|
// Pre-select the patient in the appointment form
|
||||||
const patientData = {
|
const patientData = {
|
||||||
patientId: patient.id
|
patientId: patient.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store info in session storage for the modal to pick up
|
// Store info in session storage for the modal to pick up
|
||||||
const existingData = sessionStorage.getItem('newAppointmentData');
|
const existingData = sessionStorage.getItem("newAppointmentData");
|
||||||
if (existingData) {
|
if (existingData) {
|
||||||
const parsedData = JSON.parse(existingData);
|
const parsedData = JSON.parse(existingData);
|
||||||
sessionStorage.setItem('newAppointmentData', JSON.stringify({
|
sessionStorage.setItem(
|
||||||
...parsedData,
|
"newAppointmentData",
|
||||||
...patientData
|
JSON.stringify({
|
||||||
}));
|
...parsedData,
|
||||||
|
...patientData,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [patients, user, location]);
|
}, [patients, user, location]);
|
||||||
|
|
||||||
// Create appointment mutation
|
// Create appointment mutation
|
||||||
const createAppointmentMutation = useMutation({
|
const createAppointmentMutation = useMutation({
|
||||||
mutationFn: async (appointment: InsertAppointment) => {
|
mutationFn: async (appointment: InsertAppointment) => {
|
||||||
const res = await apiRequest("POST", "/api/appointments", appointment);
|
const res = await apiRequest("POST", "/api/appointments/", appointment);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -239,7 +282,9 @@ export default function AppointmentsPage() {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Appointment created successfully.",
|
description: "Appointment created successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
// Invalidate both appointments and patients queries
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
@@ -253,8 +298,18 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
// Update appointment mutation
|
// Update appointment mutation
|
||||||
const updateAppointmentMutation = useMutation({
|
const updateAppointmentMutation = useMutation({
|
||||||
mutationFn: async ({ id, appointment }: { id: number; appointment: UpdateAppointment }) => {
|
mutationFn: async ({
|
||||||
const res = await apiRequest("PUT", `/api/appointments/${id}`, appointment);
|
id,
|
||||||
|
appointment,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
appointment: UpdateAppointment;
|
||||||
|
}) => {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"PUT",
|
||||||
|
`/api/appointments/${id}`,
|
||||||
|
appointment
|
||||||
|
);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -262,7 +317,8 @@ export default function AppointmentsPage() {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Appointment updated successfully.",
|
description: "Appointment updated successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
setEditingAppointment(undefined);
|
setEditingAppointment(undefined);
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
},
|
},
|
||||||
@@ -285,7 +341,9 @@ export default function AppointmentsPage() {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Appointment deleted successfully.",
|
description: "Appointment deleted successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
// Invalidate both appointments and patients queries
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -297,15 +355,21 @@ export default function AppointmentsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle appointment submission (create or update)
|
// Handle appointment submission (create or update)
|
||||||
const handleAppointmentSubmit = (appointmentData: InsertAppointment | UpdateAppointment) => {
|
const handleAppointmentSubmit = (
|
||||||
|
appointmentData: InsertAppointment | UpdateAppointment
|
||||||
|
) => {
|
||||||
// Make sure the date is for the selected date
|
// Make sure the date is for the selected date
|
||||||
const updatedData = {
|
const updatedData = {
|
||||||
...appointmentData,
|
...appointmentData,
|
||||||
date: format(selectedDate, 'yyyy-MM-dd')
|
date: format(selectedDate, "yyyy-MM-dd"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if we're editing an existing appointment with a valid ID
|
// Check if we're editing an existing appointment with a valid ID
|
||||||
if (editingAppointment && 'id' in editingAppointment && typeof editingAppointment.id === 'number') {
|
if (
|
||||||
|
editingAppointment &&
|
||||||
|
"id" in editingAppointment &&
|
||||||
|
typeof editingAppointment.id === "number"
|
||||||
|
) {
|
||||||
updateAppointmentMutation.mutate({
|
updateAppointmentMutation.mutate({
|
||||||
id: editingAppointment.id,
|
id: editingAppointment.id,
|
||||||
appointment: updatedData as unknown as UpdateAppointment,
|
appointment: updatedData as unknown as UpdateAppointment,
|
||||||
@@ -314,8 +378,8 @@ export default function AppointmentsPage() {
|
|||||||
// This is a new appointment
|
// This is a new appointment
|
||||||
if (user) {
|
if (user) {
|
||||||
createAppointmentMutation.mutate({
|
createAppointmentMutation.mutate({
|
||||||
...updatedData as unknown as InsertAppointment,
|
...(updatedData as unknown as InsertAppointment),
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,73 +403,58 @@ export default function AppointmentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get formatted date string for display
|
// Get formatted date string for display
|
||||||
const formattedDate = format(selectedDate, 'MMMM d, yyyy');
|
const formattedDate = format(selectedDate, "yyyy-MM-dd");
|
||||||
|
|
||||||
// Filter appointments for the selected date
|
const selectedDateAppointments = appointments.filter((apt) => {
|
||||||
const selectedDateAppointments = appointments.filter(apt =>
|
// Ensure apt.date is in 'yyyy-MM-dd' format before comparison
|
||||||
apt.date === format(selectedDate, 'yyyy-MM-dd')
|
const appointmentDate = format(new Date(apt.date), "yyyy-MM-dd");
|
||||||
);
|
return appointmentDate === formattedDate;
|
||||||
|
});
|
||||||
// Add debugging logs
|
|
||||||
console.log("Selected date:", format(selectedDate, 'yyyy-MM-dd'));
|
|
||||||
console.log("All appointments:", appointments);
|
|
||||||
console.log("Filtered appointments for selected date:", selectedDateAppointments);
|
|
||||||
|
|
||||||
// Process appointments for the scheduler view
|
// Process appointments for the scheduler view
|
||||||
const processedAppointments: ScheduledAppointment[] = selectedDateAppointments.map(apt => {
|
const processedAppointments: ScheduledAppointment[] =
|
||||||
// Find patient name
|
selectedDateAppointments.map((apt) => {
|
||||||
const patient = patients.find(p => p.id === apt.patientId);
|
// Find patient name
|
||||||
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown Patient';
|
const patient = patients.find((p) => p.id === apt.patientId);
|
||||||
|
const patientName = patient
|
||||||
|
? `${patient.firstName} ${patient.lastName}`
|
||||||
|
: "Unknown Patient";
|
||||||
|
|
||||||
// Try to determine the staff from the notes or title
|
// Try to determine the staff from the notes or title
|
||||||
let staffId = 'doctor1'; // Default to first doctor if we can't determine
|
let staffId = "doctor1"; // Default to first doctor if we can't determine
|
||||||
|
|
||||||
console.log("Processing appointment:", {
|
// Check notes first
|
||||||
id: apt.id,
|
if (apt.notes) {
|
||||||
notes: apt.notes,
|
// Look for "Appointment with Dr. X" or similar patterns
|
||||||
title: apt.title
|
for (const staff of staffMembers) {
|
||||||
|
if (apt.notes.includes(staff.name)) {
|
||||||
|
staffId = staff.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match in notes, check title
|
||||||
|
if (staffId === "doctor1" && apt.title) {
|
||||||
|
for (const staff of staffMembers) {
|
||||||
|
if (apt.title.includes(staff.name)) {
|
||||||
|
staffId = staff.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processed = {
|
||||||
|
...apt,
|
||||||
|
patientName,
|
||||||
|
staffId,
|
||||||
|
status: apt.status ?? null, // Default to null if status is undefined
|
||||||
|
date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d
|
||||||
|
};
|
||||||
|
|
||||||
|
return processed;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check notes first
|
|
||||||
if (apt.notes) {
|
|
||||||
// Look for "Appointment with Dr. X" or similar patterns
|
|
||||||
console.log("Checking notes:", apt.notes);
|
|
||||||
for (const staff of staffMembers) {
|
|
||||||
console.log(`Checking if notes contains "${staff.name}":`, apt.notes.includes(staff.name));
|
|
||||||
if (apt.notes.includes(staff.name)) {
|
|
||||||
staffId = staff.id;
|
|
||||||
console.log(`Found staff in notes: ${staff.name} (${staffId})`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no match in notes, check title
|
|
||||||
if (staffId === 'doctor1' && apt.title) {
|
|
||||||
console.log("Checking title:", apt.title);
|
|
||||||
for (const staff of staffMembers) {
|
|
||||||
if (apt.title.includes(staff.name)) {
|
|
||||||
staffId = staff.id;
|
|
||||||
console.log(`Found staff in title: ${staff.name} (${staffId})`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Final staffId assigned: ${staffId}`);
|
|
||||||
|
|
||||||
const processed = {
|
|
||||||
...apt,
|
|
||||||
patientName,
|
|
||||||
staffId,
|
|
||||||
status: apt.status ?? null, // Default to null if status is undefined
|
|
||||||
date: apt.date instanceof Date ? apt.date.toISOString() : apt.date, // Ensure d
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Processed appointment:", processed);
|
|
||||||
return processed;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if appointment exists at a specific time slot and staff
|
// Check if appointment exists at a specific time slot and staff
|
||||||
const getAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => {
|
const getAppointmentAtSlot = (timeSlot: TimeSlot, staffId: string) => {
|
||||||
if (processedAppointments.length === 0) {
|
if (processedAppointments.length === 0) {
|
||||||
@@ -414,9 +463,10 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
// In appointments for a given time slot, we'll just display the first one
|
// In appointments for a given time slot, we'll just display the first one
|
||||||
// In a real application, you might want to show multiple or stack them
|
// In a real application, you might want to show multiple or stack them
|
||||||
const appointmentsAtSlot = processedAppointments.filter(apt => {
|
const appointmentsAtSlot = processedAppointments.filter((apt) => {
|
||||||
// Fix time format comparison - the database adds ":00" seconds to the time
|
// Fix time format comparison - the database adds ":00" seconds to the time
|
||||||
const dbTime = typeof apt.startTime === 'string' ? apt.startTime.substring(0, 5) : '';
|
const dbTime =
|
||||||
|
typeof apt.startTime === "string" ? apt.startTime.substring(0, 5) : "";
|
||||||
|
|
||||||
const timeMatches = dbTime === timeSlot.time;
|
const timeMatches = dbTime === timeSlot.time;
|
||||||
const staffMatches = apt.staffId === staffId;
|
const staffMatches = apt.staffId === staffId;
|
||||||
@@ -436,35 +486,39 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
// Define drag item types
|
// Define drag item types
|
||||||
const ItemTypes = {
|
const ItemTypes = {
|
||||||
APPOINTMENT: 'appointment',
|
APPOINTMENT: "appointment",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle moving an appointment to a new time slot and staff
|
// Handle moving an appointment to a new time slot and staff
|
||||||
const handleMoveAppointment = (appointmentId: number, newTimeSlot: TimeSlot, newStaffId: string) => {
|
const handleMoveAppointment = (
|
||||||
const appointment = appointments.find(a => a.id === appointmentId);
|
appointmentId: number,
|
||||||
|
newTimeSlot: TimeSlot,
|
||||||
|
newStaffId: string
|
||||||
|
) => {
|
||||||
|
const appointment = appointments.find((a) => a.id === appointmentId);
|
||||||
if (!appointment) return;
|
if (!appointment) return;
|
||||||
|
|
||||||
// Calculate new end time (30 minutes from start)
|
// Calculate new end time (30 minutes from start)
|
||||||
const startHour = parseInt(newTimeSlot.time.split(':')[0] as string);
|
const startHour = parseInt(newTimeSlot.time.split(":")[0] as string);
|
||||||
const startMinute = parseInt(newTimeSlot.time.split(':')[1] as string);
|
const startMinute = parseInt(newTimeSlot.time.split(":")[1] as string);
|
||||||
const startDate = new Date(selectedDate);
|
const startDate = new Date(selectedDate);
|
||||||
startDate.setHours(startHour, startMinute, 0);
|
startDate.setHours(startHour, startMinute, 0);
|
||||||
|
|
||||||
const endDate = addMinutes(startDate, 30);
|
const endDate = addMinutes(startDate, 30);
|
||||||
const endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`;
|
const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
|
||||||
|
|
||||||
// Find staff member
|
// Find staff member
|
||||||
const staff = staffMembers.find(s => s.id === newStaffId);
|
const staff = staffMembers.find((s) => s.id === newStaffId);
|
||||||
|
|
||||||
// Update appointment data
|
// Update appointment data
|
||||||
// Make sure we handle the time format correctly - backend expects HH:MM but stores as HH:MM:SS
|
const { id, createdAt, ...sanitizedAppointment } = appointment;
|
||||||
const updatedAppointment: UpdateAppointment = {
|
const updatedAppointment: UpdateAppointment = {
|
||||||
...appointment,
|
...sanitizedAppointment,
|
||||||
startTime: newTimeSlot.time, // Already in HH:MM format
|
startTime: newTimeSlot.time, // Already in HH:MM format
|
||||||
endTime: endTime, // Already in HH:MM format
|
endTime: endTime, // Already in HH:MM format
|
||||||
notes: `Appointment with ${staff?.name}`,
|
notes: `Appointment with ${staff?.name}`,
|
||||||
|
staffId: newStaffId, // Update staffId
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call update mutation
|
// Call update mutation
|
||||||
updateAppointmentMutation.mutate({
|
updateAppointmentMutation.mutate({
|
||||||
id: appointmentId,
|
id: appointmentId,
|
||||||
@@ -487,7 +541,13 @@ export default function AppointmentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create a draggable appointment component
|
// Create a draggable appointment component
|
||||||
function DraggableAppointment({ appointment, staff }: { appointment: ScheduledAppointment, staff: Staff }) {
|
function DraggableAppointment({
|
||||||
|
appointment,
|
||||||
|
staff,
|
||||||
|
}: {
|
||||||
|
appointment: ScheduledAppointment;
|
||||||
|
staff: Staff;
|
||||||
|
}) {
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
type: ItemTypes.APPOINTMENT,
|
type: ItemTypes.APPOINTMENT,
|
||||||
item: { id: appointment.id },
|
item: { id: appointment.id },
|
||||||
@@ -500,13 +560,15 @@ export default function AppointmentsPage() {
|
|||||||
<div
|
<div
|
||||||
ref={drag as unknown as React.RefObject<HTMLDivElement>} // Type assertion to make TypeScript happy
|
ref={drag as unknown as React.RefObject<HTMLDivElement>} // Type assertion to make TypeScript happy
|
||||||
className={`${staff.color} border border-white shadow-md text-white rounded p-1 text-xs h-full overflow-hidden cursor-move relative ${
|
className={`${staff.color} border border-white shadow-md text-white rounded p-1 text-xs h-full overflow-hidden cursor-move relative ${
|
||||||
isDragging ? 'opacity-50' : 'opacity-100'
|
isDragging ? "opacity-50" : "opacity-100"
|
||||||
}`}
|
}`}
|
||||||
style={{ fontWeight: 500 }}
|
style={{ fontWeight: 500 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// Only allow edit on click if we're not dragging
|
// Only allow edit on click if we're not dragging
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
const fullAppointment = appointments.find(a => a.id === appointment.id);
|
const fullAppointment = appointments.find(
|
||||||
|
(a) => a.id === appointment.id
|
||||||
|
);
|
||||||
if (fullAppointment) {
|
if (fullAppointment) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleEditAppointment(fullAppointment);
|
handleEditAppointment(fullAppointment);
|
||||||
@@ -529,12 +591,12 @@ export default function AppointmentsPage() {
|
|||||||
timeSlot,
|
timeSlot,
|
||||||
staffId,
|
staffId,
|
||||||
appointment,
|
appointment,
|
||||||
staff
|
staff,
|
||||||
}: {
|
}: {
|
||||||
timeSlot: TimeSlot,
|
timeSlot: TimeSlot;
|
||||||
staffId: string,
|
staffId: string;
|
||||||
appointment: ScheduledAppointment | undefined,
|
appointment: ScheduledAppointment | undefined;
|
||||||
staff: Staff
|
staff: Staff;
|
||||||
}) {
|
}) {
|
||||||
const [{ isOver, canDrop }, drop] = useDrop(() => ({
|
const [{ isOver, canDrop }, drop] = useDrop(() => ({
|
||||||
accept: ItemTypes.APPOINTMENT,
|
accept: ItemTypes.APPOINTMENT,
|
||||||
@@ -555,13 +617,13 @@ export default function AppointmentsPage() {
|
|||||||
<td
|
<td
|
||||||
ref={drop as unknown as React.RefObject<HTMLTableCellElement>}
|
ref={drop as unknown as React.RefObject<HTMLTableCellElement>}
|
||||||
key={`${timeSlot.time}-${staffId}`}
|
key={`${timeSlot.time}-${staffId}`}
|
||||||
className={`px-1 py-1 border relative h-14 ${isOver && canDrop ? 'bg-green-100' : ''}`}
|
className={`px-1 py-1 border relative h-14 ${isOver && canDrop ? "bg-green-100" : ""}`}
|
||||||
>
|
>
|
||||||
{appointment ? (
|
{appointment ? (
|
||||||
<DraggableAppointment appointment={appointment} staff={staff} />
|
<DraggableAppointment appointment={appointment} staff={staff} />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className={`w-full h-full ${isOver && canDrop ? 'bg-green-100' : 'text-gray-400 hover:bg-gray-100'} rounded flex items-center justify-center`}
|
className={`w-full h-full ${isOver && canDrop ? "bg-green-100" : "text-gray-400 hover:bg-gray-100"} rounded flex items-center justify-center`}
|
||||||
onClick={() => handleCreateAppointmentAtSlot(timeSlot, staffId)}
|
onClick={() => handleCreateAppointmentAtSlot(timeSlot, staffId)}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
@@ -573,7 +635,10 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
<Sidebar
|
||||||
|
isMobileOpen={isMobileMenuOpen}
|
||||||
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||||
@@ -582,7 +647,9 @@ export default function AppointmentsPage() {
|
|||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Appointment Schedule</h1>
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
Appointment Schedule
|
||||||
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
View and manage the dental practice schedule
|
View and manage the dental practice schedule
|
||||||
</p>
|
</p>
|
||||||
@@ -604,7 +671,9 @@ export default function AppointmentsPage() {
|
|||||||
<Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade">
|
<Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade">
|
||||||
<Item
|
<Item
|
||||||
onClick={({ props }) => {
|
onClick={({ props }) => {
|
||||||
const fullAppointment = appointments.find(a => a.id === props.appointmentId);
|
const fullAppointment = appointments.find(
|
||||||
|
(a) => a.id === props.appointmentId
|
||||||
|
);
|
||||||
if (fullAppointment) {
|
if (fullAppointment) {
|
||||||
handleEditAppointment(fullAppointment);
|
handleEditAppointment(fullAppointment);
|
||||||
}
|
}
|
||||||
@@ -617,15 +686,21 @@ export default function AppointmentsPage() {
|
|||||||
</Item>
|
</Item>
|
||||||
<Item
|
<Item
|
||||||
onClick={({ props }) => {
|
onClick={({ props }) => {
|
||||||
const fullAppointment = appointments.find(a => a.id === props.appointmentId);
|
const fullAppointment = appointments.find(
|
||||||
|
(a) => a.id === props.appointmentId
|
||||||
|
);
|
||||||
if (fullAppointment) {
|
if (fullAppointment) {
|
||||||
// Set the appointment and patient IDs for the claim modal
|
// Set the appointment and patient IDs for the claim modal
|
||||||
setClaimAppointmentId(fullAppointment.id ?? null);
|
setClaimAppointmentId(fullAppointment.id ?? null);
|
||||||
setClaimPatientId(fullAppointment.patientId);
|
setClaimPatientId(fullAppointment.patientId);
|
||||||
|
|
||||||
// Find the patient name for the toast notification
|
// Find the patient name for the toast notification
|
||||||
const patient = patients.find(p => p.id === fullAppointment.patientId);
|
const patient = patients.find(
|
||||||
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : `Patient #${fullAppointment.patientId}`;
|
(p) => p.id === fullAppointment.patientId
|
||||||
|
);
|
||||||
|
const patientName = patient
|
||||||
|
? `${patient.firstName} ${patient.lastName}`
|
||||||
|
: `Patient #${fullAppointment.patientId}`;
|
||||||
|
|
||||||
// Show a toast notification
|
// Show a toast notification
|
||||||
toast({
|
toast({
|
||||||
@@ -644,7 +719,9 @@ export default function AppointmentsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item
|
<Item
|
||||||
onClick={({ props }) => handleDeleteAppointment(props.appointmentId)}
|
onClick={({ props }) =>
|
||||||
|
handleDeleteAppointment(props.appointmentId)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 text-red-600">
|
<span className="flex items-center gap-2 text-red-600">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
@@ -663,7 +740,9 @@ export default function AppointmentsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setSelectedDate(addDays(selectedDate, -1))}
|
onClick={() =>
|
||||||
|
setSelectedDate(addDays(selectedDate, -1))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -671,7 +750,9 @@ export default function AppointmentsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setSelectedDate(addDays(selectedDate, 1))}
|
onClick={() =>
|
||||||
|
setSelectedDate(addDays(selectedDate, 1))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -685,14 +766,18 @@ export default function AppointmentsPage() {
|
|||||||
<table className="w-full border-collapse min-w-[800px]">
|
<table className="w-full border-collapse min-w-[800px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 border bg-gray-50 w-[100px]">Time</th>
|
<th className="p-2 border bg-gray-50 w-[100px]">
|
||||||
{staffMembers.map(staff => (
|
Time
|
||||||
|
</th>
|
||||||
|
{staffMembers.map((staff) => (
|
||||||
<th
|
<th
|
||||||
key={staff.id}
|
key={staff.id}
|
||||||
className={`p-2 border bg-gray-50 ${staff.role === 'doctor' ? 'font-bold' : ''}`}
|
className={`p-2 border bg-gray-50 ${staff.role === "doctor" ? "font-bold" : ""}`}
|
||||||
>
|
>
|
||||||
{staff.name}
|
{staff.name}
|
||||||
<div className="text-xs text-gray-500">{staff.role}</div>
|
<div className="text-xs text-gray-500">
|
||||||
|
{staff.role}
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -708,7 +793,10 @@ export default function AppointmentsPage() {
|
|||||||
key={`${timeSlot.time}-${staff.id}`}
|
key={`${timeSlot.time}-${staff.id}`}
|
||||||
timeSlot={timeSlot}
|
timeSlot={timeSlot}
|
||||||
staffId={staff.id}
|
staffId={staff.id}
|
||||||
appointment={getAppointmentAtSlot(timeSlot, staff.id)}
|
appointment={getAppointmentAtSlot(
|
||||||
|
timeSlot,
|
||||||
|
staff.id
|
||||||
|
)}
|
||||||
staff={staff}
|
staff={staff}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -726,7 +814,9 @@ export default function AppointmentsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle>Calendar</CardTitle>
|
<CardTitle>Calendar</CardTitle>
|
||||||
<CardDescription>Select a date to view or schedule appointments</CardDescription>
|
<CardDescription>
|
||||||
|
Select a date to view or schedule appointments
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Calendar
|
<Calendar
|
||||||
@@ -745,32 +835,54 @@ export default function AppointmentsPage() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
<span>Appointments</span>
|
<span>Appointments</span>
|
||||||
<Button variant="ghost" size="icon" onClick={() => refetchAppointments()}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => refetchAppointments()}
|
||||||
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Statistics for {formattedDate}</CardDescription>
|
<CardDescription>
|
||||||
|
Statistics for {formattedDate}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">Total appointments:</span>
|
<span className="text-sm text-gray-500">
|
||||||
<span className="font-semibold">{selectedDateAppointments.length}</span>
|
Total appointments:
|
||||||
</div>
|
</span>
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-gray-500">With doctors:</span>
|
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{processedAppointments.filter(apt =>
|
{selectedDateAppointments.length}
|
||||||
staffMembers.find(s => s.id === apt.staffId)?.role === 'doctor'
|
|
||||||
).length}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">With hygienists:</span>
|
<span className="text-sm text-gray-500">
|
||||||
|
With doctors:
|
||||||
|
</span>
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{processedAppointments.filter(apt =>
|
{
|
||||||
staffMembers.find(s => s.id === apt.staffId)?.role === 'hygienist'
|
processedAppointments.filter(
|
||||||
).length}
|
(apt) =>
|
||||||
|
staffMembers.find((s) => s.id === apt.staffId)
|
||||||
|
?.role === "doctor"
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
With hygienists:
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{
|
||||||
|
processedAppointments.filter(
|
||||||
|
(apt) =>
|
||||||
|
staffMembers.find((s) => s.id === apt.staffId)
|
||||||
|
?.role === "hygienist"
|
||||||
|
).length
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -787,7 +899,10 @@ export default function AppointmentsPage() {
|
|||||||
open={isAddModalOpen}
|
open={isAddModalOpen}
|
||||||
onOpenChange={setIsAddModalOpen}
|
onOpenChange={setIsAddModalOpen}
|
||||||
onSubmit={handleAppointmentSubmit}
|
onSubmit={handleAppointmentSubmit}
|
||||||
isLoading={createAppointmentMutation.isPending || updateAppointmentMutation.isPending}
|
isLoading={
|
||||||
|
createAppointmentMutation.isPending ||
|
||||||
|
updateAppointmentMutation.isPending
|
||||||
|
}
|
||||||
appointment={editingAppointment}
|
appointment={editingAppointment}
|
||||||
patients={patients}
|
patients={patients}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,14 +7,23 @@ import { StatCard } from "@/components/ui/stat-card";
|
|||||||
import { PatientTable } from "@/components/patients/patient-table";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||||
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
import {
|
||||||
// import { InsertPatient, Patient, UpdatePatient, Appointment, InsertAppointment, UpdateAppointment } from "@repo/db/shared/schemas";
|
AppointmentUncheckedCreateInputObjectSchema,
|
||||||
import { Users, Calendar, CheckCircle, CreditCard, Plus, Clock } from "lucide-react";
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
|
} from "@repo/db/shared/schemas";
|
||||||
|
import {
|
||||||
|
Users,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle,
|
||||||
|
CreditCard,
|
||||||
|
Plus,
|
||||||
|
Clock,
|
||||||
|
} from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -23,78 +32,105 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
//creating types out of schema auto generated.
|
//creating types out of schema auto generated.
|
||||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||||
|
|
||||||
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const insertAppointmentSchema = (
|
||||||
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
});
|
});
|
||||||
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
||||||
|
|
||||||
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const updateAppointmentSchema = (
|
||||||
id: true,
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
createdAt: true,
|
)
|
||||||
}).partial();
|
.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
||||||
|
|
||||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const PatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
appointments: true,
|
appointments: true,
|
||||||
});
|
});
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const insertPatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
});
|
});
|
||||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||||
|
|
||||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const updatePatientSchema = (
|
||||||
id: true,
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
createdAt: true,
|
)
|
||||||
userId: true,
|
.omit({
|
||||||
}).partial();
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||||
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
||||||
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
||||||
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
|
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
|
||||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined);
|
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
||||||
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | undefined>(undefined);
|
undefined
|
||||||
|
);
|
||||||
|
const [selectedAppointment, setSelectedAppointment] = useState<
|
||||||
|
Appointment | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Fetch patients
|
// Fetch patients
|
||||||
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({
|
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||||
queryKey: ["/api/patients"],
|
Patient[]
|
||||||
|
>({
|
||||||
|
queryKey: ["/api/patients/"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/patients/");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch appointments
|
// Fetch appointments
|
||||||
const {
|
const {
|
||||||
data: appointments = [] as Appointment[],
|
data: appointments = [] as Appointment[],
|
||||||
isLoading: isLoadingAppointments
|
isLoading: isLoadingAppointments,
|
||||||
} = useQuery<Appointment[]>({
|
} = useQuery<Appointment[]>({
|
||||||
queryKey: ["/api/appointments"],
|
queryKey: ["/api/appointments/all"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/appointments/all");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add patient mutation
|
// Add patient mutation
|
||||||
const addPatientMutation = useMutation({
|
const addPatientMutation = useMutation({
|
||||||
mutationFn: async (patient: InsertPatient) => {
|
mutationFn: async (patient: InsertPatient) => {
|
||||||
const res = await apiRequest("POST", "/api/patients", patient);
|
const res = await apiRequest("POST", "/api/patients/", patient);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsAddPatientOpen(false);
|
setIsAddPatientOpen(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Patient added successfully!",
|
description: "Patient added successfully!",
|
||||||
@@ -112,13 +148,19 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// Update patient mutation
|
// Update patient mutation
|
||||||
const updatePatientMutation = useMutation({
|
const updatePatientMutation = useMutation({
|
||||||
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => {
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
patient,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
patient: UpdatePatient;
|
||||||
|
}) => {
|
||||||
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsAddPatientOpen(false);
|
setIsAddPatientOpen(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Patient updated successfully!",
|
description: "Patient updated successfully!",
|
||||||
@@ -142,14 +184,25 @@ export default function Dashboard() {
|
|||||||
if (user) {
|
if (user) {
|
||||||
addPatientMutation.mutate({
|
addPatientMutation.mutate({
|
||||||
...patient,
|
...patient,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePatient = (patient: UpdatePatient) => {
|
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
|
||||||
if (currentPatient) {
|
if (currentPatient && user) {
|
||||||
updatePatientMutation.mutate({ id: currentPatient.id, patient });
|
const { id, ...sanitizedPatient } = patient;
|
||||||
|
updatePatientMutation.mutate({
|
||||||
|
id: currentPatient.id,
|
||||||
|
patient: sanitizedPatient,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("No current patient or user found for update");
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Cannot update patient: No patient or user found",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,7 +219,7 @@ export default function Dashboard() {
|
|||||||
// Create appointment mutation
|
// Create appointment mutation
|
||||||
const createAppointmentMutation = useMutation({
|
const createAppointmentMutation = useMutation({
|
||||||
mutationFn: async (appointment: InsertAppointment) => {
|
mutationFn: async (appointment: InsertAppointment) => {
|
||||||
const res = await apiRequest("POST", "/api/appointments", appointment);
|
const res = await apiRequest("POST", "/api/appointments/", appointment);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -175,7 +228,9 @@ export default function Dashboard() {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Appointment created successfully.",
|
description: "Appointment created successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
// Invalidate both appointments and patients queries
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -188,8 +243,18 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// Update appointment mutation
|
// Update appointment mutation
|
||||||
const updateAppointmentMutation = useMutation({
|
const updateAppointmentMutation = useMutation({
|
||||||
mutationFn: async ({ id, appointment }: { id: number; appointment: UpdateAppointment }) => {
|
mutationFn: async ({
|
||||||
const res = await apiRequest("PUT", `/api/appointments/${id}`, appointment);
|
id,
|
||||||
|
appointment,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
appointment: UpdateAppointment;
|
||||||
|
}) => {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"PUT",
|
||||||
|
`/api/appointments/${id}`,
|
||||||
|
appointment
|
||||||
|
);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -198,7 +263,9 @@ export default function Dashboard() {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Appointment updated successfully.",
|
description: "Appointment updated successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
// Invalidate both appointments and patients queries
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -210,8 +277,10 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle appointment submission (create or update)
|
// Handle appointment submission (create or update)
|
||||||
const handleAppointmentSubmit = (appointmentData: InsertAppointment | UpdateAppointment) => {
|
const handleAppointmentSubmit = (
|
||||||
if (selectedAppointment && typeof selectedAppointment.id === 'number') {
|
appointmentData: InsertAppointment | UpdateAppointment
|
||||||
|
) => {
|
||||||
|
if (selectedAppointment && typeof selectedAppointment.id === "number") {
|
||||||
updateAppointmentMutation.mutate({
|
updateAppointmentMutation.mutate({
|
||||||
id: selectedAppointment.id,
|
id: selectedAppointment.id,
|
||||||
appointment: appointmentData as UpdateAppointment,
|
appointment: appointmentData as UpdateAppointment,
|
||||||
@@ -219,7 +288,7 @@ export default function Dashboard() {
|
|||||||
} else {
|
} else {
|
||||||
if (user) {
|
if (user) {
|
||||||
createAppointmentMutation.mutate({
|
createAppointmentMutation.mutate({
|
||||||
...appointmentData as InsertAppointment,
|
...(appointmentData as InsertAppointment),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -228,23 +297,24 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// Since we removed filters, just return all patients
|
// Since we removed filters, just return all patients
|
||||||
const filteredPatients = patients;
|
const filteredPatients = patients;
|
||||||
|
const today = format(new Date(), "yyyy-MM-dd");
|
||||||
|
|
||||||
// Get today's date in YYYY-MM-DD format
|
const todaysAppointments = appointments.filter((appointment) => {
|
||||||
const today = format(new Date(), 'yyyy-MM-dd');
|
const appointmentDate = format(new Date(appointment.date), "yyyy-MM-dd");
|
||||||
|
return appointmentDate === today;
|
||||||
// Filter appointments for today
|
});
|
||||||
const todaysAppointments = appointments.filter(
|
|
||||||
(appointment) => appointment.date === today
|
|
||||||
);
|
|
||||||
|
|
||||||
// Count completed appointments today
|
// Count completed appointments today
|
||||||
const completedTodayCount = todaysAppointments.filter(
|
const completedTodayCount = todaysAppointments.filter((appointment) => {
|
||||||
(appointment) => appointment.status === 'completed'
|
return appointment.status === "completed";
|
||||||
).length;
|
}).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
<Sidebar
|
||||||
|
isMobileOpen={isMobileMenuOpen}
|
||||||
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||||
@@ -281,7 +351,9 @@ export default function Dashboard() {
|
|||||||
{/* Today's Appointments Section */}
|
{/* Today's Appointments Section */}
|
||||||
<div className="flex flex-col space-y-4 mb-6">
|
<div className="flex flex-col space-y-4 mb-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
<h2 className="text-xl font-medium text-gray-800">Today's Appointments</h2>
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
|
Today's Appointments
|
||||||
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
className="mt-2 md:mt-0"
|
className="mt-2 md:mt-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -299,33 +371,70 @@ export default function Dashboard() {
|
|||||||
{todaysAppointments.length > 0 ? (
|
{todaysAppointments.length > 0 ? (
|
||||||
<div className="divide-y">
|
<div className="divide-y">
|
||||||
{todaysAppointments.map((appointment) => {
|
{todaysAppointments.map((appointment) => {
|
||||||
const patient = patients.find(p => p.id === appointment.patientId);
|
const patient = patients.find(
|
||||||
|
(p) => p.id === appointment.patientId
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div key={appointment.id} className="p-4 flex items-center justify-between">
|
<div
|
||||||
|
key={appointment.id}
|
||||||
|
className="p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="h-10 w-10 rounded-full bg-primary bg-opacity-10 text-primary flex items-center justify-center">
|
<div className="h-10 w-10 rounded-full bg-primary bg-opacity-10 text-primary flex items-center justify-center">
|
||||||
<Clock className="h-5 w-5" />
|
<Clock className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">
|
<h3 className="font-medium">
|
||||||
{patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown Patient'}
|
{patient
|
||||||
|
? `${patient.firstName} ${patient.lastName}`
|
||||||
|
: "Unknown Patient"}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-sm text-gray-500 flex items-center space-x-2">
|
<div className="text-sm text-gray-500 flex items-center space-x-2">
|
||||||
<span>{new Date(appointment.startTime).toLocaleString()} - {new Date(appointment.endTime).toLocaleString()}</span>
|
<span>
|
||||||
|
{new Date(
|
||||||
|
`${appointment.date.toString().slice(0, 10)}T${appointment.startTime}:00`
|
||||||
|
).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}{" "}
|
||||||
|
-{" "}
|
||||||
|
{new Date(
|
||||||
|
`${appointment.date.toString().slice(0, 10)}T${appointment.endTime}:00`
|
||||||
|
).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{appointment.type.charAt(0).toUpperCase() + appointment.type.slice(1)}</span>
|
<span>
|
||||||
|
{appointment.type.charAt(0).toUpperCase() +
|
||||||
|
appointment.type.slice(1)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
<span
|
||||||
${appointment.status === 'completed' ? 'bg-green-100 text-green-800' :
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||||
appointment.status === 'cancelled' ? 'bg-red-100 text-red-800' :
|
${
|
||||||
appointment.status === 'confirmed' ? 'bg-blue-100 text-blue-800' :
|
appointment.status === "completed"
|
||||||
'bg-yellow-100 text-yellow-800'}`}>
|
? "bg-green-100 text-green-800"
|
||||||
{appointment.status ? appointment.status.charAt(0).toUpperCase() + appointment.status.slice(1) : 'Scheduled'}
|
: appointment.status === "cancelled"
|
||||||
|
? "bg-red-100 text-red-800"
|
||||||
|
: appointment.status === "confirmed"
|
||||||
|
? "bg-blue-100 text-blue-800"
|
||||||
|
: "bg-yellow-100 text-yellow-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{appointment.status
|
||||||
|
? appointment.status.charAt(0).toUpperCase() +
|
||||||
|
appointment.status.slice(1)
|
||||||
|
: "Scheduled"}
|
||||||
</span>
|
</span>
|
||||||
<Link to="/appointments" className="text-primary hover:text-primary/80 text-sm">
|
<Link
|
||||||
|
to="/appointments"
|
||||||
|
className="text-primary hover:text-primary/80 text-sm"
|
||||||
|
>
|
||||||
View All
|
View All
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,7 +445,9 @@ export default function Dashboard() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" />
|
<Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" />
|
||||||
<h3 className="text-lg font-medium text-gray-900">No appointments today</h3>
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
No appointments today
|
||||||
|
</h3>
|
||||||
<p className="mt-1 text-gray-500">
|
<p className="mt-1 text-gray-500">
|
||||||
You don't have any appointments scheduled for today.
|
You don't have any appointments scheduled for today.
|
||||||
</p>
|
</p>
|
||||||
@@ -360,7 +471,9 @@ export default function Dashboard() {
|
|||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{/* Patient Header */}
|
{/* Patient Header */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
<h2 className="text-xl font-medium text-gray-800">Patient Management</h2>
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
|
Patient Management
|
||||||
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
className="mt-2 md:mt-0"
|
className="mt-2 md:mt-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -373,8 +486,6 @@ export default function Dashboard() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and filters removed */}
|
|
||||||
|
|
||||||
{/* Patient Table */}
|
{/* Patient Table */}
|
||||||
<PatientTable
|
<PatientTable
|
||||||
patients={filteredPatients}
|
patients={filteredPatients}
|
||||||
@@ -390,7 +501,9 @@ export default function Dashboard() {
|
|||||||
open={isAddPatientOpen}
|
open={isAddPatientOpen}
|
||||||
onOpenChange={setIsAddPatientOpen}
|
onOpenChange={setIsAddPatientOpen}
|
||||||
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
|
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
|
||||||
isLoading={addPatientMutation.isPending || updatePatientMutation.isPending}
|
isLoading={
|
||||||
|
addPatientMutation.isPending || updatePatientMutation.isPending
|
||||||
|
}
|
||||||
patient={currentPatient}
|
patient={currentPatient}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -408,60 +521,76 @@ export default function Dashboard() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="h-16 w-16 rounded-full bg-primary text-white flex items-center justify-center text-xl font-medium">
|
<div className="h-16 w-16 rounded-full bg-primary text-white flex items-center justify-center text-xl font-medium">
|
||||||
{currentPatient.firstName.charAt(0)}{currentPatient.lastName.charAt(0)}
|
{currentPatient.firstName.charAt(0)}
|
||||||
|
{currentPatient.lastName.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold">{currentPatient.firstName} {currentPatient.lastName}</h3>
|
<h3 className="text-xl font-semibold">
|
||||||
<p className="text-gray-500">Patient ID: {currentPatient.id.toString().padStart(4, '0')}</p>
|
{currentPatient.firstName} {currentPatient.lastName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Patient ID: {currentPatient.id.toString().padStart(4, "0")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">Personal Information</h4>
|
<h4 className="font-medium text-gray-900">
|
||||||
|
Personal Information
|
||||||
|
</h4>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Date of Birth:</span>{' '}
|
<span className="text-gray-500">Date of Birth:</span>{" "}
|
||||||
{new Date(currentPatient.dateOfBirth).toLocaleDateString()}
|
{new Date(
|
||||||
|
currentPatient.dateOfBirth
|
||||||
|
).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Gender:</span>{' '}
|
<span className="text-gray-500">Gender:</span>{" "}
|
||||||
{currentPatient.gender.charAt(0).toUpperCase() + currentPatient.gender.slice(1)}
|
{currentPatient.gender.charAt(0).toUpperCase() +
|
||||||
|
currentPatient.gender.slice(1)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Status:</span>{' '}
|
<span className="text-gray-500">Status:</span>{" "}
|
||||||
<span className={`${
|
<span
|
||||||
currentPatient.status === 'active'
|
className={`${
|
||||||
? 'text-green-600'
|
currentPatient.status === "active"
|
||||||
: 'text-amber-600'
|
? "text-green-600"
|
||||||
} font-medium`}>
|
: "text-amber-600"
|
||||||
{currentPatient.status.charAt(0).toUpperCase() + currentPatient.status.slice(1)}
|
} font-medium`}
|
||||||
|
>
|
||||||
|
{currentPatient.status.charAt(0).toUpperCase() +
|
||||||
|
currentPatient.status.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">Contact Information</h4>
|
<h4 className="font-medium text-gray-900">
|
||||||
|
Contact Information
|
||||||
|
</h4>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Phone:</span>{' '}
|
<span className="text-gray-500">Phone:</span>{" "}
|
||||||
{currentPatient.phone}
|
{currentPatient.phone}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Email:</span>{' '}
|
<span className="text-gray-500">Email:</span>{" "}
|
||||||
{currentPatient.email || 'N/A'}
|
{currentPatient.email || "N/A"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Address:</span>{' '}
|
<span className="text-gray-500">Address:</span>{" "}
|
||||||
{currentPatient.address ? (
|
{currentPatient.address ? (
|
||||||
<>
|
<>
|
||||||
{currentPatient.address}
|
{currentPatient.address}
|
||||||
{currentPatient.city && `, ${currentPatient.city}`}
|
{currentPatient.city && `, ${currentPatient.city}`}
|
||||||
{currentPatient.zipCode && ` ${currentPatient.zipCode}`}
|
{currentPatient.zipCode &&
|
||||||
|
` ${currentPatient.zipCode}`}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'N/A'
|
"N/A"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -471,44 +600,46 @@ export default function Dashboard() {
|
|||||||
<h4 className="font-medium text-gray-900">Insurance</h4>
|
<h4 className="font-medium text-gray-900">Insurance</h4>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Provider:</span>{' '}
|
<span className="text-gray-500">Provider:</span>{" "}
|
||||||
{currentPatient.insuranceProvider
|
{currentPatient.insuranceProvider
|
||||||
? currentPatient.insuranceProvider === 'delta'
|
? currentPatient.insuranceProvider === "delta"
|
||||||
? 'Delta Dental'
|
? "Delta Dental"
|
||||||
: currentPatient.insuranceProvider === 'metlife'
|
: currentPatient.insuranceProvider === "metlife"
|
||||||
? 'MetLife'
|
? "MetLife"
|
||||||
: currentPatient.insuranceProvider === 'cigna'
|
: currentPatient.insuranceProvider === "cigna"
|
||||||
? 'Cigna'
|
? "Cigna"
|
||||||
: currentPatient.insuranceProvider === 'aetna'
|
: currentPatient.insuranceProvider === "aetna"
|
||||||
? 'Aetna'
|
? "Aetna"
|
||||||
: currentPatient.insuranceProvider
|
: currentPatient.insuranceProvider
|
||||||
: 'N/A'}
|
: "N/A"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">ID:</span>{' '}
|
<span className="text-gray-500">ID:</span>{" "}
|
||||||
{currentPatient.insuranceId || 'N/A'}
|
{currentPatient.insuranceId || "N/A"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Group Number:</span>{' '}
|
<span className="text-gray-500">Group Number:</span>{" "}
|
||||||
{currentPatient.groupNumber || 'N/A'}
|
{currentPatient.groupNumber || "N/A"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Policy Holder:</span>{' '}
|
<span className="text-gray-500">Policy Holder:</span>{" "}
|
||||||
{currentPatient.policyHolder || 'Self'}
|
{currentPatient.policyHolder || "Self"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">Medical Information</h4>
|
<h4 className="font-medium text-gray-900">
|
||||||
|
Medical Information
|
||||||
|
</h4>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Allergies:</span>{' '}
|
<span className="text-gray-500">Allergies:</span>{" "}
|
||||||
{currentPatient.allergies || 'None reported'}
|
{currentPatient.allergies || "None reported"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Medical Conditions:</span>{' '}
|
<span className="text-gray-500">Medical Conditions:</span>{" "}
|
||||||
{currentPatient.medicalConditions || 'None reported'}
|
{currentPatient.medicalConditions || "None reported"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -540,7 +671,10 @@ export default function Dashboard() {
|
|||||||
open={isAddAppointmentOpen}
|
open={isAddAppointmentOpen}
|
||||||
onOpenChange={setIsAddAppointmentOpen}
|
onOpenChange={setIsAddAppointmentOpen}
|
||||||
onSubmit={handleAppointmentSubmit}
|
onSubmit={handleAppointmentSubmit}
|
||||||
isLoading={createAppointmentMutation.isPending || updateAppointmentMutation.isPending}
|
isLoading={
|
||||||
|
createAppointmentMutation.isPending ||
|
||||||
|
updateAppointmentMutation.isPending
|
||||||
|
}
|
||||||
appointment={selectedAppointment}
|
appointment={selectedAppointment}
|
||||||
patients={patients}
|
patients={patients}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,39 +4,55 @@ import { TopAppBar } from "@/components/layout/top-app-bar";
|
|||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { PatientTable } from "@/components/patients/patient-table";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||||
import { PatientSearch, SearchCriteria } from "@/components/patients/patient-search";
|
import {
|
||||||
|
PatientSearch,
|
||||||
|
SearchCriteria,
|
||||||
|
} from "@/components/patients/patient-search";
|
||||||
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
|
import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||||
// import { Patient, InsertPatient, UpdatePatient } from "@repo/db/shared/schemas";
|
// import { Patient, InsertPatient, UpdatePatient } from "@repo/db/shared/schemas";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const PatientSchema = (
|
||||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
appointments: true,
|
appointments: true,
|
||||||
});
|
});
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
const insertPatientSchema = (
|
||||||
id: true,
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
createdAt: true,
|
).omit({
|
||||||
});
|
|
||||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
|
||||||
|
|
||||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
}).partial();
|
});
|
||||||
|
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||||
|
|
||||||
|
const updatePatientSchema = (
|
||||||
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
)
|
||||||
|
.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||||
|
|
||||||
|
|
||||||
// Type for the ref to access modal methods
|
// Type for the ref to access modal methods
|
||||||
type AddPatientModalRef = {
|
type AddPatientModalRef = {
|
||||||
shouldSchedule: boolean;
|
shouldSchedule: boolean;
|
||||||
@@ -48,9 +64,13 @@ export default function PatientsPage() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
||||||
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
||||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined);
|
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(null);
|
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
|
const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
|
||||||
|
|
||||||
// File upload states
|
// File upload states
|
||||||
@@ -65,19 +85,23 @@ export default function PatientsPage() {
|
|||||||
isLoading: isLoadingPatients,
|
isLoading: isLoadingPatients,
|
||||||
refetch: refetchPatients,
|
refetch: refetchPatients,
|
||||||
} = useQuery<Patient[]>({
|
} = useQuery<Patient[]>({
|
||||||
queryKey: ["/api/patients"],
|
queryKey: ["/api/patients/"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/patients/");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add patient mutation
|
// Add patient mutation
|
||||||
const addPatientMutation = useMutation({
|
const addPatientMutation = useMutation({
|
||||||
mutationFn: async (patient: InsertPatient) => {
|
mutationFn: async (patient: InsertPatient) => {
|
||||||
const res = await apiRequest("POST", "/api/patients", patient);
|
const res = await apiRequest("POST", "/api/patients/", patient);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: (newPatient) => {
|
onSuccess: (newPatient) => {
|
||||||
setIsAddPatientOpen(false);
|
setIsAddPatientOpen(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Patient added successfully!",
|
description: "Patient added successfully!",
|
||||||
@@ -100,13 +124,19 @@ export default function PatientsPage() {
|
|||||||
|
|
||||||
// Update patient mutation
|
// Update patient mutation
|
||||||
const updatePatientMutation = useMutation({
|
const updatePatientMutation = useMutation({
|
||||||
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => {
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
patient,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
patient: UpdatePatient;
|
||||||
|
}) => {
|
||||||
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsAddPatientOpen(false);
|
setIsAddPatientOpen(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Patient updated successfully!",
|
description: "Patient updated successfully!",
|
||||||
@@ -131,14 +161,25 @@ export default function PatientsPage() {
|
|||||||
if (user) {
|
if (user) {
|
||||||
addPatientMutation.mutate({
|
addPatientMutation.mutate({
|
||||||
...patient,
|
...patient,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePatient = (patient: UpdatePatient) => {
|
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
|
||||||
if (currentPatient) {
|
if (currentPatient && user) {
|
||||||
updatePatientMutation.mutate({ id: currentPatient.id, patient });
|
const { id, ...sanitizedPatient } = patient;
|
||||||
|
updatePatientMutation.mutate({
|
||||||
|
id: currentPatient.id,
|
||||||
|
patient: sanitizedPatient,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("No current patient or user found for update");
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Cannot update patient: No patient or user found",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,7 +193,10 @@ export default function PatientsPage() {
|
|||||||
setIsViewPatientOpen(true);
|
setIsViewPatientOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading = isLoadingPatients || addPatientMutation.isPending || updatePatientMutation.isPending;
|
const isLoading =
|
||||||
|
isLoadingPatients ||
|
||||||
|
addPatientMutation.isPending ||
|
||||||
|
updatePatientMutation.isPending;
|
||||||
|
|
||||||
// Search handling
|
// Search handling
|
||||||
const handleSearch = (criteria: SearchCriteria) => {
|
const handleSearch = (criteria: SearchCriteria) => {
|
||||||
@@ -195,15 +239,15 @@ export default function PatientsPage() {
|
|||||||
// Set up a Promise to handle file reading
|
// Set up a Promise to handle file reading
|
||||||
const fileReadPromise = new Promise<string>((resolve, reject) => {
|
const fileReadPromise = new Promise<string>((resolve, reject) => {
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
if (event.target && typeof event.target.result === 'string') {
|
if (event.target && typeof event.target.result === "string") {
|
||||||
resolve(event.target.result);
|
resolve(event.target.result);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Failed to read file as base64'));
|
reject(new Error("Failed to read file as base64"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
reject(new Error('Error reading file'));
|
reject(new Error("Error reading file"));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read the file as a data URL (base64)
|
// Read the file as a data URL (base64)
|
||||||
@@ -214,20 +258,22 @@ export default function PatientsPage() {
|
|||||||
const base64Data = await fileReadPromise;
|
const base64Data = await fileReadPromise;
|
||||||
|
|
||||||
// Send file to server as base64
|
// Send file to server as base64
|
||||||
const response = await fetch('/api/upload-file', {
|
const response = await fetch("/api/upload-file", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
pdfData: base64Data,
|
pdfData: base64Data,
|
||||||
filename: uploadedFile.name
|
filename: uploadedFile.name,
|
||||||
}),
|
}),
|
||||||
credentials: 'include'
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
throw new Error(
|
||||||
|
`Server returned ${response.status}: ${response.statusText}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -238,7 +284,7 @@ export default function PatientsPage() {
|
|||||||
firstName: data.extractedInfo.firstName,
|
firstName: data.extractedInfo.firstName,
|
||||||
lastName: data.extractedInfo.lastName,
|
lastName: data.extractedInfo.lastName,
|
||||||
dateOfBirth: data.extractedInfo.dateOfBirth,
|
dateOfBirth: data.extractedInfo.dateOfBirth,
|
||||||
insuranceId: data.extractedInfo.insuranceId
|
insuranceId: data.extractedInfo.insuranceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
setExtractedInfo(simplifiedInfo);
|
setExtractedInfo(simplifiedInfo);
|
||||||
@@ -246,7 +292,8 @@ export default function PatientsPage() {
|
|||||||
// Show success message
|
// Show success message
|
||||||
toast({
|
toast({
|
||||||
title: "Information Extracted",
|
title: "Information Extracted",
|
||||||
description: "Basic patient information (name, DOB, ID) has been extracted successfully.",
|
description:
|
||||||
|
"Basic patient information (name, DOB, ID) has been extracted successfully.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,7 +304,6 @@ export default function PatientsPage() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsAddPatientOpen(true);
|
setIsAddPatientOpen(true);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message || "Failed to extract information");
|
throw new Error(data.message || "Failed to extract information");
|
||||||
}
|
}
|
||||||
@@ -265,7 +311,10 @@ export default function PatientsPage() {
|
|||||||
console.error("Error extracting information:", error);
|
console.error("Error extracting information:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: error instanceof Error ? error.message : "Failed to extract information from file",
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to extract information from file",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -282,18 +331,18 @@ export default function PatientsPage() {
|
|||||||
const term = searchCriteria.searchTerm.toLowerCase();
|
const term = searchCriteria.searchTerm.toLowerCase();
|
||||||
return patients.filter((patient) => {
|
return patients.filter((patient) => {
|
||||||
switch (searchCriteria.searchBy) {
|
switch (searchCriteria.searchBy) {
|
||||||
case 'name':
|
case "name":
|
||||||
return (
|
return (
|
||||||
patient.firstName.toLowerCase().includes(term) ||
|
patient.firstName.toLowerCase().includes(term) ||
|
||||||
patient.lastName.toLowerCase().includes(term)
|
patient.lastName.toLowerCase().includes(term)
|
||||||
);
|
);
|
||||||
case 'phone':
|
case "phone":
|
||||||
return patient.phone.toLowerCase().includes(term);
|
return patient.phone.toLowerCase().includes(term);
|
||||||
case 'insuranceProvider':
|
case "insuranceProvider":
|
||||||
return patient.insuranceProvider?.toLowerCase().includes(term);
|
return patient.insuranceProvider?.toLowerCase().includes(term);
|
||||||
case 'insuranceId':
|
case "insuranceId":
|
||||||
return patient.insuranceId?.toLowerCase().includes(term);
|
return patient.insuranceId?.toLowerCase().includes(term);
|
||||||
case 'all':
|
case "all":
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
patient.firstName.toLowerCase().includes(term) ||
|
patient.firstName.toLowerCase().includes(term) ||
|
||||||
@@ -311,7 +360,10 @@ export default function PatientsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
<Sidebar
|
||||||
|
isMobileOpen={isMobileMenuOpen}
|
||||||
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||||
@@ -409,8 +461,10 @@ export default function PatientsPage() {
|
|||||||
<div className="flex items-center my-4 px-2 py-1 bg-muted rounded-md text-sm">
|
<div className="flex items-center my-4 px-2 py-1 bg-muted rounded-md text-sm">
|
||||||
<p>
|
<p>
|
||||||
Found {filteredPatients.length}
|
Found {filteredPatients.length}
|
||||||
{filteredPatients.length === 1 ? ' patient' : ' patients'}
|
{filteredPatients.length === 1 ? " patient" : " patients"}
|
||||||
{searchCriteria.searchBy !== 'all' ? ` with ${searchCriteria.searchBy}` : ''}
|
{searchCriteria.searchBy !== "all"
|
||||||
|
? ` with ${searchCriteria.searchBy}`
|
||||||
|
: ""}
|
||||||
matching "{searchCriteria.searchTerm}"
|
matching "{searchCriteria.searchTerm}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,12 +487,16 @@ export default function PatientsPage() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
patient={currentPatient}
|
patient={currentPatient}
|
||||||
// Pass extracted info as a separate prop to avoid triggering edit mode
|
// Pass extracted info as a separate prop to avoid triggering edit mode
|
||||||
extractedInfo={!currentPatient && extractedInfo ? {
|
extractedInfo={
|
||||||
firstName: extractedInfo.firstName || "",
|
!currentPatient && extractedInfo
|
||||||
lastName: extractedInfo.lastName || "",
|
? {
|
||||||
dateOfBirth: extractedInfo.dateOfBirth || "",
|
firstName: extractedInfo.firstName || "",
|
||||||
insuranceId: extractedInfo.insuranceId || ""
|
lastName: extractedInfo.lastName || "",
|
||||||
} : undefined}
|
dateOfBirth: extractedInfo.dateOfBirth || "",
|
||||||
|
insuranceId: extractedInfo.insuranceId || "",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
1497
package-lock.json
generated
1497
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Appointment" ALTER COLUMN "startTime" SET DATA TYPE TEXT,
|
||||||
|
ALTER COLUMN "endTime" SET DATA TYPE TEXT;
|
||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
93
packages/db/prisma/seed.ts
Normal file
93
packages/db/prisma/seed.ts
Normal 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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user