feat: Sprint A Wahab review batch — categories, property hierarchy, priority grouping

Items 1-5, 9, 11 from denya-wahab-feedback-s2:
- Category.show_in_form (alert-only flag): Gas Leak hidden from issue picker
  but kept urgent for SLA/alert and reporting; emergency quick path on the
  new-issue form creates urgent tickets via include_hidden categories.
- Seed: Aluminum/Glass, Carpentry, Mould & Damp (Medium default) maintenance
  categories; Lost Property renamed Missing Item (+ sub).
- One alembic migration: add show_in_form (backfill True, Gas Leak False) +
  data rename Lost Property -> Missing Item.
- Property -> Building -> Apartment cascade with searchable apartment combobox
  on the new-issue form and ticket list filters; /api/tickets/units gains
  building filter + /units/grouped variant; /api/tickets gains additive
  building/unit_id filters. apartment_mapping.json committed (deterministic).
- Group-by-priority toggle on /tickets (four sections + unknown bucket,
  age-sortable, composes with filters, URL deep links), FM dashboard active
  tickets, and CS dashboard priority card click-through.
- Tests: 13 new (category visibility, seed idempotency/sync, unit grouping,
  ticket building/unit filters).
This commit is contained in:
root
2026-08-02 09:30:29 +00:00
parent 2e995ee758
commit 2407f294f4
16 changed files with 1579 additions and 97 deletions
+26
View File
@@ -355,6 +355,32 @@
if (diffHrs < 1) return '< 1h';
if (diffHrs < 24) return `${diffHrs}h`;
return `${diffDays}d`;
},
groupByPriority(tickets, ageDir = 'desc') {
// Group tickets into Urgent/High/Medium/Low + an other bucket,
// each sorted by age (desc = newest first, asc = oldest first).
const order = ['urgent', 'high', 'medium', 'low'];
const groups = { urgent: [], high: [], medium: [], low: [], other: [] };
const age = (t) => new Date(t.created_at).getTime() || 0;
(tickets || []).forEach(t => {
const p = String(t.priority || '').toLowerCase();
groups[order.includes(p) ? p : 'other'].push(t);
});
Object.keys(groups).forEach(k => {
groups[k].sort((a, b) => (ageDir === 'asc' ? age(a) - age(b) : age(b) - age(a)));
});
return groups;
},
priorityGroupMeta() {
return [
{ key: 'urgent', label: '🔴 Urgent', cls: 'border-red-200 bg-red-50' },
{ key: 'high', label: '🟠 High', cls: 'border-orange-200 bg-orange-50' },
{ key: 'medium', label: '🟡 Medium', cls: 'border-yellow-200 bg-yellow-50' },
{ key: 'low', label: '🟢 Low', cls: 'border-green-200 bg-green-50' },
{ key: 'other', label: '⚪ No priority / Unknown', cls: 'border-gray-200 bg-gray-50' },
];
}
}
}