// Tabi — Trip-centric Accounting (共同記帳 / 個人記帳)

// ── Shared helpers ───────────────────────────────────────────

const CAT_ICONS = {
  '餐飲': '🍜', '交通': '🚕', '住宿': '🏨', '景點': '🎢',
  '購物': '🛍️', '代購': '🎁', '事前': '✈️', '其他': '💳',
};
const CAT_LIST = Object.keys(CAT_ICONS);

function _fmtDateShort(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  return `${d.getMonth() + 1}/${d.getDate()}`;
}
function _fmtDateFull(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  return `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')}`;
}
function _fmtAmt(n, currency) {
  if (currency === 'JPY') return `¥${Number(n).toLocaleString()}`;
  if (currency === 'TWD') return `NT$${Number(n).toLocaleString()}`;
  return `${n}`;
}

function computeSettlement(members, expenses) {
  const balances = {};
  members.forEach(m => balances[m.id] = 0);
  expenses.forEach(e => {
    if (e.visibility !== 'shared') return;
    let unsettledSum = 0;
    e.splits.forEach(s => {
      if (!s.is_settled) {
        balances[s.member_id] = (balances[s.member_id] || 0) - s.amount;
        unsettledSum += s.amount;
      }
    });
    balances[e.paid_by] = (balances[e.paid_by] || 0) + unsettledSum;
  });
  const creditors = [], debtors = [];
  Object.entries(balances).forEach(([id, b]) => {
    if (b > 1)  creditors.push({ id, b });
    else if (b < -1) debtors.push({ id, b: -b });
  });
  creditors.sort((a, b) => b.b - a.b);
  debtors.sort((a, b) => b.b - a.b);
  const transfers = [];
  let i = 0, j = 0;
  while (i < debtors.length && j < creditors.length) {
    const amt = Math.min(debtors[i].b, creditors[j].b);
    transfers.push({ from: debtors[i].id, to: creditors[j].id, amount: Math.round(amt) });
    debtors[i].b -= amt; creditors[j].b -= amt;
    if (debtors[i].b < 1) i++;
    if (creditors[j].b < 1) j++;
  }
  return { balances, transfers };
}

function MemberAvatar({ m, size = 32, ring }) {
  return (
    <div style={{
      width: size, height: size, borderRadius: size / 2, flexShrink: 0,
      background: m.color || '#888', color: '#fff',
      display: 'grid', placeItems: 'center',
      fontFamily: tabiSerif, fontSize: Math.max(10, size * 0.38), fontWeight: 700,
      boxShadow: ring ? `0 0 0 2px ${TABI_COLORS.bg}, 0 0 0 4px ${m.color}` : 'none',
    }}>{m.initial || m.display_name?.[0] || '?'}</div>
  );
}

// ── Shared sheet wrapper ──────────────────────────────────────
// Uses a React portal into document.body so position:fixed escapes the
// overflow:auto stacking context of .tabi-screen (iOS Safari quirk).

function SheetWrap({ title, onClose, children, action }) {
  return ReactDOM.createPortal(
    <div style={{
      position: 'fixed', inset: 0, zIndex: 9000, background: 'rgba(0,0,0,0.45)',
      display: 'flex', alignItems: 'flex-end',
    }} onClick={e => e.target === e.currentTarget && onClose()}>
      <div style={{
        width: '100%', maxWidth: 480, margin: '0 auto',
        background: TABI_COLORS.bg, borderRadius: '20px 20px 0 0',
        display: 'flex', flexDirection: 'column',
        maxHeight: '90dvh',      /* dvh = dynamic viewport, shrinks with browser chrome */
      }}>
        {/* Header — never scrolls */}
        <div style={{
          display: 'flex', alignItems: 'center', padding: '16px 16px 14px',
          borderBottom: `1px solid ${TABI_COLORS.line}`, flexShrink: 0,
        }}>
          <div style={{ fontFamily: tabiSerif, fontSize: 17, fontWeight: 700, flex: 1 }}>{title}</div>
          <button onClick={onClose} style={{
            background: 'none', border: 'none', fontSize: 24, cursor: 'pointer',
            color: TABI_COLORS.ink3, lineHeight: 1, padding: '0 4px',
          }}>×</button>
        </div>
        {/* Scrollable content */}
        <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '14px 16px', minHeight: 0 }}>
          {children}
        </div>
        {/* Action footer — always visible, respects home indicator */}
        {action && (
          <div style={{
            flexShrink: 0,
            padding: `12px 16px calc(env(safe-area-inset-bottom, 0px) + 16px)`,
            borderTop: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
          }}>
            {action}
          </div>
        )}
      </div>
    </div>,
    document.body
  );
}

// ── Add Expense Sheet ─────────────────────────────────────────

function AddExpenseSheet({ trip, onClose, onSaved, initialItems, user }) {
  const myMember = user ? trip.members.find(m => m.user_id === user.id) : null;
  const [title, setTitle]       = React.useState('');
  const [amount, setAmount]     = React.useState('');
  const [category, setCategory] = React.useState('餐飲');
  const [paidBy, setPaidBy]     = React.useState(myMember?.id || trip.members[0]?.id || '');
  const [visibility, setVisibility] = React.useState('personal');
  const [splitMethod, setSplitMethod] = React.useState('equal');
  const [customSplits, setCustomSplits] = React.useState(() =>
    trip.members.map(m => ({ memberId: m.id, amount: '', settled: false }))
  );
  const [equalSettled, setEqualSettled] = React.useState({});
  const [isProxy, setIsProxy]   = React.useState(false);
  const [proxyFor, setProxyFor] = React.useState('');
  const [expenseDate, setExpenseDate] = React.useState(
    new Date().toISOString().slice(0, 10)
  );
  const [inputCurrency, setInputCurrency] = React.useState(trip.currency);
  const [convertedAmt, setConvertedAmt]   = React.useState(null);
  const [fetchingRate, setFetchingRate]   = React.useState(false);
  const [saving, setSaving]     = React.useState(false);
  const [error, setError]       = React.useState('');
  const [showItems, setShowItems] = React.useState(!!(initialItems && initialItems.length > 0));
  const [itemRows, setItemRows] = React.useState(
    initialItems && initialItems.length > 0
      ? initialItems.map(it => ({ name: it.translated_name || it.original_name || '', qty: it.qty || 1, price: String(it.subtotal || '') }))
      : [{ name: '', qty: 1, price: '' }]
  );

  function addItemRow() { setItemRows(prev => [...prev, { name: '', qty: 1, price: '' }]); }
  function removeItemRow(i) { setItemRows(prev => prev.filter((_, idx) => idx !== i)); }
  function updateItemRow(i, field, val) { setItemRows(prev => prev.map((r, idx) => idx === i ? { ...r, [field]: val } : r)); }

  React.useEffect(() => {
    if (!showItems) return;
    const total = itemRows.reduce((s, r) => s + (parseInt(r.price) || 0) * (parseInt(r.qty) || 1), 0);
    if (total > 0) setAmount(String(total));
  }, [itemRows, showItems]);

  React.useEffect(() => {
    if (inputCurrency === trip.currency || !amount || parseInt(amount) <= 0) { setConvertedAmt(null); return; }
    let cancelled = false;
    setFetchingRate(true);
    fetch(`/api/trips/exchange-rate?from_currency=${inputCurrency}&to_currency=${trip.currency}&date=${expenseDate}`, {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    })
      .then(r => r.json())
      .then(d => { if (!cancelled && d.rate) setConvertedAmt(Math.round(parseInt(amount) * d.rate)); })
      .catch(() => { if (!cancelled) setConvertedAmt(null); })
      .finally(() => { if (!cancelled) setFetchingRate(false); });
    return () => { cancelled = true; };
  }, [inputCurrency, amount, expenseDate]);

  const isCustom = visibility === 'shared' && splitMethod === 'custom';
  const customTotal = customSplits.reduce((s, cs) => s + (parseInt(cs.amount) || 0), 0);

  function handleSplitMethod(v) {
    if (v !== 'custom' && splitMethod === 'custom' && customTotal > 0) setAmount(String(customTotal));
    setSplitMethod(v);
  }

  async function save() {
    if (!title.trim()) { setError('請輸入名稱'); return; }
    const rawAmt = isCustom ? customTotal : parseInt(amount, 10);
    const amt = (inputCurrency !== trip.currency && convertedAmt) ? convertedAmt : rawAmt;
    if (!amt || amt <= 0) { setError(isCustom ? '請輸入各人金額' : '請輸入金額'); return; }
    setSaving(true);
    try {
      const items = showItems
        ? itemRows.filter(r => r.name.trim()).map(r => ({
            original_name: r.name.trim(), translated_name: r.name.trim(),
            qty: parseInt(r.qty) || 1, subtotal: parseInt(r.price) || 0,
          }))
        : null;
      const equalSettledIds = visibility === 'shared' && splitMethod === 'equal'
        ? trip.members.filter(m => equalSettled[m.id]).map(m => m.id)
        : [];
      const useCustomForEqual = equalSettledIds.length > 0;
      const body = {
        title: title.trim(),
        amount: amt,
        currency: trip.currency,
        paid_by: paidBy,
        category,
        icon: CAT_ICONS[category] || '💳',
        split_method: visibility === 'personal' ? 'solo' : (useCustomForEqual ? 'custom' : splitMethod),
        visibility,
        is_proxy: isProxy,
        proxy_for: isProxy ? proxyFor : null,
        expense_date: new Date(expenseDate).toISOString(),
        items,
        splits: isCustom
          ? Object.fromEntries(customSplits.filter(cs => parseInt(cs.amount) > 0).map(cs => [cs.memberId, parseInt(cs.amount)]))
          : useCustomForEqual
            ? Object.fromEntries(trip.members.map(m => [m.id, Math.round(amt / trip.members.length)]))
            : undefined,
        settled_members: isCustom
          ? customSplits.filter(cs => cs.settled && parseInt(cs.amount) > 0).map(cs => cs.memberId)
          : useCustomForEqual ? equalSettledIds : undefined,
      };
      const r = await fetch(`/api/trips/${trip.id}/expenses`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify(body),
      });
      if (!r.ok) throw new Error(await r.text());
      onSaved();
    } catch (e) {
      setError('儲存失敗：' + e.message);
      setSaving(false);
    }
  }

  const inputStyle = {
    width: '100%', padding: '10px 12px', borderRadius: 10,
    border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
    fontSize: 16, color: TABI_COLORS.ink, outline: 'none',
    fontFamily: 'inherit', boxSizing: 'border-box',
  };

  const _equalMemberPanel = (visibility === 'shared' && splitMethod === 'equal') ? (
    <div style={{ margin: '0 0 12px', background: TABI_COLORS.bg2, borderRadius: 14, padding: '10px 14px', border: `1px solid ${TABI_COLORS.line}` }}>
      {trip.members.map(m => {
        const isPayer = m.id === paidBy;
        const settled = !!equalSettled[m.id];
        const eAmt = parseInt(amount) > 0 ? Math.round(parseInt(amount) / trip.members.length) : null;
        return (
          <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 8 }}>
            <div style={{ width: 28, height: 28, borderRadius: 14, flexShrink: 0, background: m.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: '#fff' }}>
              {(m.display_name || '?')[0]}
            </div>
            <div style={{ flex: 1, fontSize: 12.5, fontWeight: 600, color: TABI_COLORS.ink }}>{m.display_name}</div>
            {eAmt !== null && <div style={{ fontSize: 12, color: TABI_COLORS.ink3 }}>¥{eAmt.toLocaleString()}</div>}
            {isPayer ? (
              <div style={{ fontSize: 10.5, fontWeight: 700, padding: '4px 8px', borderRadius: 7, background: `${m.color}22`, color: m.color, flexShrink: 0 }}>付款人</div>
            ) : (
              <button onClick={() => setEqualSettled(prev => ({ ...prev, [m.id]: !prev[m.id] }))} style={{
                padding: '4px 9px', borderRadius: 7, border: 'none', cursor: 'pointer', flexShrink: 0,
                background: settled ? TABI_COLORS.green : `${TABI_COLORS.warn}33`,
                color: settled ? '#fff' : '#a07020', fontSize: 11, fontWeight: 700,
              }}>{settled ? '✓ 已付' : '未付'}</button>
            )}
          </div>
        );
      })}
    </div>
  ) : null;

  return (
    <SheetWrap title="新增支出" onClose={onClose} action={
      <>
        {error && <div style={{ color: TABI_COLORS.red, fontSize: 12.5, marginBottom: 8 }}>{error}</div>}
        <button onClick={save} disabled={saving} style={{
          width: '100%', padding: '14px', borderRadius: 14, border: 'none',
          background: saving ? TABI_COLORS.line2 : TABI_COLORS.ink,
          color: '#fff', fontSize: 15, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer',
        }}>{saving ? '儲存中…' : '新增支出'}</button>
      </>
    }>
      {/* Title */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>名稱</div>
        <input value={title} onChange={e => setTitle(e.target.value)} placeholder="例：一蘭拉麵" style={inputStyle} />
      </div>

      {/* Currency selector */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>支付幣別</div>
        <div style={{ display: 'flex', gap: 8 }}>
          {[trip.currency, 'TWD'].filter((v, i, a) => a.indexOf(v) === i).map(c => (
            <button key={c} onClick={() => setInputCurrency(c)} style={{
              flex: 1, padding: '8px', borderRadius: 10, fontSize: 13, fontWeight: 600,
              border: `2px solid ${inputCurrency === c ? TABI_COLORS.ink : TABI_COLORS.line}`,
              background: inputCurrency === c ? TABI_COLORS.ink : 'transparent',
              color: inputCurrency === c ? '#fff' : TABI_COLORS.ink2, cursor: 'pointer',
            }}>{c === trip.currency ? trip.currency : '台幣 TWD'}</button>
          ))}
        </div>
      </div>

      {/* Amount — hidden when custom (total derived from per-member splits) */}
      {!isCustom && (
        <div style={{ marginBottom: inputCurrency !== trip.currency && convertedAmt ? 4 : 12 }}>
          <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>金額（{inputCurrency}）</div>
          <input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0" style={inputStyle} inputMode="numeric" />
        </div>
      )}
      {!isCustom && inputCurrency !== trip.currency && (
        <div style={{ marginBottom: 12, fontSize: 12, color: TABI_COLORS.ink3, paddingLeft: 2 }}>
          {fetchingRate ? '換算中…' : convertedAmt !== null ? `≈ ${trip.currency === 'JPY' ? '¥' : ''}${convertedAmt.toLocaleString()} ${trip.currency}` : '請輸入金額'}
        </div>
      )}

      {/* Date */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>日期</div>
        <input type="date" value={expenseDate} onChange={e => setExpenseDate(e.target.value)} style={inputStyle} />
      </div>

      {/* Category */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>分類</div>
        <select value={category} onChange={e => setCategory(e.target.value)} style={inputStyle}>
          {CAT_LIST.map(c => <option key={c} value={c}>{CAT_ICONS[c]} {c}</option>)}
        </select>
      </div>

      {/* Paid by — only relevant for shared expenses */}
      {visibility === 'shared' && (
        <div style={{ marginBottom: 12 }}>
          <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>誰先付</div>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {trip.members.map(m => (
              <button key={m.id} onClick={() => setPaidBy(m.id)} style={{
                padding: '6px 14px', borderRadius: 20, fontSize: 13, fontWeight: 600,
                border: `2px solid ${paidBy === m.id ? m.color : TABI_COLORS.line}`,
                background: paidBy === m.id ? m.color : 'transparent',
                color: paidBy === m.id ? '#fff' : TABI_COLORS.ink2,
                cursor: 'pointer',
              }}>{m.display_name}</button>
            ))}
          </div>
        </div>
      )}

      {/* Visibility toggle */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>可見範圍</div>
        <div style={{ display: 'flex', gap: 8 }}>
          {[['personal', '個人'], ['shared', '共同']].map(([v, label]) => (
            <button key={v} onClick={() => setVisibility(v)} style={{
              flex: 1, padding: '9px 8px', borderRadius: 10, fontSize: 13, fontWeight: 600,
              border: `2px solid ${visibility === v ? TABI_COLORS.red : TABI_COLORS.line}`,
              background: visibility === v ? `${TABI_COLORS.red}15` : 'transparent',
              color: visibility === v ? TABI_COLORS.red : TABI_COLORS.ink2,
              cursor: 'pointer',
            }}>{label}</button>
          ))}
        </div>
      </div>

      {/* Split method (shared only) */}
      {visibility === 'shared' && (
        <div style={{ marginBottom: isCustom ? 0 : 12 }}>
          <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>分攤方式</div>
          <div style={{ display: 'flex', gap: 8 }}>
            {[['equal', '均分'], ['solo', '僅我'], ['custom', '自訂金額']].map(([v, label]) => (
              <button key={v} onClick={() => handleSplitMethod(v)} style={{
                flex: 1, padding: '9px 8px', borderRadius: 10, fontSize: 12, fontWeight: 600,
                border: `2px solid ${splitMethod === v ? TABI_COLORS.ink : TABI_COLORS.line}`,
                background: splitMethod === v ? TABI_COLORS.ink : 'transparent',
                color: splitMethod === v ? '#fff' : TABI_COLORS.ink2,
                cursor: 'pointer',
              }}>{label}</button>
            ))}
          </div>
        </div>
      )}

      {/* Equal split: member settled toggles */}
      {_equalMemberPanel}

      {/* Custom per-member amount inputs */}
      {isCustom && (
        <div style={{ margin: '10px 0 12px', background: TABI_COLORS.bg2, borderRadius: 14, padding: '12px 14px', border: `1px solid ${TABI_COLORS.line}` }}>
          {trip.members.map(m => {
            const cs = customSplits.find(s => s.memberId === m.id) || { memberId: m.id, amount: '', settled: false };
            const isPayer = m.id === paidBy;
            return (
              <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 10 }}>
                <div style={{ width: 30, height: 30, borderRadius: 15, flexShrink: 0, background: m.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: '#fff' }}>
                  {(m.display_name || '?')[0]}
                </div>
                <div style={{ fontSize: 12.5, fontWeight: 600, color: TABI_COLORS.ink, width: 52, flexShrink: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {m.display_name}
                </div>
                <div style={{ position: 'relative', flex: 1 }}>
                  <span style={{ position: 'absolute', left: 9, top: '50%', transform: 'translateY(-50%)', fontSize: 13, color: TABI_COLORS.ink3, pointerEvents: 'none' }}>¥</span>
                  <input type="number" inputMode="numeric" value={cs.amount} placeholder="0"
                    onChange={e => setCustomSplits(prev => prev.map(s => s.memberId === m.id ? {...s, amount: e.target.value} : s))}
                    style={{ width: '100%', padding: '7px 8px 7px 22px', borderRadius: 9, border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.card, fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' }}
                  />
                </div>
                {isPayer ? (
                  <div style={{ fontSize: 10.5, fontWeight: 700, padding: '4px 8px', borderRadius: 7, background: `${m.color}22`, color: m.color, flexShrink: 0 }}>付款人</div>
                ) : (
                  <button onClick={() => setCustomSplits(prev => prev.map(s => s.memberId === m.id ? {...s, settled: !s.settled} : s))} style={{
                    padding: '5px 9px', borderRadius: 7, border: 'none', cursor: 'pointer', flexShrink: 0,
                    background: cs.settled ? TABI_COLORS.green : `${TABI_COLORS.warn}33`,
                    color: cs.settled ? '#fff' : '#a07020', fontSize: 11, fontWeight: 700,
                  }}>{cs.settled ? '✓ 已付' : '未付'}</button>
                )}
              </div>
            );
          })}
          <div style={{ borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 9, marginTop: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <span style={{ fontSize: 12, color: TABI_COLORS.ink3 }}>合計</span>
            <span style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.ink }}>¥{customTotal.toLocaleString()}</span>
          </div>
        </div>
      )}

      {/* Proxy toggle — personal only */}
      {visibility === 'personal' && (
        <>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 10, marginBottom: isProxy ? 12 : 4,
            padding: '10px 12px', borderRadius: 10, background: TABI_COLORS.bg2,
            border: `1px solid ${TABI_COLORS.line}`,
          }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink }}>代購</div>
              <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 1 }}>幫別人代為購買</div>
            </div>
            <div onClick={() => setIsProxy(!isProxy)} style={{
              width: 44, height: 24, borderRadius: 12, cursor: 'pointer',
              background: isProxy ? TABI_COLORS.red : TABI_COLORS.line2,
              position: 'relative', transition: 'background 0.2s', flexShrink: 0,
            }}>
              <div style={{
                position: 'absolute', top: 2, left: isProxy ? 22 : 2,
                width: 20, height: 20, borderRadius: 10, background: '#fff',
                transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
              }} />
            </div>
          </div>
          {isProxy && (
            <div style={{ marginBottom: 12 }}>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>代購對象</div>
              <input value={proxyFor} onChange={e => setProxyFor(e.target.value)}
                placeholder="誰的代購品？" style={inputStyle} />
            </div>
          )}
        </>
      )}

      {/* Items detail (optional) */}
      <div style={{ marginTop: 4, borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: showItems ? 10 : 0 }}>
          <div style={{ flex: 1, fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600 }}>明細（選填）</div>
          <button onClick={() => setShowItems(!showItems)} style={{
            padding: '4px 10px', borderRadius: 8, border: `1px solid ${TABI_COLORS.line}`,
            background: 'transparent', fontSize: 12, color: TABI_COLORS.ink2, cursor: 'pointer',
          }}>{showItems ? '收起' : '+ 展開'}</button>
        </div>
        {showItems && (
          <>
            {itemRows.map((row, i) => (
              <div key={i} style={{ display: 'flex', gap: 6, marginBottom: 8, alignItems: 'center' }}>
                <input value={row.name} onChange={e => updateItemRow(i, 'name', e.target.value)}
                  placeholder="品項名稱" style={{ ...inputStyle, flex: 3, padding: '8px 10px', fontSize: 16 }} />
                <input type="number" value={row.qty} onChange={e => updateItemRow(i, 'qty', e.target.value)}
                  placeholder="數" style={{ ...inputStyle, flex: 1, padding: '8px 10px', fontSize: 16 }} inputMode="numeric" />
                <input type="number" value={row.price} onChange={e => updateItemRow(i, 'price', e.target.value)}
                  placeholder="金額" style={{ ...inputStyle, flex: 2, padding: '8px 10px', fontSize: 16 }} inputMode="numeric" />
                <button onClick={() => removeItemRow(i)} style={{
                  background: 'none', border: 'none', fontSize: 18, cursor: 'pointer',
                  color: TABI_COLORS.ink3, padding: '0 2px', flexShrink: 0,
                }}>×</button>
              </div>
            ))}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 2 }}>
              <button onClick={addItemRow} style={{
                padding: '4px 10px', borderRadius: 8, border: `1px solid ${TABI_COLORS.line}`,
                background: 'transparent', fontSize: 12, color: TABI_COLORS.ink2, cursor: 'pointer',
              }}>+ 新增</button>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3 }}>
                小計：{_fmtAmt(itemRows.reduce((s, r) => s + (parseInt(r.price) || 0) * (parseInt(r.qty) || 1), 0), trip.currency)}
              </div>
            </div>
          </>
        )}
      </div>
    </SheetWrap>
  );
}


// ── Edit Expense Sheet ────────────────────────────────────────

function EditExpenseSheet({ trip, expense, onClose, onSaved, onDeleted }) {
  const [title, setTitle]       = React.useState(expense.title);
  const [amount, setAmount]     = React.useState(String(expense.amount));
  const [category, setCategory] = React.useState(expense.category || '餐飲');
  const [paidBy, setPaidBy]     = React.useState(expense.paid_by);
  const [visibility, setVisibility] = React.useState(expense.visibility || 'personal');
  const [splitMethod, setSplitMethod] = React.useState(expense.split_method || 'equal');
  const [customSplits, setCustomSplits] = React.useState(() => {
    if (expense.split_method === 'custom' && expense.splits?.length) {
      return trip.members.map(m => {
        const s = expense.splits.find(x => x.member_id === m.id);
        return { memberId: m.id, amount: s ? String(s.amount) : '', settled: s?.is_settled || false };
      });
    }
    return trip.members.map(m => ({ memberId: m.id, amount: '', settled: false }));
  });
  const [equalSettled, setEqualSettled] = React.useState(() => {
    if (expense.splits?.length) {
      const s = {};
      expense.splits.forEach(sp => { if (sp.is_settled) s[sp.member_id] = true; });
      return s;
    }
    return {};
  });
  const [inputCurrency, setInputCurrency] = React.useState(expense.currency || trip.currency);
  const [convertedAmt, setConvertedAmt]   = React.useState(null);
  const [fetchingRate, setFetchingRate]   = React.useState(false);
  const [isProxy, setIsProxy]   = React.useState(expense.is_proxy || false);
  const [proxyFor, setProxyFor] = React.useState(expense.proxy_for || '');
  const [expenseDate, setExpenseDate] = React.useState(
    expense.expense_date ? expense.expense_date.slice(0, 10) : new Date().toISOString().slice(0, 10)
  );
  const [saving, setSaving]     = React.useState(false);
  const [deleting, setDeleting] = React.useState(false);
  const [error, setError]       = React.useState('');
  const [showItems, setShowItems] = React.useState(!!(expense.items && expense.items.length > 0));
  const [itemRows, setItemRows] = React.useState(
    expense.items && expense.items.length > 0
      ? expense.items.map(it => ({ name: it.translated_name || it.original_name || '', qty: it.qty || 1, price: String(it.subtotal || '') }))
      : [{ name: '', qty: 1, price: '' }]
  );

  function addItemRow() { setItemRows(prev => [...prev, { name: '', qty: 1, price: '' }]); }
  function removeItemRow(i) { setItemRows(prev => prev.filter((_, idx) => idx !== i)); }
  function updateItemRow(i, field, val) { setItemRows(prev => prev.map((r, idx) => idx === i ? { ...r, [field]: val } : r)); }

  React.useEffect(() => {
    if (!showItems) return;
    const total = itemRows.reduce((s, r) => s + (parseInt(r.price) || 0) * (parseInt(r.qty) || 1), 0);
    if (total > 0) setAmount(String(total));
  }, [itemRows, showItems]);

  React.useEffect(() => {
    if (inputCurrency === trip.currency || !amount || parseInt(amount) <= 0) { setConvertedAmt(null); return; }
    let cancelled = false;
    setFetchingRate(true);
    fetch(`/api/trips/exchange-rate?from_currency=${inputCurrency}&to_currency=${trip.currency}&date=${expenseDate}`, {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    })
      .then(r => r.json())
      .then(d => { if (!cancelled && d.rate) setConvertedAmt(Math.round(parseInt(amount) * d.rate)); })
      .catch(() => { if (!cancelled) setConvertedAmt(null); })
      .finally(() => { if (!cancelled) setFetchingRate(false); });
    return () => { cancelled = true; };
  }, [inputCurrency, amount, expenseDate]);

  const isCustom = visibility === 'shared' && splitMethod === 'custom';
  const customTotal = customSplits.reduce((s, cs) => s + (parseInt(cs.amount) || 0), 0);

  function handleSplitMethod(v) {
    if (v !== 'custom' && splitMethod === 'custom' && customTotal > 0) setAmount(String(customTotal));
    setSplitMethod(v);
  }

  async function save() {
    if (!title.trim()) { setError('請輸入名稱'); return; }
    const rawAmt = isCustom ? customTotal : parseInt(amount, 10);
    const amt = (inputCurrency !== trip.currency && convertedAmt) ? convertedAmt : rawAmt;
    if (!amt || amt <= 0) { setError(isCustom ? '請輸入各人金額' : '請輸入金額'); return; }
    setSaving(true);
    try {
      const items = showItems
        ? itemRows.filter(r => r.name.trim()).map(r => ({
            original_name: r.name.trim(), translated_name: r.name.trim(),
            qty: parseInt(r.qty) || 1, subtotal: parseInt(r.price) || 0,
          }))
        : null;
      const eqSettledIds = visibility === 'shared' && splitMethod === 'equal'
        ? trip.members.filter(m => equalSettled[m.id]).map(m => m.id)
        : [];
      const useCustomForEqual = eqSettledIds.length > 0;
      const body = {
        title: title.trim(),
        amount: amt,
        currency: trip.currency,
        paid_by: paidBy,
        category,
        icon: CAT_ICONS[category] || '💳',
        split_method: visibility === 'personal' ? 'solo' : (useCustomForEqual ? 'custom' : splitMethod),
        visibility,
        is_proxy: isProxy,
        proxy_for: isProxy ? proxyFor : null,
        expense_date: new Date(expenseDate).toISOString(),
        items,
        splits: isCustom
          ? Object.fromEntries(customSplits.filter(cs => parseInt(cs.amount) > 0).map(cs => [cs.memberId, parseInt(cs.amount)]))
          : useCustomForEqual
            ? Object.fromEntries(trip.members.map(m => [m.id, Math.round(amt / trip.members.length)]))
            : undefined,
        settled_members: isCustom
          ? customSplits.filter(cs => cs.settled && parseInt(cs.amount) > 0).map(cs => cs.memberId)
          : useCustomForEqual ? eqSettledIds : undefined,
      };
      const r = await fetch(`/api/trips/${trip.id}/expenses/${expense.id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify(body),
      });
      if (!r.ok) throw new Error(await r.text());
      onSaved();
    } catch (e) {
      setError('儲存失敗：' + e.message);
      setSaving(false);
    }
  }

  async function del() {
    if (!confirm('確定要刪除這筆支出？')) return;
    setDeleting(true);
    try {
      const r = await fetch(`/api/trips/${trip.id}/expenses/${expense.id}`, {
        method: 'DELETE',
        headers: { Authorization: `Bearer ${window.tabiToken()}` },
      });
      if (!r.ok) throw new Error(await r.text());
      onDeleted();
    } catch (e) {
      setError('刪除失敗：' + e.message);
      setDeleting(false);
    }
  }

  const inputStyle = {
    width: '100%', padding: '10px 12px', borderRadius: 10,
    border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
    fontSize: 16, color: TABI_COLORS.ink, outline: 'none',
    fontFamily: 'inherit', boxSizing: 'border-box',
  };

  const _equalMemberPanel = (visibility === 'shared' && splitMethod === 'equal') ? (
    <div style={{ margin: '0 0 12px', background: TABI_COLORS.bg2, borderRadius: 14, padding: '10px 14px', border: `1px solid ${TABI_COLORS.line}` }}>
      {trip.members.map(m => {
        const isPayer = m.id === paidBy;
        const settled = !!equalSettled[m.id];
        const eAmt = parseInt(amount) > 0 ? Math.round(parseInt(amount) / trip.members.length) : null;
        return (
          <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 8 }}>
            <div style={{ width: 28, height: 28, borderRadius: 14, flexShrink: 0, background: m.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: '#fff' }}>
              {(m.display_name || '?')[0]}
            </div>
            <div style={{ flex: 1, fontSize: 12.5, fontWeight: 600, color: TABI_COLORS.ink }}>{m.display_name}</div>
            {eAmt !== null && <div style={{ fontSize: 12, color: TABI_COLORS.ink3 }}>¥{eAmt.toLocaleString()}</div>}
            {isPayer ? (
              <div style={{ fontSize: 10.5, fontWeight: 700, padding: '4px 8px', borderRadius: 7, background: `${m.color}22`, color: m.color, flexShrink: 0 }}>付款人</div>
            ) : (
              <button onClick={() => setEqualSettled(prev => ({ ...prev, [m.id]: !prev[m.id] }))} style={{
                padding: '4px 9px', borderRadius: 7, border: 'none', cursor: 'pointer', flexShrink: 0,
                background: settled ? TABI_COLORS.green : `${TABI_COLORS.warn}33`,
                color: settled ? '#fff' : '#a07020', fontSize: 11, fontWeight: 700,
              }}>{settled ? '✓ 已付' : '未付'}</button>
            )}
          </div>
        );
      })}
    </div>
  ) : null;

  return (
    <SheetWrap title="編輯支出" onClose={onClose} action={
      <>
        {error && <div style={{ color: TABI_COLORS.red, fontSize: 12.5, marginBottom: 8 }}>{error}</div>}
        <div style={{ display: 'flex', gap: 10 }}>
          <button onClick={del} disabled={deleting} style={{
            padding: '14px 18px', borderRadius: 14, border: `1px solid ${TABI_COLORS.line}`,
            background: 'transparent', color: TABI_COLORS.red,
            fontSize: 15, cursor: deleting ? 'not-allowed' : 'pointer', flexShrink: 0,
          }}>{deleting ? '…' : '🗑'}</button>
          <button onClick={save} disabled={saving} style={{
            flex: 1, padding: '14px', borderRadius: 14, border: 'none',
            background: saving ? TABI_COLORS.line2 : TABI_COLORS.ink,
            color: '#fff', fontSize: 15, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer',
          }}>{saving ? '儲存中…' : '儲存變更'}</button>
        </div>
      </>
    }>
      {/* Title */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>名稱</div>
        <input value={title} onChange={e => setTitle(e.target.value)} placeholder="例：一蘭拉麵" style={inputStyle} />
      </div>

      {/* Currency selector */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>支付幣別</div>
        <div style={{ display: 'flex', gap: 8 }}>
          {[trip.currency, 'TWD'].filter((v, i, a) => a.indexOf(v) === i).map(c => (
            <button key={c} onClick={() => setInputCurrency(c)} style={{
              flex: 1, padding: '8px', borderRadius: 10, fontSize: 13, fontWeight: 600,
              border: `2px solid ${inputCurrency === c ? TABI_COLORS.ink : TABI_COLORS.line}`,
              background: inputCurrency === c ? TABI_COLORS.ink : 'transparent',
              color: inputCurrency === c ? '#fff' : TABI_COLORS.ink2, cursor: 'pointer',
            }}>{c === trip.currency ? trip.currency : '台幣 TWD'}</button>
          ))}
        </div>
      </div>

      {/* Amount — hidden when custom */}
      {!isCustom && (
        <div style={{ marginBottom: inputCurrency !== trip.currency && convertedAmt ? 4 : 12 }}>
          <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>金額（{inputCurrency}）</div>
          <input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0" style={inputStyle} inputMode="numeric" />
        </div>
      )}
      {!isCustom && inputCurrency !== trip.currency && (
        <div style={{ marginBottom: 12, fontSize: 12, color: TABI_COLORS.ink3, paddingLeft: 2 }}>
          {fetchingRate ? '換算中…' : convertedAmt !== null ? `≈ ${trip.currency === 'JPY' ? '¥' : ''}${convertedAmt.toLocaleString()} ${trip.currency}` : '請輸入金額'}
        </div>
      )}

      {/* Date */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>日期</div>
        <input type="date" value={expenseDate} onChange={e => setExpenseDate(e.target.value)} style={inputStyle} />
      </div>

      {/* Category */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>分類</div>
        <select value={category} onChange={e => setCategory(e.target.value)} style={inputStyle}>
          {CAT_LIST.map(c => <option key={c} value={c}>{CAT_ICONS[c]} {c}</option>)}
        </select>
      </div>

      {/* Paid by — only relevant for shared expenses */}
      {visibility === 'shared' && (
        <div style={{ marginBottom: 12 }}>
          <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>誰先付</div>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {trip.members.map(m => (
              <button key={m.id} onClick={() => setPaidBy(m.id)} style={{
                padding: '6px 14px', borderRadius: 20, fontSize: 13, fontWeight: 600,
                border: `2px solid ${paidBy === m.id ? m.color : TABI_COLORS.line}`,
                background: paidBy === m.id ? m.color : 'transparent',
                color: paidBy === m.id ? '#fff' : TABI_COLORS.ink2,
                cursor: 'pointer',
              }}>{m.display_name}</button>
            ))}
          </div>
        </div>
      )}

      {/* Visibility toggle */}
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>可見範圍</div>
        <div style={{ display: 'flex', gap: 8 }}>
          {[['personal', '個人'], ['shared', '共同']].map(([v, label]) => (
            <button key={v} onClick={() => setVisibility(v)} style={{
              flex: 1, padding: '9px 8px', borderRadius: 10, fontSize: 13, fontWeight: 600,
              border: `2px solid ${visibility === v ? TABI_COLORS.red : TABI_COLORS.line}`,
              background: visibility === v ? `${TABI_COLORS.red}15` : 'transparent',
              color: visibility === v ? TABI_COLORS.red : TABI_COLORS.ink2,
              cursor: 'pointer',
            }}>{label}</button>
          ))}
        </div>
      </div>

      {/* Split method (shared only) */}
      {visibility === 'shared' && (
        <div style={{ marginBottom: isCustom ? 0 : 12 }}>
          <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>分攤方式</div>
          <div style={{ display: 'flex', gap: 8 }}>
            {[['equal', '均分'], ['solo', '僅我'], ['custom', '自訂金額']].map(([v, label]) => (
              <button key={v} onClick={() => handleSplitMethod(v)} style={{
                flex: 1, padding: '9px 8px', borderRadius: 10, fontSize: 12, fontWeight: 600,
                border: `2px solid ${splitMethod === v ? TABI_COLORS.ink : TABI_COLORS.line}`,
                background: splitMethod === v ? TABI_COLORS.ink : 'transparent',
                color: splitMethod === v ? '#fff' : TABI_COLORS.ink2,
                cursor: 'pointer',
              }}>{label}</button>
            ))}
          </div>
        </div>
      )}

      {/* Equal split: member settled toggles */}
      {_equalMemberPanel}

      {/* Custom per-member amount inputs */}
      {isCustom && (
        <div style={{ margin: '10px 0 12px', background: TABI_COLORS.bg2, borderRadius: 14, padding: '12px 14px', border: `1px solid ${TABI_COLORS.line}` }}>
          {trip.members.map(m => {
            const cs = customSplits.find(s => s.memberId === m.id) || { memberId: m.id, amount: '', settled: false };
            const isPayer = m.id === paidBy;
            return (
              <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 10 }}>
                <div style={{ width: 30, height: 30, borderRadius: 15, flexShrink: 0, background: m.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: '#fff' }}>
                  {(m.display_name || '?')[0]}
                </div>
                <div style={{ fontSize: 12.5, fontWeight: 600, color: TABI_COLORS.ink, width: 52, flexShrink: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {m.display_name}
                </div>
                <div style={{ position: 'relative', flex: 1 }}>
                  <span style={{ position: 'absolute', left: 9, top: '50%', transform: 'translateY(-50%)', fontSize: 13, color: TABI_COLORS.ink3, pointerEvents: 'none' }}>¥</span>
                  <input type="number" inputMode="numeric" value={cs.amount} placeholder="0"
                    onChange={e => setCustomSplits(prev => prev.map(s => s.memberId === m.id ? {...s, amount: e.target.value} : s))}
                    style={{ width: '100%', padding: '7px 8px 7px 22px', borderRadius: 9, border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.card, fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' }}
                  />
                </div>
                {isPayer ? (
                  <div style={{ fontSize: 10.5, fontWeight: 700, padding: '4px 8px', borderRadius: 7, background: `${m.color}22`, color: m.color, flexShrink: 0 }}>付款人</div>
                ) : (
                  <button onClick={() => setCustomSplits(prev => prev.map(s => s.memberId === m.id ? {...s, settled: !s.settled} : s))} style={{
                    padding: '5px 9px', borderRadius: 7, border: 'none', cursor: 'pointer', flexShrink: 0,
                    background: cs.settled ? TABI_COLORS.green : `${TABI_COLORS.warn}33`,
                    color: cs.settled ? '#fff' : '#a07020', fontSize: 11, fontWeight: 700,
                  }}>{cs.settled ? '✓ 已付' : '未付'}</button>
                )}
              </div>
            );
          })}
          <div style={{ borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 9, marginTop: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <span style={{ fontSize: 12, color: TABI_COLORS.ink3 }}>合計</span>
            <span style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.ink }}>¥{customTotal.toLocaleString()}</span>
          </div>
        </div>
      )}

      {/* Proxy toggle — personal only */}
      {visibility === 'personal' && (
        <>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 10, marginBottom: isProxy ? 12 : 4,
            padding: '10px 12px', borderRadius: 10, background: TABI_COLORS.bg2,
            border: `1px solid ${TABI_COLORS.line}`,
          }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink }}>代購</div>
              <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 1 }}>幫別人代為購買</div>
            </div>
            <div onClick={() => setIsProxy(!isProxy)} style={{
              width: 44, height: 24, borderRadius: 12, cursor: 'pointer',
              background: isProxy ? TABI_COLORS.red : TABI_COLORS.line2,
              position: 'relative', transition: 'background 0.2s', flexShrink: 0,
            }}>
              <div style={{
                position: 'absolute', top: 2, left: isProxy ? 22 : 2,
                width: 20, height: 20, borderRadius: 10, background: '#fff',
                transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
              }} />
            </div>
          </div>
          {isProxy && (
            <div style={{ marginBottom: 12 }}>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>代購對象</div>
              <input value={proxyFor} onChange={e => setProxyFor(e.target.value)}
                placeholder="誰的代購品？" style={inputStyle} />
            </div>
          )}
        </>
      )}

      {/* Items detail (optional) */}
      <div style={{ marginTop: 4, borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: showItems ? 10 : 0 }}>
          <div style={{ flex: 1, fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600 }}>明細（選填）</div>
          <button onClick={() => setShowItems(!showItems)} style={{
            padding: '4px 10px', borderRadius: 8, border: `1px solid ${TABI_COLORS.line}`,
            background: 'transparent', fontSize: 12, color: TABI_COLORS.ink2, cursor: 'pointer',
          }}>{showItems ? '收起' : '+ 展開'}</button>
        </div>
        {showItems && (
          <>
            {itemRows.map((row, i) => (
              <div key={i} style={{ display: 'flex', gap: 6, marginBottom: 8, alignItems: 'center' }}>
                <input value={row.name} onChange={e => updateItemRow(i, 'name', e.target.value)}
                  placeholder="品項名稱" style={{ ...inputStyle, flex: 3, padding: '8px 10px', fontSize: 16 }} />
                <input type="number" value={row.qty} onChange={e => updateItemRow(i, 'qty', e.target.value)}
                  placeholder="數" style={{ ...inputStyle, flex: 1, padding: '8px 10px', fontSize: 16 }} inputMode="numeric" />
                <input type="number" value={row.price} onChange={e => updateItemRow(i, 'price', e.target.value)}
                  placeholder="金額" style={{ ...inputStyle, flex: 2, padding: '8px 10px', fontSize: 16 }} inputMode="numeric" />
                <button onClick={() => removeItemRow(i)} style={{
                  background: 'none', border: 'none', fontSize: 18, cursor: 'pointer',
                  color: TABI_COLORS.ink3, padding: '0 2px', flexShrink: 0,
                }}>×</button>
              </div>
            ))}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 2 }}>
              <button onClick={addItemRow} style={{
                padding: '4px 10px', borderRadius: 8, border: `1px solid ${TABI_COLORS.line}`,
                background: 'transparent', fontSize: 12, color: TABI_COLORS.ink2, cursor: 'pointer',
              }}>+ 新增</button>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3 }}>
                小計：{_fmtAmt(itemRows.reduce((s, r) => s + (parseInt(r.price) || 0) * (parseInt(r.qty) || 1), 0), trip.currency)}
              </div>
            </div>
          </>
        )}
      </div>
    </SheetWrap>
  );
}


// ── Create Trip Sheet ─────────────────────────────────────────

const MEMBER_PALETTE = ['#1A4D8F', '#5B7553', '#D9A441', '#7B5EA7', '#2E8B8B', '#B85C38'];

function CreateTripSheet({ onClose, onCreated }) {
  const [name, setName]         = React.useState('');
  const [destination, setDest]  = React.useState('');
  const [startDate, setStart]   = React.useState('');
  const [endDate, setEnd]       = React.useState('');
  const [currency, setCurrency] = React.useState('JPY');
  const [members, setMembers]   = React.useState([{ name: '', color: MEMBER_PALETTE[0] }]);
  const [saving, setSaving]     = React.useState(false);
  const [error, setError]       = React.useState('');

  const inputStyle = {
    width: '100%', padding: '10px 12px', borderRadius: 10,
    border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
    fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit',
    boxSizing: 'border-box',
  };

  function addMember() {
    setMembers(prev => [...prev, { name: '', color: MEMBER_PALETTE[prev.length % MEMBER_PALETTE.length] }]);
  }
  function removeMember(i) {
    setMembers(prev => prev.filter((_, idx) => idx !== i));
  }
  function updateMember(i, name) {
    setMembers(prev => prev.map((m, idx) => idx === i ? { ...m, name } : m));
  }

  async function create() {
    if (!name.trim()) { setError('請輸入旅行名稱'); return; }
    setSaving(true);
    try {
      const initial_members = members.filter(m => m.name.trim()).map(m => ({ display_name: m.name.trim(), color: m.color }));
      const r = await fetch('/api/trips', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify({
          name: name.trim(), destination: destination.trim() || null,
          currency, start_date: startDate || null, end_date: endDate || null,
          initial_members,
        }),
      });
      if (!r.ok) throw new Error(await r.text());
      const { id } = await r.json();
      onCreated(id);
    } catch (e) {
      setError('建立失敗：' + e.message);
      setSaving(false);
    }
  }

  return (
    <SheetWrap title="新增旅行" onClose={onClose} action={
      <>
        {error && <div style={{ color: TABI_COLORS.red, fontSize: 12.5, marginBottom: 8 }}>{error}</div>}
        <button onClick={create} disabled={saving} style={{
          width: '100%', padding: '14px', borderRadius: 14, border: 'none',
          background: saving ? TABI_COLORS.line2 : TABI_COLORS.ink,
          color: '#fff', fontSize: 15, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer',
        }}>{saving ? '建立中…' : '建立旅行'}</button>
      </>
    }>
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>旅行名稱</div>
        <input value={name} onChange={e => setName(e.target.value)} placeholder="例：東京春櫻 5 日" style={inputStyle} />
      </div>
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>目的地</div>
        <input value={destination} onChange={e => setDest(e.target.value)} placeholder="例：東京、大阪" style={inputStyle} />
      </div>
      <div style={{ marginBottom: 12 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 6, fontWeight: 600 }}>旅行日期</div>
        <div style={{ border: `1px solid ${TABI_COLORS.line}`, borderRadius: 10, background: TABI_COLORS.bg, overflow: 'hidden' }}>
          <label style={{ display: 'flex', alignItems: 'center', padding: '13px 14px', borderBottom: `1px solid ${TABI_COLORS.line}`, cursor: 'pointer', position: 'relative', minHeight: 48 }}>
            <div style={{ fontSize: 14, color: TABI_COLORS.ink2, flex: 1, pointerEvents: 'none' }}>出發日</div>
            <div style={{ fontSize: 14, color: startDate ? TABI_COLORS.ink : TABI_COLORS.ink3, pointerEvents: 'none' }}>
              {startDate || '選擇日期 ›'}
            </div>
            <input type="date" value={startDate} onChange={e => setStart(e.target.value)} style={{
              position: 'absolute', inset: 0, width: '100%', height: '100%',
              opacity: 0, cursor: 'pointer', fontSize: 16,
            }} />
          </label>
          <label style={{ display: 'flex', alignItems: 'center', padding: '13px 14px', cursor: 'pointer', position: 'relative', minHeight: 48 }}>
            <div style={{ fontSize: 14, color: TABI_COLORS.ink2, flex: 1, pointerEvents: 'none' }}>回程日</div>
            <div style={{ fontSize: 14, color: endDate ? TABI_COLORS.ink : TABI_COLORS.ink3, pointerEvents: 'none' }}>
              {endDate || '選擇日期 ›'}
            </div>
            <input type="date" value={endDate} onChange={e => setEnd(e.target.value)} style={{
              position: 'absolute', inset: 0, width: '100%', height: '100%',
              opacity: 0, cursor: 'pointer', fontSize: 16,
            }} />
          </label>
        </div>
      </div>
      <div style={{ marginBottom: 16 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>幣別</div>
        <select value={currency} onChange={e => setCurrency(e.target.value)} style={inputStyle}>
          <option value="JPY">JPY 日圓</option>
          <option value="TWD">TWD 台幣</option>
          <option value="USD">USD 美元</option>
        </select>
      </div>

      {/* Member list */}
      <div style={{ borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 14 }}>
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 10 }}>
          <div style={{ flex: 1, fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600 }}>旅伴（可日後邀請替換）</div>
          <button onClick={addMember} style={{
            padding: '4px 10px', borderRadius: 8, border: `1px solid ${TABI_COLORS.line}`,
            background: 'transparent', fontSize: 12, color: TABI_COLORS.ink2, cursor: 'pointer',
          }}>+ 新增</button>
        </div>
        {members.map((m, i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
            <div style={{
              width: 32, height: 32, borderRadius: 16, background: m.color, flexShrink: 0,
              display: 'grid', placeItems: 'center', color: '#fff', fontWeight: 700, fontSize: 13,
            }}>{m.name?.[0] || '?'}</div>
            <input
              value={m.name} onChange={e => updateMember(i, e.target.value)}
              placeholder={`旅伴 ${i + 1} 名稱`}
              style={{ ...inputStyle, flex: 1, padding: '8px 12px' }}
            />
            <button onClick={() => removeMember(i)} style={{
              background: 'none', border: 'none', fontSize: 20, cursor: 'pointer',
              color: TABI_COLORS.ink3, flexShrink: 0, padding: '0 2px',
            }}>×</button>
          </div>
        ))}
        <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 4, lineHeight: 1.5 }}>
          旅伴還未加入時先佔位，邀請後可認領帳號，分攤人數即時正確
        </div>
      </div>
    </SheetWrap>
  );
}


// ── Join Trip Sheet ───────────────────────────────────────────

function JoinTripSheet({ onClose, onJoined, initialCode }) {
  const [code, setCode]       = React.useState(initialCode || '');
  const [preview, setPreview] = React.useState(null);  // trip preview from server
  const [claimId, setClaimId] = React.useState(null);  // selected member to claim
  const [loading, setLoading] = React.useState(false);
  const [saving, setSaving]   = React.useState(false);
  const [error, setError]     = React.useState('');

  React.useEffect(() => {
    if (initialCode) lookup();
  }, []);

  async function lookup() {
    if (!code.trim() && !initialCode) { setError('請輸入邀請碼'); return; }
    const lookupCode = (code.trim() || initialCode || '').toUpperCase();
    setLoading(true); setError('');
    try {
      const r = await fetch(`/api/trips/preview?code=${lookupCode}`, {
        headers: { Authorization: `Bearer ${window.tabiToken()}` },
      });
      if (!r.ok) {
        const d = await r.json();
        throw new Error(d.detail || '查詢失敗');
      }
      const data = await r.json();
      if (data.already_member) throw new Error('你已經是這個旅行的成員');
      setPreview(data);
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }

  async function join() {
    setSaving(true); setError('');
    try {
      const r = await fetch('/api/trips/join', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify({ code: (code.trim() || initialCode || '').toUpperCase(), claim_member_id: claimId || null }),
      });
      if (!r.ok) {
        const d = await r.json();
        throw new Error(d.detail || '加入失敗');
      }
      const data = await r.json();
      onJoined(data.trip_id, data.trip_name);
    } catch (e) {
      setError(e.message);
      setSaving(false);
    }
  }

  return (
    <SheetWrap title="加入旅行" onClose={onClose} action={
      <>
        {error && <div style={{ color: TABI_COLORS.red, fontSize: 12.5, marginBottom: 8 }}>{error}</div>}
        {!preview
          ? <button onClick={lookup} disabled={loading} style={{
              width: '100%', padding: '14px', borderRadius: 14, border: 'none',
              background: loading ? TABI_COLORS.line2 : TABI_COLORS.ink,
              color: '#fff', fontSize: 15, fontWeight: 700, cursor: loading ? 'not-allowed' : 'pointer',
            }}>{loading ? '查詢中…' : '查詢邀請碼'}</button>
          : <button onClick={join} disabled={saving} style={{
              width: '100%', padding: '14px', borderRadius: 14, border: 'none',
              background: saving ? TABI_COLORS.line2 : TABI_COLORS.red,
              color: '#fff', fontSize: 15, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer',
            }}>{saving ? '加入中…' : '確認加入'}</button>
        }
      </>
    }>
      {/* Step 1: enter code */}
      <div style={{ marginBottom: 14 }}>
        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 6, fontWeight: 600 }}>邀請碼</div>
        <input
          value={code} onChange={e => { setCode(e.target.value.toUpperCase()); setPreview(null); setError(''); }}
          placeholder="例：AB12CD34"
          style={{
            width: '100%', padding: '14px 12px', borderRadius: 10,
            border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
            fontSize: 20, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'monospace',
            letterSpacing: '0.18em', textAlign: 'center', boxSizing: 'border-box',
          }}
        />
      </div>

      {/* Step 2: preview + claim */}
      {preview && (
        <>
          <div style={{
            background: TABI_COLORS.card, borderRadius: 12, padding: '12px 14px', marginBottom: 14,
            border: `1px solid ${TABI_COLORS.line}`,
          }}>
            <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700 }}>{preview.trip_name}</div>
            {preview.destination && <div style={{ fontSize: 12, color: TABI_COLORS.ink3, marginTop: 2 }}>{preview.destination}</div>}
          </div>

          {preview.unclaimed_members.length > 0 && (
            <div>
              <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink, marginBottom: 4 }}>你是其中哪位旅伴？</div>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 10, lineHeight: 1.5 }}>
                選擇對應的佔位名稱，你的帳號將認領該位置；或跳過直接加入
              </div>
              {preview.unclaimed_members.map(m => (
                <div key={m.id} onClick={() => setClaimId(claimId === m.id ? null : m.id)} style={{
                  display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px',
                  borderRadius: 10, marginBottom: 8, cursor: 'pointer',
                  border: `2px solid ${claimId === m.id ? m.color : TABI_COLORS.line}`,
                  background: claimId === m.id ? `${m.color}15` : TABI_COLORS.card,
                }}>
                  <div style={{
                    width: 32, height: 32, borderRadius: 16, background: m.color,
                    display: 'grid', placeItems: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0,
                  }}>{m.initial}</div>
                  <div style={{ fontWeight: 600, fontSize: 14, color: TABI_COLORS.ink }}>{m.display_name}</div>
                  {claimId === m.id && <div style={{ marginLeft: 'auto', color: m.color, fontSize: 18 }}>✓</div>}
                </div>
              ))}
              <button onClick={() => setClaimId(null)} style={{
                width: '100%', padding: '8px', borderRadius: 10, marginTop: 4,
                border: `1px dashed ${TABI_COLORS.line2}`, background: 'transparent',
                fontSize: 12.5, color: TABI_COLORS.ink3, cursor: 'pointer',
              }}>{claimId ? '不認領，直接加入' : '✓ 以新成員身份加入'}</button>
            </div>
          )}
        </>
      )}
    </SheetWrap>
  );
}


// ── Receipt Scan Sheet ────────────────────────────────────────

function ScanReceiptSheet({ trip: propTrip, trips, onClose, onSaved, onTripCreated }) {
  const [scanning, setScanning]       = React.useState(false);
  const [result, setResult]           = React.useState(null);
  const [itemStates, setItemStates]           = React.useState([]);
  const [itemAssignments, setItemAssignments] = React.useState([]);
  const [memberSettled, setMemberSettled]     = React.useState({});
  const [editableItems, setEditableItems]     = React.useState([]);
  const [sharedSplitMethod, setSharedSplitMethod] = React.useState('equal');
  const [saving, setSaving]           = React.useState(false);
  const [scope, setScope]             = React.useState('personal');
  const [category, setCategory]       = React.useState('餐飲');
  const [paidBy, setPaidBy]           = React.useState(propTrip?.members[0]?.id || '');
  const [showCreateTrip, setShowCreateTrip] = React.useState(false);
  const [error, setError]             = React.useState('');
  // Home-screen trip selection
  const [selectedTripId, setSelectedTripId]     = React.useState(null);
  const [selectedTripData, setSelectedTripData] = React.useState(null);
  const [loadingTrip, setLoadingTrip]           = React.useState(false);
  const cameraRef  = React.useRef();
  const galleryRef = React.useRef();

  // Effective trip object — from prop (trip detail) or fetched from home selection
  const trip = propTrip || selectedTripData;
  const tripId = trip?.id;

  React.useEffect(() => {
    if (trip?.members?.length > 0) setPaidBy(trip.members[0].id);
  }, [trip?.id]);

  async function onSelectTrip(id) {
    setSelectedTripId(id);
    setSelectedTripData(null);
    if (!id) return;
    setLoadingTrip(true);
    try {
      const r = await fetch(`/api/trips/${id}`, { headers: { Authorization: `Bearer ${window.tabiToken()}` } });
      if (r.ok) setSelectedTripData(await r.json());
    } catch (_) {}
    finally { setLoadingTrip(false); }
  }

  function initItemStates(items) {
    return (items || []).map(() => ({ isProxy: false, proxyFor: '' }));
  }
  function setItemProxy(i, isProxy) {
    setItemStates(prev => prev.map((s, idx) => idx === i ? { ...s, isProxy } : s));
  }
  function setItemProxyFor(i, proxyFor) {
    setItemStates(prev => prev.map((s, idx) => idx === i ? { ...s, proxyFor } : s));
  }
  function addEditableItem() {
    setEditableItems(prev => [...prev, { translated_name: '', qty: 1, subtotal: 0 }]);
    setItemStates(prev => [...prev, { isProxy: false, proxyFor: '' }]);
    setItemAssignments(prev => [...prev, null]);
  }
  function removeEditableItem(i) {
    setEditableItems(prev => prev.filter((_, idx) => idx !== i));
    setItemStates(prev => prev.filter((_, idx) => idx !== i));
    setItemAssignments(prev => prev.filter((_, idx) => idx !== i));
  }
  function updateEditableItem(i, field, val) {
    setEditableItems(prev => prev.map((it, idx) => idx === i ? { ...it, [field]: val } : it));
  }

  async function handleFile(file) {
    if (!file) return;
    setScanning(true); setError(''); setResult(null); setItemStates([]);
    try {
      const fd = new FormData();
      fd.append('image', file);
      const r = await fetch('/api/scan/receipt', {
        method: 'POST',
        headers: { Authorization: `Bearer ${window.tabiToken()}`, ...window.tabiLLMHeaders() },
        body: fd,
      });
      if (!r.ok) {
        const d = await r.json();
        if (d.detail === 'NOT_A_RECEIPT') throw new Error('這不是收據，請重新拍攝');
        throw new Error(d.detail || '掃描失敗');
      }
      const data = await r.json();
      setResult(data);
      setItemStates(initItemStates(data.items));
      setItemAssignments((data.items || []).map(() => null));
      setMemberSettled({});
      setEditableItems((data.items || []).map(it => ({
        translated_name: it.translated_name || it.original_name || '',
        qty: it.qty || 1,
        subtotal: it.subtotal || 0,
      })));
      setSharedSplitMethod('equal');
      if (data.scope) setScope(data.scope);
      if (data.category) setCategory(data.category);
    } catch (e) {
      setError(e.message);
    } finally {
      setScanning(false);
    }
  }

  async function _postExpense(tripId, body) {
    const r = await fetch(`/api/trips/${tripId}/expenses`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(await r.text());
  }

  async function saveReceipt() {
    if (!result) return;
    setSaving(true);
    try {
      // 1. Save receipt record
      const rr = await fetch('/api/receipts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify({
          image_url:    result.image_url,
          store_name:   result.store_name,
          total_amount: editableItems.reduce((s, it) => s + (parseFloat(it.subtotal) || 0), 0) || result.total_amount,
          tax_amount:   result.tax_amount,
          receipt_date: result.receipt_date,
          items:        editableItems.map(it => ({ original_name: it.translated_name, translated_name: it.translated_name, qty: it.qty, subtotal: parseFloat(it.subtotal) || 0 })),
        }),
      });
      if (!rr.ok) throw new Error(await rr.text());
      const { id: receiptId } = await rr.json();

      if (tripId) {
        // 2. Link receipt to trip
        await fetch(`/api/trips/${tripId}/receipts`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
          body: JSON.stringify({ receipt_id: receiptId, scope }),
        });

        const items = editableItems;
        const editedTotal = items.reduce((s, it) => s + (parseFloat(it.subtotal) || 0), 0);
        const expDate = result.receipt_date
          ? new Date(result.receipt_date).toISOString()
          : new Date().toISOString();
        const base = {
          currency:     trip.currency,
          paid_by:      paidBy,
          expense_date: expDate,
          receipt_id:   receiptId,
        };
        const toItemRow = it => ({ original_name: it.translated_name, translated_name: it.translated_name, qty: it.qty, subtotal: parseFloat(it.subtotal) || 0 });

        if (scope === 'shared') {
          // ── Shared mode ──
          const settledMembers = trip.members.filter(m => memberSettled[m.id]).map(m => m.id);

          if (sharedSplitMethod === 'equal') {
            await _postExpense(tripId, {
              ...base,
              title:        result.store_name || '收據支出',
              amount:       editedTotal,
              category,     icon: CAT_ICONS[category] || '💳',
              split_method: settledMembers.length > 0 ? 'custom' : 'equal',
              visibility:   'shared',
              is_proxy:     false, proxy_for: null,
              splits:       settledMembers.length > 0
                ? Object.fromEntries(trip.members.map(m => [m.id, Math.round(editedTotal / trip.members.length)]))
                : undefined,
              settled_members: settledMembers,
              items:        items.length > 0 ? items.map(toItemRow) : null,
            });
          } else {
            // Custom (per-item assignment)
            if (items.length === 0) {
              await _postExpense(tripId, {
                ...base,
                title:        result.store_name || '收據支出',
                amount:       editedTotal,
                category,     icon: CAT_ICONS[category] || '💳',
                split_method: 'equal',
                visibility:   'shared',
                is_proxy:     false, proxy_for: null,
                items:        null,
              });
            } else {
              const memberAmounts = {};
              const unassignedTotal = items.reduce((s, item, i) => {
                if (itemAssignments[i] === null) return s + (parseFloat(item.subtotal) || 0);
                const mid = itemAssignments[i];
                memberAmounts[mid] = (memberAmounts[mid] || 0) + (parseFloat(item.subtotal) || 0);
                return s;
              }, 0);
              if (unassignedTotal > 0 && trip.members.length > 0) {
                const perMember = unassignedTotal / trip.members.length;
                trip.members.forEach(m => { memberAmounts[m.id] = (memberAmounts[m.id] || 0) + perMember; });
              }
              const splits = {};
              Object.entries(memberAmounts).forEach(([mid, a]) => {
                if (a > 0) splits[mid] = Math.round(a * 100) / 100;
              });
              await _postExpense(tripId, {
                ...base,
                title:        result.store_name || '收據支出',
                amount:       editedTotal,
                category,     icon: CAT_ICONS[category] || '💳',
                split_method: Object.keys(splits).length > 0 ? 'custom' : 'equal',
                visibility:   'shared',
                is_proxy:     false, proxy_for: null,
                splits:       Object.keys(splits).length > 0 ? splits : undefined,
                settled_members: settledMembers,
                items:        items.map(toItemRow),
              });
            }
          }
        } else {
          // ── Personal mode: proxy groups ──
          if (items.length === 0) {
            await _postExpense(tripId, {
              ...base,
              title:        result.store_name || '收據支出',
              amount:       editedTotal || result.total_amount || 0,
              category,     icon: CAT_ICONS[category] || '💳',
              split_method: 'solo',
              visibility:   'personal',
              is_proxy:     false, proxy_for: null,
              items:        null,
            });
          } else {
            const normalItems = items.filter((_, i) => !itemStates[i]?.isProxy);
            const proxyGroups = {};
            items.forEach((item, i) => {
              if (itemStates[i]?.isProxy) {
                const person = itemStates[i].proxyFor.trim() || '代購';
                if (!proxyGroups[person]) proxyGroups[person] = [];
                proxyGroups[person].push(item);
              }
            });
            const hasProxy = Object.keys(proxyGroups).length > 0;
            const normalSubtotal = normalItems.reduce((s, it) => s + (parseFloat(it.subtotal) || 0), 0);
            const normalAmt = hasProxy ? normalSubtotal : (editedTotal || result.total_amount || normalSubtotal);
            if (normalAmt > 0) {
              await _postExpense(tripId, {
                ...base,
                title:        result.store_name || '收據支出',
                amount:       normalAmt,
                category,     icon: CAT_ICONS[category] || '💳',
                split_method: 'solo',
                visibility:   'personal',
                is_proxy:     false, proxy_for: null,
                items:        normalItems.map(toItemRow),
              });
            }
            for (const [person, proxyItems] of Object.entries(proxyGroups)) {
              const a = proxyItems.reduce((s, it) => s + (parseFloat(it.subtotal) || 0), 0);
              if (a > 0) {
                await _postExpense(tripId, {
                  ...base,
                  title:        `${result.store_name || '收據'} (代購)`,
                  amount:       a,
                  category:     '代購',  icon: CAT_ICONS['代購'],
                  split_method: 'solo',
                  visibility:   'personal',
                  is_proxy:     true,
                  proxy_for:    person,
                  items:        proxyItems.map(toItemRow),
                });
              }
            }
          }
        }
      }
      onSaved();
    } catch (e) {
      setError('儲存失敗：' + e.message);
      setSaving(false);
    }
  }

  const footerAction = result ? (
    <>
      {error && <div style={{ color: TABI_COLORS.red, fontSize: 12.5, marginBottom: 8 }}>{error}</div>}
      <div style={{ display: 'flex', gap: 10 }}>
        <button onClick={() => { setResult(null); setItemStates([]); setItemAssignments([]); setMemberSettled({}); setEditableItems([]); setSharedSplitMethod('equal'); setError(''); setScope('personal'); setCategory('餐飲'); }} style={{
          flex: 1, padding: '13px', borderRadius: 12, border: `1px solid ${TABI_COLORS.line}`,
          background: 'transparent', fontSize: 14, cursor: 'pointer', color: TABI_COLORS.ink2,
        }}>重新拍攝</button>
        <button onClick={saveReceipt} disabled={saving || (trips && !propTrip && !tripId)} style={{
          flex: 2, padding: '13px', borderRadius: 12, border: 'none',
          background: (saving || (trips && !propTrip && !tripId)) ? TABI_COLORS.line2 : TABI_COLORS.ink,
          color: '#fff', fontSize: 14, fontWeight: 700,
          cursor: (saving || (trips && !propTrip && !tripId)) ? 'not-allowed' : 'pointer',
        }}>{saving ? '儲存中…' : (trips && !propTrip && !tripId) ? '請選擇旅程' : '儲存並新增支出'}</button>
      </div>
    </>
  ) : null;

  return (
    <SheetWrap title="掃描發票" onClose={onClose} action={footerAction}>
      <input ref={cameraRef} type="file" accept="image/*" capture="environment"
        style={{ display: 'none' }} onChange={e => { handleFile(e.target.files[0]); e.target.value = ''; }} />
      <input ref={galleryRef} type="file" accept="image/*"
        style={{ display: 'none' }} onChange={e => { handleFile(e.target.files[0]); e.target.value = ''; }} />

      {/* Home mode with no trips at all */}
      {trips && !propTrip && trips.length === 0 && !selectedTripId && (
        <div style={{ textAlign: 'center', padding: '44px 20px', color: TABI_COLORS.ink3 }}>
          <div style={{ fontSize: 40, marginBottom: 12 }}>✈️</div>
          <div style={{ fontFamily: tabiSerif, fontSize: 16, color: TABI_COLORS.ink2, marginBottom: 8 }}>還沒有旅程</div>
          <div style={{ fontSize: 12.5, lineHeight: 1.7, marginBottom: 22 }}>掃描發票需要指定旅程</div>
          <button onClick={() => setShowCreateTrip(true)} style={{
            padding: '12px 28px', borderRadius: 14, border: 'none', cursor: 'pointer',
            background: TABI_COLORS.ink, color: '#fff', fontSize: 14, fontWeight: 700,
          }}>新增旅程</button>
        </div>
      )}

      {showCreateTrip && ReactDOM.createPortal(
        <CreateTripSheet
          onClose={() => setShowCreateTrip(false)}
          onCreated={async (id) => {
            setShowCreateTrip(false);
            onTripCreated?.();
            await onSelectTrip(id);
          }}
        />,
        document.body
      )}

      {(!trips || propTrip || trips.length > 0 || selectedTripId) && !result && !scanning && (
        <>
          <div style={{
            width: '100%', padding: '40px 16px', borderRadius: 16,
            border: `2px dashed ${TABI_COLORS.line2}`,
            background: TABI_COLORS.bg2, textAlign: 'center', marginBottom: 14,
          }}>
            <div style={{ fontSize: 44, marginBottom: 10 }}>🧾</div>
            <div style={{ fontFamily: tabiSerif, fontSize: 15, color: TABI_COLORS.ink }}>選擇發票圖片</div>
            <div style={{ fontSize: 12, color: TABI_COLORS.ink3, marginTop: 4 }}>支援 JPG / PNG / HEIC</div>
          </div>
          <div style={{ display: 'flex', gap: 10 }}>
            <button onClick={() => cameraRef.current?.click()} style={{
              flex: 1, padding: '13px', borderRadius: 12,
              border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.card,
              color: TABI_COLORS.ink, fontSize: 13, fontWeight: 600, cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7,
            }}>
              <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
                <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
                <circle cx="12" cy="13" r="4"/>
              </svg>
              拍照
            </button>
            <button onClick={() => galleryRef.current?.click()} style={{
              flex: 1, padding: '13px', borderRadius: 12,
              border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.card,
              color: TABI_COLORS.ink, fontSize: 13, fontWeight: 600, cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7,
            }}>
              <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
                <rect x="3" y="3" width="18" height="18" rx="2"/>
                <circle cx="8.5" cy="8.5" r="1.5"/>
                <polyline points="21 15 16 10 5 21"/>
              </svg>
              相簿
            </button>
          </div>
          {error && <div style={{ color: TABI_COLORS.red, fontSize: 13, textAlign: 'center', marginTop: 12 }}>{error}</div>}
        </>
      )}

      {scanning && (
        <div style={{ textAlign: 'center', padding: '60px 0' }}>
          <div style={{ fontSize: 36, marginBottom: 12, animation: 'tabiSpin 1s linear infinite', display: 'inline-block' }}>⏳</div>
          <div style={{ fontSize: 14, color: TABI_COLORS.ink2 }}>辨識中…</div>
        </div>
      )}

      {result && (
        <>
          {/* Photo thumbnail */}
          {result.image_url && (
            <img src={result.image_url} alt="收據" style={{
              width: '100%', borderRadius: 10, marginBottom: 12,
              maxHeight: 140, objectFit: 'cover', objectPosition: 'top',
            }} />
          )}

          {/* Store header */}
          <div style={{ marginBottom: 12 }}>
            <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700 }}>
              {result.store_name || '未知店家'}
            </div>
            {result.receipt_date && (
              <div style={{ fontSize: 12, color: TABI_COLORS.ink3, marginTop: 2 }}>{result.receipt_date}</div>
            )}
          </div>

          {/* Trip selector (home mode) */}
          {trips && !propTrip && (
            <div style={{ marginBottom: 12 }}>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>選擇旅程</div>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                {trips.map(t => (
                  <button key={t.id} onClick={() => onSelectTrip(t.id)} style={{
                    padding: '6px 14px', borderRadius: 20, fontSize: 13, fontWeight: 600,
                    border: `2px solid ${selectedTripId === t.id ? TABI_COLORS.red : TABI_COLORS.line}`,
                    background: selectedTripId === t.id ? `${TABI_COLORS.red}15` : 'transparent',
                    color: selectedTripId === t.id ? TABI_COLORS.red : TABI_COLORS.ink2, cursor: 'pointer',
                  }}>{t.name}</button>
                ))}
              </div>
              {loadingTrip && <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginTop: 6 }}>載入旅程資訊…</div>}
            </div>
          )}

          {/* Category + Scope + Content — only when trip is known */}
          {trip && (
            <>
              {/* Category picker */}
              <div style={{ marginBottom: 10 }}>
                <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 }}>
                  支出分類
                  <span style={{ fontSize: 10, padding: '1px 6px', borderRadius: 20, background: `${TABI_COLORS.green}20`, color: TABI_COLORS.green, fontWeight: 700 }}>AI 建議</span>
                </div>
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                  {['餐飲','交通','住宿','景點','購物','其他'].map(c => (
                    <button key={c} onClick={() => setCategory(c)} style={{
                      padding: '6px 12px', borderRadius: 20, fontSize: 12.5, fontWeight: 600,
                      border: `2px solid ${category === c ? TABI_COLORS.ink : TABI_COLORS.line}`,
                      background: category === c ? TABI_COLORS.ink : 'transparent',
                      color: category === c ? '#fff' : TABI_COLORS.ink2, cursor: 'pointer',
                    }}>{CAT_ICONS[c] || ''} {c}</button>
                  ))}
                </div>
              </div>

              {/* Scope toggle */}
              <div style={{ marginBottom: 14 }}>
                <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 }}>
                  支出類型
                  <span style={{ fontSize: 10, padding: '1px 6px', borderRadius: 20, background: `${TABI_COLORS.green}20`, color: TABI_COLORS.green, fontWeight: 700 }}>AI 建議</span>
                </div>
                <div style={{ display: 'flex', gap: 8 }}>
                  {[['personal', '個人'], ['shared', '共同分帳']].map(([v, label]) => (
                    <button key={v} onClick={() => setScope(v)} style={{
                      flex: 1, padding: '9px 8px', borderRadius: 10, fontSize: 13, fontWeight: 600,
                      border: `2px solid ${scope === v ? TABI_COLORS.red : TABI_COLORS.line}`,
                      background: scope === v ? `${TABI_COLORS.red}15` : 'transparent',
                      color: scope === v ? TABI_COLORS.red : TABI_COLORS.ink2, cursor: 'pointer',
                    }}>{label}</button>
                  ))}
                </div>
              </div>

              {/* ── Editable item list (shared between both modes) ── */}
              {(() => {
                const editedTotal = editableItems.reduce((s, it) => s + (parseFloat(it.subtotal) || 0), 0);
                return (
                  <div style={{
                    background: TABI_COLORS.card, borderRadius: 12,
                    border: `1px solid ${TABI_COLORS.line}`, marginBottom: 14, overflow: 'hidden',
                  }}>
                    <div style={{ padding: '8px 12px', background: TABI_COLORS.bg2, borderBottom: `1px solid ${TABI_COLORS.line}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                      <span style={{ fontSize: 11, fontWeight: 700, color: TABI_COLORS.ink3, letterSpacing: '0.08em' }}>
                        {scope === 'personal' ? '明細 — 點「代購」標記代購支出' : '明細'}
                      </span>
                      <span style={{ fontSize: 10.5, color: TABI_COLORS.ink3 }}>可編輯</span>
                    </div>
                    <div style={{ padding: '8px 12px' }}>
                      {editableItems.map((item, i) => {
                        const st = itemStates[i] || { isProxy: false, proxyFor: '' };
                        const assignedId = itemAssignments[i] ?? null;
                        return (
                          <div key={i} style={{ marginBottom: st.isProxy ? 10 : 8 }}>
                            <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                              <input
                                value={item.translated_name}
                                onChange={e => updateEditableItem(i, 'translated_name', e.target.value)}
                                placeholder="品項名稱"
                                style={{
                                  flex: 1, padding: '5px 8px', borderRadius: 7,
                                  border: `1px solid ${st.isProxy ? `${TABI_COLORS.red}50` : TABI_COLORS.line}`,
                                  background: st.isProxy ? `${TABI_COLORS.red}06` : TABI_COLORS.bg,
                                  fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit',
                                }}
                              />
                              <div style={{ position: 'relative', flexShrink: 0, width: 72 }}>
                                <span style={{ position: 'absolute', left: 6, top: '50%', transform: 'translateY(-50%)', fontSize: 11, color: TABI_COLORS.ink3, pointerEvents: 'none' }}>¥</span>
                                <input
                                  type="number" inputMode="numeric"
                                  value={item.subtotal}
                                  onChange={e => updateEditableItem(i, 'subtotal', e.target.value)}
                                  placeholder="0"
                                  style={{
                                    width: '100%', padding: '5px 4px 5px 16px', borderRadius: 7,
                                    border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
                                    fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit',
                                    boxSizing: 'border-box',
                                  }}
                                />
                              </div>
                              {scope === 'personal' && (
                                <button onClick={() => setItemProxy(i, !st.isProxy)} style={{
                                  padding: '3px 8px', borderRadius: 7, fontSize: 11, fontWeight: 700,
                                  border: `1px solid ${st.isProxy ? TABI_COLORS.red : TABI_COLORS.line2}`,
                                  background: st.isProxy ? TABI_COLORS.red : 'transparent',
                                  color: st.isProxy ? '#fff' : TABI_COLORS.ink3,
                                  cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s',
                                }}>代購</button>
                              )}
                              <button onClick={() => removeEditableItem(i)} style={{
                                width: 24, height: 24, borderRadius: 12, border: 'none', cursor: 'pointer', flexShrink: 0,
                                background: TABI_COLORS.line2, color: TABI_COLORS.ink3, display: 'grid', placeItems: 'center', fontSize: 13,
                              }}>×</button>
                            </div>
                            {/* Personal: proxy name input */}
                            {scope === 'personal' && st.isProxy && (
                              <input
                                value={st.proxyFor}
                                onChange={e => setItemProxyFor(i, e.target.value)}
                                placeholder="代購對象（輸入姓名）"
                                style={{
                                  marginTop: 5, width: '100%', padding: '6px 10px', borderRadius: 8,
                                  border: `1px solid ${TABI_COLORS.red}50`, background: `${TABI_COLORS.red}08`,
                                  fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit',
                                  boxSizing: 'border-box',
                                }}
                              />
                            )}
                            {/* Shared custom: member assignment pills */}
                            {scope === 'shared' && sharedSplitMethod === 'custom' && (
                              <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', marginTop: 5 }}>
                                {trip.members.map(m => (
                                  <button key={m.id} onClick={() => setItemAssignments(prev => prev.map((a, idx) => idx === i ? (a === m.id ? null : m.id) : a))} style={{
                                    padding: '2px 8px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                                    border: `1.5px solid ${assignedId === m.id ? m.color : TABI_COLORS.line2}`,
                                    background: assignedId === m.id ? m.color : 'transparent',
                                    color: assignedId === m.id ? '#fff' : TABI_COLORS.ink3,
                                    cursor: 'pointer', transition: 'all 0.15s',
                                  }}>{m.display_name}</button>
                                ))}
                              </div>
                            )}
                          </div>
                        );
                      })}
                      <button onClick={addEditableItem} style={{
                        padding: '4px 10px', borderRadius: 8,
                        border: `1px dashed ${TABI_COLORS.line2}`, background: 'transparent',
                        fontSize: 12, color: TABI_COLORS.ink3, cursor: 'pointer', marginBottom: 6,
                      }}>+ 新增項目</button>
                      <div style={{ borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 8, marginTop: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                        <span style={{ fontSize: 13, fontWeight: 700 }}>合計</span>
                        <span style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.red }}>
                          ¥{editedTotal.toLocaleString()}
                        </span>
                      </div>
                    </div>
                  </div>
                );
              })()}

              {/* ── PERSONAL: no extra UI needed ── */}

              {/* ── SHARED mode ── */}
              {scope === 'shared' && (
                <>
                  {/* 誰先付 */}
                  <div style={{ marginBottom: 10 }}>
                    <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>誰先付</div>
                    <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                      {trip.members.map(m => (
                        <button key={m.id} onClick={() => setPaidBy(m.id)} style={{
                          padding: '6px 14px', borderRadius: 20, fontSize: 13, fontWeight: 600,
                          border: `2px solid ${paidBy === m.id ? m.color : TABI_COLORS.line}`,
                          background: paidBy === m.id ? m.color : 'transparent',
                          color: paidBy === m.id ? '#fff' : TABI_COLORS.ink2, cursor: 'pointer',
                        }}>{m.display_name}</button>
                      ))}
                    </div>
                  </div>

                  {/* Split method toggle */}
                  <div style={{ marginBottom: 12 }}>
                    <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>分攤方式</div>
                    <div style={{ display: 'flex', gap: 8 }}>
                      {[['equal', '均分'], ['custom', '個別分配']].map(([v, label]) => (
                        <button key={v} onClick={() => setSharedSplitMethod(v)} style={{
                          flex: 1, padding: '8px', borderRadius: 10, fontSize: 13, fontWeight: 600,
                          border: `2px solid ${sharedSplitMethod === v ? TABI_COLORS.ink : TABI_COLORS.line}`,
                          background: sharedSplitMethod === v ? TABI_COLORS.ink : 'transparent',
                          color: sharedSplitMethod === v ? '#fff' : TABI_COLORS.ink2, cursor: 'pointer',
                        }}>{label}</button>
                      ))}
                    </div>
                  </div>

                  {/* Member summary / settled status */}
                  <div style={{
                    background: TABI_COLORS.card, borderRadius: 12,
                    border: `1px solid ${TABI_COLORS.line}`, marginBottom: 14, overflow: 'hidden',
                  }}>
                    <div style={{ padding: '8px 12px', background: TABI_COLORS.bg2, borderBottom: `1px solid ${TABI_COLORS.line}` }}>
                      <span style={{ fontSize: 11, fontWeight: 700, color: TABI_COLORS.ink3, letterSpacing: '0.08em' }}>
                        {sharedSplitMethod === 'equal' ? '費用分配（均分）' : '費用分配'}
                      </span>
                    </div>
                    <div style={{ padding: '4px 12px 8px' }}>
                      {(() => {
                        const editedTotal = editableItems.reduce((s, it) => s + (parseFloat(it.subtotal) || 0), 0);
                        const perMember = trip.members.length > 0 ? editedTotal / trip.members.length : 0;
                        let anyShown = false;
                        const rows = trip.members.map(m => {
                          const amt = sharedSplitMethod === 'equal'
                            ? perMember
                            : editableItems.reduce((s, it, i) => itemAssignments[i] === m.id ? s + (parseFloat(it.subtotal) || 0) : s, 0);
                          if (sharedSplitMethod === 'custom' && amt === 0) return null;
                          anyShown = true;
                          const settled = !!memberSettled[m.id];
                          return (
                            <div key={m.id} style={{
                              display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0',
                              borderBottom: `1px solid ${TABI_COLORS.line}`,
                            }}>
                              <div style={{
                                width: 26, height: 26, borderRadius: 13, background: m.color,
                                display: 'flex', alignItems: 'center', justifyContent: 'center',
                                fontSize: 11, fontWeight: 700, color: '#fff', flexShrink: 0,
                              }}>{(m.display_name || '?').slice(0, 1)}</div>
                              <div style={{ flex: 1 }}>
                                <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink }}>{m.display_name}</div>
                                <div style={{ fontSize: 12, color: TABI_COLORS.ink3 }}>¥{Math.round(amt).toLocaleString()}</div>
                              </div>
                              <button onClick={() => setMemberSettled(prev => ({ ...prev, [m.id]: !prev[m.id] }))} style={{
                                padding: '5px 12px', borderRadius: 8, fontSize: 12, fontWeight: 600,
                                border: `1.5px solid ${settled ? TABI_COLORS.green : `${TABI_COLORS.warn}66`}`,
                                background: settled ? `${TABI_COLORS.green}15` : `${TABI_COLORS.warn}18`,
                                color: settled ? TABI_COLORS.green : '#a07020', cursor: 'pointer',
                              }}>{settled ? '✓ 已付' : '未付'}</button>
                            </div>
                          );
                        });
                        if (!anyShown && sharedSplitMethod === 'custom') {
                          return <div style={{ fontSize: 12.5, color: TABI_COLORS.ink3, padding: '10px 0', textAlign: 'center' }}>請在明細中為每項選擇負責人</div>;
                        }
                        return rows;
                      })()}
                    </div>
                  </div>
                </>
              )}
            </>
          )}

          {/* Prompt when in home mode and no trip selected yet */}
          {trips && !propTrip && !trip && (
            <div style={{ textAlign: 'center', padding: '16px 0', color: TABI_COLORS.ink3, fontSize: 12.5 }}>
              請選擇旅程以設定分類和支出類型
            </div>
          )}
        </>
      )}
    </SheetWrap>
  );
}


// ── Receipt Detail / Edit Sheet ───────────────────────────────

function ReceiptDetailSheet({ receipt, onClose, onSaved, onDeleted }) {
  const [editing, setEditing]       = React.useState(false);
  const [storeName, setStoreName]   = React.useState(receipt.store_name || '');
  const [totalAmt, setTotalAmt]     = React.useState(String(receipt.total_amount || ''));
  const [receiptDate, setDate]      = React.useState(
    receipt.receipt_date ? receipt.receipt_date.slice(0, 10) : ''
  );
  const [saving, setSaving]         = React.useState(false);
  const [error, setError]           = React.useState('');

  const inputStyle = {
    width: '100%', padding: '10px 12px', borderRadius: 10,
    border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
    fontSize: 16, color: TABI_COLORS.ink, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box',
  };

  async function save() {
    setSaving(true);
    try {
      const r = await fetch(`/api/receipts/${receipt.id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify({
          store_name:   storeName || null,
          total_amount: parseInt(totalAmt, 10) || null,
          receipt_date: receiptDate || null,
        }),
      });
      if (!r.ok) throw new Error(await r.text());
      onSaved();
    } catch (e) {
      setError('儲存失敗：' + e.message);
      setSaving(false);
    }
  }

  async function del() {
    if (!confirm('確定要刪除這張發票？')) return;
    try {
      await fetch(`/api/receipts/${receipt.id}`, {
        method: 'DELETE',
        headers: { Authorization: `Bearer ${window.tabiToken()}` },
      });
      onDeleted();
    } catch (e) {
      setError('刪除失敗：' + e.message);
    }
  }

  const action = editing ? (
    <>
      {error && <div style={{ color: TABI_COLORS.red, fontSize: 12.5, marginBottom: 8 }}>{error}</div>}
      <div style={{ display: 'flex', gap: 10 }}>
        <button onClick={del} style={{
          padding: '14px 18px', borderRadius: 14, border: `1px solid ${TABI_COLORS.line}`,
          background: 'transparent', color: TABI_COLORS.red, fontSize: 15, cursor: 'pointer', flexShrink: 0,
        }}>🗑</button>
        <button onClick={save} disabled={saving} style={{
          flex: 1, padding: '14px', borderRadius: 14, border: 'none',
          background: saving ? TABI_COLORS.line2 : TABI_COLORS.ink,
          color: '#fff', fontSize: 15, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer',
        }}>{saving ? '儲存中…' : '儲存變更'}</button>
      </div>
    </>
  ) : (
    <button onClick={() => setEditing(true)} style={{
      width: '100%', padding: '14px', borderRadius: 14, border: `1px solid ${TABI_COLORS.line}`,
      background: 'transparent', color: TABI_COLORS.ink2, fontSize: 15, fontWeight: 600, cursor: 'pointer',
    }}>編輯發票</button>
  );

  return (
    <SheetWrap title="發票詳情" onClose={onClose} action={action}>
      {/* Original photo */}
      {receipt.image_url && (
        <div style={{ marginBottom: 14 }}>
          <img src={receipt.image_url} alt="原始收據"
            style={{ width: '100%', borderRadius: 12, maxHeight: 260, objectFit: 'contain', background: TABI_COLORS.bg2 }} />
        </div>
      )}

      {!editing ? (
        /* View mode — styled receipt card */
        <div style={{ background: TABI_COLORS.card, borderRadius: 18, overflow: 'hidden', boxShadow: '0 2px 20px rgba(26,22,18,0.08)' }}>
          {/* Header */}
          <div style={{ padding: '18px 18px 14px', background: TABI_COLORS.ink }}>
            <div style={{ fontFamily: tabiSerif, fontSize: 19, fontWeight: 700, color: '#fff', marginBottom: 4 }}>
              {receipt.store_name || '未知店家'}
            </div>
            {receipt.receipt_date && (
              <div style={{ fontSize: 12, color: 'rgba(255,255,255,0.55)' }}>{receipt.receipt_date}</div>
            )}
          </div>

          {/* Items */}
          {(receipt.items || []).length > 0 && (
            <div style={{ padding: '12px 18px 4px' }}>
              {receipt.items.map((item, i) => {
                const showOrig = item.original_name && item.translated_name && item.original_name !== item.translated_name;
                return (
                  <div key={i} style={{
                    display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                    padding: '8px 0',
                    borderBottom: i < receipt.items.length - 1 ? `1px dashed ${TABI_COLORS.line}` : 'none',
                  }}>
                    <div style={{ flex: 1, minWidth: 0, marginRight: 12 }}>
                      <div style={{ fontSize: 13.5, color: TABI_COLORS.ink, fontWeight: 500 }}>
                        {item.translated_name || item.original_name}
                        {item.qty > 1 && <span style={{ color: TABI_COLORS.ink3, fontSize: 12, marginLeft: 5 }}>×{item.qty}</span>}
                      </div>
                      {showOrig && (
                        <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>{item.original_name}</div>
                      )}
                    </div>
                    <div style={{ fontFamily: tabiSerif, fontSize: 14.5, fontWeight: 700, flexShrink: 0 }}>
                      ¥{item.subtotal?.toLocaleString?.() ?? item.subtotal}
                    </div>
                  </div>
                );
              })}
            </div>
          )}

          {/* Total row */}
          <div style={{
            padding: '14px 18px 16px', background: TABI_COLORS.bg2,
            borderTop: `1px solid ${TABI_COLORS.line}`,
            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
            marginTop: (receipt.items || []).length > 0 ? 4 : 0,
          }}>
            <span style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: TABI_COLORS.ink2 }}>合計</span>
            <span style={{ fontFamily: tabiSerif, fontSize: 24, fontWeight: 700, color: TABI_COLORS.red }}>
              ¥{(receipt.total_amount || 0).toLocaleString()}
            </span>
          </div>
        </div>
      ) : (
        /* Edit mode */
        <>
          <div style={{ marginBottom: 12 }}>
            <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>店家名稱</div>
            <input value={storeName} onChange={e => setStoreName(e.target.value)} placeholder="店家名稱" style={inputStyle} />
          </div>
          <div style={{ marginBottom: 12 }}>
            <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>日期</div>
            <input type="date" value={receiptDate} onChange={e => setDate(e.target.value)} style={inputStyle} />
          </div>
          <div style={{ marginBottom: 12 }}>
            <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 5, fontWeight: 600 }}>合計（JPY）</div>
            <input type="number" value={totalAmt} onChange={e => setTotalAmt(e.target.value)} placeholder="0" style={inputStyle} inputMode="numeric" />
          </div>
        </>
      )}
    </SheetWrap>
  );
}


// ── Trips List Screen ─────────────────────────────────────────

function TripsScreen({ go, onOpen }) {
  const [trips, setTrips]     = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [sheet, setSheet]     = React.useState(null);  // 'create' | 'join'

  async function load() {
    setLoading(true);
    try {
      const r = await fetch('/api/trips', { headers: { Authorization: `Bearer ${window.tabiToken()}` } });
      if (r.ok) setTrips(await r.json());
    } finally {
      setLoading(false);
    }
  }

  React.useEffect(() => { load(); }, []);

  return (
    <div style={{ background: TABI_COLORS.bg, minHeight: '100%' }}>
      <TabiTopBar
        title="旅行記帳"
        subtitle="TRIP · 家計簿"
        onBack={() => go('home')}
        right={
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={() => setSheet('join')} style={{
              padding: '7px 10px', borderRadius: 10, border: `1px solid ${TABI_COLORS.line}`,
              background: 'transparent', color: TABI_COLORS.ink2, fontSize: 11.5, fontWeight: 600, cursor: 'pointer',
            }}>加入</button>
            <button onClick={() => setSheet('create')} style={{
              padding: '7px 12px', borderRadius: 10, border: 'none', cursor: 'pointer',
              background: TABI_COLORS.ink, color: '#fff', fontSize: 11.5, fontWeight: 600,
            }}>+ 新旅行</button>
          </div>
        }
      />
      <div style={{ padding: '14px 16px 30px' }}>
        {loading && (
          <div style={{ textAlign: 'center', padding: '60px 0', color: TABI_COLORS.ink3 }}>載入中…</div>
        )}
        {!loading && trips.length === 0 && (
          <div style={{ textAlign: 'center', padding: '60px 20px', color: TABI_COLORS.ink3 }}>
            <div style={{ fontSize: 40, marginBottom: 12 }}>✈️</div>
            <div style={{ fontSize: 15, fontFamily: tabiSerif, color: TABI_COLORS.ink2, marginBottom: 6 }}>尚無旅行紀錄</div>
            <div style={{ fontSize: 12.5 }}>新增第一筆旅行，開始記帳吧！</div>
          </div>
        )}
        {trips.map(t => (
          <div key={t.id} onClick={() => onOpen(t.id)} style={{
            background: TABI_COLORS.card, borderRadius: 18, padding: '16px',
            marginTop: 12, border: `1px solid ${TABI_COLORS.line}`, cursor: 'pointer',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
              <div style={{
                width: 50, height: 50, borderRadius: 12, background: t.cover_color || '#C8553D',
                flexShrink: 0, display: 'grid', placeItems: 'center',
                fontFamily: tabiSerif, fontSize: 22,
              }}>✈️</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 600, color: TABI_COLORS.ink }}>
                  {t.name}
                </div>
                <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>
                  {t.destination ? `${t.destination} · ` : ''}
                  {t.start_date ? `${_fmtDateFull(t.start_date)}${t.end_date ? ' — ' + _fmtDateFull(t.end_date) : ''}` : '尚未設定日期'}
                </div>
              </div>
            </div>
            <div style={{
              display: 'flex', alignItems: 'center', marginTop: 14,
              paddingTop: 12, borderTop: `1px dashed ${TABI_COLORS.line2}`,
            }}>
              <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3 }}>
                {t.member_count} 人 · {t.expense_count} 筆支出
              </div>
              <div style={{ marginLeft: 'auto', textAlign: 'right' }}>
                <div style={{ fontSize: 10, color: TABI_COLORS.ink3 }}>合計</div>
                <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.ink }}>
                  {_fmtAmt(t.total_amount, t.currency)}
                </div>
              </div>
            </div>
          </div>
        ))}
      </div>

      {sheet === 'create' && (
        <CreateTripSheet
          onClose={() => setSheet(null)}
          onCreated={tripId => { setSheet(null); load(); onOpen(tripId); }}
        />
      )}
      {sheet === 'join' && (
        <JoinTripSheet
          onClose={() => setSheet(null)}
          onJoined={(tripId) => { setSheet(null); load(); onOpen(tripId); }}
        />
      )}
    </div>
  );
}


// ── Expense Card ─────────────────────────────────────────────

function ExpenseCard({ e, trip, startDate, membersMap, includeProxy, onTap }) {
  const payer = membersMap[e.paid_by];
  const isPreTrip = startDate && new Date(e.expense_date) < startDate;
  const d = new Date(e.expense_date);
  const dateLabel = `${d.getMonth() + 1}/${d.getDate()}`;
  return (
    <div onClick={onTap} style={{
      background: TABI_COLORS.card, borderRadius: 18,
      marginTop: 10, cursor: 'pointer',
      boxShadow: '0 2px 14px rgba(26,22,18,0.07)',
      opacity: e.is_proxy && !includeProxy ? 0.45 : 1,
    }}>
      <div style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 14 }}>
        {/* Category icon */}
        <div style={{
          width: 46, height: 46, borderRadius: 15, fontSize: 22,
          background: TABI_COLORS.bg2, display: 'grid', placeItems: 'center', flexShrink: 0,
        }}>{e.icon || CAT_ICONS[e.category] || '💳'}</div>

        {/* Content */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 5 }}>
            <div style={{
              fontFamily: tabiSerif, fontSize: 15, fontWeight: 600, color: TABI_COLORS.ink,
              flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
            }}>{e.title}</div>
            {e.is_proxy && (
              <span style={{ fontSize: 10, background: `${TABI_COLORS.red}18`, color: TABI_COLORS.red, padding: '2px 7px', borderRadius: 6, fontWeight: 700, flexShrink: 0 }}>代購</span>
            )}
            {isPreTrip && (
              <span style={{ fontSize: 10, background: '#1A4D8F18', color: '#1A4D8F', padding: '2px 7px', borderRadius: 6, fontWeight: 600, flexShrink: 0 }}>事前</span>
            )}
          </div>
          <div style={{ fontSize: 12, color: TABI_COLORS.ink3, display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
            {payer && (
              <span style={{
                width: 17, height: 17, borderRadius: 9, background: payer.color,
                color: '#fff', display: 'inline-grid', placeItems: 'center', fontSize: 8, fontWeight: 700, flexShrink: 0,
              }}>{payer.initial}</span>
            )}
            <span style={{ fontWeight: 500 }}>{payer?.display_name}</span>
            <span style={{ opacity: 0.35 }}>·</span>
            <span>{e.category || '其他'}</span>
            <span style={{ opacity: 0.35 }}>·</span>
            <span>{dateLabel}</span>
            {e.is_proxy && e.proxy_for && (
              <><span style={{ opacity: 0.35 }}>→</span><span>{e.proxy_for}</span></>
            )}
          </div>
        </div>

        {/* Amount */}
        <div style={{ textAlign: 'right', flexShrink: 0 }}>
          <div style={{ fontFamily: tabiSerif, fontSize: 18, fontWeight: 700, color: TABI_COLORS.ink }}>
            {_fmtAmt(e.amount, e.currency)}
          </div>
          <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>
            {e.split_method === 'equal' ? `÷${trip.members.length}` : e.split_method === 'solo' ? '僅我' : '自定義'}
          </div>
        </div>
      </div>
    </div>
  );
}


// ── Date filter helpers ───────────────────────────────────────

function _filterExpensesByDate(expenses, dateFilter, startDate) {
  if (dateFilter === 'all') return expenses;
  if (dateFilter === 'pre') return expenses.filter(e => startDate && new Date(e.expense_date) < startDate);
  return expenses.filter(e => e.expense_date && e.expense_date.slice(0, 10) === dateFilter);
}

function _filterReceiptsByDate(receipts, dateFilter, startDate) {
  if (dateFilter === 'all') return receipts;
  const getDs = r => r.receipt_date || (r.created_at && r.created_at.slice(0, 10)) || '';
  if (dateFilter === 'pre') return receipts.filter(r => { const ds = getDs(r); return ds && startDate && new Date(ds) < startDate; });
  return receipts.filter(r => getDs(r) === dateFilter);
}

// ── Date filter bar ───────────────────────────────────────────

function DateFilterBar({ trip, startDate, expenses, receipts, dateFilter, onSelect }) {
  // Build list of dates from trip range; fall back to expense dates
  const dates = React.useMemo(() => {
    const set = new Set();
    if (trip.start_date && trip.end_date) {
      const end = new Date(trip.end_date);
      for (let d = new Date(trip.start_date); d <= end; d.setDate(d.getDate() + 1)) {
        set.add(d.toISOString().slice(0, 10));
      }
    }
    // Also include any in-trip expense/receipt dates not already in the range
    (expenses || []).forEach(e => { if (e.expense_date) { const ds = e.expense_date.slice(0, 10); if (!startDate || new Date(ds) >= startDate) set.add(ds); } });
    (receipts || []).forEach(r => { const ds = r.receipt_date || (r.created_at && r.created_at.slice(0, 10)); if (ds && (!startDate || new Date(ds) >= startDate)) set.add(ds); });
    return Array.from(set).sort();
  }, [trip.start_date, trip.end_date, expenses, receipts]);

  if (dates.length === 0 && !startDate) return null;

  const chips = [
    { id: 'all', label: '全部' },
    ...(startDate ? [{ id: 'pre', label: '事前' }] : []),
    ...dates.map(ds => { const d = new Date(ds + 'T00:00:00'); return { id: ds, label: `${d.getMonth()+1}/${d.getDate()}` }; }),
  ];

  return (
    <div style={{
      display: 'flex', gap: 6, overflowX: 'auto', padding: '10px 16px 9px',
      borderBottom: `1px solid ${TABI_COLORS.line}`,
      WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none',
    }}>
      {chips.map(chip => {
        const on = dateFilter === chip.id;
        return (
          <button key={chip.id} onClick={() => onSelect(chip.id)} style={{
            padding: '5px 13px', borderRadius: 20, whiteSpace: 'nowrap', flexShrink: 0,
            border: 'none',
            background: on ? TABI_COLORS.ink : TABI_COLORS.bg2,
            color: on ? '#fff' : TABI_COLORS.ink2,
            fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
            boxShadow: on ? '0 2px 8px rgba(26,22,18,0.18)' : 'none',
          }}>{chip.label}</button>
        );
      })}
    </div>
  );
}


// ── Settlement Sheet ──────────────────────────────────────────

function SettlementSheet({ transfers, membersMap, rate, onClose, onSettleAll }) {
  const [confirmSettle, setConfirmSettle] = React.useState(false);
  const [settling, setSettling]           = React.useState(false);
  return (
    <SheetWrap title="共同結算" onClose={onClose}>
      <div style={{
        background: '#5B755310', padding: '10px 12px', borderRadius: 10,
        fontSize: 12, color: TABI_COLORS.ink2, lineHeight: 1.5, marginBottom: 12,
      }}>
        ✨ 已用最簡轉帳算出 <b>{transfers.length} 筆</b>結帳（共同支出）
      </div>
      {transfers.length === 0 ? (
        <div style={{ textAlign: 'center', padding: '40px 0', color: TABI_COLORS.ink3 }}>
          <div style={{ fontSize: 36, marginBottom: 10 }}>✓</div>
          <div style={{ fontFamily: tabiSerif, fontSize: 15, color: TABI_COLORS.ink2 }}>所有人都結清了！</div>
        </div>
      ) : transfers.map((t, i) => {
        const from = membersMap[t.from];
        const to   = membersMap[t.to];
        if (!from || !to) return null;
        return (
          <div key={i} style={{
            background: TABI_COLORS.card, borderRadius: 14, padding: '14px',
            marginBottom: 8, border: `1px solid ${TABI_COLORS.line}`,
            display: 'flex', alignItems: 'center', gap: 10,
          }}>
            <MemberAvatar m={from} size={36} />
            <div style={{ flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
                <span style={{ fontFamily: tabiSerif, fontWeight: 600 }}>{from.display_name}</span>
                <svg width="20" height="10" viewBox="0 0 20 10">
                  <path d="M0 5h17M13 1l4 4-4 4" fill="none" stroke={TABI_COLORS.red} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
                <span style={{ fontFamily: tabiSerif, fontWeight: 600 }}>{to.display_name}</span>
              </div>
              <div style={{ fontFamily: tabiSerif, fontSize: 18, fontWeight: 700, color: TABI_COLORS.red, marginTop: 4 }}>
                ¥{t.amount.toLocaleString()}
                {rate && <span style={{ fontSize: 11, color: TABI_COLORS.ink3, fontWeight: 400 }}> ≈ NT${Math.round(t.amount * rate).toLocaleString()}</span>}
              </div>
            </div>
            <MemberAvatar m={to} size={36} />
          </div>
        );
      })}

      {transfers.length > 0 && onSettleAll && (
        <button onClick={() => setConfirmSettle(true)} style={{
          width: '100%', marginTop: 8, padding: '13px', borderRadius: 12,
          border: `1.5px solid ${TABI_COLORS.green}`, background: `${TABI_COLORS.green}12`,
          color: TABI_COLORS.green, fontSize: 14, fontWeight: 700, cursor: 'pointer',
        }}>全部結算 ✓</button>
      )}

      {confirmSettle && ReactDOM.createPortal(
        <div onClick={() => setConfirmSettle(false)} style={{
          position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
          zIndex: 9100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
        }}>
          <div onClick={e => e.stopPropagation()} style={{
            background: TABI_COLORS.bg, borderRadius: 20, padding: '24px 22px',
            width: '100%', maxWidth: 300,
          }}>
            <div style={{ fontFamily: tabiSerif, fontSize: 17, fontWeight: 700, color: TABI_COLORS.ink, marginBottom: 8 }}>
              確認全部結清？
            </div>
            <div style={{ fontSize: 13, color: TABI_COLORS.ink3, lineHeight: 1.6, marginBottom: 20 }}>
              所有共同支出將標記為已結算，此操作無法還原。請確認大家都已完成轉帳再繼續。
            </div>
            <div style={{ display: 'flex', gap: 10 }}>
              <button onClick={() => setConfirmSettle(false)} style={{
                flex: 1, padding: '12px', borderRadius: 12, border: `1px solid ${TABI_COLORS.line2}`,
                background: 'transparent', color: TABI_COLORS.ink, fontSize: 13, fontWeight: 600, cursor: 'pointer',
              }}>取消</button>
              <button onClick={async () => {
                setSettling(true);
                await onSettleAll();
                setSettling(false);
                setConfirmSettle(false);
              }} disabled={settling} style={{
                flex: 1, padding: '12px', borderRadius: 12, border: 'none',
                background: settling ? TABI_COLORS.line2 : TABI_COLORS.green,
                color: '#fff', fontSize: 13, fontWeight: 700, cursor: settling ? 'not-allowed' : 'pointer',
              }}>{settling ? '結算中…' : '確定結清'}</button>
            </div>
          </div>
        </div>,
        document.body
      )}
    </SheetWrap>
  );
}


// ── Proxy Settlement Sheet ───────────────────────────────────

function ProxySheet({ trip, onClose, onSaved }) {
  const [selected, setSelected]   = React.useState(null);
  const [customAmt, setCustomAmt] = React.useState('');
  const [saving, setSaving]       = React.useState(false);

  const membersMap = {};
  trip.members.forEach(m => membersMap[m.id] = m);
  const rate = trip.jpy_twd_rate;

  // All proxy expenses; remaining = amount - settled_amount
  const allProxy = trip.expenses.filter(e => e.is_proxy);
  const eRemaining = e => e.amount - (e.settled_amount || 0);

  // Group by proxy_for; include both pending and fully settled for history
  const groups = {};
  allProxy.forEach(e => {
    const k = e.proxy_for || '未指定';
    if (!groups[k]) groups[k] = { name: k, pending: 0, total: 0, paidBack: 0, expenses: [] };
    groups[k].total    += e.amount;
    groups[k].paidBack += (e.settled_amount || 0);
    groups[k].pending  += eRemaining(e);
    groups[k].expenses.push(e);
  });
  const groupList = Object.values(groups)
    .filter(g => g.pending > 0)
    .sort((a, b) => b.pending - a.pending);
  const settledGroups = Object.values(groups).filter(g => g.pending === 0 && g.total > 0);
  const selectedGroup = selected ? groups[selected] : null;

  // Cascade payment across multiple expenses (sorted by remaining desc so smaller ones clear first)
  async function settleCustomCascade(expenses, totalPay) {
    setSaving(true);
    let left = totalPay;
    const sorted = [...expenses].sort((a, b) => eRemaining(a) - eRemaining(b)); // clear small first
    for (const e of sorted) {
      if (left <= 0) break;
      const rem = eRemaining(e);
      if (rem <= 0) continue;
      const pay = Math.min(left, rem);
      await fetch(`/api/trips/${trip.id}/expenses/${e.id}/proxy-settle`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify(pay >= rem ? {} : { amount: pay }),
      });
      left -= pay;
    }
    setSaving(false);
    onSaved();
  }

  async function settleOne(expenseId) {
    setSaving(true);
    await fetch(`/api/trips/${trip.id}/expenses/${expenseId}/proxy-settle`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
      body: JSON.stringify({}),
    });
    setSaving(false);
    onSaved();
  }

  return (
    <SheetWrap title={selected ? `${selected} 的代購` : '代購結算'} onClose={selected ? () => { setSelected(null); setCustomAmt(''); } : onClose}>
      {!selected ? (
        <>
          {groupList.length === 0 && settledGroups.length === 0 && (
            <div style={{ textAlign: 'center', padding: '40px 0', color: TABI_COLORS.ink3 }}>
              <div style={{ fontSize: 32, marginBottom: 8 }}>🎁</div>
              <div style={{ fontSize: 13 }}>尚無代購紀錄</div>
            </div>
          )}

          {/* Pending groups */}
          {groupList.map(g => (
            <div key={g.name} onClick={() => setSelected(g.name)} style={{
              background: TABI_COLORS.card, borderRadius: 14, padding: '12px 14px', marginBottom: 10,
              border: `1px solid ${TABI_COLORS.line}`, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12,
            }}>
              <div style={{
                width: 40, height: 40, borderRadius: 20, background: `${TABI_COLORS.warn}25`,
                display: 'grid', placeItems: 'center', fontSize: 18, flexShrink: 0,
              }}>🎁</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: tabiSerif, fontSize: 15, fontWeight: 700, color: TABI_COLORS.ink }}>{g.name}</div>
                <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>
                  共 {_fmtAmt(g.total, trip.currency)}
                  {g.paidBack > 0 && <span style={{ color: TABI_COLORS.green }}> · 已還 {_fmtAmt(g.paidBack, trip.currency)}</span>}
                </div>
              </div>
              <div style={{ textAlign: 'right', flexShrink: 0 }}>
                <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.warn }}>
                  {_fmtAmt(g.pending, trip.currency)}
                </div>
                {rate && <div style={{ fontSize: 10, color: TABI_COLORS.ink3 }}>≈ NT${Math.round(g.pending * rate).toLocaleString()}</div>}
              </div>
              <svg width="7" height="12" viewBox="0 0 7 12"><path d="M1 1l5 5-5 5" stroke={TABI_COLORS.ink3} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" fill="none"/></svg>
            </div>
          ))}

          {/* Settled groups */}
          {settledGroups.length > 0 && (
            <div style={{ marginTop: 16 }}>
              <div style={{ fontSize: 11.5, fontWeight: 600, color: TABI_COLORS.ink3, marginBottom: 8, letterSpacing: '0.05em' }}>已結清</div>
              {settledGroups.map(g => (
                <div key={g.name} style={{
                  background: TABI_COLORS.bg2, borderRadius: 12, padding: '10px 14px', marginBottom: 8,
                  border: `1px solid ${TABI_COLORS.line}`, display: 'flex', alignItems: 'center', gap: 10, opacity: 0.65,
                }}>
                  <span style={{ fontSize: 16 }}>✓</span>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink }}>{g.name}</div>
                    <div style={{ fontSize: 11, color: TABI_COLORS.ink3 }}>{g.expenses.length} 筆</div>
                  </div>
                  <div style={{ fontFamily: tabiSerif, fontSize: 13, color: TABI_COLORS.ink3 }}>
                    {_fmtAmt(g.total, trip.currency)}
                  </div>
                </div>
              ))}
            </div>
          )}
        </>
      ) : (
        <>
          {/* Summary card */}
          {(() => {
            const pending  = selectedGroup.pending;
            const paidBack = selectedGroup.paidBack;
            const total    = selectedGroup.total;
            return (
              <div style={{
                background: `${TABI_COLORS.warn}15`, borderRadius: 14, padding: '14px', marginBottom: 14,
                border: `1px solid ${TABI_COLORS.warn}30`,
              }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
                  <div>
                    <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginBottom: 3 }}>待還</div>
                    <div style={{ fontFamily: tabiSerif, fontSize: 24, fontWeight: 700, color: TABI_COLORS.ink }}>
                      {_fmtAmt(pending, trip.currency)}
                    </div>
                    {rate && <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>≈ NT${Math.round(pending * rate).toLocaleString()}</div>}
                  </div>
                  {paidBack > 0 && (
                    <div style={{ textAlign: 'right' }}>
                      <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginBottom: 3 }}>已還</div>
                      <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.green }}>
                        {_fmtAmt(paidBack, trip.currency)}
                      </div>
                      <div style={{ fontSize: 10, color: TABI_COLORS.ink3 }}>共 {_fmtAmt(total, trip.currency)}</div>
                    </div>
                  )}
                </div>
              </div>
            );
          })()}

          {/* Per-expense rows */}
          {selectedGroup.expenses.map(e => {
            const rem   = eRemaining(e);
            const paid  = e.settled_amount || 0;
            const payer = membersMap[e.paid_by];
            const done  = rem <= 0;
            return (
              <div key={e.id} style={{
                background: done ? TABI_COLORS.bg2 : TABI_COLORS.card,
                borderRadius: 12, padding: '11px 14px', marginBottom: 8,
                border: `1px solid ${TABI_COLORS.line}`,
                display: 'flex', alignItems: 'center', gap: 10,
                opacity: done ? 0.65 : 1,
              }}>
                <span style={{ fontSize: 18, flexShrink: 0 }}>{done ? '✓' : (CAT_ICONS[e.category] || '🎁')}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink }}>{e.title}</div>
                  <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>
                    {payer?.display_name} 墊付
                    {paid > 0 && !done && <span style={{ color: TABI_COLORS.green }}> · 已還 {_fmtAmt(paid, trip.currency)}</span>}
                  </div>
                </div>
                <div style={{ textAlign: 'right', flexShrink: 0 }}>
                  <div style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: done ? TABI_COLORS.ink3 : TABI_COLORS.ink }}>
                    {done ? _fmtAmt(e.amount, trip.currency) : _fmtAmt(rem, trip.currency)}
                  </div>
                  {!done && (
                    <button onClick={() => settleOne(e.id)} disabled={saving} style={{
                      marginTop: 4, padding: '3px 10px', borderRadius: 7, border: 'none',
                      background: TABI_COLORS.green, color: '#fff', fontSize: 11, fontWeight: 600, cursor: 'pointer',
                    }}>還清</button>
                  )}
                </div>
              </div>
            );
          })}

          {/* Custom amount */}
          {selectedGroup.pending > 0 && (
            <div style={{
              background: TABI_COLORS.card, borderRadius: 14, padding: '14px', marginTop: 8,
              border: `1px solid ${TABI_COLORS.line}`,
            }}>
              <div style={{ fontSize: 12, fontWeight: 600, color: TABI_COLORS.ink3, marginBottom: 10 }}>自訂還款金額</div>
              <div style={{ display: 'flex', gap: 8 }}>
                <input
                  value={customAmt} onChange={e => setCustomAmt(e.target.value)}
                  placeholder="輸入金額" inputMode="numeric"
                  style={{
                    flex: 1, padding: '10px 12px', borderRadius: 10, border: `1px solid ${TABI_COLORS.line}`,
                    background: TABI_COLORS.bg, fontSize: 16, color: TABI_COLORS.ink, outline: 'none',
                  }}
                />
                <button onClick={() => {
                  const amt = parseInt(customAmt);
                  if (!amt || amt <= 0) return;
                  settleCustomCascade(selectedGroup.expenses, amt);
                  setCustomAmt('');
                }} disabled={saving || !customAmt} style={{
                  padding: '10px 16px', borderRadius: 10, border: 'none',
                  background: customAmt ? TABI_COLORS.green : TABI_COLORS.line2,
                  color: '#fff', fontSize: 13, fontWeight: 700, cursor: customAmt ? 'pointer' : 'not-allowed',
                }}>還款</button>
              </div>
            </div>
          )}

          {/* Settle all */}
          {selectedGroup.pending > 0 && (
            <button onClick={() => settleCustomCascade(selectedGroup.expenses, selectedGroup.pending)} disabled={saving} style={{
              width: '100%', marginTop: 12, padding: '13px', borderRadius: 12, border: 'none',
              background: saving ? TABI_COLORS.line2 : TABI_COLORS.ink,
              color: '#fff', fontSize: 14, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer',
            }}>{saving ? '處理中…' : `全部還清 ${_fmtAmt(selectedGroup.pending, trip.currency)}`}</button>
          )}
        </>
      )}
    </SheetWrap>
  );
}


// ── Trip Invite Sheet ─────────────────────────────────────────

function TripInviteSheet({ trip, inviteCode, inviteExpiry, onClose, onRegenerate }) {
  const [copied, setCopied] = React.useState(false);
  const [regenerating, setRegenerating] = React.useState(false);

  const code = inviteCode || trip.invite_code;
  const url  = `https://tabi.sungcheng.org/?join=${code}`;

  const expiresAt = inviteExpiry || trip.invite_expires_at;
  const isExpired = expiresAt && new Date(expiresAt) <= new Date();
  let expiryDateText = '';
  let expiryLabel = '';
  if (expiresAt) {
    const d = new Date(expiresAt);
    expiryDateText = `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')} 到期`;
    const diffDays = Math.ceil((d - new Date()) / 86400000);
    if (isExpired)           expiryLabel = '已過期';
    else if (diffDays === 1) expiryLabel = '明天到期';
    else                     expiryLabel = `${diffDays} 天後到期`;
  }

  async function copyLink() {
    await navigator.clipboard?.writeText(url);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }

  function shareLink() {
    navigator.share?.({ title: `加入 ${trip.name}`, url });
  }

  async function handleRegenerate() {
    setRegenerating(true);
    await onRegenerate();
    setRegenerating(false);
  }

  return (
    <SheetWrap title="邀請好友" onClose={onClose}>
      <div style={{ fontSize: 13, color: TABI_COLORS.ink2, lineHeight: 1.7, marginBottom: 20 }}>
        將此連結傳給要共同記帳的好友即可加入，或在記帳頁面點擊「加入」並輸入邀請碼。
      </div>

      {isExpired && (
        <div style={{
          background: `${TABI_COLORS.warn}18`, border: `1px solid ${TABI_COLORS.warn}50`,
          borderRadius: 10, padding: '10px 14px', marginBottom: 14,
          fontSize: 13, color: TABI_COLORS.warn, fontWeight: 600,
        }}>邀請連結已過期，請重新生成。</div>
      )}

      <div style={{
        background: TABI_COLORS.bg2, borderRadius: 12, padding: '12px 14px', marginBottom: 10,
        border: `1px solid ${isExpired ? TABI_COLORS.warn+'60' : TABI_COLORS.line2}`,
        opacity: isExpired ? 0.5 : 1,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
          <div style={{ fontSize: 11, color: TABI_COLORS.ink3, fontWeight: 600, letterSpacing: '0.05em' }}>
            邀請連結
          </div>
          {expiresAt && (
            <div style={{ fontSize: 11, color: isExpired ? TABI_COLORS.warn : TABI_COLORS.ink3, fontWeight: 600 }}>
              {expiryLabel}　{expiryDateText}
            </div>
          )}
        </div>
        <div style={{ fontFamily: 'monospace', fontSize: 12, color: TABI_COLORS.ink, wordBreak: 'break-all', lineHeight: 1.6 }}>
          {url}
        </div>
      </div>

      <div style={{
        background: TABI_COLORS.bg2, borderRadius: 12, padding: '12px 14px', marginBottom: 16,
        border: `1px solid ${isExpired ? TABI_COLORS.warn+'60' : TABI_COLORS.line2}`,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        opacity: isExpired ? 0.5 : 1,
      }}>
        <div>
          <div style={{ fontSize: 11, color: TABI_COLORS.ink3, fontWeight: 600, letterSpacing: '0.05em', marginBottom: 4 }}>邀請碼</div>
          <div style={{ fontFamily: 'monospace', fontSize: 20, fontWeight: 700, color: TABI_COLORS.ink, letterSpacing: '0.12em' }}>{code}</div>
        </div>
        <div style={{ fontSize: 11, color: TABI_COLORS.ink3, lineHeight: 1.5, textAlign: 'right', maxWidth: 100 }}>
          在記帳頁面點「加入」輸入此碼
        </div>
      </div>

      <div style={{ display: 'flex', gap: 10, marginBottom: 12 }}>
        <button onClick={copyLink} disabled={isExpired} style={{
          flex: 1, padding: '13px', borderRadius: 12, border: 'none',
          background: isExpired ? TABI_COLORS.line2 : copied ? TABI_COLORS.green : TABI_COLORS.ink,
          color: isExpired ? TABI_COLORS.ink3 : '#fff',
          fontSize: 14, fontWeight: 600, cursor: isExpired ? 'not-allowed' : 'pointer', transition: 'background 0.2s',
        }}>{copied ? '已複製！' : '複製連結'}</button>
        {typeof navigator !== 'undefined' && navigator.share && (
          <button onClick={shareLink} disabled={isExpired} style={{
            flex: 1, padding: '13px', borderRadius: 12, border: 'none',
            background: isExpired ? TABI_COLORS.line2 : TABI_COLORS.red,
            color: isExpired ? TABI_COLORS.ink3 : '#fff',
            fontSize: 14, fontWeight: 600, cursor: isExpired ? 'not-allowed' : 'pointer',
          }}>分享連結</button>
        )}
      </div>

      <button onClick={handleRegenerate} disabled={regenerating} style={{
        width: '100%', padding: '13px', borderRadius: 12,
        border: isExpired ? 'none' : `1px solid ${TABI_COLORS.line2}`,
        background: isExpired ? TABI_COLORS.red : TABI_COLORS.card,
        color: isExpired ? '#fff' : regenerating ? TABI_COLORS.ink3 : TABI_COLORS.ink2,
        fontSize: 14, fontWeight: 600, cursor: regenerating ? 'not-allowed' : 'pointer',
      }}>{regenerating ? '生成中…' : '重新生成邀請連結'}</button>
    </SheetWrap>
  );
}


// ── Trip Detail Screen ────────────────────────────────────────

function TripDetailScreen({ go, tripId, onBack, user }) {
  const [trip, setTrip]           = React.useState(null);
  const [tab, setTab]             = React.useState('overview');
  const [sheet, setSheet]         = React.useState(null);  // 'expense' | 'receipt' | 'invite'
  const [includeProxy, setProxy]  = React.useState(true);
  const [receipts, setReceipts]   = React.useState([]);
  const [loadingR, setLoadingR]   = React.useState(false);
  const [inviteCode, setInviteCode]         = React.useState(null);
  const [inviteExpiry, setInviteExpiry]     = React.useState(null);
  const [editingExpense, setEditingExpense] = React.useState(null);
  const [viewingReceipt, setViewingReceipt] = React.useState(null);
  const [dateFilter, setDateFilter]         = React.useState('all');
  const [showSettle, setShowSettle]         = React.useState(false);
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
  const [deleting, setDeleting]             = React.useState(false);
  const [statDay, setStatDay]               = React.useState(null);
  const [statCat, setStatCat]               = React.useState(null);
  const [showProxy, setShowProxy]           = React.useState(false);
  const [showMemberManage, setShowMemberManage] = React.useState(false);
  const [showPreTrip, setShowPreTrip]           = React.useState(false);

  async function loadTrip() {
    const r = await fetch(`/api/trips/${tripId}`, {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    });
    if (r.ok) setTrip(await r.json());
    else go('trips');
  }

  async function loadReceipts(showSpinner = false) {
    if (showSpinner) setLoadingR(true);
    const r = await fetch(`/api/trips/${tripId}/receipts`, {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    });
    if (r.ok) setReceipts(await r.json());
    setLoadingR(false);
  }

  React.useEffect(() => {
    loadTrip();
    const timer = setInterval(() => { if (!document.hidden) loadTrip(); }, 8000);
    return () => clearInterval(timer);
  }, [tripId]);
  React.useEffect(() => {
    if (tab !== 'receipts') return;
    loadReceipts(true);
    const timer = setInterval(() => { if (!document.hidden) loadReceipts(false); }, 8000);
    return () => clearInterval(timer);
  }, [tab]);

  async function generateInvite() {
    const r = await fetch(`/api/trips/${tripId}/invite`, {
      method: 'POST', headers: { Authorization: `Bearer ${window.tabiToken()}` },
    });
    if (r.ok) {
      const d = await r.json();
      setInviteCode(d.invite_code);
      setInviteExpiry(d.expires_at);
    }
  }

  async function openInvite() {
    if (!inviteCode && !trip.invite_code) await generateInvite();
    setSheet('invite');
  }

  async function deleteTrip() {
    setDeleting(true);
    const r = await fetch(`/api/trips/${tripId}`, {
      method: 'DELETE', headers: { Authorization: `Bearer ${window.tabiToken()}` },
    });
    if (r.ok || r.status === 204) {
      go('trips');
    } else {
      setDeleting(false);
      setShowDeleteConfirm(false);
    }
  }

  if (!trip) return (
    <div style={{ background: TABI_COLORS.bg, minHeight: '100%', display: 'grid', placeItems: 'center' }}>
      <div style={{ color: TABI_COLORS.ink3 }}>載入中…</div>
    </div>
  );

  const sharedExpenses = trip.expenses.filter(e => e.visibility === 'shared');
  const personalExpenses = trip.expenses.filter(e => e.visibility === 'personal');
  const visibleExpenses = includeProxy ? trip.expenses : trip.expenses.filter(e => !e.is_proxy);
  const rate = trip.jpy_twd_rate;

  // Current user's TripMember — used to look up split portion
  const myMember = user ? trip.members.find(m => m.user_id === user.id) : null;
  // For shared expenses return only this user's split; personal expenses are already theirs
  const myShare = (e) => {
    if (e.visibility === 'personal') return e.amount;
    if (!myMember) return e.amount;
    const split = (e.splits || []).find(s => s.member_id === myMember.id);
    return split ? split.amount : 0;
  };

  const total = visibleExpenses.reduce((s, e) => s + myShare(e), 0);

  const startDate = trip.start_date ? new Date(trip.start_date) : null;
  const preTripAmt = visibleExpenses.filter(e => startDate && new Date(e.expense_date) < startDate).reduce((s, e) => s + myShare(e), 0);
  const inTripAmt  = visibleExpenses.filter(e => !startDate || new Date(e.expense_date) >= startDate).reduce((s, e) => s + myShare(e), 0);
  const proxyExpsAll  = trip.expenses.filter(e => e.is_proxy);
  const proxyTotal    = proxyExpsAll.reduce((s, e) => s + e.amount, 0);
  const proxySettled  = proxyExpsAll.reduce((s, e) => s + (e.settled_amount || 0), 0);

  const membersMap = {};
  trip.members.forEach(m => membersMap[m.id] = m);

  const { balances, transfers } = computeSettlement(trip.members, sharedExpenses);

  // ── Daily stats for Overview ──────────────────────────────
  const tripDays = (() => {
    if (!trip.start_date || !trip.end_date) return [];
    const days = [];
    const end = new Date(trip.end_date + 'T12:00:00');
    const d   = new Date(trip.start_date + 'T12:00:00');
    while (d <= end) { days.push(d.toISOString().substring(0, 10)); d.setDate(d.getDate() + 1); }
    return days;
  })();

  const dailyMap = (() => {
    const map = {};
    visibleExpenses.forEach(e => {
      const day = e.expense_date.substring(0, 10);
      if (!map[day]) map[day] = { total: 0, expenses: [] };
      map[day].total += myShare(e);
      map[day].expenses.push(e);
    });
    return map;
  })();

  const allExpDays  = Object.keys(dailyMap).sort();
  const chartDays   = showPreTrip
    ? allExpDays
    : (tripDays.length > 0 ? tripDays : allExpDays);
  const maxDayAmt   = Math.max(1, ...chartDays.map(d => dailyMap[d]?.total || 0));
  const hasPreTripDays = startDate && allExpDays.some(d => new Date(d + 'T12:00:00') < startDate);

  const statSrcExp = statDay ? (dailyMap[statDay]?.expenses || []) : visibleExpenses;
  const catTotals  = (() => {
    const cats = {};
    statSrcExp.forEach(e => { const c = e.category || '其他'; cats[c] = (cats[c] || 0) + myShare(e); });
    return Object.entries(cats).sort((a, b) => b[1] - a[1]);
  })();
  const catMax         = Math.max(1, ...catTotals.map(([, v]) => v));
  const statSrcTotal   = statSrcExp.reduce((s, e) => s + myShare(e), 0);
  const overviewExps   = statSrcExp
    .filter(e => !statCat || (e.category || '其他') === statCat)
    .sort((a, b) => new Date(b.expense_date) - new Date(a.expense_date));
  const tripDuration   = trip.start_date && trip.end_date
    ? Math.ceil((new Date(trip.end_date) - new Date(trip.start_date)) / 86400000) + 1 : null;

  // ── Overview-specific computations ──────────────────────────
  const memberPaid = {}, memberShare = {};
  trip.members.forEach(m => { memberPaid[m.id] = 0; memberShare[m.id] = 0; });
  sharedExpenses.forEach(e => {
    if (e.paid_by) memberPaid[e.paid_by] = (memberPaid[e.paid_by] || 0) + e.amount;
    (e.splits || []).forEach(s => { memberShare[s.member_id] = (memberShare[s.member_id] || 0) + s.amount; });
  });
  const maxMemberAmt = Math.max(1, ...trip.members.map(m => Math.max(memberPaid[m.id] || 0, memberShare[m.id] || 0)));
  const avgDaily  = tripDuration ? Math.round(inTripAmt / tripDuration) : null;
  const perPerson = trip.members.length > 1 ? Math.round(total / trip.members.length) : null;
  const isEnded   = !!(trip.end_date && new Date(trip.end_date + 'T23:59:59') < new Date());
  const PALETTE   = [TABI_COLORS.red, TABI_COLORS.blue, TABI_COLORS.green, TABI_COLORS.warn, '#9B59B6', '#1ABC9C'];

  // Sort expenses newest-first, then apply date filter
  const sortedPersonal  = [...personalExpenses].sort((a, b) => new Date(b.expense_date) - new Date(a.expense_date));
  const sortedShared    = [...sharedExpenses].sort((a, b) => new Date(b.expense_date) - new Date(a.expense_date));
  const sortedReceipts  = [...receipts].sort((a, b) => new Date(b.receipt_date || b.created_at) - new Date(a.receipt_date || a.created_at));
  const filteredPersonal = _filterExpensesByDate(sortedPersonal, dateFilter, startDate);
  const filteredShared   = _filterExpensesByDate(sortedShared,   dateFilter, startDate);
  const filteredReceipts = _filterReceiptsByDate(sortedReceipts, dateFilter, startDate);

  const TABS = [
    { id: 'overview',           label: '概覽' },
    { id: 'expenses_personal',  label: '個人支出', count: personalExpenses.length },
    { id: 'expenses_shared',    label: '共同支出', count: sharedExpenses.length },
    { id: 'receipts',           label: '發票',     count: receipts.length },
  ];

  return (
    <div style={{ background: TABI_COLORS.bg, minHeight: '100%', position: 'relative', paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 100px)' }}>
      <TabiTopBar
        title={trip.name}
        subtitle={trip.destination || 'TRIP · 家計簿'}
        onBack={onBack || (() => go('trips'))}
        right={
          <button onClick={openInvite} style={{
            padding: '7px 13px', borderRadius: 10, border: 'none',
            background: TABI_COLORS.red, color: '#fff', fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
          }}>邀請</button>
        }
      />

      {/* Tab bar */}
      <div style={{ padding: '10px 16px 0', display: 'flex', gap: 4, overflowX: 'auto', scrollbarWidth: 'none' }}>
        {TABS.map(t => {
          const on = tab === t.id;
          return (
            <button key={t.id} onClick={() => setTab(t.id)} style={{
              padding: '8px 14px', borderRadius: 10, border: 'none', cursor: 'pointer', whiteSpace: 'nowrap',
              background: on ? TABI_COLORS.ink : 'transparent',
              color: on ? '#fff' : TABI_COLORS.ink2,
              fontSize: 12.5, fontWeight: 600, flexShrink: 0,
              display: 'inline-flex', alignItems: 'center', gap: 5,
            }}>
              {t.label}
              {t.count != null && t.count > 0 && (
                <span style={{
                  background: on ? 'rgba(255,255,255,0.22)' : TABI_COLORS.line2,
                  color: on ? '#fff' : TABI_COLORS.ink3,
                  fontSize: 10.5, fontWeight: 700, padding: '1px 6px',
                  borderRadius: 7, lineHeight: '15px',
                }}>{t.count}</span>
              )}
            </button>
          );
        })}
      </div>

      {/* ── Overview Tab ── */}
      {tab === 'overview' && (
        <div style={{ padding: '12px 16px' }}>

          {/* ── Quick Stats Row ── */}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8, marginBottom: 12 }}>
            {[
              { label: '總花費', value: _fmtAmt(total, trip.currency), sub: rate ? `≈ NT$${Math.round(total * rate / 1000)}k` : null },
              { label: '日均花費', value: avgDaily != null ? _fmtAmt(avgDaily, trip.currency) : `${trip.expenses.length}筆`, sub: avgDaily != null ? '旅途中' : '筆數' },
              { label: tripDuration ? '旅行天數' : '成員人數', value: tripDuration ? `${tripDuration}天` : `${trip.members.length}人`, sub: null },
            ].map((s, i) => (
              <div key={i} style={{ background: TABI_COLORS.card, borderRadius: 14, padding: '10px 8px', border: `1px solid ${TABI_COLORS.line}`, textAlign: 'center' }}>
                <div style={{ fontSize: 9, color: TABI_COLORS.ink3, fontWeight: 600, letterSpacing: '0.04em', marginBottom: 5 }}>{s.label}</div>
                <div style={{ fontFamily: tabiSerif, fontSize: 13, fontWeight: 700, color: TABI_COLORS.ink, lineHeight: 1.2 }}>{s.value}</div>
                {s.sub && <div style={{ fontSize: 9, color: TABI_COLORS.ink3, marginTop: 2 }}>{s.sub}</div>}
              </div>
            ))}
          </div>

          {/* ── Settlement Banner ── */}
          {sharedExpenses.length > 0 && (
            <div onClick={() => setShowSettle(true)} style={{
              marginBottom: 12, padding: '12px 14px', borderRadius: 14, cursor: 'pointer',
              background: transfers.length === 0 ? `${TABI_COLORS.green}12` : `${TABI_COLORS.warn}20`,
              border: `1px solid ${transfers.length === 0 ? TABI_COLORS.green + '35' : TABI_COLORS.warn + '60'}`,
              display: 'flex', alignItems: 'center', gap: 10,
            }}>
              <span style={{ fontSize: 22, flexShrink: 0 }}>{transfers.length === 0 ? '✅' : '🤝'}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 700, color: transfers.length === 0 ? TABI_COLORS.green : '#8a6010' }}>
                  {transfers.length === 0 ? '所有人都已結清 ✓' : `需 ${transfers.length} 筆轉帳完成結算`}
                </div>
                {transfers.length > 0 && (
                  <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {transfers.map(t => `${membersMap[t.from]?.display_name} → ${membersMap[t.to]?.display_name} ${_fmtAmt(t.amount, trip.currency)}`).join('　')}
                  </div>
                )}
              </div>
              <span style={{ fontSize: 14, color: TABI_COLORS.ink3, flexShrink: 0 }}>›</span>
            </div>
          )}

          {/* ── Hero Card ── */}
          <div style={{ background: TABI_COLORS.ink, color: '#fff', borderRadius: 18, padding: '18px', marginBottom: 12 }}>
            {trip.start_date && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12, opacity: 0.55, fontSize: 11 }}>
                <span>📅</span>
                <span>{_fmtDateFull(trip.start_date)}</span>
                {trip.end_date && <><span>→</span><span>{_fmtDateFull(trip.end_date)}</span></>}
                {tripDuration && <span style={{ background: 'rgba(255,255,255,0.12)', borderRadius: 6, padding: '1px 7px', marginLeft: 2 }}>{tripDuration}天</span>}
              </div>
            )}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14 }}>
              <div>
                <div style={{ fontSize: 10.5, opacity: 0.6, letterSpacing: '0.14em', fontWeight: 600 }}>我的支出</div>
                <div style={{ fontFamily: tabiSerif, fontSize: 28, fontWeight: 700, marginTop: 2 }}>
                  {_fmtAmt(total, trip.currency)}
                </div>
                {rate && <div style={{ fontSize: 11, opacity: 0.55, marginTop: 2 }}>
                  ≈ NT${Math.round(total * rate).toLocaleString()}
                </div>}
              </div>
              <div style={{ textAlign: 'right' }}>
                <div style={{ fontSize: 10.5, opacity: 0.6, marginBottom: 5 }}>含代購</div>
                <div onClick={() => setProxy(!includeProxy)} style={{
                  width: 44, height: 24, borderRadius: 12, cursor: 'pointer',
                  background: includeProxy ? '#7BC97B' : 'rgba(255,255,255,0.2)',
                  position: 'relative', transition: 'background 0.2s', marginLeft: 'auto',
                }}>
                  <div style={{ position: 'absolute', top: 2, left: includeProxy ? 22 : 2, width: 20, height: 20, borderRadius: 10, background: '#fff', transition: 'left 0.2s' }} />
                </div>
              </div>
            </div>
            <div style={{ display: 'flex', gap: 10, padding: '10px 0', borderTop: '1px solid rgba(255,255,255,0.12)' }}>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 10, opacity: 0.6, marginBottom: 3 }}>事前支出</div>
                <div style={{ fontFamily: tabiSerif, fontSize: 15, fontWeight: 700 }}>{_fmtAmt(preTripAmt, trip.currency)}</div>
                <div style={{ fontSize: 10, opacity: 0.45, marginTop: 1 }}>機票・住宿・門票等</div>
              </div>
              <div style={{ width: 1, background: 'rgba(255,255,255,0.12)' }} />
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 10, opacity: 0.6, marginBottom: 3 }}>旅途中支出</div>
                <div style={{ fontFamily: tabiSerif, fontSize: 15, fontWeight: 700 }}>{_fmtAmt(inTripAmt, trip.currency)}</div>
                <div style={{ fontSize: 10, opacity: 0.45, marginTop: 1 }}>餐飲・交通・購物</div>
              </div>
              {proxyTotal > 0 && <>
                <div style={{ width: 1, background: 'rgba(255,255,255,0.12)' }} />
                <div style={{ flex: 1, cursor: 'pointer' }} onClick={() => setShowProxy(true)}>
                  <div style={{ fontSize: 10, opacity: 0.6, marginBottom: 3 }}>代購金額 →</div>
                  <div style={{ fontFamily: tabiSerif, fontSize: 15, fontWeight: 700, color: '#F5A88E' }}>{_fmtAmt(proxyTotal, trip.currency)}</div>
                  {proxySettled > 0 && (
                    <div style={{ fontSize: 9.5, marginTop: 2 }}>
                      <span style={{ color: '#7BC97B' }}>已還 {_fmtAmt(proxySettled, trip.currency)}</span>
                      {proxySettled < proxyTotal && <span style={{ opacity: 0.5 }}> · 餘 {_fmtAmt(proxyTotal - proxySettled, trip.currency)}</span>}
                    </div>
                  )}
                  {proxySettled === 0 && <div style={{ fontSize: 9.5, opacity: 0.5, marginTop: 1 }}>點擊查看結算</div>}
                </div>
              </>}
            </div>
          </div>

          {/* ── Member Spending Comparison ── */}
          {trip.members.length > 1 && sharedExpenses.length > 0 && (
            <div style={{ background: TABI_COLORS.card, borderRadius: 16, padding: '14px', marginBottom: 12, border: `1px solid ${TABI_COLORS.line}` }}>
              <div style={{ display: 'flex', alignItems: 'center', marginBottom: 14 }}>
                <div style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: TABI_COLORS.ink, flex: 1 }}>成員支出比較</div>
                <button onClick={() => setShowMemberManage(true)} style={{ padding: '4px 10px', borderRadius: 8, border: `1px solid ${TABI_COLORS.line}`, background: 'transparent', color: TABI_COLORS.ink3, fontSize: 11, cursor: 'pointer' }}>編輯成員</button>
              </div>
              {trip.members.map(m => {
                const paid  = memberPaid[m.id]  || 0;
                const share = memberShare[m.id] || 0;
                const bal   = balances[m.id]    || 0;
                const paidW  = Math.round(paid  / maxMemberAmt * 100);
                const shareW = Math.round(share / maxMemberAmt * 100);
                return (
                  <div key={m.id} style={{ marginBottom: 12 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                      <div style={{ width: 24, height: 24, borderRadius: 12, background: m.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, color: '#fff', flexShrink: 0 }}>
                        {(m.display_name || '?')[0]}
                      </div>
                      <div style={{ flex: 1, fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink }}>{m.display_name}</div>
                      <div style={{ fontSize: 11, color: TABI_COLORS.ink3 }}>
                        實付 <span style={{ fontFamily: tabiSerif, fontWeight: 700, color: TABI_COLORS.ink }}>{_fmtAmt(paid, trip.currency)}</span>
                      </div>
                      <div style={{
                        fontSize: 11, fontWeight: 700, minWidth: 52, textAlign: 'right',
                        color: bal >= 0 ? TABI_COLORS.green : TABI_COLORS.red,
                      }}>{bal >= 0 ? '+' : ''}{_fmtAmt(Math.abs(Math.round(bal)), trip.currency)}</div>
                    </div>
                    {/* Share bar (grey bg = should pay) overlaid with paid bar (color fg = actually paid) */}
                    <div style={{ position: 'relative', height: 8, borderRadius: 4, background: TABI_COLORS.bg2, overflow: 'hidden' }}>
                      <div style={{ position: 'absolute', left: 0, top: 0, height: '100%', width: `${shareW}%`, background: `${m.color}30`, borderRadius: 4 }} />
                      <div style={{ position: 'absolute', left: 0, top: 0, height: '100%', width: `${paidW}%`, background: m.color, borderRadius: 4 }} />
                    </div>
                    <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 3 }}>
                      <div style={{ fontSize: 9.5, color: TABI_COLORS.ink3 }}>應付 {_fmtAmt(share, trip.currency)}</div>
                      <div style={{ fontSize: 9.5, color: bal >= 0 ? TABI_COLORS.green : TABI_COLORS.red, fontWeight: 600 }}>
                        {bal >= 0 ? '被欠款' : '欠款'}
                      </div>
                    </div>
                  </div>
                );
              })}
              {transfers.length > 0 && (
                <div style={{ marginTop: 4, paddingTop: 12, borderTop: `1px solid ${TABI_COLORS.line}` }}>
                  <div style={{ fontSize: 11, fontWeight: 600, color: TABI_COLORS.ink3, marginBottom: 8 }}>結算方案</div>
                  {transfers.map((t, i) => (
                    <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                      <div style={{ width: 22, height: 22, borderRadius: 11, background: membersMap[t.from]?.color, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 9, fontWeight: 700, color: '#fff' }}>{(membersMap[t.from]?.display_name || '?')[0]}</div>
                      <div style={{ fontSize: 12, color: TABI_COLORS.ink2, fontWeight: 600 }}>{membersMap[t.from]?.display_name}</div>
                      <div style={{ fontSize: 13, color: TABI_COLORS.ink3 }}>→</div>
                      <div style={{ width: 22, height: 22, borderRadius: 11, background: membersMap[t.to]?.color, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 9, fontWeight: 700, color: '#fff' }}>{(membersMap[t.to]?.display_name || '?')[0]}</div>
                      <div style={{ fontSize: 12, color: TABI_COLORS.ink2, fontWeight: 600, flex: 1 }}>{membersMap[t.to]?.display_name}</div>
                      <div style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: TABI_COLORS.red }}>{_fmtAmt(t.amount, trip.currency)}</div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}

          {/* ── Donut + Category Analysis ── */}
          {catTotals.length > 0 && (() => {
            let cumPct = 0;
            const segments = catTotals.slice(0, 6).map(([cat, amt]) => {
              const pct = statSrcTotal > 0 ? amt / statSrcTotal * 100 : 0;
              const seg = { cat, amt, pct, start: cumPct };
              cumPct += pct;
              return seg;
            });
            const gradient = segments.map((s, i) => `${PALETTE[i % PALETTE.length]} ${s.start.toFixed(1)}% ${(s.start + s.pct).toFixed(1)}%`).join(', ');
            return (
              <div style={{ background: TABI_COLORS.card, borderRadius: 16, padding: '14px', marginBottom: 12, border: `1px solid ${TABI_COLORS.line}` }}>
                <div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
                  <div style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: TABI_COLORS.ink, flex: 1 }}>
                    類別分析{statDay ? ` · ${(() => { const d = new Date(statDay + 'T12:00:00'); return `${d.getMonth()+1}/${d.getDate()}`; })()}` : ''}
                  </div>
                  {(statDay || statCat) && <button onClick={() => { setStatDay(null); setStatCat(null); }} style={{ padding: '3px 9px', borderRadius: 7, border: `1px solid ${TABI_COLORS.line}`, background: 'transparent', fontSize: 11, color: TABI_COLORS.ink3, cursor: 'pointer' }}>清除 ×</button>}
                </div>
                <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
                  {/* Donut */}
                  <div style={{ flexShrink: 0, position: 'relative', width: 90, height: 90 }}>
                    <div style={{ width: 90, height: 90, borderRadius: '50%', background: `conic-gradient(${gradient})` }} />
                    <div style={{
                      position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
                      width: 52, height: 52, borderRadius: '50%', background: TABI_COLORS.card,
                      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center',
                    }}>
                      <div style={{ fontSize: 8, color: TABI_COLORS.ink3, fontWeight: 600, lineHeight: 1.2 }}>合計</div>
                      <div style={{ fontFamily: tabiSerif, fontSize: 9.5, fontWeight: 700, color: TABI_COLORS.ink, lineHeight: 1.2 }}>{_fmtAmt(statSrcTotal, trip.currency)}</div>
                    </div>
                  </div>
                  {/* Legend */}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    {segments.map(({ cat, amt, pct }, i) => {
                      const on = statCat === cat;
                      return (
                        <div key={cat} onClick={() => setStatCat(on ? null : cat)} style={{
                          display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6,
                          padding: '3px 6px', borderRadius: 7, cursor: 'pointer',
                          background: on ? `${PALETTE[i % PALETTE.length]}18` : 'transparent',
                        }}>
                          <div style={{ width: 8, height: 8, borderRadius: 4, background: PALETTE[i % PALETTE.length], flexShrink: 0 }} />
                          <span style={{ fontSize: 11.5, fontWeight: 600, color: on ? PALETTE[i % PALETTE.length] : TABI_COLORS.ink, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{CAT_ICONS[cat] || ''} {cat}</span>
                          <span style={{ fontFamily: tabiSerif, fontSize: 11, fontWeight: 700, color: on ? PALETTE[i % PALETTE.length] : TABI_COLORS.ink, flexShrink: 0 }}>{_fmtAmt(amt, trip.currency)}</span>
                          <span style={{ fontSize: 10, color: TABI_COLORS.ink3, minWidth: 26, textAlign: 'right', flexShrink: 0 }}>{Math.round(pct)}%</span>
                        </div>
                      );
                    })}
                  </div>
                </div>
              </div>
            );
          })()}

          {/* ── Daily Bar Chart ── */}
          {chartDays.length > 0 && (
            <div style={{ background: TABI_COLORS.card, borderRadius: 16, padding: '14px 14px 10px', marginBottom: 12, border: `1px solid ${TABI_COLORS.line}` }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
                <div style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: TABI_COLORS.ink, flex: 1 }}>每日支出</div>
                {hasPreTripDays && (
                  <button onClick={() => setShowPreTrip(v => !v)} style={{
                    padding: '3px 9px', borderRadius: 7, border: `1px solid ${showPreTrip ? TABI_COLORS.blue + '60' : TABI_COLORS.line}`,
                    background: showPreTrip ? `${TABI_COLORS.blue}12` : 'transparent',
                    fontSize: 11, color: showPreTrip ? TABI_COLORS.blue : TABI_COLORS.ink3, cursor: 'pointer', fontWeight: 600,
                  }}>事前支出</button>
                )}
                {(statDay || statCat) && (
                  <button onClick={() => { setStatDay(null); setStatCat(null); }} style={{
                    padding: '3px 9px', borderRadius: 7, border: `1px solid ${TABI_COLORS.line}`,
                    background: 'transparent', fontSize: 11, color: TABI_COLORS.ink3, cursor: 'pointer',
                  }}>清除 ×</button>
                )}
              </div>
              <div style={{ display: 'flex', gap: 2, overflowX: 'auto', paddingBottom: 6, alignItems: 'flex-end', height: 88 }}>
                {chartDays.map(day => {
                  const amt      = dailyMap[day]?.total || 0;
                  const barH     = amt > 0 ? Math.max(6, Math.round((amt / maxDayAmt) * 52)) : 2;
                  const on       = statDay === day;
                  const isPreTrip = startDate && new Date(day + 'T12:00:00') < startDate;
                  const d        = new Date(day + 'T12:00:00');
                  const barColor = on ? TABI_COLORS.red : isPreTrip ? TABI_COLORS.blue : amt > 0 ? `${TABI_COLORS.ink}35` : TABI_COLORS.line;
                  return (
                    <button key={day} onClick={() => { setStatDay(on ? null : day); setStatCat(null); }} style={{
                      display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
                      padding: '4px 5px 0', borderRadius: 8, border: 'none',
                      cursor: amt > 0 ? 'pointer' : 'default',
                      background: on ? `${TABI_COLORS.red}12` : isPreTrip ? `${TABI_COLORS.blue}08` : 'transparent',
                      flexShrink: 0, minWidth: 38, height: '100%', justifyContent: 'flex-end',
                    }}>
                      <div style={{ width: 18, borderRadius: '3px 3px 0 0', height: barH, background: barColor }} />
                      <div style={{ fontSize: 9.5, color: on ? TABI_COLORS.red : isPreTrip ? TABI_COLORS.blue : TABI_COLORS.ink3, fontWeight: 600, whiteSpace: 'nowrap' }}>{d.getMonth()+1}/{d.getDate()}</div>
                      <div style={{ fontSize: 8.5, color: on ? TABI_COLORS.red : isPreTrip ? TABI_COLORS.blue : TABI_COLORS.ink3, whiteSpace: 'nowrap', minHeight: 11 }}>
                        {amt > 0 ? (amt >= 10000 ? `${Math.round(amt/1000)}k` : `¥${amt.toLocaleString()}`) : ''}
                      </div>
                    </button>
                  );
                })}
              </div>
              {showPreTrip && hasPreTripDays && (
                <div style={{ display: 'flex', gap: 12, paddingTop: 8, borderTop: `1px solid ${TABI_COLORS.line}`, marginTop: 4 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                    <div style={{ width: 8, height: 8, borderRadius: 2, background: TABI_COLORS.blue }} />
                    <span style={{ fontSize: 10, color: TABI_COLORS.ink3 }}>事前支出</span>
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                    <div style={{ width: 8, height: 8, borderRadius: 2, background: `${TABI_COLORS.ink}35` }} />
                    <span style={{ fontSize: 10, color: TABI_COLORS.ink3 }}>旅途中</span>
                  </div>
                </div>
              )}
              {statDay && (
                <div style={{ borderTop: `1px solid ${TABI_COLORS.line}`, paddingTop: 10, marginTop: 4 }}>
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                    <div style={{ fontSize: 12, color: TABI_COLORS.ink3 }}>{(() => { const d = new Date(statDay + 'T12:00:00'); return `${d.getMonth()+1}月${d.getDate()}日`; })()}</div>
                    <div style={{ fontFamily: tabiSerif, fontSize: 17, fontWeight: 700, color: TABI_COLORS.ink }}>{_fmtAmt(dailyMap[statDay]?.total || 0, trip.currency)}</div>
                    {rate && <div style={{ fontSize: 10.5, color: TABI_COLORS.ink3 }}>≈ NT${Math.round((dailyMap[statDay]?.total || 0) * rate).toLocaleString()}</div>}
                  </div>
                </div>
              )}
            </div>
          )}

          {/* ── Filtered Expense List ── */}
          {(statDay || statCat) && overviewExps.length > 0 && (
            <div style={{ marginBottom: 12 }}>
              <div style={{ fontSize: 11.5, fontWeight: 600, color: TABI_COLORS.ink3, marginBottom: 8, letterSpacing: '0.05em' }}>
                支出明細
                {statCat && <span style={{ marginLeft: 6, background: `${TABI_COLORS.red}15`, color: TABI_COLORS.red, padding: '2px 7px', borderRadius: 6, fontSize: 11 }}>{CAT_ICONS[statCat] || ''} {statCat}</span>}
              </div>
              {overviewExps.map(e => (
                <ExpenseCard key={e.id} e={e} trip={trip} startDate={startDate} membersMap={membersMap} includeProxy={includeProxy} onTap={() => setEditingExpense(e)} />
              ))}
            </div>
          )}

          {/* ── Trip Recap (trip ended) ── */}
          {isEnded && trip.expenses.length > 0 && (
            <div style={{ background: `linear-gradient(135deg, ${TABI_COLORS.ink} 0%, #2d2520 100%)`, borderRadius: 18, padding: '16px 18px', marginBottom: 12, color: '#fff' }}>
              <div style={{ fontSize: 10.5, opacity: 0.55, letterSpacing: '0.15em', fontWeight: 600, marginBottom: 8 }}>旅程回顧 · TRIP RECAP</div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
                <div>
                  <div style={{ fontSize: 9, opacity: 0.55, marginBottom: 3 }}>旅行總花費</div>
                  <div style={{ fontFamily: tabiSerif, fontSize: 18, fontWeight: 700 }}>{_fmtAmt(total, trip.currency)}</div>
                  {rate && <div style={{ fontSize: 10, opacity: 0.45 }}>≈ NT${Math.round(total * rate).toLocaleString()}</div>}
                </div>
                <div>
                  <div style={{ fontSize: 9, opacity: 0.55, marginBottom: 3 }}>每日平均</div>
                  <div style={{ fontFamily: tabiSerif, fontSize: 18, fontWeight: 700 }}>{avgDaily != null ? _fmtAmt(avgDaily, trip.currency) : '—'}</div>
                  {avgDaily && rate && <div style={{ fontSize: 10, opacity: 0.45 }}>≈ NT${Math.round(avgDaily * rate).toLocaleString()}</div>}
                </div>
                {catTotals[0] && <div>
                  <div style={{ fontSize: 9, opacity: 0.55, marginBottom: 3 }}>最多支出類別</div>
                  <div style={{ fontSize: 14, fontWeight: 700 }}>{CAT_ICONS[catTotals[0][0]] || ''} {catTotals[0][0]}</div>
                  <div style={{ fontSize: 10, opacity: 0.5 }}>{_fmtAmt(catTotals[0][1], trip.currency)}</div>
                </div>}
                {chartDays.length > 0 && (() => {
                  const topDay = chartDays.reduce((best, d) => (dailyMap[d]?.total || 0) > (dailyMap[best]?.total || 0) ? d : best, chartDays[0]);
                  const topD = new Date(topDay + 'T12:00:00');
                  return <div>
                    <div style={{ fontSize: 9, opacity: 0.55, marginBottom: 3 }}>最高消費日</div>
                    <div style={{ fontSize: 14, fontWeight: 700 }}>{topD.getMonth()+1}/{topD.getDate()}</div>
                    <div style={{ fontSize: 10, opacity: 0.5 }}>{_fmtAmt(dailyMap[topDay]?.total || 0, trip.currency)}</div>
                  </div>;
                })()}
              </div>
            </div>
          )}

          {/* ── Delete trip — owner only ── */}
          {trip.is_owner && (
            <div style={{ paddingTop: 4 }}>
              {!showDeleteConfirm ? (
                <button onClick={() => setShowDeleteConfirm(true)} style={{
                  width: '100%', padding: '11px', borderRadius: 12, cursor: 'pointer',
                  border: `1px solid rgba(200,85,61,0.25)`, background: 'transparent',
                  fontSize: 13, color: TABI_COLORS.red, fontWeight: 600,
                }}>刪除此旅程</button>
              ) : (
                <div style={{ borderRadius: 12, border: `1px solid ${TABI_COLORS.red}40`, background: `${TABI_COLORS.red}08`, padding: '12px 14px' }}>
                  <div style={{ fontSize: 13, fontWeight: 600, color: TABI_COLORS.ink, marginBottom: 4 }}>確定要刪除「{trip.name}」？</div>
                  <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginBottom: 12 }}>所有支出與發票記錄將一併刪除，且無法復原。</div>
                  <div style={{ display: 'flex', gap: 8 }}>
                    <button onClick={() => setShowDeleteConfirm(false)} style={{ flex: 1, padding: '10px', borderRadius: 10, border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.card, fontSize: 13, cursor: 'pointer', color: TABI_COLORS.ink2 }}>取消</button>
                    <button onClick={deleteTrip} disabled={deleting} style={{ flex: 1, padding: '10px', borderRadius: 10, border: 'none', background: deleting ? TABI_COLORS.line2 : TABI_COLORS.red, color: '#fff', fontSize: 13, fontWeight: 700, cursor: deleting ? 'not-allowed' : 'pointer' }}>{deleting ? '刪除中…' : '確定刪除'}</button>
                  </div>
                </div>
              )}
            </div>
          )}
        </div>
      )}

      {/* ── 支出(個人) Tab ── */}
      {tab === 'expenses_personal' && (
        <>
          <DateFilterBar trip={trip} startDate={startDate} expenses={sortedPersonal} receipts={[]} dateFilter={dateFilter} onSelect={setDateFilter} />
          <div style={{ padding: '10px 16px' }}>
            {filteredPersonal.length === 0 ? (
              <div style={{ textAlign: 'center', padding: '52px 0', color: TABI_COLORS.ink3 }}>
                <div style={{ fontSize: 38, marginBottom: 10 }}>💳</div>
                <div style={{ fontSize: 14, fontWeight: 500 }}>{dateFilter === 'all' ? '尚無個人支出' : '此日期沒有支出'}</div>
                <div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>點擊右下角 + 新增支出</div>
              </div>
            ) : (() => {
              const groups = {};
              filteredPersonal.forEach(e => {
                const day = e.expense_date.substring(0, 10);
                if (!groups[day]) groups[day] = [];
                groups[day].push(e);
              });
              const days = Object.keys(groups).sort().reverse();
              const showHeaders = dateFilter === 'all' || dateFilter === 'pre';
              return days.map(day => {
                const d = new Date(day + 'T12:00:00');
                const dayLabel = d.getMonth() + 1 + '月' + d.getDate() + '日';
                const dayTotal = groups[day].reduce((s, e) => s + myShare(e), 0);
                return (
                  <div key={day}>
                    {showHeaders && (
                      <div style={{ display: 'flex', alignItems: 'center', marginTop: 16, marginBottom: 2, gap: 8 }}>
                        <div style={{ fontSize: 12, color: TABI_COLORS.ink3, fontWeight: 700, flexShrink: 0 }}>{dayLabel}</div>
                        <div style={{ flex: 1, height: 1, background: TABI_COLORS.line }} />
                        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600, flexShrink: 0 }}>{_fmtAmt(dayTotal, trip.currency)}</div>
                      </div>
                    )}
                    {groups[day].map(e => <ExpenseCard key={e.id} e={e} trip={trip} startDate={startDate} membersMap={membersMap} includeProxy={includeProxy} onTap={() => setEditingExpense(e)} />)}
                  </div>
                );
              });
            })()}
          </div>
        </>
      )}

      {/* ── 支出(共同) Tab ── */}
      {tab === 'expenses_shared' && (
        <>
          <DateFilterBar trip={trip} startDate={startDate} expenses={sortedShared} receipts={[]} dateFilter={dateFilter} onSelect={setDateFilter} />
          <div style={{ padding: '10px 16px' }}>
            {/* Settlement button */}
            <button onClick={() => setShowSettle(true)} style={{
              width: '100%', padding: '14px 16px', borderRadius: 18, marginBottom: 12,
              border: 'none', background: TABI_COLORS.card,
              boxShadow: '0 2px 14px rgba(26,22,18,0.07)',
              display: 'flex', alignItems: 'center', gap: 14, cursor: 'pointer', textAlign: 'left',
            }}>
              <div style={{
                width: 42, height: 42, borderRadius: 13, flexShrink: 0,
                background: transfers.length === 0 ? `${TABI_COLORS.green}15` : `${TABI_COLORS.warn}15`,
                display: 'grid', placeItems: 'center', fontSize: 20,
              }}>{transfers.length === 0 ? '✅' : '🤝'}</div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 14, fontWeight: 700, color: TABI_COLORS.ink }}>查看結算</div>
                <div style={{ fontSize: 12, color: TABI_COLORS.ink3, marginTop: 2 }}>
                  {transfers.length === 0 ? '所有人都已結清 ✓' : `需 ${transfers.length} 筆轉帳完成結算`}
                </div>
              </div>
              <svg width="7" height="12" viewBox="0 0 7 12" fill="none" style={{ opacity: 0.35 }}>
                <path d="M1 1l5 5-5 5" stroke={TABI_COLORS.ink} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            </button>

            {filteredShared.length === 0 ? (
              <div style={{ textAlign: 'center', padding: '52px 0', color: TABI_COLORS.ink3 }}>
                <div style={{ fontSize: 38, marginBottom: 10 }}>💳</div>
                <div style={{ fontSize: 14, fontWeight: 500 }}>{dateFilter === 'all' ? '尚無共同支出' : '此日期沒有支出'}</div>
                <div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>點擊右下角 + 新增支出</div>
              </div>
            ) : (() => {
              const groups = {};
              filteredShared.forEach(e => {
                const day = e.expense_date.substring(0, 10);
                if (!groups[day]) groups[day] = [];
                groups[day].push(e);
              });
              const days = Object.keys(groups).sort().reverse();
              const showHeaders = dateFilter === 'all' || dateFilter === 'pre';
              return days.map(day => {
                const d = new Date(day + 'T12:00:00');
                const dayLabel = d.getMonth() + 1 + '月' + d.getDate() + '日';
                const dayTotal = groups[day].reduce((s, e) => s + e.amount, 0);
                return (
                  <div key={day}>
                    {showHeaders && (
                      <div style={{ display: 'flex', alignItems: 'center', marginTop: 16, marginBottom: 2, gap: 8 }}>
                        <div style={{ fontSize: 12, color: TABI_COLORS.ink3, fontWeight: 700, flexShrink: 0 }}>{dayLabel}</div>
                        <div style={{ flex: 1, height: 1, background: TABI_COLORS.line }} />
                        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600, flexShrink: 0 }}>{_fmtAmt(dayTotal, trip.currency)}</div>
                      </div>
                    )}
                    {groups[day].map(e => <ExpenseCard key={e.id} e={e} trip={trip} startDate={startDate} membersMap={membersMap} includeProxy={includeProxy} onTap={() => setEditingExpense(e)} />)}
                  </div>
                );
              });
            })()}
          </div>
        </>
      )}

      {/* ── Receipts Tab ── */}
      {tab === 'receipts' && (
        <>
          <DateFilterBar trip={trip} startDate={startDate} expenses={[]} receipts={sortedReceipts} dateFilter={dateFilter} onSelect={setDateFilter} />
          <div style={{ padding: '10px 16px' }}>
            <button onClick={() => setSheet('receipt')} style={{
              width: '100%', padding: '14px', borderRadius: 16,
              border: `2px dashed ${TABI_COLORS.line2}`, background: 'transparent',
              cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
              gap: 8, color: TABI_COLORS.ink2, marginBottom: 14,
            }}>
              <span style={{ fontSize: 20 }}>📷</span>
              <span style={{ fontSize: 14, fontWeight: 600 }}>掃描新發票</span>
            </button>

            {loadingR && <div style={{ textAlign: 'center', color: TABI_COLORS.ink3, padding: '30px 0' }}>載入中…</div>}
            {!loadingR && filteredReceipts.length === 0 && (
              <div style={{ textAlign: 'center', padding: '48px 0', color: TABI_COLORS.ink3 }}>
                <div style={{ fontSize: 36, marginBottom: 10 }}>🧾</div>
                <div style={{ fontSize: 14, fontWeight: 500 }}>{dateFilter === 'all' ? '尚無發票紀錄' : '此日期沒有發票'}</div>
                <div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>點擊上方按鈕掃描發票</div>
              </div>
            )}
            {filteredReceipts.map(r => (
              <div key={r.id} onClick={() => setViewingReceipt(r)} style={{
                background: TABI_COLORS.card, borderRadius: 18, padding: '14px 16px',
                marginTop: 10, cursor: 'pointer',
                boxShadow: '0 2px 14px rgba(26,22,18,0.07)',
                display: 'flex', alignItems: 'center', gap: 14,
              }}>
                <div style={{
                  width: 46, height: 46, borderRadius: 15, flexShrink: 0,
                  background: r.scope === 'shared' ? '#1A4D8F12' : `${TABI_COLORS.green}12`,
                  display: 'grid', placeItems: 'center', fontSize: 22,
                }}>🧾</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontFamily: tabiSerif, fontSize: 15, fontWeight: 600, color: TABI_COLORS.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {r.store_name || '未知店家'}
                  </div>
                  <div style={{ fontSize: 12, color: TABI_COLORS.ink3, marginTop: 4, display: 'flex', alignItems: 'center', gap: 7 }}>
                    <span>{r.receipt_date || _fmtDateFull(r.created_at)}</span>
                    <span style={{
                      background: r.scope === 'shared' ? '#1A4D8F15' : `${TABI_COLORS.green}18`,
                      color: r.scope === 'shared' ? '#1A4D8F' : TABI_COLORS.green,
                      padding: '2px 8px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                    }}>{r.scope === 'shared' ? '共同' : '個人'}</span>
                  </div>
                </div>
                {r.total_amount != null && (
                  <div style={{ fontFamily: tabiSerif, fontSize: 18, fontWeight: 700, color: TABI_COLORS.ink, flexShrink: 0 }}>
                    ¥{r.total_amount.toLocaleString()}
                  </div>
                )}
                <svg width="7" height="12" viewBox="0 0 7 12" fill="none" style={{ flexShrink: 0, opacity: 0.3 }}>
                  <path d="M1 1l5 5-5 5" stroke={TABI_COLORS.ink} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              </div>
            ))}
          </div>
        </>
      )}


      {/* FAB — add expense, clears tab bar (≈57px) + safe area */}
      {(tab === 'expenses_personal' || tab === 'expenses_shared' || tab === 'overview') && (
        <button onClick={() => setSheet('expense')} style={{
          position: 'fixed', bottom: 'calc(env(safe-area-inset-bottom, 0px) + 76px)', right: 16,
          width: 52, height: 52, borderRadius: 26, border: 'none', cursor: 'pointer',
          background: TABI_COLORS.red, color: '#fff', fontSize: 26, lineHeight: 1,
          boxShadow: '0 12px 30px rgba(200,85,61,0.4)', zIndex: 50,
        }}>+</button>
      )}

      {sheet === 'invite' && (
        <TripInviteSheet
          trip={trip}
          inviteCode={inviteCode}
          inviteExpiry={inviteExpiry}
          onClose={() => setSheet(null)}
          onRegenerate={generateInvite}
        />
      )}
      {sheet === 'expense' && (
        <AddExpenseSheet
          trip={trip}
          user={user}
          onClose={() => setSheet(null)}
          onSaved={() => { setSheet(null); loadTrip(); }}
        />
      )}
      {sheet === 'receipt' && (
        <ScanReceiptSheet
          trip={trip}
          onClose={() => setSheet(null)}
          onSaved={() => { setSheet(null); loadTrip(); loadReceipts(); }}
        />
      )}
      {editingExpense && (
        <EditExpenseSheet
          trip={trip}
          expense={editingExpense}
          onClose={() => setEditingExpense(null)}
          onSaved={() => { setEditingExpense(null); loadTrip(); }}
          onDeleted={() => { setEditingExpense(null); loadTrip(); }}
        />
      )}
      {viewingReceipt && (
        <ReceiptDetailSheet
          receipt={viewingReceipt}
          onClose={() => setViewingReceipt(null)}
          onSaved={() => { setViewingReceipt(null); loadReceipts(); }}
          onDeleted={() => { setViewingReceipt(null); loadReceipts(); }}
        />
      )}
      {showSettle && (
        <SettlementSheet
          transfers={transfers}
          membersMap={membersMap}
          rate={rate}
          onClose={() => setShowSettle(false)}
          onSettleAll={async () => {
            await fetch(`/api/trips/${trip.id}/settle-all`, {
              method: 'POST',
              headers: { Authorization: `Bearer ${window.tabiToken()}` },
            });
            setShowSettle(false);
            loadTrip();
          }}
        />
      )}
      {showProxy && (
        <ProxySheet
          trip={trip}
          onClose={() => setShowProxy(false)}
          onSaved={() => { setShowProxy(false); loadTrip(); }}
        />
      )}
      {showMemberManage && (
        <MemberManageSheet
          trip={trip}
          onClose={() => setShowMemberManage(false)}
          onSaved={() => { loadTrip(); }}
        />
      )}
    </div>
  );
}

// ─────────── MEMBER MANAGE SHEET ───────────
function MemberManageSheet({ trip, onClose, onSaved }) {
  const [editing, setEditing]   = React.useState(null);
  const [editName, setEditName] = React.useState('');
  const [editColor, setEditColor] = React.useState('');
  const [saving, setSaving]     = React.useState(false);
  const [addName, setAddName]   = React.useState('');
  const [addColor, setAddColor] = React.useState(MEMBER_PALETTE[0]);
  const [adding, setAdding]     = React.useState(false);
  const [error, setError]       = React.useState('');
  const [confirmDel, setConfirmDel] = React.useState(null); // member object
  const [deleting, setDeleting] = React.useState(false);
  const [delError, setDelError] = React.useState('');

  const openEdit = (m) => {
    setEditing(m.id);
    setEditName(m.display_name);
    setEditColor(m.color);
    setError('');
  };

  const saveEdit = async () => {
    if (!editName.trim()) return;
    setSaving(true);
    await fetch(`/api/trips/${trip.id}/members/${editing}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
      body: JSON.stringify({ display_name: editName.trim(), color: editColor }),
    });
    setSaving(false);
    setEditing(null);
    onSaved();
  };

  const addMember = async () => {
    if (!addName.trim()) { setError('請輸入名稱'); return; }
    setAdding(true);
    setError('');
    const r = await fetch(`/api/trips/${trip.id}/members`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
      body: JSON.stringify({ display_name: addName.trim(), color: addColor }),
    });
    setAdding(false);
    if (!r.ok) { setError('新增失敗，請再試一次'); return; }
    setAddName('');
    setAddColor(MEMBER_PALETTE[0]);
    onSaved();
  };

  const removeMember = async () => {
    setDeleting(true);
    setDelError('');
    const r = await fetch(`/api/trips/${trip.id}/members/${confirmDel.id}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    });
    setDeleting(false);
    if (!r.ok) {
      const d = await r.json().catch(() => ({}));
      setDelError(d.detail || '移除失敗，請再試一次');
      return;
    }
    setConfirmDel(null);
    onSaved();
  };

  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, zIndex: 9200, display: 'flex', alignItems: 'flex-end' }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.45)' }} />
      <div style={{
        position: 'relative', width: '100%', background: TABI_COLORS.bg,
        borderRadius: '20px 20px 0 0', padding: '12px 0 32px', maxHeight: '80vh', overflowY: 'auto',
      }}>
        {/* Handle */}
        <div style={{ width: 36, height: 4, borderRadius: 2, background: TABI_COLORS.line2, margin: '6px auto 16px' }} />

        {/* Header */}
        <div style={{ padding: '0 20px 14px', borderBottom: `1px solid ${TABI_COLORS.line}`, display: 'flex', alignItems: 'center' }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontFamily: tabiSerif, fontSize: 18, fontWeight: 700, color: TABI_COLORS.ink }}>編輯成員</div>
            <div style={{ fontSize: 11, color: TABI_COLORS.ink3, marginTop: 2 }}>{trip.members.length} 位成員</div>
          </div>
          <button onClick={onClose} style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 4, color: TABI_COLORS.ink3, fontSize: 20, lineHeight: 1 }}>×</button>
        </div>

        {/* Member list */}
        <div style={{ padding: '10px 16px' }}>
          {trip.members.map(m => (
            <div key={m.id} style={{ marginBottom: 10 }}>
              {editing === m.id ? (
                <div style={{ background: TABI_COLORS.card, borderRadius: 14, padding: '14px', border: `1px solid ${TABI_COLORS.line}` }}>
                  <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600, marginBottom: 6 }}>名稱</div>
                  <input
                    value={editName}
                    onChange={e => setEditName(e.target.value)}
                    style={{ width: '100%', padding: '9px 12px', borderRadius: 10, border: `1px solid ${TABI_COLORS.line}`, fontSize: 16, color: TABI_COLORS.ink, background: TABI_COLORS.bg, outline: 'none', boxSizing: 'border-box' }}
                    autoFocus
                  />
                  <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 600, margin: '10px 0 6px' }}>顏色</div>
                  <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                    {MEMBER_PALETTE.map(c => (
                      <button key={c} onClick={() => setEditColor(c)} style={{
                        width: 32, height: 32, borderRadius: 16, background: c, border: 'none', cursor: 'pointer',
                        boxShadow: editColor === c ? `0 0 0 3px ${TABI_COLORS.bg}, 0 0 0 5px ${c}` : 'none',
                      }} />
                    ))}
                  </div>
                  <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
                    <button onClick={() => setEditing(null)} style={{
                      flex: 1, padding: '10px', borderRadius: 10, border: `1px solid ${TABI_COLORS.line2}`,
                      background: 'transparent', color: TABI_COLORS.ink, fontSize: 13, fontWeight: 600, cursor: 'pointer',
                    }}>取消</button>
                    <button onClick={saveEdit} disabled={saving} style={{
                      flex: 1, padding: '10px', borderRadius: 10, border: 'none',
                      background: TABI_COLORS.ink, color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer',
                    }}>{saving ? '儲存中…' : '儲存'}</button>
                  </div>
                </div>
              ) : (
                <div style={{
                  background: TABI_COLORS.card, borderRadius: 14, padding: '12px 14px',
                  border: `1px solid ${TABI_COLORS.line}`, display: 'flex', alignItems: 'center', gap: 12,
                }}>
                  <MemberAvatar m={m} size={36} />
                  <div style={{ flex: 1 }}>
                    <div style={{ fontFamily: tabiSerif, fontSize: 15, fontWeight: 600, color: TABI_COLORS.ink }}>{m.display_name}</div>
                  </div>
                  <div style={{ display: 'flex', gap: 6 }}>
                    <button onClick={() => openEdit(m)} style={{
                      padding: '6px 14px', borderRadius: 9, border: `1px solid ${TABI_COLORS.line2}`,
                      background: 'transparent', color: TABI_COLORS.ink2, fontSize: 12, fontWeight: 600, cursor: 'pointer',
                    }}>編輯</button>
                    {m.role !== 'owner' && (
                      <button onClick={() => { setConfirmDel(m); setDelError(''); }} style={{
                        padding: '6px 10px', borderRadius: 9, border: `1px solid ${TABI_COLORS.line2}`,
                        background: 'transparent', color: TABI_COLORS.red, fontSize: 12, fontWeight: 600, cursor: 'pointer',
                      }}>移除</button>
                    )}
                  </div>
                </div>
              )}
            </div>
          ))}
        </div>

        {/* Add new member */}
        <div style={{ padding: '12px 16px 0', borderTop: `1px solid ${TABI_COLORS.line}` }}>
          <div style={{ fontFamily: tabiSerif, fontSize: 14, fontWeight: 700, color: TABI_COLORS.ink, marginBottom: 10 }}>新增成員</div>
          <input
            value={addName}
            onChange={e => { setAddName(e.target.value); setError(''); }}
            placeholder="成員名稱"
            style={{ width: '100%', padding: '9px 12px', borderRadius: 10, border: `1px solid ${TABI_COLORS.line}`, fontSize: 16, color: TABI_COLORS.ink, background: TABI_COLORS.bg, outline: 'none', boxSizing: 'border-box', marginBottom: 10 }}
          />
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 }}>
            {MEMBER_PALETTE.map(c => (
              <button key={c} onClick={() => setAddColor(c)} style={{
                width: 32, height: 32, borderRadius: 16, background: c, border: 'none', cursor: 'pointer',
                boxShadow: addColor === c ? `0 0 0 3px ${TABI_COLORS.bg}, 0 0 0 5px ${c}` : 'none',
              }} />
            ))}
          </div>
          {error && <div style={{ color: TABI_COLORS.red, fontSize: 12, marginBottom: 8 }}>{error}</div>}
          <button onClick={addMember} disabled={adding} style={{
            width: '100%', padding: '12px', borderRadius: 12, border: 'none',
            background: TABI_COLORS.ink, color: '#fff', fontSize: 14, fontWeight: 700, cursor: 'pointer',
          }}>{adding ? '新增中…' : '+ 新增成員'}</button>
        </div>
      </div>

      {/* Delete confirmation */}
      {confirmDel && ReactDOM.createPortal(
        <div onClick={() => setConfirmDel(null)} style={{
          position: 'fixed', inset: 0, zIndex: 9300,
          background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
        }}>
          <div onClick={e => e.stopPropagation()} style={{
            background: TABI_COLORS.bg, borderRadius: 18, padding: '22px', width: '100%', maxWidth: 280,
          }}>
            <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 700, color: TABI_COLORS.ink }}>
              移除「{confirmDel.display_name}」？
            </div>
            <div style={{ fontSize: 12, color: TABI_COLORS.ink3, marginTop: 6, lineHeight: 1.5 }}>
              移除後無法復原。若該成員有付款紀錄則無法移除。
            </div>
            {delError && <div style={{ fontSize: 12, color: TABI_COLORS.red, marginTop: 8 }}>{delError}</div>}
            <div style={{ display: 'flex', gap: 8, marginTop: 18 }}>
              <button onClick={() => setConfirmDel(null)} style={{
                flex: 1, padding: '11px', borderRadius: 12, border: `1px solid ${TABI_COLORS.line2}`,
                background: 'transparent', color: TABI_COLORS.ink, fontSize: 13, fontWeight: 600, cursor: 'pointer',
              }}>取消</button>
              <button onClick={removeMember} disabled={deleting} style={{
                flex: 1, padding: '11px', borderRadius: 12, border: 'none',
                background: TABI_COLORS.red, color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer',
              }}>{deleting ? '移除中…' : '移除'}</button>
            </div>
          </div>
        </div>,
        document.body
      )}
    </div>,
    document.body
  );
}

Object.assign(window, {
  computeSettlement, MemberAvatar, ExpenseCard,
  DateFilterBar, SettlementSheet, ProxySheet,
  TripsScreen, TripDetailScreen,
  AddExpenseSheet, EditExpenseSheet, CreateTripSheet, JoinTripSheet, ScanReceiptSheet, ReceiptDetailSheet,
  MemberManageSheet,
});
