| 1 | -- schema change: NO_SHOW must be added to the existing status constraint before
|
|---|
| 2 | -- anything below can ever set it
|
|---|
| 3 | ALTER TABLE appointments DROP CONSTRAINT appointments_status_chk;
|
|---|
| 4 | ALTER TABLE appointments ADD CONSTRAINT appointments_status_chk
|
|---|
| 5 | CHECK (status IN ('SCHEDULED','COMPLETED','CANCELLED','IN_PROGRESS','NO_SHOW'));
|
|---|
| 6 |
|
|---|
| 7 | -- No generated column needed - we'll compute the time ranges directly in the triggers
|
|---|
| 8 | -- This approach is simpler and avoids immutability constraints
|
|---|
| 9 |
|
|---|
| 10 | CREATE OR REPLACE FUNCTION is_valid_appointment_transition(p_old TEXT, p_new TEXT)
|
|---|
| 11 | RETURNS BOOLEAN
|
|---|
| 12 | LANGUAGE sql
|
|---|
| 13 | IMMUTABLE
|
|---|
| 14 | AS $$
|
|---|
| 15 | SELECT CASE
|
|---|
| 16 | WHEN p_old = p_new THEN TRUE
|
|---|
| 17 | WHEN p_old = 'SCHEDULED' AND p_new IN ('IN_PROGRESS', 'COMPLETED', 'CANCELLED') THEN TRUE
|
|---|
| 18 | WHEN p_old = 'IN_PROGRESS' AND p_new IN ('COMPLETED', 'CANCELLED') THEN TRUE
|
|---|
| 19 | ELSE FALSE
|
|---|
| 20 | END;
|
|---|
| 21 | $$;
|
|---|
| 22 |
|
|---|
| 23 | CREATE OR REPLACE FUNCTION trigger_appointments_enforce()
|
|---|
| 24 | RETURNS TRIGGER
|
|---|
| 25 | LANGUAGE plpgsql
|
|---|
| 26 | AS $$
|
|---|
| 27 | DECLARE
|
|---|
| 28 | v_combined_datetime TIMESTAMP;
|
|---|
| 29 | BEGIN
|
|---|
| 30 | v_combined_datetime := NEW.appointment_date::TIMESTAMP + NEW.appointment_time;
|
|---|
| 31 |
|
|---|
| 32 | IF TG_OP = 'INSERT' AND v_combined_datetime < NOW() THEN
|
|---|
| 33 | RAISE EXCEPTION 'Cannot schedule appointment in the past (appointment_date=%, appointment_time=%)',
|
|---|
| 34 | NEW.appointment_date, NEW.appointment_time;
|
|---|
| 35 | END IF;
|
|---|
| 36 |
|
|---|
| 37 | IF TG_OP = 'UPDATE' THEN
|
|---|
| 38 | IF NOT is_valid_appointment_transition(OLD.status, NEW.status) THEN
|
|---|
| 39 | RAISE EXCEPTION 'Appointment status cannot transition from % to %',
|
|---|
| 40 | OLD.status, NEW.status;
|
|---|
| 41 | END IF;
|
|---|
| 42 |
|
|---|
| 43 | IF NEW.status = 'COMPLETED' AND v_combined_datetime > NOW() THEN
|
|---|
| 44 | RAISE EXCEPTION 'Cannot mark appointment COMPLETED before its scheduled time (scheduled for %)',
|
|---|
| 45 | v_combined_datetime;
|
|---|
| 46 | END IF;
|
|---|
| 47 | END IF;
|
|---|
| 48 |
|
|---|
| 49 | RETURN NEW;
|
|---|
| 50 | END;
|
|---|
| 51 | $$;
|
|---|
| 52 |
|
|---|
| 53 | DROP TRIGGER IF EXISTS trigger_appointments_enforce ON appointments;
|
|---|
| 54 | CREATE TRIGGER trigger_appointments_enforce
|
|---|
| 55 | BEFORE INSERT OR UPDATE
|
|---|
| 56 | ON appointments
|
|---|
| 57 | FOR EACH ROW
|
|---|
| 58 | EXECUTE FUNCTION trigger_appointments_enforce();
|
|---|
| 59 |
|
|---|
| 60 | CREATE OR REPLACE FUNCTION t1_appointments_no_overlap()
|
|---|
| 61 | RETURNS TRIGGER
|
|---|
| 62 | LANGUAGE plpgsql
|
|---|
| 63 | AS $$
|
|---|
| 64 | DECLARE
|
|---|
| 65 | v_new_start TIMESTAMP;
|
|---|
| 66 | v_new_end TIMESTAMP;
|
|---|
| 67 | BEGIN
|
|---|
| 68 | IF NEW.status NOT IN ('SCHEDULED', 'IN_PROGRESS', 'COMPLETED') THEN
|
|---|
| 69 | RETURN NEW;
|
|---|
| 70 | END IF;
|
|---|
| 71 |
|
|---|
| 72 | v_new_start := NEW.appointment_date::TIMESTAMP + NEW.appointment_time;
|
|---|
| 73 | v_new_end := v_new_start + INTERVAL '30 minutes';
|
|---|
| 74 |
|
|---|
| 75 | -- Check for doctor double-booking
|
|---|
| 76 | -- Overlap condition: existing_start < new_end AND new_start < existing_end
|
|---|
| 77 | IF EXISTS (
|
|---|
| 78 | SELECT 1 FROM appointments a
|
|---|
| 79 | WHERE a.doctor_id = NEW.doctor_id
|
|---|
| 80 | AND a.status IN ('SCHEDULED', 'IN_PROGRESS', 'COMPLETED')
|
|---|
| 81 | AND (a.appointment_date::TIMESTAMP + a.appointment_time) < v_new_end
|
|---|
| 82 | AND v_new_start < (a.appointment_date::TIMESTAMP + a.appointment_time + INTERVAL '30 minutes')
|
|---|
| 83 | AND (TG_OP <> 'UPDATE' OR a.appointment_id <> NEW.appointment_id)
|
|---|
| 84 | ) THEN
|
|---|
| 85 | RAISE EXCEPTION 'Doctor % has overlapping appointment at %', NEW.doctor_id, NEW.appointment_date;
|
|---|
| 86 | END IF;
|
|---|
| 87 |
|
|---|
| 88 | -- Check for patient double-booking
|
|---|
| 89 | IF EXISTS (
|
|---|
| 90 | SELECT 1 FROM appointments a
|
|---|
| 91 | WHERE a.patient_id = NEW.patient_id
|
|---|
| 92 | AND a.status IN ('SCHEDULED', 'IN_PROGRESS', 'COMPLETED')
|
|---|
| 93 | AND (a.appointment_date::TIMESTAMP + a.appointment_time) < v_new_end
|
|---|
| 94 | AND v_new_start < (a.appointment_date::TIMESTAMP + a.appointment_time + INTERVAL '30 minutes')
|
|---|
| 95 | AND (TG_OP <> 'UPDATE' OR a.appointment_id <> NEW.appointment_id)
|
|---|
| 96 | ) THEN
|
|---|
| 97 | RAISE EXCEPTION 'Patient % has overlapping appointment at %', NEW.patient_id, NEW.appointment_date;
|
|---|
| 98 | END IF;
|
|---|
| 99 |
|
|---|
| 100 | RETURN NEW;
|
|---|
| 101 | END;
|
|---|
| 102 | $$;
|
|---|
| 103 |
|
|---|
| 104 | DROP TRIGGER IF EXISTS trigger_appointments_no_overlap ON appointments;
|
|---|
| 105 | CREATE TRIGGER trigger_appointments_no_overlap
|
|---|
| 106 | BEFORE INSERT OR UPDATE
|
|---|
| 107 | ON appointments
|
|---|
| 108 | FOR EACH ROW
|
|---|
| 109 | EXECUTE FUNCTION t1_appointments_no_overlap();
|
|---|
| 110 |
|
|---|
| 111 | CREATE OR REPLACE PROCEDURE job_mark_no_show()
|
|---|
| 112 | LANGUAGE plpgsql
|
|---|
| 113 | AS $$
|
|---|
| 114 | BEGIN
|
|---|
| 115 | UPDATE appointments
|
|---|
| 116 | SET status = 'NO_SHOW'
|
|---|
| 117 | WHERE status = 'SCHEDULED'
|
|---|
| 118 | AND (appointment_date::TIMESTAMP + appointment_time) < (NOW() - INTERVAL '45 minutes');
|
|---|
| 119 | END;
|
|---|
| 120 | $$;
|
|---|
| 121 |
|
|---|
| 122 | CREATE OR REPLACE VIEW v_overdue_appointments AS
|
|---|
| 123 | SELECT
|
|---|
| 124 | a.appointment_id, a.patient_id, p.first_name, p.last_name,
|
|---|
| 125 | a.doctor_id, d.first_name AS doctor_first_name, d.last_name AS doctor_last_name,
|
|---|
| 126 | a.appointment_date, a.appointment_time, a.status,
|
|---|
| 127 | NOW() - (a.appointment_date::TIMESTAMP + a.appointment_time) AS time_overdue
|
|---|
| 128 | FROM appointments a
|
|---|
| 129 | JOIN patients p ON a.patient_id = p.patient_id
|
|---|
| 130 | JOIN doctors d ON a.doctor_id = d.doctor_id
|
|---|
| 131 | WHERE a.status = 'SCHEDULED'
|
|---|
| 132 | AND (a.appointment_date::TIMESTAMP + a.appointment_time) < (NOW() - INTERVAL '45 minutes')
|
|---|
| 133 | ORDER BY time_overdue DESC; |
|---|