Changes between Initial Version and Version 1 of ComplexReports


Ignore:
Timestamp:
04/05/26 19:32:06 (6 days ago)
Author:
221511
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • ComplexReports

    v1 v1  
     1= Advanced Reports =
     2
     3== 1. Resource Utilization and Demand Analysis ==
     4
     5Quarterly report showing each resource's utilization rate, peak usage hours, busiest day, approval rate, and demand ranking. Helps administrators identify underutilized resources that could be repurposed and overdemanded resources that may need capacity expansion or alternatives.
     6
     7=== Solution SQL ===
     8
     9{{{
     10SET search_path TO project;
     11
     12WITH quarter_bounds AS (
     13    SELECT DATE '2026-01-01' AS q_start, DATE '2026-03-31' AS q_end
     14),
     15quarter_days AS (
     16    SELECT d::DATE AS day, EXTRACT(ISODOW FROM d)::INT AS dow
     17    FROM quarter_bounds qb, generate_series(qb.q_start, qb.q_end, '1 day'::INTERVAL) AS d
     18),
     19resource_availability AS (
     20    SELECT
     21        r.resource_id,
     22        SUM(CASE
     23            WHEN qd.dow <= 5 OR r.available_weekends
     24            THEN EXTRACT(EPOCH FROM (r.available_to - r.available_from)) / 3600.0
     25            ELSE 0
     26        END) AS total_available_hours
     27    FROM resources r
     28    CROSS JOIN quarter_days qd
     29    GROUP BY r.resource_id
     30),
     31reservation_stats AS (
     32    SELECT
     33        rv.resource_id,
     34        COUNT(*) AS total_reservations,
     35        COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved_count,
     36        COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected_count,
     37        COUNT(*) FILTER (WHERE rv.status = 'cancelled') AS cancelled_count,
     38        ROUND(SUM(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0)
     39              FILTER (WHERE rv.status IN ('approved', 'completed')), 1) AS reserved_hours
     40    FROM reservations rv, quarter_bounds qb
     41    WHERE rv.start_time >= qb.q_start
     42      AND rv.start_time < qb.q_end + INTERVAL '1 day'
     43    GROUP BY rv.resource_id
     44),
     45popular_day AS (
     46    SELECT DISTINCT ON (rv.resource_id)
     47        rv.resource_id,
     48        TRIM(TO_CHAR(rv.start_time, 'Day')) AS busiest_day
     49    FROM reservations rv, quarter_bounds qb
     50    WHERE rv.start_time >= qb.q_start
     51      AND rv.start_time < qb.q_end + INTERVAL '1 day'
     52      AND rv.status IN ('approved', 'completed')
     53    GROUP BY rv.resource_id, TRIM(TO_CHAR(rv.start_time, 'Day'))
     54    ORDER BY rv.resource_id, COUNT(*) DESC
     55),
     56peak_hour AS (
     57    SELECT DISTINCT ON (rv.resource_id)
     58        rv.resource_id,
     59        EXTRACT(HOUR FROM rv.start_time)::INT AS peak_hour
     60    FROM reservations rv, quarter_bounds qb
     61    WHERE rv.start_time >= qb.q_start
     62      AND rv.start_time < qb.q_end + INTERVAL '1 day'
     63      AND rv.status IN ('approved', 'completed')
     64    GROUP BY rv.resource_id, EXTRACT(HOUR FROM rv.start_time)
     65    ORDER BY rv.resource_id, COUNT(*) DESC
     66)
     67SELECT
     68    r.name AS resource_name,
     69    rt.type_name AS resource_type,
     70    COALESCE(l.building || ' ' || l.room, 'Digital') AS location,
     71    COALESCE(rs.total_reservations, 0) AS total_reservations,
     72    COALESCE(rs.approved_count, 0) AS approved,
     73    COALESCE(rs.rejected_count, 0) AS rejected,
     74    COALESCE(rs.cancelled_count, 0) AS cancelled,
     75    COALESCE(ROUND(rs.approved_count::NUMERIC / NULLIF(rs.total_reservations, 0) * 100, 1), 0)
     76        AS approval_rate_pct,
     77    COALESCE(rs.reserved_hours, 0) AS reserved_hours,
     78    ROUND(ra.total_available_hours, 0) AS available_hours,
     79    COALESCE(ROUND(rs.reserved_hours / NULLIF(ra.total_available_hours, 0) * 100, 2), 0)
     80        AS utilization_pct,
     81    COALESCE(pd.busiest_day, '-') AS busiest_day,
     82    COALESCE(ph.peak_hour || ':00', '-') AS peak_hour,
     83    RANK() OVER (
     84        ORDER BY COALESCE(rs.reserved_hours, 0)
     85                 / NULLIF(ra.total_available_hours, 0) DESC NULLS LAST
     86    ) AS demand_rank
     87FROM resources r
     88JOIN resource_types rt ON r.type_id = rt.type_id
     89LEFT JOIN locations l ON r.location_id = l.location_id
     90JOIN resource_availability ra ON r.resource_id = ra.resource_id
     91LEFT JOIN reservation_stats rs ON r.resource_id = rs.resource_id
     92LEFT JOIN popular_day pd ON r.resource_id = pd.resource_id
     93LEFT JOIN peak_hour ph ON r.resource_id = ph.resource_id
     94ORDER BY utilization_pct DESC;
     95}}}
     96
     97=== Solution Relational Algebra ===
     98
     99{{{
     100QuarterDays <- π day, dow (
     101  σ day >= '2026-01-01' ∧ day <= '2026-03-31' (CalendarDays)
     102)
     103
     104ResourceAvailability <-
     105  γ resource_id; total_available_hours := SUM(daily_hours) (
     106    π resource_id, (CASE dow ≤ 5 ∨ available_weekends THEN hours ELSE 0) → daily_hours (
     107      resources × QuarterDays
     108    )
     109  )
     110
     111ReservationStats <-
     112  γ resource_id;
     113    total_reservations := COUNT(*),
     114    approved_count := COUNT(σ status ∈ {'approved','completed'}),
     115    rejected_count := COUNT(σ status = 'rejected'),
     116    cancelled_count := COUNT(σ status = 'cancelled'),
     117    reserved_hours := SUM(duration_hours WHERE status ∈ {'approved','completed'}) (
     118      σ start_time >= '2026-01-01' ∧ start_time < '2026-04-01' (
     119        π resource_id, status, (end_time − start_time)/3600 → duration_hours (reservations)
     120      )
     121  )
     122
     123PopularDay <-
     124  γ resource_id; busiest_day := DAY_NAME(start_time) HAVING MAX(COUNT(*)) (
     125    σ status ∈ {'approved','completed'} ∧ start_time ∈ Quarter (reservations)
     126  )
     127
     128PeakHour <-
     129  γ resource_id; peak_hour := HOUR(start_time) HAVING MAX(COUNT(*)) (
     130    σ status ∈ {'approved','completed'} ∧ start_time ∈ Quarter (reservations)
     131  )
     132
     133Result <-
     134  π resource_name, resource_type, location, total_reservations, approved_count,
     135    rejected_count, cancelled_count, approval_rate_pct, reserved_hours,
     136    available_hours, utilization_pct, busiest_day, peak_hour, demand_rank (
     137      (
     138        (
     139          (
     140            (resources ⨝ resource_types)
     141            ⟕ locations
     142          )
     143          ⨝ ResourceAvailability
     144        )
     145        ⟕ ReservationStats
     146      )
     147      ⟕ PopularDay
     148      ⟕ PeakHour
     149  )
     150}}}
     151
     152== 2. Monthly Reservation Trends with Cumulative Statistics ==
     153
     154Monthly time-series report showing reservation counts, approval rates, month-over-month growth, cumulative totals, and the top 3 most demanded resources per month. Useful for identifying seasonal patterns and forecasting future demand at the semester or academic year level.
     155
     156=== Solution SQL ===
     157
     158{{{
     159SET search_path TO project;
     160
     161WITH monthly_overview AS (
     162    SELECT
     163        DATE_TRUNC('month', rv.start_time) AS month,
     164        COUNT(*) AS total_reservations,
     165        COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved,
     166        COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected,
     167        COUNT(*) FILTER (WHERE rv.status = 'cancelled') AS cancelled,
     168        COUNT(*) FILTER (WHERE rv.status = 'pending') AS pending,
     169        COUNT(DISTINCT rv.user_id) AS unique_users,
     170        COUNT(DISTINCT rv.resource_id) AS unique_resources,
     171        ROUND(AVG(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0), 1)
     172            AS avg_duration_hours,
     173        ROUND(SUM(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0)
     174              FILTER (WHERE rv.status IN ('approved', 'completed')), 1)
     175            AS total_approved_hours
     176    FROM reservations rv
     177    GROUP BY DATE_TRUNC('month', rv.start_time)
     178),
     179monthly_trends AS (
     180    SELECT
     181        TO_CHAR(month, 'YYYY-MM') AS month_label,
     182        month,
     183        total_reservations,
     184        approved,
     185        rejected,
     186        cancelled,
     187        pending,
     188        unique_users,
     189        unique_resources,
     190        avg_duration_hours,
     191        total_approved_hours,
     192        SUM(total_reservations) OVER (ORDER BY month) AS cumulative_total,
     193        ROUND(approved::NUMERIC / NULLIF(total_reservations, 0) * 100, 1) AS approval_rate,
     194        CASE
     195            WHEN LAG(total_reservations) OVER (ORDER BY month) IS NULL THEN NULL
     196            ELSE ROUND(
     197                (total_reservations - LAG(total_reservations) OVER (ORDER BY month))::NUMERIC
     198                / LAG(total_reservations) OVER (ORDER BY month) * 100, 1
     199            )
     200        END AS mom_change_pct
     201    FROM monthly_overview
     202),
     203resource_demand_ranked AS (
     204    SELECT
     205        TO_CHAR(DATE_TRUNC('month', rv.start_time), 'YYYY-MM') AS month_label,
     206        r.name AS resource_name,
     207        COUNT(*) AS demand_count,
     208        ROW_NUMBER() OVER (
     209            PARTITION BY DATE_TRUNC('month', rv.start_time)
     210            ORDER BY COUNT(*) DESC
     211        ) AS rank
     212    FROM reservations rv
     213    JOIN resources r ON rv.resource_id = r.resource_id
     214    GROUP BY DATE_TRUNC('month', rv.start_time), r.name
     215),
     216top_resources AS (
     217    SELECT
     218        month_label,
     219        STRING_AGG(resource_name || ' (' || demand_count || ')',
     220                   ', ' ORDER BY rank) AS top_3_resources
     221    FROM resource_demand_ranked
     222    WHERE rank <= 3
     223    GROUP BY month_label
     224)
     225SELECT
     226    mt.month_label,
     227    mt.total_reservations,
     228    mt.approved,
     229    mt.rejected,
     230    mt.cancelled,
     231    mt.pending,
     232    mt.approval_rate AS approval_rate_pct,
     233    COALESCE(mt.mom_change_pct || '%', 'N/A') AS month_over_month,
     234    mt.cumulative_total,
     235    mt.unique_users,
     236    mt.unique_resources,
     237    mt.avg_duration_hours,
     238    mt.total_approved_hours,
     239    COALESCE(tr.top_3_resources, '-') AS top_demanded_resources
     240FROM monthly_trends mt
     241LEFT JOIN top_resources tr ON mt.month_label = tr.month_label
     242ORDER BY mt.month_label;
     243}}}
     244
     245=== Solution Relational Algebra ===
     246
     247{{{
     248MonthlyOverview <-
     249  γ month := TRUNC_MONTH(start_time);
     250    total_reservations := COUNT(*),
     251    approved := COUNT(σ status ∈ {'approved','completed'}),
     252    rejected := COUNT(σ status = 'rejected'),
     253    cancelled := COUNT(σ status = 'cancelled'),
     254    pending := COUNT(σ status = 'pending'),
     255    unique_users := COUNT_DISTINCT(user_id),
     256    unique_resources := COUNT_DISTINCT(resource_id),
     257    avg_duration_hours := AVG(duration),
     258    total_approved_hours := SUM(duration WHERE status ∈ {'approved','completed'}) (
     259      π start_time, status, user_id, resource_id,
     260        (end_time − start_time)/3600 → duration (reservations)
     261  )
     262
     263MonthlyTrends <-
     264  π month_label, total_reservations, approved, rejected, cancelled, pending,
     265    approval_rate, cumulative_total, mom_change_pct (
     266      ω ORDER BY month;
     267        cumulative_total := SUM(total_reservations),
     268        mom_change_pct := (total_reservations − LAG(total_reservations))
     269                          / LAG(total_reservations) * 100 (
     270          MonthlyOverview
     271      )
     272  )
     273
     274ResourceDemandRanked <-
     275  ω PARTITION BY month ORDER BY demand_count DESC;
     276    rank := ROW_NUMBER() (
     277      γ month := TRUNC_MONTH(start_time), resource_name := r.name;
     278        demand_count := COUNT(*) (
     279          reservations rv ⨝ (rv.resource_id = r.resource_id) resources r
     280      )
     281  )
     282
     283TopResources <-
     284  γ month_label; top_3 := CONCAT(resource_name, demand_count) (
     285    σ rank ≤ 3 (ResourceDemandRanked)
     286  )
     287
     288Result <-
     289  π month_label, total_reservations, approved, rejected, cancelled, pending,
     290    approval_rate, mom_change_pct, cumulative_total, top_3 (
     291      MonthlyTrends ⟕ (month_label) TopResources
     292  )
     293}}}
     294
     295== 3. User Activity and Behavior Analysis ==
     296
     297Per-user report showing total reservations, approval and cancellation rates, most-used resource, total hours consumed, and activity ranking. Helps identify the most active users, detect problematic patterns (high cancellation rates), and understand how different user types (students vs. teaching staff) use the system.
     298
     299=== Solution SQL ===
     300
     301{{{
     302SET search_path TO project;
     303
     304WITH user_stats AS (
     305    SELECT
     306        rv.user_id,
     307        COUNT(*) AS total_reservations,
     308        COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved,
     309        COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected,
     310        COUNT(*) FILTER (WHERE rv.status = 'cancelled') AS cancelled,
     311        ROUND(COUNT(*) FILTER (WHERE rv.status = 'cancelled')::NUMERIC
     312              / NULLIF(COUNT(*), 0) * 100, 1) AS cancellation_rate,
     313        ROUND(AVG(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0), 1)
     314            AS avg_duration_hours,
     315        ROUND(SUM(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0)
     316              FILTER (WHERE rv.status IN ('approved', 'completed')), 1) AS total_hours_used,
     317        COUNT(DISTINCT rv.resource_id) AS distinct_resources_used
     318    FROM reservations rv
     319    GROUP BY rv.user_id
     320),
     321favorite_resource AS (
     322    SELECT DISTINCT ON (rv.user_id)
     323        rv.user_id,
     324        r.name AS favorite_resource,
     325        COUNT(*) AS use_count
     326    FROM reservations rv
     327    JOIN resources r ON rv.resource_id = r.resource_id
     328    WHERE rv.status IN ('approved', 'completed')
     329    GROUP BY rv.user_id, r.name
     330    ORDER BY rv.user_id, COUNT(*) DESC
     331)
     332SELECT
     333    u.first_name || ' ' || u.last_name AS user_name,
     334    ut.type_name AS user_type,
     335    COALESCE(us.total_reservations, 0) AS total_reservations,
     336    COALESCE(us.approved, 0) AS approved,
     337    COALESCE(us.rejected, 0) AS rejected,
     338    COALESCE(us.cancelled, 0) AS cancelled,
     339    COALESCE(us.cancellation_rate, 0) AS cancellation_rate_pct,
     340    COALESCE(us.avg_duration_hours, 0) AS avg_duration_hours,
     341    COALESCE(us.total_hours_used, 0) AS total_hours_used,
     342    COALESCE(us.distinct_resources_used, 0) AS distinct_resources,
     343    COALESCE(fr.favorite_resource, '-') AS most_used_resource,
     344    COALESCE(fr.use_count, 0) AS most_used_count,
     345    RANK() OVER (ORDER BY COALESCE(us.total_reservations, 0) DESC) AS activity_rank
     346FROM users u
     347JOIN user_types ut ON u.type_id = ut.type_id
     348LEFT JOIN user_stats us ON u.user_id = us.user_id
     349LEFT JOIN favorite_resource fr ON u.user_id = fr.user_id
     350ORDER BY activity_rank;
     351}}}
     352
     353=== Solution Relational Algebra ===
     354
     355{{{
     356UserStats <-
     357  γ user_id;
     358    total_reservations := COUNT(*),
     359    approved := COUNT(σ status ∈ {'approved','completed'}),
     360    rejected := COUNT(σ status = 'rejected'),
     361    cancelled := COUNT(σ status = 'cancelled'),
     362    cancellation_rate := COUNT(σ status='cancelled') / COUNT(*) * 100,
     363    avg_duration_hours := AVG(duration),
     364    total_hours_used := SUM(duration WHERE status ∈ {'approved','completed'}),
     365    distinct_resources := COUNT_DISTINCT(resource_id) (
     366      π user_id, status, resource_id,
     367        (end_time − start_time)/3600 → duration (reservations)
     368  )
     369
     370FavoriteResource <-
     371  γ user_id; favorite_resource := r.name HAVING MAX(use_count) (
     372    γ user_id, r.name; use_count := COUNT(*) (
     373      σ status ∈ {'approved','completed'} (
     374        reservations rv ⨝ (rv.resource_id = r.resource_id) resources r
     375      )
     376    )
     377  )
     378
     379Result <-
     380  π user_name, user_type, total_reservations, approved, rejected, cancelled,
     381    cancellation_rate, avg_duration_hours, total_hours_used,
     382    distinct_resources, favorite_resource, activity_rank (
     383      (
     384        (users u ⨝ (u.type_id = ut.type_id) user_types ut)
     385        ⟕ (u.user_id = us.user_id) UserStats us
     386      )
     387      ⟕ (u.user_id = fr.user_id) FavoriteResource fr
     388  )
     389}}}
     390
     391== 4. Administrator Approval Workload and Bottleneck Analysis ==
     392
     393Shows each administrator's approval workload, approval/rejection rates, workload share percentage, and system-wide pending reservation wait times. Helps management identify if approval responsibilities are evenly distributed and whether pending reservations are being handled in a timely manner.
     394
     395=== Solution SQL ===
     396
     397{{{
     398SET search_path TO project;
     399
     400WITH admin_stats AS (
     401    SELECT
     402        rv.approved_by AS admin_id,
     403        COUNT(*) AS total_reviewed,
     404        COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved_count,
     405        COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected_count,
     406        ROUND(COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed'))::NUMERIC
     407              / NULLIF(COUNT(*), 0) * 100, 1) AS approval_rate,
     408        COUNT(DISTINCT rv.resource_id) AS distinct_resources_handled,
     409        COUNT(DISTINCT rv.user_id) AS distinct_users_served
     410    FROM reservations rv
     411    WHERE rv.approved_by IS NOT NULL
     412    GROUP BY rv.approved_by
     413),
     414pending_stats AS (
     415    SELECT
     416        COUNT(*) AS total_pending,
     417        ROUND(AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - rv.created_at))
     418              / 86400.0), 1) AS avg_wait_days,
     419        MAX(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - rv.created_at))
     420            / 86400.0)::INT AS max_wait_days
     421    FROM reservations rv
     422    WHERE rv.status = 'pending'
     423),
     424workload_share AS (
     425    SELECT
     426        admin_id,
     427        total_reviewed,
     428        ROUND(total_reviewed::NUMERIC / SUM(total_reviewed) OVER () * 100, 1)
     429            AS workload_share_pct
     430    FROM admin_stats
     431)
     432SELECT
     433    u.first_name || ' ' || u.last_name AS administrator,
     434    COALESCE(ast.total_reviewed, 0) AS total_reviewed,
     435    COALESCE(ast.approved_count, 0) AS approved,
     436    COALESCE(ast.rejected_count, 0) AS rejected,
     437    COALESCE(ast.approval_rate, 0) AS approval_rate_pct,
     438    COALESCE(ws.workload_share_pct, 0) AS workload_share_pct,
     439    COALESCE(ast.distinct_resources_handled, 0) AS resources_handled,
     440    COALESCE(ast.distinct_users_served, 0) AS users_served,
     441    ps.total_pending AS system_pending_count,
     442    ps.avg_wait_days AS pending_avg_wait_days,
     443    ps.max_wait_days AS pending_max_wait_days
     444FROM users u
     445JOIN user_types ut ON u.type_id = ut.type_id
     446LEFT JOIN admin_stats ast ON u.user_id = ast.admin_id
     447LEFT JOIN workload_share ws ON u.user_id = ws.admin_id
     448CROSS JOIN pending_stats ps
     449WHERE ut.type_name = 'Administrator'
     450ORDER BY total_reviewed DESC;
     451}}}
     452
     453=== Solution Relational Algebra ===
     454
     455{{{
     456AdminStats <-
     457  γ admin_id := approved_by;
     458    total_reviewed := COUNT(*),
     459    approved_count := COUNT(σ status ∈ {'approved','completed'}),
     460    rejected_count := COUNT(σ status = 'rejected'),
     461    approval_rate := COUNT(σ status ∈ {'approved','completed'}) / COUNT(*) * 100,
     462    distinct_resources := COUNT_DISTINCT(resource_id),
     463    distinct_users := COUNT_DISTINCT(user_id) (
     464      σ approved_by IS NOT NULL (reservations)
     465  )
     466
     467PendingStats <-
     468  γ total_pending := COUNT(*),
     469    avg_wait_days := AVG((NOW() − created_at) / 86400),
     470    max_wait_days := MAX((NOW() − created_at) / 86400) (
     471      σ status = 'pending' (reservations)
     472  )
     473
     474WorkloadShare <-
     475  π admin_id, total_reviewed,
     476    total_reviewed / SUM(total_reviewed) * 100 → workload_share_pct (
     477      AdminStats
     478  )
     479
     480Result <-
     481  π administrator, total_reviewed, approved, rejected, approval_rate,
     482    workload_share_pct, resources_handled, users_served,
     483    total_pending, avg_wait_days, max_wait_days (
     484      (
     485        (
     486          σ type_name = 'Administrator' (users u ⨝ user_types ut)
     487          ⟕ (u.user_id = ast.admin_id) AdminStats ast
     488        )
     489        ⟕ (u.user_id = ws.admin_id) WorkloadShare ws
     490      )
     491      × PendingStats ps
     492  )
     493}}}
     494
     495== 5. Resource Recommendation Based on Similar Users ==
     496
     497Collaborative filtering report: given a target user, finds other users with similar reservation patterns (shared resources), then recommends resources those similar users have used that the target user has not tried yet. Useful for suggesting relevant resources to users and improving resource discovery.
     498
     499=== Solution SQL ===
     500
     501{{{
     502SET search_path TO project;
     503
     504WITH target_user AS (
     505    SELECT 5 AS user_id  -- parameterize per user
     506),
     507target_resources AS (
     508    SELECT DISTINCT rv.resource_id
     509    FROM reservations rv, target_user tu
     510    WHERE rv.user_id = tu.user_id
     511      AND rv.status IN ('approved', 'completed')
     512),
     513similar_users AS (
     514    SELECT
     515        rv.user_id AS similar_user_id,
     516        COUNT(DISTINCT rv.resource_id) AS shared_resources,
     517        RANK() OVER (ORDER BY COUNT(DISTINCT rv.resource_id) DESC) AS similarity_rank
     518    FROM reservations rv
     519    JOIN target_resources tr ON rv.resource_id = tr.resource_id
     520    CROSS JOIN target_user tu
     521    WHERE rv.user_id != tu.user_id
     522      AND rv.status IN ('approved', 'completed')
     523    GROUP BY rv.user_id
     524),
     525recommended_resources AS (
     526    SELECT
     527        rv.resource_id,
     528        r.name AS resource_name,
     529        rt.type_name AS resource_type,
     530        COALESCE(l.building || ' ' || l.room, 'Digital') AS location,
     531        COUNT(DISTINCT rv.user_id) AS recommended_by_count,
     532        ROUND(AVG(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0), 1)
     533            AS avg_usage_hours
     534    FROM reservations rv
     535    JOIN similar_users su ON rv.user_id = su.similar_user_id
     536    JOIN resources r ON rv.resource_id = r.resource_id
     537    JOIN resource_types rt ON r.type_id = rt.type_id
     538    LEFT JOIN locations l ON r.location_id = l.location_id
     539    WHERE rv.status IN ('approved', 'completed')
     540      AND su.similarity_rank <= 5
     541      AND rv.resource_id NOT IN (SELECT resource_id FROM target_resources)
     542    GROUP BY rv.resource_id, r.name, rt.type_name, l.building, l.room
     543)
     544SELECT
     545    u.first_name || ' ' || u.last_name AS target_user,
     546    rr.resource_name,
     547    rr.resource_type,
     548    rr.location,
     549    rr.recommended_by_count AS similar_users_use_it,
     550    rr.avg_usage_hours,
     551    RANK() OVER (ORDER BY rr.recommended_by_count DESC,
     552                          rr.avg_usage_hours DESC) AS recommendation_rank
     553FROM recommended_resources rr
     554CROSS JOIN target_user tu
     555JOIN users u ON tu.user_id = u.user_id
     556ORDER BY recommendation_rank;
     557}}}
     558
     559=== Solution Relational Algebra ===
     560
     561{{{
     562TargetUser <- {(user_id: 5)}
     563
     564TargetResources <-
     565  π resource_id (
     566    σ status ∈ {'approved','completed'} (
     567      reservations ⨝ (user_id) TargetUser
     568    )
     569  )
     570
     571SimilarUsers <-
     572  ω ORDER BY shared_resources DESC; similarity_rank := RANK() (
     573    γ similar_user_id := rv.user_id;
     574      shared_resources := COUNT_DISTINCT(rv.resource_id) (
     575        σ rv.user_id ≠ tu.user_id ∧ rv.status ∈ {'approved','completed'} (
     576          (reservations rv ⨝ (rv.resource_id = tr.resource_id) TargetResources tr)
     577          × TargetUser tu
     578        )
     579    )
     580  )
     581
     582RecommendedResources <-
     583  γ resource_id, resource_name, resource_type, location;
     584    recommended_by := COUNT_DISTINCT(user_id),
     585    avg_usage_hours := AVG(duration) (
     586      π rv.resource_id, r.name → resource_name, rt.type_name → resource_type,
     587        l.building || l.room → location, rv.user_id,
     588        (rv.end_time − rv.start_time)/3600 → duration (
     589          σ rv.status ∈ {'approved','completed'} ∧ su.similarity_rank ≤ 5 (
     590            (
     591              (
     592                reservations rv
     593                ⨝ (rv.user_id = su.similar_user_id) σ similarity_rank ≤ 5 (SimilarUsers su)
     594              )
     595              ⨝ (rv.resource_id = r.resource_id) resources r
     596              ⨝ (r.type_id = rt.type_id) resource_types rt
     597              ⟕ (r.location_id = l.location_id) locations l
     598            )
     599            − (π * (reservations rv ⨝ TargetResources))
     600          )
     601      )
     602  )
     603
     604Result <-
     605  π target_user, resource_name, resource_type, location,
     606    recommended_by, avg_usage_hours, recommendation_rank (
     607      ω ORDER BY recommended_by DESC, avg_usage_hours DESC;
     608        recommendation_rank := RANK() (
     609          RecommendedResources × (TargetUser ⨝ users)
     610      )
     611  )
     612}}}