Files
TapTrack-Hub/frontend/src/router/index.ts
kevin-asprec 1febb3cfa9 feat(phase-9): email dispatcher — HTML templates, delivery log, test endpoint
Backend:
- app/models/email_log.py: EmailLog table (school_id, to, subject, type, status,
  error, sent_at) with EmailType + EmailStatus enums
- migrations/002_phase9_email_logs.py: Alembic migration for email_logs table
- app/templates/email/: 6 Jinja2 HTML templates — base layout, invoice,
  low_credit, license_expiry, overdue_warning, suspension
- app/services/email.py: enhanced send_email() — accepts template_name+context
  for HTML rendering, logs every attempt to email_logs, retries up to 3x on
  transient SMTP failure with exponential backoff
- app/routers/email.py: GET /api/email/logs (paginated, filterable by type/status/school),
  POST /api/email/test (send test email, super admin)
- tasks/billing.py: invoice + overdue warning + suspension emails now use HTML templates
- tasks/sms.py: low credit alert now uses HTML template
- tasks/license.py: expiry warning now uses HTML template
- app/main.py + migrations/env.py: wire in email_log model + email router

Frontend:
- EmailLogsPage.vue: table with to/subject/type badge/status badge/sent_at/error,
  type+status filters, pagination, Send Test Email modal
- router/index.ts: /email-logs route
- AppSidebar.vue: Email Logs nav item
- api.ts: getEmailLogs, sendTestEmail
2026-03-16 14:03:02 +08:00

132 lines
3.9 KiB
TypeScript

import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', redirect: '/dashboard' },
{
path: '/login',
name: 'login',
component: () => import('@/pages/LoginPage.vue'),
meta: { requiresAuth: false },
},
// Super Admin routes
{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/pages/DashboardPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/schools',
name: 'schools',
component: () => import('@/pages/SchoolsPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/schools/:id',
name: 'school-detail',
component: () => import('@/pages/SchoolDetailPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/licenses',
name: 'licenses',
component: () => import('@/pages/LicensesPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/sms',
name: 'sms',
component: () => import('@/pages/SmsPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/billing',
name: 'billing',
component: () => import('@/pages/BillingPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/tickets',
name: 'tickets',
component: () => import('@/pages/TicketsPage.vue'),
meta: { requiresAuth: true, layout: 'app' },
},
{
path: '/tickets/:id',
name: 'ticket-detail',
component: () => import('@/pages/TicketDetailPage.vue'),
meta: { requiresAuth: true, layout: 'app' },
},
{
path: '/users',
name: 'users',
component: () => import('@/pages/UsersPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/announcements',
name: 'announcements',
component: () => import('@/pages/AnnouncementsPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
{
path: '/email-logs',
name: 'email-logs',
component: () => import('@/pages/EmailLogsPage.vue'),
meta: { requiresAuth: true, layout: 'app', role: 'super_admin' },
},
// School Portal routes
{
path: '/portal',
name: 'portal',
component: () => import('@/pages/portal/PortalOverviewPage.vue'),
meta: { requiresAuth: true, layout: 'portal' },
},
{
path: '/portal/billing',
name: 'portal-billing',
component: () => import('@/pages/portal/PortalBillingPage.vue'),
meta: { requiresAuth: true, layout: 'portal' },
},
{
path: '/portal/sms',
name: 'portal-sms',
component: () => import('@/pages/portal/PortalSmsPage.vue'),
meta: { requiresAuth: true, layout: 'portal' },
},
{
path: '/portal/tickets',
name: 'portal-tickets',
component: () => import('@/pages/portal/PortalTicketsPage.vue'),
meta: { requiresAuth: true, layout: 'portal' },
},
{
path: '/portal/profile',
name: 'portal-profile',
component: () => import('@/pages/portal/PortalProfilePage.vue'),
meta: { requiresAuth: true, layout: 'portal' },
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/pages/NotFoundPage.vue'),
},
],
})
router.beforeEach((to, _from, next) => {
const token = localStorage.getItem('hub_token')
const role = localStorage.getItem('hub_role')
const requiresAuth = to.meta.requiresAuth !== false
if (requiresAuth && !token) return next('/login')
if (to.name === 'login' && token) {
return next(role === 'super_admin' ? '/dashboard' : '/portal')
}
next()
})
export default router