Просмотр исходного кода

Extract calendar grid model into standalone calendar-grid lib

Moves week-grid building (buildWeeks), banner layout (bannersForWeek),
and date utilities (fmtDate, parseLocal, addDays, daysBetween) into
vue/src/lib/calendar-grid/index.js. Pure functions with no app
dependencies; first step toward a reusable CalendarGrid component.

calendar.html imports from the lib and delegates; behavior unchanged.
ZiB 4 месяцев назад
Родитель
Сommit
627ff6e9d3
2 измененных файлов с 104 добавлено и 62 удалено
  1. 5 62
      room_cal/vue/calendar.html
  2. 99 0
      room_cal/vue/src/lib/calendar-grid/index.js

+ 5 - 62
room_cal/vue/calendar.html

@@ -1325,6 +1325,7 @@
 <script type="module">
 import * as Vue from 'vue'
 import '@iconify/iconify'
+import { fmtDate, parseLocal, addDays, daysBetween, buildWeeks, bannersForWeek as _bannersForWeek } from './src/lib/calendar-grid/index.js'
 const API_BASE = window.API_BASE
 const { createApp, ref, computed, watch, onMounted, nextTick, reactive } = Vue;
 const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
@@ -1768,26 +1769,7 @@ createApp({
     }
 
     // ── Helpers ────────────────────────────────────────────
-    function pad(n) { return String(n).padStart(2, '0'); }
-
-    function fmtDate(d) {
-      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
-    }
-
-    function parseLocal(str) {
-      const [y, m, d] = str.split('-').map(Number);
-      return new Date(y, m - 1, d);
-    }
-
-    function addDays(d, n) {
-      const r = new Date(d);
-      r.setDate(r.getDate() + n);
-      return r;
-    }
-
-    function daysBetween(a, b) {
-      return Math.round((b - a) / 86400000);
-    }
+    // fmtDate, parseLocal, addDays, daysBetween imported from calendar-grid
 
     function capitalize(str) {
       return str ? str.charAt(0).toUpperCase() + str.slice(1) : '';
@@ -1820,20 +1802,7 @@ createApp({
     }
 
     // ── Calendar grid ──────────────────────────────────────
-    const weeks = computed(() => {
-      const first = new Date(year.value, month.value, 1);
-      const last  = new Date(year.value, month.value + 1, 0);
-      const cursor = new Date(first);
-      cursor.setDate(cursor.getDate() - cursor.getDay());
-      const result = [];
-      while (true) {
-        const week = [];
-        for (let i = 0; i < 7; i++) { week.push(new Date(cursor)); cursor.setDate(cursor.getDate() + 1); }
-        result.push(week);
-        if (cursor > last) break;
-      }
-      return result;
-    });
+    const weeks = computed(() => buildWeeks(year.value, month.value));
 
     const monthName = computed(() =>
       new Date(year.value, month.value, 1).toLocaleString('default', { month: 'long' })
@@ -2087,34 +2056,8 @@ createApp({
     });
 
     function bannersForWeek(week) {
-      const weekStart = week[0];
-      const weekEnd   = week[6];
-      const banners   = [];
-
-      for (const span of [...reservationSpans.value, ...closureSpans.value]) {
-        if (span.end < weekStart || span.start > weekEnd) continue;
-
-        const clipStart = span.start < weekStart ? weekStart : span.start;
-        const clipEnd   = span.end   > weekEnd   ? weekEnd   : span.end;
-
-        const startCol   = daysBetween(weekStart, clipStart);
-        const spanCols   = daysBetween(clipStart, clipEnd) + 1;
-        const leftInset  = span.start >= weekStart ? 0.2 : 0;
-        const rightBleed = span.end   <= weekEnd   ? 0.8 : 0;
-
-        const leftPct  = ((startCol + leftInset) / 7) * 100;
-        const widthPct = ((spanCols - leftInset - rightBleed) / 7) * 100;
-
-        banners.push({
-          cls:     span.cls,
-          label:   span.label,
-          spanKey: span.resKey ?? null,
-          span,
-          left:    `calc(${leftPct.toFixed(4)}% + 2px)`,
-          width:   `calc(${widthPct.toFixed(4)}% - 4px)`,
-        });
-      }
-      return banners;
+      return _bannersForWeek(week, [...reservationSpans.value, ...closureSpans.value])
+        .map(b => ({ ...b, spanKey: b.resKey ?? null, span: b }));
     }
 
     // ── Fetch ──────────────────────────────────────────────

+ 99 - 0
room_cal/vue/src/lib/calendar-grid/index.js

@@ -0,0 +1,99 @@
+// ── Date utilities ────────────────────────────────────────────
+
+function pad(n) { return String(n).padStart(2, '0'); }
+
+/** Date → 'YYYY-MM-DD' (local time, no timezone shift) */
+export function fmtDate(d) {
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+}
+
+/** 'YYYY-MM-DD' → Date (local midnight, no timezone shift) */
+export function parseLocal(str) {
+  const [y, m, d] = str.split('-').map(Number);
+  return new Date(y, m - 1, d);
+}
+
+/** Return a new Date offset by n days */
+export function addDays(d, n) {
+  const r = new Date(d);
+  r.setDate(r.getDate() + n);
+  return r;
+}
+
+/** Whole days between two Date objects (b − a) */
+export function daysBetween(a, b) {
+  return Math.round((b - a) / 86400000);
+}
+
+// ── Week grid ─────────────────────────────────────────────────
+
+/**
+ * Build the week grid for a given month.
+ *
+ * Returns an array of weeks; each week is an array of 7 Date objects.
+ * Lead and trailing days from adjacent months fill the first and last rows
+ * so every row is a full Sunday–Saturday week.
+ *
+ * @param {number} year  - full year (e.g. 2026)
+ * @param {number} month - 0-indexed month (0 = January)
+ * @returns {Date[][]}
+ */
+export function buildWeeks(year, month) {
+  const first  = new Date(year, month, 1);
+  const last   = new Date(year, month + 1, 0);
+  const cursor = new Date(first);
+  cursor.setDate(cursor.getDate() - cursor.getDay()); // rewind to Sunday
+  const result = [];
+  while (true) {
+    const week = [];
+    for (let i = 0; i < 7; i++) {
+      week.push(new Date(cursor));
+      cursor.setDate(cursor.getDate() + 1);
+    }
+    result.push(week);
+    if (cursor > last) break;
+  }
+  return result;
+}
+
+// ── Banner layout ─────────────────────────────────────────────
+
+/**
+ * Compute the positioned banners that should appear in a single week row.
+ *
+ * Spans are clipped to the week boundary; the returned objects carry `left`
+ * and `width` as CSS percentage strings ready to drop into an absolutely-
+ * positioned overlay, plus all original span fields passed through unchanged.
+ *
+ * @param {Date[]} week   - 7-element array from buildWeeks()
+ * @param {Array}  spans  - objects with at minimum { start: Date, end: Date }
+ *                          plus any app fields (label, cls, resKey, …)
+ * @returns {Array}       - banner objects: { left, width, ...span }
+ */
+export function bannersForWeek(week, spans) {
+  const weekStart = week[0];
+  const weekEnd   = week[6];
+  const banners   = [];
+
+  for (const span of spans) {
+    if (span.end < weekStart || span.start > weekEnd) continue;
+
+    const clipStart = span.start < weekStart ? weekStart : span.start;
+    const clipEnd   = span.end   > weekEnd   ? weekEnd   : span.end;
+
+    const startCol   = daysBetween(weekStart, clipStart);
+    const spanCols   = daysBetween(clipStart, clipEnd) + 1;
+    const leftInset  = span.start >= weekStart ? 0.2 : 0;
+    const rightBleed = span.end   <= weekEnd   ? 0.8 : 0;
+
+    const leftPct  = ((startCol + leftInset) / 7) * 100;
+    const widthPct = ((spanCols - leftInset - rightBleed) / 7) * 100;
+
+    banners.push({
+      ...span,
+      left:  `calc(${leftPct.toFixed(4)}% + 2px)`,
+      width: `calc(${widthPct.toFixed(4)}% - 4px)`,
+    });
+  }
+  return banners;
+}