1import{v as P,w as F,x as q,c as J,b as Y,h as Z,N as ee,r as m,t as d,s as y,j as e,B as l,C as se,n as b,I as _}from"./index-BKuXz_lx.js";import{B as te}from"./back-link-Cu1gDYAu.js";import{u as Q}from"./useQuery-Bz14us_N.js";import{u as v}from"./useMutation-BF1Hdgha.js";import{u as S}from"./useServerFn-DruDSVLX.js";import{D as O,a as B,b as $,c as V,d as U,e as H}from"./dialog-G5RKiVzh.js";import{S as z,a as E,b as R,c as L,d as A}from"./select-wMiXnjQh.js";import{T as ae}from"./tags-iBYDbvIE.js";import{S as M}from"./sparkles-bm4VxieG.js";import{L as re}from"./loader-circle-CBLTRqHC.js";import{P as I}from"./pencil-BSe6lC4f.js";import{X as ne}from"./x-DVmJ5vLc.js";import{P as ie}from"./plus-3PGhWy5v.js";import{T as ce}from"./trash-2-BbqMbnun.js";import"./arrow-left-CjswZ4jf.js";import"./index-CJgWmXI4.js";import"./chevron-down-C-0pMlkS.js";const le=P({method:"POST"}).middleware([F]).handler(q("5c2187194b04559b64831c82ac59088cfc8139924adb93c88914ebda95c5e4ba")),oe=P({method:"POST"}).middleware([F]).handler(q("10f376ca1283d609b594023b62579758d6a5304a96b824c2b608fa3f8436e6d7")),de=P({method:"POST"}).middleware([F]).handler(q("8d46f85086301cc64ed019f0db533d9bde648cf8ece38c3742557d2b6dbd5b8b")),ze=J("/_authenticated/_admin/merchant-rules")({component:me}),T=["dining","groceries","travel","hotel","airline","transit","rideshare","gas","entertainment","streaming","fitness","drugstore","utilities","telecom","furnishings","online_retail","wholesale_club","office_supply","home_improvement","department_store","other"];function w(t){return t.replace(/_/g," ").replace(/\b\w/g,i=>i.toUpperCase())}function me(){const{t}=Y(),i=Z(),u=ee(),[p,h]=m.useState(null),[o,j]=m.useState(!1),[c,x]=m.useState(null),r=S(le),a=S(oe),g=S(de),{data:k=[],isLoading:W}=Q({queryKey:["admin-merchant-rules"],queryFn:async()=>{const{data:s,error:n}=await y.from("merchant_rules").select("id, merchant_pattern, bucket, source, notes, created_at").order("merchant_pattern");if(n)throw n;return s??[]}}),{data:D=[],isLoading:G}=Q({queryKey:["admin-merchant-proposals"],queryFn:async()=>{const{data:s,error:n}=await y.from("merchant_rule_proposals").select("id, merchant_pattern, bucket, confidence, sample_count, example_merchants, reasoning, status, created_at").eq("status","pending").order("sample_count",{ascending:!1}).limit(200);if(n)throw n;return s??[]}}),C=v({mutationFn:()=>r({data:{limit:50,minOccurrences:3}}),onSuccess:s=>{d.success(`Scanned ${s.merchantsScanned} merchants â ${s.proposalsCreated} proposals queued`),i.invalidateQueries({queryKey:["admin-merchant-proposals"]})},onError:s=>d.error(s.message)}),f=v({mutationFn:s=>a({data:s}),onSuccess:()=>{i.invalidateQueries({queryKey:["admin-merchant-proposals"]}),i.invalidateQueries({queryKey:["admin-merchant-rules"]}),i.invalidateQueries({queryKey:["merchant-rules"]})},onError:s=>d.error(s.message)}),K=v({mutationFn:s=>g({data:{proposalId:s}}),onSuccess:()=>i.invalidateQueries({queryKey:["admin-merchant-proposals"]}),onError:s=>d.error(s.message)}),X=v({mutationFn:async s=>{const{error:n}=await y.from("merchant_rules").delete().eq("id",s);if(n)throw n},onSuccess:()=>{i.invalidateQueries({queryKey:["admin-merchant-rules"]}),i.invalidateQueries({queryKey:["merchant-rules"]}),d.success("Rule removed")},onError:s=>d.error(s.message)}),N=m.useMemo(()=>D,[D]);return e.jsxs("div",{className:"max-w-6xl mx-auto px-4 md:px-6 py-6 md:py-10 space-y-8",children:[e.jsx(te,{to:"/admin",children:t("Back to admin","Volver al admin")}),e.jsxs("header",{children:[e.jsx("p",{className:"text-xs uppercase tracking-[0.2em] text-muted-foreground",children:"Internal admin"}),e.jsxs("h1",{className:"font-display text-3xl sm:text-4xl mt-1 flex items-center gap-3",children:[e.jsx(ae,{className:"size-7 text-gold"}),"Merchant rules"]}),e.jsx("p",{className:"text-sm text-muted-foreground mt-2 max-w-2xl",children:"Global merchant â category rules. Applied across spend optimization, coverage, wallet gaps, and points-earning estimates for every user. The AI classifier scans recent transactions and queues suggestions for your review."})]}),e.jsxs("section",{className:"rounded-2xl border border-gold/30 bg-gold/5 p-5",children:[e.jsxs("div",{className:"flex items-start justify-between gap-4 mb-4 flex-wrap",children:[e.jsxs("div",{children:[e.jsxs("h2",{className:"font-display text-2xl flex items-center gap-2",children:[e.jsx(M,{className:"size-5 text-gold"})," AI Review Queue"]}),e.jsx("p",{className:"text-xs text-muted-foreground mt-1 max-w-xl",children:"The AI scans recent transactions weekly and suggests merchant-to-category mappings it finds repeatedly. Review, approve or reject each proposal â approved rules instantly become Live Rules that steer categorization for every user."}),e.jsx("p",{className:"text-xs text-muted-foreground mt-1",children:G?"Loadingâ¦":`${N.length} pending suggestion${N.length===1?"":"s"}`})]}),e.jsx(l,{onClick:()=>C.mutate(),disabled:C.isPending,children:C.isPending?e.jsxs(e.Fragment,{children:[e.jsx(re,{className:"size-4 mr-2 animate-spin"})," Scanningâ¦"]}):e.jsxs(e.Fragment,{children:[e.jsx(M,{className:"size-4 mr-2"})," Run classifier now"]})})]}),N.length===0?e.jsx("p",{className:"text-sm text-muted-foreground py-4",children:"No pending proposals. Ru
1n the classifier to scan recent transactions."}):e.jsx("ul",{className:"divide-y divide-border",children:N.map(s=>e.jsxs("li",{className:"py-3 flex items-start justify-between gap-4",children:[e.jsxs("div",{className:"min-w-0 flex-1",children:[e.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[e.jsx("span",{className:"font-medium text-platinum",children:s.merchant_pattern}),e.jsxs(z,{value:s.bucket,onValueChange:n=>x({...s,bucket:n}),disabled:f.isPending,children:[e.jsx(E,{className:"h-auto w-auto min-w-0 inline-flex items-center rounded-full bg-gold/15 text-gold border border-gold/30 px-2 py-0.5 text-[10px] uppercase tracking-wider hover:bg-gold/25 focus:ring-gold/50 focus-visible:ring-1 focus-visible:ring-gold/50 [&>span>svg]:h-3 [&>span>svg]:w-3","aria-label":"Edit category",children:e.jsx(R,{})}),e.jsx(L,{children:T.map(n=>e.jsx(A,{value:n,children:w(n)},n))})]}),s.confidence!==null&&e.jsxs("span",{className:"text-[11px] text-muted-foreground",children:[Math.round(s.confidence*100),"% confidence"]}),e.jsxs("span",{className:"text-[11px] text-muted-foreground",children:["· ",s.sample_count," tx"]})]}),s.example_merchants&&s.example_merchants.length>0&&e.jsxs("p",{className:"text-[11px] text-muted-foreground mt-1 truncate",children:["Seen as: ",s.example_merchants.slice(0,3).join(" · ")]}),s.reasoning&&e.jsx("p",{className:"text-[11px] text-muted-foreground/80 mt-1 italic",children:s.reasoning})]}),e.jsxs("div",{className:"flex items-center gap-1 shrink-0",children:[e.jsx(l,{size:"sm",variant:"outline",onClick:()=>f.mutate({proposalId:s.id}),disabled:f.isPending,className:"border-emerald-500/50 text-emerald-300 hover:bg-emerald-500/10",title:"Approve as-is",children:e.jsx(se,{className:"size-4"})}),e.jsx(l,{size:"sm",variant:"outline",onClick:()=>x(s),title:"Edit then approve",children:e.jsx(I,{className:"size-4"})}),e.jsx(l,{size:"sm",variant:"outline",onClick:()=>K.mutate(s.id),disabled:K.isPending,title:"Reject",children:e.jsx(ne,{className:"size-4"})})]})]},s.id))})]}),e.jsxs("section",{className:"rounded-2xl border border-metallic bg-metallic/40 p-5",children:[e.jsxs("div",{className:"flex items-center justify-between gap-4 mb-4 flex-wrap",children:[e.jsxs("div",{children:[e.jsxs("h2",{className:"font-display text-2xl",children:["Live Rules (",k.length,")"]}),e.jsx("p",{className:"text-xs text-muted-foreground mt-1 max-w-xl",children:"These are the active, production merchant-to-category mappings. Every transaction across all users is checked against these rules first. A single correction here automatically fixes categorization everywhere â no per-user overrides needed."}),e.jsx("p",{className:"text-xs text-muted-foreground mt-1",children:"Case-insensitive substring match. Longest pattern wins."})]}),e.jsxs(l,{onClick:()=>j(!0),children:[e.jsx(ie,{className:"size-4 mr-1"})," Add rule"]})]}),W?e.jsx("p",{className:"text-sm text-muted-foreground py-4",children:"Loadingâ¦"}):k.length===0?e.jsx("p",{className:"text-sm text-muted-foreground py-4",children:"No rules yet."}):e.jsx("ul",{className:"divide-y divide-border",children:k.map(s=>e.jsxs("li",{className:"py-3 flex items-center justify-between gap-4",children:[e.jsxs("div",{className:"min-w-0 flex-1",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"font-medium text-platinum",children:s.merchant_pattern}),e.jsxs("span",{className:"text-gold text-xs",children:["â ",w(s.bucket)]}),e.jsx("span",{className:`inline-flex items-center rounded-full px-1.5 py-0.5 text-[9px] uppercase tracking-wider border ${s.source==="ai"?"border-gold/30 bg-gold/10 text-gold":"border-border bg-background/40 text-muted-foreground"}`,children:s.source})]}),s.notes&&e.jsx("p",{className:"text-[11px] text-muted-foreground mt-0.5",children:s.notes})]}),e.jsxs("div",{className:"flex items-center gap-1 shrink-0",children:[e.jsx(l,{variant:"ghost",size:"sm",onClick:()=>h(s),children:e.jsx(I,{className:"size-4"})}),e.jsx(l,{variant:"ghost",size:"sm",onClick:async()=>{await u({title:`Remove rule "${s.merchant_pattern}"?`,confirmText:"Remove",destructive:!0})&&X.mutate(s.id)},children:e.jsx(ce,{className:"size-4 text-destructive"})})]})]},s.id))})]}),(p||o)&&e.jsx(ue,{rule:p,open:!!p||o,onOpenChange:s=>{s||(h(null),j(!1),i.invalidateQueries({queryKey:["admin-merchant-rules"]}),i.invalidateQueries({queryKey:["merchant-rules"]}))}}),c&&e.jsx(pe,{proposal:c,open:!!c,isApproving:f.isPending,onOpenChange:s=>{s||x(null)},onApprove:async(s,n)=>{await f.mutateAsync({proposalId:c.id,patternOverride:s,bucketOverride:n}),x(null)}})]})}function ue({rule:t,open:i,onOpenChange:u}){const[p,h]=m.useState(t?.merchant_pattern??""),[o,j]=m.useState(t?.bucket??"other"),[c,x]=m.useState(t?.notes??""),r=v({mutationFn:async()=>{const a=p.trim().toUpperCase();if(!a)throw new Error("Pattern can't be empty");if(t){const{error:g}=await y.from("merchant_rules").update({merchant_pattern:a,bucket:o,notes:c||null}).eq("id",t.id);if(g)throw g}else{const{error:g}=await y.from("merchant_rules").upsert({merchant_pattern:a,bucket:o,notes:c||null,source:"admin"},{onConflict:"merchant_pattern"});if(g)throw g}},onSuccess:()=>{d.success(t?"Rule updated":"Rule added"),u(!1)},onError:a=>d.error(a.message)});return e.jsx(O,{open:i,onOpenChange:u,children:e.jsxs(B,{className:"sm:max-w-md",children:[e.jsxs($,{children:[e.jsx(V,{children:t?"Edit rule":"Add merchant rule"}),e.jsx(U,{children:"Case-insensitive substring on merchant name. Applies globally to all users."})]}),e.jsxs("div",{className:"space-y-4 py-2",children:[e.jsxs("div",{className:"space-y-1.5",children:[e.jsx(b,{htmlFor:"mr-pattern",children:"Merchant matches (contains)"}),e.jsx(_,{id:"mr-pattern",value:p,onChange:a=>h(a.target.value),placeholder:"e.g. WHOLE FOODS"})]}),e.jsxs("div",{className:"space-y-1.5",children:[e.jsx(b,{htmlFor:"mr-bucket",children:"Category"}),e.jsxs(z,{value:o,onValueChange:a=>j(a),children:[e.jsx(E,{id:"mr-bucket",children:e.jsx(R,{})}),e.jsx(L,{children:T.map(a=>e.jsx(A,{value:a,children:w(a)},a))})]})]}),e.jsxs("div",{className:"space-y-1.5",children:[e.jsx(b,{htmlFor:"mr-notes",children:"Notes (optional)"}),e.jsx(_,{id:"mr-notes",value:c,onChange:a=>x(a.target.value),placeholder:"Why this rule exists"})]})]}
1),e.jsxs(H,{children:[e.jsx(l,{variant:"outline",onClick:()=>u(!1),children:"Cancel"}),e.jsx(l,{onClick:()=>r.mutate(),disabled:r.isPending,children:r.isPending?"Savingâ¦":"Save"})]})]})})}function pe({proposal:t,open:i,onOpenChange:u,onApprove:p,isApproving:h}){const[o,j]=m.useState(t.merchant_pattern),[c,x]=m.useState(t.bucket);return e.jsx(O,{open:i,onOpenChange:u,children:e.jsxs(B,{className:"sm:max-w-md",children:[e.jsxs($,{children:[e.jsx(V,{children:"Edit & approve proposal"}),e.jsx(U,{children:"Adjust the merchant pattern or category, then approve to add it to Live Rules."})]}),e.jsxs("div",{className:"space-y-4 py-2",children:[e.jsxs("div",{className:"space-y-1.5",children:[e.jsx(b,{htmlFor:"pp-pattern",children:"Merchant matches (contains)"}),e.jsx(_,{id:"pp-pattern",value:o,onChange:r=>j(r.target.value),placeholder:"e.g. WHOLE FOODS"})]}),e.jsxs("div",{className:"space-y-1.5",children:[e.jsx(b,{htmlFor:"pp-bucket",children:"Category"}),e.jsxs(z,{value:c,onValueChange:r=>x(r),children:[e.jsx(E,{id:"pp-bucket",children:e.jsx(R,{})}),e.jsx(L,{children:T.map(r=>e.jsx(A,{value:r,children:w(r)},r))})]})]}),t.example_merchants&&t.example_merchants.length>0&&e.jsxs("p",{className:"text-[11px] text-muted-foreground",children:["Seen as: ",t.example_merchants.slice(0,5).join(" · ")]})]}
1),e.jsxs(H,{children:[e.jsx(l,{variant:"outline",onClick:()=>u(!1),children:"Cancel"}),e.jsx(l,{onClick:()=>{const r=o.trim().toUpperCase();if(!r){d.error("Pattern can't be empty");return}p(r,c)},disabled:h,children:h?"Approvingâ¦":"Approve & save"})]})]})})}export{ze as Route};
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.