Skip to content

Commit c1cee94

Browse files
feat(appointment): Update appointment status to 'pending' on creation and add soft delete functionality
feat(chat): Enhance conversation user retrieval with optional role filtering and improved response structure feat(appointment): Add validation for appointment creation and update schemas to support multiple ID formats
1 parent 64ae2ae commit c1cee94

File tree

7 files changed

+129
-21
lines changed

7 files changed

+129
-21
lines changed

src/controllers/appointment.controller.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ class AppointmentController {
202202

203203
// Override patient_id to their own record (security)
204204
appointmentData.patient_id = patient.id;
205-
appointmentData.status = appointmentData.status || 'scheduled';
205+
// Patient bookings start as 'pending' until doctor confirms
206+
appointmentData.status = appointmentData.status || 'pending';
206207
appointmentData.location = appointmentData.location || 'main_office';
207208

208209
api.info('👤 Patient self-booking (security override):', {

src/controllers/chat.controller.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,32 @@ class ChatController {
227227
/**
228228
* Get users for creating conversations with role filtering
229229
* GET /chat/conversations/users?role=doctor&search_term=smith&per_page=50
230+
* If no role is provided, returns all available users based on current user's permissions
230231
*/
231232
static async getConversationUsers(req, res) {
232233
try {
233234
const { role } = req.query;
234235

235-
// Validate role parameter is provided
236+
// If no role is provided, get all available users
236237
if (!role) {
237-
throw new ErrorResponse('Role parameter is required. Must be one of: doctor, staff, admin, patient', 400, '4001');
238+
api.info('🔍 getConversationUsers: No role filter, fetching all available users');
239+
const users = await ChatService.getAvailableUsersForChat(req.user, req.query);
240+
241+
const { per_page = 100, search_term = '' } = req.query;
242+
const meta = {
243+
role_filter: 'all',
244+
total_count: users.length,
245+
per_page: parseInt(per_page),
246+
search_applied: !!search_term,
247+
context: 'conversation_creation'
248+
};
249+
250+
return new SuccessResponse(
251+
'Available users for conversations retrieved successfully',
252+
200,
253+
users,
254+
meta
255+
).send(res);
238256
}
239257

240258
// Validate role parameter value

src/repositories/appointment.repository.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,32 @@ class AppointmentRepository extends BaseRepository {
9191
}
9292
}
9393

94+
/**
95+
* Delete (soft delete) appointment by setting status to cancelled
96+
* @param {number} appointmentId - Appointment ID
97+
* @param {string} reason - Cancellation reason
98+
* @returns {Object|null} Cancelled appointment or null
99+
*/
100+
async deleteAppointment(appointmentId, reason = null) {
101+
try {
102+
const updateData = {
103+
status: 'cancelled',
104+
cancelled_at: new Date()
105+
};
106+
107+
if (reason) {
108+
updateData.cancellation_reason = reason;
109+
}
110+
111+
const appointment = await this.updateById(appointmentId, updateData);
112+
api.info('Appointment soft-deleted (cancelled):', { id: appointmentId });
113+
return appointment;
114+
} catch (error) {
115+
api.error('Error deleting appointment:', error);
116+
throw error;
117+
}
118+
}
119+
94120
/**
95121
* Get appointments with filtering and pagination
96122
* @param {Object} conditions - Filter conditions
@@ -104,7 +130,10 @@ class AppointmentRepository extends BaseRepository {
104130
// Build where clause for complex conditions
105131
const whereConditions = {};
106132

107-
if (conditions.doctor_id) whereConditions.doctor_id = conditions.doctor_id;
133+
// Support both single values and arrays for doctor_id (for role-based filtering)
134+
if (conditions.doctor_id) {
135+
whereConditions.doctor_id = conditions.doctor_id;
136+
}
108137
if (conditions.patient_id) whereConditions.patient_id = conditions.patient_id;
109138
if (conditions.status) whereConditions.status = conditions.status;
110139
if (conditions.location) whereConditions.location = conditions.location;
@@ -167,7 +196,11 @@ class AppointmentRepository extends BaseRepository {
167196
try {
168197
const whereConditions = {};
169198

170-
if (conditions.doctor_id) whereConditions.doctor_id = conditions.doctor_id;
199+
// Support both single values and arrays for doctor_id
200+
if (conditions.doctor_id) {
201+
whereConditions.doctor_id = conditions.doctor_id;
202+
}
203+
if (conditions.patient_id) whereConditions.patient_id = conditions.patient_id;
171204
if (conditions.status) whereConditions.status = conditions.status;
172205
if (conditions.location) whereConditions.location = conditions.location;
173206

src/routes/appointment.routes.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const AppointmentController = require('../controllers/appointment.controller');
33
const ClinicalNotesController = require('../controllers/clinical-notes.controller');
44
const asyncHandler = require('../utils/asyncHandler');
55
const { authenticate, authorize, authorizeAppointmentAccess, authorizePatientAppointments } = require('../middleware/auth.middleware');
6+
const { validateBody } = require('../middleware/validation.middleware');
7+
const { appointmentCreateSchema, appointmentUpdateSchema } = require('../validators/appointment.validator');
68
const { clinicalNotesValidator } = require('../validators');
79

810
const router = express.Router();
@@ -176,7 +178,7 @@ router.get('/doctors/:doctor_id/availability', asyncHandler(AppointmentControlle
176178
* name: status
177179
* schema:
178180
* type: string
179-
* enum: [scheduled, confirmed, in-progress, completed, cancelled, no-show]
181+
* enum: [pending, scheduled, confirmed, in-progress, completed, cancelled, no-show]
180182
* description: Filter by appointment status
181183
* - in: query
182184
* name: doctor_id
@@ -196,7 +198,13 @@ router.get('/doctors/:doctor_id/availability', asyncHandler(AppointmentControlle
196198
* schema:
197199
* $ref: '#/components/schemas/PaginatedResponse'
198200
*/
199-
router.post('/', authenticate, authorize('doctor', 'admin', 'staff', 'patient'), asyncHandler(AppointmentController.createAppointment));
201+
router.post(
202+
'/',
203+
authenticate,
204+
authorize('doctor', 'admin', 'staff', 'patient'),
205+
validateBody(appointmentCreateSchema),
206+
asyncHandler(AppointmentController.createAppointment)
207+
);
200208
/**
201209
* @swagger
202210
* /appointments/check-availability:
@@ -280,7 +288,7 @@ router.post('/check-availability', authenticate, asyncHandler(AppointmentControl
280288
* name: status
281289
* schema:
282290
* type: string
283-
* enum: [scheduled, confirmed, in-progress, completed, cancelled, no-show]
291+
* enum: [pending, scheduled, confirmed, in-progress, completed, cancelled, no-show]
284292
* responses:
285293
* 200:
286294
* description: Patient appointments retrieved successfully
@@ -322,7 +330,7 @@ router.get('/me', authenticate, authorize('patient'), asyncHandler(AppointmentCo
322330
* name: status
323331
* schema:
324332
* type: string
325-
* enum: [scheduled, confirmed, in-progress, completed, cancelled, no-show]
333+
* enum: [pending, scheduled, confirmed, in-progress, completed, cancelled, no-show]
326334
* responses:
327335
* 200:
328336
* description: Appointments retrieved successfully
@@ -431,7 +439,7 @@ router.get('/', authenticate, authorize('doctor', 'admin', 'staff'), asyncHandle
431439
* example: "14:00"
432440
* status:
433441
* type: string
434-
* enum: [scheduled, confirmed, in-progress, completed, cancelled, no-show]
442+
* enum: [pending, scheduled, confirmed, in-progress, completed, cancelled, no-show]
435443
* location:
436444
* type: string
437445
* reason_for_visit:
@@ -467,7 +475,13 @@ router.get('/', authenticate, authorize('doctor', 'admin', 'staff'), asyncHandle
467475
* description: Appointment not found
468476
*/
469477
router.get('/:id', authenticate, authorizeAppointmentAccess, asyncHandler(AppointmentController.getAppointmentById));
470-
router.put('/:id', authenticate, authorizeAppointmentAccess, asyncHandler(AppointmentController.updateAppointment));
478+
router.put(
479+
'/:id',
480+
authenticate,
481+
authorizeAppointmentAccess,
482+
validateBody(appointmentUpdateSchema),
483+
asyncHandler(AppointmentController.updateAppointment)
484+
);
471485
/**
472486
* @swagger
473487
* /appointments/{id}/reschedule:
@@ -546,7 +560,7 @@ router.use(
546560
* name: status
547561
* schema:
548562
* type: string
549-
* enum: [scheduled, confirmed, in-progress, completed, cancelled, no-show]
563+
* enum: [pending, scheduled, confirmed, in-progress, completed, cancelled, no-show]
550564
* responses:
551565
* 200:
552566
* description: Nested appointments retrieved successfully
@@ -614,7 +628,7 @@ patientAppointmentsRouter.get('/', asyncHandler(AppointmentController.getPatient
614628
* type: string
615629
* status:
616630
* type: string
617-
* enum: [scheduled, confirmed, in-progress, completed, cancelled, no-show]
631+
* enum: [pending, scheduled, confirmed, in-progress, completed, cancelled, no-show]
618632
* location:
619633
* type: string
620634
* reason_for_visit:
@@ -654,6 +668,7 @@ patientAppointmentsRouter.put(
654668
'/:appointment_id',
655669
mapNestedAppointmentId,
656670
authorizeAppointmentAccess,
671+
validateBody(appointmentUpdateSchema),
657672
asyncHandler(AppointmentController.updateAppointment)
658673
);
659674

src/routes/chat.routes.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ router.post('/conversations',
6969
* name: role
7070
* schema:
7171
* type: string
72-
* enum: [doctor, staff, admin]
73-
* description: Filter by user role
72+
* enum: [doctor, staff, admin, patient]
73+
* required: false
74+
* description: Filter by user role. If omitted, returns all available users based on permissions.
7475
* - in: query
7576
* name: search_term
7677
* schema:
@@ -80,7 +81,7 @@ router.post('/conversations',
8081
* name: per_page
8182
* schema:
8283
* type: integer
83-
* default: 25
84+
* default: 100
8485
* responses:
8586
* 200:
8687
* description: Users retrieved successfully

src/services/chat.service.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,7 @@ class ChatService {
12791279
return {
12801280
id: message.id,
12811281
conversation_id: message.conversation_id,
1282+
// Nested sender object (structured format)
12821283
sender: {
12831284
id: message.sender_id,
12841285
type: message.sender_type,
@@ -1287,12 +1288,21 @@ class ChatService {
12871288
last_name: senderLastName,
12881289
email: message.sender_email || null
12891290
},
1291+
// Flat sender fields (for backward compatibility and frontend isFromCurrentUser)
1292+
sender_id: message.sender_id,
1293+
sender_type: message.sender_type,
1294+
sender_name: senderFullName,
12901295
content: message.content,
1296+
message_content: message.content, // Alias for compatibility
12911297
message_type: message.message_type,
12921298
attachment,
12931299
delivery_status: message.delivery_status || message.status || 'sent',
12941300
is_read: typeof message.is_read === 'boolean' ? message.is_read : Boolean(message.read_at),
1301+
is_delivered: message.is_delivered || false,
1302+
read_at: message.read_at || null,
1303+
delivered_at: message.delivered_at || null,
12951304
sent_at: message.sent_at,
1305+
created_at: message.created_at || message.sent_at,
12961306
updated_at: message.updated_at
12971307
};
12981308
}

src/validators/appointment.validator.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,54 @@
11
const Joi = require('joi');
22

33
// Appointment validation schemas - simplified for small clinic
4+
// Supports both traditional (doctor_id/patient_id) and modern (sender_id/recipient_id) formats
45
const appointmentCreateSchema = Joi.object({
5-
doctor_id: Joi.number().integer().positive().required(),
6-
patient_id: Joi.number().integer().positive().required(),
6+
// Traditional format
7+
doctor_id: Joi.number().integer().positive(),
8+
patient_id: Joi.number().integer().positive(),
9+
10+
// Modern format (alternative)
11+
sender_id: Joi.number().integer().positive(),
12+
recipient_id: Joi.number().integer().positive(),
13+
14+
// Appointment details
715
appointment_date: Joi.string().required(), // Can be "Thursday, June 26, 2025" or "2025-06-26"
16+
date: Joi.string(), // Alternative field name for frontend compatibility
817
appointment_time: Joi.string().required(), // Can be "11:30 AM" or "14:30"
18+
time: Joi.string(), // Alternative field name for frontend compatibility
19+
920
location: Joi.string().max(100).default('main_office'),
1021
reason_for_visit: Joi.string().max(500).optional(),
1122
additional_notes: Joi.string().max(1000).optional(),
12-
status: Joi.string().valid('scheduled', 'confirmed', 'in-progress', 'completed', 'cancelled', 'no-show').default('scheduled')
13-
});
23+
notes: Joi.string().max(1000).optional(), // Alternative field name
24+
status: Joi.string().valid('pending', 'scheduled', 'confirmed', 'in-progress', 'completed', 'cancelled', 'no-show').default('pending'),
25+
26+
// Metadata
27+
created_by: Joi.number().integer().positive().optional(),
28+
updated_by: Joi.number().integer().positive().optional()
29+
})
30+
// Require at least one ID format (traditional OR modern)
31+
.or('doctor_id', 'sender_id')
32+
.or('patient_id', 'recipient_id')
33+
// Require at least one date/time format
34+
.or('appointment_date', 'date')
35+
.or('appointment_time', 'time');
1436

1537
const appointmentUpdateSchema = Joi.object({
1638
appointment_date: Joi.string().optional(),
39+
date: Joi.string().optional(),
1740
appointment_time: Joi.string().optional(),
41+
time: Joi.string().optional(),
1842
location: Joi.string().max(100).optional(),
1943
reason_for_visit: Joi.string().max(500).optional(),
2044
additional_notes: Joi.string().max(1000).optional(),
21-
status: Joi.string().valid('scheduled', 'confirmed', 'in-progress', 'completed', 'cancelled', 'no-show').optional()
45+
notes: Joi.string().max(1000).optional(),
46+
status: Joi.string().valid('pending', 'scheduled', 'confirmed', 'in-progress', 'completed', 'cancelled', 'no-show').optional(),
47+
48+
// Metadata
49+
updated_by: Joi.number().integer().positive().optional(),
50+
cancelled_at: Joi.date().optional(),
51+
cancellation_reason: Joi.string().max(500).optional()
2252
});
2353

2454
module.exports = {

0 commit comments

Comments
 (0)