Add template groups: bulk multi-location creation + duplicate action

For recurring jobs that repeat identically across many locations (e.g.
clean AC filter in every room), one task_templates row per location is
still required — schedule/completion tracking is per-location, which
matters especially for floating mode — but creating and organising
that many rows by hand didn't scale.

- New template_groups table (id, unique name) + task_templates.group_id.
  POST /api/template-groups upserts by name so picking an existing
  group from the UI never creates a duplicate folder.
- Recurring template form: optional group name (datalist-autocompleted
  from existing groups) plus a "create for multiple locations" toggle
  that reveals a checkbox list of locations; saving loops one
  createTemplate() call per selected location, all sharing the
  resolved group_id.
- Duplicate button on each template row pre-fills the New Template
  form from that row (location/asset/next-due cleared for repicking),
  landing in the same group automatically — the lightweight path for
  adding one more room later without the bulk picker.
- Recurring.tsx list now groups rows under a collapsible folder header
  per group_name; ungrouped templates render as flat rows as before.
This commit is contained in:
jtricerolph 2026-08-26 10:33:19 +00:00
parent c740b87f1b
commit ee23cb1b16
6 changed files with 285 additions and 64 deletions

View file

@ -431,6 +431,11 @@ table.data td { padding: 9px 12px; border-bottom: 1px solid var(--card-border);
table.data tr:last-child td { border-bottom: none; }
table.data tr.clickable { cursor: pointer; }
table.data tr.clickable:hover td { background: var(--body-bg); }
table.data tr.group-row { cursor: pointer; background: var(--body-bg); }
table.data tr.group-row td { font-weight: 600; display: flex; align-items: center; gap: 6px; }
table.data tr.group-row:hover td { background: var(--card-border); }
.group-row-chevron { transition: transform .15s; }
.group-row-chevron.open { transform: rotate(90deg); }
/* ── Stats strip ───────────────────────────────────────────── */
.stats-strip { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }