feat(phases-10-15): complete TapTrack Hub v1.0

Phase 10 — Support Ticket System:
- tickets.py router: SLA status (on_track/at_risk/breached/responded), email
  notifications on create+reply via background threads, school_name in list,
  priority filter, bulk-close endpoint
- tasks/tickets.py: escalate_stale Celery task (48h→high, 72h no reply→urgent)
- worker.py: escalate_stale scheduled every hour
- templates/email/ticket_notification.html: HTML ticket notification email
- TicketsPage.vue: status tabs, SLA badge, priority badge, school name column,
  checkbox bulk-close, pagination
- TicketDetailPage.vue: inline priority/status/assignee selectors, SLA timer,
  internal note lock icon, closed-ticket guard

Phase 11 — Monthly Report Generation:
- models/report.py: MonthlyReport + SchoolMonthlyStats ORM models
- tasks/reports.py: send_monthly_reports enhanced with SMS stats, attendance
  data, invoice summary, stores MonthlyReport record per school per month

Phase 12 — On-Prem Monthly Report Pull:
- tasks/reports.py: pull_monthly_stats task — httpx GET to each school's
  hub_base_url, upserts SchoolMonthlyStats; runs 1st at 5am
- worker.py: pull_monthly_stats scheduled 1st at 5am

Phase 13 — Feature Flags + Suspension:
- models/school.py: hub_base_url, feature_overrides (JSON), onboarding_completed_at
- routers/schools.py: PUT /{id}/feature-overrides endpoint
- routers/sync.py: _tier_features() merges school.feature_overrides into poll config

Phase 14 — Onboarding Wizard + Welcome Email:
- tasks/onboarding.py: send_welcome_email Celery task with license key
- routers/schools.py: auto-trigger welcome email on POST /schools,
  POST /{id}/activate (status→active + onboarding_completed_at),
  POST /{id}/resend-welcome

Phase 15 — UX Polish + Ops Tools:
- routers/search.py: GET /api/search?q= (schools + invoices + tickets, 5 each)
- routers/audit.py: GET /api/audit-logs (paginated, filterable)
- AppLayout.vue: global search bar with debounced dropdown, result navigation
- AuditLogsPage.vue: new page with filter + pagination
- AppSidebar.vue: Audit Logs nav item added
- router/index.ts: /audit-logs route
- api.ts: globalSearch, getAuditLogs, activateSchool, resendWelcomeEmail,
  updateFeatureOverrides, bulkCloseTickets

Deployment:
- docker-compose.yml: x-backend-env anchor (DRY), PDF_DIR env var,
  seed service (one-shot python seed.py on first boot)
- migrations/003_phases11_15.py: monthly_reports, school_monthly_stats tables
  + schools hub_base_url/feature_overrides/onboarding_completed_at columns
This commit is contained in:
kevin-asprec
2026-03-16 14:28:52 +08:00
parent 1febb3cfa9
commit 9ef8f1a421
26 changed files with 1375 additions and 213 deletions

View File

@@ -0,0 +1,43 @@
"""Global search across schools, invoices, tickets."""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.auth.dependencies import require_super_admin
from app.database import get_db
from app.models.user import HubUser
from app.models.school import School
from app.models.billing import Invoice
from app.models.ticket import SupportTicket
router = APIRouter(prefix="/api/search", tags=["search"])
@router.get("")
async def global_search(
q: str = Query(..., min_length=2),
_admin: HubUser = Depends(require_super_admin),
db: AsyncSession = Depends(get_db),
):
"""Search across schools, invoices, and tickets. Returns up to 5 per category."""
pattern = f"%{q}%"
schools = (await db.execute(
select(School).where(School.name.ilike(pattern)).limit(5)
)).scalars().all()
invoices = (await db.execute(
select(Invoice).where(Invoice.invoice_number.ilike(pattern)).limit(5)
)).scalars().all()
tickets = (await db.execute(
select(SupportTicket).where(
SupportTicket.subject.ilike(pattern) | SupportTicket.ticket_number.ilike(pattern)
).limit(5)
)).scalars().all()
return {
"schools": [{"id": s.id, "name": s.name, "status": s.status.value, "type": "school"} for s in schools],
"invoices": [{"id": i.id, "invoice_number": i.invoice_number, "total_amount": float(i.total_amount), "status": i.status.value, "type": "invoice"} for i in invoices],
"tickets": [{"id": t.id, "ticket_number": t.ticket_number, "subject": t.subject, "status": t.status.value, "type": "ticket"} for t in tickets],
}