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
39 lines
1.9 KiB
Vue
39 lines
1.9 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<h1 class="text-2xl font-bold text-slate-900">Billing History</h1>
|
|
<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="invoices.length === 0" class="p-12 text-center text-slate-400">No invoices yet</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">Invoice #</th>
|
|
<th class="px-5 py-3">Period</th>
|
|
<th class="px-5 py-3">Amount</th>
|
|
<th class="px-5 py-3">Status</th>
|
|
<th class="px-5 py-3">Due Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50">
|
|
<tr v-for="inv in invoices" :key="inv.id" class="hover:bg-slate-50">
|
|
<td class="px-5 py-3 font-mono text-xs font-semibold">{{ inv.invoice_number }}</td>
|
|
<td class="px-5 py-3 text-xs text-slate-500">{{ inv.billing_period_start }} — {{ inv.billing_period_end }}</td>
|
|
<td class="px-5 py-3 font-semibold">PHP {{ Number(inv.total_amount).toLocaleString() }}</td>
|
|
<td class="px-5 py-3"><StatusBadge :status="inv.status" /></td>
|
|
<td class="px-5 py-3 text-xs text-slate-500">{{ inv.due_date || '—' }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { getInvoices } from '@/lib/api'
|
|
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
|
const invoices = ref<any[]>([])
|
|
const loading = ref(true)
|
|
onMounted(async () => { try { const r = await getInvoices(); invoices.value = r.items } finally { loading.value = false } })
|
|
</script>
|