// portal.jsx — Supabase-backed client portal

const SUPABASE_URL = 'https://qwomjwrcebgobrptkwca.supabase.co';
const SUPABASE_KEY = 'sb_publishable_g7xbregQB43yyPMl9mN93w_ujBljlTn';
const sb = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);

const PORTAL_HERO = 'assets/portal-hero.jpg';

// ---- helpers ----
async function loadImage(file) {
  return new Promise((res, rej) => {
    const i = new Image();
    i.onload = () => res(i);
    i.onerror = rej;
    i.src = URL.createObjectURL(file);
  });
}

function isPanoramaImage(img) {
  if (!img || !img.width || !img.height) return false;
  const aspect = img.width / img.height;
  return aspect >= 1.95 && aspect <= 2.05;
}

async function compressImage(file, maxWidth = 1920, quality = 0.85) {
  if (!file.type.startsWith('image/')) return file;
  const img = await loadImage(file);
  const scale = Math.min(1, maxWidth / img.width);
  const w = Math.round(img.width * scale);
  const h = Math.round(img.height * scale);
  const canvas = document.createElement('canvas');
  canvas.width = w; canvas.height = h;
  canvas.getContext('2d').drawImage(img, 0, 0, w, h);
  const blob = await new Promise(res => canvas.toBlob(res, 'image/jpeg', quality));
  return new File([blob], file.name.replace(/\.\w+$/, '.jpg'), { type: 'image/jpeg' });
}

const fmtMoney = (cents) =>
  ((cents || 0) / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' });

function formatBytes(n) {
  if (n == null || isNaN(n)) return '';
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
  if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
  return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}

async function attachSizes(bucket, rows, projectId) {
  if (!rows || !rows.length) return rows || [];
  const { data } = await sb.storage.from(bucket).list(projectId + '/', { limit: 1000 });
  const sizeByName = new Map((data || []).map(o => [o.name, o.metadata?.size]));
  return rows.map(r => {
    const fileName = r.storage_path ? r.storage_path.split('/').pop() : null;
    return { ...r, _size: fileName ? sizeByName.get(fileName) : null };
  });
}

const STATUS_OPTIONS = [
  'Pre-construction',
  'Site Prep',
  'Foundation',
  'Framing',
  'Exterior',
  'Mechanicals',
  'Drywall',
  'Finishes',
  'Punchlist',
  'Complete',
];

const STATUS_PROGRESS = {
  'Pre-construction': 5,
  'Site Prep': 15,
  'Foundation': 25,
  'Framing': 40,
  'Exterior': 55,
  'Mechanicals': 65,
  'Drywall': 75,
  'Finishes': 85,
  'Punchlist': 95,
  'Complete': 100,
};
const statusPct = (status) => STATUS_PROGRESS[status] ?? 0;

function ProgressDonut({ percent, size = 48 }) {
  const stroke = 5;
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const pct = Math.max(0, Math.min(100, percent || 0));
  const offset = c * (1 - pct / 100);
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="progress-donut" aria-hidden="true">
      <circle cx={size / 2} cy={size / 2} r={r} className="progress-donut__track"
        fill="none" strokeWidth={stroke} />
      <circle cx={size / 2} cy={size / 2} r={r} className="progress-donut__fill"
        fill="none" strokeWidth={stroke} strokeLinecap="round"
        strokeDasharray={c} strokeDashoffset={offset}
        transform={`rotate(-90 ${size / 2} ${size / 2})`} />
      <text x="50%" y="50%" textAnchor="middle" dominantBaseline="central"
        className="progress-donut__label">{pct}%</text>
    </svg>
  );
}

// ---- Session hook ----
function useSession() {
  const [session, setSession] = React.useState(null);
  const [profile, setProfile] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let mounted = true;
    sb.auth.getSession().then(({ data }) => {
      if (!mounted) return;
      setSession(data.session);
      if (!data.session) setLoading(false);
    });
    const { data: sub } = sb.auth.onAuthStateChange((_e, s) => {
      setSession(s);
      if (!s) { setProfile(null); setLoading(false); }
    });
    return () => { mounted = false; sub.subscription.unsubscribe(); };
  }, []);

  // Fetch the profile once per user-id change (not every session ref change).
  // Token refreshes produce a new session object but the same user id, so this
  // avoids a brief 'loading' flicker that would unmount the dashboard and
  // wipe all of its in-memory state (active project, current tab, etc).
  const userId = session?.user?.id || null;
  React.useEffect(() => {
    if (!userId) return;
    let alive = true;
    setLoading(prev => (profile ? prev : true));
    sb.from('profiles').select('*').eq('id', userId).single()
      .then(({ data, error }) => {
        if (!alive) return;
        if (error) console.error(error);
        setProfile(data);
        setLoading(false);
      });
    return () => { alive = false; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId]);

  return { session, profile, loading };
}

// ---- Login form ----
function PortalLoginForm() {
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');
  const [error, setError] = React.useState('');
  const [info, setInfo] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setError(''); setInfo(''); setBusy(true);
    const { error } = await sb.auth.signInWithPassword({ email, password });
    if (error) setError(error.message);
    setBusy(false);
  };

  const sendMagicLink = async () => {
    if (!email) { setError('Enter your email above first.'); return; }
    setError(''); setInfo(''); setBusy(true);
    const { error } = await sb.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: window.location.origin + '/', shouldCreateUser: false },
    });
    if (error) setError(error.message);
    else setInfo('Check your email for a fresh sign-in link.');
    setBusy(false);
  };

  return (
    <React.Fragment>
      <PageHero
        title="Client Portal"
        subtitle="Sign in to view your project photos, invoices, and files."
        bg={PORTAL_HERO}
        bgPosition="center 30%"
        hidePattern
      />
      <section className="section portal-section" style={{ display: 'grid', placeItems: 'center' }}>
        <div style={{ maxWidth: 460, width: '100%' }}>
          <InstallAppBanner />
        </div>
        <form className="form" onSubmit={submit} style={{ maxWidth: 460, width: '100%' }}>
          <div className="form__field">
            <label className="form__label">Email</label>
            <input className="form__input" type="email" required autoComplete="username"
              value={email} onChange={(e) => setEmail(e.target.value)} />
          </div>
          <div className="form__field">
            <label className="form__label">Password</label>
            <input className="form__input" type="password" required autoComplete="current-password"
              value={password} onChange={(e) => setPassword(e.target.value)} />
          </div>
          {error && <p style={{ color: '#c0392b', marginTop: 8, fontSize: '0.9rem' }}>{error}</p>}
          {info && <p style={{ color: '#27ae60', marginTop: 8, fontSize: '0.9rem' }}>{info}</p>}
          <div style={{ marginTop: 10, marginBottom: 6 }}>
            <button type="button" className="portal-link-btn" disabled={busy}
              style={{ fontSize: '0.85rem' }} onClick={sendMagicLink}>
              Forgot password / link expired?
            </button>
          </div>
          <button type="submit" className="btn btn--solid" disabled={busy} style={{ marginTop: 8 }}>
            {busy ? 'Signing in…' : 'Sign In'}
          </button>
          <p style={{ marginTop: 18, fontSize: '0.85rem', opacity: 0.7 }}>
            Need access? Contact your Riggins Design + Build representative.
          </p>
        </form>
      </section>
    </React.Fragment>
  );
}

// ---- Install-as-app banner ----
// Shown inside the portal so logged-in clients can install the PWA to their
// home screen / desktop. Hides itself when already running as an installed
// PWA, when the user has dismissed it, or when the platform offers no install
// path (e.g. desktop Firefox).
function InstallAppBanner() {
  const [deferredPrompt, setDeferredPrompt] = React.useState(null);
  const [installed, setInstalled] = React.useState(false);
  const [showIosTip, setShowIosTip] = React.useState(false);
  const [dismissed, setDismissed] = React.useState(() => {
    try { return localStorage.getItem('rdb_install_dismissed') === '1'; }
    catch (e) { return false; }
  });

  const isStandalone = React.useMemo(() => (
    (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) ||
    window.navigator.standalone === true
  ), []);

  React.useEffect(() => {
    const onBefore = (e) => { e.preventDefault(); setDeferredPrompt(e); };
    const onInstalled = () => { setInstalled(true); setDeferredPrompt(null); };
    window.addEventListener('beforeinstallprompt', onBefore);
    window.addEventListener('appinstalled', onInstalled);
    return () => {
      window.removeEventListener('beforeinstallprompt', onBefore);
      window.removeEventListener('appinstalled', onInstalled);
    };
  }, []);

  if (isStandalone || installed || dismissed) return null;

  const ua = navigator.userAgent || '';
  const canPrompt = !!deferredPrompt;
  const isIosLike = /iPad|iPhone|iPod/.test(ua) && !window.MSStream;
  // iPadOS Safari in desktop mode reports a Mac UA; detect via touch points.
  const isIpadDesktopMode = /Mac/.test(ua) && navigator.maxTouchPoints > 1;
  const isMacSafari =
    /Mac/.test(ua) && /Safari/.test(ua) &&
    !/Chrome|CriOS|EdgiOS|FxiOS|OPR/.test(ua) &&
    !isIpadDesktopMode;
  const isAppleSafari =
    (isIosLike && /Safari/.test(ua) && !/CriOS|FxiOS|EdgiOS/.test(ua)) ||
    isIpadDesktopMode ||
    isMacSafari;

  // No install path on this device/browser — don't show anything.
  if (!canPrompt && !isAppleSafari) return null;

  const isMacOnly = isMacSafari;

  const dismiss = () => {
    try { localStorage.setItem('rdb_install_dismissed', '1'); } catch (e) { /* ignore */ }
    setDismissed(true);
  };

  const install = async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    try { await deferredPrompt.userChoice; } catch (e) { /* ignore */ }
    setDeferredPrompt(null);
  };

  return (
    <div style={{
      position: 'relative',
      marginBottom: 20,
      padding: '14px 36px 14px 16px',
      border: '1px solid var(--gold)',
      borderRadius: 10,
      background: 'rgba(184, 153, 104, 0.08)',
      display: 'flex',
      alignItems: 'center',
      gap: 14,
      flexWrap: 'wrap',
    }}>
      <div aria-hidden style={{
        width: 44, height: 44, flexShrink: 0,
        borderRadius: 10,
        background: 'var(--navy)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        color: 'var(--gold)',
      }}>
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="6" y="2" width="12" height="20" rx="2" />
          <line x1="12" y1="18" x2="12.01" y2="18" />
        </svg>
      </div>
      <div style={{ flex: '1 1 220px', minWidth: 0 }}>
        <div style={{ fontWeight: 600, color: 'var(--navy)', marginBottom: 2 }}>
          Install the Riggins Portal app
        </div>
        <div style={{ fontSize: 14, opacity: 0.8 }}>
          {isMacOnly
            ? 'Add it to your Dock for one-click access — no browser tab needed.'
            : 'Add it to your home screen for one-tap access — no browser needed.'}
        </div>
        {showIosTip && (
          <ol style={{ marginTop: 10, paddingLeft: 18, fontSize: 14, lineHeight: 1.7, color: 'var(--navy)' }}>
            <li>
              Click/tap the <strong>Share</strong> button{' '}
              {isMacOnly ? 'in Safari\'s toolbar' : '(square with an up-arrow) at the bottom of Safari'}.
            </li>
            <li>
              Choose <strong>{isMacOnly ? 'Add to Dock' : 'Add to Home Screen'}</strong>.
            </li>
            <li>
              Confirm by clicking/tapping <strong>Add</strong>. The Riggins Portal icon will appear on your{' '}
              {isMacOnly ? 'Dock' : 'home screen'}.
            </li>
          </ol>
        )}
      </div>
      <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
        {canPrompt ? (
          <button type="button" className="btn btn--solid" onClick={install}>Install app</button>
        ) : (
          <button type="button" className="btn btn--solid" onClick={() => setShowIosTip(s => !s)}>
            {showIosTip ? 'Hide' : 'How to install'}
          </button>
        )}
      </div>
      <button
        type="button"
        onClick={dismiss}
        aria-label="Dismiss"
        style={{
          position: 'absolute', top: 6, right: 10,
          background: 'transparent', border: 'none', cursor: 'pointer',
          fontSize: 22, lineHeight: 1, color: 'var(--navy)', opacity: 0.5, padding: 4,
        }}
      >×</button>
    </div>
  );
}

// ---- Shared page shell ----
function PortalShell({ title, subtitle, children }) {
  const [showAccount, setShowAccount] = React.useState(false);
  if (showAccount) {
    return <AccountPage onBack={() => setShowAccount(false)} />;
  }
  return (
    <React.Fragment>
      <PageHero title={title || 'Client Portal'} subtitle={subtitle} bg={PORTAL_HERO} bgPosition="center 30%" hidePattern />
      <section className="section portal-section">
        <div className="portal-section__inner">
          <InstallAppBanner />
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 24 }}>
            <button className="btn btn--solid" onClick={() => setShowAccount(true)}>Account</button>
          </div>
          {children}
        </div>
      </section>
    </React.Fragment>
  );
}

function AccountPage({ onBack }) {
  const [profile, setProfile] = React.useState(null);
  const [email, setEmail] = React.useState('');
  const [name, setName] = React.useState('');
  const [phone, setPhone] = React.useState('');
  const [address, setAddress] = React.useState('');
  const [savingContact, setSavingContact] = React.useState(false);
  const [contactMsg, setContactMsg] = React.useState('');
  const [pw, setPw] = React.useState('');
  const [pw2, setPw2] = React.useState('');
  const [pwBusy, setPwBusy] = React.useState(false);
  const [pwMsg, setPwMsg] = React.useState('');
  const [pwErr, setPwErr] = React.useState('');
  const [signatureImage, setSignatureImage] = React.useState(null);
  const [savingSig, setSavingSig] = React.useState(false);
  const [sigMsg, setSigMsg] = React.useState('');

  React.useEffect(() => {
    let alive = true;
    sb.auth.getUser().then(({ data: { user } }) => {
      if (!alive || !user) return;
      setEmail(user.email || '');
      sb.from('profiles').select('*').eq('id', user.id).single()
        .then(({ data }) => {
          if (!alive) return;
          setProfile(data || null);
          setName(data?.full_name || '');
          setPhone(data?.phone || '');
          setAddress(data?.address || '');
          setSignatureImage(data?.signature_image || null);
        });
    });
    return () => { alive = false; };
  }, []);

  const saveContact = async () => {
    if (!profile) return;
    setSavingContact(true); setContactMsg('');
    const update = {
      full_name: name.trim() || null,
      phone: phone.trim() || null,
      address: address.trim() || null,
    };
    const { error } = await sb.from('profiles').update(update).eq('id', profile.id);
    setSavingContact(false);
    if (error) { setContactMsg('Could not save: ' + error.message); return; }
    setContactMsg('Saved.');
    setTimeout(() => setContactMsg(''), 2000);
  };

  const submitPw = async (e) => {
    e.preventDefault();
    setPwErr(''); setPwMsg('');
    if (pw.length < 8) return setPwErr('Password must be at least 8 characters.');
    if (pw !== pw2) return setPwErr('Passwords do not match.');
    setPwBusy(true);
    const { error } = await sb.auth.updateUser({ password: pw });
    setPwBusy(false);
    if (error) return setPwErr(error.message);
    setPw(''); setPw2('');
    setPwMsg('Password updated.');
    setTimeout(() => setPwMsg(''), 2500);
  };

  const handleSaveSignature = async (dataUrl) => {
    if (!profile) return;
    setSavingSig(true); setSigMsg('');
    const { error } = await sb.from('profiles').update({ signature_image: dataUrl }).eq('id', profile.id);
    setSavingSig(false);
    if (error) { setSigMsg('Could not save: ' + error.message); return; }
    setSignatureImage(dataUrl);
    setSigMsg('Signature saved.');
    setTimeout(() => setSigMsg(''), 2500);
  };

  const handleClearSignature = async () => {
    if (!profile) return;
    if (!confirm('Remove your saved signature?')) return;
    setSavingSig(true); setSigMsg('');
    const { error } = await sb.from('profiles').update({ signature_image: null }).eq('id', profile.id);
    setSavingSig(false);
    if (error) { setSigMsg('Could not clear: ' + error.message); return; }
    setSignatureImage(null);
  };

  return (
    <React.Fragment>
      <PageHero title="Account" subtitle="Update your contact info, password, and signature." bg={PORTAL_HERO} bgPosition="center 30%" hidePattern />
      <section className="section portal-section">
        <div className="portal-section__inner account-page">
          <div style={{ marginBottom: 24 }}>
            <button className="btn" onClick={onBack}>← Back</button>
          </div>

          <div className="account-section">
            <h3>Contact</h3>
            <div className="form__field">
              <label className="form__label">Full name</label>
              <input className="form__input" type="text" value={name}
                onChange={(e) => setName(e.target.value)} />
            </div>
            <div className="form__field">
              <label className="form__label">Email</label>
              <input className="form__input" type="email" value={email} readOnly disabled />
            </div>
            <div className="form__field">
              <label className="form__label">Phone</label>
              <input className="form__input" type="tel" value={phone}
                placeholder="(555) 123-4567"
                onChange={(e) => setPhone(e.target.value)} />
            </div>
            <div className="form__field">
              <label className="form__label">Address</label>
              <input className="form__input" type="text" value={address}
                placeholder="Street, city, state ZIP"
                onChange={(e) => setAddress(e.target.value)} />
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8 }}>
              <button className="btn btn--solid btn--sm" onClick={saveContact} disabled={savingContact || !profile}>
                {savingContact ? 'Saving…' : 'Save'}
              </button>
              {contactMsg && <span style={{ fontSize: '0.85rem', opacity: 0.8 }}>{contactMsg}</span>}
            </div>
          </div>

          <div className="account-section">
            <h3>Password</h3>
            <form className="form" onSubmit={submitPw}>
              <div className="form__field">
                <label className="form__label">New password</label>
                <input className="form__input" type="password" autoComplete="new-password"
                  value={pw} onChange={(e) => setPw(e.target.value)} />
              </div>
              <div className="form__field">
                <label className="form__label">Confirm password</label>
                <input className="form__input" type="password" autoComplete="new-password"
                  value={pw2} onChange={(e) => setPw2(e.target.value)} />
              </div>
              {pwErr && <p style={{ color: '#c0392b', marginTop: 8, fontSize: '0.9rem' }}>{pwErr}</p>}
              {pwMsg && <p style={{ color: '#27ae60', marginTop: 8, fontSize: '0.9rem' }}>{pwMsg}</p>}
              <button type="submit" className="btn btn--solid" disabled={pwBusy} style={{ marginTop: 12 }}>
                {pwBusy ? 'Saving…' : 'Update password'}
              </button>
            </form>
          </div>

          <div className="account-section">
            <h3>Signature</h3>
            <p className="account-section__hint">
              Draw your signature once and reuse it when signing selection phases. You can also keep typing your name when signing if you prefer.
            </p>
            {signatureImage && (
              <div className="signature-saved">
                <span className="signature-saved__label">Saved signature</span>
                <img src={signatureImage} alt="Saved signature" className="signature-saved__img" />
                <button type="button" className="btn" onClick={handleClearSignature} disabled={savingSig}>
                  Remove
                </button>
              </div>
            )}
            <SignaturePad onSave={handleSaveSignature} busy={savingSig} />
            {sigMsg && <p style={{ marginTop: 10, fontSize: '0.9rem', opacity: 0.8 }}>{sigMsg}</p>}
          </div>

          <div className="account-section account-section--danger">
            <button className="btn btn--solid btn--signout" onClick={() => sb.auth.signOut()}>
              Sign out
            </button>
          </div>
        </div>
      </section>
    </React.Fragment>
  );
}

function SignaturePad({ onSave, busy, height = 180 }) {
  const canvasRef = React.useRef(null);
  const drawingRef = React.useRef(false);
  const lastPointRef = React.useRef(null);
  const [hasInk, setHasInk] = React.useState(false);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const setupCanvas = () => {
      const ratio = window.devicePixelRatio || 1;
      const rect = canvas.getBoundingClientRect();
      canvas.width = rect.width * ratio;
      canvas.height = rect.height * ratio;
      const ctx = canvas.getContext('2d');
      ctx.scale(ratio, ratio);
      ctx.lineWidth = 2.2;
      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';
      ctx.strokeStyle = '#1f3a5f';
    };
    setupCanvas();
    window.addEventListener('resize', setupCanvas);
    return () => window.removeEventListener('resize', setupCanvas);
  }, []);

  const getPoint = (e) => {
    const canvas = canvasRef.current;
    const rect = canvas.getBoundingClientRect();
    const touch = e.touches && e.touches[0];
    const clientX = touch ? touch.clientX : e.clientX;
    const clientY = touch ? touch.clientY : e.clientY;
    return { x: clientX - rect.left, y: clientY - rect.top };
  };

  const startDraw = (e) => {
    e.preventDefault();
    drawingRef.current = true;
    lastPointRef.current = getPoint(e);
  };

  const draw = (e) => {
    if (!drawingRef.current) return;
    e.preventDefault();
    const ctx = canvasRef.current.getContext('2d');
    const p = getPoint(e);
    const last = lastPointRef.current;
    if (last) {
      ctx.beginPath();
      ctx.moveTo(last.x, last.y);
      ctx.lineTo(p.x, p.y);
      ctx.stroke();
    }
    lastPointRef.current = p;
    if (!hasInk) setHasInk(true);
  };

  const endDraw = () => {
    drawingRef.current = false;
    lastPointRef.current = null;
  };

  const clear = () => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    setHasInk(false);
  };

  const save = () => {
    if (!hasInk) return;
    const canvas = canvasRef.current;
    const exported = trimSignature(canvas);
    onSave(exported || canvas.toDataURL('image/png'));
    clear();
  };

  return (
    <div className="signature-pad">
      <div className="signature-pad__frame" style={{ height }}>
        <canvas
          ref={canvasRef}
          className="signature-pad__canvas"
          onMouseDown={startDraw}
          onMouseMove={draw}
          onMouseUp={endDraw}
          onMouseLeave={endDraw}
          onTouchStart={startDraw}
          onTouchMove={draw}
          onTouchEnd={endDraw}
        />
        {!hasInk && (
          <div className="signature-pad__cue" aria-hidden="true">
            <span className="signature-pad__cross">×</span>
            <span className="signature-pad__hint">Sign here</span>
          </div>
        )}
      </div>
      <div className="signature-pad__actions">
        <button type="button" className="signature-pad__link" onClick={save} disabled={!hasInk || busy}>
          {busy ? 'Saving…' : 'Save'}
        </button>
        <button type="button" className="signature-pad__link" onClick={clear} disabled={!hasInk || busy}>
          Clear
        </button>
      </div>
    </div>
  );
}

// Crop transparent pixels from a signature canvas so the saved image is tight.
function trimSignature(canvas) {
  const ctx = canvas.getContext('2d');
  const w = canvas.width, h = canvas.height;
  const img = ctx.getImageData(0, 0, w, h);
  let top = h, bottom = 0, left = w, right = 0;
  let found = false;
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const a = img.data[(y * w + x) * 4 + 3];
      if (a > 0) {
        found = true;
        if (y < top) top = y;
        if (y > bottom) bottom = y;
        if (x < left) left = x;
        if (x > right) right = x;
      }
    }
  }
  if (!found) return null;
  const pad = 6;
  top = Math.max(0, top - pad);
  left = Math.max(0, left - pad);
  bottom = Math.min(h - 1, bottom + pad);
  right = Math.min(w - 1, right + pad);
  const cw = right - left + 1, ch = bottom - top + 1;
  const out = document.createElement('canvas');
  out.width = cw; out.height = ch;
  out.getContext('2d').drawImage(canvas, left, top, cw, ch, 0, 0, cw, ch);
  return out.toDataURL('image/png');
}

function ChangePasswordModal({ onClose }) {
  const [pw, setPw] = React.useState('');
  const [pw2, setPw2] = React.useState('');
  const [error, setError] = React.useState('');
  const [info, setInfo] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setError(''); setInfo('');
    if (pw.length < 8) return setError('Password must be at least 8 characters.');
    if (pw !== pw2) return setError('Passwords do not match.');
    setBusy(true);
    const { error } = await sb.auth.updateUser({ password: pw });
    setBusy(false);
    if (error) return setError(error.message);
    setInfo('Password updated.');
    setTimeout(onClose, 1200);
  };

  return (
    <div className="selections-sign-modal" onClick={onClose}>
      <div className="selections-sign-modal__card" onClick={(e) => e.stopPropagation()}>
        <h3>Change Password</h3>
        <form className="form" onSubmit={submit}>
          <div className="form__field">
            <label className="form__label">New Password</label>
            <input className="form__input" type="password" required autoComplete="new-password"
              value={pw} onChange={(e) => setPw(e.target.value)} autoFocus />
          </div>
          <div className="form__field">
            <label className="form__label">Confirm Password</label>
            <input className="form__input" type="password" required autoComplete="new-password"
              value={pw2} onChange={(e) => setPw2(e.target.value)} />
          </div>
          {error && <p style={{ color: '#c0392b', marginTop: 8, fontSize: '0.9rem' }}>{error}</p>}
          {info && <p style={{ color: '#27ae60', marginTop: 8, fontSize: '0.9rem' }}>{info}</p>}
          <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
            <button type="submit" className="btn btn--solid" disabled={busy}>
              {busy ? 'Saving…' : 'Save'}
            </button>
            <button type="button" className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ---- Customer dashboard ----
function CustomerDashboard({ profile }) {
  const [projects, setProjects] = React.useState([]);
  const [activeId, setActiveId] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    (async () => {
      const { data } = await sb.from('projects').select('*')
        .is('archived_at', null)
        .order('created_at', { ascending: false });
      setProjects(data || []);
      if (data && data.length === 1) setActiveId(data[0].id);
      setLoading(false);
    })();
  }, [profile.id]);

  if (loading) return <PortalShell><p>Loading…</p></PortalShell>;

  if (projects.length === 0) {
    return (
      <PortalShell
        title={`Welcome, ${profile.full_name || ''}`}
        subtitle="Your project hasn't been set up yet. Your Riggins representative will be in touch shortly.">
        <p style={{ textAlign: 'center', opacity: 0.7 }}>No project assigned yet.</p>
      </PortalShell>
    );
  }

  if (activeId) {
    const proj = projects.find(p => p.id === activeId);
    if (!proj) { setActiveId(null); return null; }
    const showBack = projects.length > 1;
    return <CustomerProject project={proj} profile={profile} onBack={showBack ? () => setActiveId(null) : null} />;
  }

  return (
    <PortalShell title={`Welcome, ${profile.full_name || ''}`} subtitle="Choose a project to view.">
      <div className="portal-project-list">
        {projects.map(p => (
          <button key={p.id} className="portal-project-card" onClick={() => setActiveId(p.id)}>
            <div className="portal-project-card__head">
              <div className="portal-project-card__heading">
                <strong>{p.name}</strong>
                <span>{p.address || ''}</span>
                <span style={{ opacity: 0.7 }}>{p.status}</span>
              </div>
              <ProgressDonut percent={statusPct(p.status)} />
            </div>
          </button>
        ))}
      </div>
    </PortalShell>
  );
}

function CustomerProject({ project, profile, onBack }) {
  const [photos, setPhotos] = React.useState([]);
  const [invoices, setInvoices] = React.useState([]);
  const [files, setFiles] = React.useState([]);
  const [folders, setFolders] = React.useState([]);
  const [tab, setTab] = React.useState('photos');

  React.useEffect(() => {
    (async () => {
      const [{ data: ph }, { data: inv }, { data: fl }, foldersResp] = await Promise.all([
        sb.from('photos').select('*').eq('project_id', project.id).order('created_at', { ascending: false }),
        sb.from('invoices').select('*').eq('project_id', project.id).order('created_at', { ascending: false }),
        sb.from('files').select('*').eq('project_id', project.id).order('created_at', { ascending: false }),
        sb.from('project_file_folders').select('*').eq('project_id', project.id).order('name', { ascending: true }),
      ]);
      const [invoicesWithSize, filesWithSize] = await Promise.all([
        attachSizes('project-invoices', inv || [], project.id),
        attachSizes('project-files', fl || [], project.id),
      ]);
      setPhotos(ph || []); setInvoices(invoicesWithSize); setFiles(filesWithSize);
      setFolders(foldersResp.data || []);
    })();
  }, [project.id]);

  return (
    <PortalShell title={project.name} subtitle={project.address}>
      {onBack && (
        <div style={{ marginBottom: 16 }}>
          <button className="btn" onClick={onBack}>← All Projects</button>
        </div>
      )}
      <p style={{ textAlign: 'center', marginBottom: 32 }}>
        <strong>Status:</strong> {project.status}
      </p>
      <div className="portal-tabs">
        <button className={tab === 'photos' ? 'is-active' : ''} onClick={() => setTab('photos')}>Photos</button>
        <button className={tab === 'selections' ? 'is-active' : ''} onClick={() => setTab('selections')}>Selections</button>
        <button className={tab === 'accounting' ? 'is-active' : ''} onClick={() => setTab('accounting')}>Accounting</button>
        <button className={tab === 'files' ? 'is-active' : ''} onClick={() => setTab('files')}>Files</button>
        <button className={tab === 'invoices' ? 'is-active' : ''} onClick={() => setTab('invoices')}>Invoices</button>
      </div>
      {tab === 'photos' && (
        <React.Fragment>
          <PhotoAlbums project={project} isAdmin={false} />
          {isPunchlistPhase(project.status) && (
            <PunchListPhotos project={project} isAdmin={false} canUpload={true} />
          )}
        </React.Fragment>
      )}
      {tab === 'selections' && <SelectionsSheet project={project} isAdmin={false} profile={profile} />}
      {tab === 'accounting' && <AccountingSheet project={project} isAdmin={false} />}
      {tab === 'files' && (
        <React.Fragment>
          <FileList items={files} folders={folders} />
          {isPunchlistPhase(project.status) && (
            <PunchListNote project={project} canEdit={true} />
          )}
        </React.Fragment>
      )}
      {tab === 'invoices' && <InvoiceList items={invoices} />}
    </PortalShell>
  );
}

// ---- Reusable views ----
function SignedImg({ bucket, path, alt, style }) {
  const [url, setUrl] = React.useState(null);
  React.useEffect(() => {
    sb.storage.from(bucket).createSignedUrl(path, 3600).then(({ data }) => {
      setUrl(data?.signedUrl || null);
    });
  }, [bucket, path]);
  return url
    ? <img src={url} alt={alt || ''} style={style} />
    : <div style={{ ...style, background: '#eee' }} />;
}

function formatAlbumDate(d) {
  if (!d) return '';
  const dt = new Date(d + 'T00:00:00');
  return dt.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}

function PhotoAlbums({ project, isAdmin, kind = 'client', canCopyToClient = false }) {
  const bucket = kind === 'jobsite' ? 'project-jobsite-photos' : 'project-photos';
  const showCopyToClient = canCopyToClient && kind === 'jobsite';
  const [albums, setAlbums] = React.useState([]);
  const [photos, setPhotos] = React.useState([]);
  const [activeAlbumId, setActiveAlbumId] = React.useState(null);
  const [creating, setCreating] = React.useState(false);
  const [newTitle, setNewTitle] = React.useState('');
  const [newDate, setNewDate] = React.useState(new Date().toISOString().slice(0, 10));
  const [selectMode, setSelectMode] = React.useState(false);
  const [selectedIds, setSelectedIds] = React.useState(() => new Set());
  const [editingAlbum, setEditingAlbum] = React.useState(false);
  const [copyPhotos, setCopyPhotos] = React.useState(null);
  const [copyTarget, setCopyTarget] = React.useState({ mode: 'new', title: '', date: '', albumId: '' });
  const [clientAlbums, setClientAlbums] = React.useState([]);
  const [copyBusy, setCopyBusy] = React.useState(false);
  const [copyProgress, setCopyProgress] = React.useState({ done: 0, total: 0 });

  const reload = React.useCallback(async () => {
    const [{ data: al }, { data: ph }] = await Promise.all([
      sb.from('photo_albums').select('*').eq('project_id', project.id).eq('kind', kind).order('date', { ascending: false }),
      sb.from('photos').select('*').eq('project_id', project.id).eq('kind', kind).order('created_at', { ascending: false }),
    ]);
    setAlbums(al || []);
    setPhotos(ph || []);
  }, [project.id, kind]);
  React.useEffect(() => { reload(); }, [reload]);

  const photosByAlbum = React.useMemo(() => {
    const m = new Map();
    photos.forEach(p => {
      if (!m.has(p.album_id)) m.set(p.album_id, []);
      m.get(p.album_id).push(p);
    });
    return m;
  }, [photos]);

  const createAlbum = async (e) => {
    e.preventDefault();
    if (!newTitle.trim()) return;
    await sb.from('photo_albums').insert({ project_id: project.id, title: newTitle.trim(), date: newDate, kind });
    setNewTitle(''); setCreating(false);
    reload();
  };

  const deleteAlbum = async (album) => {
    const albumPhotos = photosByAlbum.get(album.id) || [];
    if (!confirm(`Delete "${album.title}" and its ${albumPhotos.length} photo${albumPhotos.length === 1 ? '' : 's'}? This cannot be undone.`)) return;
    if (albumPhotos.length) {
      await sb.storage.from(bucket).remove(albumPhotos.map(p => p.storage_path));
    }
    await sb.from('photo_albums').delete().eq('id', album.id);
    setActiveAlbumId(null);
    reload();
  };

  const deletePhoto = async (p) => {
    if (!confirm('Delete this photo?')) return;
    await sb.storage.from(bucket).remove([p.storage_path]);
    await sb.from('photos').delete().eq('id', p.id);
    reload();
  };

  const setAlbumCover = async (album, photoId) => {
    const newId = album.cover_photo_id === photoId ? null : photoId;
    const { error } = await sb.from('photo_albums').update({ cover_photo_id: newId }).eq('id', album.id);
    if (error) { alert(error.message); return; }
    reload();
  };

  const toggleSelected = (id) => {
    setSelectedIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  };
  const exitSelectMode = () => { setSelectMode(false); setSelectedIds(new Set()); };

  const openCopyDialog = async (photosToCopy, sourceAlbum) => {
    if (!photosToCopy || !photosToCopy.length) return;
    const { data } = await sb.from('photo_albums')
      .select('*').eq('project_id', project.id).eq('kind', 'client')
      .order('date', { ascending: false });
    const list = data || [];
    setClientAlbums(list);
    setCopyTarget({
      mode: list.length ? 'existing' : 'new',
      title: sourceAlbum?.title || '',
      date: sourceAlbum?.date || new Date().toISOString().slice(0, 10),
      albumId: list[0]?.id || '',
    });
    setCopyPhotos(photosToCopy);
  };
  const closeCopyDialog = () => { if (!copyBusy) { setCopyPhotos(null); } };

  const executeCopy = async () => {
    if (!copyPhotos || !copyPhotos.length) return;
    let targetAlbumId = null;
    if (copyTarget.mode === 'existing') {
      if (!copyTarget.albumId) { alert('Pick a destination folder.'); return; }
      targetAlbumId = copyTarget.albumId;
    } else {
      const title = (copyTarget.title || '').trim();
      if (!title) { alert('Give the new folder a title.'); return; }
      const { data: created, error } = await sb.from('photo_albums')
        .insert({ project_id: project.id, title, date: copyTarget.date || new Date().toISOString().slice(0, 10), kind: 'client' })
        .select().single();
      if (error) { alert(error.message); return; }
      targetAlbumId = created.id;
    }
    setCopyBusy(true);
    setCopyProgress({ done: 0, total: copyPhotos.length });
    let copied = 0;
    let failed = 0;
    for (const p of copyPhotos) {
      try {
        const { data: blob, error: dErr } = await sb.storage.from('project-jobsite-photos').download(p.storage_path);
        if (dErr || !blob) throw (dErr || new Error('download failed'));
        const newPath = `${project.id}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`;
        const { error: uErr } = await sb.storage.from('project-photos')
          .upload(newPath, blob, { contentType: blob.type || 'image/jpeg' });
        if (uErr) throw uErr;
        const { error: iErr } = await sb.from('photos').insert({
          project_id: project.id, storage_path: newPath,
          caption: p.caption || null, taken_at: p.taken_at || null,
          album_id: targetAlbumId, is_panorama: !!p.is_panorama, kind: 'client',
        });
        if (iErr) throw iErr;
        copied += 1;
      } catch (e) {
        console.error('Copy failed for photo', p.id, e);
        failed += 1;
      }
      setCopyProgress(prev => ({ ...prev, done: prev.done + 1 }));
    }
    setCopyBusy(false);
    setCopyPhotos(null);
    exitSelectMode();
    if (failed) alert(`Copied ${copied} of ${copyPhotos.length} photos. ${failed} failed — see console.`);
    else alert(`Copied ${copied} photo${copied === 1 ? '' : 's'} to client side.`);
  };

  if (activeAlbumId) {
    const album = albums.find(a => a.id === activeAlbumId);
    if (!album) { setActiveAlbumId(null); return null; }
    const albumPhotos = photosByAlbum.get(album.id) || [];
    const selectedPhotos = albumPhotos.filter(p => selectedIds.has(p.id));
    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
          <button className="btn" onClick={() => { exitSelectMode(); setEditingAlbum(false); setActiveAlbumId(null); }}>← All Folders</button>
          <div style={{ flex: 1, minWidth: 200 }}>
            <h3 style={{ margin: 0, fontFamily: 'var(--serif)', fontSize: '1.6rem', color: 'var(--navy)' }}>{album.title}</h3>
            <p style={{ margin: '2px 0 0', fontSize: '0.85rem', color: 'var(--muted)' }}>{formatAlbumDate(album.date)}</p>
          </div>
          {selectMode ? (
            <React.Fragment>
              <span style={{ fontSize: '0.9rem', color: 'var(--muted)' }}>{selectedPhotos.length} selected</span>
              <button type="button" className="btn btn--solid" disabled={!selectedPhotos.length}
                onClick={() => openCopyDialog(selectedPhotos, album)}>Copy to client →</button>
              <button type="button" className="btn" onClick={exitSelectMode}>Cancel</button>
            </React.Fragment>
          ) : (
            <React.Fragment>
              {albumPhotos.length > 0 && <DownloadAlbumButton album={album} photos={albumPhotos} bucket={bucket} />}
              {isAdmin && (
                <button type="button" className="album-download-btn"
                  onClick={() => setEditingAlbum(v => !v)}
                  aria-expanded={editingAlbum}
                  aria-label={editingAlbum ? 'Close folder menu' : 'Open folder menu'}
                  title={editingAlbum ? 'Close folder menu' : 'Folder menu'}>
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                    strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <line x1="3" y1="6" x2="21" y2="6" />
                    <line x1="3" y1="12" x2="21" y2="12" />
                    <line x1="3" y1="18" x2="21" y2="18" />
                  </svg>
                </button>
              )}
            </React.Fragment>
          )}
        </div>
        {isAdmin && editingAlbum && !selectMode && (
          <div className="form" style={{ marginBottom: 24 }}>
            <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
              <button type="button" className="portal-action-row__delete"
                onClick={() => deleteAlbum(album)}
                aria-label="Delete folder"
                title="Delete folder">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
                  stroke="currentColor" strokeWidth="2"
                  strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <polyline points="3 6 5 6 21 6" />
                  <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
                  <line x1="10" y1="11" x2="10" y2="17" />
                  <line x1="14" y1="11" x2="14" y2="17" />
                  <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
                </svg>
              </button>
            </div>
            <UploadPhoto project={project} albumId={album.id} onDone={reload} bucket={bucket} kind={kind} />
            {showCopyToClient && albumPhotos.length > 0 && (
              <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginTop: 14 }}>
                <button type="button" className="btn"
                  onClick={() => openCopyDialog(albumPhotos, album)}>Copy folder to client →</button>
                <button type="button" className="btn" onClick={() => setSelectMode(true)}>Pick photos</button>
              </div>
            )}
          </div>
        )}
        <PhotoGrid items={albumPhotos}
          onDelete={isAdmin && editingAlbum ? deletePhoto : null}
          coverPhotoId={album.cover_photo_id}
          onSetCover={isAdmin && editingAlbum ? (id) => setAlbumCover(album, id) : null}
          bucket={bucket}
          selectMode={selectMode}
          selectedIds={selectedIds}
          onToggleSelect={toggleSelected} />
        {copyPhotos && (
          <div className="portal-modal" onClick={closeCopyDialog}>
            <div className="portal-modal__card" role="dialog" aria-modal="true" aria-labelledby="copy-photos-title"
              onClick={(e) => e.stopPropagation()}>
              <div className="portal-modal__head">
                <h3 id="copy-photos-title" className="portal-modal__title">
                  Copy {copyPhotos.length} photo{copyPhotos.length === 1 ? '' : 's'} to client
                </h3>
                <button type="button" className="portal-modal__close" aria-label="Close"
                  onClick={closeCopyDialog} disabled={copyBusy}>×</button>
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
                <label style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
                  <input type="radio" name="copy-target-mode" value="existing"
                    checked={copyTarget.mode === 'existing'}
                    disabled={copyBusy || clientAlbums.length === 0}
                    onChange={() => setCopyTarget(t => ({ ...t, mode: 'existing' }))} />
                  <span>Add to existing client folder</span>
                </label>
                {copyTarget.mode === 'existing' && (
                  <select className="form__input" value={copyTarget.albumId} disabled={copyBusy}
                    onChange={e => setCopyTarget(t => ({ ...t, albumId: e.target.value }))}>
                    {clientAlbums.length === 0 && <option value="">No client folders yet</option>}
                    {clientAlbums.map(a => (
                      <option key={a.id} value={a.id}>{a.title} — {formatAlbumDate(a.date)}</option>
                    ))}
                  </select>
                )}
                <label style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
                  <input type="radio" name="copy-target-mode" value="new"
                    checked={copyTarget.mode === 'new'} disabled={copyBusy}
                    onChange={() => setCopyTarget(t => ({ ...t, mode: 'new' }))} />
                  <span>Create new client folder</span>
                </label>
                {copyTarget.mode === 'new' && (
                  <div className="form__row">
                    <div className="form__field"><label className="form__label">Title</label>
                      <input className="form__input" value={copyTarget.title} disabled={copyBusy}
                        onChange={e => setCopyTarget(t => ({ ...t, title: e.target.value }))} /></div>
                    <div className="form__field"><label className="form__label">Date</label>
                      <input className="form__input" type="date" value={copyTarget.date} disabled={copyBusy}
                        onChange={e => setCopyTarget(t => ({ ...t, date: e.target.value }))} /></div>
                  </div>
                )}
                {copyBusy && copyProgress.total > 0 && (
                  <div className="upload-progress">
                    <div className="upload-progress__bar">
                      <div className="upload-progress__fill"
                        style={{ width: `${Math.round((copyProgress.done / copyProgress.total) * 100)}%` }} />
                    </div>
                    <div className="upload-progress__meta">
                      <span>{copyProgress.done} of {copyProgress.total} copied</span>
                    </div>
                  </div>
                )}
                <div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end', marginTop: 6 }}>
                  <button type="button" className="btn" onClick={closeCopyDialog} disabled={copyBusy}>Cancel</button>
                  <button type="button" className="btn btn--solid" onClick={executeCopy} disabled={copyBusy}>
                    {copyBusy ? 'Copying…' : 'Copy'}
                  </button>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>
    );
  }

  return (
    <div>
      {isAdmin && (
        <div style={{ marginBottom: 28 }}>
          {!creating ? (
            <button className="btn btn--solid" onClick={() => setCreating(true)}>+ New Folder</button>
          ) : (
            <form onSubmit={createAlbum} style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'flex-end', maxWidth: 720 }}>
              <div className="form__field" style={{ flex: 1, minWidth: 220 }}>
                <label className="form__label">Title</label>
                <input className="form__input" required value={newTitle} onChange={e => setNewTitle(e.target.value)} placeholder="e.g. Foundation poured" />
              </div>
              <div className="form__field">
                <label className="form__label">Date</label>
                <input className="form__input" type="date" value={newDate} onChange={e => setNewDate(e.target.value)} />
              </div>
              <button type="submit" className="btn btn--solid">Create</button>
              <button type="button" className="btn" onClick={() => { setCreating(false); setNewTitle(''); }}>Cancel</button>
            </form>
          )}
        </div>
      )}
      {albums.length === 0 ? (
        <p style={{ textAlign: 'center', opacity: 0.7, padding: '32px 0' }}>
          {isAdmin ? 'No folders yet — create one to start uploading photos.' : 'No photo folders yet.'}
        </p>
      ) : (
        <div className="album-grid">
          {albums.map(a => {
            const albumPhotos = photosByAlbum.get(a.id) || [];
            const cover = (a.cover_photo_id && albumPhotos.find(p => p.id === a.cover_photo_id)) || albumPhotos[0];
            return (
              <button key={a.id} type="button" className="album-card" onClick={() => setActiveAlbumId(a.id)}>
                <div className="album-card__cover">
                  {cover
                    ? <SignedImg bucket={bucket} path={cover.storage_path} alt={a.title}
                        style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                    : <div className="album-card__placeholder">No photos yet</div>}
                </div>
                <div className="album-card__body">
                  <div className="album-card__title">{a.title}</div>
                  <div className="album-card__meta">
                    <span>{formatAlbumDate(a.date)}</span>
                    <span>{albumPhotos.length} photo{albumPhotos.length === 1 ? '' : 's'}</span>
                  </div>
                </div>
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

const PUNCHLIST_BUCKET = 'project-punchlist-photos';
const isPunchlistPhase = (status) => status === 'Punchlist' || status === 'Complete';

function PunchListPhotos({ project, isAdmin, canUpload }) {
  const [photos, setPhotos] = React.useState([]);
  const [showUpload, setShowUpload] = React.useState(false);
  const [currentUserId, setCurrentUserId] = React.useState(null);

  const reload = React.useCallback(async () => {
    const { data } = await sb.from('photos')
      .select('*').eq('project_id', project.id).eq('kind', 'punchlist')
      .order('created_at', { ascending: false });
    setPhotos(data || []);
  }, [project.id]);
  React.useEffect(() => { reload(); }, [reload]);
  React.useEffect(() => {
    sb.auth.getUser().then(({ data: { user } }) => setCurrentUserId(user?.id || null));
  }, []);

  const deletePhoto = async (p) => {
    if (!confirm('Delete this punch-list photo?')) return;
    await sb.storage.from(PUNCHLIST_BUCKET).remove([p.storage_path]);
    const { error } = await sb.from('photos').delete().eq('id', p.id);
    if (error) { alert(error.message); return; }
    reload();
  };
  const canDeletePhoto = (p) => isAdmin || p.uploaded_by === currentUserId;

  return (
    <div className="punchlist-section">
      <div className="punchlist-section__head">
        <h3 className="punchlist-section__title">Punch List Photos</h3>
        {canUpload && (
          <button type="button" className="btn btn--solid btn--sm"
            onClick={() => setShowUpload(v => !v)}>
            {showUpload ? 'Cancel' : '+ Add punchlist photos'}
          </button>
        )}
      </div>
      <p className="punchlist-section__hint">
        {canUpload
          ? 'Spotted something that needs attention? Add photos of punch-list items here for your builder to review.'
          : 'Punch-list photos added by the homeowner.'}
      </p>
      {showUpload && canUpload && (
        <div className="folder-edit-card">
          <UploadPhoto project={project} albumId={null}
            bucket={PUNCHLIST_BUCKET} kind="punchlist"
            onDone={() => { setShowUpload(false); reload(); }} />
        </div>
      )}
      <PhotoGrid items={photos} bucket={PUNCHLIST_BUCKET}
        onDelete={deletePhoto} canDeletePhoto={canDeletePhoto} />
    </div>
  );
}

function PunchListNote({ project, canEdit }) {
  const [content, setContent] = React.useState('');
  const [loaded, setLoaded] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [savedMsg, setSavedMsg] = React.useState('');

  React.useEffect(() => {
    let alive = true;
    sb.from('punchlist_notes').select('*').eq('project_id', project.id).maybeSingle()
      .then(({ data }) => {
        if (!alive) return;
        setContent(data?.content || '');
        setLoaded(true);
      });
    return () => { alive = false; };
  }, [project.id]);

  const save = async () => {
    setSaving(true); setSavedMsg('');
    const { data: { user } } = await sb.auth.getUser();
    const { error } = await sb.from('punchlist_notes').upsert({
      project_id: project.id,
      content,
      updated_at: new Date().toISOString(),
      updated_by: user?.id || null,
    });
    setSaving(false);
    if (error) { alert('Could not save: ' + error.message); return; }
    setSavedMsg('Saved.');
    setTimeout(() => setSavedMsg(''), 2000);
  };

  if (!loaded) return null;

  return (
    <div className="punchlist-section">
      <div className="punchlist-section__head">
        <h3 className="punchlist-section__title">Punch List Notes</h3>
      </div>
      <p className="punchlist-section__hint">
        {canEdit
          ? 'Write down anything you’d like added to the punch list — your builder can see this.'
          : 'Punch-list notes from the homeowner.'}
      </p>
      <textarea className="punchlist-note__area"
        value={content}
        onChange={(e) => setContent(e.target.value)}
        readOnly={!canEdit}
        rows={8}
        placeholder={canEdit
          ? 'e.g. Touch-up paint in the upstairs hallway, loose cabinet handle in the kitchen, squeaky door in the master…'
          : 'No punch-list notes yet.'} />
      {canEdit && (
        <div className="punchlist-note__actions">
          <button type="button" className="btn btn--solid btn--sm" onClick={save} disabled={saving}>
            {saving ? 'Saving…' : 'Save notes'}
          </button>
          {savedMsg && <span className="punchlist-note__saved">{savedMsg}</span>}
        </div>
      )}
    </div>
  );
}

function PhotoGrid({ items, onDelete, canDeletePhoto, coverPhotoId, onSetCover, bucket = 'project-photos',
  selectMode = false, selectedIds = null, onToggleSelect = null }) {
  const [lightboxIdx, setLightboxIdx] = React.useState(null);
  if (!items.length) return <p style={{ textAlign: 'center', opacity: 0.7 }}>No photos yet.</p>;
  return (
    <React.Fragment>
      <div className="portal-photo-grid">
        {items.map((p, i) => {
          const isSelected = selectMode && selectedIds && selectedIds.has(p.id);
          const handleClick = () => {
            if (selectMode && onToggleSelect) onToggleSelect(p.id);
            else setLightboxIdx(i);
          };
          return (
            <div key={p.id}
              className={`portal-photo${selectMode ? ' is-selectable' : ''}${isSelected ? ' is-selected' : ''}`}
              onClick={handleClick}>
              <SignedImg bucket={bucket} path={p.storage_path} alt={p.caption}
                style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
              {p.is_panorama && <span className="portal-photo__pano">360°</span>}
              {coverPhotoId === p.id && <span className="portal-photo__cover-badge">Cover</span>}
              {selectMode && (
                <span className={`portal-photo__check${isSelected ? ' is-active' : ''}`} aria-hidden="true">
                  {isSelected ? '✓' : ''}
                </span>
              )}
              {(p.caption || p.taken_at) && (
                <div className="portal-photo__cap">
                  {p.caption}{p.taken_at ? ` · ${p.taken_at}` : ''}
                </div>
              )}
              {!selectMode && onSetCover && (
                <button className={`portal-photo__cover-btn ${coverPhotoId === p.id ? 'is-active' : ''}`}
                  title={coverPhotoId === p.id ? 'Cover photo (click to clear)' : 'Set as cover'}
                  aria-label={coverPhotoId === p.id ? 'Clear cover' : 'Set as cover'}
                  onClick={(e) => { e.stopPropagation(); onSetCover(p.id); }}>★</button>
              )}
              {!selectMode && onDelete && (!canDeletePhoto || canDeletePhoto(p)) && (
                <button className="portal-photo__del"
                  onClick={(e) => { e.stopPropagation(); onDelete(p); }}>×</button>
              )}
            </div>
          );
        })}
      </div>
      {lightboxIdx !== null && !selectMode && (
        <PortalPhotoLightbox
          items={items}
          idx={lightboxIdx}
          onClose={() => setLightboxIdx(null)}
          onIdxChange={setLightboxIdx}
          bucket={bucket}
        />
      )}
    </React.Fragment>
  );
}

function sanitizeFilename(s) {
  return (s || 'album').replace(/[\\/:*?"<>|]+/g, '_').replace(/\s+/g, ' ').trim().slice(0, 80) || 'album';
}

function DownloadAlbumButton({ album, photos, bucket = 'project-photos' }) {
  const [busy, setBusy] = React.useState(false);
  const [progress, setProgress] = React.useState(0);

  const download = async () => {
    if (busy || !photos.length) return;
    if (typeof window.JSZip !== 'function') {
      alert('Download tool failed to load. Please refresh and try again.');
      return;
    }
    setBusy(true);
    setProgress(0);
    try {
      const zip = new window.JSZip();
      const used = new Set();
      for (let i = 0; i < photos.length; i++) {
        const p = photos[i];
        const { data, error } = await sb.storage.from(bucket)
          .createSignedUrl(p.storage_path, 600);
        if (error || !data?.signedUrl) throw new Error(error?.message || 'Could not get download URL');
        const resp = await fetch(data.signedUrl);
        if (!resp.ok) throw new Error(`Fetch failed: ${resp.status}`);
        const blob = await resp.blob();
        const ext = (p.storage_path.split('.').pop() || 'jpg').toLowerCase();
        let base = sanitizeFilename(p.caption) || `photo-${String(i + 1).padStart(3, '0')}`;
        let name = `${base}.${ext}`;
        let n = 2;
        while (used.has(name)) { name = `${base} (${n}).${ext}`; n++; }
        used.add(name);
        zip.file(name, blob);
        setProgress(i + 1);
      }
      const zipBlob = await zip.generateAsync({ type: 'blob' });
      const url = URL.createObjectURL(zipBlob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${sanitizeFilename(album.title)}.zip`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    } catch (err) {
      alert(`Download failed: ${err.message || err}`);
    } finally {
      setBusy(false);
      setProgress(0);
    }
  };

  const label = busy
    ? `Downloading ${progress}/${photos.length}`
    : `Download ${photos.length} photo${photos.length === 1 ? '' : 's'}`;
  return (
    <button className="album-download-btn" onClick={download} disabled={busy}
      title={label} aria-label={label}>
      {busy ? (
        <span className="album-download-btn__progress">{progress}/{photos.length}</span>
      ) : (
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
          strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
          <polyline points="7 10 12 15 17 10" />
          <line x1="12" y1="15" x2="12" y2="3" />
        </svg>
      )}
    </button>
  );
}

function dollarsToCents(s) {
  if (s == null || s === '') return 0;
  const n = parseFloat(String(s).replace(/[^0-9.\-]/g, ''));
  if (!isFinite(n)) return 0;
  return Math.round(n * 100);
}

function centsToInputDollars(cents) {
  if (cents == null || cents === 0) return '';
  return (cents / 100).toFixed(2);
}

function centsToDisplayDollars(cents) {
  if (cents == null || cents === 0) return '';
  return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

function formatLastLogin(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  if (isNaN(d)) return '';
  const time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
  const date = d.toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
  return `${time}, ${date}`;
}

function AccountingSheet({ project, isAdmin }) {
  const [items, setItems] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [customEditingId, setCustomEditingId] = React.useState(null);
  const [amountEditingId, setAmountEditingId] = React.useState(null);
  const initialContract = React.useMemo(() => {
    if (project.contract_terms) return project.contract_terms;
    return centsToDisplayDollars(project.contract_price_cents || 0);
  }, [project.contract_terms, project.contract_price_cents]);
  const [contractDraft, setContractDraft] = React.useState(initialContract);
  const [savedContract, setSavedContract] = React.useState(initialContract);
  const [savingContract, setSavingContract] = React.useState(false);
  const [savedFlash, setSavedFlash] = React.useState(false);

  const contractDirty = contractDraft.trim() !== savedContract.trim();

  const saveContractPrice = async () => {
    if (!contractDirty) return;
    setSavingContract(true);
    const text = contractDraft.trim();
    const cents = text ? dollarsToCents(text) : 0;
    // If the input is purely a dollar amount, store cents and clear free text.
    // Otherwise store the free text alongside any embedded number.
    const cleaned = text.replace(/[$,\s.\d-]/g, '');
    const isPurelyNumber = text !== '' && cleaned === '';
    const patch = isPurelyNumber
      ? { contract_price_cents: cents, contract_terms: null }
      : { contract_price_cents: cents, contract_terms: text || null };
    const { error } = await sb.from('projects').update(patch).eq('id', project.id);
    setSavingContract(false);
    if (error) { alert(`Could not save contract price: ${error.message}`); return; }
    project.contract_price_cents = cents;
    project.contract_terms = patch.contract_terms;
    const newDisplay = isPurelyNumber ? centsToDisplayDollars(cents) : text;
    setContractDraft(newDisplay);
    setSavedContract(newDisplay);
    setSavedFlash(true);
    setTimeout(() => setSavedFlash(false), 1500);
  };

  const reload = React.useCallback(async () => {
    const { data } = await sb.from('accounting_items')
      .select('*').eq('project_id', project.id)
      .order('sort_order', { ascending: true })
      .order('created_at', { ascending: true });
    setItems(data || []);
    setLoading(false);
  }, [project.id]);
  React.useEffect(() => { reload(); }, [reload]);

  const moveRow = async (id, direction) => {
    const row = items.find(r => r.id === id);
    if (!row) return;
    const peers = items
      .filter(r => r.section === row.section)
      .sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)
        || new Date(a.created_at) - new Date(b.created_at));
    const idx = peers.findIndex(r => r.id === id);
    const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
    if (swapIdx < 0 || swapIdx >= peers.length) return;
    const other = peers[swapIdx];
    const a = row.sort_order || 0;
    const b = other.sort_order || 0;
    const newA = a === b ? (direction === 'up' ? b - 1 : b + 1) : b;
    const newB = a === b ? a : a;
    setItems(prev => prev.map(r => {
      if (r.id === row.id) return { ...r, sort_order: newA };
      if (r.id === other.id) return { ...r, sort_order: newB };
      return r;
    }));
    const [{ error: e1 }, { error: e2 }] = await Promise.all([
      sb.from('accounting_items').update({ sort_order: newA }).eq('id', row.id),
      sb.from('accounting_items').update({ sort_order: newB }).eq('id', other.id),
    ]);
    if (e1 || e2) { alert('Could not save row order'); reload(); }
  };

  const ALLOWANCE_PRESETS = [
    'Sitework',
    'Foundation',
    'Framing shell materials',
    'Plumbing Fixtures',
    'Light Fixtures',
    'Electrical wiring',
    'HVAC',
    'Trim Extras L&M',
    'Interior and Exterior Painting',
    'Summer Kitchen',
    'Tile Materials',
    'Wood floor allowance',
    'Front door and exterior doors',
    'Windows and sliders',
    'Cabinets',
    'Carpet',
    'Appliances',
    'Quartz Countertops',
    'Fireplace',
    'Closet shelving',
    'Glass panel/shower door',
    'Structured wiring',
    'Screen Porch',
    'Driveway',
    'Landscaping',
    'Well & Septic',
  ];

  const addRow = async (section) => {
    const sectionRows = items.filter(r => r.section === section);
    if (section === 'Allowances' && sectionRows.length === 0) {
      const rows = ALLOWANCE_PRESETS.map((name, i) => ({
        project_id: project.id, section, sort_order: i, item: name,
      }));
      const { data, error } = await sb.from('accounting_items').insert(rows).select('*');
      if (error) { alert(`Could not seed allowances: ${error.message}`); return; }
      if (data) setItems([...items, ...data]);
      return;
    }
    const sortOrder = sectionRows.length
      ? Math.max(...sectionRows.map(r => r.sort_order)) + 1 : 0;
    const { data } = await sb.from('accounting_items').insert({
      project_id: project.id, section, sort_order: sortOrder, item: '',
    }).select('*').single();
    if (data) setItems([...items, data]);
  };

  const updateRow = (id, patch) => {
    // Optimistic local update
    setItems(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r));
  };
  const persistRow = async (id, patch) => {
    const { error } = await sb.from('accounting_items').update(patch).eq('id', id);
    if (error) {
      console.error('accounting_items update failed', { patch, error });
      alert(`Could not save: ${error.message}`);
    }
  };

  const deleteRow = async (id) => {
    if (!confirm('Delete this row?')) return;
    await sb.from('accounting_items').delete().eq('id', id);
    setItems(items.filter(r => r.id !== id));
  };

  // Build section list in display order
  const sections = [];
  const seen = new Set();
  items.forEach(r => { if (!seen.has(r.section)) { seen.add(r.section); sections.push(r.section); } });
  if (!sections.length && isAdmin) sections.push('Allowances');

  const CHECKS_SECTION = 'Checks';
  const isChecks = (s) => s === CHECKS_SECTION;

  const markupBaseFor = (r) => {
    const actual = r.actual_cost_cents || 0;
    const allowance = r.allowance_cents || 0;
    if (actual === 0) return 0;
    if (allowance > 0 && actual > allowance) return actual - allowance;
    return actual;
  };
  const computeMarkup = (r) =>
    Math.round(markupBaseFor(r) * (Number(r.markup_pct) || 0) / 100);

  const totalsFor = (rows) => rows.reduce((acc, r) => ({
    allowance: acc.allowance + (r.allowance_cents || 0),
    actual: acc.actual + (r.actual_cost_cents || 0),
    markup: acc.markup + computeMarkup(r),
  }), { allowance: 0, actual: 0, markup: 0 });

  const checksTotal = (rows) =>
    rows.reduce((sum, r) => sum + (r.actual_cost_cents || 0), 0);

  if (loading) return <p style={{ textAlign: 'center', opacity: 0.7 }}>Loading…</p>;

  if (!isAdmin && items.length === 0) {
    return <p style={{ textAlign: 'center', opacity: 0.7, padding: '32px 0' }}>No accounting details posted yet.</p>;
  }

  if (isAdmin) {
    const renameSection = async (oldName, newName) => {
      if (!newName || newName === oldName) return;
      await sb.from('accounting_items')
        .update({ section: newName }).eq('project_id', project.id).eq('section', oldName);
      reload();
    };
    const newSection = async () => {
      const name = prompt('Section name:', 'Change Orders');
      if (!name) return;
      await addRow(name);
    };

    const PCT_PRESETS = [0, 5, 10, 15, 20, 25, 30];
    const sortRows = (rows) => [...rows].sort(
      (a, b) => (a.sort_order || 0) - (b.sort_order || 0)
        || new Date(a.created_at) - new Date(b.created_at));
    return (
      <div className="accounting-admin">
        {sections.map(section => {
          const sectionRows = sortRows(items.filter(r => r.section === section));
          if (isChecks(section)) {
            const paid = checksTotal(sectionRows);
            return (
              <div key={section} className="accounting-admin__section">
                <div className="accounting-admin__section-head">
                  <span className="accounting-admin__section-title accounting-admin__section-title--readonly">{section}</span>
                </div>
                <div className="accounting-admin__table-wrap">
                  <table className="accounting-admin__table">
                    <thead>
                      <tr>
                        <th>Date</th>
                        <th>Description</th>
                        <th>Amount</th>
                        <th></th>
                      </tr>
                    </thead>
                    <tbody>
                      {sectionRows.map(r => (
                        <tr key={r.id}>
                          <td style={{ width: 160 }}>
                            <input className="accounting-admin__cell" type="date" defaultValue={r.date || ''}
                              onBlur={(e) => { const v = e.target.value || null; if (v !== r.date) { updateRow(r.id, { date: v }); persistRow(r.id, { date: v }); } }} />
                          </td>
                          <td>
                            <input className="accounting-admin__cell" type="text" defaultValue={r.item} placeholder="Check #1234"
                              onBlur={(e) => { const v = e.target.value; if (v !== r.item) { updateRow(r.id, { item: v }); persistRow(r.id, { item: v }); } }} />
                          </td>
                          <td>
                            <input className="accounting-admin__cell accounting-admin__cell--num" type="text" inputMode="decimal"
                              defaultValue={centsToInputDollars(r.actual_cost_cents)}
                              onBlur={(e) => { const c = dollarsToCents(e.target.value); if (c !== r.actual_cost_cents) { updateRow(r.id, { actual_cost_cents: c }); persistRow(r.id, { actual_cost_cents: c }); } }} />
                          </td>
                          <td>
                            <div className="accounting-admin__row-actions">
                              <button className="accounting-admin__move" onClick={() => moveRow(r.id, 'up')} aria-label="Move up" title="Move up">↑</button>
                              <button className="accounting-admin__move" onClick={() => moveRow(r.id, 'down')} aria-label="Move down" title="Move down">↓</button>
                              <button className="accounting-admin__del" onClick={() => deleteRow(r.id)} aria-label="Delete row">×</button>
                            </div>
                          </td>
                        </tr>
                      ))}
                      <tr className="accounting-admin__totals">
                        <td colSpan={2}>Total Paid</td>
                        <td>{fmtMoney(paid)}</td>
                        <td></td>
                      </tr>
                    </tbody>
                  </table>
                </div>
                <button className="btn" style={{ marginTop: 8 }} onClick={() => addRow(section)}>+ Add Check</button>
              </div>
            );
          }
          const t = totalsFor(sectionRows);
          return (
            <div key={section} className="accounting-admin__section">
              <div className="accounting-admin__section-head">
                <input
                  className="accounting-admin__section-title"
                  defaultValue={section}
                  onBlur={(e) => renameSection(section, e.target.value.trim())}
                />
              </div>
              <div className="accounting-admin__table-wrap">
                <table className="accounting-admin__table">
                  <thead>
                    <tr>
                      <th>Item</th>
                      <th>Allowance</th>
                      <th>Actual Cost</th>
                      <th>Markup %</th>
                      <th>Difference</th>
                      <th>Notes</th>
                      <th></th>
                    </tr>
                  </thead>
                  <tbody>
                    {sectionRows.map(r => {
                      const markup = computeMarkup(r);
                      const rowHasActual = (r.actual_cost_cents || 0) > 0;
                      const diff = rowHasActual ? (r.actual_cost_cents || 0) + markup - (r.allowance_cents || 0) : 0;
                      return (
                        <tr key={r.id}>
                          <td>
                            <input className="accounting-admin__cell" type="text" defaultValue={r.item}
                              onBlur={(e) => { const v = e.target.value; if (v !== r.item) { updateRow(r.id, { item: v }); persistRow(r.id, { item: v }); } }} />
                          </td>
                          <td>
                            <input className="accounting-admin__cell accounting-admin__cell--num" type="text" inputMode="decimal"
                              defaultValue={centsToInputDollars(r.allowance_cents)}
                              onBlur={(e) => { const c = dollarsToCents(e.target.value); if (c !== r.allowance_cents) { updateRow(r.id, { allowance_cents: c }); persistRow(r.id, { allowance_cents: c }); } }} />
                          </td>
                          <td>
                            <input className="accounting-admin__cell accounting-admin__cell--num" type="text" inputMode="decimal"
                              defaultValue={centsToInputDollars(r.actual_cost_cents)}
                              onBlur={(e) => { const c = dollarsToCents(e.target.value); if (c !== r.actual_cost_cents) { updateRow(r.id, { actual_cost_cents: c }); persistRow(r.id, { actual_cost_cents: c }); } }} />
                          </td>
                          {(() => {
                            const pct = Number(r.markup_pct) || 0;
                            const isPreset = PCT_PRESETS.includes(pct);
                            const isEditing = customEditingId === r.id;
                            return (
                              <td className="accounting-admin__pct-cell">
                                {isEditing ? (
                                  <span className="accounting-admin__pct-input">
                                    <input
                                      className="accounting-admin__cell accounting-admin__cell--pct"
                                      type="number" step="0.5" min="0" max="100" autoFocus
                                      defaultValue={pct}
                                      onBlur={(e) => {
                                        const v = parseFloat(e.target.value || '0');
                                        const newPct = isFinite(v) ? v : 0;
                                        if (newPct !== pct) { updateRow(r.id, { markup_pct: newPct }); persistRow(r.id, { markup_pct: newPct }); }
                                        setCustomEditingId(null);
                                      }}
                                      onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }}
                                    />
                                    <span className="accounting-admin__pct-suffix">%</span>
                                  </span>
                                ) : (
                                  <select
                                    className="accounting-admin__cell accounting-admin__cell--pct"
                                    value={isPreset ? String(pct) : '__custom__'}
                                    onChange={(e) => {
                                      if (e.target.value === '__custom__') { setCustomEditingId(r.id); return; }
                                      const v = parseFloat(e.target.value);
                                      if (v !== pct) { updateRow(r.id, { markup_pct: v }); persistRow(r.id, { markup_pct: v }); }
                                    }}
                                  >
                                    {PCT_PRESETS.map(p => <option key={p} value={p}>{p}%</option>)}
                                    {!isPreset && <option value="__custom__">{pct}%</option>}
                                    {isPreset && <option value="__custom__">Custom…</option>}
                                  </select>
                                )}
                                {(() => {
                                  const actual = r.actual_cost_cents || 0;
                                  const base = markupBaseFor(r);
                                  const isAmtEditing = amountEditingId === r.id;
                                  if (isAmtEditing) {
                                    return (
                                      <input
                                        className="accounting-admin__cell accounting-admin__cell--num accounting-admin__pct-amount-input"
                                        type="text" inputMode="decimal" autoFocus
                                        defaultValue={centsToInputDollars(markup)}
                                        onBlur={(e) => {
                                          const newAmt = dollarsToCents(e.target.value);
                                          if (base > 0) {
                                            const newPct = Math.round((newAmt / base) * 1000) / 10;
                                            if (newPct !== pct) { updateRow(r.id, { markup_pct: newPct }); persistRow(r.id, { markup_pct: newPct }); }
                                          }
                                          setAmountEditingId(null);
                                        }}
                                        onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }}
                                      />
                                    );
                                  }
                                  return (
                                    <button type="button" className="accounting-admin__pct-amount accounting-admin__pct-amount--editable"
                                      title={actual > 0 ? 'Click to set a dollar amount' : 'Enter an actual cost first'}
                                      disabled={actual <= 0}
                                      onClick={() => actual > 0 && setAmountEditingId(r.id)}>
                                      {markup ? fmtMoney(markup) : (actual > 0 ? '$0.00' : '')}
                                    </button>
                                  );
                                })()}
                              </td>
                            );
                          })()}
                          <td className={`accounting-admin__diff ${rowHasActual && diff > 0 ? 'is-over' : rowHasActual && diff < 0 ? 'is-under' : ''}`}>
                            {rowHasActual ? fmtMoney(diff) : ''}
                          </td>
                          <td>
                            <input className="accounting-admin__cell" type="text" defaultValue={r.notes || ''}
                              onBlur={(e) => { const v = e.target.value || null; if (v !== r.notes) { updateRow(r.id, { notes: v }); persistRow(r.id, { notes: v }); } }} />
                          </td>
                          <td>
                            <div className="accounting-admin__row-actions">
                              <button className="accounting-admin__move" onClick={() => moveRow(r.id, 'up')} aria-label="Move up" title="Move up">↑</button>
                              <button className="accounting-admin__move" onClick={() => moveRow(r.id, 'down')} aria-label="Move down" title="Move down">↓</button>
                              <button className="accounting-admin__del" onClick={() => deleteRow(r.id)} aria-label="Delete row">×</button>
                            </div>
                          </td>
                        </tr>
                      );
                    })}
                    {(() => {
                      const tA = totalsFor(sectionRows);
                      const tHasActual = sectionRows.some(r => (r.actual_cost_cents || 0) > 0);
                      const tDiff = tA.actual + tA.markup - tA.allowance;
                      return (
                        <tr className="accounting-admin__totals">
                          <td>Section total</td>
                          <td>{fmtMoney(tA.allowance)}</td>
                          <td>{fmtMoney(tA.actual)}</td>
                          <td>{fmtMoney(tA.markup)}</td>
                          <td className={tHasActual && tDiff > 0 ? 'is-over' : tHasActual && tDiff < 0 ? 'is-under' : ''}>
                            {tHasActual ? fmtMoney(tDiff) : ''}
                          </td>
                          <td colSpan={2}></td>
                        </tr>
                      );
                    })()}
                  </tbody>
                </table>
              </div>
              <button className="btn" style={{ marginTop: 8 }} onClick={() => addRow(section)}>+ Add Row</button>
            </div>
          );
        })}
        {sections.length > 0 && items.length > 0 && (() => {
          const activeRows = items.filter(r => !isChecks(r.section) && (r.actual_cost_cents || 0) > 0);
          const checkRows = items.filter(r => isChecks(r.section));
          const g = totalsFor(activeRows);
          const paid = checksTotal(checkRows);
          const gDiff = g.actual + g.markup - g.allowance - paid;
          return (
            <div className="accounting-admin__grand">
              <span>Project allowance balance</span>
              <span className="accounting-admin__grand-spacer" />
              <span className={gDiff > 0 ? 'is-over' : gDiff < 0 ? 'is-under' : ''}>
                {fmtMoney(gDiff)}
              </span>
            </div>
          );
        })()}
        <div className="accounting-admin__grand accounting-admin__grand--contract">
          <span>Contract Price</span>
          <span className="accounting-admin__grand-spacer" />
          <input className="accounting-admin__grand-input" type="text"
            value={contractDraft}
            onChange={(e) => setContractDraft(e.target.value)}
            onBlur={() => { if (contractDirty) saveContractPrice(); }}
            onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
            placeholder="$0.00" />
          {savingContract && <span className="accounting-admin__contract-saving">Saving…</span>}
          {savedFlash && <span className="accounting-admin__contract-saved">Saved</span>}
        </div>
        <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginTop: 24 }}>
          <button className="btn btn--solid" onClick={newSection}>+ Add Section</button>
          {!sections.includes(CHECKS_SECTION) && (
            <button className="btn" onClick={() => addRow(CHECKS_SECTION)}>+ Add Checks Section</button>
          )}
        </div>
      </div>
    );
  }

  // Customer / read-only luxe ledger
  const hasActual = (r) => (r.actual_cost_cents || 0) > 0;
  const costRowsAll = items.filter(r => !isChecks(r.section));
  const checkRowsAll = items.filter(r => isChecks(r.section));
  const grand = totalsFor(costRowsAll.filter(hasActual));
  const grandPaid = checksTotal(checkRowsAll);
  const sortRows = (rows) => [...rows].sort(
    (a, b) => (a.sort_order || 0) - (b.sort_order || 0)
      || new Date(a.created_at) - new Date(b.created_at));
  return (
    <div className="accounting-ledger">
      {sections.map(section => {
        const sectionRows = sortRows(items.filter(r => r.section === section))
          .filter(r => (r.allowance_cents || 0) > 0 || (r.actual_cost_cents || 0) > 0);
        if (isChecks(section)) return null;
        if (sectionRows.length === 0) return null;
        return (
          <section key={section} className="accounting-ledger__section">
            <h4 className="accounting-ledger__section-title">{section}</h4>
            <ul className="accounting-ledger__list">
              {sectionRows.map(r => {
                const markup = computeMarkup(r);
                const rowHasActual = hasActual(r);
                const diff = rowHasActual ? (r.actual_cost_cents || 0) + markup - (r.allowance_cents || 0) : 0;
                return (
                  <li key={r.id} className="accounting-ledger__row">
                    <div className="accounting-ledger__row-head">
                      <span className="accounting-ledger__name">{r.item || '—'}</span>
                      <span className="accounting-ledger__leader" />
                      {rowHasActual && (
                        <span className={`accounting-ledger__diff ${diff > 0 ? 'is-over' : diff < 0 ? 'is-under' : ''}`}>
                          {fmtMoney(diff)}
                        </span>
                      )}
                    </div>
                    <div className="accounting-ledger__detail">
                      {(r.allowance_cents || 0) > 0 && (
                        <span><em>Allowance Amount</em> {fmtMoney(r.allowance_cents)}</span>
                      )}
                      {rowHasActual && (
                        <span><em>Actual Cost</em> {fmtMoney((r.actual_cost_cents || 0) + markup)}</span>
                      )}
                    </div>
                    {r.notes && <p className="accounting-ledger__notes">{r.notes}</p>}
                  </li>
                );
              })}
            </ul>
          </section>
        );
      })}
      {checkRowsAll.length > 0 && (
        <section className="accounting-ledger__section accounting-ledger__checks">
          <h4 className="accounting-ledger__section-title">Payments</h4>
          <ul className="accounting-ledger__list">
            {sortRows(checkRowsAll).map(c => {
              const dateLabel = c.date
                ? new Date(c.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
                : '';
              return (
                <li key={c.id} className="accounting-ledger__row">
                  <div className="accounting-ledger__row-head">
                    <span className="accounting-ledger__name">{c.item || 'Payment'}</span>
                    <span className="accounting-ledger__leader" />
                    <span>{fmtMoney(c.actual_cost_cents)}</span>
                  </div>
                  {dateLabel && (
                    <div className="accounting-ledger__detail">
                      <span><em>Paid</em> {dateLabel}</span>
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        </section>
      )}
      {sections.length > 0 && (() => {
        const overages = grand.actual + grand.markup - grand.allowance - grandPaid;
        return (
          <React.Fragment>
            <div className="accounting-ledger__paid">
              <span>Total Paid</span>
              <span className="accounting-ledger__leader" />
              <span className="is-under">−{fmtMoney(grandPaid)}</span>
            </div>
            <div className="accounting-ledger__grand">
              <span>Allowance overages total</span>
              <span className="accounting-ledger__leader" />
              <span className={overages > 0 ? 'is-over' : overages < 0 ? 'is-under' : ''}>
                {fmtMoney(overages)}
              </span>
            </div>
          </React.Fragment>
        );
      })()}
    </div>
  );
}

// ---- Selections ----
const PHASE_TEMPLATES = [
  {
    num: 1,
    title: 'Pre-Slab Exterior',
    description: 'Required before the lot is cleared. Confirmed in person at the Exterior Meeting; an Exterior Spec sheet will be created and used for any necessary HOA approvals.',
    sections: [
      {
        title: 'Roof & Exterior Material',
        fields: [
          { key: 'shingle_color', label: 'Shingle Color', type: 'text' },
          { key: 'drip_edge_color', label: 'Drip Edge Color', type: 'radio', options: ['Black', 'White', 'Bronze'] },
          { key: 'exterior_material', label: 'Exterior Accent', type: 'radio', options: ['None', 'Brick', 'Stone', 'Stamped Brick', 'Other'] },
          { key: 'exterior_other', label: 'Other / detail', type: 'text', placeholder: 'e.g. Old Chicago brick front porch flooring', disableWhen: { key: 'exterior_material', value: 'None' } },
        ],
      },
      {
        title: 'Windows',
        fields: [
          { key: 'window_grid_front', label: 'Window Grid — Front', type: 'text', placeholder: 'e.g. 2/2' },
          { key: 'window_grid_rear', label: 'Window Grid — Rear / Sides', type: 'text', placeholder: 'e.g. clear/clear' },
          { key: 'window_color', label: 'Window Color', type: 'text', placeholder: 'e.g. white/white' },
          { key: 'slider_color', label: 'Slider Color', type: 'text' },
        ],
      },
      {
        title: 'Doors',
        fields: [
          { key: 'front_entry_door_style', label: 'Front Entry Door Style', type: 'text' },
          { key: 'exterior_door_style', label: 'Exterior Door Style', type: 'text' },
          { key: 'garage_door_style', label: 'Garage Door Style', type: 'text' },
        ],
      },
      {
        title: 'Garage Door Options',
        fields: [
          { key: 'garage_includes_windows', label: 'Includes Windows', type: 'checkbox' },
          { key: 'garage_no_windows', label: 'No Windows', type: 'checkbox' },
          { key: 'garage_keypads', label: 'Keypad(s)', type: 'checkbox' },
          { key: 'garage_wifi', label: 'Wifi', type: 'checkbox' },
        ],
      },
      {
        title: 'Exterior Colors',
        note: 'See final Exterior Spec sheet.',
        fields: [
          { key: 'hoa_approval_required', label: 'Needs HOA Approval', type: 'radio', options: ['Yes', 'No'] },
          { key: 'color_main_body', label: 'Main Body', type: 'text', placeholder: 'e.g. SW7551 Greek Villa' },
          { key: 'color_trim', label: 'Trim', type: 'text' },
          { key: 'color_fascia', label: 'Fascia', type: 'text' },
          { key: 'color_soffit', label: 'Soffit', type: 'text' },
          { key: 'color_accent', label: 'Accent', type: 'text', placeholder: 'e.g. Shutters: SW 6211 Rainwashed' },
          { key: 'color_front_entry', label: 'Front Entry', type: 'text' },
          { key: 'color_exterior_doors', label: 'Exterior Doors', type: 'text' },
          { key: 'color_garage_doors', label: 'Garage Doors', type: 'text' },
        ],
      },
    ],
  },
  {
    num: 2,
    title: 'Pre-Slab Interior',
    description: 'Required before the slab is poured. These selections cannot be changed or added to after the slab is completed.',
    sections: [
      {
        title: 'Pool & Outdoor',
        fields: [
          { key: 'pool', label: 'Pool', type: 'radio', options: ['Yes', 'No'] },
          { key: 'pool_heater', label: 'Pool Heater', type: 'radio', options: ['None', 'Gas', 'Electric', 'Solar'] },
          { key: 'pool_notes', label: 'Notes', type: 'textarea' },
          { key: 'summer_kitchen', label: 'Summer Kitchen', type: 'radio', options: ['Yes', 'No', 'Future plumb only'] },
          { key: 'summer_kitchen_sink', label: 'Summer Kitchen Sink', type: 'radio', options: ['Yes', 'No'] },
          { key: 'summer_kitchen_ice_maker', label: 'Ice Maker', type: 'radio', options: ['Yes', 'No'] },
        ],
      },
      {
        title: 'Layout & Rough-Ins',
        note: 'Refer to printed plans for cabinet, outlet, and foundation detail.',
        fields: [
          { key: 'cabinet_layout', label: 'Cabinet Layout', type: 'textarea', placeholder: 'Plumbing & appliance locations to plan for. Does not include cabinet style/color.' },
          { key: 'floor_outlets', label: 'Floor Outlets', type: 'radio', options: ['Yes', 'No'] },
          { key: 'floor_outlets_notes', label: 'Floor outlet notes', type: 'textarea', placeholder: 'Quantity and locations', disableWhen: { key: 'floor_outlets', value: 'No' } },
          { key: 'foundation_steps_recess', label: 'Foundation Steps & Recess', type: 'textarea', placeholder: 'Includes showers' },
        ],
      },
      {
        title: 'Vendor Approvals',
        fields: [
          { key: 'appliance_cooktop', label: 'Cooktop', type: 'radio', options: ['Gas', 'Electric'] },
          { key: 'plumbing_approval_date', label: 'Plumbing Approval Date', type: 'date' },
          { key: 'appliance_approval_date', label: 'Appliance Approval Date', type: 'date' },
        ],
      },
    ],
  },
  {
    num: 3,
    title: 'Framing',
    description: 'Required before framing. After framing we’ll do a walk-through to confirm everything is in place and make any necessary corrections.',
    sections: [
      {
        title: 'Framing',
        fields: [
          { key: 'shower_niches', label: 'Shower Niches', type: 'radio', options: ['Yes', 'No'] },
          { key: 'shower_niches_notes', label: 'Niche notes', type: 'text', placeholder: 'e.g. spec’d on site at walk-thru' },
          { key: 'shower_ledge', label: 'Shower Ledge', type: 'radio', options: ['Yes', 'No'] },
          { key: 'wall_blocking', label: 'Blocking in walls or ceiling', type: 'radio', options: ['Yes', 'No'] },
          { key: 'wall_blocking_notes', label: 'Blocking notes', type: 'text' },
        ],
      },
      {
        title: 'Stairs',
        fields: [
          { key: 'stairs', label: 'Stairs', type: 'radio', options: ['Yes', 'No'] },
          { key: 'stairs_material', label: 'Stair Material', type: 'radio', options: ['Wood', 'Carpet', 'LVT'], hideWhen: { key: 'stairs', value: 'No' } },
          { key: 'stair_risers', label: 'Risers', type: 'radio', options: ['Stained', 'Painted'], hideWhen: { key: 'stairs', value: 'No' } },
          { key: 'stair_treads', label: 'Treads', type: 'radio', options: ['Stained', 'Painted'], hideWhen: { key: 'stairs', value: 'No' } },
          { key: 'stair_railing', label: 'Railing', type: 'radio', options: ['Standard', 'Custom'], hideWhen: { key: 'stairs', value: 'No' } },
          { key: 'stair_rail_finish', label: 'Rail Finish', type: 'radio', options: ['Stained', 'Painted'], hideWhen: { key: 'stairs', value: 'No' } },
        ],
      },
      {
        title: 'Fireplace',
        fields: [
          { key: 'fireplace', label: 'Fireplace', type: 'radio', options: ['Yes', 'No'] },
          { key: 'fireplace_selection', label: 'Fireplace Selection', type: 'text', placeholder: 'e.g. Vector 50" gas Fireplace', disableWhen: { key: 'fireplace', value: 'No' } },
          { key: 'fireplace_mantle', label: 'Mantle', type: 'radio', options: ['Yes', 'No'], disableWhen: { key: 'fireplace', value: 'No' } },
          { key: 'fireplace_placement', label: 'Placement', type: 'radio', options: ['Set back', 'Set forward', 'Flush to wall'], disableWhen: { key: 'fireplace', value: 'No' } },
          { key: 'fireplace_height', label: 'Height', type: 'radio', options: ['Floor', 'Raised'], disableWhen: { key: 'fireplace', value: 'No' } },
          { key: 'fireplace_notes', label: 'Notes', type: 'text', placeholder: 'e.g. mantle with leg surround', disableWhen: { key: 'fireplace', value: 'No' } },
        ],
      },
      {
        title: 'Cabinetry',
        fields: [
          { key: 'cabinet_binder_approved', label: 'Cabinet Drawings + Binder Approved for order', type: 'radio', options: ['Yes', 'No'] },
          { key: 'cabinet_framing_changes', label: 'Framing changes needed for cabinet install', type: 'radio', options: ['Yes', 'No'] },
          { key: 'cabinet_framing_changes_spec', label: 'Framing changes — specify', type: 'textarea' },
          { key: 'puck_lights', label: 'Cabinet Puck Lights', type: 'radio', options: ['Yes', 'No'] },
          { key: 'cabinet_approval_date', label: 'Cabinet Approval Date', type: 'date' },
        ],
      },
    ],
  },
  {
    num: 4,
    title: 'Trades',
    description: 'Required before trades begin. Includes a pre-sub walk-through where the cabinet layout is drawn on the floor and lighting, trim, plumbing, and electrical locations are finalized.',
    sections: [
      {
        title: 'Interior Trim Package',
        fields: [
          { key: 'door_casing', label: 'Door Casing', type: 'radio', options: ['Flat Stock', 'Other'] },
          { key: 'door_casing_other', label: 'Door casing — specify', type: 'text' },
          { key: 'window_casing', label: 'Window Casing', type: 'radio', options: ['Yes', 'No'] },
          { key: 'window_casing_style', label: 'Window casing — style', type: 'text' },
          { key: 'window_casing_locations', label: 'Window casing — locations', type: 'radio', options: ['Main Living', 'Bedrooms/Bathrooms'], fullWidth: true },
          { key: 'beams', label: 'Beams', type: 'radio', options: ['Yes', 'No'] },
          { key: 'beams_style', label: 'Beams — style and location', type: 'text' },
          { key: 'wainscoting', label: 'Wainscoting', type: 'radio', options: ['Yes', 'No'] },
          { key: 'wainscoting_style', label: 'Wainscoting — style and location', type: 'text' },
          { key: 'slick_walls', label: 'Slick Walls', type: 'radio', options: ['Yes', 'No'] },
          { key: 'slick_walls_locations', label: 'Slick walls — locations', type: 'text' },
          { key: 'crown_molding', label: 'Crown Molding', type: 'radio', options: ['Yes', 'No'] },
          { key: 'crown_molding_style', label: 'Crown Molding Style', type: 'text' },
          { key: 'crown_molding_locations', label: 'Crown molding — locations', type: 'radio', options: ['Main Living', 'Bedrooms', 'Bathrooms'], fullWidth: true },
          { key: 'ceiling_details', label: 'Ceiling Details', type: 'radio', options: ['Yes', 'No'] },
          { key: 'ceiling_details_style', label: 'Ceiling details — style and location', type: 'text' },
          { key: 'main_flooring', label: 'Main Flooring', type: 'radio', options: ['Wood', 'Vinyl', 'Tile'] },
          { key: 'guest_bedrooms_flooring', label: 'Guest Bedrooms Flooring', type: 'radio', options: ['Wood', 'Vinyl', 'Carpet'] },
          { key: 'master_bedroom_flooring', label: 'Master Bedroom Flooring', type: 'radio', options: ['Wood', 'Vinyl', 'Carpet'] },
          { key: 'bonus_room_flooring', label: 'Bonus Room Flooring', type: 'radio', options: ['Wood', 'Vinyl', 'Carpet'] },
          { key: 'fireplace_design', label: 'Fireplace Mantle Design', type: 'textarea' },
          { key: 'additional_trim_work', label: 'Additional Trim Work incl. Built Ins, Benches, Floating Shelves', type: 'textarea' },
        ],
      },
      {
        title: 'Doors & Hardware',
        fields: [
          { key: 'interior_hinge_finish', label: 'Interior Hinge Finish', type: 'text' },
          { key: 'exterior_hinge_finish', label: 'Exterior Hinge Finish', type: 'text' },
          { key: 'interior_door_style', label: 'Interior Doors', type: 'radio', options: ['Solid Core', 'Hollow Core'] },
          { key: 'interior_door_style_note', label: 'Interior Door Style', type: 'text' },
          { key: 'exterior_door_style_note', label: 'Exterior Door Style — note (selected in Phase 1)', type: 'text' },
          { key: 'interior_door_hardware', label: 'Interior Door Hardware', type: 'radio', options: ['Kwikset', 'Emtek'] },
          { key: 'interior_door_hardware_finish', label: 'Interior hardware finish', type: 'text' },
          { key: 'exterior_door_hardware', label: 'Exterior Door Hardware', type: 'text' },
          { key: 'exterior_door_hardware_finish', label: 'Exterior hardware finish', type: 'text' },
          { key: 'keypad_locks', label: 'Keypad Electronic Locks', type: 'radio', options: ['Yes', 'No'] },
          { key: 'keypad_locks_locations', label: 'Keypad locations', type: 'text' },
          { key: 'keypad_locks_model', label: 'Keypad Lock Model Number', type: 'text' },
        ],
      },
      {
        title: 'Accent Doors',
        fields: [
          { key: 'accent_door_study', label: 'Study', type: 'text' },
          { key: 'accent_door_pantry', label: 'Pantry Door', type: 'text' },
          { key: 'accent_door_other', label: 'Other', type: 'text' },
        ],
      },
      {
        title: 'Electrical',
        note: 'On-site walk-through with the electrician — switch locations, fixture types, outlets, TVs, and all wiring. Actual fixtures are picked in Phase 5.',
        fields: [
          { key: 'bulb_color_temp', label: 'Light Bulb Color Temperature', type: 'radio', options: ['4000k (standard)', '3000k'] },
          { key: 'light_switches', label: 'Light Switches', type: 'radio', options: ['Toggle (standard)', 'Rocker (Decora)'] },
          { key: 'dimmer_qty', label: 'Dimmer Switches', type: 'radio', options: ['Yes', 'No'] },
          { key: 'dimmer_locations', label: 'Dimmer Switches — Locations', type: 'text' },
          { key: 'generator', label: 'Generator', type: 'radio', options: ['Yes', 'No', 'Plug only', 'Pre-wire only'] },
          { key: 'structured_wiring', label: 'Structured Wiring', type: 'radio', options: ['Yes', 'No'] },
          { key: 'security_system', label: 'Security System', type: 'radio', options: ['Yes', 'No'] },
          { key: 'surround_sound', label: 'Surround Sound', type: 'radio', options: ['Yes', 'No'] },
        ],
      },
      {
        title: 'Lighting Heights',
        note: 'Standard heights below — pick "Other" to spec a custom height during install.',
        fields: [
          { key: 'vanity_bar_height', label: 'Vanity bars (46" above splash to backplate)', type: 'radio', options: ['Standard', 'Other'] },
          { key: 'vanity_bar_height_other', label: 'Vanity bars — custom height', type: 'text' },
          { key: 'dining_pendant_height', label: 'Dining pendants (72" above floor to bottom)', type: 'radio', options: ['Standard', 'Other'] },
          { key: 'dining_pendant_height_other', label: 'Dining pendants — custom height', type: 'text' },
          { key: 'island_pendant_height', label: 'Island pendants (72" above floor to bottom)', type: 'radio', options: ['Standard', 'Other'] },
          { key: 'island_pendant_height_other', label: 'Island pendants — custom height', type: 'text' },
          { key: 'bath_sconce_height', label: 'Bathroom sconces (65" above floor to bottom)', type: 'radio', options: ['Standard', 'Other'] },
          { key: 'bath_sconce_height_other', label: 'Bath sconces — custom height', type: 'text' },
        ],
      },
      {
        title: 'HVAC',
        fields: [
          { key: 'thermostat_location', label: 'Thermostat Location', type: 'radio', options: ['Recommended', 'Custom'] },
          { key: 'custom_vent_locations', label: 'Custom vent locations (ceiling detail)', type: 'textarea' },
        ],
      },
      {
        title: 'Pool',
        fields: [
          { key: 'pool_control', label: 'Pool Control', type: 'radio', options: ['Light Switch', 'Automation'] },
          { key: 'pool_heater_phase4', label: 'Pool Heater', type: 'radio', options: ['None', 'Gas', 'Electric', 'Solar'] },
        ],
      },
      {
        title: 'Electrical Plan Approval',
        fields: [
          { key: 'electrical_plan_approved', label: 'Electrical plan approved', type: 'radio', options: ['Yes', 'No'] },
        ],
      },
    ],
  },
  {
    num: 5,
    title: 'Finishes',
    description: 'Final fit and finish selections — approved before trades order materials. Some sections have vendor binders that stay attached as references to your selections.',
    sections: [
      {
        title: 'Interior Paint Colors',
        note: 'Standard finishes — Walls: Flat · Ceilings: Flat · Trim + Doors: Satin · Exterior Doors: Satin.',
        fields: [
          { key: 'paint_approved', label: 'Approved', type: 'radio', options: ['Yes', 'No'] },
          { key: 'paint_approval_date', label: 'Approval Date', type: 'date' },
        ],
      },
      {
        title: 'Tile',
        note: 'See tile selections sheet.',
        fields: [
          { key: 'tile_selections_approved', label: 'Tile Selections Approved', type: 'radio', options: ['Yes', 'No'] },
        ],
      },
      {
        title: 'Flooring',
        fields: [
          { key: 'flooring_selection_1', label: 'Flooring Selection', type: 'textarea' },
          { key: 'flooring_selection_2', label: 'Flooring Selection', type: 'textarea' },
          { key: 'flooring_selection_3', label: 'Flooring Selection', type: 'textarea' },
          { key: 'flooring_install_direction', label: 'Install Direction', type: 'text', placeholder: 'e.g. front to back' },
          { key: 'flooring_transitions', label: 'Transitions', type: 'radio', options: ['Standard', 'Custom Linear'] },
          { key: 'flooring_approval_date', label: 'Flooring Approval Date', type: 'date' },
        ],
      },
      {
        title: 'Counter Tops',
        fields: [
          { key: 'countertop_selections_approved', label: 'Countertop Selections Approved', type: 'radio', options: ['Yes', 'No'] },
          { key: 'counter_outlets_in_splash', label: 'Outlets installed in splash', type: 'radio', options: ['Yes', 'No'] },
          { key: 'counter_outlets_locations', label: 'Outlet locations', type: 'text' },
          { key: 'counter_approval_date', label: 'Selection + Layout Approval Date', type: 'date' },
        ],
      },
      {
        title: 'Lighting Package Approval',
        fields: [
          { key: 'lighting_approval_date', label: 'Lighting Package Approval Date', type: 'date' },
        ],
      },
      {
        title: 'Shower Glass',
        fields: [
          { key: 'shower_glass_approved', label: 'Approved', type: 'radio', options: ['Yes', 'No'] },
          { key: 'shower_glass_approval_date', label: 'Shower Glass Approval Date', type: 'date' },
        ],
      },
      {
        title: 'Closet Shelving',
        fields: [
          { key: 'closet_change_order_needed', label: 'Is change order needed to move outlet locations?', type: 'radio', options: ['Yes', 'No'] },
          { key: 'closet_change_order_specify', label: 'Please specify', type: 'textarea' },
          { key: 'closet_approval_date', label: 'Closet Shelving Approval Date', type: 'date' },
        ],
      },
    ],
  },
];

function SelectionsSheet({ project, isAdmin, profile, canSign = true }) {
  const [rows, setRows] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [openPhases, setOpenPhases] = React.useState(() => new Set());
  const [signingPhase, setSigningPhase] = React.useState(null);
  const [signName, setSignName] = React.useState('');
  const [signing, setSigning] = React.useState(false);
  const [signMethod, setSignMethod] = React.useState('typed');
  const [drawnSignature, setDrawnSignature] = React.useState(null);
  const [savedSignature, setSavedSignature] = React.useState(profile?.signature_image || null);

  const refreshSavedSignature = React.useCallback(async () => {
    const { data: { user } } = await sb.auth.getUser();
    if (!user) return null;
    const { data } = await sb.from('profiles').select('signature_image').eq('id', user.id).single();
    const sig = data?.signature_image || null;
    setSavedSignature(sig);
    return sig;
  }, []);

  React.useEffect(() => {
    let alive = true;
    refreshSavedSignature().then(() => { if (!alive) {} });
    return () => { alive = false; };
  }, [refreshSavedSignature]);

  const reload = React.useCallback(async () => {
    const { data } = await sb.from('project_selections')
      .select('*').eq('project_id', project.id);
    setRows(data || []);
    setLoading(false);
  }, [project.id]);
  React.useEffect(() => { reload(); }, [reload]);

  const rowFor = (phase) => rows.find(r => r.phase === phase);

  const togglePhase = (num) => {
    setOpenPhases(prev => {
      const next = new Set(prev);
      if (next.has(num)) next.delete(num); else next.add(num);
      return next;
    });
  };

  const persistField = async (phase, key, value) => {
    const existing = rowFor(phase);
    const newData = { ...(existing?.data || {}), [key]: value };
    if (existing) {
      setRows(prev => prev.map(r => r.phase === phase ? { ...r, data: newData } : r));
      const { error } = await sb.from('project_selections')
        .update({ data: newData, updated_at: new Date().toISOString() })
        .eq('id', existing.id);
      if (error) { alert(`Could not save: ${error.message}`); reload(); }
    } else {
      const { data, error } = await sb.from('project_selections')
        .insert({ project_id: project.id, phase, data: newData })
        .select('*').single();
      if (error) { alert(`Could not save: ${error.message}`); return; }
      if (data) setRows(prev => [...prev, data]);
    }
  };

  const openSignDialog = async (phase) => {
    setSigningPhase(phase);
    setSignName(profile?.full_name || '');
    setDrawnSignature(null);
    const sig = await refreshSavedSignature();
    setSignMethod(sig ? 'saved' : 'typed');
  };
  const closeSignDialog = () => {
    setSigningPhase(null);
    setSignName('');
    setDrawnSignature(null);
    setSignMethod('typed');
  };
  const submitSign = async () => {
    const name = signName.trim();
    if (!name) { alert('Please type your full name to sign.'); return; }
    let signatureImage = null;
    if (signMethod === 'saved') {
      if (!savedSignature) { alert('No saved signature found. Add one in Account.'); return; }
      signatureImage = savedSignature;
    } else if (signMethod === 'drawn') {
      if (!drawnSignature) { alert('Please draw and save your signature below.'); return; }
      signatureImage = drawnSignature;
    }
    setSigning(true);
    const { error } = await sb.rpc('sign_selection', {
      p_project_id: project.id,
      p_phase: signingPhase,
      p_name: name,
      p_signature_image: signatureImage,
    });
    setSigning(false);
    if (error) { alert(error.message); return; }
    closeSignDialog();
    reload();
  };

  const unsignPhase = async (phase) => {
    if (!confirm('Clear signature for this phase? The client will be able to sign again after any changes.')) return;
    const { error } = await sb.rpc('unsign_selection', { p_project_id: project.id, p_phase: phase });
    if (error) { alert(error.message); return; }
    reload();
  };

  const savePhaseAsPDF = (phase) => {
    const phaseEl = document.getElementById(`selections-phase-${phase.num}`);
    if (!phaseEl) return;

    const win = window.open('', '_blank');
    if (!win) {
      alert('Please allow popups for this site so we can open the PDF preview.');
      return;
    }

    const styleLinks = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
      .map(l => `<link rel="stylesheet" href="${l.href}">`)
      .join('\n');

    const projectLabel = (project && (project.name || project.address)) || '';
    const docTitle = `Phase ${phase.num} — ${phase.title}${projectLabel ? ' · ' + projectLabel : ''}`;

    win.document.open();
    win.document.write(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>${docTitle}</title>
  ${styleLinks}
  <style>
    body { background: #fff; padding: 28px; font-family: system-ui, -apple-system, sans-serif; color: #1f2937; margin: 0; }
    .pdf-header { margin-bottom: 24px; padding-bottom: 14px; border-bottom: 2px solid #1f3a5f; }
    .pdf-header h1 { margin: 0; color: #1f3a5f; font-family: Georgia, 'Times New Roman', serif; font-size: 1.5rem; }
    .pdf-header .pdf-meta { margin-top: 6px; color: #6b7280; font-size: 0.9rem; }
    .selections-phase__head, .selections-phase__toggle, .selections-phase__badge { display: none !important; }
    .selections-phase__body { display: block !important; padding: 0 !important; border: 0 !important; }
    .selections-phase__pdf, .pdf-button, button { display: none !important; }
    .selections-signature { background: #faf7ee !important; }
    input, textarea, select {
      background: #fff !important;
      color: #1f2937 !important;
      opacity: 1 !important;
    }
    input:disabled, textarea:disabled, select:disabled {
      background: #f7f7f4 !important;
      color: #1f2937 !important;
      opacity: 1 !important;
    }
    @media print {
      /* Put the breathing room on @page so EVERY page (including pages
         that come from a natural section break) has clean top/bottom
         margins, not just the first page. The browser-injected date /
         page-title headers are suppressed by unchecking "Headers and
         footers" in the print dialog (one-time setting; Chrome
         remembers it). */
      @page { margin: 0.6in 0.5in; size: letter; }
      html, body {
        background: #fff !important;
        margin: 0 !important;
        padding: 0 !important;
      }
      /* Drop the card border + background that surrounds the phase on
         screen — looks like a grey box around the whole document in print. */
      .selections-phase {
        border: 0 !important;
        border-radius: 0 !important;
        background: #fff !important;
        box-shadow: none !important;
        overflow: visible !important;
      }
      .pdf-header { page-break-after: avoid; }
      /* Let sections break across pages so they fill each page naturally
         (avoid leaves huge white gaps when a section is taller than the
         remaining page). Keep titles attached to their content; also add
         a little top padding so a section that lands at the top of a
         page has visible breathing room above its title. */
      .selections-section { page-break-inside: auto; padding-top: 6px; }
      .selections-section:first-of-type { padding-top: 0; }
      .selections-section__title { page-break-after: avoid; break-after: avoid; }
      .selections-field { page-break-inside: avoid; break-inside: avoid; }
    }
  </style>
</head>
<body>
  <div class="pdf-header">
    <h1>Phase ${phase.num} — ${phase.title}</h1>
    ${projectLabel ? `<div class="pdf-meta">${projectLabel}</div>` : ''}
    <div class="pdf-meta">Saved ${new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</div>
  </div>
  ${phaseEl.outerHTML}
</body>
</html>`);
    win.document.close();

    const triggerPrint = () => {
      try { win.focus(); } catch (e) {}
      win.print();
    };
    if (win.document.readyState === 'complete') {
      setTimeout(triggerPrint, 300);
    } else {
      win.addEventListener('load', () => setTimeout(triggerPrint, 300));
    }
  };

  if (loading) return <p style={{ textAlign: 'center', opacity: 0.7 }}>Loading…</p>;

  return (
    <div className="selections-sheet">
      {!isAdmin && canSign && (
        <p className="selections-sheet__intro">
          Review your selections for each phase. Once you’re satisfied, sign the bottom of a phase to lock it in.
        </p>
      )}
      {PHASE_TEMPLATES.map(phase => {
        const row = rowFor(phase.num);
        const data = (row && row.data) || {};
        const signed = !!(row && row.signed_at);
        const isOpen = openPhases.has(phase.num);
        const lockedForClient = signed && !isAdmin;
        return (
          <div key={phase.num} id={`selections-phase-${phase.num}`} className={`selections-phase${signed ? ' is-signed' : ''}${lockedForClient ? ' is-locked' : ''}${isOpen ? ' is-open' : ''}`}>
            <button type="button" className="selections-phase__head" onClick={() => togglePhase(phase.num)}>
              <span className="selections-phase__num">Phase {phase.num}</span>
              <span className="selections-phase__title">{phase.title}</span>
              {signed && <span className="selections-phase__badge">✓ Signed</span>}
              <span className="selections-phase__toggle">{isOpen ? '−' : '+'}</span>
            </button>
            {isOpen && (
              <div className="selections-phase__body">
                {phase.description && <p className="selections-phase__desc">{phase.description}</p>}
                {phase.sections.map(section => (
                  <SelectionSection
                    key={section.title}
                    section={section}
                    data={data}
                    canEdit={isAdmin && !signed}
                    onChange={(k, v) => persistField(phase.num, k, v)}
                  />
                ))}
                <SelectionSignatureBlock
                  row={row}
                  isAdmin={isAdmin}
                  canSign={canSign}
                  onSign={() => openSignDialog(phase.num)}
                  onUnsign={() => unsignPhase(phase.num)}
                />
                <div className="selections-phase__pdf">
                  <button type="button" className="portal-link-btn pdf-button" onClick={() => savePhaseAsPDF(phase)}>
                    Save as PDF
                  </button>
                </div>
              </div>
            )}
          </div>
        );
      })}

      {signingPhase != null && (
        <div className="selections-sign-modal" onClick={closeSignDialog}>
          <div className="selections-sign-modal__card" onClick={(e) => e.stopPropagation()}>
            <h3>Sign Phase {signingPhase}</h3>
            <p>Confirm your full name and choose how you'd like to sign. Once signed, this phase is locked.</p>
            <div className="form__field">
              <label className="form__label">Full name</label>
              <input className="form__input" type="text" value={signName}
                onChange={(e) => setSignName(e.target.value)} autoFocus />
            </div>

            <div className="sign-method">
              <span className="sign-method__label">Signature</span>
              <div className="sign-method__options">
                <label className="sign-method__opt">
                  <input type="radio" name="sign-method" value="typed"
                    checked={signMethod === 'typed'}
                    onChange={() => setSignMethod('typed')} />
                  <span>Just my typed name</span>
                </label>
                <label className={`sign-method__opt${!savedSignature ? ' is-disabled' : ''}`}>
                  <input type="radio" name="sign-method" value="saved"
                    checked={signMethod === 'saved'}
                    disabled={!savedSignature}
                    onChange={() => setSignMethod('saved')} />
                  <span>Use my saved signature</span>
                </label>
                <label className="sign-method__opt">
                  <input type="radio" name="sign-method" value="drawn"
                    checked={signMethod === 'drawn'}
                    onChange={() => setSignMethod('drawn')} />
                  <span>Draw a one-time signature</span>
                </label>
              </div>
              {!savedSignature && (
                <p className="sign-method__hint">
                  Tip: save a signature in Account to reuse it across phases.
                </p>
              )}
            </div>

            {signMethod === 'saved' && savedSignature && (
              <div className="sign-method__preview">
                <img src={savedSignature} alt="Saved signature" />
              </div>
            )}
            {signMethod === 'drawn' && (
              <div className="sign-method__pad">
                {drawnSignature ? (
                  <div className="signature-saved">
                    <span className="signature-saved__label">Drawn signature</span>
                    <img src={drawnSignature} alt="Drawn signature" className="signature-saved__img" />
                    <button type="button" className="btn" onClick={() => setDrawnSignature(null)}>
                      Redraw
                    </button>
                  </div>
                ) : (
                  <SignaturePad onSave={(d) => setDrawnSignature(d)} height={150} />
                )}
              </div>
            )}

            <div style={{ display: 'flex', gap: 12, marginTop: 16 }}>
              <button className="btn btn--solid" onClick={submitSign} disabled={signing}>
                {signing ? 'Signing…' : 'Sign & Lock'}
              </button>
              <button className="btn" onClick={closeSignDialog} disabled={signing}>Cancel</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function SelectionSection({ section, data, canEdit, onChange }) {
  const isVisible = (f) => {
    if (!f.hideWhen) return true;
    return data[f.hideWhen.key] !== f.hideWhen.value;
  };
  const isDisabledByCondition = (f) => {
    if (!f.disableWhen) return false;
    return data[f.disableWhen.key] === f.disableWhen.value;
  };
  return (
    <div className="selections-section">
      <h4 className="selections-section__title">{section.title}</h4>
      {section.note && <p className="selections-section__note">{section.note}</p>}
      <div className="selections-section__fields">
        {section.fields.filter(isVisible).map((f, idx) => {
          if (f.type === 'header') {
            return (
              <h5
                key={f.label + '-' + idx}
                className="selections-section__subtitle selections-field--full"
              >
                {f.label}
              </h5>
            );
          }
          const conditionallyDisabled = isDisabledByCondition(f);
          return (
            <SelectionField
              key={f.key}
              field={f}
              value={data[f.key]}
              canEdit={canEdit && !conditionallyDisabled}
              isConditionallyDisabled={conditionallyDisabled}
              onCommit={(v) => onChange(f.key, v)}
            />
          );
        })}
      </div>
    </div>
  );
}

function SelectionField({ field, value, canEdit, isConditionallyDisabled, onCommit }) {
  const dimClass = isConditionallyDisabled ? ' selections-field--dim' : '';
  if (field.type === 'checkbox') {
    return (
      <label className={`selections-field selections-field--check${dimClass}`}>
        <input type="checkbox" checked={!!value} disabled={!canEdit}
          onChange={(e) => onCommit(e.target.checked)} />
        <span>{field.label}</span>
      </label>
    );
  }
  if (field.type === 'radio') {
    return (
      <div className={`selections-field${field.fullWidth ? ' selections-field--full' : ''}${dimClass}`}>
        <span className="selections-field__label">{field.label}</span>
        <div className="selections-field__radio-group">
          {field.options.map((opt, idx) => {
            const optValue = typeof opt === 'object' ? opt.value : opt;
            const optLabel = typeof opt === 'object' ? opt.label : opt;
            return (
              <label key={`${optValue}-${idx}`} className="selections-field__radio">
                <input type="radio" name={field.key} value={optValue}
                  checked={value === optValue} disabled={!canEdit}
                  onChange={() => onCommit(optValue)} />
                <span>{optLabel}</span>
              </label>
            );
          })}
          {canEdit && value && (
            <button type="button" className="selections-field__clear"
              onClick={() => onCommit(null)} aria-label="Clear">Clear</button>
          )}
        </div>
      </div>
    );
  }
  if (field.type === 'textarea') {
    return (
      <div className={`selections-field selections-field--full${dimClass}`}>
        <span className="selections-field__label">{field.label}</span>
        <textarea
          className="selections-field__textarea"
          defaultValue={value || ''}
          placeholder={field.placeholder || ''}
          disabled={!canEdit}
          rows={2}
          onBlur={(e) => { const v = e.target.value || null; if (v !== (value || null)) onCommit(v); }}
        />
      </div>
    );
  }
  // text or date
  return (
    <div className={`selections-field${dimClass}`}>
      <span className="selections-field__label">{field.label}</span>
      <input
        className="selections-field__input"
        type={field.type === 'date' ? 'date' : 'text'}
        defaultValue={value || ''}
        placeholder={field.placeholder || ''}
        disabled={!canEdit}
        onBlur={(e) => { const v = e.target.value || null; if (v !== (value || null)) onCommit(v); }}
      />
    </div>
  );
}

function SelectionSignatureBlock({ row, isAdmin, canSign = true, onSign, onUnsign }) {
  const signed = !!(row && row.signed_at);
  const signedDate = signed ? new Date(row.signed_at).toLocaleDateString('en-US', {
    month: 'long', day: 'numeric', year: 'numeric',
  }) : '';
  return (
    <div className={`selections-signature${signed ? ' is-signed' : ''}`}>
      {signed ? (
        <React.Fragment>
          <div className="selections-signature__stamp">
            {row.signed_signature_image && (
              <img src={row.signed_signature_image} alt="Signature"
                className="selections-signature__img" />
            )}
            <span className="selections-signature__name">{row.signed_name || 'Signed'}</span>
            <span className="selections-signature__meta">Signed on {signedDate}</span>
          </div>
          {isAdmin && (
            <button type="button" className="btn" onClick={onUnsign}>
              Clear signature
            </button>
          )}
        </React.Fragment>
      ) : (
        <React.Fragment>
          <div className="selections-signature__line">
            <span className="selections-signature__line-label">Client Signature</span>
            <span className="selections-signature__line-rule" />
          </div>
          {canSign && (
            <button type="button" className="btn btn--solid" onClick={onSign}>
              Sign this phase
            </button>
          )}
        </React.Fragment>
      )}
    </div>
  );
}

function PortalPhotoLightbox({ items, idx, onClose, onIdxChange, bucket = 'project-photos' }) {
  const [url, setUrl] = React.useState(null);
  const total = items.length;

  React.useEffect(() => {
    let alive = true;
    setUrl(null);
    sb.storage.from(bucket)
      .createSignedUrl(items[idx].storage_path, 3600)
      .then(({ data }) => { if (alive) setUrl(data?.signedUrl || null); });
    return () => { alive = false; };
  }, [items, idx, bucket]);

  React.useEffect(() => {
    const preload = (i) => {
      sb.storage.from(bucket).createSignedUrl(items[i].storage_path, 3600)
        .then(({ data }) => { if (data?.signedUrl) { const im = new Image(); im.src = data.signedUrl; } });
    };
    if (total > 1) {
      preload((idx + 1) % total);
      preload((idx - 1 + total) % total);
    }
  }, [items, idx, total, bucket]);

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight' && total > 1) onIdxChange((idx + 1) % total);
      if (e.key === 'ArrowLeft' && total > 1) onIdxChange((idx - 1 + total) % total);
    };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => {
      window.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
    };
  }, [idx, total, onClose, onIdxChange]);

  // Swipe support for mobile
  const touchStart = React.useRef(null);
  const onTouchStart = (e) => { touchStart.current = e.touches[0].clientX; };
  const onTouchEnd = (e) => {
    if (touchStart.current == null || total <= 1) return;
    const dx = e.changedTouches[0].clientX - touchStart.current;
    if (dx > 50) onIdxChange((idx - 1 + total) % total);
    else if (dx < -50) onIdxChange((idx + 1) % total);
    touchStart.current = null;
  };

  const item = items[idx];
  const panoRef = React.useRef(null);
  React.useEffect(() => {
    if (!item.is_panorama || !url || !panoRef.current || !window.pannellum) return;
    const viewer = window.pannellum.viewer(panoRef.current, {
      type: 'equirectangular',
      panorama: url,
      autoLoad: true,
      showControls: true,
      compass: false,
    });
    return () => { try { viewer.destroy(); } catch {} };
  }, [item.is_panorama, url]);
  return (
    <div className="portal-lightbox" onClick={onClose} onTouchStart={item.is_panorama ? undefined : onTouchStart} onTouchEnd={item.is_panorama ? undefined : onTouchEnd}>
      <button className="portal-lightbox__close" onClick={onClose} aria-label="Close">×</button>
      <div className="portal-lightbox__counter">
        {idx + 1} / {total}{item.is_panorama && ' • 360°'}
      </div>
      {!url && <div className="portal-lightbox__loading">Loading…</div>}
      {url && item.is_panorama && (
        <div ref={panoRef} className="portal-lightbox__pano" onClick={(e) => e.stopPropagation()} />
      )}
      {url && !item.is_panorama && (
        <img className="portal-lightbox__img" src={url} alt={item.caption || ''} onClick={(e) => e.stopPropagation()} />
      )}
      {item.caption && <div className="portal-lightbox__caption" onClick={(e) => e.stopPropagation()}>{item.caption}</div>}
      {total > 1 && (
        <React.Fragment>
          <button className="portal-lightbox__nav portal-lightbox__nav--prev"
            onClick={(e) => { e.stopPropagation(); onIdxChange((idx - 1 + total) % total); }}
            aria-label="Previous">‹</button>
          <button className="portal-lightbox__nav portal-lightbox__nav--next"
            onClick={(e) => { e.stopPropagation(); onIdxChange((idx + 1) % total); }}
            aria-label="Next">›</button>
        </React.Fragment>
      )}
    </div>
  );
}

function EditModeToggle({ active, onClick, label = 'Edit' }) {
  return (
    <button type="button" className="album-download-btn"
      onClick={onClick}
      aria-expanded={active}
      aria-label={active ? `Close ${label.toLowerCase()} menu` : `${label} menu`}
      title={active ? `Close ${label.toLowerCase()} menu` : label}>
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
        strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <line x1="3" y1="6" x2="21" y2="6" />
        <line x1="3" y1="12" x2="21" y2="12" />
        <line x1="3" y1="18" x2="21" y2="18" />
      </svg>
    </button>
  );
}

function DownloadLink({ bucket, path, label }) {
  const open = (e) => {
    // Open a placeholder tab SYNCHRONOUSLY so iOS Safari keeps the user
    // gesture; we redirect it to the signed URL once it comes back.
    const win = window.open('about:blank', '_blank');
    sb.storage.from(bucket).createSignedUrl(path, 60, { download: true })
      .then(({ data }) => {
        if (!data?.signedUrl) {
          if (win) win.close();
          return;
        }
        if (win && !win.closed) {
          win.location.href = data.signedUrl;
        } else {
          // Popup blocked — fall back to navigating the current tab.
          // Content-Disposition: attachment makes the browser download
          // without leaving the portal.
          window.location.href = data.signedUrl;
        }
      });
  };
  return <button className="portal-link-btn" onClick={open}>{label}</button>;
}

function InvoiceList({ items, onDelete, onTogglePaid }) {
  if (!items.length) return <p style={{ textAlign: 'center', opacity: 0.7 }}>No invoices yet.</p>;
  return (
    <table className="portal-table">
      <thead><tr>
        <th>Description</th><th>Amount</th><th>Due</th><th>Status</th><th></th>
        {onTogglePaid && <th></th>}
        {onDelete && <th></th>}
      </tr></thead>
      <tbody>
        {items.map(i => (
          <tr key={i.id}>
            <td>{i.description || 'Invoice'}</td>
            <td>{fmtMoney(i.amount_cents)}</td>
            <td>{i.due_date || '—'}</td>
            <td><span style={{ color: i.paid ? '#27ae60' : '#c0392b', fontWeight: 600 }}>{i.paid ? 'Paid' : 'Unpaid'}</span></td>
            <td>{i.storage_path && (
              <span style={{ display: 'inline-flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
                <DownloadLink bucket="project-invoices" path={i.storage_path} label="Download" />
                {i._size != null && <span style={{ color: 'var(--muted)', fontSize: '0.85rem' }}>{formatBytes(i._size)}</span>}
              </span>
            )}</td>
            {onTogglePaid && (
              <td>
                <button className="portal-link-btn" onClick={() => onTogglePaid(i)}>
                  Mark {i.paid ? 'Unpaid' : 'Paid'}
                </button>
              </td>
            )}
            {onDelete && <td><button className="portal-link-btn" onClick={() => onDelete(i)}>Delete</button></td>}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function FileList({ items, folders = [], onDelete, onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFile }) {
  const [activeFolderId, setActiveFolderId] = React.useState(null);
  const [dragOverFolderId, setDragOverFolderId] = React.useState(null);
  const [renaming, setRenaming] = React.useState(false);
  const [renameDraft, setRenameDraft] = React.useState('');
  const [savedFlash, setSavedFlash] = React.useState(false);
  const renameInputRef = React.useRef(null);
  const skipBlurSave = React.useRef(false);

  // Focus + select-all when entering rename mode.
  React.useEffect(() => {
    if (renaming && renameInputRef.current) {
      renameInputRef.current.focus();
      renameInputRef.current.select();
    }
  }, [renaming]);

  // Auto-hide the "Saved" indicator after 1.5s.
  React.useEffect(() => {
    if (!savedFlash) return;
    const t = setTimeout(() => setSavedFlash(false), 1500);
    return () => clearTimeout(t);
  }, [savedFlash]);

  const isAdmin = !!onDelete;

  // Pre-migration fallback: if no real folders came back but files still
  // have legacy folder text, synthesize folder objects so the UI keeps
  // working. Mutations on these legacy folders aren't supported.
  const usingLegacyFolders = folders.length === 0 && items.some(f => f.folder && !f.folder_id);
  let effectiveFolders = folders;
  let folderKeyForFile;
  if (usingLegacyFolders) {
    const names = Array.from(new Set(items.map(f => (f.folder || '').trim()).filter(Boolean))).sort();
    effectiveFolders = names.map(n => ({ id: '__txt:' + n, name: n, _legacy: true }));
    folderKeyForFile = (f) => {
      const t = (f.folder || '').trim();
      return t ? '__txt:' + t : null;
    };
  } else {
    folderKeyForFile = (f) => f.folder_id || null;
  }

  const filesByFolder = new Map();
  items.forEach(f => {
    const key = folderKeyForFile(f) || '__loose__';
    if (!filesByFolder.has(key)) filesByFolder.set(key, []);
    filesByFolder.get(key).push(f);
  });

  const sortedFolders = [...effectiveFolders].sort((a, b) => a.name.localeCompare(b.name));
  const looseFiles = filesByFolder.get('__loose__') || [];

  // Drilled into a folder
  if (activeFolderId !== null) {
    const folder = sortedFolders.find(f => f.id === activeFolderId);
    if (!folder) {
      // Folder vanished (deleted) — pop back up.
      setTimeout(() => setActiveFolderId(null), 0);
      return null;
    }
    const folderFiles = filesByFolder.get(folder.id) || [];
    const canMutate = isAdmin && !folder._legacy;
    const canRename = canMutate && !!onRenameFolder;

    const enterRenameMode = () => {
      if (!canRename) return;
      setRenameDraft(folder.name);
      setRenaming(true);
    };

    const exitRenameMode = () => setRenaming(false);

    const submitRename = async () => {
      const trimmed = (renameDraft || '').trim();
      // No change or empty → just exit cleanly, nothing to save.
      if (!trimmed || trimmed === folder.name || !onRenameFolder) {
        skipBlurSave.current = true;
        exitRenameMode();
        return;
      }
      const ok = await onRenameFolder(folder, trimmed);
      if (ok) {
        skipBlurSave.current = true;
        exitRenameMode();
        setSavedFlash(true);
      }
      // Failure: alert was already shown by parent — stay in edit mode so user can retry or hit Escape.
    };

    const cancelRename = () => {
      skipBlurSave.current = true;
      exitRenameMode();
    };

    const handleBlur = () => {
      if (skipBlurSave.current) {
        skipBlurSave.current = false;
        return;
      }
      submitRename();
    };

    return (
      <div className="file-folders">
        <div className="file-folders__head">
          <button type="button" className="btn btn--sm" onClick={() => { setActiveFolderId(null); exitRenameMode(); }}>
            ← All folders
          </button>
          {renaming ? (
            <div className="file-folders__rename">
              <span className="file-folders__icon" aria-hidden="true">📁</span>
              <input
                ref={renameInputRef}
                className="file-folders__title-input"
                value={renameDraft}
                onChange={(e) => setRenameDraft(e.target.value)}
                onBlur={handleBlur}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); }
                  else if (e.key === 'Escape') { e.preventDefault(); cancelRename(); }
                }} />
            </div>
          ) : canRename ? (
            <button
              type="button"
              className="file-folders__title-button"
              title="Click to rename"
              onClick={enterRenameMode}>
              <span className="file-folders__icon" aria-hidden="true">📁</span>
              <span className="file-folders__title-text">{folder.name}</span>
            </button>
          ) : (
            <h4 className="file-folders__title">
              <span className="file-folders__icon" aria-hidden="true">📁</span>
              {folder.name}
            </h4>
          )}
          {savedFlash && !renaming && (
            <span className="file-folders__saved-flash" role="status" aria-live="polite">Saved ✓</span>
          )}
          {!renaming && (
            <span className="file-folders__count-pill">
              {folderFiles.length} file{folderFiles.length === 1 ? '' : 's'}
            </span>
          )}
          {canMutate && !renaming && onDeleteFolder && (
            <div className="file-folders__head-actions">
              <button type="button" className="file-folders__delete-icon-btn"
                aria-label="Delete folder"
                title="Delete folder"
                onClick={async () => {
                  const ok = await onDeleteFolder(folder);
                  if (ok) setActiveFolderId(null);
                }}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
                  stroke="currentColor" strokeWidth="2"
                  strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <polyline points="3 6 5 6 21 6" />
                  <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
                  <line x1="10" y1="11" x2="10" y2="17" />
                  <line x1="14" y1="11" x2="14" y2="17" />
                  <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
                </svg>
              </button>
            </div>
          )}
        </div>
        {folderFiles.length === 0 ? (
          <p style={{ opacity: 0.7 }}>No files in this folder yet.</p>
        ) : (
          <FileTable files={folderFiles} onDelete={onDelete} />
        )}
      </div>
    );
  }

  // Top level — empty state
  if (sortedFolders.length === 0 && looseFiles.length === 0) {
    return (
      <div>
        <p style={{ textAlign: 'center', opacity: 0.7 }}>No files yet.</p>
        {isAdmin && onCreateFolder && (
          <div style={{ display: 'flex', justifyContent: 'center', marginTop: 16 }}>
            <NewFolderButton onCreate={onCreateFolder} />
          </div>
        )}
      </div>
    );
  }

  // No folders at all → flat file list (just loose files), with a "+ New folder" button for admin
  if (sortedFolders.length === 0) {
    return (
      <React.Fragment>
        {isAdmin && onCreateFolder && (
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
            <NewFolderButton onCreate={onCreateFolder} />
          </div>
        )}
        <FileTable files={looseFiles} onDelete={onDelete} />
      </React.Fragment>
    );
  }

  // Top level — folder cards + (optionally) loose files
  const handleDragStartFile = (e, fileId) => {
    e.dataTransfer.setData('text/plain', fileId);
    e.dataTransfer.effectAllowed = 'move';
  };
  const handleDragOverFolder = (e, folderId) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    setDragOverFolderId(folderId);
  };
  const handleDragLeaveFolder = () => setDragOverFolderId(null);
  const handleDropOnFolder = async (e, folder) => {
    e.preventDefault();
    setDragOverFolderId(null);
    if (!onMoveFile || folder._legacy) return;
    const fileId = e.dataTransfer.getData('text/plain');
    if (!fileId) return;
    const file = items.find(f => f.id === fileId);
    if (!file) return;
    if ((file.folder_id || null) === folder.id) return;
    await onMoveFile(file, folder.id);
  };

  return (
    <div className="file-folders">
      {isAdmin && onCreateFolder && (
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
          <NewFolderButton onCreate={onCreateFolder} />
        </div>
      )}
      <ul className="documents-folders">
        {sortedFolders.map(folder => {
          const count = (filesByFolder.get(folder.id) || []).length;
          const isDropTarget = dragOverFolderId === folder.id;
          const canDrop = isAdmin && !!onMoveFile && !folder._legacy;
          return (
            <li key={folder.id}>
              <button type="button"
                className={`documents-folder-card ${isDropTarget ? 'is-drop-target' : ''}`}
                onClick={() => setActiveFolderId(folder.id)}
                onDragOver={canDrop ? (e) => handleDragOverFolder(e, folder.id) : undefined}
                onDragLeave={canDrop ? handleDragLeaveFolder : undefined}
                onDrop={canDrop ? (e) => handleDropOnFolder(e, folder) : undefined}>
                <span className="documents-folder-card__icon" aria-hidden="true">📁</span>
                <span className="documents-folder-card__name">{folder.name}</span>
                <span className="documents-folder-card__count">{count} file{count === 1 ? '' : 's'}</span>
                <span className="documents-folder-card__chev" aria-hidden="true">›</span>
              </button>
            </li>
          );
        })}
      </ul>
      {looseFiles.length > 0 && (
        <section className="file-folders__loose">
          <h4 className="file-folders__loose-title">
            Loose files
            {isAdmin && onMoveFile && <span className="file-folders__hint"> — drag a row onto a folder to file it</span>}
          </h4>
          <FileTable
            files={looseFiles}
            onDelete={onDelete}
            draggable={isAdmin && !!onMoveFile}
            onDragStartFile={handleDragStartFile} />
        </section>
      )}
    </div>
  );
}

function NewFolderButton({ onCreate }) {
  const [open, setOpen] = React.useState(false);
  const [name, setName] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const submit = async () => {
    const trimmed = name.trim();
    if (!trimmed) return;
    setBusy(true);
    const ok = await onCreate(trimmed);
    setBusy(false);
    if (ok) {
      setName('');
      setOpen(false);
    }
  };

  if (!open) {
    return (
      <button type="button" className="btn btn--sm" onClick={() => setOpen(true)}>+ New folder</button>
    );
  }
  return (
    <div className="file-folders__new-folder">
      <input className="form__input" autoFocus placeholder="Folder name" value={name}
        onChange={(e) => setName(e.target.value)}
        onKeyDown={(e) => { if (e.key === 'Enter') submit(); if (e.key === 'Escape') setOpen(false); }} />
      <button type="button" className="btn btn--solid btn--sm" onClick={submit} disabled={busy || !name.trim()}>
        {busy ? 'Creating…' : 'Create'}
      </button>
      <button type="button" className="btn btn--sm" onClick={() => { setName(''); setOpen(false); }}>Cancel</button>
    </div>
  );
}

function FileTable({ files, onDelete, draggable = false, onDragStartFile }) {
  return (
    <table className="portal-table">
      <thead>
        <tr>
          <th>Name</th>
          <th></th>
          {onDelete && <th></th>}
        </tr>
      </thead>
      <tbody>
        {files.map(f => (
          <tr key={f.id}
            draggable={draggable}
            onDragStart={draggable && onDragStartFile ? (e) => onDragStartFile(e, f.id) : undefined}
            style={draggable ? { cursor: 'grab' } : undefined}>
            <td>{f.name}</td>
            <td>
              <span style={{ display: 'inline-flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
                <DownloadLink bucket="project-files" path={f.storage_path} label="Download" />
                {f._size != null && <span style={{ color: 'var(--muted)', fontSize: '0.85rem' }}>{formatBytes(f._size)}</span>}
              </span>
            </td>
            {onDelete && <td><button className="portal-link-btn" onClick={() => onDelete(f)}>Delete</button></td>}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// ---- Admin dashboard ----
function AdminDashboard({ profile, session }) {
  const showFinancials = canSeeFinancials(session?.user?.email);
  const showClients = canSeeClients(session?.user?.email);
  const showManageProjects = canManageProjects(session?.user?.email);
  const [projects, setProjects] = React.useState([]);
  const [profiles, setProfiles] = React.useState([]);
  const [activeId, setActiveId] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [tab, setTab] = React.useState('projects');
  const [showArchived, setShowArchived] = React.useState(false);
  const [newProjectOpen, setNewProjectOpen] = React.useState(false);

  const [financials, setFinancials] = React.useState({});
  const reload = async () => {
    const [{ data: pr }, { data: pm }, { data: pf }, { data: ls }] = await Promise.all([
      sb.from('projects').select('*').order('created_at', { ascending: false }),
      sb.from('project_members').select('project_id, user_id'),
      sb.from('profiles').select('*').order('created_at', { ascending: false }),
      sb.rpc('get_member_last_sign_ins'),
    ]);
    const profilesById = new Map((pf || []).map(p => [p.id, p]));
    const lastSignInById = new Map((ls || []).map(r => [r.user_id, r.last_sign_in_at]));
    const membersByProject = new Map();
    (pm || []).forEach(m => {
      if (!membersByProject.has(m.project_id)) membersByProject.set(m.project_id, []);
      const profile = profilesById.get(m.user_id);
      if (!profile) return;
      membersByProject.get(m.project_id).push({
        ...profile,
        last_sign_in_at: lastSignInById.get(m.user_id) || null,
      });
    });
    const enriched = (pr || []).map(p => ({
      ...p,
      members: (membersByProject.get(p.id) || []).filter(Boolean),
    }));
    setProjects(enriched); setProfiles(pf || []); setLoading(false);
    if (!enriched.some(p => p.archived_at)) setShowArchived(false);

    if (showFinancials) {
      const { data: fin, error: finErr } = await sb.rpc('get_financials');
      if (finErr) { console.error('get_financials', finErr); return; }
      const finByProject = {};
      (fin || []).forEach(r => {
        finByProject[r.project_id] = {
          allowance: r.allowance_cents || 0,
          spent: r.spent_cents || 0,
          difference: r.difference_cents || 0,
          received: r.received_cents || 0,
        };
      });
      setFinancials(finByProject);
    }
  };
  React.useEffect(() => { reload(); }, []);

  if (loading) return <PortalShell><p>Loading…</p></PortalShell>;

  if (activeId) {
    const proj = projects.find(p => p.id === activeId);
    if (!proj) { setActiveId(null); return null; }
    return <AdminProject project={proj} session={session}
      onBack={() => { setActiveId(null); reload(); }} />;
  }

  return (
    <PortalShell title="Admin Portal" subtitle="Manage client projects, photos, invoices, and files.">
      <div className="admin-tabs">
        <button className={`admin-tabs__tab ${tab === 'projects' ? 'is-active' : ''}`}
          onClick={() => setTab('projects')}>Projects</button>
        {showFinancials && (
          <button className={`admin-tabs__tab ${tab === 'financials' ? 'is-active' : ''}`}
            onClick={() => setTab('financials')}>Financials</button>
        )}
        <button className={`admin-tabs__tab ${tab === 'documents' ? 'is-active' : ''}`}
          onClick={() => setTab('documents')}>Documents</button>
        {showClients && (
          <button className={`admin-tabs__tab ${tab === 'clients' ? 'is-active' : ''}`}
            onClick={() => setTab('clients')}>Clients</button>
        )}
      </div>
      {tab === 'projects' && (
        <React.Fragment>
          <div className="admin-projects-header">
            <h3 className="admin-projects-header__title">{showArchived ? 'Archived projects' : 'Projects'}</h3>
            <div className="admin-projects-header__actions">
              {showArchived ? (
                <button className="admin-financials__view-btn" onClick={() => setShowArchived(false)}>
                  ← Back to active
                </button>
              ) : projects.some(p => p.archived_at) ? (
                <button className="admin-financials__view-btn" onClick={() => setShowArchived(true)}>
                  Show archived ({projects.filter(p => p.archived_at).length})
                </button>
              ) : null}
              {showManageProjects && (
                <button type="button" className="btn btn--solid btn--sm" onClick={() => setNewProjectOpen(v => !v)}>
                  + New Project
                </button>
              )}
            </div>
          </div>
          {showManageProjects && newProjectOpen && (
            <div className="admin-projects-forms">
              <NewProjectForm profiles={profiles} onCreated={reload}
                open={newProjectOpen} onClose={() => setNewProjectOpen(false)} />
            </div>
          )}
          {(() => {
            const visible = projects.filter(p => showArchived ? p.archived_at : !p.archived_at);
            if (visible.length === 0) {
              return <p style={{ opacity: 0.7 }}>{showArchived ? 'No archived projects.' : 'No projects yet.'}</p>;
            }
            return (
              <div className="portal-project-list">
                {visible.map(p => (
                  <button key={p.id} className={`portal-project-card ${p.archived_at ? 'is-archived' : ''}`}
                    onClick={() => setActiveId(p.id)}>
                    <div className="portal-project-card__head">
                      <div className="portal-project-card__heading">
                        <strong>{p.name}</strong>
                        {(p.members || []).length === 0 ? (
                          <span>—</span>
                        ) : (
                          <ul className="portal-project-card__members">
                            {p.members.map(m => (
                              <li key={m.id}>
                                <span className="portal-project-card__member-name">
                                  {m.full_name || '(no name)'}
                                  {m.last_sign_in_at ? (
                                    <span className="login-ok" aria-label="Has logged in"
                                      title={`Last login: ${formatLastLogin(m.last_sign_in_at)}`}> ✓</span>
                                  ) : (
                                    <span className="login-never" aria-label="Never logged in" title="Never logged in"> ×</span>
                                  )}
                                </span>
                              </li>
                            ))}
                          </ul>
                        )}
                        {p.archived_at && <span style={{ opacity: 0.7 }}>Archived</span>}
                      </div>
                      <ProgressDonut percent={statusPct(p.status)} />
                    </div>
                  </button>
                ))}
              </div>
            );
          })()}
        </React.Fragment>
      )}
      {tab === 'documents' && <DocumentsPage session={session} profiles={profiles} />}
      {tab === 'clients' && showClients && (
        <ClientsPage projects={projects} profiles={profiles} onChange={reload}
          onOpenProject={(id) => setActiveId(id)} />
      )}
      {tab === 'financials' && showFinancials && (
        <AdminFinancials projects={projects} financials={financials} />
      )}
    </PortalShell>
  );
}

function DocumentsPage({ session, profiles }) {
  const [folders, setFolders] = React.useState([]);
  const [documents, setDocuments] = React.useState([]);
  const [folderViewers, setFolderViewers] = React.useState({});
  const [activeFolderId, setActiveFolderId] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [settingsModalFolder, setSettingsModalFolder] = React.useState(null); // null = closed; 'new' = create
  const [renamingId, setRenamingId] = React.useState(null);
  const [renameDraft, setRenameDraft] = React.useState('');
  const [error, setError] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const currentUserId = session?.user?.id || null;

  // Other admins available to grant view access to (excludes the current user).
  const adminCandidates = React.useMemo(() => (
    (profiles || []).filter(p => p.role === 'admin' && p.id !== currentUserId)
  ), [profiles, currentUserId]);

  const reload = React.useCallback(async () => {
    setLoading(true);
    const [{ data: fol }, { data: docs }, { data: viewers }] = await Promise.all([
      sb.from('admin_document_folders').select('*').order('name', { ascending: true }),
      sb.from('admin_documents').select('*').order('created_at', { ascending: false }),
      sb.from('admin_document_folder_viewers').select('*'),
    ]);
    setFolders(fol || []);
    setDocuments(docs || []);
    const vmap = {};
    (viewers || []).forEach(r => {
      if (!vmap[r.folder_id]) vmap[r.folder_id] = [];
      vmap[r.folder_id].push(r.user_id);
    });
    setFolderViewers(vmap);
    setLoading(false);
  }, []);
  React.useEffect(() => { reload(); }, [reload]);

  const canManageAccess = (folder) => {
    if (!folder) return false;
    return folder.created_by === currentUserId;
  };

  const docsInFolder = (folderId) => documents.filter(d =>
    folderId === null ? d.folder_id === null : d.folder_id === folderId
  );
  const folderName = (id) => {
    if (id === null) return 'Unfiled';
    return (folders.find(f => f.id === id) || {}).name || 'Folder';
  };
  const fmtBytes = (b) => {
    if (b == null) return '';
    if (b < 1024) return `${b} B`;
    if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
    if (b < 1024 * 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`;
    return `${(b / 1024 / 1024 / 1024).toFixed(2)} GB`;
  };

  const saveFolderSettings = async (folder, name, viewerIds) => {
    setError(''); setBusy(true);
    const trimmedName = (name || '').trim();
    if (!trimmedName) { setBusy(false); return; }
    const { data: { user } } = await sb.auth.getUser();
    let folderId = folder?.id || null;
    if (!folderId) {
      const { data: created, error: insErr } = await sb.from('admin_document_folders').insert({
        name: trimmedName, created_by: user?.id || null,
      }).select('*').single();
      if (insErr) { setBusy(false); setError('Could not create folder: ' + insErr.message); return; }
      folderId = created.id;
    } else if (trimmedName !== folder.name) {
      const { error: updErr } = await sb.from('admin_document_folders').update({ name: trimmedName }).eq('id', folderId);
      if (updErr) { setBusy(false); setError('Could not rename folder: ' + updErr.message); return; }
    }
    // Only finance / creator can manage viewers; if they're not, skip syncing.
    if (canManageAccess(folder || { created_by: user?.id })) {
      await sb.from('admin_document_folder_viewers').delete().eq('folder_id', folderId);
      if (viewerIds && viewerIds.length > 0) {
        const rows = viewerIds.map(uid => ({
          folder_id: folderId,
          user_id: uid,
          granted_by: user?.id || null,
        }));
        const { error: vErr } = await sb.from('admin_document_folder_viewers').insert(rows);
        if (vErr) { setBusy(false); setError('Could not save access list: ' + vErr.message); return; }
      }
    }
    setBusy(false);
    setSettingsModalFolder(null);
    reload();
  };

  const renameFolder = async (id, name) => {
    const trimmed = (name || '').trim();
    if (!trimmed) return;
    setBusy(true);
    const { error: err } = await sb.from('admin_document_folders').update({ name: trimmed }).eq('id', id);
    setBusy(false);
    if (err) { alert('Could not rename: ' + err.message); return; }
    setRenamingId(null);
    reload();
  };

  const deleteFolder = async (folder) => {
    const docCount = docsInFolder(folder.id).length;
    const msg = docCount > 0
      ? `Delete folder "${folder.name}"? Its ${docCount} file${docCount === 1 ? '' : 's'} will move to Unfiled.`
      : `Delete folder "${folder.name}"?`;
    if (!confirm(msg)) return;
    setBusy(true);
    const { error: err } = await sb.from('admin_document_folders').delete().eq('id', folder.id);
    setBusy(false);
    if (err) { alert('Could not delete: ' + err.message); return; }
    if (activeFolderId === folder.id) setActiveFolderId(null);
    reload();
  };

  const uploadFiles = async (fileList) => {
    if (!fileList || !fileList.length) return;
    setError(''); setBusy(true);
    const { data: { user } } = await sb.auth.getUser();
    let failed = 0;
    for (let i = 0; i < fileList.length; i++) {
      const f = fileList[i];
      const safeName = (f.name || 'file').replace(/[^a-zA-Z0-9._-]/g, '_');
      const path = `${activeFolderId || 'unfiled'}/${Date.now()}-${i}-${safeName}`;
      const { error: upErr } = await sb.storage.from('admin-documents').upload(path, f);
      if (upErr) { failed++; continue; }
      const { error: insErr } = await sb.from('admin_documents').insert({
        folder_id: activeFolderId,
        storage_path: path,
        name: f.name,
        mime_type: f.type || null,
        size_bytes: f.size || null,
        created_by: user?.id || null,
      });
      if (insErr) { failed++; }
    }
    setBusy(false);
    if (failed > 0) alert(`${failed} file${failed === 1 ? '' : 's'} failed to upload.`);
    reload();
  };

  const deleteDocument = async (doc) => {
    if (!confirm(`Delete "${doc.name}"? This cannot be undone.`)) return;
    setBusy(true);
    await sb.storage.from('admin-documents').remove([doc.storage_path]);
    const { error: err } = await sb.from('admin_documents').delete().eq('id', doc.id);
    setBusy(false);
    if (err) { alert('Could not delete: ' + err.message); return; }
    reload();
  };

  if (loading) return <p style={{ textAlign: 'center', opacity: 0.7, marginTop: 24 }}>Loading…</p>;

  if (activeFolderId !== null) {
    const folder = folders.find(f => f.id === activeFolderId);
    const folderDocs = docsInFolder(activeFolderId);
    return (
      <React.Fragment>
        <DocumentsFolderView
          folder={folder}
          docs={folderDocs}
          busy={busy}
          renaming={renamingId === activeFolderId}
          renameDraft={renameDraft}
          onStartRename={() => { setRenamingId(activeFolderId); setRenameDraft(folder?.name || ''); }}
          onCancelRename={() => setRenamingId(null)}
          onChangeRename={setRenameDraft}
          onSubmitRename={() => renameFolder(activeFolderId, renameDraft)}
          onDeleteFolder={() => deleteFolder(folder)}
          onManageAccess={canManageAccess(folder) ? () => setSettingsModalFolder(folder) : null}
          onBack={() => { setActiveFolderId(null); setRenamingId(null); }}
          onUpload={uploadFiles}
          onDeleteDoc={deleteDocument}
          fmtBytes={fmtBytes}
        />
        {settingsModalFolder && (
          <FolderSettingsModal
            initial={{
              name: settingsModalFolder.name || '',
              viewerIds: folderViewers[settingsModalFolder.id] || [],
            }}
            mode="edit"
            canManageAccess={canManageAccess(settingsModalFolder)}
            adminCandidates={adminCandidates}
            busy={busy}
            onSave={(name, viewerIds) => saveFolderSettings(settingsModalFolder, name, viewerIds)}
            onClose={() => setSettingsModalFolder(null)} />
        )}
      </React.Fragment>
    );
  }

  const unfiled = docsInFolder(null);

  return (
    <div className="documents-page">
      <div className="documents-page__head">
        <h3>Documents</h3>
        <div className="documents-page__head-actions">
          <button type="button" className="btn btn--solid btn--sm"
            onClick={() => setSettingsModalFolder('new')} disabled={busy}>
            + New folder
          </button>
        </div>
      </div>

      {error && <p style={{ color: '#c0392b', fontSize: '0.9rem' }}>{error}</p>}

      {folders.length === 0 && unfiled.length === 0 ? (
        <p style={{ opacity: 0.7, marginTop: 24 }}>No folders yet. Create one to start uploading.</p>
      ) : (
        <ul className="documents-folders">
          {folders.map(f => {
            const count = docsInFolder(f.id).length;
            return (
              <li key={f.id}>
                <button type="button" className="documents-folder-card"
                  onClick={() => setActiveFolderId(f.id)}>
                  <span className="documents-folder-card__icon" aria-hidden="true">📁</span>
                  <span className="documents-folder-card__name">{f.name}</span>
                  <span className="documents-folder-card__count">{count} file{count === 1 ? '' : 's'}</span>
                  <span className="documents-folder-card__chev" aria-hidden="true">›</span>
                </button>
              </li>
            );
          })}
        </ul>
      )}

      {unfiled.length > 0 && (
        <div className="documents-page__unfiled">
          <h4>Unfiled</h4>
          <DocumentsFileTable
            docs={unfiled}
            onDeleteDoc={deleteDocument}
            fmtBytes={fmtBytes}
            busy={busy}
            getFolderName={folderName}
            showFolder={false}
          />
        </div>
      )}

      {settingsModalFolder === 'new' && (
        <FolderSettingsModal
          initial={null}
          mode="create"
          canManageAccess={true}
          adminCandidates={adminCandidates}
          busy={busy}
          onSave={(name, viewerIds) => saveFolderSettings(null, name, viewerIds)}
          onClose={() => setSettingsModalFolder(null)} />
      )}
    </div>
  );
}

function FolderSettingsModal({ initial, mode, canManageAccess, adminCandidates, busy, onSave, onClose }) {
  const [name, setName] = React.useState(initial?.name || '');
  const [viewerIds, setViewerIds] = React.useState(() => new Set(initial?.viewerIds || []));

  const toggle = (id) => setViewerIds(prev => {
    const next = new Set(prev);
    if (next.has(id)) next.delete(id); else next.add(id);
    return next;
  });

  const submit = (e) => {
    e?.preventDefault();
    const trimmed = name.trim();
    if (!trimmed) return;
    onSave(trimmed, Array.from(viewerIds));
  };

  const title = mode === 'create' ? 'New folder' : 'Folder settings';
  const cta = mode === 'create' ? 'Create folder' : 'Save changes';

  return (
    <div className="portal-modal" onClick={onClose}>
      <div className="portal-modal__card" role="dialog" aria-modal="true"
        aria-labelledby="folder-settings-title" onClick={(e) => e.stopPropagation()}>
        <div className="portal-modal__head">
          <h3 id="folder-settings-title" className="portal-modal__title">{title}</h3>
          <button type="button" className="portal-modal__close" aria-label="Close" onClick={onClose}>×</button>
        </div>
        <form onSubmit={submit}>
          <div className="form__field">
            <label className="form__label">Folder name</label>
            <input className="form__input" type="text" autoFocus
              value={name} onChange={(e) => setName(e.target.value)}
              placeholder="e.g. Permits, Contracts, Marketing" />
          </div>

          {canManageAccess && (
            <div className="folder-access">
              <h4 className="folder-access__title">Who can view this folder?</h4>
              {adminCandidates.length === 0 ? (
                <p className="folder-access__empty">No other admins to add yet.</p>
              ) : (
                <ul className="folder-access__list">
                  {adminCandidates.map(a => (
                    <li key={a.id}>
                      <label className="folder-access__opt">
                        <input type="checkbox" checked={viewerIds.has(a.id)}
                          onChange={() => toggle(a.id)} />
                        <span>{a.full_name || '(unnamed admin)'}</span>
                      </label>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          )}

          <div className="portal-modal__actions">
            <button type="button" className="btn btn--sm" onClick={onClose} disabled={busy}>
              Cancel
            </button>
            <button type="submit" className="btn btn--solid btn--sm" disabled={busy || !name.trim()}>
              {busy ? 'Saving…' : cta}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}


function DocumentsFolderView({ folder, docs, busy, renaming, renameDraft, onStartRename, onCancelRename, onChangeRename, onSubmitRename, onDeleteFolder, onManageAccess, onBack, onUpload, onDeleteDoc, fmtBytes }) {
  const inputRef = React.useRef(null);
  const [dragOver, setDragOver] = React.useState(false);
  const [editing, setEditing] = React.useState(false);
  const onDrop = (e) => { e.preventDefault(); setDragOver(false); onUpload(e.dataTransfer?.files); };

  React.useEffect(() => {
    // Drop out of any rename when leaving edit mode.
    if (!editing && renaming) onCancelRename();
  }, [editing]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div className="documents-page">
      <button className="btn btn--sm" onClick={onBack} style={{ marginBottom: 18 }}>
        ← All folders
      </button>

      <div className="documents-page__head">
        {renaming ? (
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flex: 1 }}>
            <input className="form__input" autoFocus value={renameDraft}
              onChange={(e) => onChangeRename(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter') onSubmitRename(); if (e.key === 'Escape') onCancelRename(); }} />
            <button className="btn btn--solid btn--sm" onClick={onSubmitRename} disabled={busy}>Save</button>
            <button className="btn btn--sm" onClick={onCancelRename} disabled={busy}>Cancel</button>
          </div>
        ) : (
          <h3>📁 {folder?.name || 'Folder'}</h3>
        )}
        <div className="documents-page__head-actions">
          {!renaming && (
            <EditModeToggle active={editing} onClick={() => setEditing(v => !v)} label="Folder" />
          )}
        </div>
      </div>

      {editing && !renaming && (
        <div className="folder-edit-card">
          <div className="folder-edit-card__row">
            <div className="folder-edit-card__actions">
              <button type="button" className="btn btn--sm" onClick={onStartRename} disabled={busy}>Rename</button>
              {onManageAccess && (
                <button type="button" className="btn btn--sm" onClick={onManageAccess} disabled={busy}>Manage access</button>
              )}
            </div>
            <button type="button" className="portal-action-row__delete"
              onClick={onDeleteFolder} disabled={busy}
              aria-label="Delete folder" title="Delete folder">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
                stroke="currentColor" strokeWidth="2"
                strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <polyline points="3 6 5 6 21 6" />
                <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
                <line x1="10" y1="11" x2="10" y2="17" />
                <line x1="14" y1="11" x2="14" y2="17" />
                <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
              </svg>
            </button>
          </div>
          <div className={`documents-drop ${dragOver ? 'is-active' : ''}`}
            onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
            onDragLeave={() => setDragOver(false)}
            onDrop={onDrop}>
            <p>Drag & drop files here, or</p>
            <input ref={inputRef} type="file" multiple
              onChange={(e) => { onUpload(e.target.files); if (inputRef.current) inputRef.current.value = ''; }} />
          </div>
        </div>
      )}

      <DocumentsFileTable docs={docs}
        onDeleteDoc={editing ? onDeleteDoc : undefined}
        fmtBytes={fmtBytes} busy={busy} showFolder={false} />
    </div>
  );
}

function DocumentsFileTable({ docs, onDeleteDoc, fmtBytes, busy, showFolder, getFolderName }) {
  if (docs.length === 0) {
    return <p style={{ opacity: 0.7, marginTop: 12 }}>No files yet.</p>;
  }
  return (
    <table className="documents-table">
      <thead>
        <tr>
          <th>Name</th>
          {showFolder && <th>Folder</th>}
          <th>Size</th>
          <th>Uploaded</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        {docs.map(d => (
          <tr key={d.id}>
            <td>{d.name}</td>
            {showFolder && <td>{getFolderName(d.folder_id)}</td>}
            <td>{fmtBytes(d.size_bytes)}</td>
            <td>{new Date(d.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</td>
            <td className="documents-table__actions">
              <DownloadLink bucket="admin-documents" path={d.storage_path} label="Download" />
              {onDeleteDoc && (
                <button type="button" className="portal-link-btn" onClick={() => onDeleteDoc(d)} disabled={busy}>Delete</button>
              )}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function AdminFinancials({ projects, financials }) {
  const [excluded, setExcluded] = React.useState(() => new Set());
  const [views, setViews] = React.useState([]);
  const [activeViewId, setActiveViewId] = React.useState('');

  const loadViews = React.useCallback(async () => {
    const { data, error } = await sb.from('financial_views')
      .select('*').order('name', { ascending: true });
    if (error) { console.error('financial_views load', error); return; }
    setViews(data || []);
  }, []);
  React.useEffect(() => { loadViews(); }, [loadViews]);

  const applyView = (id) => {
    setActiveViewId(id);
    setExcluded(new Set());
  };

  const toggleExcluded = (id) => {
    setExcluded(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  };

  const buildSnapshot = () => projects
    .filter(p => !excluded.has(p.id))
    .map(p => {
      const f = financials[p.id] || { allowance: 0, spent: 0, difference: 0, received: 0 };
      return {
        project_id: p.id,
        project_name: p.name,
        status: p.status || '',
        allowance_cents: f.allowance,
        spent_cents: f.spent,
        difference_cents: f.difference,
        received_cents: f.received,
      };
    });

  const saveAsNewView = async () => {
    const name = prompt('Name this snapshot (e.g. "Q1 2026"):', '');
    if (!name || !name.trim()) return;
    const { data, error } = await sb.from('financial_views')
      .insert({ name: name.trim(), snapshot: buildSnapshot() })
      .select('*').single();
    if (error) { alert(`Save failed: ${error.message}`); return; }
    setViews(prev => [...prev, data].sort((a, b) => a.name.localeCompare(b.name)));
    setActiveViewId(data.id);
    setExcluded(new Set());
  };

  const deleteActiveView = async () => {
    if (!activeViewId) return;
    const v = views.find(x => x.id === activeViewId);
    if (!v) return;
    if (!confirm(`Delete snapshot "${v.name}"?`)) return;
    const { error } = await sb.from('financial_views').delete().eq('id', activeViewId);
    if (error) { alert(`Delete failed: ${error.message}`); return; }
    setViews(prev => prev.filter(x => x.id !== activeViewId));
    setActiveViewId('');
  };

  const activeView = activeViewId ? views.find(v => v.id === activeViewId) : null;
  const isSnapshot = !!activeView;

  const rows = isSnapshot
    ? (activeView.snapshot || []).map(r => ({
        id: r.project_id, name: r.project_name, status: r.status,
        allowance: r.allowance_cents || 0,
        difference: r.difference_cents || 0,
        received: r.received_cents || 0,
      }))
    : projects.filter(p => !p.archived_at).map(p => {
        const f = financials[p.id] || { allowance: 0, difference: 0, received: 0 };
        return { id: p.id, name: p.name, status: p.status || '',
          allowance: f.allowance, difference: f.difference, received: f.received };
      });

  const grand = rows
    .filter(r => isSnapshot || !excluded.has(r.id))
    .reduce((acc, r) => ({
      allowance: acc.allowance + r.allowance,
      difference: acc.difference + r.difference,
      received: acc.received + r.received,
    }), { allowance: 0, difference: 0, received: 0 });
  return (
    <section className="admin-financials">
      <div className="admin-financials__head">
        <div>
          <h3 style={{ margin: 0 }}>Financials</h3>
          {isSnapshot && (
            <p className="admin-financials__snapshot-hint">
              Snapshot saved {new Date(activeView.created_at).toLocaleDateString()} — read-only
            </p>
          )}
        </div>
        <div className="admin-financials__view-controls">
          <select className="admin-financials__view-select"
            value={activeViewId} onChange={(e) => applyView(e.target.value)}>
            <option value="">Live (current data)</option>
            {views.map(v => (
              <option key={v.id} value={v.id}>{v.name}</option>
            ))}
          </select>
          {!isSnapshot && (
            <button className="admin-financials__view-btn" onClick={saveAsNewView}>Save snapshot…</button>
          )}
          {isSnapshot && (
            <button className="admin-financials__view-btn admin-financials__view-btn--danger"
              onClick={deleteActiveView}>Delete</button>
          )}
          {!isSnapshot && excluded.size > 0 && (
            <button className="admin-financials__restore" onClick={() => setExcluded(new Set())}>
              Restore all ({excluded.size} hidden)
            </button>
          )}
        </div>
      </div>
      <div className="admin-financials__cards">
        <div className="admin-financials__card">
          <span className="admin-financials__card-label">Total Difference</span>
          <span className="admin-financials__card-amount">{fmtMoney(grand.difference)}</span>
          <span className="admin-financials__card-hint">Actual + markup − allowance for rows with actual cost</span>
        </div>
        <div className="admin-financials__card">
          <span className="admin-financials__card-label">Total Received (Checks)</span>
          <span className="admin-financials__card-amount">{fmtMoney(grand.received)}</span>
          <span className="admin-financials__card-hint">All check entries, included projects</span>
        </div>
        <div className="admin-financials__card">
          <span className="admin-financials__card-label">Net Balance</span>
          <span className={`admin-financials__card-amount ${(() => {
            const v = grand.received - grand.difference;
            return v < 0 ? 'is-negative' : v > 0 ? 'is-positive' : '';
          })()}`}>
            {fmtMoney(grand.received - grand.difference)}
          </span>
          <span className="admin-financials__card-hint">Received − Difference</span>
        </div>
      </div>
      {rows.length === 0 ? (
        <p style={{ opacity: 0.7 }}>{isSnapshot ? 'This snapshot has no projects.' : 'No projects yet.'}</p>
      ) : (
        <div className="admin-financials__table-wrap">
        <table className="admin-financials__table">
          <thead>
            <tr>
              <th>Project</th>
              <th>Status</th>
              <th>Project Total</th>
              <th>Difference</th>
              <th>Received</th>
              <th>Balance</th>
              {!isSnapshot && <th aria-label="Toggle"></th>}
            </tr>
          </thead>
          <tbody>
            {rows.map(r => {
              const balance = r.received - r.difference;
              const isExcluded = !isSnapshot && excluded.has(r.id);
              return (
                <tr key={r.id} className={isExcluded ? 'is-excluded' : ''}>
                  <td>{r.name}</td>
                  <td style={{ opacity: 0.7 }}>{r.status}</td>
                  <td>{fmtMoney(r.allowance)}</td>
                  <td>{fmtMoney(r.difference)}</td>
                  <td>{fmtMoney(r.received)}</td>
                  <td className={!isExcluded ? (balance < 0 ? 'is-negative' : balance > 0 ? 'is-positive' : '') : ''}>{fmtMoney(balance)}</td>
                  {!isSnapshot && (
                    <td>
                      <button className="admin-financials__toggle"
                        onClick={() => toggleExcluded(r.id)}
                        aria-label={isExcluded ? 'Include in totals' : 'Exclude from totals'}
                        title={isExcluded ? 'Include in totals' : 'Exclude from totals'}>
                        {isExcluded ? '+' : '×'}
                      </button>
                    </td>
                  )}
                </tr>
              );
            })}
            <tr className="admin-financials__totals">
              <td colSpan="2">{isSnapshot ? 'Snapshot total' : (excluded.size > 0 ? 'Included projects' : 'All projects')}</td>
              <td>{fmtMoney(grand.allowance)}</td>
              <td>{fmtMoney(grand.difference)}</td>
              <td>{fmtMoney(grand.received)}</td>
              <td className={(() => {
                const v = grand.received - grand.difference;
                return v < 0 ? 'is-negative' : v > 0 ? 'is-positive' : '';
              })()}>
                {fmtMoney(grand.received - grand.difference)}
              </td>
              {!isSnapshot && <td></td>}
            </tr>
          </tbody>
        </table>
        </div>
      )}
    </section>
  );
}

function ClientPicker({ clients, selectedIds, onChange }) {
  const [query, setQuery] = React.useState('');
  const [open, setOpen] = React.useState(false);
  const wrapperRef = React.useRef(null);

  React.useEffect(() => {
    const onDocClick = (e) => {
      if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', onDocClick);
    return () => document.removeEventListener('mousedown', onDocClick);
  }, []);

  const selected = clients.filter(c => selectedIds.includes(c.id));
  const q = query.trim().toLowerCase();
  const available = clients
    .filter(c => !selectedIds.includes(c.id))
    .filter(c => !q || (c.full_name || '').toLowerCase().includes(q));

  const add = (id) => { onChange([...selectedIds, id]); setQuery(''); };
  const remove = (id) => onChange(selectedIds.filter(x => x !== id));

  return (
    <div className="client-picker" ref={wrapperRef}>
      {selected.length > 0 && (
        <div className="client-picker__chips">
          {selected.map(c => (
            <span key={c.id} className="client-picker__chip">
              {c.full_name || '(no name)'}
              <button type="button" onClick={() => remove(c.id)} aria-label="Remove">×</button>
            </span>
          ))}
        </div>
      )}
      <div className="client-picker__search">
        <input
          type="text"
          className="form__input"
          placeholder={selected.length ? 'Add another client…' : 'Search clients…'}
          value={query}
          onChange={e => { setQuery(e.target.value); setOpen(true); }}
          onFocus={() => setOpen(true)}
        />
        {open && (available.length > 0 || (q && available.length === 0)) && (
          <ul className="client-picker__dropdown">
            {available.length === 0 ? (
              <li className="client-picker__empty">No matches</li>
            ) : available.slice(0, 8).map(c => (
              <li key={c.id}>
                <button type="button" onClick={() => add(c.id)}>{c.full_name || '(no name)'}</button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

function ClientsPage({ projects, profiles, onChange, onOpenProject }) {
  const [clientId, setClientId] = React.useState(null);
  const [inviting, setInviting] = React.useState(false);
  const [lastSignIns, setLastSignIns] = React.useState({});
  const [emails, setEmails] = React.useState({});
  const [busyId, setBusyId] = React.useState(null);
  const [error, setError] = React.useState('');
  const [sortAsc, setSortAsc] = React.useState(false);
  const [actionMsg, setActionMsg] = React.useState('');
  const [actionBusy, setActionBusy] = React.useState(false);

  React.useEffect(() => {
    sb.rpc('get_member_last_sign_ins').then(({ data }) => {
      const map = {};
      (data || []).forEach(r => { map[r.user_id] = r.last_sign_in_at; });
      setLastSignIns(map);
    });
    sb.rpc('get_user_emails').then(({ data }) => {
      const map = {};
      (data || []).forEach(r => { map[r.user_id] = r.email; });
      setEmails(map);
    });
  }, []);

  const customers = profiles.filter(p => p.role === 'customer');
  const projectsForClient = (cid) => (projects || []).filter(p => (p.members || []).some(m => m.id === cid));

  const initialsFor = (name) => {
    const trimmed = (name || '').trim();
    if (!trimmed) return '?';
    return trimmed.split(/\s+/).map(s => s[0] || '').join('').slice(0, 2).toUpperCase();
  };

  const updateProfileField = async (id, key, newValue) => {
    const value = (newValue || '').trim() || null;
    const { error: err } = await sb.from('profiles').update({ [key]: value }).eq('id', id);
    if (err) { alert('Could not save: ' + err.message); return; }
    onChange();
  };

  const sendPasswordReset = async (email) => {
    if (!email) return;
    setActionMsg(''); setActionBusy(true);
    const { error: err } = await sb.auth.resetPasswordForEmail(email, {
      redirectTo: window.location.origin + '/',
    });
    setActionBusy(false);
    if (err) { setActionMsg('Could not send reset: ' + err.message); return; }
    setActionMsg(`Password reset email sent to ${email}.`);
    setTimeout(() => setActionMsg(''), 4000);
  };

  const sendMagicLink = async (email) => {
    if (!email) return;
    setActionMsg(''); setActionBusy(true);
    const { error: err } = await sb.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: window.location.origin + '/', shouldCreateUser: false },
    });
    setActionBusy(false);
    if (err) { setActionMsg('Could not send link: ' + err.message); return; }
    setActionMsg(`Sign-in link sent to ${email}.`);
    setTimeout(() => setActionMsg(''), 4000);
  };

  const deleteClient = async (c) => {
    if (!confirm(`Permanently delete ${c.full_name || 'this client'}? This cannot be undone.`)) return;
    setError(''); setBusyId(c.id);
    const { data, error: err } = await sb.functions.invoke('delete-customer', {
      body: { user_id: c.id },
    });
    setBusyId(null);
    if (err || data?.error) {
      setError(err?.message || data?.error || 'Delete failed');
      return;
    }
    setClientId(null);
    onChange();
  };

  if (clientId) {
    const c = customers.find(cu => cu.id === clientId);
    if (!c) {
      return (
        <div className="clients-page">
          <button className="btn btn--sm" onClick={() => setClientId(null)}>← All clients</button>
          <p style={{ marginTop: 16, opacity: 0.7 }}>Client not found.</p>
        </div>
      );
    }
    const cps = projectsForClient(c.id);
    const active = cps.filter(p => !p.archived_at);
    const archived = cps.filter(p => p.archived_at);
    const lastSign = lastSignIns[c.id];
    const email = emails[c.id];
    const canDelete = active.length === 0 && busyId !== c.id;

    return (
      <div className="clients-page">
        <button className="btn btn--sm" onClick={() => setClientId(null)} style={{ marginBottom: 18 }}>
          ← All clients
        </button>

        <div className="client-detail">
          <div className="client-detail__head">
            <div className="client-detail__avatar">{initialsFor(c.full_name)}</div>
            <div className="client-detail__name-block">
              <h2>{c.full_name || '(no name)'}</h2>
              <p className="client-detail__meta">
                {lastSign ? `Last signed in ${formatLastLogin(lastSign)}` : 'Never signed in'} ·
                {' '}{active.length} active · {archived.length} archived
              </p>
            </div>
          </div>

          <div className="client-detail__section">
            <h4>Contact</h4>
            <EditableField label="Name" value={c.full_name}
              onSave={(v) => updateProfileField(c.id, 'full_name', v)} />
            <div className="client-detail__field">
              <span className="client-detail__field-label">Email</span>
              <span className="client-detail__field-value">
                {email ? <a href={`mailto:${email}`}>{email}</a>
                       : <em className="client-detail__field-empty">—</em>}
              </span>
            </div>
            <EditableField label="Phone" type="tel" value={c.phone}
              placeholder="(555) 123-4567"
              onSave={(v) => updateProfileField(c.id, 'phone', v)} />
            <EditableField label="Address" value={c.address}
              placeholder="Street, city, state ZIP"
              onSave={(v) => updateProfileField(c.id, 'address', v)} />
          </div>

          <div className="client-detail__section">
            <h4>Notes</h4>
            <EditableField label="Notes" type="textarea" value={c.notes}
              placeholder="Internal notes about this client (only admins can see this)…"
              emptyText="No notes yet."
              onSave={(v) => updateProfileField(c.id, 'notes', v)} />
          </div>

          <div className="client-detail__section">
            <h4>Account actions</h4>
            <div className="client-detail__actions">
              <button type="button" className="btn btn--sm" disabled={!email || actionBusy}
                onClick={() => sendPasswordReset(email)}>
                Send password reset
              </button>
              <button type="button" className="btn btn--sm" disabled={!email || actionBusy}
                onClick={() => sendMagicLink(email)}>
                Re-send sign-in link
              </button>
            </div>
            {!email && (
              <p className="client-detail__hint">Email lookup unavailable — run the latest migration.</p>
            )}
            {actionMsg && (
              <p className="client-detail__action-msg">{actionMsg}</p>
            )}
          </div>

          {c.signature_image && (
            <div className="client-detail__section">
              <h4>Saved signature</h4>
              <img src={c.signature_image} alt="Signature" className="client-detail__sig" />
            </div>
          )}

          <div className="client-detail__section">
            <h4>Active projects ({active.length})</h4>
            {active.length === 0 ? (
              <p className="client-detail__empty">No active projects.</p>
            ) : (
              <ul className="client-detail__projects">
                {active.map(p => (
                  <li key={p.id}>
                    <button type="button" className="client-detail__project-link"
                      onClick={() => onOpenProject(p.id)}>
                      <strong>{p.name}</strong>
                      <span>{p.address || ''}</span>
                      <span className="client-detail__project-status">{p.status || ''}</span>
                    </button>
                  </li>
                ))}
              </ul>
            )}
          </div>

          {archived.length > 0 && (
            <div className="client-detail__section">
              <h4>Archived projects ({archived.length})</h4>
              <ul className="client-detail__projects">
                {archived.map(p => (
                  <li key={p.id}>
                    <button type="button" className="client-detail__project-link"
                      onClick={() => onOpenProject(p.id)}>
                      <strong>{p.name}</strong>
                      <span>{p.address || ''}</span>
                    </button>
                  </li>
                ))}
              </ul>
            </div>
          )}

          <div className="client-detail__section client-detail__danger">
            <h4>Danger zone</h4>
            {error && <p style={{ color: '#c0392b', fontSize: '0.9rem', marginBottom: 8 }}>{error}</p>}
            <button className="btn btn--sm btn--signout" disabled={!canDelete}
              title={canDelete ? 'Delete client' : 'Remove from active projects first'}
              onClick={() => deleteClient(c)}>
              {busyId === c.id ? 'Deleting…' : 'Delete client'}
            </button>
            {active.length > 0 && (
              <p className="client-detail__hint">
                Remove this client from all active projects before deleting their account.
              </p>
            )}
          </div>
        </div>
      </div>
    );
  }

  const sorted = [...customers].sort((a, b) => {
    const aT = lastSignIns[a.id] || '';
    const bT = lastSignIns[b.id] || '';
    return sortAsc ? aT.localeCompare(bT) : bT.localeCompare(aT);
  });

  return (
    <div className="clients-page">
      <div className="clients-page__head">
        <h3 className="admin-projects-header__title">Clients ({customers.length})</h3>
        <button type="button" className="btn btn--solid btn--sm" onClick={() => setInviting(v => !v)}>
          {inviting ? 'Cancel' : '+ Invite client'}
        </button>
      </div>

      {inviting && (
        <div className="clients-page__invite">
          <InviteClientForm
            onCreated={() => { setInviting(false); onChange(); }}
            onCancel={() => setInviting(false)} />
        </div>
      )}

      {customers.length === 0 ? (
        <p style={{ opacity: 0.7, marginTop: 24 }}>No clients yet. Invite one to get started.</p>
      ) : (
        <React.Fragment>
          <div className="clients-list__sort">
            <button type="button" className="portal-link-btn" onClick={() => setSortAsc(v => !v)}>
              Last sign-in: {sortAsc ? 'oldest first' : 'newest first'}
            </button>
          </div>
          <ul className="clients-list">
            {sorted.map(c => {
              const cps = projectsForClient(c.id);
              const active = cps.filter(p => !p.archived_at).length;
              const archived = cps.filter(p => p.archived_at).length;
              const lastSign = lastSignIns[c.id];
              return (
                <li key={c.id}>
                  <button type="button" className="clients-list__row"
                    onClick={() => setClientId(c.id)}>
                    <span className="clients-list__avatar">{initialsFor(c.full_name)}</span>
                    <span className="clients-list__name-block">
                      <span className="clients-list__name">{c.full_name || '(no name)'}</span>
                      {emails[c.id] && (
                        <span className="clients-list__email">{emails[c.id]}</span>
                      )}
                    </span>
                    <span className="clients-list__counts">
                      {active} active{archived ? ` · ${archived} archived` : ''}
                    </span>
                    <span className="clients-list__last">
                      {lastSign ? formatLastLogin(lastSign) : 'never signed in'}
                    </span>
                    <span className="clients-list__chev" aria-hidden="true">›</span>
                  </button>
                </li>
              );
            })}
          </ul>
        </React.Fragment>
      )}
    </div>
  );
}

function EditableField({ label, value, onSave, type = 'text', placeholder, emptyText = '—' }) {
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(value || '');
  const [saving, setSaving] = React.useState(false);
  React.useEffect(() => { setDraft(value || ''); }, [value]);

  const submit = async () => {
    setSaving(true);
    try { await onSave(draft); setEditing(false); }
    finally { setSaving(false); }
  };
  const cancel = () => { setDraft(value || ''); setEditing(false); };

  if (!editing) {
    return (
      <div className="client-detail__field">
        <span className="client-detail__field-label">{label}</span>
        <span className="client-detail__field-value">
          {value ? (type === 'textarea'
            ? <span style={{ whiteSpace: 'pre-wrap' }}>{value}</span>
            : value
          ) : <em className="client-detail__field-empty">{emptyText}</em>}
        </span>
        <button type="button" className="portal-link-btn" onClick={() => setEditing(true)}>
          Edit
        </button>
      </div>
    );
  }

  return (
    <div className="client-detail__field client-detail__field--editing">
      <span className="client-detail__field-label">{label}</span>
      {type === 'textarea' ? (
        <textarea className="form__input" value={draft} rows={4}
          placeholder={placeholder || ''}
          onChange={(e) => setDraft(e.target.value)} autoFocus />
      ) : (
        <input className="form__input" type={type === 'tel' ? 'tel' : 'text'} value={draft}
          placeholder={placeholder || ''}
          onChange={(e) => setDraft(e.target.value)} autoFocus />
      )}
      <div className="client-detail__field-actions">
        <button type="button" className="btn btn--solid btn--sm" onClick={submit} disabled={saving}>
          {saving ? 'Saving…' : 'Save'}
        </button>
        <button type="button" className="btn btn--sm" onClick={cancel} disabled={saving}>
          Cancel
        </button>
      </div>
    </div>
  );
}

function ClientsManager({ projects, profiles, onChange, open, onClose }) {
  const [inviting, setInviting] = React.useState(false);
  const [lastSignIns, setLastSignIns] = React.useState({});
  const [sortAsc, setSortAsc] = React.useState(false);
  const [busyId, setBusyId] = React.useState(null);
  const [error, setError] = React.useState('');

  React.useEffect(() => {
    if (!open) return;
    sb.rpc('get_member_last_sign_ins').then(({ data }) => {
      const map = {};
      (data || []).forEach(r => { map[r.user_id] = r.last_sign_in_at; });
      setLastSignIns(map);
    });
  }, [open]);

  const customers = profiles.filter(p => p.role === 'customer');

  const activeCount = {};
  const archivedCount = {};
  customers.forEach(c => { activeCount[c.id] = 0; archivedCount[c.id] = 0; });
  (projects || []).forEach(p => {
    (p.members || []).forEach(m => {
      if (p.archived_at) archivedCount[m.id] = (archivedCount[m.id] || 0) + 1;
      else activeCount[m.id] = (activeCount[m.id] || 0) + 1;
    });
  });

  const sorted = [...customers].sort((a, b) => {
    const aT = lastSignIns[a.id] || '';
    const bT = lastSignIns[b.id] || '';
    if (sortAsc) return aT.localeCompare(bT);
    return bT.localeCompare(aT);
  });

  const deleteClient = async (c) => {
    if (!confirm(`Permanently delete ${c.full_name || 'this client'}? This cannot be undone.`)) return;
    setError(''); setBusyId(c.id);
    const { data, error } = await sb.functions.invoke('delete-customer', {
      body: { user_id: c.id },
    });
    setBusyId(null);
    if (error || data?.error) {
      setError(error?.message || data?.error || 'Delete failed');
      return;
    }
    onChange();
  };

  if (!open) return null;
  const closeAll = () => { onClose && onClose(); setInviting(false); setError(''); };

  return (
    <div className="form" style={{ maxWidth: 560, width: '100%' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
        <h3 style={{ marginTop: 0 }}>Clients</h3>
        <button type="button" className="portal-link-btn" onClick={closeAll}>
          Close
        </button>
      </div>

      {!inviting ? (
        <button type="button" className="portal-link-btn" onClick={() => setInviting(true)} style={{ marginBottom: 16 }}>
          + Invite new client
        </button>
      ) : (
        <InviteClientForm
          onCreated={() => { setInviting(false); onChange(); }}
          onCancel={() => setInviting(false)}
        />
      )}

      {customers.length > 0 && (
        <React.Fragment>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginTop: 16, marginBottom: 4 }}>
            <span style={{ fontSize: '0.85rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--navy)' }}>
              Existing clients
            </span>
            <button type="button" className="portal-link-btn" onClick={() => setSortAsc(v => !v)} style={{ fontSize: '0.85rem' }}>
              Last sign-in: {sortAsc ? 'oldest first' : 'newest first'}
            </button>
          </div>
          {error && <p style={{ color: '#c0392b', fontSize: '0.9rem' }}>{error}</p>}
          {sorted.map(c => {
            const active = activeCount[c.id] || 0;
            const archived = archivedCount[c.id] || 0;
            const lastSign = lastSignIns[c.id];
            const canDelete = active === 0 && busyId !== c.id;
            return (
              <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderTop: '1px solid var(--line)' }}>
                <div style={{ flex: 1 }}>
                  <div>{c.full_name || '(no name)'}</div>
                  <div style={{ fontSize: '0.8rem', color: 'var(--muted)' }}>
                    {active} active · {archived} archived · {lastSign ? `last sign-in ${formatLastLogin(lastSign)}` : 'never signed in'}
                  </div>
                </div>
                <button
                  className="portal-client-remove"
                  aria-label="Delete client"
                  disabled={!canDelete}
                  title={active === 0 ? 'Delete client' : 'Remove from active projects first'}
                  onClick={() => deleteClient(c)}
                  style={{ opacity: canDelete ? 1 : 0.3, cursor: canDelete ? 'pointer' : 'not-allowed' }}>
                  ×
                </button>
              </div>
            );
          })}
        </React.Fragment>
      )}
    </div>
  );
}

function InviteClientForm({ onCreated, onCancel }) {
  const [email, setEmail] = React.useState('');
  const [fullName, setFullName] = React.useState('');
  const [projectName, setProjectName] = React.useState('');
  const [address, setAddress] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const [success, setSuccess] = React.useState('');

  const submit = async (e) => {
    e.preventDefault();
    setError(''); setSuccess(''); setBusy(true);
    const { data, error } = await sb.functions.invoke('invite-customer', {
      body: { email, full_name: fullName, project_name: projectName, address }
    });
    if (error) {
      setError(error.message || 'Invite failed');
    } else if (data?.error) {
      setError(data.error);
    } else {
      setSuccess(`Invite email sent to ${email}.`);
      setEmail(''); setFullName(''); setProjectName(''); setAddress('');
      onCreated();
    }
    setBusy(false);
  };

  return (
    <form onSubmit={submit}>
      <h4 style={{ marginTop: 0 }}>Invite new client</h4>
      <p style={{ fontSize: '0.85rem', opacity: 0.7, marginTop: -4 }}>
        Sends an email invite. They'll set their own password on first login.
      </p>
      <div className="form__row">
        <div className="form__field"><label className="form__label">Email</label>
          <input className="form__input" type="email" value={email}
            onChange={e => setEmail(e.target.value)} required /></div>
        <div className="form__field"><label className="form__label">Full Name</label>
          <input className="form__input" value={fullName}
            onChange={e => setFullName(e.target.value)} required /></div>
      </div>
      <div className="form__field"><label className="form__label">Project Name</label>
        <input className="form__input" value={projectName}
          onChange={e => setProjectName(e.target.value)} required /></div>
      <div className="form__field"><label className="form__label">Address</label>
        <input className="form__input" value={address}
          onChange={e => setAddress(e.target.value)} /></div>
      {error && <p style={{ color: '#c0392b' }}>{error}</p>}
      {success && <p style={{ color: '#27ae60' }}>{success}</p>}
      <div style={{ display: 'flex', gap: 12, marginTop: 12 }}>
        <button type="submit" className="btn btn--solid" disabled={busy}>{busy ? 'Sending…' : 'Send Invite'}</button>
        <button type="button" className="btn" onClick={() => { setError(''); setSuccess(''); onCancel(); }}>Cancel</button>
      </div>
    </form>
  );
}

function NewProjectForm({ profiles, onCreated, open, onClose }) {
  const [showCustomers, setShowCustomers] = React.useState(false);
  const [memberIds, setMemberIds] = React.useState([]);
  const [name, setName] = React.useState('');
  const [address, setAddress] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');

  const customers = profiles.filter(p => p.role === 'customer');

  const submit = async (e) => {
    e.preventDefault(); setError(''); setBusy(true);
    const { data: proj, error: projErr } = await sb.from('projects')
      .insert({ name, address }).select('id').single();
    if (projErr || !proj) { setError(projErr?.message || 'Insert failed'); setBusy(false); return; }
    if (memberIds.length) {
      const rows = memberIds.map(uid => ({ project_id: proj.id, user_id: uid }));
      const { error: memErr } = await sb.from('project_members').insert(rows);
      if (memErr) { setError(memErr.message); setBusy(false); return; }
    }
    setName(''); setAddress(''); setMemberIds([]); setShowCustomers(false);
    onClose && onClose();
    onCreated();
    setBusy(false);
  };

  if (!open) return null;
  return (
    <form className="form" onSubmit={submit} style={{ maxWidth: 560 }}>
      <h3 style={{ marginTop: 0 }}>New Project</h3>
      <div className="form__field">
        <label className="form__label">Project Name</label>
        <input className="form__input" value={name} onChange={e => setName(e.target.value)} required />
      </div>
      <div className="form__field">
        <label className="form__label">Address</label>
        <input className="form__input" value={address} onChange={e => setAddress(e.target.value)} />
      </div>
      <div style={{ marginTop: 8 }}>
        {!showCustomers ? (
          <button type="button" className="portal-link-btn" onClick={() => setShowCustomers(true)}>
            Add clients
          </button>
        ) : (
          <div style={{ marginTop: 4 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
              <span style={{ fontSize: '0.85rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--navy)' }}>
                Clients
              </span>
              <button type="button" className="portal-link-btn" onClick={() => { setShowCustomers(false); setMemberIds([]); }}>
                Hide
              </button>
            </div>
            {customers.length === 0 ? (
              <p style={{ fontSize: '0.85rem', opacity: 0.7, margin: 0 }}>No clients yet — invite one first.</p>
            ) : (
              <ClientPicker clients={customers} selectedIds={memberIds} onChange={setMemberIds} />
            )}
            <p style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: 8, marginBottom: 0 }}>
              You can add or remove clients later from the project page.
            </p>
          </div>
        )}
      </div>
      {error && <p style={{ color: '#c0392b' }}>{error}</p>}
      <div style={{ display: 'flex', gap: 12, marginTop: 16 }}>
        <button type="submit" className="btn btn--solid" disabled={busy}>{busy ? 'Creating…' : 'Create'}</button>
        <button type="button" className="btn" onClick={() => onClose && onClose()}>Cancel</button>
      </div>
    </form>
  );
}

function AdminProject({ project, session, onBack }) {
  const showAccounting = canSeeAccounting(session?.user?.email);
  const showSelections = canSeeSelections(session?.user?.email);
  const showInvoices = canSeeInvoices(session?.user?.email);
  const showPhotos = canSeePhotos(session?.user?.email);
  const showProjectClients = canSeeClients(session?.user?.email);
  const canManageProject = canManageProjects(session?.user?.email);
  const [photos, setPhotos] = React.useState([]);
  const [invoices, setInvoices] = React.useState([]);
  const [files, setFiles] = React.useState([]);
  const [folders, setFolders] = React.useState([]);
  const [members, setMembers] = React.useState([]);
  const [allCustomers, setAllCustomers] = React.useState([]);
  const [tab, setTab] = React.useState('photos');
  const [photoKind, setPhotoKind] = React.useState(showPhotos ? 'client' : 'jobsite');
  const [clientsModalOpen, setClientsModalOpen] = React.useState(false);

  // Close the Manage Clients modal on Escape.
  React.useEffect(() => {
    if (!clientsModalOpen) return;
    const onKey = (e) => { if (e.key === 'Escape') setClientsModalOpen(false); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [clientsModalOpen]);
  const [editing, setEditing] = React.useState(false);
  const [editingFiles, setEditingFiles] = React.useState(false);
  const [editingInvoices, setEditingInvoices] = React.useState(false);
  const [name, setName] = React.useState(project.name);
  const [address, setAddress] = React.useState(project.address || '');
  const [status, setStatus] = React.useState(project.status || '');
  const [addCustomerId, setAddCustomerId] = React.useState('');
  const [inviteOpen, setInviteOpen] = React.useState(false);
  const [inviteEmail, setInviteEmail] = React.useState('');
  const [inviteName, setInviteName] = React.useState('');
  const [inviteBusy, setInviteBusy] = React.useState(false);
  const [inviteMsg, setInviteMsg] = React.useState('');

  const reload = async () => {
    const [{ data: ph }, { data: inv }, { data: fl }, { data: mem }, { data: cust }, { data: ls }, foldersResp] = await Promise.all([
      sb.from('photos').select('*').eq('project_id', project.id).order('created_at', { ascending: false }),
      sb.from('invoices').select('*').eq('project_id', project.id).order('created_at', { ascending: false }),
      sb.from('files').select('*').eq('project_id', project.id).order('created_at', { ascending: false }),
      sb.from('project_members').select('user_id').eq('project_id', project.id),
      sb.from('profiles').select('id, full_name, role').eq('role', 'customer'),
      sb.rpc('get_member_last_sign_ins'),
      sb.from('project_file_folders').select('*').eq('project_id', project.id).order('name', { ascending: true }),
    ]);
    const custMap = new Map((cust || []).map(c => [c.id, c]));
    const lastSignInById = new Map((ls || []).map(r => [r.user_id, r.last_sign_in_at]));
    const enrichedMembers = (mem || []).map(m => ({
      user_id: m.user_id,
      profile: custMap.get(m.user_id),
      last_sign_in_at: lastSignInById.get(m.user_id) || null,
    }));
    const [invoicesWithSize, filesWithSize] = await Promise.all([
      attachSizes('project-invoices', inv || [], project.id),
      attachSizes('project-files', fl || [], project.id),
    ]);
    setPhotos(ph || []); setInvoices(invoicesWithSize); setFiles(filesWithSize);
    setFolders(foldersResp.data || []);
    setMembers(enrichedMembers); setAllCustomers(cust || []);
  };

  const createFolder = async (name) => {
    const trimmed = (name || '').trim();
    if (!trimmed) return false;
    const { error } = await sb.from('project_file_folders')
      .insert({ project_id: project.id, name: trimmed });
    if (error) {
      alert(error.message.includes('duplicate') || error.code === '23505'
        ? `A folder named "${trimmed}" already exists.`
        : `Could not create folder: ${error.message}`);
      return false;
    }
    await reload();
    return true;
  };

  const renameFolder = async (folder, newName) => {
    const trimmed = (newName || '').trim();
    if (!trimmed || trimmed === folder.name) return false;
    const { error } = await sb.from('project_file_folders')
      .update({ name: trimmed }).eq('id', folder.id);
    if (error) {
      alert(error.message.includes('duplicate') || error.code === '23505'
        ? `A folder named "${trimmed}" already exists.`
        : `Could not rename folder: ${error.message}`);
      return false;
    }
    await reload();
    return true;
  };

  const deleteFolderAction = async (folder) => {
    const filesIn = files.filter(f => f.folder_id === folder.id).length;
    const msg = filesIn > 0
      ? `Delete the "${folder.name}" folder? The ${filesIn} file${filesIn === 1 ? '' : 's'} inside will become loose (not deleted).`
      : `Delete the "${folder.name}" folder?`;
    if (!confirm(msg)) return false;
    const { error } = await sb.from('project_file_folders').delete().eq('id', folder.id);
    if (error) { alert(`Could not delete folder: ${error.message}`); return false; }
    await reload();
    return true;
  };

  const moveFile = async (file, newFolderId) => {
    if ((file.folder_id || null) === (newFolderId || null)) return false;
    const { error } = await sb.from('files')
      .update({ folder_id: newFolderId || null }).eq('id', file.id);
    if (error) { alert(`Could not move file: ${error.message}`); return false; }
    await reload();
    return true;
  };
  React.useEffect(() => { reload(); }, [project.id]);

  const memberIds = new Set(members.map(m => m.user_id));
  const addableCustomers = allCustomers.filter(c => !memberIds.has(c.id));

  const addMember = async () => {
    if (!addCustomerId) return;
    await sb.from('project_members').insert({ project_id: project.id, user_id: addCustomerId });
    setAddCustomerId('');
    reload();
  };
  const removeMember = async (userId) => {
    if (!confirm('Remove this client from the project?')) return;
    await sb.from('project_members').delete()
      .eq('project_id', project.id).eq('user_id', userId);
    reload();
  };

  const inviteToProject = async (e) => {
    e.preventDefault();
    setInviteMsg(''); setInviteBusy(true);
    const { data, error } = await sb.functions.invoke('invite-customer', {
      body: { email: inviteEmail, full_name: inviteName, project_id: project.id },
    });
    setInviteBusy(false);
    if (error || data?.error) {
      setInviteMsg(error?.message || data?.error || 'Invite failed');
      return;
    }
    setInviteEmail(''); setInviteName(''); setInviteOpen(false);
    setInviteMsg(`Invite sent to ${inviteEmail}.`);
    reload();
  };

  const saveProject = async () => {
    await sb.from('projects').update({ name, address, status }).eq('id', project.id);
    setEditing(false);
  };

  const deleteProject = async () => {
    if (!confirm('Delete this project and ALL its photos/invoices/files? This cannot be undone.')) return;
    await sb.from('projects').delete().eq('id', project.id);
    onBack();
  };

  const toggleArchived = async () => {
    const archived = !!project.archived_at;
    if (!archived && !confirm('Archive this project? It will be hidden from the dashboard until restored.')) return;
    const archived_at = archived ? null : new Date().toISOString();
    const { error } = await sb.from('projects').update({ archived_at }).eq('id', project.id);
    if (error) { alert(`Update failed: ${error.message}`); return; }
    project.archived_at = archived_at;
    onBack();
  };

  const deleteInvoice = async (i) => {
    if (!confirm('Delete this invoice?')) return;
    if (i.storage_path) await sb.storage.from('project-invoices').remove([i.storage_path]);
    await sb.from('invoices').delete().eq('id', i.id);
    reload();
  };
  const deleteFile = async (f) => {
    if (!confirm('Delete this file?')) return;
    await sb.storage.from('project-files').remove([f.storage_path]);
    await sb.from('files').delete().eq('id', f.id);
    reload();
  };
  const toggleInvoicePaid = async (i) => {
    await sb.from('invoices').update({ paid: !i.paid }).eq('id', i.id);
    reload();
  };
  const quickSetStatus = async (newStatus) => {
    await sb.from('projects').update({ status: newStatus }).eq('id', project.id);
    setStatus(newStatus);
    project.status = newStatus;
  };

  return (
    <PortalShell title={project.name} subtitle={project.address}>
      <div className="portal-action-row">
        <button className="btn btn--compact" onClick={onBack}>← Back</button>
        {canManageProject && (
          <button className="btn btn--compact" onClick={() => setEditing(v => !v)}>{editing ? 'Cancel' : 'Edit'}</button>
        )}
      </div>
      {canManageProject && editing && (
        <div className="form" style={{ maxWidth: 560, marginBottom: 24 }}>
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
            <button type="button" className="portal-action-row__delete"
              onClick={deleteProject}
              aria-label="Delete project"
              title="Delete project">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
                stroke="currentColor" strokeWidth="2"
                strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <polyline points="3 6 5 6 21 6" />
                <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
                <line x1="10" y1="11" x2="10" y2="17" />
                <line x1="14" y1="11" x2="14" y2="17" />
                <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
              </svg>
            </button>
          </div>
          <div className="form__field"><label className="form__label">Name</label>
            <input className="form__input" value={name} onChange={e => setName(e.target.value)} /></div>
          <div className="form__field"><label className="form__label">Address</label>
            <input className="form__input" value={address} onChange={e => setAddress(e.target.value)} /></div>
          <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', marginTop: 18 }}>
            <button type="button" className="btn btn--compact" onClick={toggleArchived}>
              {project.archived_at ? 'Unarchive project' : 'Archive project'}
            </button>
            {showProjectClients && (
              <button type="button" className="btn btn--compact" onClick={() => setClientsModalOpen(true)}>Manage clients</button>
            )}
            <button className="btn btn--solid" style={{ marginLeft: 'auto' }} onClick={saveProject}>Save</button>
          </div>
        </div>
      )}
      <p style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
        <strong>Status:</strong>
        {canManageProject ? (
          <select value={status} onChange={e => quickSetStatus(e.target.value)}
            style={{ padding: '6px 10px', fontFamily: 'inherit', borderRadius: 4, border: '1px solid var(--line)' }}>
            {STATUS_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
        ) : (
          <span>{status}</span>
        )}
      </p>
      <div className="portal-tabs">
        <button className={tab === 'photos' ? 'is-active' : ''} onClick={() => setTab('photos')}>Photos</button>
        {showSelections && (
          <button className={tab === 'selections' ? 'is-active' : ''} onClick={() => setTab('selections')}>Selections</button>
        )}
        {showAccounting && (
          <button className={tab === 'accounting' ? 'is-active' : ''} onClick={() => setTab('accounting')}>Accounting</button>
        )}
        <button className={tab === 'files' ? 'is-active' : ''} onClick={() => setTab('files')}>Files</button>
        {showInvoices && (
          <button className={tab === 'invoices' ? 'is-active' : ''} onClick={() => setTab('invoices')}>Invoices</button>
        )}
      </div>
      {tab === 'photos' && (
        <React.Fragment>
          {showPhotos && (
            <div className="photo-kind-toggle" role="tablist" aria-label="Photo type">
              <button type="button" role="tab" aria-selected={photoKind === 'client'}
                className={`photo-kind-toggle__btn ${photoKind === 'client' ? 'is-active' : ''}`}
                onClick={() => setPhotoKind('client')}>Client</button>
              <button type="button" role="tab" aria-selected={photoKind === 'jobsite'}
                className={`photo-kind-toggle__btn ${photoKind === 'jobsite' ? 'is-active' : ''}`}
                onClick={() => setPhotoKind('jobsite')}>Jobsite</button>
              {isPunchlistPhase(project.status) && (
                <button type="button" role="tab" aria-selected={photoKind === 'punchlist'}
                  className={`photo-kind-toggle__btn ${photoKind === 'punchlist' ? 'is-active' : ''}`}
                  onClick={() => setPhotoKind('punchlist')}>Punch List</button>
              )}
            </div>
          )}
          {showPhotos && photoKind === 'punchlist' && isPunchlistPhase(project.status) ? (
            <PunchListPhotos project={project} isAdmin={true} canUpload={canManageProject} />
          ) : (
            <PhotoAlbums project={project} isAdmin={true}
              kind={showPhotos ? (photoKind === 'punchlist' ? 'client' : photoKind) : 'jobsite'}
              canCopyToClient={showPhotos} />
          )}
        </React.Fragment>
      )}
      {tab === 'selections' && showSelections && <SelectionsSheet project={project} isAdmin={canManageProject} canSign={canManageProject} />}
      {tab === 'accounting' && showAccounting && <AccountingSheet project={project} isAdmin={true} />}
      {tab === 'files' && <React.Fragment>
        {canManageProject && (
          <div className="admin-tab-toolbar">
            <EditModeToggle active={editingFiles} onClick={() => setEditingFiles(v => !v)} label="Files" />
          </div>
        )}
        {canManageProject && editingFiles && (
          <div className="folder-edit-card">
            <UploadFile project={project} folders={folders} onDone={reload} />
          </div>
        )}
        <FileList items={files} folders={folders}
          onDelete={canManageProject && editingFiles ? deleteFile : undefined}
          onCreateFolder={canManageProject && editingFiles ? createFolder : undefined}
          onRenameFolder={canManageProject && editingFiles ? renameFolder : undefined}
          onDeleteFolder={canManageProject && editingFiles ? deleteFolderAction : undefined}
          onMoveFile={canManageProject && editingFiles ? moveFile : undefined} />
        {isPunchlistPhase(project.status) && (
          <PunchListNote project={project} canEdit={canManageProject} />
        )}
      </React.Fragment>}
      {tab === 'invoices' && showInvoices && <React.Fragment>
        <div className="admin-tab-toolbar">
          <EditModeToggle active={editingInvoices} onClick={() => setEditingInvoices(v => !v)} label="Invoices" />
        </div>
        {editingInvoices && (
          <div className="folder-edit-card">
            <UploadInvoice project={project} onDone={reload} />
          </div>
        )}
        <InvoiceList items={invoices}
          onDelete={editingInvoices ? deleteInvoice : undefined}
          onTogglePaid={editingInvoices ? toggleInvoicePaid : undefined} />
      </React.Fragment>}
      {showProjectClients && clientsModalOpen && (
        <div className="portal-modal" onClick={() => setClientsModalOpen(false)}>
          <div className="portal-modal__card" role="dialog" aria-modal="true" aria-labelledby="manage-clients-title"
            onClick={(e) => e.stopPropagation()}>
            <div className="portal-modal__head">
              <h3 id="manage-clients-title" className="portal-modal__title">Manage clients</h3>
              <button type="button" className="portal-modal__close" aria-label="Close"
                onClick={() => setClientsModalOpen(false)}>×</button>
            </div>
            {members.length === 0 && <p style={{ opacity: 0.7, fontSize: '0.9rem' }}>No clients yet.</p>}
            {members.map(m => (
              <div key={m.user_id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '6px 0' }}>
                <span style={{ flex: 1 }}>
                  {m.profile?.full_name || '(no name)'}
                  {!m.last_sign_in_at && (
                    <span className="login-never" aria-label="Never logged in" title="Never logged in"> ×</span>
                  )}
                </span>
                {m.last_sign_in_at && (
                  <span style={{ fontFamily: 'var(--sans)', fontSize: '0.82rem', color: 'var(--muted)', fontVariantNumeric: 'tabular-nums' }}>
                    {formatLastLogin(m.last_sign_in_at)}
                  </span>
                )}
                <button className="portal-client-remove" aria-label="Remove client"
                  onClick={() => removeMember(m.user_id)}>×</button>
              </div>
            ))}
            {addableCustomers.length > 0 && (
              <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
                <select value={addCustomerId} onChange={e => setAddCustomerId(e.target.value)}
                  style={{ flex: 1, padding: '6px 10px', fontFamily: 'inherit', borderRadius: 4, border: '1px solid var(--line)' }}>
                  <option value="">— Add existing client —</option>
                  {addableCustomers.map(c => <option key={c.id} value={c.id}>{c.full_name || '(no name)'}</option>)}
                </select>
                <button className="btn btn--solid" onClick={addMember} disabled={!addCustomerId}>Add</button>
              </div>
            )}
            <div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--line)' }}>
              {!inviteOpen ? (
                <button className="btn btn--compact" onClick={() => { setInviteOpen(true); setInviteMsg(''); }}>
                  + Invite Client
                </button>
              ) : (
                <form onSubmit={inviteToProject} style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                  <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                    <input type="email" placeholder="Email" required
                      value={inviteEmail} onChange={e => setInviteEmail(e.target.value)}
                      style={{ flex: 1, minWidth: 200, padding: '6px 10px', borderRadius: 4, border: '1px solid var(--line)', fontFamily: 'inherit' }} />
                    <input type="text" placeholder="Full name"
                      value={inviteName} onChange={e => setInviteName(e.target.value)}
                      style={{ flex: 1, minWidth: 200, padding: '6px 10px', borderRadius: 4, border: '1px solid var(--line)', fontFamily: 'inherit' }} />
                  </div>
                  <div style={{ display: 'flex', gap: 8 }}>
                    <button type="submit" className="btn btn--solid" disabled={inviteBusy}>
                      {inviteBusy ? 'Sending…' : 'Send Invite'}
                    </button>
                    <button type="button" className="btn" onClick={() => setInviteOpen(false)}>Cancel</button>
                  </div>
                </form>
              )}
              {inviteMsg && <p style={{ marginTop: 10, fontSize: '0.9rem', opacity: 0.85 }}>{inviteMsg}</p>}
            </div>
          </div>
        </div>
      )}
    </PortalShell>
  );
}

function UploadPhoto({ project, albumId, onDone, bucket = 'project-photos', kind = 'client' }) {
  const [files, setFiles] = React.useState([]);
  const [caption, setCaption] = React.useState('');
  const [date, setDate] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [progress, setProgress] = React.useState({ done: 0, total: 0, startedAt: 0 });
  const ref = React.useRef();

  const upload = async (e) => {
    e.preventDefault();
    if (!files.length) return;
    setBusy(true);
    setProgress({ done: 0, total: files.length, startedAt: Date.now() });
    const { data: { user } } = await sb.auth.getUser();
    for (let i = 0; i < files.length; i++) {
      const f = files[i];
      let isPano = false;
      try {
        const probe = await loadImage(f);
        isPano = isPanoramaImage(probe);
      } catch { /* keep default */ }
      const compressed = await compressImage(f, isPano ? 6144 : 1920, isPano ? 0.9 : 0.85);
      const path = `${project.id}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`;
      const { error: uErr } = await sb.storage.from(bucket).upload(path, compressed, { contentType: 'image/jpeg' });
      if (uErr) { alert(uErr.message); continue; }
      await sb.from('photos').insert({
        project_id: project.id, storage_path: path,
        caption: caption || null, taken_at: date || null,
        album_id: albumId || null,
        is_panorama: isPano,
        kind,
        uploaded_by: user?.id || null,
      });
      setProgress(p => ({ ...p, done: p.done + 1 }));
    }
    setFiles([]); setCaption(''); setDate('');
    if (ref.current) ref.current.value = '';
    setBusy(false);
    setProgress({ done: 0, total: 0, startedAt: 0 });
    onDone();
  };

  const formatEta = () => {
    if (progress.done === 0) return '';
    const elapsed = (Date.now() - progress.startedAt) / 1000;
    const perItem = elapsed / progress.done;
    const remaining = Math.max(0, Math.round(perItem * (progress.total - progress.done)));
    if (remaining === 0) return 'almost done';
    if (remaining < 60) return `~${remaining}s left`;
    const min = Math.floor(remaining / 60);
    const sec = remaining % 60;
    return `~${min}m ${sec}s left`;
  };

  const pct = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;

  return (
    <form className="form" onSubmit={upload} style={{ marginBottom: 24 }}>
      <div className="form__field">
        <label className="form__label">Photos</label>
        <input ref={ref} className="form__input" type="file" accept="image/*" multiple
          onChange={e => setFiles(Array.from(e.target.files))} />
      </div>
      <div className="form__row">
        <div className="form__field"><label className="form__label">Caption (optional)</label>
          <input className="form__input" value={caption} onChange={e => setCaption(e.target.value)} /></div>
        <div className="form__field"><label className="form__label">Date taken</label>
          <input className="form__input" type="date" value={date} onChange={e => setDate(e.target.value)} /></div>
      </div>
      {busy && progress.total > 0 && (
        <div className="upload-progress">
          <div className="upload-progress__bar">
            <div className="upload-progress__fill" style={{ width: `${pct}%` }} />
          </div>
          <div className="upload-progress__meta">
            <span>{progress.done} of {progress.total} uploaded</span>
            <span>{pct}% · {formatEta()}</span>
          </div>
        </div>
      )}
      <button className="btn btn--solid" disabled={busy || !files.length}>
        {busy ? `Uploading ${progress.done}/${progress.total}…` : `Upload ${files.length || 0}`}
      </button>
    </form>
  );
}

function UploadInvoice({ project, onDone }) {
  const [file, setFile] = React.useState(null);
  const [description, setDescription] = React.useState('');
  const [amount, setAmount] = React.useState('');
  const [due, setDue] = React.useState('');
  const [paid, setPaid] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const ref = React.useRef();

  const upload = async (e) => {
    e.preventDefault(); setBusy(true);
    let storage_path = null;
    if (file) {
      const safeName = (file.name || 'invoice').replace(/[^a-zA-Z0-9._-]/g, '_');
      storage_path = `${project.id}/${Date.now()}-${safeName}`;
      const { error } = await sb.storage.from('project-invoices').upload(storage_path, file);
      if (error) { alert(error.message); setBusy(false); return; }
    }
    await sb.from('invoices').insert({
      project_id: project.id, storage_path, description, paid,
      amount_cents: Math.round(parseFloat(amount || '0') * 100),
      due_date: due || null,
    });
    setFile(null); setDescription(''); setAmount(''); setDue(''); setPaid(false);
    if (ref.current) ref.current.value = '';
    setBusy(false); onDone();
  };

  return (
    <form className="form" onSubmit={upload} style={{ marginBottom: 24 }}>
      <div className="form__row">
        <div className="form__field"><label className="form__label">Description</label>
          <input className="form__input" value={description} onChange={e => setDescription(e.target.value)} required /></div>
        <div className="form__field"><label className="form__label">Amount (USD)</label>
          <input className="form__input" type="number" step="0.01" value={amount} onChange={e => setAmount(e.target.value)} required /></div>
      </div>
      <div className="form__row">
        <div className="form__field"><label className="form__label">Due date</label>
          <input className="form__input" type="date" value={due} onChange={e => setDue(e.target.value)} /></div>
        <div className="form__field"><label className="form__label">PDF (optional)</label>
          <input ref={ref} className="form__input" type="file" accept="application/pdf,image/*"
            onChange={e => setFile(e.target.files[0] || null)} /></div>
      </div>
      <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
        <input type="checkbox" checked={paid} onChange={e => setPaid(e.target.checked)} /> Paid
      </label>
      <button className="btn btn--solid" disabled={busy}>{busy ? 'Saving…' : 'Add Invoice'}</button>
    </form>
  );
}

function UploadFile({ project, folders = [], onDone }) {
  const [files, setFiles] = React.useState([]);
  // folderId is either '' (no folder) or a project_file_folders.id.
  const [folderId, setFolderId] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [progress, setProgress] = React.useState({ done: 0, total: 0, startedAt: 0 });
  const [dragOver, setDragOver] = React.useState(false);
  const ref = React.useRef();

  const addFiles = (incoming) => {
    const arr = Array.from(incoming || []).filter(Boolean);
    if (arr.length === 0) return;
    setFiles(prev => [...prev, ...arr]);
  };

  const removeAt = (i) => setFiles(prev => prev.filter((_, idx) => idx !== i));

  const onDrop = (e) => {
    e.preventDefault();
    setDragOver(false);
    addFiles(e.dataTransfer?.files);
  };

  const upload = async (e) => {
    e.preventDefault();
    if (files.length === 0) return;
    setBusy(true);
    setProgress({ done: 0, total: files.length, startedAt: Date.now() });
    const folderName = folderId ? (folders.find(f => f.id === folderId)?.name || null) : null;
    let failed = 0;
    for (let i = 0; i < files.length; i++) {
      const f = files[i];
      const safeName = (f.name || 'file').replace(/[^a-zA-Z0-9._-]/g, '_');
      const path = `${project.id}/${Date.now()}-${i}-${safeName}`;
      const { error: upErr } = await sb.storage.from('project-files').upload(path, f);
      if (upErr) { failed++; continue; }
      await sb.from('files').insert({
        project_id: project.id, storage_path: path,
        name: f.name, kind: null,
        folder_id: folderId || null,
        folder: folderName,
      });
      setProgress(p => ({ ...p, done: p.done + 1 }));
    }
    setBusy(false);
    setFiles([]);
    setProgress({ done: 0, total: 0, startedAt: 0 });
    if (ref.current) ref.current.value = '';
    if (failed > 0) alert(`${failed} file${failed === 1 ? '' : 's'} failed to upload.`);
    onDone();
  };

  const formatEta = () => {
    if (progress.done === 0) return '';
    const elapsed = (Date.now() - progress.startedAt) / 1000;
    const perItem = elapsed / progress.done;
    const remaining = Math.max(0, Math.round(perItem * (progress.total - progress.done)));
    if (remaining === 0) return 'almost done';
    if (remaining < 60) return `~${remaining}s left`;
    const min = Math.floor(remaining / 60);
    const sec = remaining % 60;
    return `~${min}m ${sec}s left`;
  };
  const pct = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;

  return (
    <form className="form file-upload" onSubmit={upload} style={{ marginBottom: 24 }}>
      <div className="form__field">
        <label className="form__label">Folder</label>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <select className="form__select" value={folderId}
            onChange={e => setFolderId(e.target.value)}
            style={{ flex: 1, minWidth: 200 }}>
            <option value="">— No folder —</option>
            {folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
          </select>
        </div>
      </div>
      <div className={`file-upload__drop ${dragOver ? 'is-active' : ''}`}
        onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onDrop={onDrop}>
        <p className="file-upload__drop-text">
          Drag &amp; drop files here, or
        </p>
        <input ref={ref} type="file" multiple
          onChange={e => addFiles(e.target.files)}
          style={{ display: 'block', margin: '8px auto 0' }} />
        {files.length > 0 && (
          <ul className="file-upload__queue">
            {files.map((f, i) => (
              <li key={i}>
                <span>{f.name}</span>
                <span className="file-upload__queue-size">{formatBytes(f.size)}</span>
                <button type="button" className="file-upload__queue-remove"
                  onClick={() => removeAt(i)} aria-label="Remove">×</button>
              </li>
            ))}
          </ul>
        )}
      </div>
      {busy && progress.total > 0 && (
        <div className="upload-progress">
          <div className="upload-progress__bar">
            <div className="upload-progress__fill" style={{ width: `${pct}%` }} />
          </div>
          <div className="upload-progress__meta">
            <span>{progress.done} of {progress.total} uploaded</span>
            <span>{pct}% · {formatEta()}</span>
          </div>
        </div>
      )}
      <button className="btn btn--solid" disabled={busy || files.length === 0} style={{ marginTop: 12 }}>
        {busy
          ? `Uploading ${progress.done}/${progress.total}…`
          : files.length === 0
            ? 'Upload'
            : `Upload ${files.length} file${files.length === 1 ? '' : 's'}`}
      </button>
    </form>
  );
}

// ---- First-time password setup (after invite/recovery) ----
function SetPasswordForm({ onDone }) {
  const [pw, setPw] = React.useState('');
  const [pw2, setPw2] = React.useState('');
  const [error, setError] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setError('');
    if (pw.length < 8) return setError('Password must be at least 8 characters.');
    if (pw !== pw2) return setError('Passwords do not match.');
    setBusy(true);
    const { error } = await sb.auth.updateUser({ password: pw });
    setBusy(false);
    if (error) return setError(error.message);
    history.replaceState(null, '', window.location.pathname + window.location.search);
    onDone();
  };

  return (
    <PortalShell title="Welcome" subtitle="Set a password to finish activating your client portal account.">
      <form className="form" onSubmit={submit} style={{ maxWidth: 460 }}>
        <div className="form__field">
          <label className="form__label">New Password</label>
          <input className="form__input" type="password" required autoComplete="new-password"
            value={pw} onChange={(e) => setPw(e.target.value)} />
        </div>
        <div className="form__field">
          <label className="form__label">Confirm Password</label>
          <input className="form__input" type="password" required autoComplete="new-password"
            value={pw2} onChange={(e) => setPw2(e.target.value)} />
        </div>
        {error && <p style={{ color: '#c0392b', marginTop: 8, fontSize: '0.9rem' }}>{error}</p>}
        <button type="submit" className="btn btn--solid" disabled={busy} style={{ marginTop: 16 }}>
          {busy ? 'Saving…' : 'Set Password & Continue'}
        </button>
      </form>
    </PortalShell>
  );
}

// ---- Root ----
function PortalRoot() {
  const { session, profile, loading } = useSession();
  const [needsPassword, setNeedsPassword] = React.useState(() => !!window.__isInviteFlow);
  if (!session) return <PortalLoginForm />;
  if (loading) return <PortalShell><p style={{ textAlign: 'center' }}>Loading…</p></PortalShell>;
  if (!profile) return <PortalShell><p style={{ textAlign: 'center' }}>Setting up your account…</p></PortalShell>;
  if (needsPassword) return <SetPasswordForm onDone={() => setNeedsPassword(false)} />;
  if (profile.role === 'admin') return <AdminDashboard profile={profile} session={session} />;
  return <CustomerDashboard profile={profile} />;
}

const canSeeFinancials = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^(taybin|tyson|joe)@rigginsdesign\.[a-z]+$/.test(e);
};

const canSeeAccounting = (email) => {
  const e = (email || '').trim().toLowerCase();
  return canSeeFinancials(email) || /^lauren@rigginsdesign\.[a-z]+$/.test(e);
};

// Documents super-admin (always sees every folder regardless of viewer list).
// Currently Tyson only.
const isDocumentsSuperAdmin = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^tyson@rigginsdesign\.[a-z]+$/.test(e);
};

const canSeeSelections = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^(taybin|tyson|joe|lauren|tim)@rigginsdesign\.[a-z]+$/.test(e);
};

const canSeeClients = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^(taybin|tyson|joe|lauren)@rigginsdesign\.[a-z]+$/.test(e);
};

const canSeeInvoices = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^(taybin|tyson|joe|lauren)@rigginsdesign\.[a-z]+$/.test(e);
};

const canSeePhotos = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^(taybin|tyson|joe|lauren)@rigginsdesign\.[a-z]+$/.test(e);
};

const canManageProjects = (email) => {
  const e = (email || '').trim().toLowerCase();
  return /^(taybin|tyson|joe|lauren)@rigginsdesign\.[a-z]+$/.test(e);
};

Object.assign(window, { PortalRoot });
