PageSourceSearch

https://filewisely.lovable.app/assets/PortalTriage-ClxUnPlJ.js

js filewisely.lovable.app collected 2026-10-03 03:32:58 UTC 9,872 bytes, 1 lines download raw bytes

1import{j as s}from"./vendor-radix-BMN_-y0O.js";import{e as X,r}from"./vendor-react-LwREbmHq.js";import{c as Y,s as l,b as m,B as u,Q as Z}from"./index-vfr963b2.js";import{C as b,a as j,c as w,b as k,d as ee}from"./card-D4x0WM4V.js";import{B as x}from"./badge-CP4NROTX.js";import{C as se}from"./checkbox-D6lsrQY5.js";import{L as z,R as te,b3 as ne,$ as ae,B as ie}from"./vendor-icons-B40_8yDG.js";import"./vendor-data-DUXRAtvF.js";import"./vendor-date-DYQ7tMgT.js";const C={active:"Still moving",chase_money:"Money still owed",vehicle_still_here:"Vehicle never picked up",close_now:"Safe to close",review:"Needs a decision",never_checked_in:"Never checked in",check_in_unknown:"Check-in unknown"},re={active:"border-success/40 bg-success/10 text-success",chase_money:"border-warning/40 bg-warning/10 text-warning",vehicle_still_here:"border-warning/40 bg-warning/10 text-warning",close_now:"border-border bg-secondary text-secondary-foreground",review:"border-border bg-muted text-muted-foreground",never_checked_in:"border-border bg-muted text-muted-foreground",check_in_unknown:"border-border bg-muted text-muted-foreground"},$=["active","review","chase_money","vehicle_still_here","close_now"],P=["never_checked_in","check_in_unknown"],oe=[...$,...P],ce=["close_now","review","never_checked_in"],le={staff:"Staff walkaround",staff_manual:"Manually confirmed",kiosk:"Kiosk",crew_app:"Crew app",import_verified:"Drop-off + pickup on record",system_migration:"Migrated record"};function U(a){return a<1?"today":a===1?"1 day":a<60?`${a} days`:`${Math.round(a/30)} months`}function A(a){return[a.repair_order_number?`RO ${a.repair_order_number}`:null,a.customer_name].filter(Boolean).join(" · ")||"Unnamed job"}const de=a=>Number(a||0).toLocaleString("en-US",{style:"currency",currency:"USD"});function je(){const a=X(),{currentBusiness:v}=Y(),d=v?.id,[_,T]=r.useState([]),[R,M]=r.useState([]),[q,O]=r.useState(!0),[E,B]=r.useState(!1),[i,p]=r.useState(new Set),[g,K]=r.useState("active"),h=r.useCallback(async()=>{if(d){O(!0);try{const[e,t]=await Promise.all([l.from("v_portal_staleness").select("*").eq("business_id",d).order("idle_days",{ascending:!1}).limit(1e3),l.from("portal_triage_actions").select("id, intake_link_id, reason, idle_days, created_at, undone_at, previous_state").eq("business_id",d).order("created_at",{ascending:!1}).limit(25)]);if(e.error)throw e.error;T(e.data||[]),M(t.data||[])}catch(e){m.error("Could not load portal triage",{description:e?.message})}finally{O(!1)}}},[d]);r.useEffect(()=>{h()},[h]);const N=r.useMemo(()=>{const e=new Map;for(const t of oe)e.set(t,[]);for(const t of _)e.get(t.disposition)?.push(t);return e},[_]),o=N.get(g)||[],y=ce.includes(g),f=_.filter(e=>e.is_open_job),V=_.filter(e=>!e.is_open_job),L=f.filter(e=>e.disposition!=="active").length,H=f.length?Math.round(L/f.length*100):0,Q=e=>p(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n}),G=()=>{const e=o.every(t=>i.has(t.intake_link_id));p(e?new Set:new Set(o.map(t=>t.intake_link_id)))},F=async()=>{if(!d||i.size===0)return;const e=o.filter(n=>i.has(n.intake_link_id)),t=e.filter(n=>Number(n.balance||0)>0);if(t.length){m.error(`${t.length} of those still owe money — refresh and try again`);return}B(!0);try{const n=new Date().toISOString(),{data:S}=await l.auth.getUser(),W=S?.user?.id??null,{error:I}=await l.from("business_intake_links").update({closed_at:n,status:"completed"}).in("id",e.map(c=>c.intake_link_id));if(I)throw I;const{error:D}=await l.from("portal_triage_actions").insert(e.map(c=>({business_id:d,policy_share_id:c.policy_share_id,intake_link_id:c.intake_link_id,action:"close",reason:c.disposition_reason,idle_days:c.idle_days,balance_at_action:c.balance??0,previous_state:{closed_at:null,status:c.link_status},created_by:W})));if(D)throw D;m.success(`Closed ${e.length} stale job${e.length===1?"":"s"}`),p(new Set),await h()}catch(n){m.error("Close failed",{description:n?.message})}finally{B(!1)}},J=async e=>{if(e.intake_link_id)try{const t=e.previous_state||{},{error:n}=await l.from("business_intake_links").update({closed_at:null,status:t.status||"active"}).eq("id",e.intake_link_id);if(n)throw n;const{data:S}=await l.auth.getUser();await l.from("portal_triage_actions").update({undone_at:new Date().toISOString(),und
1one_by:S?.user?.id??null}).eq("id",e.id),m.success("Reopened"),await h()}catch(t){m.error("Undo failed",{description:t?.message})}};return q?s.jsx("div",{className:"flex h-64 items-center justify-center",children:s.jsx(z,{className:"h-5 w-5 animate-spin text-muted-foreground"})}):s.jsxs("div",{className:"mx-auto w-full max-w-5xl space-y-6 p-4 md:p-6",children:[s.jsxs("header",{className:"flex flex-wrap items-start justify-between gap-3",children:[s.jsxs("div",{children:[s.jsx("h1",{className:"text-2xl font-semibold tracking-tight",children:"Portal triage"}),s.jsx("p",{className:"mt-1 text-sm text-muted-foreground",children:"Rule #1: no check-in, no open repair. A job only counts as open once the vehicle was verifiably checked in. Everything else is a lead, an estimate or an appointment — it never belonged in your open count, your cycle time or Theo's answers."}),s.jsx("p",{className:"mt-1 text-xs text-muted-foreground",children:"Runs itself nightly: delivered-and-paid jobs close, and estimates that never became a job — no vehicle, no money, 90+ days quiet — expire. Anything owing money or with a car on the lot is left alone."})]}),s.jsxs(u,{variant:"outline",size:"sm",onClick:()=>void h(),children:[s.jsx(te,{className:"mr-2 h-4 w-4"}),"Refresh"]})]}),s.jsxs("div",{className:"grid gap-3 sm:grid-cols-3",children:[s.jsx(b,{children:s.jsxs(j,{className:"pb-2",children:[s.jsx(w,{children:"Open jobs (checked in)"}),s.jsx(k,{className:"text-3xl",children:f.length}),s.jsxs("p",{className:"text-xs text-muted-foreground",children:[V.length," records were never checked in"]})]})}),s.jsx(b,{children:s.jsxs(j,{className:"pb-2",children:[s.jsx(w,{children:"Idle 30+ days"}),s.jsx(k,{className:"text-3xl text-warning",children:L}),s.jsxs("p",{className:"text-xs text-muted-foreground",children:[H,"% of the open jobs"]})]})}),s.jsx(b,{children:s.jsxs(j,{className:"pb-2",children:[s.jsx(w,{children:"Safe to close right now"}),s.jsx(k,{className:"text-3xl",children:(N.get("close_now")||[]).length}),s.jsx("p",{className:"text-xs text-muted-foreground",children:"No activity in 4+ months, nothing owed"})]})})]}),[{title:"Open portals",hint:"Vehicle checked in",order:$},{title:"Non-open records",hint:"No canonical check-in event",order:P}].map(e=>s.jsxs("div",{className:"space-y-2",children:[s.jsxs("div",{className:"flex items-baseline gap-2",children:[s.jsx("h2",{className:"text-sm font-semibold",children:e.title}),s.jsx("span",{className:"text-xs text-muted-foreground",children:e.hint})]}),s.jsx("div",{className:"flex flex-wrap gap-2",children:e.order.map(t=>{const n=(N.get(t)||[]).length;return s.jsxs(u,{size:"sm",variant:g===t?"default":"outline",onClick:()=>{K(t),p(new Set)},children:[C[t],s.jsx(x,{variant:"secondary",className:"ml-2",children:n})]},t)})})]},e.title)),y&&o.length>0&&s.jsxs("div",{className:"flex flex-wrap items-center gap-3 rounded-lg border border-border bg-muted/40 p-3",children:[s.jsx(u,{variant:"ghost",size:"sm",onClick:G,children:o.every(e=>i.has(e.intake_link_id))?"Clear all":"Select all"}),s.jsxs("span",{className:"text-sm text-muted-foreground",children:[i.size," selected"]}),s.jsxs(u,{size:"sm",className:"ml-auto",disabled:i.size===0||E,onClick:()=>void F(),children:[E?s.jsx(z,{className:"mr-2 h-4 w-4 animate-spin"}):s.jsx(ne,{className:"mr-2 h-4 w-4"}),"Close ",i.size||""," job",i.size===1?"":"s"]})]}),!y&&o.length>0&&s.jsxs("p",{className:"rounded-lg border border-warning/40 bg-warning/10 p-3 text-sm text-warning",children:["These can't be bulk-closed. ",C[g]," — settle it on the job first."]}),s.jsxs("div",{className:"space-y-2",children:[o.length===0&&s.jsx("p",{className:"rounded-lg border border-border p-6 text-center text-sm text-muted-foreground",children:"Nothing in this bucket."}),o.map(e=>s.jsxs("div",{className:"flex items-center gap-3 rounded-lg border border-border bg-card p-3",children:[y&&s.jsx(se,{checked:i.has(e.intake_link_id),onCheckedChange:()=>Q(e.intake_link_id),"aria-label":`Select ${A(e)}`}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[s.jsx("span",{className:"truncate font-medium",children:A(e)}),s.jsx(x,{variant:"outline",className:Z("text-[10px]",re[e.disposition]),children:C[e.disposition]}),Number(e.balance||0)>0&&s.jsxs(x,{variant:"outline",className:"text-[10px]",children:[de(e.balance)," owed"]}),e.checked_in&&e.check_in_source&&s.jsx(x,{variant:"outline",className:"text-[10px]",children:le[e.check_in_source]||e.check_in_source})]}),s.jsxs("p",{className:"mt-0.5 text-xs text-muted-foreground",children:["Quiet for ",U(e.idle_days)," · ",e.disposition_reason]})]}),e.policy_share_id&&s.jsxs(u,{variant:"ghost",size:"sm",onClick:()=>a(`/business/portal/${e.policy_share_id}`),children:["Open ",s.jsx(ae,{className:"ml-1 h-3.5 w-3.5"})]})]},e.intake_link_id))]}),R.length>0&&s.jsxs(b,{children:[s.jsxs(j,{className:"pb-2",children:[s.jsx(k,{className:"text-base",children:"Recent triage"}),s.jsx(w,{children:"Every close is logged and reversible."})]}),s.jsx(ee,{className:"space-y-2",children:R.map(e=>s.jsxs("div",{className:"flex items-center gap-3 text-sm",children:[s.jsxs("span",{className:"text-muted-foreground",children:[new Date(e.created_at).toLocaleDateString()," · closed after"," ",U(e.idle_days||0)," quiet"]}),e.undone_at?s.jsx(x,{variant:"outline",className:"ml-auto text-[10px]",children:"Reo
1pened"}):s.jsxs(u,{variant:"ghost",size:"sm",className:"ml-auto",onClick:()=>void J(e),children:[s.jsx(ie,{className:"mr-1 h-3.5 w-3.5"})," Undo"]})]},e.id))})]})]})}export{je as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.