// screens-admin-people.jsx — People (Members + Coaches), Member detail, Coach detail const { fmtIDR, findClass, findCoach } = window.NADI_HELPERS; function AdminPeopleScreen({ openMember, openCoach, showToast }) { const [seg, setSeg] = useState("members"); // members | coaches const [query, setQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); // all | active | new | lapsed return (
people

{seg === "members" ? "Members" : "Coaches"}.

{/* Segmented */}
{[["members", "Members", window.NADI_ADMIN.MEMBERS.length], ["coaches", "Coaches", window.NADI.COACHES.length]].map(([id, label, n]) => ( ))}
{/* Search */}
setQuery(e.target.value)} placeholder={seg === "members" ? "Search by name, email, phone…" : "Search teachers…"} style={{ width: "100%", padding: "12px 14px 12px 36px", border: "1px solid var(--rule)", background: "var(--bg)", fontFamily: "Geist, sans-serif", fontSize: 14, color: "var(--ink)", }} />
{seg === "members" && (
{[["all","All"],["active","Active"],["new","New"],["lapsed","Lapsed"]].map(([id, label]) => ( setStatusFilter(id)}>{label} ))}
)}
{/* List */}
{seg === "members" ? ( ) : ( )}
); } function MemberList({ query, statusFilter, openMember }) { let list = window.NADI_ADMIN.MEMBERS; if (statusFilter !== "all") list = list.filter((m) => m.status === statusFilter); if (query) { const q = query.toLowerCase(); list = list.filter((m) => m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q) || m.phone.includes(query)); } if (list.length === 0) return (
no matches
); return (
{list.map((m) => openMember(m)} />)}
); } function MemberRow({ m, onClick }) { const statusColors = { active: "var(--accent)", new: "var(--primary)", lapsed: "var(--muted)", }; return ( ); } function CoachList({ query, openCoach, showToast }) { let list = window.NADI.COACHES; if (query) { const q = query.toLowerCase(); list = list.filter((c) => c.name.toLowerCase().includes(q)); } return (
{list.map((c) => { const payout = window.NADI_ADMIN.PAYOUTS.find((p) => p.coachId === c.id) || { classes: 0, hours: 0, rate: 200000, paid: false }; const currentRate = c.rate || payout.rate || 200000; const currentTotal = payout.classes * currentRate; return ( ); })}
); } // ── MEMBER DETAIL SHEET (full CRUD) ───────────────────────────────── function AdminMemberSheet({ member, onClose, showToast }) { const [tab, setTab] = useState("overview"); // overview | bookings | packs | edit if (!member) return null; return (
member · #{member.id}

{member.name}

joined {new Date(member.joined).toLocaleDateString("en-GB", { month: "short", year: "numeric" }).toLowerCase()}
{/* Stats row */}
{/* Contact */}
contact
{member.email} {member.phone}
{/* Tabs */}
{[["overview","Overview"],["bookings","Bookings"],["packs","Packs"],["edit","Edit"]].map(([id, label]) => ( ))}
{tab === "overview" && } {tab === "bookings" && } {tab === "packs" && } {tab === "edit" && }
); } function SmallStat({ label, value, suffix, accent, text }) { return (
{label}
{value}
{suffix && {suffix}}
); } function MemberOverview({ member, showToast }) { return (
{/* Quick actions */}
{/* Last visit */}
last visit
{member.lastVisit}
{/* Recent payments for this member */}
recent purchases
{window.NADI_ADMIN.PAYMENTS.filter((p) => p.memberId === member.id).map((p) => (
{p.item}
{p.date} · {p.method.toLowerCase()}
{fmtIDR(p.amount)}
))} {window.NADI_ADMIN.PAYMENTS.filter((p) => p.memberId === member.id).length === 0 && (
no purchases yet
)}
); } function MemberBookings({ member, showToast }) { // Mock 4 sessions const upcoming = window.NADI.SCHEDULE.slice(0, 2); const past = window.NADI.SCHEDULE.slice(2, 6); return (
upcoming · {upcoming.length}
{upcoming.map((s) => { const c = findClass(s.classId), co = findCoach(s.coachId); return (
{c.name}
{window.NADI_HELPERS.DAYS_SHORT[s.day]} {s.time} · {co.firstName.toLowerCase()}
); })}
past · {past.length}
{past.map((s) => { const c = findClass(s.classId), co = findCoach(s.coachId); return (
{c.name}
{window.NADI_HELPERS.DAYS_SHORT[s.day]} {s.time} · {co.firstName.toLowerCase()}
attended
); })}
); } function MemberPacks({ member, showToast }) { const packs = member.credits > 0 ? [ { name: member.package.split(" + ")[0] || "Standard 5", credits: member.credits, total: 5, expires: "2026-06-04" }, ] : []; return (
{packs.length === 0 ? (
no active packs
) : packs.map((p, i) => (
{p.name} expires {p.expires.slice(5)}
{p.credits}/{p.total} credits remaining
))}
); } function MemberEdit({ member, showToast, onClose }) { const [name, setName] = useState(member.name); const [email, setEmail] = useState(member.email); const [phone, setPhone] = useState(member.phone); return (
danger zone

Suspending freezes credits and prevents booking. Deleting is permanent.

); } // ── COACH DETAIL SHEET ──────────────────────────────────────────── function AdminCoachSheet({ coach, onClose, showToast }) { const [tab, setTab] = useState("overview"); const [name, setName] = React.useState(""); const [title, setTitle] = React.useState(""); const [yrs, setYrs] = React.useState(0); const [bio, setBio] = React.useState(""); const [rate, setRate] = React.useState(200000); React.useEffect(() => { if (coach) { setName(coach.name || ""); setTitle(coach.title || ""); setYrs(coach.yrs || 0); setBio(coach.bio || ""); setRate(coach.rate || 200000); } }, [coach]); if (!coach) return null; const payout = window.NADI_ADMIN.PAYOUTS.find((p) => p.coachId === coach.id) || { classes: 0, hours: 0, rate: 200000, paid: false }; const currentRate = coach.rate || payout.rate || 200000; const currentTotal = payout.classes * currentRate; const upcomingClasses = window.NADI.SCHEDULE.filter((s) => s.coachId === coach.id).slice(0, 5); return (
coach · {coach.role || "teacher"}

{coach.name}

{coach.title.toLowerCase()} · {coach.yrs} yrs

{coach.bio}

{/* Tabs */}
{[["overview","Overview"],["schedule","Schedule"],["pay","Pay"],["edit","Edit"]].map(([id, label]) => ( ))}
{tab === "overview" && (
specialties
{coach.specialties.map((sid) => {findClass(sid).name.toLowerCase()})}
)} {tab === "schedule" && (
{upcomingClasses.map((s) => { const c = findClass(s.classId); return (
{c.name}
{window.NADI_HELPERS.DAYS_SHORT[s.day].toLowerCase()} {s.time} · {s.roomShort.toLowerCase()}
{s.booked}/{s.capacity}
); })}
)} {tab === "pay" && (
this month · {new Date().toLocaleDateString("en-GB", { month: "short" }).toLowerCase()}
{fmtIDR(currentTotal)} {payout.paid ? "paid" : "owed"}
{payout.classes} classes × {fmtIDR(currentRate)}/class {!payout.paid && (
)}
)} {tab === "edit" && (
setYrs(parseInt(v, 10) || 0)} type="number" /> setRate(parseInt(v, 10) || 0)} type="number" />
)}
); } Object.assign(window, { AdminPeopleScreen, AdminMemberSheet, AdminCoachSheet, SmallStat });