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>
33 lines
13 KiB
JavaScript
33 lines
13 KiB
JavaScript
import"./hoisted.yFz1BYXO.js";const V=window.__PW_API||"https://api.performancewest.net",O="pw_admin_token",U="pw_admin_user",Y=document.getElementById("login-screen"),W=document.getElementById("dashboard-screen"),le=document.getElementById("login-form"),k=document.getElementById("login-error"),f=document.getElementById("login-btn"),ce=document.getElementById("logout-btn"),me=document.getElementById("refresh-btn"),G=document.getElementById("refresh-indicator"),ge=document.getElementById("admin-user"),ue=document.getElementById("stats-bar"),p=document.getElementById("orders-tbody"),S=document.getElementById("orders-loading"),x=document.getElementById("orders-empty"),K=document.getElementById("pagination"),pe=document.getElementById("pagination-info"),Q=document.getElementById("prev-btn"),X=document.getElementById("next-btn"),$=document.getElementById("detail-panel"),ye=document.getElementById("detail-title"),T=document.getElementById("detail-loading"),z=document.getElementById("detail-content"),be=document.getElementById("detail-close"),u=document.getElementById("action-feedback"),C=document.getElementById("filter-status"),P=document.getElementById("filter-automation"),N=document.getElementById("filter-priority"),M=document.getElementById("filter-assigned"),fe=document.getElementById("apply-filters-btn"),xe=document.getElementById("clear-filters-btn"),Z=document.getElementById("action-status"),ee=document.getElementById("action-automation"),te=document.getElementById("action-priority"),F=document.getElementById("action-note"),he=document.getElementById("action-assign"),H=document.getElementById("action-standalone-note"),ve=document.getElementById("action-add-note");let c=1;const h=25;let v=0,i=null,ne=Date.now(),j=null,D=null;function ae(){return localStorage.getItem(O)}function Ee(e,t){localStorage.setItem(O,e),localStorage.setItem(U,t)}function oe(){localStorage.removeItem(O),localStorage.removeItem(U)}function re(){return localStorage.getItem(U)||"admin"}async function y(e,t={}){const o=ae(),n={"Content-Type":"application/json",...t.headers||{}};o&&(n.Authorization=`Bearer ${o}`);const a=await fetch(`${V}${e}`,{...t,headers:n});if(a.status===401)throw oe(),q(),new Error("Session expired. Please log in again.");if(!a.ok){const d=await a.json().catch(()=>({}));throw new Error(d.error||d.message||`API error: ${a.status}`)}return a.json()}function s(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}function $e(e){return e?new Date(e).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):"—"}function Ie(e){return e?new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"—"}function Le(e){const t=Math.floor((Date.now()-e)/1e3);if(t<5)return"just now";if(t<60)return`${t}s ago`;const o=Math.floor(t/60);return o<60?`${o}m ago`:`${Math.floor(o/60)}h ${o%60}m ago`}const Be={received:"bg-gray-100 text-gray-700",processing:"bg-blue-100 text-blue-700",submitted:"bg-indigo-100 text-indigo-700",filed:"bg-green-100 text-green-700",delivered:"bg-emerald-100 text-emerald-800",cancelled:"bg-red-100 text-red-700"},we={pending:"bg-gray-100 text-gray-700",running:"bg-blue-100 text-blue-700",succeeded:"bg-green-100 text-green-700",failed:"bg-red-100 text-red-700",manual:"bg-amber-100 text-amber-700"},_e={low:"bg-gray-100 text-gray-600",normal:"bg-blue-100 text-blue-700",high:"bg-amber-100 text-amber-700",urgent:"bg-red-100 text-red-700"};function A(e,t){return`<span class="inline-block px-2 py-0.5 text-xs font-medium rounded-full ${t}">${s(e)}</span>`}function q(){Y.classList.remove("hidden"),W.classList.add("hidden"),$.classList.add("hidden"),i=null,j&&clearInterval(j),D&&clearInterval(D)}function se(){Y.classList.add("hidden"),W.classList.remove("hidden"),ge.textContent=re(),I(),g(),j=setInterval(()=>{I()},3e4),D=setInterval(()=>{G.textContent=`Last refreshed: ${Le(ne)}`},5e3)}le.addEventListener("submit",async e=>{e.preventDefault(),k.classList.add("hidden"),f.disabled=!0,f.textContent="Signing in...";const t=document.getElementById("login-username").value.trim(),o=document.getElementById("login-password").value;try{const n=await fetch(`${V}/api/v1/admin/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:o})});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||d.message||"Invalid credentials")}const a=await n.json();Ee(a.token,a.user?.username||t),se()}catch(n){k.textContent=n.message||"Login failed",k.classList.remove("hidden")}finally{f.disabled=!1,f.textContent="Sign In"}});ce.addEventListener("click",()=>{oe(),q()});async function I(){try{const e=await y("/api/v1/admin/stats");ne=Date.now(),G.textContent="Last refreshed: just now";const t=[{key:"received",label:"Received",color:"border-gray-300 bg-white"},{key:"processing",label:"Processing",color:"border-blue-300 bg-blue-50"},{key:"submitted",label:"Submitted",color:"border-indigo-300 bg-indigo-50"},{key:"filed",label:"Filed",color:"border-green-300 bg-green-50"},{key:"delivered",label:"Delivered",color:"border-emerald-300 bg-emerald-50"},{key:"automation_failed",label:"Auto Failed",color:"border-red-300 bg-red-50"},{key:"manual_required",label:"Manual Req",color:"border-amber-300 bg-amber-50"},{key:"urgent",label:"Urgent",color:"border-red-400 bg-red-50"},{key:"unassigned",label:"Unassigned",color:"border-red-300 bg-red-50"}];ue.innerHTML=t.map(o=>{const n=e[o.key]??0,a=["automation_failed","urgent","unassigned"].includes(o.key)&&n>0?"text-red-700 font-bold":["filed","delivered"].includes(o.key)?"text-green-700":["processing"].includes(o.key)?"text-blue-700":"text-gray-900";return`<div class="border rounded-lg px-3 py-2 text-center ${o.color}">
|
|
<div class="text-[10px] uppercase tracking-wider text-gray-500 font-medium">${o.label}</div>
|
|
<div class="text-lg font-bold ${a}">${n}</div>
|
|
</div>`}).join("")}catch(e){console.error("Failed to load stats:",e)}}async function g(){S.classList.remove("hidden"),x.classList.add("hidden"),p.innerHTML="",K.classList.add("hidden");const e=new URLSearchParams;e.set("limit",String(h)),e.set("offset",String((c-1)*h)),C.value&&e.set("status",C.value),P.value&&e.set("automation",P.value),N.value&&e.set("priority",N.value),M.value&&e.set("assigned",M.value);try{const t=await y(`/api/v1/admin/formations?${e.toString()}`);S.classList.add("hidden");const o=t.orders||t.formations||t.data||t.items||[];if(v=t.total??t.total_count??o.length,o.length===0){x.classList.remove("hidden");return}p.innerHTML=o.map(a=>{const d=A(a.status||"unknown",Be[a.status||""]||"bg-gray-100 text-gray-700"),w=A(a.automation_status||"pending",we[a.automation_status||""]||"bg-gray-100 text-gray-700"),b=A(a.priority||"normal",_e[a.priority||""]||"bg-blue-100 text-blue-700");return`<tr class="border-b border-gray-100 hover:bg-pw-50 cursor-pointer transition-colors" data-order-id="${s(a.id)}">
|
|
<td class="px-3 py-2 font-mono text-xs text-pw-700 font-medium">${s(String(a.order_number||a.id||"—"))}</td>
|
|
<td class="px-3 py-2 text-xs text-gray-500 whitespace-nowrap">${$e(a.created_at||"")}</td>
|
|
<td class="px-3 py-2 font-medium text-gray-900 max-w-[200px] truncate">${s(a.entity_name||"—")}</td>
|
|
<td class="px-3 py-2 text-xs text-gray-600">${s(a.state_code||a.state||"—")}</td>
|
|
<td class="px-3 py-2 text-xs text-gray-600">${s(a.entity_type||"—")}</td>
|
|
<td class="px-3 py-2">${d}</td>
|
|
<td class="px-3 py-2">${w}</td>
|
|
<td class="px-3 py-2">${b}</td>
|
|
<td class="px-3 py-2 text-xs text-gray-600">${s(a.assigned_name||a.assigned_username||"—")}</td>
|
|
<td class="px-3 py-2">
|
|
<button type="button" class="view-btn text-xs text-pw-600 hover:text-pw-800 font-medium" data-id="${s(a.id)}">View</button>
|
|
</td>
|
|
</tr>`}).join("");const n=Math.ceil(v/h);(n>1||v>h)&&(K.classList.remove("hidden"),pe.textContent=`Page ${c} of ${n} (${v} orders)`,Q.disabled=c<=1,X.disabled=c>=n),p.querySelectorAll("tr[data-order-id]").forEach(a=>{a.addEventListener("click",()=>{const d=a.dataset.orderId;d&&B(d)})})}catch(t){S.classList.add("hidden"),x.textContent=`Error loading orders: ${t.message}`,x.classList.remove("hidden")}}Q.addEventListener("click",()=>{c>1&&(c--,g())});X.addEventListener("click",()=>{c++,g()});fe.addEventListener("click",()=>{c=1,g()});xe.addEventListener("click",()=>{C.value="",P.value="",N.value="",M.value="",c=1,g()});me.addEventListener("click",()=>{I(),g(),i&&B(i)});async function B(e){i=e,$.classList.remove("hidden"),T.classList.remove("hidden"),z.classList.add("hidden"),u.classList.add("hidden"),$.scrollIntoView({behavior:"smooth",block:"start"}),p.querySelectorAll("tr").forEach(o=>{o.classList.toggle("bg-pw-100",o.dataset.orderId===e),o.classList.toggle("hover:bg-pw-50",o.dataset.orderId!==e)});try{let o=function(r){return!r||Object.keys(r).length===0?"<em class='text-gray-400'>Not provided</em>":[r.street||r.line1,r.city,r.state,r.zip||r.postal_code].filter(Boolean).map(s).join(", ")};var t=o;const n=await y(`/api/v1/admin/formations/${e}`);T.classList.add("hidden"),z.classList.remove("hidden"),ye.textContent=`Order ${n.order_number||n.id?.slice(0,8)||e} — ${n.entity_name||"Unknown Entity"}`,Z.value=n.status||"received",ee.value=n.automation_status||"pending",te.value=n.priority||"normal",F.value="",H.value="";const a=n.customer||{};document.getElementById("detail-customer").innerHTML=`
|
|
<p><strong>Name:</strong> ${s(a.name||n.customer_name||"—")}</p>
|
|
<p><strong>Email:</strong> ${s(a.email||n.customer_email||"—")}</p>
|
|
<p><strong>Phone:</strong> ${s(a.phone||n.customer_phone||"—")}</p>
|
|
`,document.getElementById("detail-entity").innerHTML=`
|
|
<p><strong>Name:</strong> ${s(n.entity_name||"—")}</p>
|
|
<p><strong>Type:</strong> ${s(n.entity_type||"—")}</p>
|
|
<p><strong>State:</strong> ${s(n.state||"—")}</p>
|
|
<p><strong>Purpose:</strong> ${s(n.purpose||n.entity?.purpose||"—")}</p>
|
|
`;const d=n.principal_address||n.principal||{},w=n.mailing_address||n.mailing||{};document.getElementById("detail-addresses").innerHTML=`
|
|
<p><strong>Principal:</strong> ${o(d)}</p>
|
|
<p><strong>Mailing:</strong> ${o(w)}</p>
|
|
`;const b=n.members||[];document.getElementById("detail-members").innerHTML=b.length===0?"<em class='text-gray-400'>None listed</em>":b.map((r,_)=>`<p><strong>${r.role||`Member ${_+1}`}:</strong> ${s(r.name||"—")} ${r.title?`(${s(r.title)})`:""}</p>`).join("");const R=n.addons||[],m=n.pricing||{};let l="";m.base_price!=null&&(l+=`<p><strong>Base Price:</strong> $${Number(m.base_price).toFixed(2)}</p>`),m.state_fee!=null&&(l+=`<p><strong>State Fee:</strong> $${Number(m.state_fee).toFixed(2)}</p>`),R.length>0&&(l+='<p><strong>Add-Ons:</strong></p><ul class="ml-4 list-disc">',R.forEach(r=>{l+=`<li>${s(r.name||r.label||"Add-on")}${r.price!=null?` — $${Number(r.price).toFixed(2)}`:""}</li>`}),l+="</ul>"),m.total!=null&&(l+=`<p class="font-semibold mt-1"><strong>Total:</strong> $${Number(m.total).toFixed(2)}</p>`),m.stripe_payment_id&&(l+=`<p class="text-xs text-gray-400 mt-1">Stripe: ${s(m.stripe_payment_id)}</p>`),l||(l="<em class='text-gray-400'>No pricing data</em>"),document.getElementById("detail-pricing").innerHTML=l;const de=n.audit_log||[],ie=n.notes||[],J=[...de.map(r=>({ts:r.timestamp||"",text:`[${r.action||"action"}] ${r.details||""}`,user:r.user||""})),...ie.map(r=>({ts:r.timestamp||"",text:r.text||"",user:r.user||""}))].sort((r,_)=>new Date(_.ts).getTime()-new Date(r.ts).getTime());document.getElementById("detail-audit").innerHTML=J.length===0?"<em class='text-gray-400'>No audit entries</em>":J.map(r=>`<div class="flex gap-2 py-1 border-b border-gray-100 last:border-0">
|
|
<span class="text-[10px] text-gray-400 whitespace-nowrap shrink-0">${Ie(r.ts)}</span>
|
|
<span class="text-[10px] text-gray-500 shrink-0">${s(r.user)}</span>
|
|
<span class="text-xs text-gray-700">${s(r.text)}</span>
|
|
</div>`).join("")}catch(o){T.innerHTML=`<span class="text-red-500">Error: ${s(o.message)}</span>`}}be.addEventListener("click",()=>{$.classList.add("hidden"),i=null,p.querySelectorAll("tr").forEach(e=>{e.classList.remove("bg-pw-100"),e.classList.add("hover:bg-pw-50")})});function L(e,t=!1){u.textContent=e,u.className=`text-xs rounded px-3 py-2 ${t?"bg-red-50 text-red-700 border border-red-200":"bg-green-50 text-green-700 border border-green-200"}`,u.classList.remove("hidden"),setTimeout(()=>u.classList.add("hidden"),4e3)}async function E(e){if(!i)return;const t=F.value.trim(),o={...e};t&&(o.note=t);try{await y(`/api/v1/admin/formations/${i}`,{method:"PATCH",body:JSON.stringify(o)}),L("Updated successfully"),F.value="",B(i),g(),I()}catch(n){L(n.message||"Update failed",!0)}}document.querySelectorAll(".action-save-btn").forEach(e=>{e.addEventListener("click",()=>{const t=e.dataset.action;t==="status"?E({status:Z.value}):t==="automation"?E({automation_status:ee.value}):t==="priority"&&E({priority:te.value})})});he.addEventListener("click",()=>{E({assigned_to:re()})});ve.addEventListener("click",async()=>{if(!i)return;const e=H.value.trim();if(e)try{await y(`/api/v1/admin/formations/${i}`,{method:"PATCH",body:JSON.stringify({note:e})}),L("Note added"),H.value="",B(i)}catch(t){L(t.message||"Failed to add note",!0)}});ae()?se():q();
|