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
+25 -3
View File
@@ -135,23 +135,39 @@ SEED_CATEGORIES_DATA: list[dict] = [
{"type": "maintenance", "name": "Security", "subs": ["Lock broken", "Door not closing", "Window latch", "CCTV issue"]},
{"type": "maintenance", "name": "Internet", "subs": ["WiFi down", "Slow speed", "Router reset"]},
{"type": "maintenance", "name": "Structural", "subs": ["Wall crack", "Ceiling leak", "Floor tile", "Paint touch-up"]},
{"type": "maintenance", "name": "Aluminum/Glass",
"subs": ["Window repair", "Window replacement", "Door repair (sliding)", "Door repair (fixed)",
"Glass replacement", "Aluminum frame repair", "Shower screen", "Mirror replacement"]},
{"type": "maintenance", "name": "Carpentry",
"subs": ["Door repair", "Door replacement", "Door frame", "Cabinet repair", "Wardrobe repair",
"Shelving", "Timber repair", "Loose hinge/lock plate"]},
{"type": "maintenance", "name": "Mould & Damp",
"subs": ["Wall mould", "Ceiling damp patch", "Bathroom mould", "Mould after leak",
"Damp proofing / remediation"],
"sla_urgency": "medium"},
# ── Customer Service ─────────────────────────────────────────
{"type": "cs", "name": "Check-in", "subs": ["Early check-in", "Key handover", "Welcome instructions"]},
{"type": "cs", "name": "Check-out", "subs": ["Late checkout", "Key return", "Inspection"]},
{"type": "cs", "name": "Housekeeping", "subs": ["Mid-stay cleaning", "Linen change", "Restocking"]},
{"type": "cs", "name": "Lost Property", "subs": ["Guest left items behind"]},
{"type": "cs", "name": "Missing Item", "subs": ["Guest left items behind", "Item search request"]},
{"type": "cs", "name": "Billing", "subs": ["Invoice question", "Payment issue", "Deposit query"]},
{"type": "cs", "name": "Staff Behaviour", "subs": ["Staff conduct feedback"]},
# ── Emergency ────────────────────────────────────────────────
{"type": "emergency", "name": "Fire", "subs": ["Smoke detected", "Fire alarm", "Sprinkler issue"], "sla_urgency": "urgent"},
{"type": "emergency", "name": "Flood", "subs": ["Major water leak", "Burst pipe", "Overflowing"], "sla_urgency": "urgent"},
{"type": "emergency", "name": "Gas Leak", "subs": ["Gas smell", "Suspected leak"], "sla_urgency": "urgent"},
{"type": "emergency", "name": "Gas Leak", "subs": ["Gas smell", "Suspected leak"], "sla_urgency": "urgent", "show_in_form": False},
{"type": "emergency", "name": "Electrical Hazard", "subs": ["Sparking", "Exposed wires", "Power outage multiple units"], "sla_urgency": "urgent"},
]
async def seed_categories(db: AsyncSession) -> list[Category]:
"""Seed the categories table with the full PRD §7 taxonomy."""
"""Seed the categories table with the full PRD §7 taxonomy.
Idempotent: only inserts missing rows. ``show_in_form`` on existing
parents is synced when the seed data explicitly sets it (e.g. Gas Leak
is alert-only, so already-seeded databases get flipped to False on the
next startup).
"""
created: list[Category] = []
for group in SEED_CATEGORIES_DATA:
@@ -170,10 +186,16 @@ async def seed_categories(db: AsyncSession) -> list[Category]:
name=group["name"],
parent_id=None,
sla_urgency=group.get("sla_urgency"),
show_in_form=group.get("show_in_form", True),
)
db.add(parent)
await db.flush()
created.append(parent)
elif "show_in_form" in group and parent.show_in_form != group["show_in_form"]:
# Sync visibility for existing rows (e.g. Gas Leak → alert-only)
parent.show_in_form = group["show_in_form"]
await db.flush()
created.append(parent)
# Seed sub-categories
for sub_name in group["subs"]: