diff --git a/src/controllers/aptcontrol.js b/src/controllers/aptcontrol.js index 109c59d..7aec91e 100644 --- a/src/controllers/aptcontrol.js +++ b/src/controllers/aptcontrol.js @@ -1,26 +1,77 @@ -import Appointment from '../models/appointment.js'; -import mongoose from 'mongoose'; +import Appointment from "../models/appointment.js"; +import mongoose from "mongoose"; + +const DAYS = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +const validateWorkingHours = (doctorDoc, date, time) => { + if (!doctorDoc.workingHours || doctorDoc.workingHours.length === 0) { + return { valid: true }; // No working hours defined — skip validation + } + + const dayName = DAYS[new Date(date).getDay()]; + const workingDay = doctorDoc.workingHours.find((d) => d.day === dayName); + + if (!workingDay || !workingDay.isAvailable || workingDay.slots.length === 0) { + return { + valid: false, + message: `Dr. ${doctorDoc.name} is not available on ${dayName}.`, + }; + } + + const withinSlot = workingDay.slots.some( + (slot) => time >= slot.startTime && time < slot.endTime, + ); + + if (!withinSlot) { + const slotList = workingDay.slots + .map((s) => `${s.startTime}–${s.endTime}`) + .join(", "); + return { + valid: false, + message: `Dr. ${doctorDoc.name} is only available on ${dayName} during: ${slotList}.`, + }; + } + + return { valid: true }; +}; const createAppointment = async (req, res) => { try { - const { patient, dept, doctor } = req.body; + const { patient, dept, doctor, date, time } = req.body; - const [patientDoc, deptDoc, doctorDoc] = await Promise.all([ - mongoose.model('Patient').findById(patient), - mongoose.model('Department').findById(dept), - mongoose.model('User').findOne({ _id: doctor, role: 'doctor' }) + mongoose.model("Patient").findById(patient), + mongoose.model("Department").findById(dept), + mongoose.model("User").findOne({ _id: doctor, role: "doctor" }), ]); - if (!patientDoc) return res.status(400).json({ message: 'Invalid patient ID' }); - if (!deptDoc) return res.status(400).json({ message: 'Invalid department ID' }); - if (!doctorDoc) return res.status(400).json({ message: 'Invalid doctor ID or user is not a doctor' }); + if (!patientDoc) return res.status(400).json({ message: "Invalid patient ID" }); + if (!deptDoc) return res.status(400).json({ message: "Invalid department ID" }); + if (!doctorDoc) + return res + .status(400) + .json({ message: "Invalid doctor ID or user is not a doctor" }); + + if (date && time) { + const { valid, message } = validateWorkingHours(doctorDoc, date, time); + if (!valid) { + return res.status(400).json({ message }); + } + } const appointment = new Appointment(req.body); await appointment.save(); res.status(201).json(appointment); } catch (err) { - console.error('Error creating appointment:', err); + console.error("Error creating appointment:", err); res.status(400).json({ message: err.message }); } }; @@ -30,43 +81,47 @@ const getAppointments = async (req, res) => { const { id: userId, role } = req.user; let query = {}; - if (role === 'doctor') { - query.doctor = userId; - } else if (role === 'patient') { - const patient = await mongoose.model('Patient').findOne({ email: req.user.email }); + if (role === "doctor") { + query.doctor = userId; + } else if (role === "patient") { + const patient = await mongoose.model("Patient").findOne({ email: req.user.email }); if (!patient) { - return res.status(404).json({ message: 'Patient record not found' }); + return res.status(404).json({ message: "Patient record not found" }); } query.patient = patient._id; } const appointments = await Appointment.find(query) - .populate('patient', 'name email') - .populate('doctor', 'name spec') - .populate('dept', 'dept') + .populate("patient", "name email") + .populate("doctor", "name spec") + .populate("dept", "dept") .lean(); res.json(appointments); } catch (err) { - console.error('Error fetching appointments:', err); - res.status(500).json({ message: 'Server error while fetching appointments', error: err.message }); + console.error("Error fetching appointments:", err); + res + .status(500) + .json({ message: "Server error while fetching appointments", error: err.message }); } }; const getAppointmentById = async (req, res) => { try { const appointment = await Appointment.findById(req.params.id) - .populate('patient', 'name email') - .populate('doctor', 'name spec') - .populate('dept', 'dept') + .populate("patient", "name email") + .populate("doctor", "name spec") + .populate("dept", "dept") .lean(); - if (!appointment) return res.status(404).json({ message: 'Appointment not found' }); + if (!appointment) return res.status(404).json({ message: "Appointment not found" }); res.json(appointment); } catch (err) { - console.error('Error fetching appointment by ID:', err); - res.status(500).json({ message: 'Server error while fetching appointment', error: err.message }); + console.error("Error fetching appointment by ID:", err); + res + .status(500) + .json({ message: "Server error while fetching appointment", error: err.message }); } }; @@ -79,7 +134,7 @@ const updateAppointment = async (req, res, next) => { if (!appointment) { return res.status(404).json({ success: false, - message: "Appointment not found" + message: "Appointment not found", }); } @@ -102,24 +157,22 @@ const updateAppointment = async (req, res, next) => { if (patient || dept || doctor) { const checks = []; - if (patient) checks.push(mongoose.model('Patient').findById(patient)); - if (dept) checks.push(mongoose.model('Department').findById(dept)); - if (doctor) checks.push( - mongoose.model('User').findOne({ _id: doctor, role: 'doctor' }) - ); + if (patient) checks.push(mongoose.model("Patient").findById(patient)); + if (dept) checks.push(mongoose.model("Department").findById(dept)); + if (doctor) + checks.push(mongoose.model("User").findOne({ _id: doctor, role: "doctor" })); const results = await Promise.all(checks); if (patient && !results[0]) - return res.status(400).json({ success: false, message: 'Invalid patient ID' }); - + return res.status(400).json({ success: false, message: "Invalid patient ID" }); } if (status) { const validTransitions = { scheduled: ["completed", "cancelled"], completed: [], - cancelled: [] + cancelled: [], }; const allowed = validTransitions[appointment.status] || []; @@ -127,7 +180,7 @@ const updateAppointment = async (req, res, next) => { if (!allowed.includes(status)) { return res.status(400).json({ success: false, - message: "Invalid status transition" + message: "Invalid status transition", }); } @@ -142,9 +195,8 @@ const updateAppointment = async (req, res, next) => { res.status(200).json({ success: true, - data: appointment + data: appointment, }); - } catch (err) { next(err); } @@ -169,4 +221,4 @@ export { getAppointmentById, updateAppointment, //deleteAppointment, -}; \ No newline at end of file +}; diff --git a/src/controllers/patient.js b/src/controllers/patient.js index 9492ffe..7d017ca 100644 --- a/src/controllers/patient.js +++ b/src/controllers/patient.js @@ -1,26 +1,74 @@ -import Patient from '../models/patient.js'; -import { createPatientPortalAccount } from '../services/patientPortal.service.js'; +import Patient from "../models/patient.js"; +import { createPatientPortalAccount } from "../services/patientPortal.service.js"; +import { checkUniqueUser } from "../utils/uniqueness.js"; +import User from "../models/User.js"; + +const normalizeEmail = (email) => { + if (!email) return email; + return email.trim().toLowerCase(); +}; + +const sanitizePhone = (phno) => { + if (!phno) return phno; + return phno.toString().replace(/\D/g, ""); +}; const createPatient = async (req, res, next) => { try { - const { name, email, phno, paymentMode } = req.body; + const { name, email, phno } = req.body; + + console.log(`Creating patient: ${name}, Email: ${email}`); + + const normalizedEmail = normalizeEmail(email); + const sanitizedPhno = sanitizePhone(phno); + + await checkUniqueUser(Patient, { + email: normalizedEmail, + phno: sanitizedPhno, + }); - console.log(`Creating patient: ${name}, Email: ${email}, PaymentMode: ${paymentMode}`); + const patient = new Patient({ + ...req.body, + email: normalizedEmail, + phno: sanitizedPhno, + }); - const patient = new Patient(req.body); await patient.save(); - if (paymentMode === "online" && email) { - await createPatientPortalAccount(email); + if (normalizedEmail) { + try { + await createPatientPortalAccount(normalizedEmail); + } catch (portalErr) { + console.error( + `Patient saved but portal creation failed for ${normalizedEmail}:`, + portalErr.message, + ); + + return res.status(201).json({ + success: true, + patient, + warning: + "Patient created but portal account could not be set up. Please retry manually.", + }); + } } res.status(201).json({ success: true, - patient + patient, }); - } catch (err) { console.error(`Error in createPatient: ${err.message}`); + + if (err.code === 11000) { + if (err.keyPattern?.email) { + return next(new Error("Email already exists")); + } + if (err.keyPattern?.phno) { + return next(new Error("Phone number already exists")); + } + } + next(err); } }; @@ -39,7 +87,7 @@ const getPatientById = async (req, res, next) => { const { id } = req.params; const patient = await Patient.findById(id); if (!patient) { - const error = new Error('Patient not found'); + const error = new Error("Patient not found"); error.statusCode = 404; return next(error); } @@ -53,45 +101,78 @@ const updatePatient = async (req, res, next) => { try { const { id } = req.params; - const patient = await Patient.findByIdAndUpdate(id, req.body, { - new: true, - runValidators: true + let normalizedEmail = req.body.email ? normalizeEmail(req.body.email) : undefined; + let sanitizedPhno = req.body.phno ? sanitizePhone(req.body.phno) : undefined; + + if (normalizedEmail || sanitizedPhno) { + await checkUniqueUser(Patient, { + email: normalizedEmail || undefined, + phno: sanitizedPhno || undefined, + excludeId: id, + }); + } + + const updateData = { ...req.body }; + if (normalizedEmail) updateData.email = normalizedEmail; + if (sanitizedPhno) updateData.phno = sanitizedPhno; + + const patient = await Patient.findByIdAndUpdate(id, updateData, { + new: true, + runValidators: true, }); if (!patient) { - const error = new Error('Patient not found'); + const error = new Error("Patient not found"); error.statusCode = 404; return next(error); } res.json(patient); } catch (err) { + console.error(`Error in updatePatient: ${err.message}`); + + if (err.code === 11000) { + if (err.keyPattern?.email) { + return next(new Error("Email already exists")); + } + if (err.keyPattern?.phno) { + return next(new Error("Phone number already exists")); + } + } + next(err); } }; const deletePatient = async (req, res, next) => { + const session = await Patient.startSession(); + session.startTransaction(); + try { const { id } = req.params; - const patient = await Patient.findByIdAndDelete(id); + const patient = await Patient.findByIdAndDelete(id, { session }); if (!patient) { - const error = new Error('Patient not found'); + await session.abortTransaction(); + const error = new Error("Patient not found"); error.statusCode = 404; return next(error); } - res.json({ message: 'Patient deleted successfully' }); + if (patient.email) { + await User.findOneAndDelete({ email: patient.email }, { session }); + } + + await session.commitTransaction(); + + res.json({ message: "Patient deleted successfully" }); } catch (err) { + await session.abortTransaction(); next(err); + } finally { + session.endSession(); } }; -export { - createPatient, - getPatients, - getPatientById, - updatePatient, - deletePatient -}; +export { createPatient, getPatients, getPatientById, updatePatient, deletePatient }; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index 3faac62..b1ee160 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -1,10 +1,18 @@ -export const errorHandler = (err, req, res) => { +export const errorHandler = (err, req, res, next) => { console.error(err); - const statusCode = err.statusCode || 500; + let statusCode = err.statusCode || 500; + let message = err.message || "Internal Server Error"; + + if (err.code === 11000) { + statusCode = 400; + if (err.keyPattern?.email) message = "Email already exists"; + else if (err.keyPattern?.phno) message = "Phone number already exists"; + else message = "Duplicate entry detected"; + } res.status(statusCode).json({ success: false, - message: err.message || "Internal Server Error", + message, }); -}; \ No newline at end of file +}; diff --git a/src/models/User.js b/src/models/User.js index 6d0245f..0ccd1b5 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -110,6 +110,7 @@ const userSchema = new mongoose.Schema( phno: { type: String, match: /^[0-9]{10}$/, + sparse: true, unique: true, }, dob: { type: Date }, diff --git a/src/models/patient.js b/src/models/patient.js index 675207a..e1d5e1e 100644 --- a/src/models/patient.js +++ b/src/models/patient.js @@ -5,11 +5,13 @@ const Patientschema = new mongoose.Schema( name: { type: String, required: true }, email: { type: String, + unique: true, required: true, match: /.+@.+\..+/, }, phno: { type: String, + unique: true, required: true, match: /^[0-9]{10}$/, }, @@ -46,8 +48,7 @@ const Patientschema = new mongoose.Schema( medical_history: { type: String }, paymentMode: { type: String, - enum: ["cash", "online"], - default: "cash", + required: false, }, }, { timestamps: true }, diff --git a/src/services/billing.service.js b/src/services/billing.service.js index 8d54caa..f441cd6 100644 --- a/src/services/billing.service.js +++ b/src/services/billing.service.js @@ -1,69 +1,125 @@ -import User from '../models/User.js'; -import bcrypt from 'bcrypt'; +import bcrypt from "bcrypt"; +import { checkUniqueUser } from "../utils/uniqueness.js"; +import UserModel from "../models/User.js"; + +const getUserModel = () => UserModel; + +const normalizeEmail = (email) => { + if (!email) return email; + return email.trim().toLowerCase(); +}; + +const sanitizePhone = (phno) => { + if (!phno) return phno; + return phno.toString().replace(/\D/g, ""); +}; const createBillingStaff = async (data) => { - const password = data.password || 'billing@123'; - const hashedPassword = await bcrypt.hash(password, 10); + const UserModel = await getUserModel(); - const billing = new User({ - ...data, - password: hashedPassword, - role: 'billing', + const email = normalizeEmail(data.email); + const phno = sanitizePhone(data.phno); + + await checkUniqueUser(UserModel, { email, phno }); + + const password = data.password || "billing@123"; + const hashedPassword = await bcrypt.hash(password, 10); + + try { + const billing = new UserModel({ + ...data, + email, + phno, + password: hashedPassword, + role: "billing", }); await billing.save(); return { - success: true, - message: 'Billing staff created successfully', + success: true, + message: "Billing staff created successfully", }; + } catch (err) { + if (err.code === 11000) { + if (err.keyPattern?.email) throw new Error("Email aleady exists"); + if (err.keyPattern?.phno) throw new Error("Phone number already exists"); + } + throw err; + } }; - const changePassword = async (userId, oldPassword, newPassword) => { - const billing = await User.findById(userId); - if(!billing) throw new Error('Billing staff not found'); + const UserModel = await getUserModel(); + const billing = await UserModel.findById(userId); + if (!billing) throw new Error("Billing staff not found"); - const isMatch = await bcrypt.compare(oldPassword, billing.password); - if(!isMatch) throw new Error('Old password is incorrect'); + const isMatch = await bcrypt.compare(oldPassword, billing.password); + if (!isMatch) throw new Error("Old password is incorrect"); - billing.password = await bcrypt.hash(newPassword, 10); - await billing.save(); + billing.password = await bcrypt.hash(newPassword, 10); + await billing.save(); - return { message: 'Password changed successfully' }; + return { message: "Password changed successfully" }; }; const getAllBillingStaff = async () => { - return await User.find({ role: 'billing' }); + const UserModel = await getUserModel(); + return await UserModel.find({ role: "billing" }); }; const updateBillingStaff = async (id, data) => { - if(data.password) { - data.password = await bcrypt.hash(data.password, 10); - } + const UserModel = await getUserModel(); + let email, phno; - const billing = await User.findOneAndUpdate( - { _id: id, role: 'billing' }, - data, - { new: true, runValidators: true } - ); + if (data.email) email = normalizeEmail(data.email); + if (data.phno) phno = sanitizePhone(data.phone); - if(!billing) throw new Error('Billing staff not found'); + if (email || phno) { + await checkUniqueUser(UserModel, { + email: email || undefined, + phno: phno || undefined, + excludeId: id, + }); + } + if (data.password) { + data.password = await bcrypt.hash(data.password, 10); + } + + if (email) data.email = email; + if (phno) data.phno = phno; + + try { + const billing = await UserModel.findOneAndUpdate({ _id: id, role: "billing" }, data, { + new: true, + runValidators: true, + }); + + if (!billing) throw new Error("Billing staff not found"); return billing; + } catch (err) { + if (err.code === 11000) { + if (err.keyPattern?.email) throw new Error("Email already exists"); + if (err.keyPattern?.phno) throw new Error("Phone number already exists"); + } + throw err; + } }; const deleteBillingStaff = async (id) => { - const billing = await User.findOneAndDelete({ _id: id, role: 'billing' }); - if(!billing) throw new Error('Billing staff not found'); + const UserModel = await getUserModel(); - return { message: 'Billing staff deleted successfully' }; + const billing = await UserModel.findOneAndDelete({ _id: id, role: "billing" }); + if (!billing) throw new Error("Billing staff not found"); + + return { message: "Billing staff deleted successfully" }; }; -export default{ +export default { createBillingStaff, changePassword, getAllBillingStaff, updateBillingStaff, - deleteBillingStaff -} \ No newline at end of file + deleteBillingStaff, +}; diff --git a/src/services/patientPortal.service.js b/src/services/patientPortal.service.js index 42e47a6..d8d6d78 100644 --- a/src/services/patientPortal.service.js +++ b/src/services/patientPortal.service.js @@ -1,18 +1,17 @@ -import bcrypt from 'bcrypt'; -import User from '../models/User.js'; -import { generateRandomPassword } from '../utils/password.utils.js'; -import { sendCredentialsEmail } from './email.service.js'; +import bcrypt from "bcrypt"; +import User from "../models/User.js"; +import { generateRandomPassword } from "../utils/password.utils.js"; +import { sendCredentialsEmail } from "./email.service.js"; export const createPatientPortalAccount = async (email) => { - - try {const existingUser = await User.findOne({ email }); + const existingUser = await User.findOne({ email }); if (existingUser) { + console.log(`Portal account already exists for: ${email}`); return; } const rawPassword = generateRandomPassword(10); - const hashedPassword = await bcrypt.hash(rawPassword, 10); const user = new User({ @@ -20,13 +19,12 @@ export const createPatientPortalAccount = async (email) => { password: hashedPassword, role: "patient", status: "Active", - mustChangePassword: true + mustChangePassword: true, }); await user.save(); await sendCredentialsEmail(email, rawPassword); -} catch (err) { - console.error("Patient pirtal account creation failed:", err); -} -}; \ No newline at end of file + + console.log(`Portal account created for: ${email}`); +}; diff --git a/src/services/reception.service.js b/src/services/reception.service.js index 1e8e011..2b2c234 100644 --- a/src/services/reception.service.js +++ b/src/services/reception.service.js @@ -1,71 +1,108 @@ -import User from '../models/User.js'; -import bcrypt from 'bcrypt'; +import User from "../models/User.js"; +import bcrypt from "bcrypt"; + +const normalizeEmail = (email) => { + if (!email) return email; + return email.trim().toLowerCase(); +}; + +const sanitizePhone = (phno) => { + if (!phno) return phno; + return phno.toString().replace(/\D/g, ""); +}; const creatReceptionist = async (data) => { - const password = data.password || 'reception@123'; - const hashedPassword = await bcrypt.hash(password, 10); + const email = normalizeEmail(data.email); + const phno = sanitizePhone(data.phno); + const password = data.password || "reception@123"; + const hashedPassword = await bcrypt.hash(password, 10); + + try { const receptionist = new User({ - ...data, - password: hashedPassword, - role: 'receptionist' + ...data, + email, + phno, + password: hashedPassword, + role: "receptionist", }); await receptionist.save(); return { - success: true, - message: 'Receptionist created successfully' + success: true, + message: "Receptionist created successfully", }; + } catch (err) { + if (err.code === 11000) { + if (err.keyPattern?.email) throw new Error("Email already exists"); + if (err.keyPattern?.phno) throw new Error("Phone number already exists"); + } + throw err; + } }; const changePassword = async (userId, oldPassword, newPassword) => { - const receptionist = await User.findById(userId); - if(!receptionist) throw new Error('Receptionist not found'); + const receptionist = await User.findById(userId); + if (!receptionist) throw new Error("Receptionist not found"); - const isMatch = await bcrypt.compare(oldPassword, receptionist.password); - if(!isMatch) throw new Error('Old password is incorrect'); + const isMatch = await bcrypt.compare(oldPassword, receptionist.password); + if (!isMatch) throw new Error("Old password is incorrect"); - receptionist.password = await bcrypt.hash(newPassword, 10); - await receptionist.save(); + receptionist.password = await bcrypt.hash(newPassword, 10); + await receptionist.save(); - return { message: 'Password changes successfully' }; + return { message: "Password changed successfully" }; }; const getReceptionists = async () => { - return await User.find({ role: 'receptionist' }); + return await User.find({ role: "receptionist" }); }; const updateReceptionist = async (id, data) => { - if(data.password) { - data.password = await bcrypt.hash(data.password, 10); - } + let email, phno; + + if (data.email) email = normalizeEmail(data.email); + if (data.phno) phno = sanitizePhone(data.phno); + + if (data.password) { + data.password = await bcrypt.hash(data.password, 10); + } + + if (email) data.email = email; + if (phno) data.phno = phno; + try { const receptionist = await User.findOneAndUpdate( - { _id: id, role: 'receptionist' }, - data, - { new: true, runValidators:true } + { _id: id, role: "receptionist" }, + data, + { new: true, runValidators: true }, ); - if(!receptionist) throw new Error('Receptionist not found'); + if (!receptionist) throw new Error("Receptionist not found"); return receptionist; + } catch (err) { + if (err.code === 11000) { + if (err.keyPattern?.email) throw new Error("Email already exists"); + if (err.keyPattern?.phno) throw new Error("Phone number already exists"); + } + throw err; + } }; const deleteReceptionist = async (id) => { - const receptionist = await User.findOneAndDelete( - { _id: id, role: 'receptionist' } - ); + const receptionist = await User.findOneAndDelete({ _id: id, role: "receptionist" }); - if(!receptionist) throw new Error('Receptionist not found'); + if (!receptionist) throw new Error("Receptionist not found"); - return { message: 'Receptionist deleted successfully' }; + return { message: "Receptionist deleted successfully" }; }; export default { - creatReceptionist, - changePassword, - getReceptionists, - updateReceptionist, - deleteReceptionist -}; \ No newline at end of file + creatReceptionist, + changePassword, + getReceptionists, + updateReceptionist, + deleteReceptionist, +};