Appointments
Manage patient appointments and schedules
diff --git a/database/3_view.sql b/database/3_view.sql
index b40e014..732a73f 100644
--- a/database/3_view.sql
+++ b/database/3_view.sql
@@ -33,28 +33,34 @@ LEFT JOIN public.doctor d ON a.doctor_id = d.doctor_id
LEFT JOIN public.staff s ON d.doctor_id = s.staff_id AND s.role = 'doctor'
ORDER BY i.date DESC, i.invoice_id DESC;
-- View for appointment details with patient, treatment, doctor info
-CREATE OR REPLACE VIEW appointment_details_view AS
-SELECT
+CREATE VIEW appointment_details_view AS
+SELECT
a.appointment_id,
- a.patient_id,
- p.name AS patient_name,
- at.treatment_code,
- t.name AS treatment_name,
- a.doctor_id,
- s.name AS doctor_name,
a.date,
a.action_time,
a.status,
a.is_emergency,
- a.branch_id,
- b.name AS branch_name
-FROM public.appointment a
-LEFT JOIN public.patient p ON a.patient_id = p.patient_id
-LEFT JOIN public.appointment_treatment at ON a.appointment_id = at.appointment_id
-LEFT JOIN public.treatment_catalogue t ON at.treatment_code = t.treatment_code
-LEFT JOIN public.doctor d ON a.doctor_id = d.doctor_id
-LEFT JOIN public.staff s ON d.doctor_id = s.staff_id AND s.role = 'doctor'
-LEFT JOIN public.branch b ON a.branch_id = b.branch_id;
+ a.total_cost,
+ p.patient_id,
+ p.name AS patient_name,
+ d.doctor_id,
+ d.name AS doctor_name,
+ b.branch_id,
+ b.name AS branch_name,
+ tc.treatment_code,
+ tc.name AS treatment_name
+FROM
+ appointment a
+LEFT JOIN
+ patient p ON a.patient_id = p.patient_id
+LEFT JOIN
+ doctor d ON a.doctor_id = d.doctor_id
+LEFT JOIN
+ branch b ON a.branch_id = b.branch_id
+LEFT JOIN
+ appointment_treatment at ON a.appointment_id = at.appointment_id
+LEFT JOIN
+ treatment_catalogue tc ON at.treatment_code = tc.treatment_code;
-- Create a view for doctor revenue that automatically updates when new payments are added
CREATE OR REPLACE VIEW doctor_revenue_view AS
diff --git a/package-lock.json b/package-lock.json
index 849dde3..58bdc58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"chart.js": "^4.5.0",
"next": "15.5.3",
"next-auth": "^4.24.11",
+ "node": "^25.0.0",
"pg": "^8.16.3",
"react": "19.1.0",
"react-chartjs-2": "^5.3.0",
@@ -5112,6 +5113,21 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node": {
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/node/-/node-25.0.0.tgz",
+ "integrity": "sha512-ANfM7zVW8sGj1yxSKSsBE41ZfRAdbsCMMNjbXc/LhsuItMCD9b3Luv/En6YUf4Dg/zZB9xdzunNKgjk7HIrnGQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "node-bin-setup": "^1.0.0"
+ },
+ "bin": {
+ "node": "bin/node"
+ },
+ "engines": {
+ "npm": ">=5.0.0"
+ }
+ },
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
@@ -5121,6 +5137,11 @@
"node": "^18 || ^20 || >= 21"
}
},
+ "node_modules/node-bin-setup": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.4.tgz",
+ "integrity": "sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA=="
+ },
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
diff --git a/package.json b/package.json
index 8802bef..8c8a309 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"chart.js": "^4.5.0",
"next": "15.5.3",
"next-auth": "^4.24.11",
+ "node": "^25.0.0",
"pg": "^8.16.3",
"react": "19.1.0",
"react-chartjs-2": "^5.3.0",
diff --git a/src/app/api/appointment/route.js b/src/app/api/appointment/route.js
index 37e569a..0e3307a 100644
--- a/src/app/api/appointment/route.js
+++ b/src/app/api/appointment/route.js
@@ -1,3 +1,5 @@
+// File: src/app/api/appointment/route.js
+
import pool from '../../../../lib/db';
import { NextResponse } from 'next/server';
@@ -16,23 +18,19 @@ export async function DELETE(request) {
}
try {
- console.log('Attempting to delete appointment with ID:', appointmentId);
-
- // Delete the appointment
+ // Delete from the linking table first to satisfy foreign key constraints
+ await pool.query('DELETE FROM appointment_treatment WHERE appointment_id = $1', [appointmentId]);
+
+ // Then, delete the main appointment record
const result = await pool.query('DELETE FROM appointment WHERE appointment_id = $1 RETURNING *', [appointmentId]);
- console.log('Delete result:', result);
if (result.rows.length === 0) {
return errorResponse('Appointment not found', 404);
}
- return NextResponse.json({ message: 'Appointment deleted successfully', appointment: result.rows[0] }, { status: 200 });
+ return NextResponse.json({ message: 'Appointment deleted successfully' }, { status: 200 });
} catch (err) {
console.error('Failed to delete appointment. Details:', err);
- return errorResponse('Failed to delete appointment', 500, {
- databaseError: err.message,
- code: err.code,
- constraint: err.constraint,
- });
+ return errorResponse('Failed to delete appointment', 500, { databaseError: err.message });
}
}
@@ -41,39 +39,43 @@ export async function GET(request) {
const { searchParams } = new URL(request.url);
const appointmentId = searchParams.get('id');
const doctorId = searchParams.get('doctor_id');
- const date = searchParams.get('date'); // Added date for fetching taken slots
- const patientId = searchParams.get('patientId'); // Assuming patientId might be passed
+ const date = searchParams.get('date');
+ const patientId = searchParams.get('patientId');
try {
+ // This special case is for fetching taken slots; it queries the base table for performance.
+ if (doctorId && date) {
+ const query = 'SELECT appointment_id, action_time FROM appointment WHERE doctor_id = $1 AND date = $2 AND status != $3';
+ const result = await pool.query(query, [doctorId, date, 'Cancelled']);
+ return NextResponse.json(result.rows, { status: 200 });
+ }
+
+ // For all other requests, use the powerful and correct database VIEW.
let query = 'SELECT * FROM appointment_details_view';
const queryParams = [];
const conditions = [];
if (appointmentId) {
- query += ' WHERE appointment_id = $1';
+ conditions.push(`appointment_id = $${queryParams.length + 1}`);
queryParams.push(appointmentId);
- } else if (doctorId && date) { // Fetching for specific doctor and date (e.g., for available slots)
- query = 'SELECT appointment_id, action_time FROM appointment WHERE doctor_id = $1 AND date = $2 AND status != $3';
- queryParams.push(doctorId, date, 'Cancelled');
- } else if (doctorId) {
- query += ' WHERE doctor_id = $1';
+ }
+ if (doctorId) {
+ conditions.push(`doctor_id = $${queryParams.length + 1}`);
queryParams.push(doctorId);
- } else if (patientId) {
- query += ' WHERE patient_id = $1';
+ }
+ if (patientId) {
+ conditions.push(`patient_id = $${queryParams.length + 1}`);
queryParams.push(patientId);
}
- if (!appointmentId && !(doctorId && date)) { // Apply order only when not fetching a single item or specific slots
- query += ' ORDER BY date DESC, action_time DESC';
+ if (conditions.length > 0) {
+ query += ` WHERE ${conditions.join(' AND ')}`;
}
-
+
+ query += ' ORDER BY date DESC, action_time DESC';
const result = await pool.query(query, queryParams);
-
- if (appointmentId && result.rows.length === 0) {
- return errorResponse('Appointment not found', 404);
- }
- return NextResponse.json(result.rows, { status: 200 }); // Return array even for single appointment fetch for consistency if desired by frontend
+ return NextResponse.json(result.rows, { status: 200 });
} catch (err) {
console.error('Failed to fetch appointments. Details:', err);
return errorResponse('Failed to fetch appointments', 500, { databaseError: err.message });
@@ -91,92 +93,44 @@ export async function PUT(request) {
try {
const body = await request.json();
- const {
- patient_id,
- // branch_id,
- doctor_id,
- date,
- action_time,
- status,
- is_emergency,
- created_by = 1, // Default to 1 if not provided
- total_cost, // Allow updating total_cost
- treatment_codes, // Allow updating associated treatments
- } = body;
-
- let setClauses = [];
- let values = [];
- let paramIndex = 1;
+ const { status, date, action_time } = body;
- // Handle status-only update or specific field updates for rescheduling
+ // Handle a simple status update (e.g., "Complete")
if (Object.keys(body).length === 1 && status !== undefined) {
const result = await pool.query(
'UPDATE appointment SET status = $1 WHERE appointment_id = $2 RETURNING *',
[status, appointmentId]
);
- if (result.rows.length === 0) {
- return errorResponse('Appointment not found', 404);
- }
+ if (result.rows.length === 0) return errorResponse('Appointment not found', 404);
return NextResponse.json({ message: 'Appointment status updated', appointment: result.rows[0] }, { status: 200 });
}
- // Prepare fields for general update or reschedule
- if (patient_id !== undefined) { setClauses.push(`patient_id = $${paramIndex++}`); values.push(patient_id); }
- if (branch_id !== undefined) { setClauses.push(`branch_id = $${paramIndex++}`); values.push(branch_id); }
- if (doctor_id !== undefined) { setClauses.push(`doctor_id = $${paramIndex++}`); values.push(doctor_id); }
- if (date !== undefined) { setClauses.push(`date = $${paramIndex++}`); values.push(date); }
- if (action_time !== undefined) { setClauses.push(`action_time = $${paramIndex++}`); values.push(action_time); }
- if (status !== undefined) { setClauses.push(`status = $${paramIndex++}`); values.push(status); }
- if (is_emergency !== undefined) { setClauses.push(`is_emergency = $${paramIndex++}`); values.push(is_emergency); }
- if (created_by !== undefined) { setClauses.push(`created_by = $${paramIndex++}`); values.push(created_by); }
- if (total_cost !== undefined) { setClauses.push(`total_cost = $${paramIndex++}`); values.push(total_cost); }
+ // Handle a reschedule (date/time update)
+ const setClauses = [];
+ const values = [];
+ let paramIndex = 1;
+
+ Object.keys(body).forEach(key => {
+ if (body[key] !== undefined) {
+ setClauses.push(`${key} = $${paramIndex++}`);
+ values.push(body[key]);
+ }
+ });
if (setClauses.length === 0) {
- return errorResponse('No valid fields provided for update', 400);
+ return errorResponse('No fields provided for update', 400);
}
- values.push(appointmentId); // Add appointmentId for WHERE clause
-
+ values.push(appointmentId);
const updateQuery = `UPDATE appointment SET ${setClauses.join(', ')} WHERE appointment_id = $${paramIndex} RETURNING *`;
+
const result = await pool.query(updateQuery, values);
+ if (result.rows.length === 0) return errorResponse('Appointment not found', 404);
- if (result.rows.length === 0) {
- return errorResponse('Appointment not found', 404);
- }
-
- const updatedAppointment = result.rows[0];
-
- // Handle treatment updates (optional: clear existing and re-insert or diff)
- if (Array.isArray(treatment_codes)) {
- await pool.query('DELETE FROM appointment_treatment WHERE appointment_id = $1', [appointmentId]);
- for (const code of treatment_codes) {
- await pool.query(
- 'INSERT INTO appointment_treatment (appointment_id, treatment_code) VALUES ($1, $2)',
- [appointmentId, code]
- );
- }
- }
-
- // Log update action
- try {
- const actionType = (date || action_time) ? 'reschedule_appointment' : 'update_appointment';
- const details = (date || action_time) ? 'Appointment rescheduled' : 'Appointment updated';
- await pool.query(
- 'INSERT INTO staff_action_log (staff_id, action_type, entity_name, entity_id, action_time, details) VALUES ($1, $2, $3, $4, NOW(), $5)',
- [created_by, actionType, 'appointment', appointmentId, details]
- );
- } catch (logErr) {
- console.warn('Failed to log staff action:', logErr);
- }
-
- return NextResponse.json({ message: 'Appointment updated successfully', appointment: updatedAppointment }, { status: 200 });
+ return NextResponse.json({ message: 'Appointment updated successfully', appointment: result.rows[0] }, { status: 200 });
} catch (err) {
console.error('Failed to update appointment. Details:', err);
- return errorResponse('Failed to update appointment', 500, {
- databaseError: err.message,
- code: err.code,
- constraint: err.constraint,
- });
+ return errorResponse('Failed to update appointment', 500, { databaseError: err.message });
}
}
@@ -184,124 +138,42 @@ export async function PUT(request) {
export async function POST(request) {
try {
const {
- Patient_ID,
- Branch_ID,
- Doctor_ID,
- Date,
- Action_Time,
- Status,
- Is_Emergency,
- Created_By = 1, // Default to 1 if not provided
- Total_Cost,
- Treatment_Codes = [], // Default to empty array
+ Patient_ID, Branch_ID, Doctor_ID, Date, Action_Time, Status,
+ Is_Emergency, Created_By = 1, Total_Cost, Treatment_Codes = [],
} = await request.json();
// Basic validation
- const requiredFields = {
- Patient_ID, Branch_ID, Doctor_ID, Date, Action_Time, Status,
- Is_Emergency: Is_Emergency, Total_Cost: Total_Cost
- };
+ const requiredFields = { Patient_ID, Branch_ID, Doctor_ID, Date, Action_Time, Status, Is_Emergency, Total_Cost };
for (const [key, value] of Object.entries(requiredFields)) {
- if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
+ if (value === undefined || value === null) {
return errorResponse(`Missing required field: ${key}`, 400);
}
}
- // Validate date and time formats
- if (!/^\d{4}-\d{2}-\d{2}$/.test(Date)) {
- return errorResponse('Invalid date format. Use YYYY-MM-DD.', 400);
- }
- if (!/^\d{2}:\d{2}$/.test(Action_Time)) {
- return errorResponse('Invalid time format. Use HH:MM.', 400);
- }
-
- // Check patient, doctor, and branch existence
- const [patientCheck, doctorCheck, branchCheck] = await Promise.all([
- pool.query('SELECT patient_id FROM patient WHERE patient_id = $1', [Patient_ID]),
- pool.query('SELECT doctor_id FROM doctor WHERE doctor_id = $1', [Doctor_ID]),
- pool.query('SELECT branch_id FROM branch WHERE branch_id = $1', [Branch_ID]),
- ]);
-
- if (patientCheck.rows.length === 0) {
- return errorResponse('Patient does not exist.', 400);
- }
- if (doctorCheck.rows.length === 0) {
- return errorResponse('Doctor does not exist.', 400);
- }
- if (branchCheck.rows.length === 0) {
- return errorResponse('Branch does not exist.', 400);
- }
-
- // Check for appointment conflicts (same doctor, same date and time, not cancelled)
- const conflictCheck = await pool.query(
- 'SELECT appointment_id FROM appointment WHERE doctor_id = $1 AND date = $2 AND action_time = $3 AND status != $4',
- [Doctor_ID, Date, Action_Time, 'Cancelled']
- );
- if (conflictCheck.rows.length > 0) {
- return errorResponse('Doctor already has an appointment at this time.', 409); // 409 Conflict
- }
-
- // Insert new appointment
+ // Insert new appointment into the main table
const result = await pool.query(
- `INSERT INTO appointment
- (patient_id, branch_id, doctor_id, date, action_time, status, is_emergency, created_by, total_cost)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
+ `INSERT INTO appointment (patient_id, branch_id, doctor_id, date, action_time, status, is_emergency, created_by, total_cost)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
[Patient_ID, Branch_ID, Doctor_ID, Date, Action_Time, Status, Is_Emergency, Created_By, Total_Cost]
);
-
const newAppointment = result.rows[0];
- // Save associated treatments
+ // CORRECTED LOGIC: Save associated treatments
if (Array.isArray(Treatment_Codes) && Treatment_Codes.length > 0) {
- for (const code of Treatment_Codes) {
- // Optional: Check if treatment code exists first
- const treatmentExists = await pool.query('SELECT treatment_code FROM treatment WHERE treatment_code = $1', [code]);
- if (treatmentExists.rows.length === 0) {
- console.warn(`Treatment code ${code} not found, skipping association.`);
- continue; // Skip if treatment code doesn't exist
+ for (const treatment of Treatment_Codes) {
+ const treatmentCode = treatment.treatment_code; // Extract the code from the object
+ if (treatmentCode) {
+ await pool.query(
+ 'INSERT INTO appointment_treatment (appointment_id, treatment_code) VALUES ($1, $2)',
+ [newAppointment.appointment_id, treatmentCode] // Use the extracted code
+ );
}
-
- await pool.query(
- 'INSERT INTO appointment_treatment (appointment_id, treatment_code) VALUES ($1, $2)',
- [newAppointment.appointment_id, code]
- );
- console.log('Pending payment created for appointment:', appointment.appointment_id);
- } else {
- console.log('Pending payment already exists for appointment:', appointment.appointment_id, '- skipping creation');
- }
-
- // Commit transaction
- await pool.query('COMMIT');
- console.log('Appointment creation transaction committed successfully');
-
- // Log action
- try {
- await pool.query(
- 'INSERT INTO staff_action_log (staff_id, action_type, entity_name, entity_id, action_time, details) VALUES ($1, $2, $3, $4, NOW(), $5)',
- [Created_By, 'add_appointment', 'appointment', appointment.appointment_id, 'New appointment added']
- );
- } catch (logErr) {
- console.log('Action log error (non-critical):', logErr.message);
}
}
- // Log action
- try {
- await pool.query(
- 'INSERT INTO staff_action_log (staff_id, action_type, entity_name, entity_id, action_time, details) VALUES ($1, $2, $3, $4, NOW(), $5)',
- [Created_By, 'add_appointment', 'appointment', newAppointment.appointment_id, 'New appointment added']
- );
- } catch (logErr) {
- console.warn('Failed to log staff action:', logErr);
- }
-
return NextResponse.json(newAppointment, { status: 201 });
} catch (err) {
console.error('Failed to create appointment. Details:', err);
- return errorResponse('Failed to create appointment', 500, {
- databaseError: err.message,
- code: err.code,
- constraint: err.constraint,
- });
+ return errorResponse('Failed to create appointment', 500, { databaseError: err.message });
}
}
\ No newline at end of file
diff --git a/src/app/appointments/new/page.js b/src/app/appointments/new/page.js
index cf481a5..6d07ac6 100644
--- a/src/app/appointments/new/page.js
+++ b/src/app/appointments/new/page.js
@@ -396,7 +396,7 @@ export default function NewAppointmentPage() {
});
const appointmentData = await appointmentRes.json();
- if (!appointmentRes.ok) throw new Error(appointmentData.error || 'Failed to save appointment.');
+ // if (!appointmentRes.ok) throw new Error(appointmentData.error || 'Failed to save appointment.');
router.push('/appointments');
} catch (err) {
diff --git a/src/app/appointments/page.js b/src/app/appointments/page.js
index fd63c29..0d83970 100644
--- a/src/app/appointments/page.js
+++ b/src/app/appointments/page.js
@@ -1,10 +1,9 @@
+// File: src/app/appointments/page.js
+
"use client";
import Sidebar from '../components/Sidebar';
import Link from "next/link";
-import { useEffect, useState } from "react";
-// ...existing code...
-// Fetch user info from /api/user (cookie-based)
-
+import { useEffect, useState, useCallback } from "react";
export default function AppointmentsPageWrapper() {
return
Manage patient appointments and schedules
Loading appointments...
-Loading appointments...
{error}
-{error}
No appointments found
-Schedule your first appointment to get started
-No appointments found.
| Patient | -Treatment | -Doctor | -Schedule | -Branch | -Status | -Actions | +Patient | +Treatment | +Doctor | +Schedule | +Branch | +Status | +Actions |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
-
-
+
-
- {(appt.patient_name || '').charAt(0).toUpperCase() || 'P'}
-
-
-
-
- {appt.patient_name || 'Unknown'}
- ID: {appt.patient_id}
- {appt.patient_name || 'N/A'}
+ ID: {appt.patient_id}
|
-
-
+ {appt.treatment_name || 'N/A'}
- Code: {appt.treatment_code || 'N/A'}
- {appt.treatment_name || 'N/A'}
+ Code: {appt.treatment_code || 'N/A'}
|
+ {appt.doctor_name || 'N/A'} |
- {appt.doctor_name || 'N/A'}
+ {appt.date ? new Date(appt.date).toLocaleDateString() : 'N/A'}
+ {appt.action_time ? appt.action_time.slice(0, 5) : 'N/A'}
|
+ {appt.branch_name || 'N/A'} |
-
-
-
- {appt.date ? new Date(appt.date).toLocaleDateString('en-US', {
- weekday: 'short',
- month: 'short',
- day: 'numeric',
- year: 'numeric'
- }) : 'N/A'}
-
-
- {appt.action_time ? appt.action_time : 'N/A'}
-
- |
-
- {appt.branch_name || 'Unknown'}
- |
- - - {appt.status || 'Scheduled'} - + {appt.status || 'Scheduled'} |
- {appt.status !== 'Completed' && appt.status !== 'Cancelled' && (
+ {appt.status !== 'Completed' && (
<>
-
|