// Bylt Capital CRM — Admin-only pages: team, partners, settings.

// Who sees what for each role: src/roles.js and src/access.js (DEC-091).
const ROLE_OPTIONS = [
  { value: "Owner", label: "Owner (everything, including your private clients and their commissions)" },
  { value: "Admin", label: "Admin (everything except the Owner's private clients; team and settings)" },
  { value: "Partner", label: "Partner, investor (an Admin's powers plus the Owner's private clients; no team or settings)" },
  { value: "Manager", label: "Manager (their own deals and their team's)" },
  { value: "Rep", label: "Rep (their own deals and their team's)" },
  { value: "Assistant", label: "Assistant (every lead and deal: offer links, texting, notes; no clients or commissions)" },
  { value: "Bookkeeper", label: "Bookkeeper (every commission and income figure, read-only; no leads, deals or clients)" },
  { value: "Viewer", label: "Viewer (sees everything except the Owner's private clients, changes nothing)" },
];
const PRIVATE_ROLES = ["Owner", "Partner"];
const EARNS = ["Owner", "Admin", "Partner", "Manager", "Rep"];       // roles that can be credited on a deal
// The roles `me` may hand out: only an Owner gives Owner or Partner (an Admin may name the first Owner).
function roleOptions(me, users, current) {
  const owner = !!(me && me.can && me.can.owner);
  const noOwner = !(users || []).some(u => u.role === "Owner" && u.active !== false);
  return ROLE_OPTIONS.filter(o => owner || o.value === current || !PRIVATE_ROLES.includes(o.value) || (o.value === "Owner" && noOwner));
}
const lockedFor = (me, u) => !!u && PRIVATE_ROLES.includes(u.role) && !(me && me.can && me.can.owner);

// ── Team ────────────────────────────────────────────────────────────────────
function UserSheet({ user, users, first, me, onClose, onSaved }) {
  const editing = !!user;
  const u = user || {};
  const c = u.commission || {};
  const [v, set] = useForm({ name: u.name || "", email: u.email || "", role: first ? "Admin" : u.role || "Rep", password: "", active: u.active !== false, payoutPct: c.payoutPct != null ? String(c.payoutPct) : "", phone: u.phone || "", leaderId: u.leaderId || "" });
  const leaderOptions = [{ value: "", label: "No team leader" }].concat((users || []).filter(x => x.id !== u.id && !["Viewer", "Assistant", "Bookkeeper"].includes(x.role) && (x.active !== false || x.id === u.leaderId)).map(x => ({ value: x.id, label: x.name })));
  // Team leaders who earn a percentage of the commission on this person's deals.
  const [overs, setOvers] = useState((c.overrides || []).filter(o => (users || []).some(x => x.id === o.userId)).map(o => ({ userId: o.userId, pct: String(o.pct) })));
  const setOver = (i, k, val) => setOvers(list => list.map((o, j) => (j === i ? { ...o, [k]: val } : o)));
  const leaders = (current, i) => [{ value: "", label: "Choose…" }].concat((users || []).filter(x => x.id !== u.id && (x.active !== false || x.id === current) && !overs.some((o, j) => j !== i && o.userId === x.id)).map(x => ({ value: x.id, label: x.name })));
  const pct = (s) => Number(String(s).replace(/[%\s]/g, ""));
  const save = useSave(onSaved);
  const submit = () => {
    if (!v.name.trim()) return save.setError("Enter their name.");
    if (!editing && !/^\S+@\S+\.\S+$/.test(v.email.trim())) return save.setError("Enter a valid email address.");
    if ((!editing || v.password) && v.password.trim().length < 8) return save.setError("Password needs at least 8 characters.");
    const payout = v.payoutPct.trim() === "" ? null : pct(v.payoutPct);
    if (payout !== null && !(payout >= 0 && payout <= 100)) return save.setError("Payout should be a percentage from 0 to 100.");
    if (overs.some(o => !o.userId || !(pct(o.pct) > 0 && pct(o.pct) <= 100))) return save.setError("Each override needs a team member and a percentage above 0.");
    if ((payout || 0) + overs.reduce((t, o) => t + pct(o.pct), 0) > 100) return save.setError("Payout plus overrides can't add up to more than 100% of the commission.");
    const body = { name: v.name.trim(), role: v.role, phone: v.phone.trim(), leaderId: (v.role === "Manager" || v.role === "Rep") && leaderOptions.some(o => o.value && o.value === v.leaderId) ? v.leaderId : null, commission: { payoutPct: payout, overrides: overs.map(o => ({ userId: o.userId, pct: pct(o.pct) })) } };
    if (editing) { body.active = v.active; if (v.password) body.password = v.password; }
    else { body.email = v.email.trim(); body.password = v.password; }
    save.run(() => (editing ? API.updateUser(u.id, body) : API.createUser(body)));
  };
  return (
    <Sheet eyebrow="Team" title={editing ? "Edit team member" : "Add team member"} onClose={onClose} onSubmit={submit}
           submitLabel={editing ? "Save changes" : "Add team member"} busy={save.busy} error={save.error}>
      <div className="form-section">
        <div className="form-grid">
          <Field label="Name" full><TextInput value={v.name} onChange={x => set("name", x)} /></Field>
          <Field label="Email" full hint={editing ? "Email can't be changed. Add a new team member instead." : "They sign in with this."}>
            <TextInput value={v.email} onChange={x => set("email", x)} type="email" disabled={editing} autoComplete="off" />
          </Field>
          <Field label="Role" full hint={first ? "The first team member is always an Admin, so someone can manage the team once the passcode is retired." : null}>
            <Select value={v.role} onChange={x => set("role", x)} options={first ? ROLE_OPTIONS : roleOptions(me, users, u.role)} disabled={!!first} />
          </Field>
          {v.role === "Owner" && u.role !== "Owner" ? <div className="banner info full">An Owner's clients are private: only Owners and Partners see them, their deals, advances and commissions. Admins, Viewers and Assistants don't.</div> : null}
          {v.role === "Manager" || v.role === "Rep" ? (
            <Field label="Team leader" full hint={`${v.name.trim() || "This person"} sees only their own deals, leads and clients. Their team leader sees those too, and so does anyone above that leader. Admins see everything.`}>
              <Select value={v.leaderId} onChange={x => set("leaderId", x)} options={leaderOptions} />
            </Field>
          ) : null}
          <Field label="Mobile phone" full hint="Shown on the offer links they send, and where they are texted when a client opens or accepts one.">
            <TextInput value={v.phone} onChange={x => set("phone", x)} inputMode="tel" autoComplete="off" />
          </Field>
          <Field label={editing ? "New password" : "Password"} full hint={editing ? "Leave blank to keep their current password." : "At least 8 characters. Share it with them privately."}>
            <TextInput value={v.password} onChange={x => set("password", x)} type="text" autoComplete="new-password" />
          </Field>
          {editing ? (
            <label className="check full">
              <input type="checkbox" checked={v.active} onChange={e => set("active", e.target.checked)} />
              Can sign in
            </label>
          ) : null}
        </div>
      </div>
      {EARNS.includes(v.role) || c.payoutPct != null || (c.overrides || []).length ? <div className="form-section">
        <span className="label">Commission</span>
        <div className="form-grid" style={{ marginTop: 8 }}>
          <Field label="Payout %" full hint="Their share of the commission on deals they are credited with. Used when a deal funds; deals already funded keep the rate they funded at.">
            <TextInput value={v.payoutPct} onChange={x => set("payoutPct", x)} inputMode="decimal" placeholder="e.g. 40" />
          </Field>
        </div>
        <div className="label" style={{ marginTop: 14 }}>Overrides on their deals</div>
        <p className="hint" style={{ margin: "4px 0 2px" }}>A team leader's percentage of the commission whenever {v.name.trim() || "this person"} funds a deal. It is paid from the office's share on top of the payout above, and only Admins can see it.</p>
        {overs.map((o, i) => (
          <div className="line-edit" key={i}>
            <Field label="Paid to"><Select value={o.userId} onChange={x => setOver(i, "userId", x)} options={leaders(o.userId, i)} /></Field>
            <Field label="Override %"><TextInput value={o.pct} onChange={x => setOver(i, "pct", x)} inputMode="decimal" placeholder="e.g. 25" /></Field>
            <div className="line-out"><button type="button" className="icon-btn" onClick={() => setOvers(list => list.filter((_, j) => j !== i))} aria-label="Remove this override"><Icon name="trash" size={15} /></button></div>
          </div>
        ))}
        {(users || []).some(x => x.id !== u.id) && overs.length < 5 ? <button type="button" className="btn btn-sm" style={{ marginTop: 10 }} onClick={() => setOvers(list => list.concat([{ userId: "", pct: "" }]))}><Icon name="plus" size={13} />Add an override</button> : null}
        {v.payoutPct.trim() !== "" || overs.length ? <p className="hint" style={{ marginTop: 10 }}>On a $10,000 commission: {v.name.trim() || "they"} get{v.name.trim() ? "s" : ""} {fmt.money(100 * (pct(v.payoutPct) || 0))}{overs.filter(o => o.userId && pct(o.pct) > 0).map(o => `, ${((users || []).find(x => x.id === o.userId) || {}).name} ${fmt.money(100 * pct(o.pct))}`).join("")}, and the office keeps {fmt.money(10000 - 100 * ((pct(v.payoutPct) || 0) + overs.reduce((t, o) => t + (pct(o.pct) || 0), 0)))}.</p> : null}
      </div> : null}
    </Sheet>
  );
}

function TeamView({ me, toast, confirm, onTeamChanged, go }) {
  const [users, setUsers] = useState(null);
  const [comm, setComm] = useState(null);       // funded and commission figures per person, this month
  const [error, setError] = useState("");
  const [sheet, setSheet] = useState(null);
  const load = useCallback(async () => {
    try { setUsers(await API.users()); setError(""); onTeamChanged(); } catch (err) { if (err.status !== 401) setError(err.message); }
    try { setComm(await API.commissions()); } catch (err) { /* the figures are extra; the team list still works */ }
  }, []);
  const figs = (id) => (comm && comm.people.find(p => p.userId === id)) || null;
  const mLabel = comm ? comm.monthLabel.split(" ")[0].slice(0, 3) : "this month";   // "Oct": the full name pushes the table wider than its card
  useEffect(() => { load(); }, []);
  const askDelete = (u) => confirm({
    title: "Remove this team member?", body: `${u.name} will be signed out and won't be able to sign in again.`, confirmLabel: "Remove",
    onConfirm: async () => { await API.deleteUser(u.id); await load(); toast("Team member removed"); },
  });
  return (
    <main className="workspace single">
      <div className="page">
        <div className="page-head">
          <div>
            <h1>Team</h1>
            <p>Who can sign in, whose deals they see, and what they are paid. Click a name for what they have funded and earned.</p>
          </div>
          <div className="page-tools"><button className="btn btn-primary" onClick={() => setSheet({})}><Icon name="plus" size={14} />Add team member</button></div>
        </div>
        {error ? <div className="banner" role="alert">{error} <button className="link-btn" onClick={load}>Try again</button></div> : null}
        {users && !users.length ? (
          <div className="banner info" style={{ marginBottom: 16 }}>
            You're signed in with the first-run passcode. Add yourself as an Admin here — once any team member exists, the passcode stops working.
          </div>
        ) : null}
        <div className="card">
          {users === null && !error ? <div className="empty"><p>Loading team…</p></div>
            : users && !users.length ? <Empty title="No team members yet">Add the first Admin account to replace the shared passcode.</Empty>
            : (
              <div className="table-wrap">
                <table className="list">
                  <thead><tr><th>Name</th><th>Role</th><th>Payout</th><th className="num">Funded in {mLabel}</th><th className="num">Commission in {mLabel}</th><th className="num">Owed</th><th>Sign-in</th><th /></tr></thead>
                  <tbody>
                    {(users || []).map(u => (
                      <tr key={u.id}>
                        <td><div className="who"><Avatar name={u.name} size="sm" /><div>{figs(u.id) ? <button className="link-btn strong" onClick={() => setSheet({ person: figs(u.id) })}>{u.name}{me.id === u.id ? " (you)" : ""}</button> : <b>{u.name}{me.id === u.id ? " (you)" : ""}</b>}<span>{u.email}</span>{u.leaderId && users.some(x => x.id === u.leaderId) ? <span>Team leader: {users.find(x => x.id === u.leaderId).name}</span> : users.some(x => x.leaderId === u.id) ? <span>Leads {users.filter(x => x.leaderId === u.id).map(x => x.name.split(" ")[0]).join(", ")}</span> : null}</div></div></td>
                        <td data-label="Role"><span className="pill plain">{u.role}</span></td>
                        <td data-label="Payout">{u.commission && u.commission.payoutPct != null ? pctText(u.commission.payoutPct) : <span className="muted">Not set</span>}{u.commission && (u.commission.overrides || []).filter(o => users.some(x => x.id === o.userId)).map(o => <span className="sub" key={o.userId}>{users.find(x => x.id === o.userId).name} +{pctText(o.pct)}</span>)}</td>
                        <td className="num tnum" data-label={`Funded in ${mLabel}`}>{figs(u.id) ? <>{fmt.money(figs(u.id).fundedMonth)}<span className="sub">all time {fmt.moneyShort(figs(u.id).fundedTotal)}</span></> : "—"}</td>
                        <td className="num tnum" data-label={`Commission in ${mLabel}`}>{figs(u.id) ? <>{fmt.money(figs(u.id).earnedMonth)}<span className="sub">all time {fmt.money(figs(u.id).earnedTotal)}</span></> : "—"}</td>
                        <td className="num tnum" data-label="Owed">{figs(u.id) ? (figs(u.id).owed ? <b>{fmt.money(figs(u.id).owed)}</b> : fmt.money(0)) : "—"}</td>
                        <td data-label="Sign-in">{u.active === false ? <Pill status="off" label="Disabled" /> : <Pill status="active" label="Active" />}</td>
                        <td className="acts">
                          {lockedFor(me, u) ? <span className="muted" style={{ fontSize: 12.5 }}>Only an Owner can change this account</span> : <>
                            <button className="btn btn-sm" onClick={() => setSheet({ user: u })}>Edit</button>
                            {me.id !== u.id ? <button className="btn btn-sm btn-danger" onClick={() => askDelete(u)}>Remove</button> : null}
                          </>}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
        </div>
      </div>
      {sheet && sheet.person ? <PersonSheet person={sheet.person} data={comm} go={go} onClose={() => setSheet(null)} /> : null}
      {sheet && !sheet.person ? <UserSheet user={sheet.user} users={users || []} me={me} first={!sheet.user && users && !users.length} onClose={() => setSheet(null)} onSaved={() => { const was = sheet.user; setSheet(null); load(); toast(was ? "Changes saved" : "Team member added"); }} /> : null}
    </main>
  );
}

// ── Partners (ISOs) ─────────────────────────────────────────────────────────
function PartnerSheet({ partner, onClose, onSaved }) {
  const editing = !!partner;
  const p = partner || {};
  const [v, set] = useForm({ company: p.company || "", contact: p.contact || "", email: p.email || "", phone: p.phone || "", commissionRate: p.commissionRate ? String(p.commissionRate) : "", active: p.active !== false });
  const save = useSave(onSaved);
  const submit = () => {
    if (!v.company.trim()) return save.setError("Enter the company name.");
    if (!/^\S+@\S+\.\S+$/.test(v.email.trim())) return save.setError("Enter a valid email address.");
    const rate = v.commissionRate.trim() === "" ? 0 : Number(v.commissionRate);
    if (!Number.isFinite(rate) || rate < 0 || rate > 100) return save.setError("Commission should be a percentage between 0 and 100.");
    const body = { company: v.company.trim(), contact: v.contact.trim(), email: v.email.trim(), phone: v.phone.trim(), commissionRate: rate };
    if (editing) body.active = v.active;
    save.run(() => (editing ? API.updatePartner(p.id, body) : API.createPartner(body)));
  };
  return (
    <Sheet eyebrow="Partners" title={editing ? "Edit partner" : "Add partner"} onClose={onClose} onSubmit={submit}
           submitLabel={editing ? "Save partner" : "Add partner"} busy={save.busy} error={save.error}>
      <div className="form-section">
        <div className="form-grid">
          <Field label="Company" full><TextInput value={v.company} onChange={x => set("company", x)} /></Field>
          <Field label="Contact name" full><TextInput value={v.contact} onChange={x => set("contact", x)} /></Field>
          <Field label="Email"><TextInput value={v.email} onChange={x => set("email", x)} type="email" /></Field>
          <Field label="Phone"><TextInput value={v.phone} onChange={x => set("phone", x)} type="tel" inputMode="tel" /></Field>
          <Field label="Commission (%)"><TextInput value={v.commissionRate} onChange={x => set("commissionRate", x)} inputMode="decimal" /></Field>
          {editing ? (
            <label className="check full">
              <input type="checkbox" checked={v.active} onChange={e => set("active", e.target.checked)} />
              Access key works
            </label>
          ) : null}
        </div>
      </div>
    </Sheet>
  );
}

function PartnersView({ toast, confirm }) {
  const [partners, setPartners] = useState(null);
  const [error, setError] = useState("");
  const [sheet, setSheet] = useState(null);
  const [shown, setShown] = useState({});
  const load = useCallback(async () => {
    try { setPartners(await API.partners()); setError(""); } catch (err) { if (err.status !== 401) setError(err.message); }
  }, []);
  useEffect(() => { load(); }, []);
  const copy = async (token) => {
    try { await navigator.clipboard.writeText(token); toast("Access key copied"); }
    catch { toast("Couldn't copy. Show the key and copy it by hand.", true); }
  };
  const askDelete = (p) => confirm({
    title: "Remove this partner?", body: `${p.company}'s access key will stop working. Leads they already sent stay in place.`, confirmLabel: "Remove partner",
    onConfirm: async () => { await API.deletePartner(p.id); await load(); toast("Partner removed"); },
  });
  return (
    <main className="workspace single">
      <div className="page">
        <div className="page-head">
          <div>
            <h1>Partners</h1>
            <p>Brokers and referral partners who send you deals. Each has an access key for the partner portal; what they submit arrives under Leads.</p>
          </div>
          <div className="page-tools"><button className="btn btn-primary" onClick={() => setSheet({})}><Icon name="plus" size={14} />Add partner</button></div>
        </div>
        {error ? <div className="banner" role="alert">{error} <button className="link-btn" onClick={load}>Try again</button></div> : null}
        <div className="card">
          {partners === null && !error ? <div className="empty"><p>Loading partners…</p></div>
            : partners && !partners.length ? <Empty title="No partners yet">Add a partner to give them an access key.</Empty>
            : (
              <div className="table-wrap">
                <table className="list">
                  <thead><tr><th>Company</th><th>Contact</th><th className="num">Commission</th><th>Access key</th><th /></tr></thead>
                  <tbody>
                    {(partners || []).map(p => (
                      <tr key={p.id}>
                        <td><div className="who"><Avatar name={p.company} size="sm" /><div><b>{p.company}</b><span>{p.contact}</span></div></div></td>
                        <td data-label="Contact">{[p.email, p.phone].filter(Boolean).join(" · ")}</td>
                        <td className="num tnum" data-label="Commission">{p.commissionRate ? `${p.commissionRate}%` : ""}</td>
                        <td data-label="Access key">
                          {p.active === false ? <Pill status="off" label="Disabled" /> : (
                            <span style={{ display: "inline-flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                              <span className="token">{shown[p.id] ? p.token : "••••••••" + String(p.token || "").slice(-4)}</span>
                              <button className="icon-btn" style={{ width: 28, height: 28 }} onClick={() => setShown(s => ({ ...s, [p.id]: !s[p.id] }))} aria-label={shown[p.id] ? "Hide access key" : "Show access key"}><Icon name="eye" size={14} /></button>
                              <button className="icon-btn" style={{ width: 28, height: 28 }} onClick={() => copy(p.token)} aria-label="Copy access key"><Icon name="copy" size={14} /></button>
                            </span>
                          )}
                        </td>
                        <td className="acts">
                          <button className="btn btn-sm" onClick={() => setSheet({ partner: p })}>Edit</button>
                          <button className="btn btn-sm btn-danger" onClick={() => askDelete(p)}>Remove</button>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
        </div>
      </div>
      {sheet ? <PartnerSheet partner={sheet.partner} onClose={() => setSheet(null)} onSaved={() => { const was = sheet.partner; setSheet(null); load(); toast(was ? "Partner saved" : "Partner added"); }} /> : null}
    </main>
  );
}

// ── Settings ────────────────────────────────────────────────────────────────
const PREVIEW_CHOICES = [["white", "White", "offer-white.png"], ["blue", "Blue", "offer-blue.png"], ["card", "Card", "offer-card.png"], ["original", "Original", "offer-original.jpg"]];
// One section at a time (Danny, 2026-10-19: "C" on desktop, "B" on phones).
// Desktop: groups on a dark icon rail, the group's sections in a column beside it, the open section on the right.
// Phones: groups that fold open to their sections, with a search box; a section opens full screen with a way back.
// The section is in the address: #/settings/<section>.
const SETTINGS_GROUPS = [
  { id: "alerts", label: "Alerts", icon: "bell" },
  { id: "offers", label: "Offer links", short: "Offers", icon: "link" },
  { id: "connections", label: "Connections", short: "Connect", icon: "spark" },
  { id: "portal", label: "Client portal", short: "Portal", icon: "users" },
  { id: "data", label: "Data", icon: "download" },
];
const SETTINGS_SECTIONS = [
  { id: "alerts", group: "alerts", title: "Office alerts", sub: "Alert email, milestones, renewals, birthdays",
    desc: "Where office alerts go, and which repayment points, renewals and birthdays trigger one." },
  { id: "rep-alerts", group: "alerts", title: "Rep alerts", sub: "When a client opens or accepts",
    desc: "Tell the rep the moment their client opens or accepts an offer link." },
  { id: "offer-links", group: "offers", title: "Number and picture", sub: "Office number, picture on texted links",
    desc: "The number shown on offer links and the picture a phone shows when a link is texted." },
  { id: "sending", group: "offers", title: "Sending", sub: "Text and email from the office",
    desc: "Whether reps can send offer links from the office number and address, or from their own phone and mail." },
  { id: "statements", group: "connections", title: "Statement reading", sub: "Claude, with Workers AI as the fallback", ready: (s) => s.statements && s.statements.available,
    desc: "Reads each bank statement and works out the breakdown. The CRM's own code does the arithmetic." },
  { id: "sms", group: "connections", title: "Text messages", sub: "Twilio", ready: (s) => s.twilio && s.twilio.available,
    desc: "Twilio, used for offer links and alerts." },
  { id: "email", group: "connections", title: "Email", sub: "SMTP", ready: (s) => s.smtp && s.smtp.available,
    desc: "The mailbox offer links and alerts are sent from." },
  { id: "grok", group: "connections", title: "Grok", sub: "API key", ready: (s) => s.grok && s.grok.available,
    desc: "Lets Grok work on leads, deals and clients through the API." },
  { id: "constant-contact", group: "connections", title: "Constant Contact", sub: "Offer emails", ready: (s) => s.constantContact && s.constantContact.connected,
    desc: "Leads with an offer link go into a Constant Contact list, and Constant Contact sends them the offer email." },
  { id: "pii", group: "connections", title: "SSN encryption key", sub: "PII_KEY", ready: (s) => s.pii && s.pii.available,
    desc: "The key SSNs, and the EIN and date of birth of imported leads, are stored encrypted with." },
  { id: "portal", group: "portal", title: "Client portal", sub: "Open or closed, states, bank verification",
    desc: "Merchant sign-up and applications, the states taken online and bank verification." },
  { id: "portal-accounts", group: "portal", title: "Portal accounts", sub: "Merchant sign-ins", wide: true,
    desc: "Merchant accounts: reset sign-in methods, link one to a client, turn one off or delete it." },
  { id: "data-tools", group: "data", title: "Data tools", sub: "Encrypt stored SSNs, deals back to Leads",
    desc: "Housekeeping on the stored records." },
  { id: "tags", group: "data", title: "Tags", sub: "Rename and merge",
    desc: "Every tag in use on leads. Rename a tag onto one that exists to merge the two." },
  { id: "import-layouts", group: "data", title: "Import layouts", sub: "Saved columns and assign rules",
    desc: "Column layouts and assign rules saved from lead imports." },
];
const settingsSection = (id) => SETTINGS_SECTIONS.find(x => x.id === id);
const settingsGroup = (id) => SETTINGS_GROUPS.find(g => g.id === id);
// Phone width (the CRM's 900px break), kept current as the window changes.
function useSettingsPhone() {
  const mq = "(max-width: 900px)";
  const [on, setOn] = useState(() => window.matchMedia(mq).matches);
  useEffect(() => { const m = window.matchMedia(mq); const f = () => setOn(m.matches); m.addEventListener("change", f); return () => m.removeEventListener("change", f); }, []);
  return on;
}

// Settings → Constant Contact (DEC-095): connect the office's account once, pick the list, auto-send.
function ConstantContactCard({ toast, confirm, go, onChanged }) {
  const [st, setSt] = useState(null);
  const [lists, setLists] = useState(null);
  const [newName, setNewName] = useState("");
  const [busy, setBusy] = useState("");
  const load = async () => {
    try { const x = await API.ccStatus(); setSt(x); if (x.connected) API.ccLists().then(setLists).catch(err => { setLists([]); toast(err.message, true); }); }
    catch (err) { toast(err.message, true); setSt({ ready: false }); }
  };
  useEffect(() => {
    // Back from Constant Contact's sign-in: #/settings/constant-contact?cc=connected (or =error&msg=…).
    const q = new URLSearchParams(location.hash.split("?")[1] || "");
    if (q.get("cc") === "connected") { toast("Constant Contact connected"); if (onChanged) onChanged(); }
    else if (q.get("cc") === "error") toast(q.get("msg") || "Constant Contact didn't connect", true);
    if (q.get("cc")) go("settings", "constant-contact", true);
    load();
  }, []);
  const connect = async () => { setBusy("connect"); try { const r = await API.ccConnect(); location.href = r.url; } catch (err) { toast(err.message, true); setBusy(""); } };
  const update = async (change, msg) => { try { await API.ccUpdate(change); await load(); if (onChanged) onChanged(); if (msg) toast(msg); } catch (err) { toast(err.message, true); } };
  const pickList = (id) => { const l = (lists || []).find(x => x.id === id); update({ listId: id || null, listName: l ? l.name : "" }, id ? `Leads will go to ${l ? l.name : "that list"}` : "No list picked"); };
  const makeList = async () => {
    if (!newName.trim()) return;
    setBusy("list");
    try { const l = await API.ccCreateList(newName.trim()); setNewName(""); await update({ listId: l.id, listName: l.name }, `List ${l.name} made and picked`); }
    catch (err) { toast(err.message, true); }
    setBusy("");
  };
  const disconnect = () => confirm({ title: "Disconnect Constant Contact?", body: "The CRM stops sending leads to it. Your contacts and lists stay in Constant Contact.", confirmLabel: "Disconnect",
    onConfirm: async () => { await API.ccDisconnect(); toast("Constant Contact disconnected"); setLists(null); await load(); if (onChanged) onChanged(); } });

  if (!st) return <div className="card"><div className="card-body dim">Loading…</div></div>;
  if (!st.ready) return (
    <div className="card">
      <div className="switch-row"><div><div className="t">Constant Contact <Pill status="neutral" label="Not set up" /></div><div className="d">The CC_CLIENT_SECRET secret (the app's client secret from Constant Contact's developer portal) isn't set on the server yet.</div></div></div>
    </div>);
  if (!st.connected) return (
    <div className="card">
      {st.error ? <div className="banner" role="alert" style={{ margin: 16 }}>The sign-in to Constant Contact lapsed ({st.error}). Connect it again.</div> : null}
      <div className="switch-row">
        <div><div className="t">Constant Contact <Pill status="neutral" label="Not connected" /></div><div className="d">Sign in with the office's Constant Contact account and allow the CRM to add contacts. You come back here when it's done.</div></div>
        <button className="btn btn-primary btn-sm" disabled={busy === "connect"} onClick={connect}>{busy === "connect" ? "Opening…" : "Connect Constant Contact"}</button>
      </div>
    </div>);
  const opts = [{ value: "", label: lists === null ? "Loading lists…" : "Pick a list" }].concat((lists || []).map(l => ({ value: l.id, label: l.count == null ? l.name : `${l.name} (${l.count})` })));
  return (
    <div className="card">
      <div className="switch-row">
        <div><div className="t">Constant Contact <Pill status="active" label="Connected" /></div><div className="d">{st.connectedBy ? `Connected by ${st.connectedBy}` : "Connected"}{st.connectedAt ? ` ${fmt.when(st.connectedAt).replace(/^(Today|Yesterday)/, m => m.toLowerCase())}` : ""}. The CRM made the six fields the offer email reads.</div></div>
        <button className="btn btn-sm" onClick={disconnect}>Disconnect</button>
      </div>
      <div className="card-body" style={{ borderTop: "1px solid var(--rule-soft)" }}>
        <div className="form-grid">
          <Field label="Send leads to this list" hint="Set Constant Contact to send the offer email when a contact joins it.">
            <Select value={st.listId || ""} onChange={pickList} options={opts} aria-label="Constant Contact list" />
          </Field>
          <Field label="Or make a new list">
            <div style={{ display: "flex", gap: 8 }}><input className="input" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === "Enter" && makeList()} placeholder="Offer emails" /><button className="btn" disabled={!newName.trim() || busy === "list"} onClick={makeList}>Make</button></div>
          </Field>
        </div>
      </div>
      <div className="switch-row">
        <div><div className="t">Send automatically</div><div className="d">When offer links are made in bulk (from Leads or an import), those leads go straight to the list. Leads with no email are skipped.</div></div>
        <Switch checked={!!st.autoPush} onChange={on => st.listId || !on ? update({ autoPush: on }, on ? "Auto-send on" : "Auto-send off") : toast("Pick the list first.", true)} label="Send automatically" />
      </div>
      {st.lastPush ? <div className="card-body dim" style={{ fontSize: 12.5, borderTop: "1px solid var(--rule-soft)" }}>Last sent: {st.lastPush.sent} {st.lastPush.sent === 1 ? "lead" : "leads"}{st.lastPush.listName ? ` to ${st.lastPush.listName}` : ""} {fmt.when(st.lastPush.at).replace(/^(Today|Yesterday)/, m => m.toLowerCase())}{st.lastPush.by ? ` · by ${st.lastPush.by}` : ""}</div> : null}
    </div>);
}

function SettingsView({ toast, confirm, onOfficeChanged, section, go }) {
  const [phone, setPhone] = useState("");
  const [s, setS] = useState(null);
  const [error, setError] = useState("");
  const [email, setEmail] = useState("");
  const [days, setDays] = useState("7");
  const [renewPct, setRenewPct] = useState("50");
  const [busy, setBusy] = useState("");
  const [q, setQ] = useState("");
  const phoneView = useSettingsPhone();
  const current = settingsSection(section);
  const [open, setOpen] = useState(() => new Set(current ? [current.group] : []));
  const load = useCallback(async () => {
    try {
      const x = await API.settings();
      setS(x); setEmail(x.alertEmail || ""); setDays(String((x.bday && x.bday.daysBefore) || 7)); setRenewPct(String(x.renewalPct || 50)); setPhone(x.officePhone || ""); setError("");
    } catch (err) { if (err.status !== 401) setError(err.message); }
  }, []);
  useEffect(() => { load(); }, []);
  // An unknown section in the address goes back to the list.
  useEffect(() => { if (section && !current) go("settings", null, true); }, [section]);
  useEffect(() => { if (current) setOpen(prev => prev.has(current.group) ? prev : new Set([...prev, current.group])); }, [section]);

  // Show the change straight away; put it back if the save fails.
  const save = async (change, message) => {
    const before = s;
    setS({ ...s, ...change, milestones: { ...s.milestones, ...(change.milestones || {}) }, bday: { ...s.bday, ...(change.bday || {}) }, twilio: { ...s.twilio, ...(change.twilio || {}) }, smtp: { ...s.smtp, ...(change.smtp || {}) },
      portal: { ...s.portal, ...(change.portal || {}) }, bankVerify: { ...s.bankVerify, ...(change.bankVerify || {}) } });
    try { await API.saveSettings(change); setS(await API.settings()); if (onOfficeChanged) onOfficeChanged(); toast(message || "Settings saved"); }
    catch (err) { setS(before); toast(err.message, true); }
  };
  const saveAlerts = () => {
    if (email.trim() && !/^\S+@\S+\.\S+$/.test(email.trim())) return toast("Enter a valid email address.", true);
    const d = Math.round(Number(days));
    if (!Number.isFinite(d) || d < 1 || d > 60) return toast("Birthday notice should be 1 to 60 days.", true);
    const r = Math.round(Number(renewPct));
    if (!Number.isFinite(r) || r < 1 || r > 100) return toast("Renewal follow-up should be 1 to 100 percent.", true);
    save({ alertEmail: email.trim(), bday: { daysBefore: d }, renewalPct: r }, "Alert settings saved");
  };
  const test = async (kind) => {
    setBusy(kind);
    try { await (kind === "sms" ? API.testSms() : API.testEmail()); toast(kind === "sms" ? "Test text sent" : "Test email sent"); }
    catch (err) { toast(err.detail ? `${err.message}: ${err.detail}` : err.message, true); }
    setBusy("");
  };

  if (error) return <main className="workspace single"><div className="page"><div className="banner" role="alert">{error} <button className="link-btn" onClick={load}>Try again</button></div></div></main>;
  if (!s) return <main className="workspace single"><div className="page"><div className="empty"><p>Loading settings…</p></div></div></main>;
  const ms = s.milestones || {};
  const bd = s.bday || {};
  const ready = (ok) => ok ? <Pill status="active" label="Connected" /> : <Pill status="neutral" label="Not set up" />;
  const secretsNote = <div className="card-body dim" style={{ fontSize: 12.5, borderTop: "1px solid var(--rule-soft)" }}>Keys are stored as secrets on the server and can't be viewed or changed from this page.</div>;

  // Desktop always shows a section (the first one when the address names none); phones show the list until one is picked.
  const shown = current || SETTINGS_SECTIONS[0];
  const shownGroup = shown.group;
  const pick = (id, replace) => go("settings", id, replace);

  const body = (id) => {
    switch (id) {
      case "alerts": return (
        <div className="card">
          <div className="card-body">
            <div className="form-grid">
              <Field label="Send alerts to" full hint="Repayment milestones and birthday reminders go to this address.">
                <TextInput value={email} onChange={setEmail} type="email" placeholder="ops@yourcompany.com" />
              </Field>
              <Field label="Birthday notice (days ahead)"><TextInput value={days} onChange={setDays} inputMode="numeric" /></Field>
              <Field label="Renewal follow-up at (% repaid)" hint="Counts every advance on a client's file, ours and other brokers'. The deal's rep, their team leader and Admins are told once."><TextInput value={renewPct} onChange={setRenewPct} inputMode="numeric" /></Field>
              <div className="field" style={{ alignSelf: "end" }}><button className="btn btn-primary btn-block" style={{ height: 42 }} onClick={saveAlerts}>Save alert settings</button></div>
            </div>
          </div>
          {[["p25", "25% repaid"], ["p50", "50% repaid"], ["p75", "75% repaid"]].map(([k, label]) => (
            <div className="switch-row" key={k} style={k === "p25" ? { borderTop: "1px solid var(--rule-soft)" } : null}>
              <div><div className="t">{label}</div><div className="d">Alert when a funded deal crosses this point. A good time to talk renewal.</div></div>
              <Switch checked={ms[k] !== false} onChange={on => save({ milestones: { [k]: on } })} label={`Alert at ${label}`} />
            </div>
          ))}
          <div className="switch-row">
            <div><div className="t">Renewal follow-ups</div><div className="d">Text and email the rep, their team leader and Admins when an advance reaches the renewal mark.</div></div>
            <Switch checked={s.renewalAlerts !== false} onChange={on => save({ renewalAlerts: on })} label="Renewal follow-up alerts" />
          </div>
          <div className="switch-row">
            <div><div className="t">Client birthdays</div><div className="d">Email a reminder ahead of each owner's birthday.</div></div>
            <Switch checked={bd.enabled !== false} onChange={on => save({ bday: { enabled: on } })} label="Birthday reminders" />
          </div>
        </div>);
      case "rep-alerts": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">Tell the rep when their client opens or accepts</div><div className="d">By text to the number on their profile and by email, when those are set up.</div></div>
            <Switch checked={s.repAlerts !== false} onChange={on => save({ repAlerts: on })} label="Tell the rep when their client opens or accepts" />
          </div>
        </div>);
      case "offer-links": return (
        <div className="card">
          <div className="card-body">
            <div className="form-grid">
              <Field label="Office phone number" hint="Shown on offer links from anyone with no number on their profile.">
                <TextInput value={phone} onChange={setPhone} inputMode="tel" />
              </Field>
              <div className="field" style={{ alignSelf: "start", marginTop: 21 }}><button className="btn btn-block" style={{ height: 42 }} onClick={() => save({ officePhone: phone.trim() }, "Office number saved")}>Save number</button></div>
            </div>
            <div className="label" style={{ marginTop: 16 }}>Picture shown when a link is texted</div>
            <div className="linkpic-picks" role="radiogroup" aria-label="Picture shown when a link is texted">
              {PREVIEW_CHOICES.map(([pid, label, file]) => (
                <button key={pid} type="button" role="radio" aria-checked={(s.previewImage || "white") === pid} className="linkpic" onClick={() => save({ previewImage: pid }, "Link picture changed")}>
                  <img src={`/og/${file}`} alt="" loading="lazy" /><span>{label}</span>
                </button>
              ))}
            </div>
          </div>
        </div>);
      case "sending": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">Text links from the office number</div><div className="d">{s.twilio && s.twilio.available ? "Reps can text a client their offer link from the CRM. The text is logged on the deal." : "Not set up yet. Until it is, reps send links from their own phone."}</div></div>
            <Switch checked={!!(s.twilio && s.twilio.connected)} onChange={on => s.twilio && s.twilio.available ? save({ twilio: { connected: on } }) : toast("Text messages aren't set up yet.", true)} label="Text links from the office number" />
          </div>
          <div className="switch-row">
            <div><div className="t">Email links from the office address</div><div className="d">{s.smtp && s.smtp.available ? "Reps can email a client their offer link from the CRM." : "Not set up yet. Until it is, reps send links from their own mail."}</div></div>
            <Switch checked={!!(s.smtp && s.smtp.connected)} onChange={on => s.smtp && s.smtp.available ? save({ smtp: { connected: on } }) : toast("Email isn't set up yet.", true)} label="Email links from the office address" />
          </div>
        </div>);
      case "statements": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">Statement reading {ready(s.statements && s.statements.available)}</div><div className="d">Each bank statement PDF is read as it is uploaded. Statements that arrive on a lead are read when someone presses Analyze statements, or automatically on lead-pack imports for leads stating $100,000+ a month.</div></div>
          </div>
          <div className="switch-row">
            <div><div className="t">Claude {ready(s.claude && s.claude.available)}</div><div className="d">The ANTHROPIC_API_KEY secret. Haiku reads first; Sonnet reads again when a statement doesn't tie out. Without it, or when Claude can't answer, Workers AI reads.</div></div>
          </div>
          {secretsNote}
        </div>);
      case "sms": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">Text messages {ready(s.twilio && s.twilio.available)}</div><div className="d">Twilio. Used for offer links and alerts.</div></div>
            <button className="btn btn-sm" disabled={!(s.twilio && s.twilio.available) || busy === "sms"} onClick={() => test("sms")}>{busy === "sms" ? "Sending…" : "Send test"}</button>
          </div>
          {secretsNote}
        </div>);
      case "email": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">Email {ready(s.smtp && s.smtp.available)}</div><div className="d">SMTP. The test goes to the alert address under Alerts.</div></div>
            <button className="btn btn-sm" disabled={!(s.smtp && s.smtp.available) || busy === "email"} onClick={() => test("email")}>{busy === "email" ? "Sending…" : "Send test"}</button>
          </div>
          {secretsNote}
        </div>);
      case "grok": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">Grok {ready(s.grok && s.grok.available)}</div><div className="d">Lets Grok find and update leads, deals and clients and log lender offers, notes and documents at <span className="token">{location.origin}/api/grok</span>.</div></div>
          </div>
          {secretsNote}
        </div>);
      case "pii": return (
        <div className="card">
          <div className="switch-row">
            <div><div className="t">SSN encryption key {ready(s.pii && s.pii.available)}</div><div className="d">The PII_KEY secret. SSNs, and the EIN and date of birth of imported leads, are stored encrypted with it. Data tools checks the stored records against it.</div></div>
            <button className="btn btn-sm" onClick={() => pick("data-tools")}>Data tools</button>
          </div>
          {secretsNote}
        </div>);
      case "constant-contact": return <ConstantContactCard toast={toast} confirm={confirm} go={go} onChanged={() => { load(); if (onOfficeChanged) onOfficeChanged(); }} />;
      case "portal": return <PortalSettingsCard s={s} save={save} />;
      case "portal-accounts": return <PortalAccountsCard toast={toast} confirm={confirm} />;
      case "data-tools": return <DataToolsCard s={s} toast={toast} confirm={confirm} />;
      case "tags": return <TagsCard toast={toast} confirm={confirm} />;
      case "import-layouts": return <ImportLayoutsCard toast={toast} />;
      default: return null;
    }
  };
  const status = (x) => x.ready ? (x.ready(s) ? <span className="set-dot ok" role="img" aria-label="Connected" /> : <span className="set-dot off" role="img" aria-label="Not set up" />) : null;

  const sectionPage = (x, phoneBack) => (
    <div className={`set-page${x.wide ? " wide" : ""}`}>
      {phoneBack ? <button className="back" onClick={() => go("settings")}><Icon name="back" size={15} />Settings</button> : null}
      <div className="set-eyebrow">{settingsGroup(x.group).label}</div>
      <h1 className="set-title">{x.title}</h1>
      <p className="set-desc">{x.desc}</p>
      <div className="set-body" key={x.id}>{body(x.id)}</div>
    </div>
  );

  // Phones: the folding list, or a search across every section.
  const needle = q.trim().toLowerCase();
  const hits = needle ? SETTINGS_SECTIONS.filter(x => (x.title + " " + x.sub + " " + x.desc + " " + settingsGroup(x.group).label).toLowerCase().includes(needle)) : null;
  const toggle = (gid) => setOpen(prev => { const n = new Set(prev); n.has(gid) ? n.delete(gid) : n.add(gid); return n; });
  const row = (x) => (
    <button key={x.id} className="set-mrow" onClick={() => pick(x.id)}>
      <span className="txt"><span className="t">{x.title}</span><span className="d">{x.sub}</span></span>
      {status(x)}
      <Icon name="chev" size={15} />
    </button>
  );

  if (phoneView) {
    if (current) return <main className="workspace single settings-phone"><div className="page">{sectionPage(current, true)}</div></main>;
    return (
      <main className="workspace single settings-phone">
        <div className="page">
          <div className="page-head"><div><h1>Settings</h1></div></div>
          <label className="set-search"><Icon name="search" size={17} /><input type="search" value={q} onChange={e => setQ(e.target.value)} placeholder="Search settings" aria-label="Search settings" /></label>
          {hits ? (
            <div className="card set-mlist">{hits.length ? hits.map(row) : <div className="card-body dim">No setting matches “{q.trim()}”.</div>}</div>
          ) : (
            <div className="card set-mlist">
              {SETTINGS_GROUPS.map(g => {
                const isOpen = open.has(g.id);
                const items = SETTINGS_SECTIONS.filter(x => x.group === g.id);
                return (
                  <div key={g.id} className="set-mgroup">
                    <button className="set-mhead" aria-expanded={isOpen} onClick={() => toggle(g.id)}>
                      <Icon name={g.icon} size={19} /><span className="t">{g.label}</span><span className="n">{items.length}</span>
                      <span className={`chev${isOpen ? " open" : ""}`}><Icon name="chev" size={15} /></span>
                    </button>
                    {isOpen ? <div className="set-mitems">{items.map(row)}</div> : null}
                  </div>
                );
              })}
            </div>
          )}
        </div>
      </main>
    );
  }

  return (
    <main className="workspace settings-ws">
      <nav className="set-rail" aria-label="Settings groups">
        {SETTINGS_GROUPS.map(g => (
          <button key={g.id} aria-current={g.id === shownGroup ? "true" : undefined} onClick={() => pick(SETTINGS_SECTIONS.find(x => x.group === g.id).id, true)}>
            <Icon name={g.icon} size={21} /><span>{g.short || g.label}</span>
          </button>
        ))}
      </nav>
      <nav className="set-sub scroll" aria-label={`${settingsGroup(shownGroup).label} settings`}>
        <h2>{settingsGroup(shownGroup).label}</h2>
        {SETTINGS_SECTIONS.filter(x => x.group === shownGroup).map(x => (
          <button key={x.id} className="set-item" aria-current={x.id === shown.id ? "page" : undefined} onClick={() => pick(x.id, true)}>
            <span className="t">{x.title}{status(x)}</span><span className="d">{x.sub}</span>
          </button>
        ))}
      </nav>
      <div className="set-main scroll">{sectionPage(shown, false)}</div>
    </main>
  );
}

Object.assign(window, { TeamView, PartnersView, SettingsView });
