Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import compression from 'compression';
import hpp from 'hpp';

import { globalLimiter } from './middleware/rateLimiter.js';

Check warning on line 8 in src/app.js

View workflow job for this annotation

GitHub Actions / build (20.x)

'globalLimiter' is defined but never used
import { errorHandler } from './middleware/errorHandler.js';
import { metricsMiddleware } from './middleware/metricsMiddleware.js';
import { register } from './config/metrics.js';
Expand All @@ -22,6 +22,7 @@
import receptionRoutes from './routes/reception.routes.js';
import billingRoutes from './routes/billing.routes.js';

import leaveRoutes from './routes/leave.routes.js';
import patientRoutes from './routes/patient.js';
import deptRoutes from './routes/dept.js';
import appointmentRoutes from './routes/appointment.js';
Expand Down Expand Up @@ -99,6 +100,7 @@
app.use('/api/users/doctors', doctorRoutes);
app.use('/api/users/receptionist', receptionRoutes);
app.use('/api/billing', billingRoutes);
app.use('/api/leaves', leaveRoutes);

app.use('/api/patients', patientRoutes);
app.use('/api/departments', deptRoutes);
Expand Down
63 changes: 63 additions & 0 deletions src/controllers/aptcontrol.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Appointment from "../models/appointment.js";
import mongoose from "mongoose";
import Leave from '../models/leave.model.js'

const DAYS = [
"Sunday",
Expand All @@ -11,6 +12,58 @@ const DAYS = [
"Saturday",
];

const checkDoctorLeave = async (doctorId, date) => {
const appointmentDate = new Date(date)
const leave = await Leave.findOne({
doctor: doctorId,
status: 'approved',
startDate: { $lte: appointmentDate },
endDate: { $gte: appointmentDate }
})
if (leave) {
return {
onLeave: true,
message: `Doctor is on approved leave on this date. Please choose a different date or doctor.`
}
}
return { onLeave: false }
}

const checkTimeConflict = async (doctorId, date, time) => {
const startOfDay = new Date(date)
startOfDay.setHours(0, 0, 0, 0)
const endOfDay = new Date(date)
endOfDay.setHours(23, 59, 59, 999)

const existing = await Appointment.find({
doctor: doctorId,
date: { $gte: startOfDay, $lte: endOfDay },
status: 'scheduled'
}).lean()

if (existing.length === 0) return { conflict: false }

const toMinutes = (t) => {
const [h, m] = t.split(':').map(Number)
return h * 60 + m
}

const newTime = toMinutes(time)

for (const apt of existing) {
const existingTime = toMinutes(apt.time)
const diff = Math.abs(newTime - existingTime)
if (diff < 20) {
return {
conflict: true,
message: `Dr. is already booked at ${apt.time}. Please choose a time at least 20 minutes apart.`
}
}
}

return { conflict: false }
}

const validateWorkingHours = (doctorDoc, date, time) => {
if (!doctorDoc.workingHours || doctorDoc.workingHours.length === 0) {
return { valid: true }; // No working hours defined — skip validation
Expand Down Expand Up @@ -65,6 +118,16 @@ const createAppointment = async (req, res) => {
if (!valid) {
return res.status(400).json({ message });
}

const { conflict, message: conflictMsg } = await checkTimeConflict(doctor, date, time);
if (conflict) {
return res.status(400).json({ message: conflictMsg });
}
}

const { onLeave, message: leaveMsg } = await checkDoctorLeave(doctor, date);
if (onLeave) {
return res.status(400).json({ message: leaveMsg });
}

const appointment = new Appointment(req.body);
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/authcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const login = async (req, res) => {
const user = await User.findOne({
email: { $regex: new RegExp(`^${email}$`, "i") },
role: { $regex: new RegExp(`^${role}$`, "i") },
}).select("email role status password name");
}).select("email role status password name phno");
if (!user) {
return res.status(401).json({ success: false, message: "Invalid role or email" });
}
Expand Down Expand Up @@ -56,6 +56,7 @@ export const login = async (req, res) => {
role: user.role,
status: user.status,
name: user.name,
phno: user.phno || ''
},
});
} catch (error) {
Expand Down
57 changes: 44 additions & 13 deletions src/controllers/doctor.controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import doctorService from '../services/doctor.service.js';
import User from '../models/User.js';

const createDoctor = async (req, res, next) => {

Check warning on line 4 in src/controllers/doctor.controller.js

View workflow job for this annotation

GitHub Actions / build (20.x)

'next' is defined but never used
try {
const doctor = await doctorService.createDoctor(req.body);
res.status(201).json(doctor);
Expand Down Expand Up @@ -52,22 +53,50 @@
};

const updateMyWorkingHours = async (req, res) => {
try {
try {
const doctor = await User.findById(req.user.id)
if (!doctor) return res.status(404).json({ message: 'Doctor not found' })

doctor.pendingWorkingHours = req.body.workingHours
doctor.pendingWorkingHoursStatus = 'pending'
await doctor.save()

if (req.user.role !== 'doctor') {
return res.status(403).json({ message: "Access denied" });
}
res.json({ message: 'Schedule change submitted for admin approval.' })
} catch (err) {
res.status(500).json({ message: err.message })
}
}

const result = await doctorService.updateWorkingHours(
req.user.id,
req.body.workingHours
);
const approveSchedule = async (req, res) => {
try {
const doctor = await User.findById(req.params.id)
if (!doctor) return res.status(404).json({ message: 'Doctor not found' })
if (!doctor.pendingWorkingHours) return res.status(400).json({ message: 'No pending schedule.' })

res.status(200).json(result);
doctor.workingHours = doctor.pendingWorkingHours
doctor.pendingWorkingHours = undefined
doctor.pendingWorkingHoursStatus = 'approved'
await doctor.save()

} catch (error) {
res.status(400).json({ message: error.message });
}
res.json({ message: 'Schedule approved.' })
} catch (err) {
res.status(500).json({ message: err.message })
}
}

const rejectSchedule = async (req, res) => {
try {
const doctor = await User.findById(req.params.id)
if (!doctor) return res.status(404).json({ message: 'Doctor not found' })

doctor.pendingWorkingHours = undefined
doctor.pendingWorkingHoursStatus = 'rejected'
await doctor.save()

res.json({ message: 'Schedule rejected.' })
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export {
Expand All @@ -76,5 +105,7 @@
getDoctors,
updateDoctor,
deleteDoctor,
updateMyWorkingHours
updateMyWorkingHours,
approveSchedule,
rejectSchedule
};
75 changes: 75 additions & 0 deletions src/controllers/leave.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Leave from '../models/leave.model.js'

export const applyLeave = async (req, res) => {
try {
const { startDate, endDate, reason } = req.body
if (!startDate || !endDate) return res.status(400).json({ message: 'Start and end date are required.' })
if (new Date(startDate) > new Date(endDate)) return res.status(400).json({ message: 'Start date cannot be after end date.' })

const leave = await Leave.create({ doctor: req.user.id, startDate, endDate, reason })
res.status(201).json(leave)
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export const getMyLeaves = async (req, res) => {
try {
const leaves = await Leave.find({ doctor: req.user.id }).sort({ createdAt: -1 })
res.json(leaves)
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export const getAllLeaves = async (req, res) => {
try {
const leaves = await Leave.find({ status: 'pending' })
.populate('doctor', 'name email')
.sort({ createdAt: -1 })
res.json(leaves)
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export const approveLeave = async (req, res) => {
try {
const leave = await Leave.findByIdAndUpdate(req.params.id, { status: 'approved' }, { new: true })
if (!leave) return res.status(404).json({ message: 'Leave not found.' })
res.json(leave)
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export const rejectLeave = async (req, res) => {
try {
const leave = await Leave.findByIdAndUpdate(req.params.id, { status: 'rejected' }, { new: true })
if (!leave) return res.status(404).json({ message: 'Leave not found.' })
res.json(leave)
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export const cancelLeave = async (req, res) => {
try {
const leave = await Leave.findOne({ _id: req.params.id, doctor: req.user.id })
if (!leave) return res.status(404).json({ message: 'Leave not found.' })
if (leave.status !== 'pending') return res.status(400).json({ message: 'Only pending leaves can be cancelled.' })
await leave.deleteOne()
res.json({ message: 'Leave cancelled.' })
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export const getDoctorApprovedLeaves = async (req, res) => {
try {
const leaves = await Leave.find({ doctor: req.params.doctorId, status: 'approved' })
res.json(leaves)
} catch (err) {
res.status(500).json({ message: err.message })
}
}
10 changes: 10 additions & 0 deletions src/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ const userSchema = new mongoose.Schema(
workingHours: {
type: [workingDaySchema],
},

pendingWorkingHours: {
type: [workingDaySchema],
default: undefined
},
pendingWorkingHoursStatus: {
type: String,
enum: ['pending', 'approved', 'rejected'],
default: undefined
}
},
{ timestamps: true },
);
Expand Down
19 changes: 19 additions & 0 deletions src/models/leave.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import mongoose from 'mongoose'

const leaveSchema = new mongoose.Schema({
doctor: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
reason: { type: String, trim: true },
status: {
type: String,
enum: ['pending', 'approved', 'rejected'],
default: 'pending'
}
}, { timestamps: true })

export default mongoose.model('Leave', leaveSchema)
27 changes: 27 additions & 0 deletions src/routes/auth.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,31 @@ router.post('/signup', async (req, res) => {
}
});

router.put('/me', protect, async (req, res) => {
try {
const { phno } = req.body;

if (!phno || !/^\d{10}$/.test(phno.replace(/\D/g, ''))) {
return res.status(400).json({ success: false, message: 'Valid 10-digit phone number is required.' });
}

const cleanPhone = phno.replace(/\D/g, '');

const existing = await User.findOne({ phno: cleanPhone, _id: { $ne: req.user.id } });
if (existing) {
return res.status(400).json({ success: false, message: 'This phone number is already registered.' });
}

const updated = await User.findByIdAndUpdate(
req.user.id,
{ phno: cleanPhone },
{ new: true, runValidators: true }
).select('-password');

res.json({ success: true, data: updated });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});

export default router;
5 changes: 4 additions & 1 deletion src/routes/doctor.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ router.get('/', protect, authorize('admin', 'doctor', 'receptionist'), doctorCon

router.put('/change-password', protect, authorize('doctor'), doctorController.changePassword);

router.put('/:id', protect, authorize('admin'), doctorController.updateDoctor);
router.put('/:id', protect, authorize('admin', 'doctor'), doctorController.updateDoctor);

router.put('/me/working-hours',
protect,
authorize('doctor'),
doctorController.updateMyWorkingHours
);

router.put('/:id/approve-schedule', protect, authorize(['admin']), doctorController.approveSchedule)
router.put('/:id/reject-schedule', protect, authorize(['admin']), doctorController.rejectSchedule)

router.delete('/:id', protect, authorize('admin'), doctorController.deleteDoctor);

export default router;
15 changes: 15 additions & 0 deletions src/routes/leave.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import express from 'express'
import { protect, authorize } from '../middleware/authmiddleware.js'
import * as leaveController from '../controllers/leave.controller.js'

const router = express.Router()

router.post('/', protect, authorize('doctor'), leaveController.applyLeave)
router.get('/my', protect, authorize('doctor'), leaveController.getMyLeaves)
router.get('/pending', protect, authorize('admin'), leaveController.getAllLeaves)
router.put('/:id/approve', protect, authorize('admin'), leaveController.approveLeave)
router.put('/:id/reject', protect, authorize('admin'), leaveController.rejectLeave)
router.delete('/:id', protect, authorize('doctor'), leaveController.cancelLeave)
router.get('/doctor/:doctorId', protect, leaveController.getDoctorApprovedLeaves)

export default router
Loading