Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
359
site/src/components/intake/Wizard.astro
Normal file
359
site/src/components/intake/Wizard.astro
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
---
|
||||
/**
|
||||
* Intake Wizard shell.
|
||||
*
|
||||
* Renders a step progress bar + back/next buttons; the active step's
|
||||
* content is rendered inline from the matching component under
|
||||
* ./steps/<kebab>Step.astro via dynamic import.
|
||||
*
|
||||
* Props:
|
||||
* service_slug: which catalog entry this wizard targets
|
||||
* steps: ordered list from INTAKE_MANIFEST[slug]
|
||||
* title: page title shown above the wizard
|
||||
*
|
||||
* State lives in window.sessionStorage under the key
|
||||
* "pw-intake-<slug>" so a reload doesn't wipe progress. Step components
|
||||
* read/write via the shared `PWIntake` browser helper declared below.
|
||||
*/
|
||||
|
||||
import EntityStep from "./steps/EntityStep.astro";
|
||||
import CategoryStep from "./steps/CategoryStep.astro";
|
||||
import OfficerStep from "./steps/OfficerStep.astro";
|
||||
import JurisdictionStep from "./steps/JurisdictionStep.astro";
|
||||
import HistoryStep from "./steps/HistoryStep.astro";
|
||||
import WirelessStep from "./steps/WirelessStep.astro";
|
||||
import EarthStationStep from "./steps/EarthStationStep.astro";
|
||||
import AudioBridgingStep from "./steps/AudioBridgingStep.astro";
|
||||
import RevenueStep from "./steps/RevenueStep.astro";
|
||||
import BundledServiceStep from "./steps/BundledServiceStep.astro";
|
||||
import IccImportStep from "./steps/IccImportStep.astro";
|
||||
import ResellerCertStep from "./steps/ResellerCertStep.astro";
|
||||
import LNPARegionStep from "./steps/LNPARegionStep.astro";
|
||||
import Block6CertStep from "./steps/Block6CertStep.astro";
|
||||
import BDCDataStep from "./steps/BDCDataStep.astro";
|
||||
import STIRShakenStep from "./steps/STIRShakenStep.astro";
|
||||
import CALEAStep from "./steps/CALEAStep.astro";
|
||||
import ForeignCarrierStep from "./steps/ForeignCarrierStep.astro";
|
||||
import ForeignQualStep from "./steps/ForeignQualStep.astro";
|
||||
import DCAgentStep from "./steps/DCAgentStep.astro";
|
||||
import CPNIStep from "./steps/CPNIStep.astro";
|
||||
import CDRPeriodStep from "./steps/CDRPeriodStep.astro";
|
||||
import OCNStep from "./steps/OCNStep.astro";
|
||||
import ClassificationWizard from "./steps/ClassificationWizard.astro";
|
||||
import ReviewStep from "./steps/ReviewStep.astro";
|
||||
import PaymentStep from "./steps/PaymentStep.astro";
|
||||
|
||||
export interface Props {
|
||||
service_slug: string;
|
||||
steps: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { service_slug, steps, title } = Astro.props;
|
||||
|
||||
const STEP_LABELS: Record<string, string> = {
|
||||
entity: "Carrier",
|
||||
category: "Line 105",
|
||||
classification: "Carrier Type",
|
||||
officer: "Officers",
|
||||
jurisdiction: "Jurisdictions",
|
||||
history: "Service Start",
|
||||
wireless: "Wireless",
|
||||
earth_station: "Satellite / Pvt Line",
|
||||
audio_bridging: "Audio Bridging",
|
||||
revenue: "Revenue",
|
||||
bundled_service: "Bundles",
|
||||
icc_import: "ICC Import",
|
||||
reseller_cert: "Resellers",
|
||||
lnpa_region: "LNPA Regions",
|
||||
block6_cert: "Certifications",
|
||||
bdc_data: "BDC Data",
|
||||
stir_shaken: "STIR/SHAKEN",
|
||||
calea: "CALEA",
|
||||
foreign_carrier: "Foreign Affiliation",
|
||||
foreign_qual: "State Registration",
|
||||
dc_agent: "D.C. Agent",
|
||||
cpni_questions: "CPNI Details",
|
||||
cdr_period: "Reporting Period",
|
||||
ocn: "OCN Details",
|
||||
review: "Review",
|
||||
payment: "Payment",
|
||||
};
|
||||
---
|
||||
|
||||
<div class="pw-wizard" data-service={service_slug} data-steps={JSON.stringify(steps)}>
|
||||
<header class="pw-wizard-header">
|
||||
<h1>{title}</h1>
|
||||
<ol class="pw-wizard-stepbar">
|
||||
{steps.map((step, i) => (
|
||||
<li class="pw-step-chip" data-step={step} data-idx={i}>
|
||||
<span class="pw-step-num">{i + 1}</span>
|
||||
<span class="pw-step-label">{STEP_LABELS[step] ?? step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</header>
|
||||
|
||||
<section class="pw-wizard-body">
|
||||
{steps.includes("entity") && <div data-step="entity" hidden><EntityStep /></div>}
|
||||
{steps.includes("category") && <div data-step="category" hidden><CategoryStep /></div>}
|
||||
{steps.includes("officer") && <div data-step="officer" hidden><OfficerStep /></div>}
|
||||
{steps.includes("jurisdiction") && <div data-step="jurisdiction" hidden><JurisdictionStep /></div>}
|
||||
{steps.includes("history") && <div data-step="history" hidden><HistoryStep /></div>}
|
||||
<div data-step="wireless" hidden><WirelessStep /></div>
|
||||
<div data-step="earth_station" hidden><EarthStationStep /></div>
|
||||
<div data-step="audio_bridging" hidden><AudioBridgingStep /></div>
|
||||
{steps.includes("revenue") && <div data-step="revenue" hidden><RevenueStep /></div>}
|
||||
{steps.includes("bundled_service") && <div data-step="bundled_service" hidden><BundledServiceStep /></div>}
|
||||
{steps.includes("icc_import") && <div data-step="icc_import" hidden><IccImportStep /></div>}
|
||||
{steps.includes("reseller_cert") && <div data-step="reseller_cert" hidden><ResellerCertStep /></div>}
|
||||
{steps.includes("lnpa_region") && <div data-step="lnpa_region" hidden><LNPARegionStep /></div>}
|
||||
{steps.includes("block6_cert") && <div data-step="block6_cert" hidden><Block6CertStep /></div>}
|
||||
{steps.includes("bdc_data") && <div data-step="bdc_data" hidden><BDCDataStep service_slug={service_slug} /></div>}
|
||||
{steps.includes("stir_shaken") && <div data-step="stir_shaken" hidden><STIRShakenStep /></div>}
|
||||
{steps.includes("calea") && <div data-step="calea" hidden><CALEAStep /></div>}
|
||||
{steps.includes("foreign_carrier") && <div data-step="foreign_carrier" hidden><ForeignCarrierStep /></div>}
|
||||
{steps.includes("foreign_qual") && <div data-step="foreign_qual" hidden><ForeignQualStep /></div>}
|
||||
{steps.includes("dc_agent") && <div data-step="dc_agent" hidden><DCAgentStep /></div>}
|
||||
{steps.includes("cpni_questions") && <div data-step="cpni_questions" hidden><CPNIStep /></div>}
|
||||
{steps.includes("cdr_period") && <div data-step="cdr_period" hidden><CDRPeriodStep /></div>}
|
||||
{steps.includes("ocn") && <div data-step="ocn" hidden><OCNStep /></div>}
|
||||
{steps.includes("classification") && <div data-step="classification" hidden><ClassificationWizard /></div>}
|
||||
{steps.includes("review") && <div data-step="review" hidden><ReviewStep service_slug={service_slug} /></div>}
|
||||
{steps.includes("payment") && <div data-step="payment" hidden><PaymentStep service_slug={service_slug} /></div>}
|
||||
</section>
|
||||
|
||||
<footer class="pw-wizard-nav">
|
||||
<button type="button" class="pw-btn pw-btn-plain" id="pw-back">← Back</button>
|
||||
<button type="button" class="pw-btn" id="pw-next">Next →</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pw-wizard { max-width: 820px; margin: 0 auto; padding: 1rem; }
|
||||
.pw-wizard-header h1 { margin: 0 0 1rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-wizard-stepbar {
|
||||
display: flex; align-items: center; flex-wrap: wrap;
|
||||
padding: 0; margin: 0 0 1.5rem; list-style: none;
|
||||
gap: 0; background: #f8fafc; border-radius: 8px;
|
||||
border: 1px solid #e2e8f0; overflow: hidden;
|
||||
}
|
||||
.pw-step-chip {
|
||||
display: flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.15s;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pw-step-chip:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute; right: -0.5rem; top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 0.7rem; height: 0.7rem;
|
||||
border-top: 1px solid #cbd5e1;
|
||||
border-right: 1px solid #cbd5e1;
|
||||
background: #f8fafc;
|
||||
z-index: 1;
|
||||
}
|
||||
.pw-step-chip[data-active="true"] { background: #1a2744; color: #fff; }
|
||||
.pw-step-chip[data-active="true"]::after { background: #1a2744; border-color: #1a2744; }
|
||||
.pw-step-chip[data-done="true"] { color: #065f46; }
|
||||
.pw-step-chip[data-done="true"]::after { border-color: #a7f3d0; }
|
||||
.pw-step-num {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.2rem; height: 1.2rem; border-radius: 50%;
|
||||
background: #e2e8f0; color: #64748b;
|
||||
font-size: 0.7rem; font-weight: 700;
|
||||
}
|
||||
.pw-step-chip[data-active="true"] .pw-step-num { background: rgba(255,255,255,0.25); color: #fff; }
|
||||
.pw-step-chip[data-done="true"] .pw-step-num { background: #d1fae5; color: #065f46; }
|
||||
.pw-wizard-body {
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
|
||||
padding: 1.5rem; min-height: 280px;
|
||||
}
|
||||
.pw-wizard-nav {
|
||||
display: flex; justify-content: space-between;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
.pw-btn {
|
||||
padding: 0.65rem 1.4rem; border: 0; border-radius: 6px;
|
||||
background: #059669; color: #fff; font-weight: 600;
|
||||
cursor: pointer; font-size: 0.95rem;
|
||||
}
|
||||
.pw-btn-plain { background: #e2e8f0; color: #1f2937; }
|
||||
.pw-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// ── Client state machine + sessionStorage persistence ──────────────
|
||||
type IntakeState = {
|
||||
service_slug: string;
|
||||
step_index: number;
|
||||
entity: Record<string, any>;
|
||||
officers: Record<string, any>[];
|
||||
intake_data: Record<string, any>;
|
||||
email: string;
|
||||
name: string;
|
||||
telecom_entity_id: number | null;
|
||||
};
|
||||
|
||||
const wizard = document.querySelector(".pw-wizard")!;
|
||||
const slug = wizard.getAttribute("data-service")!;
|
||||
const initialSteps: string[] = JSON.parse(wizard.getAttribute("data-steps")!);
|
||||
let steps: string[] = [...initialSteps];
|
||||
const storageKey = `pw-intake-${slug}`;
|
||||
|
||||
// Step-label map used by the dynamic step bar rebuilder when
|
||||
// line_105_categories changes. Mirror of the Astro-side STEP_LABELS.
|
||||
(window as any).PW_STEP_LABELS = {
|
||||
entity: "Carrier", category: "Line 105", officer: "Officers",
|
||||
jurisdiction: "Jurisdictions", history: "Service Start",
|
||||
wireless: "Wireless", earth_station: "Satellite / Pvt Line",
|
||||
audio_bridging: "Audio Bridging", revenue: "Revenue",
|
||||
bundled_service: "Bundles", icc_import: "ICC Import",
|
||||
reseller_cert: "Resellers", lnpa_region: "LNPA Regions",
|
||||
block6_cert: "Certifications", bdc_data: "BDC Data",
|
||||
stir_shaken: "STIR/SHAKEN", calea: "CALEA",
|
||||
foreign_carrier: "Foreign Affiliation", foreign_qual: "State Registration", dc_agent: "D.C. Agent", cpni_questions: "CPNI Details", cdr_period: "Reporting Period",
|
||||
ocn: "OCN Details", review: "Review", payment: "Payment",
|
||||
};
|
||||
|
||||
// Category-gated dynamic step insertion. After the user picks Line 105
|
||||
// categories on the CategoryStep, this re-shapes steps[] to insert
|
||||
// wireless / earth_station / audio_bridging between category and
|
||||
// revenue (or between entity and revenue if no category step).
|
||||
const CATEGORY_GATED: Record<string, string[]> = {
|
||||
wireless: ["wireless"],
|
||||
satellite: ["earth_station"],
|
||||
mobile_satellite: ["earth_station"],
|
||||
private_line: ["earth_station"],
|
||||
audio_bridging: ["audio_bridging"],
|
||||
};
|
||||
|
||||
function reshapeSteps(categories: Array<{id: string}>): string[] {
|
||||
const base = [...initialSteps];
|
||||
const gated = new Set<string>();
|
||||
for (const cat of categories) {
|
||||
for (const step of (CATEGORY_GATED[cat.id] || [])) {
|
||||
gated.add(step);
|
||||
}
|
||||
}
|
||||
if (gated.size === 0) return base;
|
||||
// Insert gated steps after `history` if present, else after `category`,
|
||||
// else before `revenue`.
|
||||
const anchorIdx = Math.max(
|
||||
base.indexOf("history"),
|
||||
base.indexOf("category"),
|
||||
);
|
||||
const insertAt = anchorIdx >= 0 ? anchorIdx + 1 : base.indexOf("revenue");
|
||||
if (insertAt < 0) return base;
|
||||
return [
|
||||
...base.slice(0, insertAt),
|
||||
...Array.from(gated).filter((s) => !base.includes(s)),
|
||||
...base.slice(insertAt),
|
||||
];
|
||||
}
|
||||
|
||||
function rebuildStepBar() {
|
||||
const bar = wizard.querySelector(".pw-wizard-stepbar")!;
|
||||
bar.innerHTML = "";
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "pw-step-chip";
|
||||
li.setAttribute("data-step", steps[i]);
|
||||
li.setAttribute("data-idx", String(i));
|
||||
const label = (window as any).PW_STEP_LABELS?.[steps[i]] ?? steps[i];
|
||||
li.innerHTML = `<span class="pw-step-num">${i + 1}</span><span class="pw-step-label">${label}</span>`;
|
||||
bar.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
function loadState(): IntakeState {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(storageKey);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return {
|
||||
service_slug: slug,
|
||||
step_index: 0,
|
||||
entity: {},
|
||||
officers: [{}, {}, {}],
|
||||
intake_data: {},
|
||||
email: "",
|
||||
name: "",
|
||||
telecom_entity_id: null,
|
||||
};
|
||||
}
|
||||
function saveState(s: IntakeState) {
|
||||
sessionStorage.setItem(storageKey, JSON.stringify(s));
|
||||
}
|
||||
|
||||
// Expose helper on window for step components to read/write.
|
||||
(window as any).PWIntake = {
|
||||
get: (): IntakeState => loadState(),
|
||||
set: (patch: Partial<IntakeState>) => {
|
||||
const s = { ...loadState(), ...patch };
|
||||
saveState(s);
|
||||
return s;
|
||||
},
|
||||
patchIntakeData: (patch: Record<string, any>) => {
|
||||
const s = loadState();
|
||||
s.intake_data = { ...s.intake_data, ...patch };
|
||||
saveState(s);
|
||||
// If the patch touched line_105_categories, reshape the step list
|
||||
if ("line_105_categories" in patch) {
|
||||
const newSteps = reshapeSteps(patch.line_105_categories || []);
|
||||
if (JSON.stringify(newSteps) !== JSON.stringify(steps)) {
|
||||
steps = newSteps;
|
||||
(window as any).PWIntake.steps = steps;
|
||||
rebuildStepBar();
|
||||
}
|
||||
}
|
||||
return s;
|
||||
},
|
||||
slug,
|
||||
get steps() { return steps; },
|
||||
set steps(s: string[]) { steps = s; },
|
||||
};
|
||||
|
||||
function renderStep(idx: number) {
|
||||
const state = loadState();
|
||||
state.step_index = idx;
|
||||
saveState(state);
|
||||
wizard.querySelectorAll<HTMLElement>(".pw-wizard-body > [data-step]").forEach((el) => {
|
||||
el.hidden = el.getAttribute("data-step") !== steps[idx];
|
||||
});
|
||||
wizard.querySelectorAll<HTMLElement>(".pw-step-chip").forEach((chip, i) => {
|
||||
chip.setAttribute("data-active", String(i === idx));
|
||||
chip.setAttribute("data-done", String(i < idx));
|
||||
});
|
||||
(document.getElementById("pw-back") as HTMLButtonElement).disabled = idx === 0;
|
||||
const nextBtn = document.getElementById("pw-next") as HTMLButtonElement;
|
||||
nextBtn.textContent = idx === steps.length - 1 ? "Finish" : "Next →";
|
||||
// Let the active step hydrate itself
|
||||
window.dispatchEvent(new CustomEvent("pw:step-shown", {
|
||||
detail: { step: steps[idx], idx },
|
||||
}));
|
||||
}
|
||||
|
||||
document.getElementById("pw-back")!.addEventListener("click", () => {
|
||||
const s = loadState();
|
||||
if (s.step_index > 0) renderStep(s.step_index - 1);
|
||||
});
|
||||
document.getElementById("pw-next")!.addEventListener("click", () => {
|
||||
const s = loadState();
|
||||
// Validate the current step — it may cancel by setting a flag.
|
||||
const cancelEvt = new CustomEvent<{ cancel: boolean; reason?: string }>(
|
||||
"pw:step-next", { detail: { cancel: false }, cancelable: true },
|
||||
);
|
||||
window.dispatchEvent(cancelEvt);
|
||||
// Step components call evt.preventDefault() to block.
|
||||
if (cancelEvt.defaultPrevented) return;
|
||||
if (s.step_index < steps.length - 1) renderStep(s.step_index + 1);
|
||||
});
|
||||
|
||||
// Kick off
|
||||
renderStep(loadState().step_index || 0);
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue