| 1 | SET search_path TO project;
|
|---|
| 2 |
|
|---|
| 3 | DO $$
|
|---|
| 4 | BEGIN
|
|---|
| 5 | IF NOT EXISTS (
|
|---|
| 6 | SELECT 1
|
|---|
| 7 | FROM pg_type t
|
|---|
| 8 | JOIN pg_namespace n ON n.oid = t.typnamespace
|
|---|
| 9 | WHERE n.nspname = 'project'
|
|---|
| 10 | AND t.typname = 'approval_decision_domain'
|
|---|
| 11 | ) THEN
|
|---|
| 12 | CREATE DOMAIN project.approval_decision_domain AS VARCHAR(128)
|
|---|
| 13 | CHECK (VALUE IN ('approved', 'rejected'));
|
|---|
| 14 | END IF;
|
|---|
| 15 | END $$;
|
|---|
| 16 |
|
|---|
| 17 | DROP VIEW IF EXISTS project.v_quarterly_room_utilization;
|
|---|
| 18 | DROP VIEW IF EXISTS project.v_pending_reservation_details;
|
|---|
| 19 |
|
|---|
| 20 | DROP TRIGGER IF EXISTS trg_prevent_room_overlap ON project.reservations;
|
|---|
| 21 | DROP TRIGGER IF EXISTS ctg_reservation_has_resource ON project.reservations;
|
|---|
| 22 | DROP TRIGGER IF EXISTS ctg_reservation_equipment_has_resource ON project.reservation_equipment;
|
|---|
| 23 | DROP TRIGGER IF EXISTS trg_validate_equipment_availability_on_request ON project.reservation_equipment;
|
|---|
| 24 | DROP TRIGGER IF EXISTS trg_validate_equipment_availability_on_reservation ON project.reservations;
|
|---|
| 25 | DROP TRIGGER IF EXISTS trg_validate_approval_direct_write ON project.approvals;
|
|---|
| 26 | DROP TRIGGER IF EXISTS trg_sync_reservation_status_after_approval ON project.approvals;
|
|---|
| 27 |
|
|---|
| 28 | DROP FUNCTION IF EXISTS project.fn_prevent_room_overlap();
|
|---|
| 29 | DROP FUNCTION IF EXISTS project.fn_check_reservation_has_resource();
|
|---|
| 30 | DROP FUNCTION IF EXISTS project.fn_check_reservation_equipment_resource();
|
|---|
| 31 | DROP FUNCTION IF EXISTS project.fn_validate_equipment_availability(INTEGER);
|
|---|
| 32 | DROP FUNCTION IF EXISTS project.fn_trg_validate_equipment_availability();
|
|---|
| 33 | DROP FUNCTION IF EXISTS project.fn_validate_approval_direct_write();
|
|---|
| 34 | DROP FUNCTION IF EXISTS project.fn_sync_reservation_status_after_approval();
|
|---|
| 35 | DROP FUNCTION IF EXISTS project.fn_approve_or_reject_reservation(
|
|---|
| 36 | INTEGER,
|
|---|
| 37 | INTEGER,
|
|---|
| 38 | project.approval_decision_domain,
|
|---|
| 39 | VARCHAR
|
|---|
| 40 | );
|
|---|
| 41 |
|
|---|
| 42 | CREATE OR REPLACE FUNCTION project.fn_prevent_room_overlap()
|
|---|
| 43 | RETURNS TRIGGER AS $$
|
|---|
| 44 | BEGIN
|
|---|
| 45 | IF NEW.room_id IS NULL OR NEW.status NOT IN ('pending', 'approved') THEN
|
|---|
| 46 | RETURN NEW;
|
|---|
| 47 | END IF;
|
|---|
| 48 |
|
|---|
| 49 | IF EXISTS (
|
|---|
| 50 | SELECT 1
|
|---|
| 51 | FROM project.reservations r
|
|---|
| 52 | WHERE r.room_id = NEW.room_id
|
|---|
| 53 | AND r.reservation_date = NEW.reservation_date
|
|---|
| 54 | AND r.status IN ('pending', 'approved')
|
|---|
| 55 | AND r.reservation_id <> COALESCE(NEW.reservation_id, -1)
|
|---|
| 56 | AND NEW.start_time < r.end_time
|
|---|
| 57 | AND NEW.end_time > r.start_time
|
|---|
| 58 | ) THEN
|
|---|
| 59 | RAISE EXCEPTION
|
|---|
| 60 | 'Room reservation overlap detected for room_id %, date %, time interval % - %.',
|
|---|
| 61 | NEW.room_id,
|
|---|
| 62 | NEW.reservation_date,
|
|---|
| 63 | NEW.start_time,
|
|---|
| 64 | NEW.end_time;
|
|---|
| 65 | END IF;
|
|---|
| 66 |
|
|---|
| 67 | RETURN NEW;
|
|---|
| 68 | END;
|
|---|
| 69 | $$ LANGUAGE plpgsql;
|
|---|
| 70 |
|
|---|
| 71 | CREATE TRIGGER trg_prevent_room_overlap
|
|---|
| 72 | BEFORE INSERT OR UPDATE OF room_id, reservation_date, start_time, end_time, status
|
|---|
| 73 | ON project.reservations
|
|---|
| 74 | FOR EACH ROW
|
|---|
| 75 | EXECUTE FUNCTION project.fn_prevent_room_overlap();
|
|---|
| 76 |
|
|---|
| 77 | CREATE OR REPLACE FUNCTION project.fn_check_reservation_has_resource()
|
|---|
| 78 | RETURNS TRIGGER AS $$
|
|---|
| 79 | DECLARE
|
|---|
| 80 | v_has_equipment BOOLEAN;
|
|---|
| 81 | BEGIN
|
|---|
| 82 | SELECT EXISTS (
|
|---|
| 83 | SELECT 1
|
|---|
| 84 | FROM project.reservation_equipment re
|
|---|
| 85 | WHERE re.reservation_id = NEW.reservation_id
|
|---|
| 86 | )
|
|---|
| 87 | INTO v_has_equipment;
|
|---|
| 88 |
|
|---|
| 89 | IF NEW.room_id IS NULL AND NOT v_has_equipment THEN
|
|---|
| 90 | RAISE EXCEPTION
|
|---|
| 91 | 'Reservation % must include at least one resource: either a room or requested equipment.',
|
|---|
| 92 | NEW.reservation_id;
|
|---|
| 93 | END IF;
|
|---|
| 94 |
|
|---|
| 95 | RETURN NEW;
|
|---|
| 96 | END;
|
|---|
| 97 | $$ LANGUAGE plpgsql;
|
|---|
| 98 |
|
|---|
| 99 | CREATE CONSTRAINT TRIGGER ctg_reservation_has_resource
|
|---|
| 100 | AFTER INSERT OR UPDATE OF room_id
|
|---|
| 101 | ON project.reservations
|
|---|
| 102 | DEFERRABLE INITIALLY DEFERRED
|
|---|
| 103 | FOR EACH ROW
|
|---|
| 104 | EXECUTE FUNCTION project.fn_check_reservation_has_resource();
|
|---|
| 105 |
|
|---|
| 106 | CREATE OR REPLACE FUNCTION project.fn_check_reservation_equipment_resource()
|
|---|
| 107 | RETURNS TRIGGER AS $$
|
|---|
| 108 | DECLARE
|
|---|
| 109 | v_reservation_id INTEGER;
|
|---|
| 110 | v_room_id INTEGER;
|
|---|
| 111 | v_equipment_count INTEGER;
|
|---|
| 112 | BEGIN
|
|---|
| 113 | v_reservation_id := COALESCE(NEW.reservation_id, OLD.reservation_id);
|
|---|
| 114 |
|
|---|
| 115 | SELECT room_id
|
|---|
| 116 | INTO v_room_id
|
|---|
| 117 | FROM project.reservations
|
|---|
| 118 | WHERE reservation_id = v_reservation_id;
|
|---|
| 119 |
|
|---|
| 120 | IF NOT FOUND THEN
|
|---|
| 121 | RETURN COALESCE(NEW, OLD);
|
|---|
| 122 | END IF;
|
|---|
| 123 |
|
|---|
| 124 | SELECT COUNT(*)
|
|---|
| 125 | INTO v_equipment_count
|
|---|
| 126 | FROM project.reservation_equipment
|
|---|
| 127 | WHERE reservation_id = v_reservation_id;
|
|---|
| 128 |
|
|---|
| 129 | IF v_room_id IS NULL AND v_equipment_count = 0 THEN
|
|---|
| 130 | RAISE EXCEPTION
|
|---|
| 131 | 'Reservation % cannot remain without both room and requested equipment.',
|
|---|
| 132 | v_reservation_id;
|
|---|
| 133 | END IF;
|
|---|
| 134 |
|
|---|
| 135 | RETURN COALESCE(NEW, OLD);
|
|---|
| 136 | END;
|
|---|
| 137 | $$ LANGUAGE plpgsql;
|
|---|
| 138 |
|
|---|
| 139 | CREATE CONSTRAINT TRIGGER ctg_reservation_equipment_has_resource
|
|---|
| 140 | AFTER INSERT OR UPDATE OR DELETE
|
|---|
| 141 | ON project.reservation_equipment
|
|---|
| 142 | DEFERRABLE INITIALLY DEFERRED
|
|---|
| 143 | FOR EACH ROW
|
|---|
| 144 | EXECUTE FUNCTION project.fn_check_reservation_equipment_resource();
|
|---|
| 145 |
|
|---|
| 146 | CREATE OR REPLACE FUNCTION project.fn_validate_equipment_availability(
|
|---|
| 147 | p_reservation_id INTEGER
|
|---|
| 148 | )
|
|---|
| 149 | RETURNS VOID AS $$
|
|---|
| 150 | DECLARE
|
|---|
| 151 | rec RECORD;
|
|---|
| 152 | v_overlapping_quantity INTEGER;
|
|---|
| 153 | BEGIN
|
|---|
| 154 | FOR rec IN
|
|---|
| 155 | SELECT
|
|---|
| 156 | req.equipment_id,
|
|---|
| 157 | e.name AS equipment_name,
|
|---|
| 158 | e.stock_quantity,
|
|---|
| 159 | req.requested_quantity,
|
|---|
| 160 | res.reservation_date,
|
|---|
| 161 | res.start_time,
|
|---|
| 162 | res.end_time,
|
|---|
| 163 | res.status
|
|---|
| 164 | FROM project.reservation_equipment req
|
|---|
| 165 | JOIN project.reservations res
|
|---|
| 166 | ON req.reservation_id = res.reservation_id
|
|---|
| 167 | JOIN project.equipment e
|
|---|
| 168 | ON req.equipment_id = e.equipment_id
|
|---|
| 169 | WHERE req.reservation_id = p_reservation_id
|
|---|
| 170 | LOOP
|
|---|
| 171 | IF rec.status NOT IN ('pending', 'approved') THEN
|
|---|
| 172 | CONTINUE;
|
|---|
| 173 | END IF;
|
|---|
| 174 |
|
|---|
| 175 | SELECT COALESCE(SUM(req2.requested_quantity), 0)
|
|---|
| 176 | INTO v_overlapping_quantity
|
|---|
| 177 | FROM project.reservation_equipment req2
|
|---|
| 178 | JOIN project.reservations res2
|
|---|
| 179 | ON req2.reservation_id = res2.reservation_id
|
|---|
| 180 | WHERE req2.equipment_id = rec.equipment_id
|
|---|
| 181 | AND res2.reservation_id <> p_reservation_id
|
|---|
| 182 | AND res2.status IN ('pending', 'approved')
|
|---|
| 183 | AND res2.reservation_date = rec.reservation_date
|
|---|
| 184 | AND rec.start_time < res2.end_time
|
|---|
| 185 | AND rec.end_time > res2.start_time;
|
|---|
| 186 |
|
|---|
| 187 | IF v_overlapping_quantity + rec.requested_quantity > rec.stock_quantity THEN
|
|---|
| 188 | RAISE EXCEPTION
|
|---|
| 189 | 'Not enough general stock for equipment %. Requested quantity %, overlapping active quantity %, available stock %.',
|
|---|
| 190 | rec.equipment_name,
|
|---|
| 191 | rec.requested_quantity,
|
|---|
| 192 | v_overlapping_quantity,
|
|---|
| 193 | rec.stock_quantity;
|
|---|
| 194 | END IF;
|
|---|
| 195 | END LOOP;
|
|---|
| 196 | END;
|
|---|
| 197 | $$ LANGUAGE plpgsql;
|
|---|
| 198 |
|
|---|
| 199 | CREATE OR REPLACE FUNCTION project.fn_trg_validate_equipment_availability()
|
|---|
| 200 | RETURNS TRIGGER AS $$
|
|---|
| 201 | BEGIN
|
|---|
| 202 | PERFORM project.fn_validate_equipment_availability(
|
|---|
| 203 | COALESCE(NEW.reservation_id, OLD.reservation_id)
|
|---|
| 204 | );
|
|---|
| 205 |
|
|---|
| 206 | RETURN COALESCE(NEW, OLD);
|
|---|
| 207 | END;
|
|---|
| 208 | $$ LANGUAGE plpgsql;
|
|---|
| 209 |
|
|---|
| 210 | CREATE TRIGGER trg_validate_equipment_availability_on_request
|
|---|
| 211 | AFTER INSERT OR UPDATE
|
|---|
| 212 | ON project.reservation_equipment
|
|---|
| 213 | FOR EACH ROW
|
|---|
| 214 | EXECUTE FUNCTION project.fn_trg_validate_equipment_availability();
|
|---|
| 215 |
|
|---|
| 216 | CREATE TRIGGER trg_validate_equipment_availability_on_reservation
|
|---|
| 217 | AFTER UPDATE OF reservation_date, start_time, end_time, status
|
|---|
| 218 | ON project.reservations
|
|---|
| 219 | FOR EACH ROW
|
|---|
| 220 | WHEN (NEW.status IN ('pending', 'approved'))
|
|---|
| 221 | EXECUTE FUNCTION project.fn_trg_validate_equipment_availability();
|
|---|
| 222 |
|
|---|
| 223 | CREATE OR REPLACE FUNCTION project.fn_validate_approval_direct_write()
|
|---|
| 224 | RETURNS TRIGGER AS $$
|
|---|
| 225 | DECLARE
|
|---|
| 226 | v_role VARCHAR(128);
|
|---|
| 227 | v_status VARCHAR(128);
|
|---|
| 228 | BEGIN
|
|---|
| 229 | SELECT role
|
|---|
| 230 | INTO v_role
|
|---|
| 231 | FROM project.users
|
|---|
| 232 | WHERE user_id = NEW.approver_id;
|
|---|
| 233 |
|
|---|
| 234 | IF NOT FOUND THEN
|
|---|
| 235 | RAISE EXCEPTION 'Approver user % does not exist.', NEW.approver_id;
|
|---|
| 236 | END IF;
|
|---|
| 237 |
|
|---|
| 238 | IF v_role NOT IN ('admin', 'approver') THEN
|
|---|
| 239 | RAISE EXCEPTION
|
|---|
| 240 | 'User % cannot approve reservations because role % is not allowed.',
|
|---|
| 241 | NEW.approver_id,
|
|---|
| 242 | v_role;
|
|---|
| 243 | END IF;
|
|---|
| 244 |
|
|---|
| 245 | SELECT status
|
|---|
| 246 | INTO v_status
|
|---|
| 247 | FROM project.reservations
|
|---|
| 248 | WHERE reservation_id = NEW.reservation_id;
|
|---|
| 249 |
|
|---|
| 250 | IF NOT FOUND THEN
|
|---|
| 251 | RAISE EXCEPTION 'Reservation % does not exist.', NEW.reservation_id;
|
|---|
| 252 | END IF;
|
|---|
| 253 |
|
|---|
| 254 | IF v_status NOT IN ('pending', NEW.decision) THEN
|
|---|
| 255 | RAISE EXCEPTION
|
|---|
| 256 | 'Approval can be created only for pending reservations or reservations already synchronized with the same decision. Current status is %.',
|
|---|
| 257 | v_status;
|
|---|
| 258 | END IF;
|
|---|
| 259 |
|
|---|
| 260 | RETURN NEW;
|
|---|
| 261 | END;
|
|---|
| 262 | $$ LANGUAGE plpgsql;
|
|---|
| 263 |
|
|---|
| 264 | CREATE TRIGGER trg_validate_approval_direct_write
|
|---|
| 265 | BEFORE INSERT OR UPDATE
|
|---|
| 266 | ON project.approvals
|
|---|
| 267 | FOR EACH ROW
|
|---|
| 268 | EXECUTE FUNCTION project.fn_validate_approval_direct_write();
|
|---|
| 269 |
|
|---|
| 270 | CREATE OR REPLACE FUNCTION project.fn_sync_reservation_status_after_approval()
|
|---|
| 271 | RETURNS TRIGGER AS $$
|
|---|
| 272 | BEGIN
|
|---|
| 273 | UPDATE project.reservations
|
|---|
| 274 | SET status = NEW.decision
|
|---|
| 275 | WHERE reservation_id = NEW.reservation_id;
|
|---|
| 276 |
|
|---|
| 277 | RETURN NEW;
|
|---|
| 278 | END;
|
|---|
| 279 | $$ LANGUAGE plpgsql;
|
|---|
| 280 |
|
|---|
| 281 | CREATE TRIGGER trg_sync_reservation_status_after_approval
|
|---|
| 282 | AFTER INSERT
|
|---|
| 283 | ON project.approvals
|
|---|
| 284 | FOR EACH ROW
|
|---|
| 285 | EXECUTE FUNCTION project.fn_sync_reservation_status_after_approval();
|
|---|
| 286 |
|
|---|
| 287 | CREATE OR REPLACE FUNCTION project.fn_approve_or_reject_reservation(
|
|---|
| 288 | p_reservation_id INTEGER,
|
|---|
| 289 | p_approver_id INTEGER,
|
|---|
| 290 | p_decision project.approval_decision_domain,
|
|---|
| 291 | p_note VARCHAR DEFAULT NULL
|
|---|
| 292 | )
|
|---|
| 293 | RETURNS TABLE (
|
|---|
| 294 | out_approval_id INTEGER,
|
|---|
| 295 | out_reservation_id INTEGER,
|
|---|
| 296 | out_status VARCHAR,
|
|---|
| 297 | out_decision VARCHAR,
|
|---|
| 298 | out_decision_time TIMESTAMP
|
|---|
| 299 | ) AS $$
|
|---|
| 300 | DECLARE
|
|---|
| 301 | v_status VARCHAR(128);
|
|---|
| 302 | v_role VARCHAR(128);
|
|---|
| 303 | v_approval_id INTEGER;
|
|---|
| 304 | v_decision_time TIMESTAMP;
|
|---|
| 305 | BEGIN
|
|---|
| 306 | SELECT status
|
|---|
| 307 | INTO v_status
|
|---|
| 308 | FROM project.reservations
|
|---|
| 309 | WHERE reservation_id = p_reservation_id
|
|---|
| 310 | FOR UPDATE;
|
|---|
| 311 |
|
|---|
| 312 | IF NOT FOUND THEN
|
|---|
| 313 | RAISE EXCEPTION 'Reservation % does not exist.', p_reservation_id;
|
|---|
| 314 | END IF;
|
|---|
| 315 |
|
|---|
| 316 | IF v_status <> 'pending' THEN
|
|---|
| 317 | RAISE EXCEPTION
|
|---|
| 318 | 'Only pending reservations can be approved or rejected. Current status is %.',
|
|---|
| 319 | v_status;
|
|---|
| 320 | END IF;
|
|---|
| 321 |
|
|---|
| 322 | SELECT role
|
|---|
| 323 | INTO v_role
|
|---|
| 324 | FROM project.users
|
|---|
| 325 | WHERE user_id = p_approver_id;
|
|---|
| 326 |
|
|---|
| 327 | IF NOT FOUND THEN
|
|---|
| 328 | RAISE EXCEPTION 'Approver user % does not exist.', p_approver_id;
|
|---|
| 329 | END IF;
|
|---|
| 330 |
|
|---|
| 331 | IF v_role NOT IN ('admin', 'approver') THEN
|
|---|
| 332 | RAISE EXCEPTION
|
|---|
| 333 | 'User % cannot approve reservations because role % is not allowed.',
|
|---|
| 334 | p_approver_id,
|
|---|
| 335 | v_role;
|
|---|
| 336 | END IF;
|
|---|
| 337 |
|
|---|
| 338 | UPDATE project.reservations
|
|---|
| 339 | SET status = p_decision::VARCHAR
|
|---|
| 340 | WHERE reservation_id = p_reservation_id;
|
|---|
| 341 |
|
|---|
| 342 | INSERT INTO project.approvals (
|
|---|
| 343 | reservation_id,
|
|---|
| 344 | approver_id,
|
|---|
| 345 | decision,
|
|---|
| 346 | decision_time,
|
|---|
| 347 | note
|
|---|
| 348 | )
|
|---|
| 349 | VALUES (
|
|---|
| 350 | p_reservation_id,
|
|---|
| 351 | p_approver_id,
|
|---|
| 352 | p_decision::VARCHAR,
|
|---|
| 353 | CURRENT_TIMESTAMP,
|
|---|
| 354 | p_note
|
|---|
| 355 | )
|
|---|
| 356 | RETURNING approval_id, decision_time
|
|---|
| 357 | INTO v_approval_id, v_decision_time;
|
|---|
| 358 |
|
|---|
| 359 | RETURN QUERY
|
|---|
| 360 | SELECT
|
|---|
| 361 | v_approval_id,
|
|---|
| 362 | p_reservation_id,
|
|---|
| 363 | p_decision::VARCHAR,
|
|---|
| 364 | p_decision::VARCHAR,
|
|---|
| 365 | v_decision_time;
|
|---|
| 366 | END;
|
|---|
| 367 | $$ LANGUAGE plpgsql;
|
|---|
| 368 |
|
|---|
| 369 | CREATE OR REPLACE VIEW project.v_pending_reservation_details AS
|
|---|
| 370 | SELECT
|
|---|
| 371 | res.reservation_id,
|
|---|
| 372 | u.full_name AS requester_name,
|
|---|
| 373 | res.reservation_date,
|
|---|
| 374 | res.start_time,
|
|---|
| 375 | res.end_time,
|
|---|
| 376 | COALESCE(r.room_code, 'No room') AS room_code,
|
|---|
| 377 | COALESCE(b.name, 'No building') AS building_name,
|
|---|
| 378 | res.status,
|
|---|
| 379 | COALESCE(eq.requested_equipment, 'No equipment') AS requested_equipment
|
|---|
| 380 | FROM project.reservations res
|
|---|
| 381 | JOIN project.users u
|
|---|
| 382 | ON res.user_id = u.user_id
|
|---|
| 383 | LEFT JOIN project.rooms r
|
|---|
| 384 | ON res.room_id = r.room_id
|
|---|
| 385 | LEFT JOIN project.buildings b
|
|---|
| 386 | ON r.building_id = b.building_id
|
|---|
| 387 | LEFT JOIN (
|
|---|
| 388 | SELECT
|
|---|
| 389 | req.reservation_id,
|
|---|
| 390 | string_agg(e.name || ' x' || req.requested_quantity, ', ' ORDER BY e.name) AS requested_equipment
|
|---|
| 391 | FROM project.reservation_equipment req
|
|---|
| 392 | JOIN project.equipment e
|
|---|
| 393 | ON req.equipment_id = e.equipment_id
|
|---|
| 394 | GROUP BY req.reservation_id
|
|---|
| 395 | ) eq
|
|---|
| 396 | ON res.reservation_id = eq.reservation_id
|
|---|
| 397 | WHERE res.status = 'pending';
|
|---|
| 398 |
|
|---|
| 399 | CREATE OR REPLACE VIEW project.v_quarterly_room_utilization AS
|
|---|
| 400 | WITH room_reservation_base AS (
|
|---|
| 401 | SELECT
|
|---|
| 402 | date_trunc('quarter', res.reservation_date)::date AS quarter_start,
|
|---|
| 403 | b.name AS building_name,
|
|---|
| 404 | r.room_id,
|
|---|
| 405 | r.room_code,
|
|---|
| 406 | r.type,
|
|---|
| 407 | r.capacity,
|
|---|
| 408 | res.reservation_id,
|
|---|
| 409 | res.status,
|
|---|
| 410 | EXTRACT(EPOCH FROM (res.end_time - res.start_time)) / 3600.0 AS reservation_hours
|
|---|
| 411 | FROM project.reservations res
|
|---|
| 412 | JOIN project.rooms r
|
|---|
| 413 | ON res.room_id = r.room_id
|
|---|
| 414 | JOIN project.buildings b
|
|---|
| 415 | ON r.building_id = b.building_id
|
|---|
| 416 | WHERE res.room_id IS NOT NULL
|
|---|
| 417 | ),
|
|---|
| 418 | room_quarter_summary AS (
|
|---|
| 419 | SELECT
|
|---|
| 420 | quarter_start,
|
|---|
| 421 | building_name,
|
|---|
| 422 | room_id,
|
|---|
| 423 | room_code,
|
|---|
| 424 | type,
|
|---|
| 425 | capacity,
|
|---|
| 426 | COUNT(reservation_id) AS total_room_reservations,
|
|---|
| 427 | COUNT(*) FILTER (WHERE status = 'approved') AS approved_reservations,
|
|---|
| 428 | COUNT(*) FILTER (WHERE status = 'rejected') AS rejected_reservations,
|
|---|
| 429 | COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_reservations,
|
|---|
| 430 | COUNT(*) FILTER (WHERE status = 'pending') AS pending_reservations,
|
|---|
| 431 | ROUND(SUM(reservation_hours), 2) AS requested_hours,
|
|---|
| 432 | ROUND(COALESCE(SUM(reservation_hours) FILTER (WHERE status = 'approved'), 0), 2) AS approved_hours
|
|---|
| 433 | FROM room_reservation_base
|
|---|
| 434 | GROUP BY
|
|---|
| 435 | quarter_start,
|
|---|
| 436 | building_name,
|
|---|
| 437 | room_id,
|
|---|
| 438 | room_code,
|
|---|
| 439 | type,
|
|---|
| 440 | capacity
|
|---|
| 441 | )
|
|---|
| 442 | SELECT
|
|---|
| 443 | quarter_start,
|
|---|
| 444 | building_name,
|
|---|
| 445 | room_code,
|
|---|
| 446 | type,
|
|---|
| 447 | capacity,
|
|---|
| 448 | total_room_reservations,
|
|---|
| 449 | approved_reservations,
|
|---|
| 450 | rejected_reservations,
|
|---|
| 451 | cancelled_reservations,
|
|---|
| 452 | pending_reservations,
|
|---|
| 453 | requested_hours,
|
|---|
| 454 | approved_hours,
|
|---|
| 455 | ROUND(
|
|---|
| 456 | approved_hours / NULLIF(SUM(approved_hours) OVER (PARTITION BY quarter_start), 0) * 100,
|
|---|
| 457 | 2
|
|---|
| 458 | ) AS approved_hours_share_percent,
|
|---|
| 459 | DENSE_RANK() OVER (
|
|---|
| 460 | PARTITION BY quarter_start
|
|---|
| 461 | ORDER BY approved_hours DESC, total_room_reservations DESC, room_code
|
|---|
| 462 | ) AS utilization_rank,
|
|---|
| 463 | CASE
|
|---|
| 464 | WHEN approved_hours >= 4 THEN 'high_usage'
|
|---|
| 465 | WHEN approved_hours >= 2 THEN 'medium_usage'
|
|---|
| 466 | WHEN approved_hours > 0 THEN 'low_usage'
|
|---|
| 467 | ELSE 'no_approved_usage'
|
|---|
| 468 | END AS utilization_level
|
|---|
| 469 | FROM room_quarter_summary; |
|---|