From a6e13e611cffddc93ae5ffb47eb38cc96038a1ed Mon Sep 17 00:00:00 2001 From: "Nemo (Claude Code)" Date: Wed, 1 Apr 2026 03:30:18 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20remove=20portal=20from=20admin=20app?= =?UTF-8?q?=20=E2=80=94=20extracted=20to=20fiberops-portal=20(FIBEROPS-249?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete app/(portal)/ route group (login, dashboard, invoices, tickets) - Delete lib/portal-api.ts and lib/portal-auth-store.ts - Remove Phase 9 portal tests (17–22) from business-flow E2E spec - Keep portalAccessEnabled in Client type and admin client profile view --- app/(portal)/layout.tsx | 19 --- app/(portal)/portal/dashboard/page.tsx | 206 ------------------------- app/(portal)/portal/invoices/page.tsx | 114 -------------- app/(portal)/portal/login/page.tsx | 118 -------------- app/(portal)/portal/tickets/page.tsx | 206 ------------------------- e2e/business-flow.spec.ts | 112 +------------- lib/portal-api.ts | 33 ---- lib/portal-auth-store.ts | 49 ------ 8 files changed, 3 insertions(+), 854 deletions(-) delete mode 100644 app/(portal)/layout.tsx delete mode 100644 app/(portal)/portal/dashboard/page.tsx delete mode 100644 app/(portal)/portal/invoices/page.tsx delete mode 100644 app/(portal)/portal/login/page.tsx delete mode 100644 app/(portal)/portal/tickets/page.tsx delete mode 100644 lib/portal-api.ts delete mode 100644 lib/portal-auth-store.ts diff --git a/app/(portal)/layout.tsx b/app/(portal)/layout.tsx deleted file mode 100644 index cf89664..0000000 --- a/app/(portal)/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client'; - -export default function PortalLayout({ children }: { children: React.ReactNode }) { - return ( -
-
-
- - FiberOps - - - Subscriber Portal - -
-
-
{children}
-
- ); -} diff --git a/app/(portal)/portal/dashboard/page.tsx b/app/(portal)/portal/dashboard/page.tsx deleted file mode 100644 index 25e733b..0000000 --- a/app/(portal)/portal/dashboard/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { usePortalAuthStore } from '@/lib/portal-auth-store'; -import portalApi from '@/lib/portal-api'; -import { formatCurrency } from '@/lib/utils'; - -interface AccountData { - accountNumber: string; - firstName: string; - lastName: string; - email?: string; - phone?: string; - subscription?: { - planName: string; - downloadMbps: number; - uploadMbps: number; - monthlyRate: number; - status: string; - }; - balanceDue: number; -} - -const statusColors: Record = { - ACTIVE: { bg: '#DCFCE7', text: '#16A34A' }, - SUSPENDED: { bg: '#FEF9C3', text: '#CA8A04' }, - CANCELLED: { bg: '#FEE2E2', text: '#DC2626' }, - PENDING: { bg: '#F1F5F9', text: '#64748B' }, -}; - -export default function PortalDashboardPage() { - const router = useRouter(); - const { isAuthenticated, subscriber, logout } = usePortalAuthStore(); - const [mounted, setMounted] = useState(false); - const [account, setAccount] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - useEffect(() => { setMounted(true); }, []); - - useEffect(() => { - if (!mounted) return; - if (!isAuthenticated) { router.replace('/portal/login'); return; } - portalApi.get('/api/v1/portal/account') - .then((res) => setAccount(res.data)) - .catch(() => setError('Failed to load account info.')) - .finally(() => setLoading(false)); - }, [mounted, isAuthenticated, router]); - - if (!mounted || !isAuthenticated) return null; - - const handleLogout = () => { logout(); router.replace('/portal/login'); }; - - return ( -
- {/* Header row */} -
-
-

- Welcome, {subscriber?.firstName ?? 'Subscriber'} -

-

Account #{subscriber?.accountNumber}

-
- -
- - {error && ( -
- {error} -
- )} - - {loading ? ( -
- {[1, 2, 3].map((i) => ( -
- ))} -
- ) : account && ( -
- {/* Account Info */} -
-

Account Information

-
- {[ - { label: 'Full Name', value: `${account.firstName} ${account.lastName}` }, - { label: 'Account Number', value: account.accountNumber }, - { label: 'Email', value: account.email || '—' }, - { label: 'Phone', value: account.phone || '—' }, - ].map(({ label, value }) => ( -
-

{label}

-

{value}

-
- ))} -
-
- - {/* Subscription */} -
-

Active Subscription

- {account.subscription ? ( -
-
-

Plan

-

{account.subscription.planName}

-
-
-

Speed

-

- {account.subscription.downloadMbps}↓ / {account.subscription.uploadMbps}↑ Mbps -

-
-
-

Monthly Rate

-

{formatCurrency(account.subscription.monthlyRate)}

-
-
-

Status

- - {account.subscription.status} - -
-
- ) : ( -

No active subscription.

- )} -
- - {/* Balance Due */} -
0 ? '1px solid #FECACA' : '1px solid #E2E8F0' }}> -
-

Balance Due

- 0 ? '#DC2626' : '#16A34A', - }}> - {formatCurrency(account.balanceDue)} - -
- {account.balanceDue > 0 && ( -

- You have an outstanding balance. Please settle your invoices to avoid service interruption. -

- )} -
- - {/* Quick Links */} -
- -
🧾
-

View Invoices

-

See your billing history

- - -
🎫
-

Support Tickets

-

View or raise a ticket

- -
-
- )} -
- ); -} - -const cardStyle: React.CSSProperties = { - backgroundColor: '#ffffff', - borderRadius: 12, - border: '1px solid #E2E8F0', - padding: 24, - boxShadow: '0 1px 3px rgba(0,0,0,0.04)', -}; - -const cardTitleStyle: React.CSSProperties = { - fontSize: 15, - fontWeight: 600, - color: '#0F172A', - margin: 0, -}; - -const labelStyle: React.CSSProperties = { - fontSize: 11, - fontWeight: 600, - color: '#94A3B8', - textTransform: 'uppercase', - letterSpacing: '0.05em', - marginBottom: 2, -}; diff --git a/app/(portal)/portal/invoices/page.tsx b/app/(portal)/portal/invoices/page.tsx deleted file mode 100644 index ec076ee..0000000 --- a/app/(portal)/portal/invoices/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { usePortalAuthStore } from '@/lib/portal-auth-store'; -import portalApi from '@/lib/portal-api'; -import { formatCurrency, formatDate } from '@/lib/utils'; - -interface PortalInvoice { - id: string; - invoiceNumber?: string; - total: number; - balance: number; - dueDate?: string; - status: string; -} - -const statusBadge: Record = { - PAID: { bg: '#DCFCE7', text: '#16A34A' }, - PARTIAL: { bg: '#FEF9C3', text: '#CA8A04' }, - OVERDUE: { bg: '#FEE2E2', text: '#DC2626' }, - SENT: { bg: '#F1F5F9', text: '#64748B' }, - DRAFT: { bg: '#F1F5F9', text: '#64748B' }, - VOID: { bg: '#F1F5F9', text: '#94A3B8' }, -}; - -export default function PortalInvoicesPage() { - const router = useRouter(); - const { isAuthenticated } = usePortalAuthStore(); - const [mounted, setMounted] = useState(false); - const [invoices, setInvoices] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - useEffect(() => { setMounted(true); }, []); - - useEffect(() => { - if (!mounted) return; - if (!isAuthenticated) { router.replace('/portal/login'); return; } - portalApi.get('/api/v1/portal/invoices') - .then((res) => { - const data = res.data; - setInvoices(Array.isArray(data) ? data : data.data ?? []); - }) - .catch(() => setError('Failed to load invoices.')) - .finally(() => setLoading(false)); - }, [mounted, isAuthenticated, router]); - - if (!mounted || !isAuthenticated) return null; - - return ( -
-
- - ← Back - -

Invoice History

-
- - {error && ( -
- {error} -
- )} - -
- {loading ? ( -
Loading…
- ) : invoices.length === 0 ? ( -
No invoices found.
- ) : ( - - - - {['Invoice #', 'Amount', 'Balance', 'Due Date', 'Status'].map((h) => ( - - ))} - - - - {invoices.map((inv, i) => { - const badge = statusBadge[inv.status] ?? statusBadge.SENT; - return ( - - - - - - - - ); - })} - -
- {h} -
- {inv.invoiceNumber ?? inv.id.slice(0, 8)} - - {formatCurrency(Number(inv.total ?? 0))} - 0 ? 600 : 400, color: Number(inv.balance) > 0 ? '#DC2626' : '#64748B' }}> - {formatCurrency(Number(inv.balance ?? 0))} - - {inv.dueDate ? formatDate(inv.dueDate) : '—'} - - - {inv.status} - -
- )} -
-
- ); -} diff --git a/app/(portal)/portal/login/page.tsx b/app/(portal)/portal/login/page.tsx deleted file mode 100644 index b373e5d..0000000 --- a/app/(portal)/portal/login/page.tsx +++ /dev/null @@ -1,118 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { usePortalAuthStore } from '@/lib/portal-auth-store'; - -export default function PortalLoginPage() { - const router = useRouter(); - const login = usePortalAuthStore((s) => s.login); - const [form, setForm] = useState({ tenantSlug: '', accountNumber: '', password: '' }); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setLoading(true); - try { - await login(form.tenantSlug, form.accountNumber, form.password); - router.replace('/portal/dashboard'); - } catch (err: unknown) { - const msg = (err as any)?.response?.data?.message ?? 'Login failed. Check your credentials.'; - setError(msg); - } finally { - setLoading(false); - } - }; - - return ( -
-
-
-

Sign in to your account

-

Enter your ISP code and account details to continue.

- - {error && ( -
- {error} -
- )} - -
-
- - setForm({ ...form, tenantSlug: e.target.value })} - placeholder="e.g. demo-isp" - style={inputStyle} - /> -
-
- - setForm({ ...form, accountNumber: e.target.value })} - placeholder="e.g. ACC-2025-0001" - style={inputStyle} - /> -
-
- - setForm({ ...form, password: e.target.value })} - placeholder="••••••••" - style={inputStyle} - /> -
- - -
-
-
-
- ); -} - -const inputStyle: React.CSSProperties = { - width: '100%', - padding: '9px 12px', - border: '1px solid #D1D5DB', - borderRadius: 8, - fontSize: 14, - color: '#0F172A', - backgroundColor: '#ffffff', - outline: 'none', - boxSizing: 'border-box', -}; diff --git a/app/(portal)/portal/tickets/page.tsx b/app/(portal)/portal/tickets/page.tsx deleted file mode 100644 index 10d264a..0000000 --- a/app/(portal)/portal/tickets/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { usePortalAuthStore } from '@/lib/portal-auth-store'; -import portalApi from '@/lib/portal-api'; -import { formatDate } from '@/lib/utils'; - -interface PortalTicket { - id: string; - subject: string; - status: string; - type?: string; - createdAt: string; -} - -const statusBadge: Record = { - OPEN: { bg: '#DBEAFE', text: '#1D4ED8' }, - IN_PROGRESS: { bg: '#FEF9C3', text: '#CA8A04' }, - RESOLVED: { bg: '#DCFCE7', text: '#16A34A' }, - CLOSED: { bg: '#F1F5F9', text: '#64748B' }, -}; - -export default function PortalTicketsPage() { - const router = useRouter(); - const { isAuthenticated } = usePortalAuthStore(); - const [mounted, setMounted] = useState(false); - const [tickets, setTickets] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [showModal, setShowModal] = useState(false); - const [form, setForm] = useState({ subject: '', description: '' }); - const [submitting, setSubmitting] = useState(false); - const [submitError, setSubmitError] = useState(''); - - useEffect(() => { setMounted(true); }, []); - - useEffect(() => { - if (!mounted) return; - if (!isAuthenticated) { router.replace('/portal/login'); return; } - loadTickets(); - }, [mounted, isAuthenticated, router]); - - const loadTickets = () => { - setLoading(true); - portalApi.get('/api/v1/portal/tickets') - .then((res) => { - const data = res.data; - setTickets(Array.isArray(data) ? data : data.data ?? []); - }) - .catch(() => setError('Failed to load tickets.')) - .finally(() => setLoading(false)); - }; - - const handleSubmitTicket = async (e: React.FormEvent) => { - e.preventDefault(); - setSubmitError(''); - setSubmitting(true); - try { - await portalApi.post('/api/v1/portal/tickets', form); - setShowModal(false); - setForm({ subject: '', description: '' }); - loadTickets(); - } catch (err: unknown) { - setSubmitError((err as any)?.response?.data?.message ?? 'Failed to submit ticket.'); - } finally { - setSubmitting(false); - } - }; - - if (!mounted || !isAuthenticated) return null; - - return ( -
-
-
- - ← Back - -

Support Tickets

-
- -
- - {error && ( -
- {error} -
- )} - -
- {loading ? ( -
Loading…
- ) : tickets.length === 0 ? ( -
- No tickets yet. Click "New Ticket" to raise a support request. -
- ) : ( - - - - {['Subject', 'Type', 'Status', 'Date'].map((h) => ( - - ))} - - - - {tickets.map((t, i) => { - const badge = statusBadge[t.status] ?? statusBadge.CLOSED; - return ( - - - - - - - ); - })} - -
- {h} -
{t.subject}{t.type ?? '—'} - - {t.status.replace('_', ' ')} - - {formatDate(t.createdAt)}
- )} -
- - {/* New Ticket Modal */} - {showModal && ( -
-
-

New Support Ticket

-

Describe your issue and our team will get back to you.

- - {submitError && ( -
- {submitError} -
- )} - -
-
- - setForm({ ...form, subject: e.target.value })} - placeholder="e.g. Internet not working" - style={inputStyle} - /> -
-
- -