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:
59
backend/migrations/versions/003_phases11_15.py
Normal file
59
backend/migrations/versions/003_phases11_15.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""phases 11-15: monthly_reports, school_monthly_stats, school new columns
|
||||
|
||||
Revision ID: 003_phases11_15
|
||||
Revises: 002_phase9
|
||||
Create Date: 2026-03-16
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "003_phases11_15"
|
||||
down_revision = "002_phase9"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Schools: new columns
|
||||
op.add_column("schools", sa.Column("hub_base_url", sa.String(500), nullable=True))
|
||||
op.add_column("schools", sa.Column("feature_overrides", sa.Text, nullable=True))
|
||||
op.add_column("schools", sa.Column("onboarding_completed_at", sa.DateTime(timezone=True), nullable=True))
|
||||
|
||||
# monthly_reports table
|
||||
op.create_table(
|
||||
"monthly_reports",
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column("school_id", sa.String(36), sa.ForeignKey("schools.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("report_month", sa.String(7), nullable=False),
|
||||
sa.Column("email_sent_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("report_data", sa.JSON, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("ix_monthly_reports_school_id", "monthly_reports", ["school_id"])
|
||||
|
||||
# school_monthly_stats table
|
||||
op.create_table(
|
||||
"school_monthly_stats",
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column("school_id", sa.String(36), sa.ForeignKey("schools.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("report_month", sa.String(7), nullable=False),
|
||||
sa.Column("total_students", sa.Integer, nullable=True),
|
||||
sa.Column("school_days", sa.Integer, nullable=True),
|
||||
sa.Column("present_days_total", sa.Integer, nullable=True),
|
||||
sa.Column("absent_days_total", sa.Integer, nullable=True),
|
||||
sa.Column("late_days_total", sa.Integer, nullable=True),
|
||||
sa.Column("avg_attendance_rate", sa.Numeric(5, 2), nullable=True),
|
||||
sa.Column("sms_sent", sa.Integer, nullable=True),
|
||||
sa.Column("pull_status", sa.String(20), nullable=False, server_default="unavailable"),
|
||||
sa.Column("pulled_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("hub_base_url", sa.String(500), nullable=True),
|
||||
)
|
||||
op.create_index("ix_school_monthly_stats_school_id", "school_monthly_stats", ["school_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("school_monthly_stats")
|
||||
op.drop_table("monthly_reports")
|
||||
op.drop_column("schools", "onboarding_completed_at")
|
||||
op.drop_column("schools", "feature_overrides")
|
||||
op.drop_column("schools", "hub_base_url")
|
||||
Reference in New Issue
Block a user