diff --git a/ht_booking/assets/views/calendar/week.html b/ht_booking/assets/views/calendar/week.html index daf2381..9aba281 100644 --- a/ht_booking/assets/views/calendar/week.html +++ b/ht_booking/assets/views/calendar/week.html @@ -95,7 +95,7 @@
{{ court_name }} · {{ week_label }}
-
+
@@ -110,6 +110,14 @@ {% for row in rows %} + {% if row.free_group %} + + + + + {% else %} {% for cell in row.cells %} @@ -147,6 +155,7 @@ {% endfor %} + {% endif %} {% endfor %}
{{ row.hour_label }} +
{{ t(key="free", lang=lang) }}
+
{{ row.hour_label }}
diff --git a/ht_booking/src/controllers/calendar.rs b/ht_booking/src/controllers/calendar.rs index c9ae2b4..27f94b9 100644 --- a/ht_booking/src/controllers/calendar.rs +++ b/ht_booking/src/controllers/calendar.rs @@ -64,6 +64,9 @@ pub struct Cell { pub struct Row { pub hour_label: String, pub cells: Vec, + /// Public view only: `true` when this row collapses a run of fully-free + /// hours into one compact strip. The admin grid never sets it. + pub free_group: bool, } #[derive(Debug, Serialize)] @@ -107,6 +110,46 @@ fn week_monday(week: Option<&str>) -> NaiveDate { monday_of(base) } +/// Collapses runs of fully-free hour rows into one compact strip so a court +/// that isn't booked solid doesn't waste vertical space. Rows holding at +/// least one booking are kept full-size so bookings stay easy to read. +/// Public calendar only — the admin grid always shows every hour. +fn group_free_rows(rows: Vec) -> Vec { + let mut out: Vec = Vec::new(); + let mut run: Vec = Vec::new(); + for row in rows { + if row.cells.iter().all(|c| !c.booked) { + run.push(row); + } else { + flush_free_run(&mut run, &mut out); + out.push(row); + } + } + flush_free_run(&mut run, &mut out); + out +} + +/// Drains a pending run of free rows into `out` as a single collapsed row. +fn flush_free_run(run: &mut Vec, out: &mut Vec) { + if run.is_empty() { + return; + } + let hour_of = |r: &Row| r.cells.first().map_or(FIRST_HOUR, |c| c.hour); + let first = run.first().map_or(FIRST_HOUR, hour_of); + let last = run.last().map_or(first, hour_of); + let hour_label = if run.len() > 1 { + format!("{first:02}:00 – {:02}:00", last + 1) + } else { + format!("{first:02}:00") + }; + out.push(Row { + hour_label, + cells: Vec::new(), + free_group: true, + }); + run.clear(); +} + /// Builds the calendar grid for the selected court and week. pub async fn build_calendar( ctx: &AppContext, @@ -201,10 +244,15 @@ pub async fn build_calendar( Row { hour_label: format!("{hour:02}:00"), cells, + free_group: false, } }) .collect(); + // The public calendar collapses empty stretches to stay compact; the admin + // grid always shows every hour so each slot stays individually editable. + let rows = if is_admin { rows } else { group_free_rows(rows) }; + Ok(CalendarPage { lang: lang.to_string(), is_admin,