new-site/site/src/components/intake/steps/CPNIStep.astro
justin 862c06a8fd Add validation to CPNI and STIR/SHAKEN intake steps
- CPNI: requires either clean compliance checkbox OR issues section opened
- STIR/SHAKEN: requires selecting implementation status before advancing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 18:40:28 -05:00

168 lines
6.8 KiB
Text

---
// CPNIStep — simplified CPNI certification confirmation.
// Most carriers have zero complaints/breaches/issues. Default to "all clean"
// with a single checkbox to confirm, and an expandable section for carriers
// that DID have issues during the reporting period.
---
<div class="pw-step">
<h2>CPNI Certification — <span id="pw-cpni-year"></span></h2>
<p class="pw-help">
Confirm your CPNI compliance status for the reporting period.
Most carriers can certify clean compliance — just confirm below.
</p>
<label class="pw-confirm-box">
<input type="checkbox" id="pw-cpni-clean" checked />
<div>
<strong>I confirm clean CPNI compliance for this reporting period:</strong>
<ul>
<li>No complaints regarding unauthorized release or use of CPNI</li>
<li>No data breaches involving CPNI</li>
<li>No employee disciplinary actions for CPNI violations</li>
<li>No unauthorized data broker access to CPNI</li>
<li>We do not use CPNI for marketing beyond subscribed services</li>
</ul>
</div>
</label>
<details id="pw-cpni-issues" class="pw-issues">
<summary>I had compliance issues during this period (complaints, breaches, etc.)</summary>
<div class="pw-issues-body">
<label class="pw-field">CPNI complaints received
<input type="number" id="pw-cpni-complaints-count" min="0" value="0" />
</label>
<label class="pw-field">Complaint details (if any)
<textarea id="pw-cpni-complaints-desc" rows="2" placeholder="Nature of complaints and how resolved"></textarea>
</label>
<label class="pw-field">Data breaches involving CPNI
<input type="number" id="pw-cpni-breaches-count" min="0" value="0" />
</label>
<label class="pw-field">Breach details (if any)
<textarea id="pw-cpni-breaches-desc" rows="2" placeholder="Description, notifications filed per 47 CFR § 64.2011"></textarea>
</label>
<label class="pw-field">Employees disciplined for CPNI violations
<input type="number" id="pw-cpni-disciplinary-count" min="0" value="0" />
</label>
<label class="pw-field">Data broker issues
<textarea id="pw-cpni-brokers-desc" rows="2" placeholder="Actions taken against data brokers, if any"></textarea>
</label>
<label class="pw-field">CPNI marketing usage
<select id="pw-cpni-marketing">
<option value="no">No — we do not use CPNI for marketing</option>
<option value="opt_in">Yes — with opt-in customer approval</option>
<option value="opt_out">Yes — with opt-out customer approval</option>
</select>
</label>
</div>
</details>
<div id="pw-cpni-err" class="pw-err" hidden></div>
</div>
<style>
.pw-step h2 { margin: 0 0 0.5rem; color: #1a2744; }
.pw-help { color: #64748b; font-size: 0.9rem; margin-bottom: 1rem; }
.pw-confirm-box {
display: flex; gap: 0.75rem; padding: 1rem; background: #f0fdf4;
border: 1px solid #86efac; border-radius: 8px; cursor: pointer;
margin-bottom: 1rem; align-items: flex-start;
}
.pw-confirm-box input { margin-top: 0.3rem; width: 18px; height: 18px; accent-color: #059669; }
.pw-confirm-box strong { display: block; font-size: 0.9rem; color: #065f46; margin-bottom: 0.3rem; }
.pw-confirm-box ul { margin: 0; padding-left: 1.25rem; font-size: 0.82rem; color: #047857; }
.pw-confirm-box li { margin: 0.15rem 0; }
.pw-issues { margin-top: 0.5rem; }
.pw-issues summary {
cursor: pointer; font-size: 0.85rem; color: #b45309; font-weight: 600;
padding: 0.5rem 0;
}
.pw-issues-body {
padding: 0.75rem; background: #fffbeb; border: 1px solid #fde68a;
border-radius: 6px; margin-top: 0.5rem;
}
.pw-field { display: block; font-size: 0.82rem; color: #475569; margin-bottom: 0.6rem; }
.pw-field input, .pw-field textarea, .pw-field select {
display: block; width: 100%; padding: 0.4rem 0.5rem;
border: 1px solid #cbd5e1; border-radius: 4px; font-size: 0.85rem; margin-top: 0.2rem;
}
.pw-field input[type="number"] { width: 80px; }
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
</style>
<script>
const yearEl = document.getElementById("pw-cpni-year")!;
const reportingYear = new Date().getFullYear() - 1;
yearEl.textContent = String(reportingYear);
const cleanBox = document.getElementById("pw-cpni-clean") as HTMLInputElement;
const issuesEl = document.getElementById("pw-cpni-issues") as HTMLDetailsElement;
// When "clean" is unchecked, auto-open issues
cleanBox.addEventListener("change", () => {
if (!cleanBox.checked) issuesEl.open = true;
});
// When issues are opened, uncheck clean
issuesEl.addEventListener("toggle", () => {
if (issuesEl.open && cleanBox.checked) {
// Only uncheck if they actually entered something
}
});
window.addEventListener("pw:step-next", (evt: any) => {
const PW = (window as any).PWIntake;
if (PW.steps[PW.get().step_index] !== "cpni_questions") return;
const errEl = document.getElementById("pw-cpni-err") as HTMLDivElement;
// Validate: if not clean, must have opened issues section
if (!cleanBox.checked && !issuesEl.open) {
errEl.hidden = false;
errEl.textContent = "Please either confirm clean compliance or expand the issues section and provide details.";
evt.preventDefault();
return;
}
errEl.hidden = true;
const g = (id: string) => (document.getElementById(id) as HTMLInputElement)?.value || "";
if (cleanBox.checked) {
// Clean compliance — simple case
PW.patchIntakeData({
cpni: {
reporting_year: reportingYear,
clean_compliance: true,
complaints: "no", complaints_count: 0,
breaches: "no", breaches_count: 0,
disciplinary: "no", disciplinary_count: 0,
data_brokers: "no",
marketing_usage: "no",
},
});
} else {
// Has issues — collect details
PW.patchIntakeData({
cpni: {
reporting_year: reportingYear,
clean_compliance: false,
complaints: parseInt(g("pw-cpni-complaints-count")) > 0 ? "yes" : "no",
complaints_count: parseInt(g("pw-cpni-complaints-count")) || 0,
complaints_description: g("pw-cpni-complaints-desc"),
breaches: parseInt(g("pw-cpni-breaches-count")) > 0 ? "yes" : "no",
breaches_count: parseInt(g("pw-cpni-breaches-count")) || 0,
breaches_description: g("pw-cpni-breaches-desc"),
disciplinary: parseInt(g("pw-cpni-disciplinary-count")) > 0 ? "yes" : "no",
disciplinary_count: parseInt(g("pw-cpni-disciplinary-count")) || 0,
data_brokers: g("pw-cpni-brokers-desc").trim() ? "yes" : "no",
data_brokers_description: g("pw-cpni-brokers-desc"),
marketing_usage: g("pw-cpni-marketing"),
},
});
}
});
</script>