// TruckOrderDelivery.jsx — per-vendor delivery logging.
//
// Drops into the portal's OrderSummaryView VendorSection. Same logic
// runs on the iPad via window.TruckDeliveryPanel — the component is
// surface-agnostic; only the styling assumes portal-ish CSS vars.
//
// Phase 1 (this file): get-or-create the delivery row, upload photos
// to Supabase Storage, render the photo grid, support delete.
// Phase 2 (later): kick the extract-invoice Edge Function and surface
// AI extraction + discrepancy results inline.

const INVOICE_BUCKET = 'truck-invoices';

// Get-or-create the delivery row for (order, vendor). Returns the row.
async function getOrCreateDelivery(orderId, vendorId) {
  if (!window.supa) return null;
  const restId = window.RESTAURANT_ID;
  const { data: existing, error: e1 } = await window.supa
    .from('truck_order_deliveries')
    .select('*')
    .eq('order_id', orderId)
    .eq('vendor_id', vendorId)
    .maybeSingle();
  if (e1) { console.error('delivery lookup failed', e1); return null; }
  if (existing) return existing;

  const { data: created, error: e2 } = await window.supa
    .from('truck_order_deliveries')
    .insert({ order_id: orderId, vendor_id: vendorId, restaurant_id: restId })
    .select()
    .single();
  if (e2) { console.error('delivery create failed', e2); return null; }
  return created;
}

// Upload one File to Storage, return the storage key.
async function uploadInvoicePhoto(delivery, file) {
  const restId = window.RESTAURANT_ID;
  const ext = (file.name.split('.').pop() || 'jpg')
    .toLowerCase()
    .replace(/[^a-z0-9]/g, '') || 'jpg';
  const uuid = crypto.randomUUID
    ? crypto.randomUUID()
    : (Date.now() + '-' + Math.random().toString(36).slice(2));
  const key = `${restId}/${delivery.order_id}/${delivery.vendor_id}/${delivery.id}/${uuid}.${ext}`;
  const { error } = await window.supa.storage
    .from(INVOICE_BUCKET)
    .upload(key, file, {
      contentType: file.type || 'image/' + ext,
      upsert: false,
    });
  if (error) { console.error('storage upload failed', error); throw error; }
  return key;
}

function publicUrlFor(key) {
  if (!window.supa || !key) return '';
  return window.supa.storage.from(INVOICE_BUCKET).getPublicUrl(key).data.publicUrl;
}

// Save the updated photo_keys array.
async function setPhotoKeys(deliveryId, keys) {
  await window.supa
    .from('truck_order_deliveries')
    .update({ photo_keys: keys, updated_at: new Date().toISOString() })
    .eq('id', deliveryId);
}

// Kick the extract-invoice Edge Function. Fire-and-forget — the
// function flips ai_status and writes results back, which we receive
// via the realtime subscription. We optimistically mark the row
// 'queued' here so the UI flips state instantly even before the
// function's own update lands.
async function triggerExtract(deliveryId) {
  if (!window.supa || !deliveryId) return;
  await window.supa.from('truck_order_deliveries')
    .update({ ai_status: 'queued', ai_error: null })
    .eq('id', deliveryId);
  void window.supa.functions
    .invoke('extract-invoice', { body: { delivery_id: deliveryId } })
    .catch(err => console.warn('extract-invoice invoke failed', err));
}

// ------------------------------------------------------------------
// Main panel — usable inside the portal VendorSection.
// ------------------------------------------------------------------
const TruckDeliveryPanel = ({ order, vendor, currentStaff = null }) => {
  const [delivery, setDelivery] = useState(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');
  const [previewKey, setPreviewKey] = useState(null);
  const fileInputRef = useRef(null);
  const cameraInputRef = useRef(null);

  // Load + subscribe to this delivery so cross-device uploads land live.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const d = await getOrCreateDelivery(order.id, vendor.id);
      if (!cancelled) setDelivery(d);
    })();

    if (!window.supa) return;
    const ch = window.supa
      .channel(`delivery:${order.id}:${vendor.id}`)
      .on('postgres_changes', {
        event: '*', schema: 'public', table: 'truck_order_deliveries',
        filter: 'order_id=eq.' + order.id,
      }, (payload) => {
        const r = payload.new || payload.old;
        if (!r || r.vendor_id !== vendor.id) return;
        if (payload.eventType === 'DELETE') return;
        setDelivery(r);
      })
      .subscribe();
    return () => { cancelled = true; window.supa.removeChannel(ch); };
  }, [order.id, vendor.id]);

  const handleFiles = async (fileList) => {
    if (!fileList || fileList.length === 0 || !delivery) return;
    setError(''); setBusy(true);
    try {
      const files = Array.from(fileList);
      const keys = [];
      for (const f of files) {
        const k = await uploadInvoicePhoto(delivery, f);
        keys.push(k);
      }
      const merged = [...(delivery.photo_keys || []), ...keys];
      await setPhotoKeys(delivery.id, merged);
      if (!delivery.received_by_staff_id && currentStaff?.id) {
        await window.supa
          .from('truck_order_deliveries')
          .update({ received_by_staff_id: currentStaff.id })
          .eq('id', delivery.id);
      }
      setDelivery(d => d ? { ...d, photo_keys: merged } : d);
      // Kick the AI extraction. Fire-and-forget — realtime will deliver
      // ai_status / ai_extracted_lines updates as they happen.
      triggerExtract(delivery.id);
    } catch (e) {
      setError(e?.message || 'Upload failed.');
    } finally {
      setBusy(false);
      if (fileInputRef.current) fileInputRef.current.value = '';
      if (cameraInputRef.current) cameraInputRef.current.value = '';
    }
  };

  const reRunExtraction = () => {
    if (!delivery) return;
    triggerExtract(delivery.id);
  };

  const confirmMatch = async () => {
    if (!delivery) return;
    const stamp = new Date().toISOString();
    const patch = {
      confirmed_match: true,
      confirmed_at: stamp,
      confirmed_by_staff_id: currentStaff?.id || null,
    };
    await window.supa
      .from('truck_order_deliveries')
      .update(patch)
      .eq('id', delivery.id);
    setDelivery(d => d ? { ...d, ...patch } : d);
  };

  // Wrong invoice — drop everything and let the user start over. Wipes
  // photo_keys + AI fields. Storage objects are best-effort removed
  // (RLS allows admin DELETE on truck-invoices).
  const discardDelivery = async () => {
    if (!delivery) return;
    if (!confirm('Discard all photos + AI results for this delivery? You\'ll re-upload from scratch.')) return;
    const oldKeys = delivery.photo_keys || [];
    const patch = {
      photo_keys: [],
      ai_status: 'pending',
      ai_extracted_lines: [],
      ai_discrepancies: {},
      ai_invoice_date: null,
      ai_invoice_vendor_name: null,
      ai_invoice_total_units: null,
      ai_match_confidence: null,
      ai_match_breakdown: {},
      ai_error: null,
      ai_ran_at: null,
      confirmed_match: false,
      confirmed_at: null,
      confirmed_by_staff_id: null,
    };
    await window.supa
      .from('truck_order_deliveries')
      .update(patch)
      .eq('id', delivery.id);
    if (oldKeys.length > 0) {
      void window.supa.storage.from(INVOICE_BUCKET).remove(oldKeys).then(() => {});
    }
    setDelivery(d => d ? { ...d, ...patch } : d);
  };

  // Apply a single price-change suggestion: updates inventory_items
  // .current_price and appends a price_history entry. Admin-only (RLS
  // gates inventory writes); the iPad version omits this action.
  const acceptPriceChange = async (change) => {
    if (!change?.item_id || typeof change.new_price !== 'number') return;
    // Read current to append a history line.
    const { data: it } = await window.supa
      .from('inventory_items')
      .select('current_price, price_history')
      .eq('id', change.item_id).maybeSingle();
    const history = Array.isArray(it?.price_history) ? it.price_history.slice() : [];
    const nowIso = new Date().toISOString();
    history.push({
      // `date` (yyyy-mm-dd) is what the Inventory page sorts + displays by;
      // manual edits from the Inventory editor write only `date`. Without
      // it, the inventory list would crash trying to localeCompare `undefined`.
      date: nowIso.slice(0, 10),
      price: change.new_price,
      previous: it?.current_price ?? null,
      at: nowIso,
      source: 'delivery:' + delivery.id,
    });
    await window.supa.from('inventory_items')
      .update({ current_price: change.new_price, price_history: history })
      .eq('id', change.item_id);
    // Drop the change from ai_discrepancies — both locally (instant UI
    // feedback) AND in the DB. Without the DB write, refreshing the page
    // re-reads the stale snapshot and the accepted change reappears even
    // though current_price is already updated. The snapshot is the cached
    // result of the last extract-invoice run; trimming it here keeps the
    // post-acceptance view consistent without a costly re-extraction.
    const curDisc = delivery.ai_discrepancies || {};
    const newDisc = {
      ...curDisc,
      price_changes: (curDisc.price_changes || []).filter(c => c.item_id !== change.item_id),
    };
    await window.supa.from('truck_order_deliveries')
      .update({ ai_discrepancies: newDisc, updated_at: new Date().toISOString() })
      .eq('id', delivery.id);
    setDelivery(d => d ? { ...d, ai_discrepancies: newDisc } : d);
  };

  // Accept EVERY pending price change at once — same effect as clicking each
  // Accept, but batched: update each item's current_price + append a
  // price_history line, then clear them all from the discrepancy snapshot in
  // one write. Once the snapshot has no price_changes left (and the match is
  // trusted/confirmed), the Orders list flips this delivery to "Matched".
  const acceptAllPriceChanges = async () => {
    const disc = delivery.ai_discrepancies || {};
    // Dedupe by item_id: one update per item (a second change for the same
    // item would otherwise race both reads on the same price_history base).
    const seenItems = new Set();
    const changes = (disc.price_changes || []).filter(c => {
      if (!c || !c.item_id || typeof c.new_price !== 'number' || seenItems.has(c.item_id)) return false;
      seenItems.add(c.item_id);
      return true;
    });
    if (changes.length === 0) return;
    const nowIso = new Date().toISOString();
    const ids = changes.map(c => c.item_id);
    const { data: items } = await window.supa
      .from('inventory_items')
      .select('id, current_price, price_history')
      .in('id', ids);
    const byId = new Map((items || []).map(it => [it.id, it]));
    await Promise.all(changes.map(change => {
      const it = byId.get(change.item_id);
      const history = Array.isArray(it?.price_history) ? it.price_history.slice() : [];
      history.push({
        date: nowIso.slice(0, 10),
        price: change.new_price,
        previous: it?.current_price ?? null,
        at: nowIso,
        source: 'delivery:' + delivery.id,
      });
      return window.supa.from('inventory_items')
        .update({ current_price: change.new_price, price_history: history })
        .eq('id', change.item_id);
    }));
    const acceptedIds = new Set(ids);
    const newDisc = { ...disc, price_changes: (disc.price_changes || []).filter(c => !acceptedIds.has(c.item_id)) };
    await window.supa.from('truck_order_deliveries')
      .update({ ai_discrepancies: newDisc, updated_at: nowIso })
      .eq('id', delivery.id);
    setDelivery(d => d ? { ...d, ai_discrepancies: newDisc } : d);
  };

  const removePhoto = async (keyToRemove) => {
    if (!delivery) return;
    if (!confirm('Remove this photo?')) return;
    const next = (delivery.photo_keys || []).filter(k => k !== keyToRemove);
    await setPhotoKeys(delivery.id, next);
    // Also delete the storage object — anon DELETE on storage.objects
    // is admin-only by RLS, so this only succeeds from the portal (which
    // is admin-authed). On the iPad it fails silently and the orphan
    // file is fine; nothing references it.
    void window.supa.storage.from(INVOICE_BUCKET).remove([keyToRemove]).then(() => {});
    setDelivery(d => d ? { ...d, photo_keys: next } : d);
  };

  const photos = delivery?.photo_keys || [];

  return (
    <div style={{
      padding: '14px 18px',
      background: '#FBFAF8',
      borderTop: '1px solid var(--border-2)',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
        <PIcon name="download" size={14} color="var(--fg-2)" />
        <div style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--fg-1)' }}>
          Delivery — {vendor.name}
        </div>
        {photos.length > 0 && (
          <span style={{
            fontSize: 11, fontWeight: 500, padding: '2px 8px', borderRadius: 999,
            background: '#DCFCE7', color: '#166534',
          }}>{photos.length} {photos.length === 1 ? 'photo' : 'photos'}</span>
        )}
        <div style={{ flex: 1 }} />
        <div style={{ display: 'flex', gap: 6 }}>
          <input
            ref={cameraInputRef}
            type="file"
            accept="image/*"
            capture="environment"
            multiple
            style={{ display: 'none' }}
            onChange={(e) => handleFiles(e.target.files)}
          />
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*,.pdf"
            multiple
            style={{ display: 'none' }}
            onChange={(e) => handleFiles(e.target.files)}
          />
          <PBtn variant="secondary" size="sm" disabled={busy || !delivery}
            onClick={() => cameraInputRef.current?.click()}>
            <PIcon name="eye" size={12} /> Take photo
          </PBtn>
          <PBtn variant="secondary" size="sm" disabled={busy || !delivery}
            onClick={() => fileInputRef.current?.click()}>
            <PIcon name="plus" size={12} /> Upload
          </PBtn>
        </div>
      </div>

      {error && (
        <div style={{ fontSize: 12, color: '#B91C1C', marginBottom: 8 }}>{error}</div>
      )}

      {photos.length === 0 ? (
        <div style={{
          padding: '20px 12px', textAlign: 'center',
          fontSize: 12.5, color: 'var(--fg-3)',
          border: '1px dashed var(--border-1)', borderRadius: 8,
          background: '#FFFFFF',
        }}>
          {busy ? 'Uploading…' : 'No invoice photos yet. Tap Take photo or Upload to log the delivery.'}
        </div>
      ) : (
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(96px, 1fr))',
          gap: 8,
        }}>
          {photos.map(k => (
            <div key={k} style={{ position: 'relative' }}>
              <button
                onClick={() => setPreviewKey(k)}
                title="Open"
                style={{
                  display: 'block', padding: 0, border: '1px solid var(--border-2)',
                  borderRadius: 8, overflow: 'hidden', background: '#FFFFFF', width: '100%',
                }}
              >
                <img
                  src={publicUrlFor(k)}
                  alt="invoice"
                  style={{ display: 'block', width: '100%', aspectRatio: '1 / 1', objectFit: 'cover' }}
                />
              </button>
              <button
                onClick={() => removePhoto(k)}
                title="Remove"
                style={{
                  position: 'absolute', top: 4, right: 4,
                  width: 22, height: 22, borderRadius: 999,
                  background: 'rgba(0,0,0,0.65)', color: '#FFFFFF',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  border: 'none', cursor: 'pointer',
                }}
              ><PIcon name="x" size={11} color="#fff" /></button>
            </div>
          ))}
        </div>
      )}

      {delivery && photos.length > 0 && (
        <AiResultsPanel
          delivery={delivery}
          onRerun={reRunExtraction}
          onAcceptPrice={acceptPriceChange}
          onAcceptAllPrices={acceptAllPriceChanges}
          onConfirmMatch={confirmMatch}
          onDiscard={discardDelivery}
        />
      )}

      {/* Submitted-vs-invoice comparison + returns. Both read the extracted
          invoice lines (delivery.ai_extracted_lines), so they only render
          once the invoice has been extracted. */}
      {delivery && (delivery.ai_extracted_lines || []).length > 0 && (
        <>
          <CompareSection order={order} vendor={vendor} delivery={delivery} />
          <ReturnsSection order={order} vendor={vendor} delivery={delivery} currentStaff={currentStaff} />
        </>
      )}

      {previewKey && (
        <DeliveryPhotoPreview keyName={previewKey} onClose={() => setPreviewKey(null)} />
      )}
    </div>
  );
};

// ------------------------------------------------------------------
// AiResultsPanel — renders the inline AI extraction state below the
// photo grid. Four states cycle through `delivery.ai_status`:
//   pending     → first visit; user just uploaded, hasn't triggered yet
//   queued      → invocation accepted, function hasn't started
//   processing  → Claude is reading the photos right now (~5–30s)
//   done        → extraction complete; show missing/extra/price-changes
//   error       → show ai_error + a "Try again" button
// ------------------------------------------------------------------
const AiResultsPanel = ({ delivery, onRerun, onAcceptPrice, onAcceptAllPrices, onConfirmMatch, onDiscard }) => {
  const status = delivery.ai_status;
  const disc = delivery.ai_discrepancies || {};
  const missing = disc.missing || [];
  const extra = disc.extra || [];
  const prices = disc.price_changes || [];
  // Lines the extractor could read a dollar total for but NOT confidently pin
  // to a per-purchase-unit price (a smudged/misread qty or unit price, or two
  // invoice lines sharing one catalog item). We deliberately do NOT record a
  // price change for these — a bogus "-96.7%" is worse than silence — so they
  // surface here for a human to eyeball instead. See TorPriceResolve / v11.06.
  const needsReview = disc.needs_review || [];
  const conf = typeof delivery.ai_match_confidence === 'number' ? delivery.ai_match_confidence : null;
  // Auto-trust above 0.9; require explicit confirm at 0.7+; loud warning below.
  const trusted = delivery.confirmed_match || (conf !== null && conf >= 0.9);

  return (
    <div style={{
      marginTop: 12, padding: 12,
      background: '#FFFFFF', border: '1px solid var(--border-2)',
      borderRadius: 10,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <PIcon name="sparkle" size={13} color="#7C3AED" />
        <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-1)', letterSpacing: '-0.005em' }}>
          AI invoice match
        </div>
        {status === 'done' && conf !== null && (
          <ConfidenceBadge value={conf} />
        )}
        <div style={{ flex: 1 }} />
        {(status === 'done' || status === 'error') && (
          <PBtn variant="ghost" size="xs" onClick={onRerun}>Re-run</PBtn>
        )}
      </div>

      <div style={{ marginTop: 8, fontSize: 12.5, color: 'var(--fg-2)' }}>
        {aiStatusLine(delivery)}
      </div>

      {status === 'done' && conf !== null && (
        <ConfidenceBreakdown delivery={delivery} />
      )}

      {/* Match-gate banner. Shown when confidence is borderline (<0.9)
          and the admin hasn't explicitly confirmed yet. Discrepancies
          are still rendered below but Accept buttons stay disabled
          until trusted. */}
      {status === 'done' && !trusted && (
        <MatchGateBanner
          delivery={delivery}
          confidence={conf}
          onConfirm={onConfirmMatch}
          onDiscard={onDiscard}
        />
      )}

      {status === 'done' && (missing.length + extra.length + prices.length + needsReview.length > 0) && (
        <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
          {missing.length > 0 && (
            <DiscBlock
              tone="red"
              title="Missing from delivery"
              hint="Ordered but not seen on the invoice."
              rows={missing.map(m => `${m.name} — ordered ${m.ordered} ${m.purchase_unit || ''}`.trim())}
            />
          )}
          {extra.length > 0 && (
            <DiscBlock
              tone="amber"
              title="Extra on invoice"
              hint="Lines we couldn't match to anything you ordered."
              rows={extra.map(x => `${x.name} — ${x.qty}${x.unit_price != null ? ` @ $${x.unit_price}` : ''}`)}
            />
          )}
          {prices.length > 0 && (
            <div style={{
              padding: '8px 10px',
              background: '#FFFBEB',
              border: '1px solid #FDE68A',
              borderRadius: 8,
            }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <div style={{ fontSize: 11, fontWeight: 600, color: '#92400E', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
                  Price changes ({prices.length})
                </div>
                <div style={{ flex: 1 }} />
                {prices.length > 1 && (
                  <PBtn variant="primary" size="xs" disabled={!trusted} onClick={onAcceptAllPrices}
                    title={trusted ? 'Accept every price change on this invoice' : 'Confirm the invoice match first'}>
                    Accept all ({prices.length})
                  </PBtn>
                )}
              </div>
              <div style={{ fontSize: 11.5, color: '#92400E', marginTop: 2, opacity: 0.85 }}>
                {trusted
                  ? 'Accept to update inventory.current_price + log to price_history.'
                  : 'Confirm the invoice match above before accepting price changes.'}
              </div>
              <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
                {prices.map(p => {
                  // New price = item had no prior price (yellow). Otherwise
                  // compute the swing; > 5% in either direction is a red flag.
                  const isNew = p.old_price == null || Number(p.old_price) === 0;
                  const pct = (!isNew && Number(p.old_price) !== 0)
                    ? (Number(p.new_price) - Number(p.old_price)) / Number(p.old_price)
                    : null;
                  const bigChange = pct != null && Math.abs(pct) > 0.05;
                  const pctLabel = pct != null
                    ? `${pct >= 0 ? '+' : '-'}${Math.abs(pct * 100).toFixed(1)}%`
                    : null;
                  // Accept-button colour is the at-a-glance indicator:
                  //   red  = price jumped > 5%   ·   yellow = brand-new price
                  const btnStyle = bigChange
                    ? { background: '#DC2626', color: '#FFFFFF', border: '1px solid #DC2626' }
                    : isNew
                      ? { background: '#F59E0B', color: '#FFFFFF', border: '1px solid #F59E0B' }
                      : undefined;
                  return (
                    <div key={p.item_id} style={{
                      display: 'flex', alignItems: 'center', gap: 10,
                      padding: '6px 8px', background: '#FFFFFF',
                      border: '1px solid ' + (bigChange ? '#FCA5A5' : '#FDE68A'),
                      borderRadius: 6, fontSize: 12,
                    }}>
                      <div style={{ flex: 1, minWidth: 0, color: 'var(--fg-1)', fontWeight: 500 }}>
                        {p.name}
                        {isNew && (
                          <span style={{
                            marginLeft: 6, fontSize: 9.5, fontWeight: 700, color: '#92400E',
                            background: '#FEF3C7', padding: '1px 6px', borderRadius: 4,
                            letterSpacing: '0.04em', textTransform: 'uppercase',
                          }}>New</span>
                        )}
                      </div>
                      <div style={{ fontFamily: 'var(--font-num)', color: 'var(--fg-3)', whiteSpace: 'nowrap' }}>
                        {p.old_price != null ? `$${p.old_price}` : '—'} → <strong style={{ color: bigChange ? '#B91C1C' : '#92400E' }}>${p.new_price}</strong>
                        {pctLabel && (
                          <span style={{ marginLeft: 6, fontWeight: 700, color: bigChange ? '#B91C1C' : 'var(--fg-3)' }}>{pctLabel}</span>
                        )}
                      </div>
                      <PBtn variant="primary" size="xs" disabled={!trusted} onClick={() => onAcceptPrice(p)} style={btnStyle}>Accept</PBtn>
                    </div>
                  );
                })}
              </div>
            </div>
          )}
          {needsReview.length > 0 && (
            <DiscBlock
              tone="slate"
              title="Needs your eyes"
              hint="Couldn't confidently read a per-unit price here, so no price change was applied — check these lines against the invoice photo."
              rows={needsReview.map(n => {
                const why = TOR_REVIEW_REASON[n.reason] || 'Needs a manual check.';
                const nums = (n.eff != null || n.catalog != null)
                  ? ` (invoice ≈ $${n.eff ?? '?'} vs catalog $${n.catalog ?? '?'})`
                  : '';
                return `${n.name} — ${why}${nums}`;
              })}
            />
          )}
        </div>
      )}
    </div>
  );
};

// Human-readable text for each ai_discrepancies.needs_review reason code
// (written by the extract-invoice edge function's price-resolution waterfall).
const TOR_REVIEW_REASON = {
  duplicate_match:      'two invoice lines matched this one item — reconcile the quantities',
  large_swing_low_conf: 'the invoice figure is far from the catalog price but couldn’t be verified',
  small_swing_low_conf: 'a small difference that couldn’t be verified against the order',
  no_line_total:        'no line-total amount could be read',
  unresolvable:         'not enough on the line to work out a per-unit price',
};

// Three-tier badge: green ≥ 0.85, amber 0.55–0.85, red < 0.55. Threshold
// for "auto-trust" (no confirm required) is higher (0.90) so the badge
// staying yellow + banner showing makes sense even at 0.85.
const ConfidenceBadge = ({ value }) => {
  const pct = Math.round(value * 100);
  let bg, fg;
  if (value >= 0.85)      { bg = '#DCFCE7'; fg = '#166534'; }
  else if (value >= 0.55) { bg = '#FEF3C7'; fg = '#92400E'; }
  else                    { bg = '#FEE2E2'; fg = '#991B1B'; }
  return (
    <span style={{
      fontSize: 11, fontWeight: 700,
      padding: '2px 8px', borderRadius: 999,
      background: bg, color: fg,
      fontFamily: 'var(--font-num)', letterSpacing: '0.02em',
    }}>{pct}% match</span>
  );
};

const ConfidenceBreakdown = ({ delivery }) => {
  const b = delivery.ai_match_breakdown || {};
  const chip = (label, sub, score) => {
    const tone = score >= 0.7 ? '#16A34A' : score >= 0.4 ? '#CA8A04' : '#B91C1C';
    return (
      <div style={{
        display: 'inline-flex', flexDirection: 'column',
        padding: '6px 10px',
        background: '#F9FAFB', border: '1px solid var(--border-2)', borderRadius: 8,
        minWidth: 0,
      }}>
        <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--fg-3)', letterSpacing: '0.04em', textTransform: 'uppercase' }}>{label}</div>
        <div style={{ fontSize: 12, color: 'var(--fg-1)', marginTop: 2, fontWeight: 500 }}>{sub}</div>
        <div style={{
          marginTop: 4, height: 3, borderRadius: 2,
          background: '#E5E7EB', overflow: 'hidden',
        }}>
          <div style={{ width: `${Math.round(score * 100)}%`, height: '100%', background: tone }} />
        </div>
      </div>
    );
  };
  return (
    <div style={{ marginTop: 10, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 6 }}>
      {chip('Vendor', b.seen_vendor || '—', b.vendor_match || 0)}
      {chip(
        'Date',
        b.invoice_date && b.order_date
          ? `${b.invoice_date.slice(0,10)} vs ${String(b.order_date).slice(0,10)}`
          : (b.invoice_date ? `Invoice ${b.invoice_date}` : '—'),
        b.date_match || 0
      )}
      {chip('Line overlap', `${Math.round((b.line_overlap || 0) * 100)}% of items matched`, b.line_overlap || 0)}
      {chip(
        'Total units',
        `${b.seen_total_units ?? '—'} delivered vs ${b.ordered_total_units ?? '—'} ordered`,
        b.qty_match || 0
      )}
    </div>
  );
};

const MatchGateBanner = ({ delivery, confidence, onConfirm, onDiscard }) => {
  const conf = confidence ?? 0;
  const tone = conf < 0.55
    ? { bg: '#FEE2E2', border: '#FCA5A5', fg: '#991B1B', label: 'This may be the wrong invoice' }
    : { bg: '#FEF3C7', border: '#FDE68A', fg: '#92400E', label: 'Borderline match — please confirm' };
  return (
    <div style={{
      marginTop: 10, padding: '10px 12px',
      background: tone.bg, border: '1px solid ' + tone.border, borderRadius: 8,
    }}>
      <div style={{ fontSize: 12.5, fontWeight: 700, color: tone.fg, letterSpacing: '-0.005em' }}>
        {tone.label}
      </div>
      <div style={{ fontSize: 12, color: tone.fg, marginTop: 2, opacity: 0.9, lineHeight: 1.45 }}>
        Vendor on the invoice, date, ordered items, or total units don't all line up with this order.
        Review the breakdown above before accepting any price changes.
      </div>
      <div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
        <PBtn variant="primary" size="sm" onClick={onConfirm}>Confirm match — looks right</PBtn>
        <PBtn variant="secondary" size="sm" onClick={onDiscard}>Discard photos</PBtn>
      </div>
    </div>
  );
};

const DiscBlock = ({ tone, title, hint, rows }) => {
  const palette = tone === 'red'
    ? { bg: '#FEF2F2', border: '#FCA5A5', fg: '#991B1B' }
    : tone === 'slate'
      ? { bg: '#F1F5F9', border: '#CBD5E1', fg: '#334155' }
      : { bg: '#FFFBEB', border: '#FDE68A', fg: '#92400E' };
  return (
    <div style={{
      padding: '8px 10px',
      background: palette.bg,
      border: '1px solid ' + palette.border,
      borderRadius: 8,
    }}>
      <div style={{ fontSize: 11, fontWeight: 600, color: palette.fg, textTransform: 'uppercase', letterSpacing: '0.06em' }}>
        {title} ({rows.length})
      </div>
      <div style={{ fontSize: 11.5, color: palette.fg, marginTop: 2, opacity: 0.85 }}>{hint}</div>
      <ul style={{ margin: '6px 0 0', padding: '0 0 0 16px', fontSize: 12, color: palette.fg, lineHeight: 1.6 }}>
        {rows.map((r, i) => <li key={i}>{r}</li>)}
      </ul>
    </div>
  );
};

function aiStatusLine(d) {
  switch (d.ai_status) {
    case 'pending':    return 'No extraction yet — upload at least one photo to start.';
    case 'queued':     return 'Queued — extraction starting…';
    case 'processing': return 'Reading the invoice (typically 5–30s)…';
    case 'done': {
      const disc = d.ai_discrepancies || {};
      const missing = (disc.missing || []).length;
      const extra   = (disc.extra   || []).length;
      const prices  = (disc.price_changes || []).length;
      const review  = (disc.needs_review || []).length;
      if (missing === 0 && extra === 0 && prices === 0 && review === 0) return '✓ All ordered items delivered, no price changes.';
      const parts = [];
      if (missing) parts.push(`${missing} missing`);
      if (extra)   parts.push(`${extra} extra`);
      if (prices)  parts.push(`${prices} price change${prices === 1 ? '' : 's'}`);
      if (review)  parts.push(`${review} to review`);
      return parts.join(' · ');
    }
    case 'error':      return 'Extraction failed — ' + (d.ai_error || 'unknown error');
    default:           return d.ai_status;
  }
}

const DeliveryPhotoPreview = ({ keyName, onClose }) => {
  useEffect(() => {
    const handler = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [onClose]);
  return (
    <div
      onClick={onClose}
      style={{
        position: 'fixed', inset: 0,
        background: 'rgba(0,0,0,0.82)',
        zIndex: 1000,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: 40,
      }}
    >
      <img
        src={publicUrlFor(keyName)}
        alt="invoice"
        style={{
          maxWidth: '100%', maxHeight: '100%',
          objectFit: 'contain',
          boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
        }}
        onClick={e => e.stopPropagation()}
      />
      <button
        onClick={onClose}
        style={{
          position: 'absolute', top: 24, right: 24,
          width: 44, height: 44, borderRadius: 999,
          background: 'rgba(255,255,255,0.92)', color: '#000',
          border: 'none', cursor: 'pointer',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        }}
      ><PIcon name="x" size={18} color="#000" /></button>
    </div>
  );
};

// ------------------------------------------------------------------
// Money + name helpers (module-local; prefixed `Tdp` to avoid the
// global top-level-const collision documented in HANDBOOK §9 — there's
// already a `fmtMoney` in Inventory.jsx).
// ------------------------------------------------------------------
const fmtMoneyTdp = (n) => {
  if (n === null || n === undefined || isNaN(Number(n))) return '—';
  return '$' + Number(n).toFixed(2);
};
const tdpNormName = (s) => String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();

const CompareChip = ({ label, bg, fg, n }) => (
  <span style={{ fontSize: 10.5, fontWeight: 600, padding: '2px 7px', borderRadius: 999, background: bg, color: fg, whiteSpace: 'nowrap' }}>
    {n != null ? `${n} ` : ''}{label}
  </span>
);

// ------------------------------------------------------------------
// CompareSection — submitted truck order vs the actual extracted invoice
// for THIS vendor. Highlights add-ons/replacements (on the invoice, not
// submitted), not-delivered (submitted, not on invoice), and qty diffs.
// Matched on matched_item_id where the AI resolved a catalog item, else
// by normalized name. Collapsed by default; differences summarized as
// chips on the header bar.
// ------------------------------------------------------------------
const CompareSection = ({ order, vendor, delivery }) => {
  const rows = useMemo(() => {
    const items = window.SAMPLE_INVENTORY_ITEMS || [];
    const itemById = new Map(items.map(it => [it.id, it]));

    const submitted = (order.lines || [])
      .map(l => ({ ...l, item: itemById.get(l.itemId) }))
      .filter(l => l.item && (l.item.vendorId || '__none') === vendor.id && Number(l.final) > 0);

    const invoice = delivery.ai_extracted_lines || [];
    const keyFor = (itemId, name) => itemId ? ('id:' + itemId) : ('nm:' + tdpNormName(name));
    const map = new Map();

    submitted.forEach(l => {
      const k = keyFor(l.itemId, l.item.name);
      map.set(k, { key: k, name: l.item.name, submittedQty: Number(l.final), invoiceQty: 0, unitPrice: null });
    });
    invoice.forEach(iv => {
      const k = keyFor(iv.matched_item_id, iv.name);
      const existing = map.get(k);
      if (existing) {
        existing.invoiceQty += Number(iv.qty) || 0;
        if (iv.unit_price != null) existing.unitPrice = Number(iv.unit_price);
      } else {
        const catalog = iv.matched_item_id ? itemById.get(iv.matched_item_id) : null;
        map.set(k, {
          key: k,
          name: catalog ? catalog.name : iv.name,
          submittedQty: 0,
          invoiceQty: Number(iv.qty) || 0,
          unitPrice: iv.unit_price != null ? Number(iv.unit_price) : null,
        });
      }
    });

    const rank = { addon: 0, missing: 1, qty: 2, match: 3 };
    return [...map.values()].map(r => {
      let tag = 'match';
      if (r.submittedQty === 0 && r.invoiceQty > 0) tag = 'addon';
      else if (r.invoiceQty === 0 && r.submittedQty > 0) tag = 'missing';
      else if (r.submittedQty !== r.invoiceQty) tag = 'qty';
      return { ...r, tag };
    }).sort((a, b) => rank[a.tag] - rank[b.tag]);
  }, [order, vendor, delivery]);

  const counts = useMemo(() => {
    const c = { addon: 0, missing: 0, qty: 0 };
    rows.forEach(r => { if (c[r.tag] !== undefined) c[r.tag] += 1; });
    return c;
  }, [rows]);

  const [open, setOpen] = useState(false);
  const TAG = {
    addon:   { label: 'add-on',        bg: '#EDE9FE', fg: '#5B21B6' },
    missing: { label: 'not delivered', bg: '#FEE2E2', fg: '#991B1B' },
    qty:     { label: 'qty differs',   bg: '#FEF3C7', fg: '#92400E' },
    match:   { label: 'match',         bg: 'var(--bg-sunken)', fg: 'var(--fg-3)' },
  };

  return (
    <div style={{ marginTop: 12, background: '#FFFFFF', border: '1px solid var(--border-2)', borderRadius: 10, overflow: 'hidden' }}>
      <button onClick={() => setOpen(o => !o)} style={{
        width: '100%', display: 'flex', alignItems: 'center', gap: 8,
        padding: '10px 12px', background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left',
      }}>
        <PIcon name="list" size={13} color="var(--fg-2)" />
        <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-1)' }}>Submitted vs invoice</div>
        <div style={{ display: 'flex', gap: 6 }}>
          {counts.addon > 0 && <CompareChip {...TAG.addon} n={counts.addon} />}
          {counts.missing > 0 && <CompareChip {...TAG.missing} n={counts.missing} />}
          {counts.qty > 0 && <CompareChip {...TAG.qty} n={counts.qty} />}
        </div>
        <div style={{ flex: 1 }} />
        <PIcon name={open ? 'chevronU' : 'chevronD'} size={14} color="var(--fg-3)" />
      </button>
      {open && (
        <table style={{ borderCollapse: 'collapse', width: '100%', borderTop: '1px solid var(--border-2)' }}>
          <thead>
            <tr style={{ background: '#FAFAF9' }}>
              {['Item', 'Submitted', 'Invoice', 'Unit price', ''].map((h, i) => (
                <th key={i} style={{ textAlign: i === 0 || i === 4 ? 'left' : 'right', padding: '8px 12px', fontSize: 10.5, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.05em', whiteSpace: 'nowrap' }}>{h}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map(r => (
              <tr key={r.key} style={{ borderTop: '1px solid var(--border-2)' }}>
                <td style={{ padding: '8px 12px', fontSize: 12.5, color: 'var(--fg-1)' }}>{r.name}</td>
                <td style={{ padding: '8px 12px', fontSize: 12.5, textAlign: 'right', fontFamily: 'var(--font-num)', color: r.submittedQty ? 'var(--fg-1)' : 'var(--fg-3)' }}>{r.submittedQty || '—'}</td>
                <td style={{ padding: '8px 12px', fontSize: 12.5, textAlign: 'right', fontFamily: 'var(--font-num)', color: r.invoiceQty ? 'var(--fg-1)' : 'var(--fg-3)' }}>{r.invoiceQty || '—'}</td>
                <td style={{ padding: '8px 12px', fontSize: 12.5, textAlign: 'right', fontFamily: 'var(--font-num)', color: 'var(--fg-2)' }}>{fmtMoneyTdp(r.unitPrice)}</td>
                <td style={{ padding: '8px 12px' }}>{r.tag !== 'match' && <CompareChip {...TAG[r.tag]} />}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
};

// ------------------------------------------------------------------
// ReturnsSection — log + manage returns for THIS vendor order. Returns
// are picked from the actual invoice (delivery.ai_extracted_lines) and
// snapshot name/price into truck_order_returns so each return stands
// alone in the report even if the invoice is re-extracted.
// ------------------------------------------------------------------
const ReturnsSection = ({ order, vendor, delivery, currentStaff }) => {
  const [returns, setReturns] = useState([]);
  const [modalOpen, setModalOpen] = useState(false);

  const refetch = React.useCallback(async () => {
    if (!window.supa) return;
    const { data } = await window.supa
      .from('truck_order_returns')
      .select('*')
      .eq('order_id', order.id)
      .eq('vendor_id', vendor.id)
      .order('created_at', { ascending: false });
    setReturns(data || []);
  }, [order.id, vendor.id]);

  useEffect(() => {
    refetch();
    if (!window.supa) return;
    const ch = window.supa
      .channel(`returns:${order.id}:${vendor.id}`)
      .on('postgres_changes', {
        event: '*', schema: 'public', table: 'truck_order_returns',
        filter: 'order_id=eq.' + order.id,
      }, (payload) => {
        const r = payload.new || payload.old;
        if (!r || r.vendor_id !== vendor.id) return;
        refetch();
      })
      .subscribe();
    return () => { window.supa.removeChannel(ch); };
  }, [order.id, vendor.id, refetch]);

  const togglePickedUp = async (r) => {
    const picked = r.status !== 'picked_up';
    await window.supa.from('truck_order_returns')
      .update({ status: picked ? 'picked_up' : 'pending', picked_up_at: picked ? new Date().toISOString() : null, updated_at: new Date().toISOString() })
      .eq('id', r.id);
    refetch();
  };
  const deleteReturn = async (r) => {
    if (!confirm('Delete this return line?')) return;
    await window.supa.from('truck_order_returns').delete().eq('id', r.id);
    refetch();
  };

  const total = returns.reduce((s, r) => s + (Number(r.amount) || 0), 0);

  return (
    <div style={{ marginTop: 12, background: '#FFFFFF', border: '1px solid var(--border-2)', borderRadius: 10, padding: 12 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <PIcon name="arrowR" size={13} color="var(--fg-2)" style={{ transform: 'scaleX(-1)' }} />
        <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-1)' }}>Returns</div>
        {returns.length > 0 && (
          <span style={{ fontSize: 11, color: 'var(--fg-3)', fontFamily: 'var(--font-num)' }}>
            {returns.length} · {fmtMoneyTdp(total)}
          </span>
        )}
        <div style={{ flex: 1 }} />
        <PBtn variant="secondary" size="sm" icon="plus" onClick={() => setModalOpen(true)}>Return items</PBtn>
      </div>

      {returns.length > 0 && (
        <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
          {returns.map(r => (
            <div key={r.id} style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '8px 10px', border: '1px solid var(--border-2)', borderRadius: 8,
              background: r.status === 'picked_up' ? '#F0FDF4' : '#FFFFFF',
            }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12.5, fontWeight: 500, color: 'var(--fg-1)' }}>{r.item_name}</div>
                <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 1 }}>
                  <span style={{ fontFamily: 'var(--font-num)' }}>{r.return_qty}</span> × {fmtMoneyTdp(r.unit_price)} ={' '}
                  <span style={{ fontFamily: 'var(--font-num)', fontWeight: 600, color: 'var(--fg-2)' }}>{fmtMoneyTdp(r.amount)}</span>
                  {r.reason ? <span> · {r.reason}</span> : ''}
                </div>
              </div>
              <button onClick={() => togglePickedUp(r)} title="Toggle picked up" style={{
                fontSize: 10.5, fontWeight: 600, padding: '3px 9px', borderRadius: 999, cursor: 'pointer',
                border: '1px solid ' + (r.status === 'picked_up' ? 'transparent' : 'var(--border-1)'),
                background: r.status === 'picked_up' ? '#DCFCE7' : '#FFFFFF',
                color: r.status === 'picked_up' ? '#166534' : 'var(--fg-2)',
              }}>
                {r.status === 'picked_up' ? '✓ picked up' : 'mark picked up'}
              </button>
              <button onClick={() => deleteReturn(r)} title="Delete" style={{
                width: 24, height: 24, borderRadius: 6, border: '1px solid var(--border-2)',
                background: '#FFFFFF', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              }}><PIcon name="trash" size={12} color="var(--fg-3)" /></button>
            </div>
          ))}
        </div>
      )}

      {modalOpen && (
        <ReturnPickerModal
          order={order} vendor={vendor} delivery={delivery} currentStaff={currentStaff}
          onClose={() => setModalOpen(false)}
          onSaved={() => { setModalOpen(false); refetch(); }}
        />
      )}
    </div>
  );
};

// ------------------------------------------------------------------
// ReturnPickerModal — pick items to return FROM the extracted invoice.
// Per line: return qty + an amount that defaults to invoice price × qty
// (editable) + an optional reason. Saves one truck_order_returns row per
// selected line, snapshotting the invoice line's name/price.
// ------------------------------------------------------------------
const ReturnPickerModal = ({ order, vendor, delivery, currentStaff, onClose, onSaved }) => {
  const invoiceLines = delivery.ai_extracted_lines || [];
  const [draft, setDraft] = useState({});
  const [saving, setSaving] = useState(false);

  // Per-unit credit for a returned qty: prefer the printed line total (金额) ÷
  // invoice qty — correct even when the invoice's qty and unit-price columns use
  // different units — falling back to the per-unit price for legacy lines.
  const perUnitCredit = (iv) => {
    const ivQty = Number(iv && iv.qty) || 0;
    if (iv && iv.line_total != null && ivQty > 0) return Number(iv.line_total) / ivQty;
    return (iv && iv.unit_price != null) ? Number(iv.unit_price) : null;
  };
  const onQty = (idx, raw, iv) => {
    const qty = Math.max(0, Number(raw) || 0);
    const perUnit = perUnitCredit(iv);
    setDraft(d => {
      const cur = d[idx] || {};
      const amount = cur.amountEdited ? cur.amount : (perUnit != null ? +(perUnit * qty).toFixed(2) : (cur.amount || 0));
      return { ...d, [idx]: { ...cur, qty, amount } };
    });
  };
  const onAmount = (idx, raw) => setDraft(d => ({ ...d, [idx]: { ...(d[idx] || {}), amount: Math.max(0, Number(raw) || 0), amountEdited: true } }));
  const onReason = (idx, v) => setDraft(d => ({ ...d, [idx]: { ...(d[idx] || {}), reason: v } }));

  const selected = Object.entries(draft).filter(([, v]) => (Number(v.qty) || 0) > 0);
  const totalAmt = selected.reduce((s, [, v]) => s + (Number(v.amount) || 0), 0);

  const save = async () => {
    if (selected.length === 0 || !window.supa) return;
    setSaving(true);
    const rows = selected.map(([idx, v]) => {
      const iv = invoiceLines[Number(idx)];
      return {
        restaurant_id: window.RESTAURANT_ID,
        order_id: order.id,
        vendor_id: vendor.id,
        item_name: iv.name,
        matched_item_id: iv.matched_item_id || null,
        unit_price: iv.unit_price != null ? Number(iv.unit_price) : null,
        return_qty: Number(v.qty),
        amount: Number(v.amount) || 0,
        reason: (v.reason || '').trim() || null,
        created_by_staff_id: currentStaff?.id || null,
      };
    });
    const { error } = await window.supa.from('truck_order_returns').insert(rows);
    setSaving(false);
    if (error) { alert('Save failed — ' + error.message); return; }
    onSaved();
  };

  return (
    <PModal open onClose={onClose} title={`Return items — ${vendor.name}`} width={640} footer={
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%' }}>
        <div style={{ fontSize: 12.5, color: 'var(--fg-2)' }}>
          {selected.length} item{selected.length === 1 ? '' : 's'} · <span style={{ fontFamily: 'var(--font-num)', fontWeight: 600 }}>{fmtMoneyTdp(totalAmt)}</span>
        </div>
        <div style={{ flex: 1 }} />
        <PBtn variant="ghost" size="md" onClick={onClose}>Cancel</PBtn>
        <PBtn variant="primary" size="md" disabled={selected.length === 0 || saving} onClick={save}>
          {saving ? 'Saving…' : `Log return${selected.length === 1 ? '' : 's'}`}
        </PBtn>
      </div>
    }>
      <div style={{ fontSize: 12.5, color: 'var(--fg-2)', marginBottom: 12, lineHeight: 1.5 }}>
        Pick items to return from the <b>actual invoice</b> below — including any add-ons or replacements the vendor delivered. Enter a return quantity per item; the amount defaults to invoice price × qty, editable if the credit differs.
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 440, overflowY: 'auto' }}>
        {invoiceLines.map((iv, idx) => {
          const d = draft[idx] || {};
          const active = (Number(d.qty) || 0) > 0;
          return (
            <div key={idx} style={{
              border: '1px solid ' + (active ? 'var(--fg-1)' : 'var(--border-2)'),
              borderRadius: 8, padding: '8px 10px',
              background: active ? '#FBFAF8' : '#FFFFFF',
            }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 12.5, color: 'var(--fg-1)', fontWeight: 500 }}>{iv.name}</div>
                  <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 1 }}>
                    invoice: <span style={{ fontFamily: 'var(--font-num)' }}>{iv.qty}</span>
                    {iv.line_total != null
                      ? <> · <span style={{ fontFamily: 'var(--font-num)' }}>{fmtMoneyTdp(iv.line_total)}</span></>
                      : (iv.unit_price != null ? <> @ {fmtMoneyTdp(iv.unit_price)}</> : null)}
                    {!iv.matched_item_id && <span style={{ marginLeft: 6, color: '#5B21B6' }}>· add-on</span>}
                  </div>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <div style={{ fontSize: 10, color: 'var(--fg-3)', marginBottom: 2 }}>Return qty</div>
                  <input type="number" min="0" step="1" value={d.qty ?? ''} placeholder="0"
                    onChange={e => onQty(idx, e.target.value, iv)}
                    style={{ width: 60, height: 30, padding: '0 8px', border: '1px solid var(--border-1)', borderRadius: 6, fontFamily: 'var(--font-num)', textAlign: 'right' }} />
                </div>
                <div style={{ textAlign: 'right' }}>
                  <div style={{ fontSize: 10, color: 'var(--fg-3)', marginBottom: 2 }}>Amount $</div>
                  <input type="number" min="0" step="0.01" value={active ? (d.amount ?? '') : ''} placeholder="0.00" disabled={!active}
                    onChange={e => onAmount(idx, e.target.value)}
                    style={{ width: 78, height: 30, padding: '0 8px', border: '1px solid var(--border-1)', borderRadius: 6, fontFamily: 'var(--font-num)', textAlign: 'right', opacity: active ? 1 : 0.5 }} />
                </div>
              </div>
              {active && (
                <input type="text" value={d.reason || ''} placeholder="Reason (optional) — e.g. damaged, wrong item, not needed"
                  onChange={e => onReason(idx, e.target.value)}
                  style={{ marginTop: 6, width: '100%', height: 28, padding: '0 8px', border: '1px solid var(--border-2)', borderRadius: 6, fontSize: 12, boxSizing: 'border-box' }} />
              )}
            </div>
          );
        })}
      </div>
    </PModal>
  );
};

window.TruckDeliveryPanel = TruckDeliveryPanel;
