PageSourceSearch

https://stuartandrews.me/assets/MemberGroupsTab-Bc8eRONF.js

js stuartandrews.me collected 2026-09-24 22:18:22 UTC 17,854 bytes, 2 lines download raw bytes

1import{r as a,j as e}from"./react-vendor-DHsc3z8R.js";import{S as K}from"./StatusPill-BSgGM2e7.js";import{S as B}from"./StatTile-TXro_MK2.js";import{R as Z}from"./RowActionMenu-Drqy9eBI.js";import{a as _,z as V,j as Q,X as ee}from"./index-D70tPUFV.js";import{P as te}from"./PageShell-C8EKG4cP.js";import{E as se}from"./EmptyState-COlsfekm.js";import{T as re}from"./textarea-38Xa26Rl.js";import{U as D}from"./users-L8YqcNFi.js";import{C as W}from"./circle-alert-Dyl6ivaA.js";import{R as L}from"./refresh-cw-BOTEdsq6.js";import{T as ae}from"./trash-2-Bln-_Txq.js";import{P as H}from"./plus-DnB-b9cR.js";import{U as J}from"./users-round-BYAdZ-92.js";import{P as oe}from"./pencil-BrPZR3CI.js";import{A as ne}from"./archive-D-i3N4_x.js";import"./arrow-up-CNO4iuae.js";import"./arrow-down-CTqNljq1.js";import"./dropdown-menu-0sz3yqpS.js";import"./ui-vendor-CSbCjZRx.js";import"./check-DNgNGAzZ.js";import"./chevron-right-15wZkIJf.js";import"./circle-vqIdLyen.js";import"./ellipsis-BJXFPxOb.js";import"./query-vendor-BpSM1h0G.js";import"./motion-vendor-BtpmimIC.js";const de=50;function ie({open:v,onClose:N,groupId:x,groupName:g,refreshToken:b,onMembersChanged:F}){const{toast:w}=_(),M=V(),k={...Q(),"Content-Type":"application/json"},[$,r]=a.useState(!1),[i,l]=a.useState(!1),[p,C]=a.useState(null),[f,A]=a.useState([]),[j,P]=a.useState(0),[I,y]=a.useState(1),[U,T]=a.useState(!1),[S,E]=a.useState(""),[R,G]=a.useState(!1),[c,t]=a.useState(null),[n,d]=a.useState(null),h=async(s,m)=>{if(x){m?l(!0):r(!0),C(null);try{const o=await fetch(`/api/admin/cap/member-groups/${x}/members?page=${s}&limit=${de}`,{headers:k});if(!o.ok){const q=await o.json().catch(()=>({}));C(q.error||`Failed to load members (${o.status})`);return}const u=await o.json(),z=Array.isArray(u.members)?u.members:[];A(q=>m?[...q,...z]:z),P(typeof u.total=="number"?u.total:z.length),y(s)}catch{C("Network error — could not load members")}finally{r(!1),l(!1)}}};a.useEffect(()=>{v&&(T(!1),t(null),E(""),A([]),P(0),h(1,!1))},[v,x,b]);const O=async()=>{const s=S.split(/[\n,]/).map(m=>m.trim()).filter(Boolean);if(!(s.length===0||!x)){G(!0),t(null);try{const m=await fetch(`/api/admin/cap/member-groups/${x}/members`,{method:"POST",headers:k,body:JSON.stringify({emails:s})}),o=await m.json().catch(()=>({}));if(!m.ok)throw new Error(o.error||"Failed to add members");const u={added:typeof o.added=="number"?o.added:0,alreadyActive:typeof o.alreadyActive=="number"?o.alreadyActive:0,notFoundIds:Array.isArray(o.notFoundIds)?o.notFoundIds:[],notFoundEmails:Array.isArray(o.notFoundEmails)?o.notFoundEmails:[]};t(u),w({title:`${u.added} member${u.added===1?"":"s"} added`}),E(""),await h(1,!1),F?.()}catch(m){w({title:"Failed to add members",description:m.message,variant:"destructive"})}finally{G(!1)}}},X=async s=>{if(!(!x||!await M({title:`Remove ${s.email} from ${g||"this group"}?`,description:"They are no longer tagged with this group. This does not change their tier, subscription, or access to anything gated on it.",confirmLabel:"Remove",danger:!0}))){d(s.userId);try{const o=await fetch(`/api/admin/cap/member-groups/${x}/members/${s.userId}`,{method:"DELETE",headers:k});if(!o.ok){const u=await o.json().catch(()=>({}));throw new Error(u.error||"Failed to remove member")}w({title:"Member removed",description:s.email}),await h(1,!1),F?.()}catch(o){w({title:"Failed to remove member",description:o.message,variant:"destructive"})}finally{d(null)}}};if(!v)return null;const Y=f.length<j;return e.jsxs("div",{className:"fixed inset-0 z-50 flex justify-end",children:[e.jsx("div",{className:"absolute inset-0 bg-black/50",onClick:N}),e.jsxs("div",{className:"relative w-full max-w-lg bg-card border-l border-border h-full flex flex-col shadow-2xl",children:[e.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-border",children:[e.jsxs("div",{children:[e.jsxs("h3",{className:"font-semibold text-foreground flex items-center gap-2",children:[e.jsx(D,{className:"w-4 h-4 text-accent-gold"})," ",g||"Group"," members"]}),e.jsxs("p",{className:"text-xs text-muted-foreground mt-0.5",children:[j," member",j===1?"":"s"]})]}),e.jsx("button",{onClick:N,className:"text-muted-foreground hover:text-foreground p-1","data-testid":"button-close-member-group-drawer",children:e.jsx(ee,{className:"w-5 h-5"})})]}),e.jsx("div",{className:"flex-1 overflow-y-auto p-4 space-y-2",children:$?e.jsx("div",{className:"flex justify-center py-12",children:e.jsx("div",{className:"w-5 h-5 border-2 border-accent-gold/30 border-t-accent-gold rounded-full animate-spin","data-testid":"member-group-members-loading"})}):p?e.jsxs("div",{className:"flex flex-col items-center py-12 gap-3 text-center","data-testid":"member-group-members-error",children:[e.jsx(W,{className:"w-8 h-8 text-destructive-ink opacity-70"}),e.jsx("p",{className:"text-sm text-destructive-ink",children:p}),e.jsxs("button",{onClick:()=>h(1,!1),className:"text-xs text-muted-foreground hover:text-foreground border border-border px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5","data-testid":"button-retry-member-group-members",children:[e.jsx(L,{className:"w-3 h-3"})," Retry"]})]}):f.length===0?e.jsxs("div",{className:"text-center py-12 text-muted-foreground","data-testid":"member-group-members-empty",children:[e.jsx(D,{className:"w-8 h-8 mx-auto mb-3 opacity-40"}),e.jsx("p",{className:"text-sm",children:"No members in this group yet"}),e.jsx("p",{className:"text-xs mt-1",children:"Add members by email below"})]}):e.jsxs(e.Fragment,{children:[f.map(s=>e.jsxs("div",{className:"bg-secondary border border-border rounded-lg p-3 flex items-center justify-between gap-2","data-testid":`group-member-row-${s.userId}`,children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("p",{className:"text-sm font-medium text-foreground truncate",children:[s.firstName," ",s.lastName]}),e.jsx("p",{className:"text-xs text-muted-foreground truncate",children:s.email}),e.jsxs("p",{className:"text-[10px] text-muted-foreground mt-0.5",children:["Added ",new Date(s.addedAt).toLocaleDateString(),s.addedBy?` by ${s.addedBy}`:""]})]}),e.jsx("button",{onClick:()=>X(s),disabled:n===s.userId,className:"text-xs bg-secondary hover:bg-destructive/40 text-muted-foreground hover:text-destructive-ink rounded-lg px-2 py-1 transition-colors disabled:opacity-40","data-testid":`button-remove-member-${s.userId}`,children:n===s.userId?e.jsx(L,{className:"w-3 h-3 animate-spin"}):e.jsx(ae,{className:"w-3 h-3"})})]},s.id)),Y&&e.jsxs("button",{onClick:()=>h(I+1,!0),disabled:i,className:"w-full text-xs text-muted-foreground hover:text-foreground border border-border px-3 py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 disabled:opacity-50","data-testid":"button-load-more-group-members",children:[i?e.jsx(L,{className:"w-3 h-3 animate-spin"}):null,i?"Loading…":`Load more (${f.length} of ${j})`]})]})}),e.jsx("div",{className:"border-t border-border p-4 space-y-3",children:U?e.jsxs("div",{className:"space-y-2",children:[e.jsx("label",{className:"text-xs text-muted-foreground font-medium",children:"Emails, one per line or comma-separated"}),e.jsx(re,{value:S,onChange:s=>E(s.target.value),placeholder:`[email protected]
2[email protected]`,className:"min-h-[100px] bg-secondary border-border text-sm","data-testid":"textarea-add-group-members"}),c&&e.jsxs("div",{className:"text-xs space-y-1","data-testid":"text-add-group-members-result",children:[c.added>0&&e.jsxs("p",{className:"text-success-ink",children:[c.added," added"]}),c.alreadyActive>0&&e.jsxs("p",{className:"text-muted-foreground",children:[c.alreadyActive," already in this group"]}
2),c.notFoundEmails.length>0&&e.jsxs("p",{className:"text-destructive-ink",children:[c.notFoundEmails.length," not found: ",c.notFoundEmails.join(", ")]})]}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx("button",{onClick:O,disabled:R||!S.trim(),className:"flex-1 text-sm font-medium bg-accent-gold hover:bg-accent-gold-light text-navy-deep px-3 py-2 rounded-lg transition-colors disabled:opacity-50","data-testid":"button-confirm-add-group-members",children:R?"Adding…":"Add members"}),e.jsx("button",{onClick:()=>{T(!1),E(""),t(null)},className:"px-3 py-2 text-sm text-muted-foreground hover:text-foreground rounded-lg transition-colors","data-testid":"button-cancel-add-group-members",children:"Cancel"})]})]}):e.jsxs("button",{onClick:()=>T(!0),className:"w-full flex items-center justify-center gap-2 text-sm font-medium bg-accent-gold/10 hover:bg-accent-gold/20 text-accent-gold-ink border border-accent-gold/30 hover:border-accent-gold/50 px-3 py-2 rounded-lg transition-colors","data-testid":"button-add-group-members",children:[e.jsx(H,{className:"w-4 h-4"})," Add members"]})})]})]})}const le=/^[a-z0-9-]{2,40}$/;function ce(v){return new Date(v).toLocaleDateString("en-US",{day:"numeric",month:"short",year:"numeric"})}const me={id:null,slug:"",name:"",notes:""};function Ie({token:v}){const{toast:N}=_(),x=V(),g={...Q(),"Content-Type":"application/json"},[b,F]=a.useState([]),[w,M]=a.useState(!1),[k,$]=a.useState(null),[r,i]=a.useState(null),[l,p]=a.useState({}),[C,f]=a.useState(!1),[A,j]=a.useState(null),[P,I]=a.useState(0),y=async()=>{M(!0),$(null);try{const t=await fetch("/api/admin/cap/member-groups",{headers:g});if(!t.ok){const d=await t.json().catch(()=>({}));$(d.error||`Failed to load member groups (${t.status})`);return}const n=await t.json();F(Array.isArray(n.groups)?n.groups:[])}catch{$("Network error — could not load member groups")}finally{M(!1)}};a.useEffect(()=>{y()},[]);const U=a.useMemo(()=>b.filter(t=>t.active),[b]),T=a.useMemo(()=>b.reduce((t,n)=>t+(n.memberCount||0),0),[b]),S=()=>{i({...me}),p({})},E=t=>{i({id:t.id,slug:t.slug,name:t.name,notes:t.notes||""}),p({})},R=async()=>{if(!r)return;const t={},n=r.id===null;if(n&&!le.test(r.slug)&&(t.slug="Lowercase letters, numbers, hyphens only (2-40 chars)"),(!r.name.trim()||r.name.trim().length<2)&&(t.name="Name is required (at least 2 characters)"),Object.keys(t).length>0){p(t);return}p({}),f(!0);try{const d={name:r.name.trim(),notes:r.notes.trim()||null},h=n?await fetch("/api/admin/cap/member-groups",{method:"POST",headers:g,body:JSON.stringify({...d,slug:r.slug.trim()})}):await fetch(`/api/admin/cap/member-groups/${r.id}`,{method:"PATCH",headers:g,body:JSON.stringify(d)}),O=await h.json().catch(()=>({}));if(!h.ok){p({general:O.error||"Failed to save group"});return}N({title:n?"Group created":"Group updated"}),i(null),await y()}catch{p({general:"Network error"})}finally{f(!1)}},G=async t=>{if(await x({title:`Archive ${t.name}?`,description:"This group is hidden from active use across the platform, including as a filter elsewhere. Existing memberships are kept, not removed — there is no way to reactivate a group from this panel once archived.",confirmLabel:"Archive",danger:!0}))try{const d=await fetch(`/api/admin/cap/member-groups/${t.id}`,{method:"DELETE",headers:g});if(!d.ok)throw new Error((await d.json().catch(()=>({}))).error||"Failed to archive");N({title:"Group archived"}),await y()}catch(d){N({title:"Failed to archive group",description:d.message,variant:"destructive"})}},c=t=>{const n=[{id:"members",label:"Members",icon:D,quick:!0,onClick:()=>{j(t),I(d=>d+1)}},{id:"edit",label:"Edit",icon:oe,quick:!0,onClick:()=>E(t)}];return t.active&&n.push({id:"archive",label:"Archive",icon:ne,danger:!0,onClick:()=>G(t)}),n};return e.jsxs("div",{className:"space-y-4","data-testid":"panel-member-groups",children:[e.jsx(te,{title:"Member Groups",subtitle:"Admin-managed tags for groupings no purchase captures — pilots, VIPs, event cohorts. A member can belong to many groups at once.",action:e.jsxs("button",{onClick:S,className:"flex items-center gap-1.5 px-3 py-2 rounded-lg bg-accent-gold text-navy-deep text-sm font-medium hover:bg-accent-gold-light transition-colors","data-testid":"button-new-member-group",children:[e.jsx(H,{className:"w-4 h-4"})," Create group"]}),stats:e.jsxs(e.Fragment,{children:[e.jsx(B,{label:"Active groups",value:U.length,icon:J,accent:!0,"data-testid":"stat-active-groups"}),e.jsx(B,{label:"Total group members",value:T,icon:D,"data-testid":"stat-total-group-members"})]}),children:w?e.jsx("div",{className:"flex justify-center py-12",children:e.jsx("div",{className:"w-6 h-6 border-2 border-accent-gold/30 border-t-accent-gold rounded-full animate-spin"})}):k?e.jsxs("div",{className:"flex flex-col items-center py-12 gap-3 text-center",children:[e.jsx(W,{className:"w-8 h-8 text-destructive-ink opacity-70"}),e.jsx("p",{className:"text-sm text-destructive-ink",children:k}),e.jsxs("button",{onClick:y,className:"text-xs text-muted-foreground hover:text-foreground border border-border px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5","data-testid":"button-retry-member-groups",children:[e.jsx(L,{className:"w-3 h-3"})," Retry"]})]}):b.length===0?e.jsx(se,{icon:J,title:"No groups yet",description:"Create a group to tag a cohort of members — a pilot, a VIP list, an event — that no purchase already captures.",action:e.jsxs("button",{onClick:S,className:"flex items-center gap-1.5 px-3 py-2 rounded-lg bg-accent-gold text-navy-deep text-sm font-medium hover:bg-accent-gold-light transition-colors","data-testid":"button-new-member-group-empty",children:[e.jsx(H,{className:"w-4 h-4"})," Create group"]}),compact:!0,"data-testid":"empty-state-member-groups"}):e.jsx("div",{className:"overflow-x-auto",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-border/40 text-left text-muted-foreground text-xs",children:[e.jsx("th",{className:"pb-3 pr-4 font-medium",children:"Group"}),e.jsx("th",{className:"pb-3 pr-4 font-medium",children:"Members"}),e.jsx("th",{className:"pb-3 pr-4 font-medium",children:"Created"}),e.jsx("th",{className:"pb-3 pr-4 font-medium",children:"Notes"}),e.jsx("th",{className:"pb-3 font-medium",children:"Actions"})]})}),e.jsx("tbody",{className:"divide-y divide-border/20",children:b.map(t=>e.jsxs("tr",{className:"hover:bg-secondary/20 group/row","data-testid":`row-member-group-${t.id}`,children:[e.jsx("td",{className:"py-3 pr-4",children:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs("div",{children:[e.jsx("span",{className:"font-medium text-foreground",children:t.name}),e.jsx("div",{className:"text-muted-foreground text-xs font-mono",children:t.slug})]}),!t.active&&e.jsx(K,{variant:"muted",size:"sm",children:"Archived"})]})}),e.jsx("td",{className:"py-3 pr-4 text-foreground text-xs",children:t.memberCount}),e.jsx("td",{className:"py-3 pr-4 text-muted-foreground text-xs",children:ce(t.createdAt)}),e.jsx("td",{className:"py-3 pr-4 text-muted-foreground text-xs max-w-xs truncate",children:t.notes||"—"}),e.jsx("td",{className:"py-3",children:e.jsx(Z,{actions:c(t),label:`${t.name} actions`})})]},t.id))})]})})}),r&&e.jsx("div",{className:"fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4",onClick:()=>i(null),"data-testid":"member-group-form-modal-backdrop",children:e.jsxs("div",{className:"bg-background border border-border rounded-xl max-w-md w-full p-6 max-h-[90vh] overflow-y-auto",onClick:t=>t.stopPropagation(),children:[e.jsx("h3",{className:"text-lg font-semibold text-foreground mb-4",children:r.id===null?"Create group":`Edit ${r.name}`}),l.general&&e.jsx("p",{className:"text-xs text-destructive mb-3",children:l.general}),e.jsxs("div",{className:"space-y-3",children:[e.jsxs("div",{children:[e.jsx("label",{className:"text-xs text-muted-foreground font-medium mb-1 block",children:"Name"}),e.jsx("input",{value:r.name,onChange:t=>i({...r,name:t.target.value}),placeholder:"Q1 pilot cohort",className:`w-full bg-secondary/50 border rounded-lg px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring/50 ${l.name?"border-destructive":"border-border/50"}`,"data-testid":"input-member-group-name"}),l.name&&e.jsx("p",{className:"text-xs text-destructive mt-1",children:l.name})]}),e.jsxs("div",{children:[e.jsxs("label",{className:"text-xs text-muted-foreground font-medium mb-1 block",children:["Slug ",r.id!==null&&e.jsx("span",{className:"font-normal text-muted-foreground
2/70",children:"(immutable)"})]}),e.jsx("input",{value:r.slug,disabled:r.id!==null,onChange:t=>i({...r,slug:t.target.value}),placeholder:"q1-pilot-cohort",className:`w-full bg-secondary/50 border rounded-lg px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring/50 disabled:opacity-60 font-mono ${l.slug?"border-destructive":"border-border/50"}`,"data-testid":"input-member-group-slug"}),l.slug&&e.jsx("p",{className:"text-xs text-destructive mt-1",children:l.slug})]}),e.jsxs("div",{children:[e.jsxs("label",{className:"text-xs text-muted-foreground font-medium mb-1 block",children:["Notes ",e.jsx("span",{className:"font-normal text-muted-foreground/70",children:"(optional)"})]}),e.jsx("input",{value:r.notes,onChange:t=>i({...r,notes:t.target.value}),placeholder:"Why this group exists",className:"w-full bg-secondary/50 border border-border/50 rounded-lg px-3 py-2 text-sm text-foreground focus:outline-none","data-testid":"input-member-group-notes"})]})]}),e.jsxs("div",{className:"flex justify-end gap-2 mt-5",children:[e.jsx("button",{onClick:()=>i(null),className:"px-3 py-1.5 text-sm rounded bg-secondary text-muted-foreground hover:text-foreground","data-testid":"button-cancel-member-group-form",children:"Cancel"}),e.jsx("button",{onClick:R,disabled:C,className:"px-4 py-1.5 text-sm rounded bg-accent-gold text-navy-deep font-medium hover:bg-accent-gold-light disabled:opacity-50","data-testid":"button-submit-member-group-form",children:C?"Saving…":r.id===null?"Create":"Save changes"})]})]})}),e.jsx(ie,{open:!!A,onClose:()=>j(null),groupId:A?.id,groupName:A?.name,refreshToken:P,onMembersChanged:y})]})}export{Ie as MemberGroupsTab};

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.