feat(phase-1): TapTrack Hub initial scaffold
Full project scaffold for TapTrack Hub — cloud SaaS control plane for managing on-prem TapTrack school deployments. ## Infrastructure - Docker Compose: backend (gunicorn+uvicorn), Celery worker + beat, frontend (Vite build + nginx), PostgreSQL 15, Redis 7, nginx proxy - Dockerfile for backend and frontend, nginx reverse proxy config ## Backend (FastAPI + SQLAlchemy async + Celery) Database schema (10 tables): hub_users, schools, licenses, sms_jobs, sms_credit_ledger, invoices, invoice_line_items, school_subscriptions, support_tickets, ticket_replies, audit_logs, announcements Auth: JWT (python-jose) + bcrypt + role-based FastAPI dependencies (get_current_user, require_super_admin, require_school_admin) Routers (11): auth, schools, licenses, sms, billing, tickets, users, dashboard, school_portal, announcements, sync Celery tasks (6): sms.process_queue, billing.generate_monthly_invoices, billing.send_invoice_email, billing.check_overdue, license.check_expiry, reports.send_monthly_reports Services: SMTP email helper (smtplib + Jinja2) Seed script: creates super admin admin@taptrack.io ## Frontend (Vue 3 + Vite + Pinia + Tailwind CSS) Router: 14 routes across super admin + school portal layouts Stores: Pinia auth store with localStorage persistence API client: full axios client for all backend endpoints Layouts: AppLayout (super admin), PortalLayout (school), AuthLayout Components: AppSidebar, PortalSidebar, SidebarItem, KpiCard, StatusBadge, ToastStack Pages: Login, Dashboard, Schools, SchoolDetail, Licenses, SMS, Billing, Tickets, TicketDetail, Users, Announcements, 404 Portal pages: Overview, Billing, SMS Reports, Tickets, Profile ## PAUL Planning Files - .paul/ROADMAP.md: full 15-phase roadmap with detailed scope - .paul/STATE.md: current position, tech stack, architecture notes - .paul/phases/01-setup/01-PLAN.md: complete Phase 1 plan (done) - .paul/phases/02 through 15: README stubs for all future phases
This commit is contained in:
56
frontend/src/pages/SmsPage.vue
Normal file
56
frontend/src/pages/SmsPage.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900">SMS Gateway</h1>
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<select v-model="statusFilter" class="border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl overflow-hidden" style="box-shadow:0 2px 8px #0000000A">
|
||||
<div v-if="loading" class="p-8 text-center text-sm text-slate-400 animate-pulse">Loading…</div>
|
||||
<div v-else-if="jobs.length === 0" class="p-12 text-center text-slate-400">No SMS jobs found</div>
|
||||
<table v-else class="w-full text-sm">
|
||||
<thead class="bg-slate-50 border-b border-slate-100">
|
||||
<tr class="text-left text-xs text-slate-500 font-semibold uppercase tracking-wide">
|
||||
<th class="px-5 py-3">Recipient</th>
|
||||
<th class="px-5 py-3">Message</th>
|
||||
<th class="px-5 py-3">Sender</th>
|
||||
<th class="px-5 py-3">Status</th>
|
||||
<th class="px-5 py-3">Trigger</th>
|
||||
<th class="px-5 py-3">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
<tr v-for="j in jobs" :key="j.id" class="hover:bg-slate-50">
|
||||
<td class="px-5 py-3 font-mono text-xs">{{ j.recipient_phone }}</td>
|
||||
<td class="px-5 py-3 text-slate-600 max-w-xs truncate">{{ j.message }}</td>
|
||||
<td class="px-5 py-3 font-mono text-xs">{{ j.sender_name }}</td>
|
||||
<td class="px-5 py-3"><StatusBadge :status="j.status" /></td>
|
||||
<td class="px-5 py-3 text-slate-500 text-xs">{{ j.trigger || '—' }}</td>
|
||||
<td class="px-5 py-3 text-slate-500 text-xs">{{ new Date(j.created_at).toLocaleString() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { getSmsJobs } from '@/lib/api'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
const jobs = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const statusFilter = ref('')
|
||||
async function fetchJobs() {
|
||||
loading.value = true
|
||||
try { const r = await getSmsJobs({ status: statusFilter.value || undefined }); jobs.value = r.items }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
watch(statusFilter, fetchJobs)
|
||||
onMounted(fetchJobs)
|
||||
</script>
|
||||
Reference in New Issue
Block a user