feat(fulfillment): state-trucking intake form + hazmat/emissions products

- Add StateTruckingIntakeStep.astro with slug-gated sections (IRP/IFTA,
  emissions, intrastate authority, OSOW, hazmat/PHMSA); wired into Wizard
- Register hazmat-phmsa + state-emissions products & SERVICE_INFO
- Add server-side bundle/mutual-exclusion enforcement + REQUIRED_FIELDS
- State-trucking slugs now collect real intake data (were review-only)
- Surface slug-specific intake fields in admin todo (_summarize_intake)
- Remove state slugs from email ADMIN_ASSISTED set (now get intake links)
This commit is contained in:
justin 2026-06-02 03:27:51 -05:00
parent 71b888f993
commit 9c6b8d95e0
3 changed files with 703 additions and 0 deletions

View file

@ -0,0 +1,296 @@
---
// State-level trucking + hazmat/emissions intake. Slug-gated sections, mirroring
// DOTIntakeStep.astro. Collects the carrier identity plus the specific data each
// state filing / hazmat / emissions handler needs.
---
<div class="pw-step" data-slug="state-trucking">
<h2>Filing Information</h2>
<p class="pw-help">
Provide your carrier details so we can prepare your state filing(s). We file on your behalf.
</p>
<div class="pw-form-grid">
<!-- ═══ Carrier Identity (always shown) ═══ -->
<h3>Carrier Information</h3>
<div class="pw-row">
<label class="pw-field"><span>Legal Entity Name <em>*</em></span>
<input type="text" id="st-legal-name" required placeholder="As registered with FMCSA" /></label>
</div>
<div class="pw-row-3">
<label class="pw-field"><span>USDOT Number <em>*</em></span>
<input type="text" id="st-dot" required placeholder="e.g. 1234567" /></label>
<label class="pw-field"><span>MC/MX/FF Number</span>
<input type="text" id="st-mc" placeholder="e.g. MC-123456" /></label>
<label class="pw-field"><span>Email <em>*</em></span>
<input type="email" id="st-email" required placeholder="you@company.com" /></label>
</div>
<div class="pw-row-2">
<label class="pw-field"><span>Base State <em>*</em></span>
<select id="st-base-state" required>
<option value="">--</option>
<option>AL</option><option>AK</option><option>AZ</option><option>AR</option><option>CA</option><option>CO</option><option>CT</option><option>DE</option><option>FL</option><option>GA</option><option>HI</option><option>ID</option><option>IL</option><option>IN</option><option>IA</option><option>KS</option><option>KY</option><option>LA</option><option>ME</option><option>MD</option><option>MA</option><option>MI</option><option>MN</option><option>MS</option><option>MO</option><option>MT</option><option>NE</option><option>NV</option><option>NH</option><option>NJ</option><option>NM</option><option>NY</option><option>NC</option><option>ND</option><option>OH</option><option>OK</option><option>OR</option><option>PA</option><option>RI</option><option>SC</option><option>SD</option><option>TN</option><option>TX</option><option>UT</option><option>VT</option><option>VA</option><option>WA</option><option>WV</option><option>WI</option><option>WY</option><option>DC</option>
</select></label>
<label class="pw-field"><span>Power Units (trucks) <em>*</em></span>
<input type="number" id="st-power-units" min="0" placeholder="e.g. 5" /></label>
</div>
<!-- ═══ IRP / IFTA (interstate registration + fuel tax) ═══ -->
<div id="st-sec-irp-ifta" hidden>
<h3>Interstate Operations</h3>
<div class="pw-row-2">
<label class="pw-field"><span>Primary Fuel Type</span>
<select id="st-fuel-type">
<option value="">Select...</option>
<option value="diesel">Diesel</option>
<option value="gasoline">Gasoline</option>
<option value="propane">Propane / LPG</option>
<option value="cng">CNG / LNG</option>
<option value="electric">Electric</option>
<option value="other">Other</option>
</select></label>
<label class="pw-field"><span>Gross Weight Bracket</span>
<select id="st-gross-weight">
<option value="">Select...</option>
<option value="under_26k">Under 26,000 lbs</option>
<option value="26k_to_80k">26,00180,000 lbs</option>
<option value="over_80k">Over 80,000 lbs</option>
</select></label>
</div>
<div class="pw-row">
<label class="pw-field"><span>Operating States (besides base) <em>*</em></span>
<input type="text" id="st-operating-states" placeholder="e.g. CA, NV, AZ, OR (comma-separated)" /></label>
</div>
<p class="pw-field-help">After your order, we'll send a short follow-up form to collect each vehicle's VIN, plate, and registered weight for the apportioned/IFTA filing.</p>
</div>
<!-- ═══ CA MCP + CARB / Emissions ═══ -->
<div id="st-sec-emissions" hidden>
<h3>Fleet Emissions Profile</h3>
<div class="pw-row-2">
<label class="pw-field"><span>CA Number (if any)</span>
<input type="text" id="st-ca-number" placeholder="CHP CA# if already issued" /></label>
<label class="pw-field"><span>Oldest Engine Model Year</span>
<input type="number" id="st-engine-year" min="1990" max="2030" placeholder="e.g. 2014" /></label>
</div>
<p class="pw-field-help">Clean-truck programs (CARB Clean Truck Check, NY/CO/MD/NJ/MA Advanced Clean Trucks) phase out older engines. We'll review your fleet's model years against your states' thresholds.</p>
</div>
<!-- ═══ Intrastate Authority ═══ -->
<div id="st-sec-intrastate" hidden>
<h3>Intrastate Operating Authority</h3>
<div class="pw-row-2">
<label class="pw-field"><span>Authority Type <em>*</em></span>
<select id="st-authority-type">
<option value="">Select...</option>
<option value="common">Common Carrier (COA)</option>
<option value="contract">Contract Carrier</option>
<option value="cpcn">Certificate of Public Convenience &amp; Necessity</option>
<option value="household_goods">Household Goods Mover</option>
<option value="unknown">Not sure — please advise</option>
</select></label>
<label class="pw-field"><span>BOC-3 already on file?</span>
<select id="st-boc3">
<option value="">Select...</option>
<option value="yes">Yes</option>
<option value="no">No</option>
<option value="unknown">Not sure</option>
</select></label>
</div>
<div class="pw-row-2">
<label class="pw-field"><span>Insurance Carrier</span>
<input type="text" id="st-ins-carrier" placeholder="Your liability insurer" /></label>
<label class="pw-field"><span>Insurance Policy #</span>
<input type="text" id="st-ins-policy" placeholder="Policy number" /></label>
</div>
</div>
<!-- ═══ OSOW Permit ═══ -->
<div id="st-sec-osow" hidden>
<h3>Oversize / Overweight Load</h3>
<div class="pw-row-2">
<label class="pw-field"><span>Load Dimensions (L×W×H)</span>
<input type="text" id="st-load-dims" placeholder="e.g. 75ft × 12ft × 14ft" /></label>
<label class="pw-field"><span>Gross Load Weight (lbs)</span>
<input type="number" id="st-load-weight" min="0" placeholder="e.g. 120000" /></label>
</div>
</div>
<!-- ═══ Hazmat / PHMSA ═══ -->
<div id="st-sec-hazmat" hidden>
<h3>Hazmat Profile (PHMSA Registration)</h3>
<p class="pw-field-help">PHMSA registration is required for carriers transporting placardable quantities of hazardous materials (49 CFR Part 107).</p>
<div class="pw-hazmat-grid">
<label><input type="checkbox" data-hazmat="class1" /> Class 1 — Explosives</label>
<label><input type="checkbox" data-hazmat="class2" /> Class 2 — Gases</label>
<label><input type="checkbox" data-hazmat="class3" /> Class 3 — Flammable Liquids</label>
<label><input type="checkbox" data-hazmat="class4" /> Class 4 — Flammable Solids</label>
<label><input type="checkbox" data-hazmat="class5" /> Class 5 — Oxidizers</label>
<label><input type="checkbox" data-hazmat="class6" /> Class 6 — Toxic/Infectious</label>
<label><input type="checkbox" data-hazmat="class7" /> Class 7 — Radioactive</label>
<label><input type="checkbox" data-hazmat="class8" /> Class 8 — Corrosives</label>
<label><input type="checkbox" data-hazmat="class9" /> Class 9 — Misc.</label>
</div>
<div class="pw-row-2" style="margin-top:0.75rem">
<label class="pw-field"><span>Transport in bulk packaging?</span>
<select id="st-bulk">
<option value="">Select...</option>
<option value="no">No</option>
<option value="yes">Yes</option>
</select></label>
<label class="pw-field"><span>Small business? (under SBA size standard)</span>
<select id="st-small-biz">
<option value="">Select...</option>
<option value="yes">Yes (lower PHMSA fee)</option>
<option value="no">No</option>
</select></label>
</div>
</div>
</div>
<div id="pw-st-errors" class="pw-err" hidden></div>
</div>
<style>
.pw-step h2 { margin: 0 0 0.5rem; color: #1a2744; }
.pw-step h3 { color: #1a2744; margin: 1.25rem 0 0.5rem; font-size: 0.95rem; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3rem; }
.pw-help { color: #64748b; font-size: 0.9rem; margin-bottom: 1rem; }
.pw-form-grid { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem 1.25rem; }
.pw-row, .pw-row-2, .pw-row-3 { margin-bottom: 0.75rem; }
.pw-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.pw-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.75rem; }
.pw-field { display: flex; flex-direction: column; gap: 0.2rem; }
.pw-field span { font-size: 0.8rem; font-weight: 600; color: #374151; }
.pw-field em { color: #dc2626; font-style: normal; }
.pw-field input, .pw-field select { padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; }
.pw-field input:focus, .pw-field select:focus { outline: none; border-color: #f97316; box-shadow: 0 0 0 2px rgba(249,115,22,0.2); }
.pw-field-help { font-size: 0.85rem; color: #64748b; margin: 0.25rem 0 0.5rem; }
.pw-hazmat-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.3rem 1rem; font-size: 0.85rem; color: #374151; }
.pw-hazmat-grid label { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; }
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; background: #fee2e2; padding: 0.5rem 0.75rem; border-radius: 6px; }
@media (max-width: 640px) { .pw-row-2, .pw-row-3 { grid-template-columns: 1fr; } .pw-hazmat-grid { grid-template-columns: 1fr 1fr; } }
</style>
<script is:inline>
if (!document.querySelector('[data-slug="state-trucking"], [data-step="state-trucking"]')) {
// Not a state-trucking intake page — skip
} else {
// Which sections to show per slug.
const ST_SECTIONS = {
"irp-registration": ["st-sec-irp-ifta"],
"ifta-application": ["st-sec-irp-ifta"],
"ifta-quarterly": ["st-sec-irp-ifta"],
"or-weight-mile-tax": ["st-sec-irp-ifta"],
"ny-hut-registration": ["st-sec-irp-ifta"],
"ky-kyu-registration": ["st-sec-irp-ifta"],
"nm-weight-distance": ["st-sec-irp-ifta"],
"ct-highway-use-fee": ["st-sec-irp-ifta"],
"ca-mcp-carb": ["st-sec-emissions"],
"state-emissions": ["st-sec-emissions"],
"state-dot-registration":[],
"intrastate-authority": ["st-sec-intrastate"],
"osow-permit": ["st-sec-osow"],
"state-trucking-bundle": ["st-sec-irp-ifta", "st-sec-emissions", "st-sec-intrastate"],
"hazmat-phmsa": ["st-sec-hazmat"],
};
const ALL_SECTIONS = ["st-sec-irp-ifta","st-sec-emissions","st-sec-intrastate","st-sec-osow","st-sec-hazmat"];
function showSections() {
const PW = window.PWIntake;
const state = PW?.get?.() || {};
const wizardEl = document.querySelector(".pw-wizard[data-service]");
const pageSlug = wizardEl?.getAttribute("data-service") || "";
const slugs = state.batch_slugs || [pageSlug || state.service_slug || ""];
const show = new Set();
for (const slug of slugs) for (const sec of (ST_SECTIONS[slug] || [])) show.add(sec);
for (const id of ALL_SECTIONS) {
const el = document.getElementById(id);
if (el) el.hidden = !show.has(id);
}
}
function initSections() {
const wizardEl = document.querySelector(".pw-wizard[data-service]");
if (!wizardEl) { setTimeout(initSections, 100); return; }
showSections();
}
initSections();
window.addEventListener("pw:step-shown", (evt) => {
if (evt.detail.step !== "state-trucking") return;
showSections();
const s = window.PWIntake.get();
const d = s.intake_data || {};
const map = {
"st-legal-name": d.legal_name || d.entity_name || "", "st-dot": d.dot_number || "",
"st-mc": d.mc_number || "", "st-email": d.email || s.email || "",
"st-base-state": d.base_state || d.address_state || "", "st-power-units": d.power_units || "",
"st-fuel-type": d.fuel_type || "", "st-gross-weight": d.gross_weight_bracket || "",
"st-operating-states": Array.isArray(d.operating_states) ? d.operating_states.join(", ") : (d.operating_states || ""),
"st-ca-number": d.ca_number || "", "st-engine-year": d.engine_model_years || "",
"st-authority-type": d.authority_type || "", "st-boc3": d.boc3_on_file || "",
"st-ins-carrier": d.insurance_carrier || "", "st-ins-policy": d.insurance_policy || "",
"st-load-dims": d.load_dimensions || "", "st-load-weight": d.load_weight || "",
"st-bulk": d.bulk_packaging || "", "st-small-biz": d.small_business || "",
};
for (const [id, val] of Object.entries(map)) {
const el = document.getElementById(id);
if (el && val) el.value = val;
}
});
window.addEventListener("pw:step-next", (evt) => {
const PW = window.PWIntake;
if (PW.steps[PW.get().step_index] !== "state-trucking") return;
const errDiv = document.getElementById("pw-st-errors");
errDiv.hidden = true;
const val = (id) => (document.getElementById(id))?.value?.trim() || "";
// Base required (always)
const required = ["st-legal-name","st-dot","st-email","st-base-state","st-power-units"];
const missing = [];
for (const id of required) {
const el = document.getElementById(id);
if (!el || !el.value.trim()) missing.push(el?.parentElement?.querySelector("span")?.textContent || id);
}
// Section-specific required
const sectionRequired = {
"st-sec-irp-ifta": [["st-operating-states","Operating States"]],
"st-sec-intrastate": [["st-authority-type","Authority Type"]],
};
for (const [secId, fields] of Object.entries(sectionRequired)) {
const sec = document.getElementById(secId);
if (sec && !sec.hidden) {
for (const [id, label] of fields) {
if (!val(id)) missing.push(label);
}
}
}
// Hazmat: at least one class
const hazSec = document.getElementById("st-sec-hazmat");
let hazmatClasses = [];
document.querySelectorAll("[data-hazmat]").forEach((cb) => { if (cb.checked) hazmatClasses.push(cb.dataset.hazmat); });
if (hazSec && !hazSec.hidden && hazmatClasses.length === 0) {
missing.push("At least one hazmat class");
}
if (missing.length) { evt.preventDefault(); errDiv.hidden = false; errDiv.textContent = "Please fill in: " + missing.join(", "); return; }
const opStates = val("st-operating-states").split(",").map(s => s.trim().toUpperCase()).filter(Boolean);
const state = PW.get();
PW.set({ ...state, intake_data: { ...state.intake_data,
legal_name: val("st-legal-name"), entity_name: val("st-legal-name"),
dot_number: val("st-dot"), mc_number: val("st-mc"), email: val("st-email"),
base_state: val("st-base-state"), power_units: val("st-power-units"),
fuel_type: val("st-fuel-type"), gross_weight_bracket: val("st-gross-weight"),
operating_states: opStates,
ca_number: val("st-ca-number"), engine_model_years: val("st-engine-year"),
authority_type: val("st-authority-type"), boc3_on_file: val("st-boc3"),
insurance_carrier: val("st-ins-carrier"), insurance_policy: val("st-ins-policy"),
load_dimensions: val("st-load-dims"), load_weight: val("st-load-weight"),
hazmat_classes: hazmatClasses, bulk_packaging: val("st-bulk"), small_business: val("st-small-biz") === "yes",
}});
});
} // end guard
</script>