From b435847a394cd65a12e7259b4ad1cf8e98a4c30a Mon Sep 17 00:00:00 2001 From: kevin-asprec Date: Mon, 16 Mar 2026 08:09:59 +0800 Subject: [PATCH] feat(phase-2): school registry + license management UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 2-01: School create/edit modal + license editor - SchoolsPage: full create/edit modal (name, city, address, tier, contact, billing email, SMS sender name, student limit, status, notes); debounced search + status filter; Edit button per row opens pre-filled modal - SchoolDetailPage: Activate / Suspend / Edit quick-action buttons in header; license panel with key copy, expiry, last seen (time-ago); license editor (expiry date, max students, tier) calls PUT /api/licenses/{id}; Revoke button calls POST /api/licenses/{id}/revoke - Backend: GET /api/licenses/school/{school_id} — fetch license by school - api.ts: getSchoolLicense added Plan 2-02: Subscription setup + SMS credit top-up + ledger - SchoolDetailPage: subscription panel (monthly fee, SMS cost per msg, billing cycle, next billing date) upserts via billing API - SMS Credits panel: color-coded credit meter (green/amber/red based on balance), preset +100/+500/+1000 buttons, custom amount + note field, recent 5 ledger entries, link to full SMS page - All operations toast on success/error PAUL: Phase 2 marked complete, STATE + ROADMAP updated --- .paul/ROADMAP.md | 6 +- .paul/STATE.md | 14 +- .paul/phases/02-school-registry/README.md | 49 +- backend/app/routers/licenses.py | 20 + frontend/src/lib/api.ts | 1 + frontend/src/pages/SchoolDetailPage.vue | 534 ++++++++++++++++++++-- frontend/src/pages/SchoolsPage.vue | 239 +++++++++- 7 files changed, 784 insertions(+), 79 deletions(-) diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 12c2a3b..bd15c43 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -19,8 +19,8 @@ SMS flow: On-prem TapTrack polls Hub every 30s → Hub queues SMS jobs → Hub s ## Current Milestone **v1.0 — Foundation & Core Services** -Status: Phase 1 complete — Phase 2 next -Phases: 1 of 15 complete +Status: Phase 2 complete — Phase 3 next +Phases: 2 of 15 complete --- @@ -29,7 +29,7 @@ Phases: 1 of 15 complete | Phase | Name | Plans | Status | Completed | |-------|------------------------------|-------|--------------|------------| | 1 | Project Setup & Infrastructure | 1 | ✅ Complete | 2026-03-15 | -| 2 | School Registry + License Mgmt | TBD | Not started | — | +| 2 | School Registry + License Mgmt | 2 | ✅ Complete | 2026-03-15 | | 3 | On-Prem License Validation | TBD | Not started | — | | 4 | SMS Gateway (credits + queue) | TBD | Not started | — | | 5 | On-Prem SMS Polling Agent | TBD | Not started | — | diff --git a/.paul/STATE.md b/.paul/STATE.md index 4e1090e..6ab2f86 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -3,10 +3,10 @@ ## Current Position Milestone: v1.0 — Foundation & Core Services -Phase: 1 of 15 (Project Setup & Infrastructure — complete) -Plan: Phase 1 complete — no active plan -Status: **Phase 1 applied — ready to begin Phase 2** -Last activity: 2026-03-15 — Phase 1 complete (full stack scaffold: Docker Compose, all backend models/routers/tasks, Vue 3 frontend skeleton with all page stubs) +Phase: 2 of 15 (School Registry + License Management — complete) +Plan: Phase 2 complete — Phase 3 next +Status: **Phase 2 applied — ready to begin Phase 3** +Last activity: 2026-03-15 — Phase 2 complete (school create/edit modal, license editor, activate/suspend, subscription plan, SMS credit top-up, ledger) ## Loop Position @@ -20,7 +20,7 @@ PLAN ──▶ APPLY ──▶ UNIFY ### v1.0 Foundation - Phase 1 (Project Setup & Infrastructure): [██████████] 100% ✓ -- Phase 2 (School Registry + License Mgmt): [░░░░░░░░░░] 0% +- Phase 2 (School Registry + License Mgmt): [██████████] 100% ✓ - Phase 3 (On-Prem License Validation): [░░░░░░░░░░] 0% - Phase 4 (SMS Gateway): [░░░░░░░░░░] 0% - Phase 5 (On-Prem SMS Polling Agent): [░░░░░░░░░░] 0% @@ -37,8 +37,8 @@ PLAN ──▶ APPLY ──▶ UNIFY ## Next Action -Run: `/paul:plan` for Phase 2 — School Registry + License Management -Resume file: .paul/ROADMAP.md → Phase 2 +Run: `/paul:plan` for Phase 3 — On-Prem License Validation +Resume file: .paul/ROADMAP.md → Phase 3 ## Repo diff --git a/.paul/phases/02-school-registry/README.md b/.paul/phases/02-school-registry/README.md index 4fc85f5..c367847 100644 --- a/.paul/phases/02-school-registry/README.md +++ b/.paul/phases/02-school-registry/README.md @@ -1,20 +1,49 @@ # Phase 2: School Registry + License Management -**Status:** Not started -**Depends on:** Phase 1 +**Status:** ✅ Complete +**Completed:** 2026-03-15 ## Goal Super admin can register schools, issue license keys, set billing plans, add SMS credits, and manage the school lifecycle (pending → active → suspended/expired). -## Plans -- [ ] 2-01: School create/edit modal + license expiry editor in SchoolDetailPage -- [ ] 2-02: Subscription setup + SMS credit top-up form + ledger table +## Plans Completed -## Key Endpoints (already built, need UI) +### 2-01: School create/edit modal + license editor +**SchoolsPage.vue:** +- "Add School" button opens full create modal (name, city, address, tier, contact info, + billing email, SMS sender name, student limit, notes) +- "Edit" button in each row opens pre-filled edit modal (same fields + status selector) +- Modal validates required fields, shows inline errors, toasts on success +- Auto-issues license key on school creation (handled by backend) + +**SchoolDetailPage.vue:** +- Header: Activate / Suspend / Edit quick-action buttons +- School info panel: all fields displayed +- License panel: key display + copy button, status badge, expiry, last seen (time ago) +- License editor: set expiry date, max students, tier → calls PUT /api/licenses/{id} +- Revoke button: calls POST /api/licenses/{id}/revoke → suspends school + +**Backend:** +- GET /api/licenses/school/{school_id} — new endpoint to fetch a school's license by school ID + +### 2-02: Subscription setup + SMS credit top-up + ledger +**SchoolDetailPage.vue:** +- Subscription panel: monthly fee (PHP), SMS cost per message, billing cycle, next billing date + → calls PUT /api/billing/subscriptions/{school_id} (upsert) +- SMS Credits panel: current balance with color-coded credit meter (green/amber/red) + → preset buttons (+100, +500, +1000) + custom amount + note field + → calls POST /api/schools/{id}/credits +- Recent ledger: shows last 5 transactions (description + signed amount) + → link to full SMS ledger page + +## Key Endpoints Used - POST /api/schools — create school (auto-issues license) -- PUT /api/schools/{id} — update school details -- PUT /api/licenses/{id} — set expiry, tier, max_students -- POST /api/licenses/{id}/revoke — revoke license +- PUT /api/schools/{id} — update school details + status +- GET /api/licenses/school/{school_id} — get license by school +- PUT /api/licenses/{id} — update expiry, tier, max_students +- POST /api/licenses/{id}/revoke — revoke and suspend - POST /api/schools/{id}/credits — add SMS credits -- PUT /api/billing/subscriptions/{school_id} — set monthly fee +- GET /api/sms/credits/{school_id} — credit ledger +- PUT /api/billing/subscriptions/{school_id} — set billing plan +- GET /api/billing/subscriptions/{school_id} — get current plan diff --git a/backend/app/routers/licenses.py b/backend/app/routers/licenses.py index 6da5086..18484c4 100644 --- a/backend/app/routers/licenses.py +++ b/backend/app/routers/licenses.py @@ -107,6 +107,26 @@ async def validate_license( "features": _tier_features(lic.tier), } +@router.get("/school/{school_id}") +async def get_school_license( + school_id: str, + _admin: HubUser = Depends(require_super_admin), + db: AsyncSession = Depends(get_db), +): + """Get the license for a specific school.""" + lic = (await db.execute(select(License).where(License.school_id == school_id))).scalar_one_or_none() + if not lic: + raise HTTPException(404, "No license found for this school") + return { + "id": lic.id, "school_id": lic.school_id, "key": lic.key, + "status": lic.status.value, "tier": lic.tier, + "issued_at": lic.issued_at.isoformat(), + "expires_at": lic.expires_at.isoformat() if lic.expires_at else None, + "last_validated_at": lic.last_validated_at.isoformat() if lic.last_validated_at else None, + "last_seen_ip": lic.last_seen_ip, "max_students": lic.max_students, + } + + def _tier_features(tier: str) -> dict: base = {"sms": True, "reports": True, "websocket": True, "multi_terminal": True} if tier == "premium": diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7156c0b..1b95b9a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -52,6 +52,7 @@ export const addSmsCredits = (id: string, amount: number, description?: string) // ── Licenses ────────────────────────────────────────────────────────────────── export const getLicenses = () => api.get('/licenses').then(r => r.data) +export const getSchoolLicense = (schoolId: string) => api.get(`/licenses/school/${schoolId}`).then(r => r.data) export const updateLicense = (id: string, data: object) => api.put(`/licenses/${id}`, data).then(r => r.data) export const revokeLicense = (id: string) => api.post(`/licenses/${id}/revoke`).then(r => r.data) diff --git a/frontend/src/pages/SchoolDetailPage.vue b/frontend/src/pages/SchoolDetailPage.vue index 67e0876..9f84c26 100644 --- a/frontend/src/pages/SchoolDetailPage.vue +++ b/frontend/src/pages/SchoolDetailPage.vue @@ -1,76 +1,524 @@ + + diff --git a/frontend/src/pages/SchoolsPage.vue b/frontend/src/pages/SchoolsPage.vue index c55b425..08bfebc 100644 --- a/frontend/src/pages/SchoolsPage.vue +++ b/frontend/src/pages/SchoolsPage.vue @@ -5,7 +5,7 @@

Schools

{{ total }} registered schools

- @@ -69,9 +69,10 @@ {{ s.license_last_seen ? new Date(s.license_last_seen).toLocaleDateString() : '—' }} - - View - +
+ View + +
@@ -87,34 +88,240 @@ + + + + +
+
+ + +
+

+ {{ editingId ? 'Edit School' : 'Add New School' }} +

+ +
+ + +
+ +
+

Basic Information

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Contact

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

TapTrack Settings

+
+
+ + +

{{ (form.sms_sender_name || '').length }}/11

+
+
+ + +
+
+
+ +
+

Status

+ +
+ +
+ + +
+ +

{{ modalError }}

+
+ + +
+ + +
+
+
+
+
+ +