new-site/site/public/portal/dashboard/index.html
justin bd9a70607f fix: maintain Services dropdown header from one canonical source
The site header / Services mega-dropdown was duplicated across two render
systems (Astro pages via Base.astro->nav.html, and ~80 pre-rendered static
public/**/index.html pages each embedding their own copy). They had drifted
into 5 different variants (missing 'New Carrier Setup', misplaced Healthcare
column, NEW vs FREE badges, em-dash encoding differences), so
dev.performancewest.net, the order pages, and the rest of the site disagreed.

- Make site/src/partials/nav.html the single source of truth (adopts the most
  complete variant).
- Add scripts/sync_nav.py to rewrite every static page's <nav> block from
  nav.html (idempotent; --check guards against drift in CI/deploy).
- Run the sync automatically in deploy.sh and scripts/deploy-dev.sh.
- Deprecate scripts/inject_healthcare_nav.py (now delegates to sync_nav.py).
- Neutralize the broken no-op SiteNav.astro component.

All 80 headers + the Astro-built order pages now render the identical dropdown.
2026-06-05 14:27:24 -05:00

400 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Customer dashboard — view your orders, invoices, and account details with Performance West Inc.">
<title>My Account | Performance West Inc.</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="canonical" href="https://performancewest.net/portal/dashboard">
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script defer src="https://analytics.performancewest.net/script.js" data-website-id="55250014-ee15-44ac-a1f6-81dabad3fe0f"></script>
<link rel="stylesheet" href="/_astro/about.DhmoKVOS.css">
<script type="module" src="/_astro/hoisted.yFz1BYXO.js"></script>
</head>
<body class="min-h-screen flex flex-col"> <!-- Navigation --> <nav class="border-b border-gray-200 bg-white sticky top-0 z-50"> <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex justify-between h-24 items-center"> <a href="/" class="flex items-center"> <img src="/images/logo.png" alt="Performance West" class="h-20 w-auto" width="83" height="70"> </a> <div class="hidden md:flex items-center gap-8"> <!-- Services dropdown --> <div class="relative" id="services-dropdown"> <button type="button" class="text-sm text-gray-600 hover:text-gray-900 inline-flex items-center gap-1" id="services-btn"> Services <svg class="w-3.5 h-3.5 transition-transform" id="services-chevron" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path></svg> </button> <div id="services-menu" class="absolute left-1/2 -translate-x-1/2 top-full mt-2 rounded-xl border border-gray-200 bg-white shadow-xl hidden z-50" style="width: 720px;"> <div class="grid grid-cols-3 gap-0 p-4"> <!-- Column 1 --> <div class="pr-4 border-r border-gray-100"> <p class="text-[11px] font-semibold uppercase tracking-wider text-blue-500 mb-2">Telecom</p> <a href="/services/telecom/fcc-499a" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">FCC 499A Filing</a> <a href="/services/telecom/stir-shaken" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">STIR/SHAKEN</a> <a href="/services/telecom/ipes-isp" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">FCC Carrier / ISP Registration</a> <a href="/services/telecom/database-management" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Telecom Databases</a> <a href="/services/telecom/state-puc" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">State PUC/PSC Filings</a> <a href="/services/telecom/canada-crtc" class="block py-1.5 text-sm font-medium text-blue-600 hover:text-blue-800">Canada CRTC Package <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-red-500 text-white ml-1 animate-pulse">HOT</span></a> <a href="/tools/fcc-compliance-check" class="block py-1.5 text-sm font-medium text-green-600 hover:text-green-800">FCC Compliance Check <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-green-500 text-white ml-1">FREE</span></a> <p class="text-[11px] font-semibold uppercase tracking-wider text-orange-500 mb-2 mt-4">Trucking / DOT</p> <a href="/services/trucking" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">DOT Compliance Services</a> <a href="/order/dot-compliance" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">MCS-150 / BOC-3 / UCR</a> <a href="/order/trucking-new-carrier" class="block py-1.5 text-sm font-medium text-orange-600 hover:text-orange-800">New Carrier Setup</a> <a href="/tools/dot-compliance-check" class="block py-1.5 text-sm font-medium text-orange-600 hover:text-orange-800">DOT Compliance Check <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-orange-500 text-white ml-1">FREE</span></a> </div> <!-- Column 2 --> <div class="px-4 border-r border-gray-100"> <p class="text-[11px] font-semibold uppercase tracking-wider text-purple-500 mb-2">Data Privacy</p> <a href="/services/privacy/ccpa-audit" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">CCPA/CPRA Audit</a> <a href="/services/privacy/privacy-policy" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Privacy Policy Review</a> <a href="/services/privacy/data-mapping" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Data Mapping</a> <a href="/services/privacy/breach-response" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Breach Response Plan</a> <p class="text-[11px] font-semibold uppercase tracking-wider text-green-500 mb-2 mt-4">TCPA</p> <a href="/services/tcpa/consent-audit" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Consent Audit</a> <a href="/services/tcpa/dnc-compliance" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">DNC Compliance</a> <a href="/services/tcpa/campaign-review" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Campaign Review</a> </div> <!-- Column 3 --> <div class="pl-4"> <p class="text-[11px] font-semibold uppercase tracking-wider text-slate-500 mb-2">Corporate</p> <a href="/services/corporate/formation" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Business Formation</a> <a href="/services/corporate/state-registration" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">State Registration</a> <a href="/services/corporate/annual-reports" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Annual Reports</a> <a href="/services/corporate/registered-agent" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Registered Agent</a> <p class="text-[11px] font-semibold uppercase tracking-wider text-teal-500 mb-2 mt-4">Healthcare</p> <a href="/services/healthcare/npi-revalidation" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Medicare Revalidation</a> <a href="/services/healthcare/medicare-enrollment" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">Medicare Enrollment (PECOS)</a> <a href="/services/healthcare" class="block py-1.5 text-sm text-gray-700 hover:text-pw-700">NPI / NPPES Services</a> <a href="/tools/npi-compliance-check" class="block py-1.5 text-sm font-medium text-teal-600 hover:text-teal-800">NPI Compliance Check <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-teal-500 text-white ml-1">FREE</span></a> <a href="/order/formation" class="mt-3 block py-2 px-3 text-sm font-medium text-white bg-pw-700 hover:bg-pw-800 rounded-lg text-center transition-colors">Form a Business</a> </div> </div> </div> </div> <a href="/services" class="text-sm text-gray-600 hover:text-gray-900">All Services</a> <a href="/pricing" class="text-sm text-gray-600 hover:text-gray-900">Pricing</a> <a href="/tools/contractor-quiz" class="text-sm text-gray-600 hover:text-gray-900">Free Tools</a> <a href="/contact" class="text-sm text-gray-600 hover:text-gray-900">Contact</a> <a href="/order/formation" class="ml-2 px-4 py-2 text-sm font-medium text-white bg-pw-700 hover:bg-pw-800 rounded-lg transition-colors">Form a Business</a> <!-- Account button - links to ERPNext portal --> <a href="https://portal.performancewest.net" id="nav-login-btn" class="ml-1 flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-600 hover:text-pw-700 hover:bg-pw-50 rounded-lg transition-colors border border-gray-200 hover:border-pw-300"> <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"></path></svg> Client Portal </a> <!-- Account button - logged in (hidden until session confirmed) --> <div id="nav-account-btn" class="hidden relative ml-1" id="nav-account-dropdown-root"> <button type="button" id="nav-account-trigger" class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-pw-700 hover:bg-pw-50 rounded-lg transition-colors border border-pw-200"> <div class="w-6 h-6 rounded-full bg-pw-600 flex items-center justify-center shrink-0"> <svg class="w-3.5 h-3.5 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"></path></svg> </div> <span id="nav-greeting" class="max-w-[120px] truncate">My Account</span> <svg class="w-3.5 h-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path></svg> </button> <!-- Dropdown --> <div id="nav-account-menu" class="absolute right-0 top-full mt-1 w-48 bg-white rounded-xl border border-gray-200 shadow-lg hidden z-50 py-1"> <div class="px-4 py-2 border-b border-gray-100"> <p class="text-xs text-gray-500">Signed in as</p> <p id="nav-account-email" class="text-xs font-medium text-gray-800 truncate"></p> </div> <button type="button" id="nav-logout-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"> Sign out </button> </div> </div> </div> <!-- Mobile menu button --> <button type="button" class="md:hidden text-gray-600 hover:text-gray-900" id="mobile-menu-btn"> <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"></path></svg> </button> </div> </div> <!-- Mobile menu --> <div id="mobile-menu" class="md:hidden hidden border-t border-gray-200 bg-white"> <div class="px-4 py-3 space-y-1"> <p class="text-xs font-semibold text-blue-500 uppercase tracking-wider px-2 pt-1">Telecom</p> <a href="/services/telecom/fcc-499a" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">FCC 499A Filing</a> <a href="/services/telecom/stir-shaken" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">STIR/SHAKEN</a> <a href="/services/telecom/ipes-isp" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">FCC Carrier / ISP Registration</a> <a href="/services/telecom/database-management" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Telecom Databases</a> <a href="/services/telecom/state-puc" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">State PUC/PSC</a> <a href="/services/telecom/canada-crtc" class="block px-2 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 rounded">Canada CRTC Package <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-red-500 text-white ml-1 animate-pulse">HOT</span></a> <p class="text-xs font-semibold text-orange-500 uppercase tracking-wider px-2 pt-3">Trucking / DOT</p> <a href="/services/trucking" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">DOT Compliance Services</a> <a href="/order/dot-compliance" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">MCS-150 / BOC-3 / UCR</a> <a href="/order/trucking-new-carrier" class="block px-2 py-2 text-sm font-medium text-orange-600 hover:bg-orange-50 rounded">New Carrier Setup</a> <a href="/tools/dot-compliance-check" class="block px-2 py-2 text-sm font-medium text-orange-600 hover:bg-orange-50 rounded">DOT Compliance Check <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-orange-500 text-white ml-1">FREE</span></a> <p class="text-xs font-semibold text-purple-500 uppercase tracking-wider px-2 pt-3">Data Privacy</p> <a href="/services/privacy/ccpa-audit" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">CCPA/CPRA Audit</a> <a href="/services/privacy/privacy-policy" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Privacy Policy Review</a> <a href="/services/privacy/data-mapping" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Data Mapping</a> <a href="/services/privacy/breach-response" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Breach Response Plan</a> <p class="text-xs font-semibold text-green-500 uppercase tracking-wider px-2 pt-3">TCPA</p> <a href="/services/tcpa/consent-audit" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Consent Audit</a> <a href="/services/tcpa/dnc-compliance" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">DNC Compliance</a> <a href="/services/tcpa/campaign-review" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Campaign Review</a> <p class="text-xs font-semibold text-teal-500 uppercase tracking-wider px-2 pt-3">Healthcare</p> <a href="/services/healthcare/npi-revalidation" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Medicare Revalidation</a> <a href="/services/healthcare/medicare-enrollment" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Medicare Enrollment (PECOS)</a> <a href="/services/healthcare" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">NPI / NPPES Services</a> <a href="/tools/npi-compliance-check" class="block px-2 py-2 text-sm font-medium text-teal-600 hover:bg-teal-50 rounded">NPI Compliance Check <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-teal-500 text-white ml-1">FREE</span></a> <p class="text-xs font-semibold text-slate-500 uppercase tracking-wider px-2 pt-3">Corporate</p> <a href="/services/corporate/formation" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Business Formation</a> <a href="/services/corporate/state-registration" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">State Registration</a> <a href="/services/corporate/annual-reports" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Annual Reports</a> <a href="/services/corporate/registered-agent" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Registered Agent</a> <div class="border-t border-gray-100 my-2"></div> <a href="/services" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">All Services</a> <a href="/pricing" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Pricing</a> <a href="/tools/contractor-quiz" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Free Tools</a> <a href="/contact" class="block px-2 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded">Contact</a> <a href="/order/formation" class="block mx-2 mt-2 px-4 py-2.5 text-sm font-medium text-white bg-pw-700 hover:bg-pw-800 rounded-lg text-center transition-colors">Form a Business</a> </div> </div> </nav>
<!-- Breadcrumb -->
<div class="bg-gray-50 border-b border-gray-200">
<div class="max-w-5xl mx-auto px-4 py-2">
<nav class="text-xs text-gray-500">
<a href="/" class="hover:text-gray-700">Home</a>
<span class="mx-1">/</span>
<span class="text-gray-800 font-medium">My Account</span>
</nav>
</div>
</div>
<!-- Loading state -->
<div id="dash-loading" class="flex-1 flex items-center justify-center py-24">
<div style="display: flex; flex-direction: column; align-items: center; gap: 1rem;">
<div style="width: 2.5rem; height: 2.5rem; border: 3px solid #e5e7eb; border-top-color: #2563eb; border-radius: 50%; animation: pw-spin 0.8s linear infinite;"></div>
<p style="color: #6b7280; font-size: 0.875rem;">Loading your account...</p>
</div>
</div>
<!-- Not authenticated — prompt to log in -->
<div id="dash-auth" class="hidden flex-1">
<div class="max-w-md mx-auto px-4 py-24 text-center">
<div style="width: 4rem; height: 4rem; border-radius: 50%; background: #eff6ff; display: flex; align-items: center; justify-content: center; margin: 0 auto 1.5rem;">
<svg style="width: 2rem; height: 2rem; color: #2563eb;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"/></svg>
</div>
<h1 style="font-size: 1.5rem; font-weight: 700; color: #111827; margin-bottom: 0.5rem;">Sign in to your account</h1>
<p style="color: #6b7280; margin-bottom: 2rem;">Log in to view your orders, invoices, and account details.</p>
<button type="button" id="dash-login-btn" style="background: #2563eb; color: #fff; font-weight: 600; padding: 0.75rem 2rem; border-radius: 0.5rem; border: none; cursor: pointer; font-size: 0.95rem; transition: background 0.2s;" onmouseover="this.style.background='#1d4ed8'" onmouseout="this.style.background='#2563eb'">
Sign In / Register
</button>
</div>
</div>
<!-- Error state -->
<div id="dash-error" class="hidden flex-1">
<div class="max-w-md mx-auto px-4 py-24 text-center">
<div style="width: 4rem; height: 4rem; border-radius: 50%; background: #fef2f2; display: flex; align-items: center; justify-content: center; margin: 0 auto 1.5rem;">
<svg style="width: 2rem; height: 2rem; color: #dc2626;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
</div>
<h2 style="font-size: 1.25rem; font-weight: 700; color: #111827; margin-bottom: 0.5rem;">Something went wrong</h2>
<p id="dash-error-msg" style="color: #6b7280; margin-bottom: 1.5rem;">Unable to load your account data. Please try again.</p>
<button type="button" onclick="location.reload()" style="background: #2563eb; color: #fff; font-weight: 600; padding: 0.625rem 1.5rem; border-radius: 0.5rem; border: none; cursor: pointer; font-size: 0.875rem;">Retry</button>
</div>
</div>
<!-- Dashboard (shown when authenticated) -->
<div id="dash-main" class="hidden flex-1">
<!-- Account header -->
<section style="background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%);" class="py-10">
<div class="max-w-5xl mx-auto px-4">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 style="font-size: 1.75rem; font-weight: 700; color: #fff; margin-bottom: 0.25rem;">My Account</h1>
<p id="dash-name" style="color: #bfdbfe; font-size: 1rem;"></p>
<p id="dash-email" style="color: #93c5fd; font-size: 0.875rem;"></p>
<p id="dash-company" style="color: #93c5fd; font-size: 0.875rem;"></p>
</div>
<button type="button" id="dash-logout-btn" style="background: rgba(255,255,255,0.15); color: #fff; font-weight: 500; padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.25); cursor: pointer; font-size: 0.875rem; transition: background 0.2s; align-self: flex-start;" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'">
Sign Out
</button>
</div>
</div>
</section>
<!-- Orders section -->
<section class="py-10">
<div class="max-w-5xl mx-auto px-4">
<h2 style="font-size: 1.25rem; font-weight: 700; color: #111827; margin-bottom: 1rem;">Your Orders</h2>
<!-- Tab bar -->
<div id="dash-tabs" style="display: flex; gap: 0; border-bottom: 2px solid #e5e7eb; margin-bottom: 1.5rem; overflow-x: auto;">
<button type="button" data-tab="all" class="pw-tab pw-tab-active" style="padding: 0.625rem 1.25rem; font-size: 0.875rem; font-weight: 600; border: none; background: none; cursor: pointer; white-space: nowrap; border-bottom: 2px solid #2563eb; margin-bottom: -2px; color: #2563eb;">All Orders</button>
<button type="button" data-tab="dot" class="pw-tab" style="padding: 0.625rem 1.25rem; font-size: 0.875rem; font-weight: 500; border: none; background: none; cursor: pointer; white-space: nowrap; border-bottom: 2px solid transparent; margin-bottom: -2px; color: #6b7280;">DOT/Trucking</button>
<button type="button" data-tab="fcc" class="pw-tab" style="padding: 0.625rem 1.25rem; font-size: 0.875rem; font-weight: 500; border: none; background: none; cursor: pointer; white-space: nowrap; border-bottom: 2px solid transparent; margin-bottom: -2px; color: #6b7280;">FCC/Telecom</button>
</div>
<!-- Orders grid -->
<div id="dash-orders" class="grid gap-4"></div>
<!-- Empty state -->
<div id="dash-empty" class="hidden" style="text-align: center; padding: 3rem 1rem;">
<div style="width: 3.5rem; height: 3.5rem; border-radius: 50%; background: #f3f4f6; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
<svg style="width: 1.75rem; height: 1.75rem; color: #9ca3af;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"/></svg>
</div>
<p style="font-weight: 600; color: #374151; margin-bottom: 0.25rem;">No orders yet</p>
<p style="color: #6b7280; font-size: 0.875rem; margin-bottom: 1.25rem;">Browse our compliance services to get started.</p>
<a href="/services" style="display: inline-block; background: #2563eb; color: #fff; font-weight: 600; padding: 0.625rem 1.5rem; border-radius: 0.5rem; text-decoration: none; font-size: 0.875rem; transition: background 0.2s;" onmouseover="this.style.background='#1d4ed8'" onmouseout="this.style.background='#2563eb'">Browse Services</a>
</div>
</div>
</section>
</div>
<!-- Footer -->
<footer class="border-t border-gray-200 bg-gray-50 mt-auto py-8">
<div class="max-w-4xl mx-auto px-4 text-center text-xs text-gray-400">
<img src="/images/logo.png" alt="Performance West" class="h-10 mx-auto mb-3">
<p>Performance West Inc. · 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 · <a href="https://performancewest.net" class="text-gray-500">performancewest.net</a> · (888) 411-0383</p>
<p class="mt-2">Performance West is a regulatory compliance consulting firm, not a law firm. This does not constitute legal advice.</p>
</div>
</footer>
<style>
@keyframes pw-spin { to { transform: rotate(360deg); } }
</style>
<script>
(function() {
var API = window.__PW_API;
var $loading = document.getElementById('dash-loading');
var $auth = document.getElementById('dash-auth');
var $error = document.getElementById('dash-error');
var $main = document.getElementById('dash-main');
// --- Helpers ---
function show(el) { el.classList.remove('hidden'); el.style.display = ''; }
function hide(el) { el.classList.add('hidden'); }
function formatCents(cents) {
var amt = (cents || 0) / 100;
return '$' + amt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatDate(iso) {
if (!iso) return '--';
var d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function statusBadge(status) {
var colors = {
paid: { bg: '#dcfce7', text: '#15803d', label: 'Paid' },
completed: { bg: '#dcfce7', text: '#15803d', label: 'Completed' },
pending: { bg: '#fef9c3', text: '#a16207', label: 'Pending' },
failed: { bg: '#fef2f2', text: '#dc2626', label: 'Failed' },
refunded: { bg: '#f3f4f6', text: '#6b7280', label: 'Refunded' }
};
var c = colors[status] || colors.pending;
return '<span style="display:inline-block;padding:0.2rem 0.65rem;border-radius:9999px;font-size:0.7rem;font-weight:600;background:' + c.bg + ';color:' + c.text + ';">' + c.label + '</span>';
}
function paymentIcon(method) {
if (!method) return '';
var m = method.toLowerCase();
if (m.indexOf('stripe') !== -1 || m.indexOf('card') !== -1) {
return '<svg style="width:1.25rem;height:1.25rem;color:#6366f1;flex-shrink:0;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"/></svg>';
}
if (m.indexOf('paypal') !== -1) {
return '<svg style="width:1.25rem;height:1.25rem;color:#0070ba;flex-shrink:0;" fill="currentColor" viewBox="0 0 24 24"><path d="M7.076 21.337H2.47a.641.641 0 01-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797H9.603c-.536 0-.99.394-1.073.926l-1.454 9.08zm12.328-14.62c-.06.364-.136.72-.233 1.07-1.008 3.612-3.96 5.272-7.537 5.272H9.993l-1.33 8.277h2.898c.47 0 .87-.339.943-.803l.039-.2.748-4.748.048-.26a.953.953 0 01.943-.803h.594c3.845 0 6.858-1.562 7.738-6.08.367-1.89.177-3.466-.794-4.575a3.83 3.83 0 00-1.09-.84z"/></svg>';
}
if (m.indexOf('crypto') !== -1 || m.indexOf('shkeeper') !== -1) {
return '<svg style="width:1.25rem;height:1.25rem;color:#f7931a;flex-shrink:0;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>';
}
if (m.indexOf('ach') !== -1 || m.indexOf('bank') !== -1) {
return '<svg style="width:1.25rem;height:1.25rem;color:#059669;flex-shrink:0;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21v-8.25M15.75 21v-8.25M8.25 21v-8.25M3 9l9-6 9 6m-1.5 12V10.332A48.36 48.36 0 0012 9.75c-2.551 0-5.056.2-7.5.582V21M3 21h18M12 6.75h.008v.008H12V6.75z"/></svg>';
}
return '';
}
// DOT/trucking slugs
var DOT_SLUGS = ['mcs-150','boc-3','ucr','dot-new-entrant','dot-compliance','ifta','irp','mcp','carb','ca-number','hazmat','oversize-overweight'];
// FCC/telecom slugs
var FCC_SLUGS = ['fcc-499a','fcc-499q','stir-shaken','rmd','cpni','ipes-isp','state-puc','neca-ocn','lata','fcc-214','fcc-477','fcc-registration','fcc-red-light','canada-crtc'];
function classifyOrder(slug) {
if (!slug) return 'other';
var s = slug.toLowerCase();
for (var i = 0; i < DOT_SLUGS.length; i++) { if (s.indexOf(DOT_SLUGS[i]) !== -1) return 'dot'; }
for (var i = 0; i < FCC_SLUGS.length; i++) { if (s.indexOf(FCC_SLUGS[i]) !== -1) return 'fcc'; }
return 'other';
}
// --- Logout ---
function doLogout() {
fetch(API + '/api/v1/auth/logout', { method: 'POST', credentials: 'include' })
.then(function() { location.reload(); })
.catch(function() { location.reload(); });
}
document.getElementById('dash-logout-btn').addEventListener('click', doLogout);
// --- Login button ---
document.getElementById('dash-login-btn').addEventListener('click', function() {
if (typeof window.pwOpenAuth === 'function') {
window.pwOpenAuth('login');
} else {
// Fallback: redirect to login page or portal
window.location.href = 'https://portal.performancewest.net';
}
});
// --- Render orders ---
var allOrders = [];
function renderOrders(filter) {
var $grid = document.getElementById('dash-orders');
var $empty = document.getElementById('dash-empty');
var filtered = allOrders;
if (filter === 'dot') {
filtered = allOrders.filter(function(o) { return classifyOrder(o.service_slug) === 'dot'; });
} else if (filter === 'fcc') {
filtered = allOrders.filter(function(o) { return classifyOrder(o.service_slug) === 'fcc'; });
}
if (filtered.length === 0) {
$grid.innerHTML = '';
show($empty);
return;
}
hide($empty);
// Group by batch_id where applicable
var batches = {};
var standalone = [];
filtered.forEach(function(o) {
if (o.batch_id) {
if (!batches[o.batch_id]) batches[o.batch_id] = [];
batches[o.batch_id].push(o);
} else {
standalone.push(o);
}
});
var html = '';
// Render batch groups
Object.keys(batches).forEach(function(bid) {
var items = batches[bid];
var first = items[0];
var totalCents = 0;
var names = [];
items.forEach(function(o) {
totalCents += (o.service_fee_cents || 0) - (o.discount_cents || 0) + (o.surcharge_cents || 0);
if (names.indexOf(o.service_name) === -1) names.push(o.service_name);
});
html += '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:0.75rem;padding:1.25rem;transition:box-shadow 0.2s;" onmouseover="this.style.boxShadow=\'0 4px 12px rgba(0,0,0,0.08)\'" onmouseout="this.style.boxShadow=\'none\'">';
html += '<div style="display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:0.75rem;">';
// Left
html += '<div style="flex:1;min-width:200px;">';
html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.375rem;">';
html += '<span style="font-weight:700;color:#111827;font-size:0.95rem;">Batch ' + bid + '</span>';
html += '<span style="color:#9ca3af;font-size:0.75rem;">(' + items.length + ' items)</span>';
html += '</div>';
html += '<p style="color:#4b5563;font-size:0.875rem;margin-bottom:0.375rem;">' + names.join(', ') + '</p>';
html += '<p style="color:#9ca3af;font-size:0.75rem;">' + formatDate(first.paid_at || first.created_at) + '</p>';
html += '</div>';
// Right
html += '<div style="display:flex;align-items:center;gap:1rem;flex-shrink:0;">';
html += paymentIcon(first.payment_method);
html += '<span style="font-weight:700;color:#111827;font-size:1rem;">' + formatCents(totalCents) + '</span>';
html += statusBadge(first.payment_status);
html += '</div>';
html += '</div></div>';
});
// Render standalone orders
standalone.forEach(function(o) {
var totalCents = (o.service_fee_cents || 0) - (o.discount_cents || 0) + (o.surcharge_cents || 0);
html += '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:0.75rem;padding:1.25rem;transition:box-shadow 0.2s;" onmouseover="this.style.boxShadow=\'0 4px 12px rgba(0,0,0,0.08)\'" onmouseout="this.style.boxShadow=\'none\'">';
html += '<div style="display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:0.75rem;">';
// Left
html += '<div style="flex:1;min-width:200px;">';
html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.375rem;">';
html += '<span style="font-weight:700;color:#111827;font-size:0.95rem;">#' + (o.order_number || '--') + '</span>';
html += '</div>';
html += '<p style="color:#4b5563;font-size:0.875rem;margin-bottom:0.375rem;">' + (o.service_name || o.service_slug || 'Order') + '</p>';
html += '<p style="color:#9ca3af;font-size:0.75rem;">' + formatDate(o.paid_at || o.created_at) + '</p>';
html += '</div>';
// Right
html += '<div style="display:flex;align-items:center;gap:1rem;flex-shrink:0;">';
html += paymentIcon(o.payment_method);
html += '<span style="font-weight:700;color:#111827;font-size:1rem;">' + formatCents(totalCents) + '</span>';
html += statusBadge(o.payment_status);
html += '</div>';
html += '</div></div>';
});
$grid.innerHTML = html;
}
// --- Tab switching ---
var currentTab = 'all';
document.getElementById('dash-tabs').addEventListener('click', function(e) {
var btn = e.target.closest('[data-tab]');
if (!btn) return;
currentTab = btn.getAttribute('data-tab');
// Update tab styles
var tabs = document.querySelectorAll('#dash-tabs .pw-tab');
tabs.forEach(function(t) {
if (t.getAttribute('data-tab') === currentTab) {
t.style.borderBottomColor = '#2563eb';
t.style.color = '#2563eb';
t.style.fontWeight = '600';
} else {
t.style.borderBottomColor = 'transparent';
t.style.color = '#6b7280';
t.style.fontWeight = '500';
}
});
renderOrders(currentTab);
});
// --- Init ---
fetch(API + '/api/v1/auth/me', { credentials: 'include' })
.then(function(r) {
if (!r.ok) throw new Error('not_auth');
return r.json();
})
.then(function() {
// Authenticated — fetch portal data
return fetch(API + '/api/v1/portal/me', { credentials: 'include' });
})
.then(function(r) {
if (!r.ok) throw new Error('portal_error');
return r.json();
})
.then(function(data) {
hide($loading);
// Populate header
var cust = data.customer || {};
document.getElementById('dash-name').textContent = cust.name || '';
document.getElementById('dash-email').textContent = cust.email || '';
document.getElementById('dash-company').textContent = cust.company || '';
// Merge compliance_orders and orders (CRTC)
allOrders = (data.compliance_orders || []).slice();
if (data.orders && data.orders.length) {
data.orders.forEach(function(o) {
allOrders.push({
order_number: o.order_number || o.id,
batch_id: o.batch_id || null,
service_slug: o.service_slug || o.type || '',
service_name: o.service_name || o.type || 'CRTC Order',
service_fee_cents: o.service_fee_cents || o.amount_cents || 0,
discount_cents: o.discount_cents || 0,
surcharge_cents: o.surcharge_cents || 0,
payment_status: o.payment_status || o.status || 'pending',
payment_method: o.payment_method || '',
created_at: o.created_at,
paid_at: o.paid_at
});
});
}
// Sort newest first
allOrders.sort(function(a, b) {
return new Date(b.paid_at || b.created_at || 0) - new Date(a.paid_at || a.created_at || 0);
});
show($main);
renderOrders('all');
})
.catch(function(err) {
hide($loading);
if (err.message === 'not_auth') {
show($auth);
} else {
document.getElementById('dash-error-msg').textContent = 'Unable to load your account data. Please try again later.';
show($error);
}
});
// Listen for auth success (from the auth modal)
window.addEventListener('pw:auth:success', function() {
location.reload();
});
})();
</script>
</body>
</html>