/* Uleez — Lessor screens part 3: messages, payments, profile, vehicle form */
const { useState: useS3, useEffect: useE3, useRef: useR3 } = React;

// Animated "…" bubble shown while the other participant is typing.
function TypingBubble() {
  return (
    <div className="bubble-row them">
      <div className="bubble typing-bubble">
        <span className="typing-dot" /><span className="typing-dot" /><span className="typing-dot" />
      </div>
    </div>
  );
}

// ============================================================
// PAYMENTS
// ============================================================
//
// Everything on this screen is driven by the buckets the API returns (see
// PaymentsService.computeLessorPayments). The client no longer derives a single date: it used
// to rebuild the calendar itself and match the server's LOCALIZED labels against its own, which
// silently failed for the monthly view ("08/2026" vs "Août") and drew a year of real revenue as
// a flat line on zero.

const VCOLS = ['#059669', '#3b82f6', '#f59e0b', '#7c3aed', '#f43f5e', '#06b6d4', '#84cc16', '#fb923c'];

// Empty accounting shape, so a screen with no data renders zeros instead of "undefined DA".
const EMPTY_VIEW = {
  rental: 0, commission: 0, serviceFee: 0, discount: 0,
  lessorNet: 0, clientTotal: 0, dueToPlatform: 0, bookings: 0,
};

// ─── PeriodCarousel — pick the bucket every figure on the page refers to ──
function PeriodCarousel({ buckets, selectedKey, onSelect, granularity }) {
  const t = window.I18N ? I18N.useT() : ((k) => k);
  const scroller = useR3(null);

  // Keep the active chip in view when the selection moves (arrows, or a click on the chart).
  useE3(() => {
    const el = scroller.current;
    if (!el) return;
    const active = el.querySelector('.pc-chip.on');
    if (active && active.scrollIntoView) {
      active.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
    }
  }, [selectedKey, buckets]);

  if (!buckets.length) return null;

  const idx = buckets.findIndex(b => b.key === selectedKey);
  const step = (dir) => {
    // From "toute la période", stepping back lands on the last elapsed bucket rather than the
    // last future one — that is the number a lessor actually wants to look at.
    if (idx < 0) {
      const lastReal = buckets.map(b => b.isFuture).lastIndexOf(false);
      onSelect(buckets[lastReal >= 0 ? lastReal : buckets.length - 1].key);
      return;
    }
    const next = idx + dir;
    if (next < 0 || next >= buckets.length) return;
    onSelect(buckets[next].key);
  };

  const unit = granularity === 'month' ? 'mois' : granularity === 'week' ? 'semaine' : 'jour';

  return (
    <Card style={{ marginBottom: 20 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px' }}>
        <button className="pc-nav" title={`${unit} précédent`} onClick={() => step(-1)} disabled={idx === 0}>
          <Icon name="chevLeft" size={16} />
        </button>

        <div className="pc-track" ref={scroller}>
          <button
            className={`pc-chip ${selectedKey === null ? 'on' : ''}`}
            onClick={() => onSelect(null)}
          >
            <span className="pc-lbl">{t('rev.wholePeriod')}</span>
            <span className="pc-val">{t('rev.cumul')}</span>
          </button>
          {buckets.map(b => {
            // Une pastille à 0 est ambiguë : « rien ne s'est passé » et « des encaissements
            // ne sont pas confirmés » se ressemblent, alors qu'ils appellent des actions
            // opposées. Le second cas est signalé.
            const waiting = !b.rental && (b.awaitingSettlement || 0) > 0;
            const title = waiting
              ? `${b.label} — ${b.awaitingSettlement} encaissement(s) à confirmer`
              : b.presumedRental
                ? `${b.label} — dont ${KD.fmtNum(b.presumedRental)} DA présumés`
                : b.label;
            return (
              <button
                key={b.key}
                className={`pc-chip ${b.key === selectedKey ? 'on' : ''} ${b.isFuture ? 'future' : ''} ${b.isCurrent ? 'now' : ''} ${waiting ? 'waiting' : ''}`}
                onClick={() => onSelect(b.key)}
                title={title}
              >
                <span className="pc-lbl">{b.shortLabel}{b.isCurrent ? ' · en cours' : ''}</span>
                <span className="pc-val">
                  {waiting
                    ? `${b.awaitingSettlement} à confirmer`
                    : b.isFuture && !b.rental ? '—' : KD.fmtNum(b.rental) + ' DA'}
                  {b.presumedRental > 0 && !waiting ? ' *' : ''}
                </span>
              </button>
            );
          })}
        </div>

        <button className="pc-nav" title={`${unit} suivant`} onClick={() => step(1)} disabled={idx >= 0 && idx === buckets.length - 1}>
          <Icon name="chevRight" size={16} />
        </button>
      </div>
    </Card>
  );
}

// ─── RevenueChart — buckets + projection, click to select ────────────────
function RevenueChart({ buckets, forecast, selectedKey, onSelect, height }) {
  const t = window.I18N ? I18N.useT() : ((k) => k);
  const [chartType, setChartType] = useS3('bar');
  const [hovered, setHovered] = useS3(null);
  const [ready, setReady] = useS3(false);

  useE3(() => {
    setReady(false); setHovered(null);
    const t = setTimeout(() => setReady(true), 80);
    return () => clearTimeout(t);
  }, [buckets]);

  const fmtY = v => v >= 1e6 ? (v / 1e6).toFixed(1) + 'M' : v >= 1000 ? (v / 1000).toFixed(1) + 'k' : String(Math.round(v));

  const n = buckets.length;
  const projByKey = {};
  (forecast?.buckets || []).forEach(p => { projByKey[p.key] = p; });

  // Series drawn: realised rental per bucket, and the projected total (realised + estimated)
  // for buckets that are not settled yet.
  const rows = buckets.map(b => {
    const p = projByKey[b.key];
    return {
      ...b,
      projected: p && p.isProjection ? p.value : null,
      estimated: p && p.isProjection ? p.estimated : 0,
    };
  });

  const lastRealIdx = rows.map(r => r.isFuture).lastIndexOf(false);

  // ── SVG layout ─────────────────────────────────────────────────────────
  const W = 480, H = height || 280;
  const PL = 52, PR = 14, PT = 20, PB = 40;
  const cW = W - PL - PR, cH = H - PT - PB;

  const maxBar = Math.max(1, ...rows.map(r => Math.max(r.rental, r.projected || 0))) * 1.2;

  let cum = 0, cumProj = 0;
  const cumRows = rows.map((r, i) => {
    cum += r.rental;
    cumProj += (r.projected != null ? r.projected : r.rental);
    return { ...r, cum, cumProj, i };
  });
  const maxLine = Math.max(1, cumRows.length ? Math.max(cumRows[cumRows.length - 1].cum, cumRows[cumRows.length - 1].cumProj) : 1) * 1.15;

  const sx = i => PL + (n > 1 ? (i / (n - 1)) * cW : cW / 2);
  const syL = v => PT + cH - Math.min(1, Math.max(0, v / maxLine)) * cH;
  const syB = v => PT + cH - Math.min(1, Math.max(0, v / maxBar)) * cH;

  const bz = pts => {
    if (!pts.length) return '';
    if (pts.length === 1) return `M${pts[0].x},${pts[0].y}`;
    let d = `M${pts[0].x},${pts[0].y}`;
    for (let i = 1; i < pts.length; i++) {
      const cx = (pts[i - 1].x + pts[i].x) / 2;
      d += ` C${cx},${pts[i - 1].y} ${cx},${pts[i].y} ${pts[i].x},${pts[i].y}`;
    }
    return d;
  };

  // Never print more than ~9 x labels, whatever the granularity.
  const xStep = Math.max(1, Math.ceil(n / 9));

  const scaffold = (maxV, syFn, content) => React.createElement('svg', {
    viewBox: `0 0 ${W} ${H}`, width: '100%', height: H, overflow: 'visible',
    style: { display: 'block', opacity: ready ? 1 : 0, transition: 'opacity .45s' },
    onMouseLeave: () => setHovered(null),
  },
    [0, 0.25, 0.5, 0.75, 1].map((f, ti) => {
      const v = maxV * f;
      return React.createElement('g', { key: ti },
        v > 0 && React.createElement('line', { x1: PL, y1: syFn(v), x2: PL + cW, y2: syFn(v), stroke: 'var(--slate-100)', strokeWidth: 1 }),
        React.createElement('text', { x: PL - 6, y: syFn(v) + 4, textAnchor: 'end', fontSize: 9, fill: 'var(--muted)' }, fmtY(v)),
      );
    }),
    React.createElement('line', { x1: PL, y1: syFn(0), x2: PL + cW, y2: syFn(0), stroke: 'var(--slate-200)', strokeWidth: 1 }),
    rows.map((r, i) => i % xStep === 0 && React.createElement('text', {
      key: 'x' + i, x: sx(i), y: H - PB + 15, textAnchor: 'middle', fontSize: 9,
      fill: r.key === selectedKey ? 'var(--brand-700)' : 'var(--muted)',
      fontWeight: r.key === selectedKey ? 700 : 400,
    }, r.shortLabel)),
    ...content,
  );

  const tooltip = (i, syFn, val, extra) => {
    const r = rows[i];
    const tx = sx(i), ty = syFn(val);
    const bw = 168, bh = extra ? 66 : 50;
    const bx = Math.min(Math.max(tx - bw / 2, PL), W - bw - 2);
    const by = Math.max(ty - bh - 12, PT);
    return React.createElement('g', { key: 'tip', style: { pointerEvents: 'none' } },
      React.createElement('rect', { x: bx, y: by, width: bw, height: bh, rx: 8, fill: '#0f172a', opacity: 0.94 }),
      React.createElement('text', { x: bx + bw / 2, y: by + 16, textAnchor: 'middle', fontSize: 9.5, fill: 'rgba(255,255,255,.65)' }, r.label),
      React.createElement('text', { x: bx + bw / 2, y: by + 34, textAnchor: 'middle', fontSize: 13, fill: '#6ee7b7', fontWeight: 700 }, KD.fmtDA(Math.round(val))),
      extra && React.createElement('text', { x: bx + bw / 2, y: by + 52, textAnchor: 'middle', fontSize: 9.5, fill: 'rgba(255,255,255,.7)' }, extra),
    );
  };

  // ── Bars: realised (stacked by vehicle) + projection for what is left ──
  const renderBar = () => {
    const bW = Math.max(3, Math.min(30, cW / Math.max(n, 1) - 3));

    const bars = rows.map((r, i) => {
      const bx = sx(i) - bW / 2;
      const delay = i * 0.015;
      const parts = [];

      // Real revenue, split by the vehicles that actually earned it in THIS bucket.
      if (r.rental > 0) {
        const segs = r.vehicles && r.vehicles.length ? r.vehicles : [{ name: '', value: r.rental }];
        let stack = 0;
        segs.forEach((s, si) => {
          const segH = Math.max(0, (s.value / maxBar) * cH);
          parts.push(React.createElement('rect', {
            key: `s${si}`, x: bx, y: syB(0) - stack - segH, width: bW, height: segH,
            rx: si === segs.length - 1 ? 2 : 0,
            fill: VCOLS[si % VCOLS.length],
            opacity: ready ? (selectedKey && r.key !== selectedKey ? 0.35 : 0.9) : 0,
            style: { transition: `opacity .4s ease ${delay}s` },
          }));
          stack += segH;
        });
      }

      // Estimated top-up, hatched, sitting on top of what is already earned.
      if (r.estimated > 0) {
        const estH = Math.max(0, (r.estimated / maxBar) * cH);
        const baseH = (r.rental / maxBar) * cH;
        parts.push(React.createElement('rect', {
          key: 'est', x: bx, y: syB(0) - baseH - estH, width: bW, height: estH, rx: 2,
          fill: 'url(#projHatch)', stroke: '#4ade80', strokeWidth: 0.8, strokeDasharray: '3,2',
          opacity: ready ? (selectedKey && r.key !== selectedKey ? 0.3 : 0.85) : 0,
          style: { transition: `opacity .4s ease ${delay}s` },
        }));
      }

      return React.createElement('g', {
        key: r.key,
        onMouseEnter: () => setHovered(i),
        onClick: () => onSelect(r.key === selectedKey ? null : r.key),
        style: { cursor: 'pointer' },
      },
        // Invisible full-height hit area — a 4px bar is impossible to hover on a dense range.
        React.createElement('rect', { x: bx - 2, y: PT, width: bW + 4, height: cH, fill: 'transparent' }),
        ...parts,
      );
    });

    const tip = hovered != null && rows[hovered]
      ? tooltip(hovered, syB, rows[hovered].projected != null ? rows[hovered].projected : rows[hovered].rental,
          rows[hovered].estimated > 0
            ? `dont ${KD.fmtDA(rows[hovered].estimated)} estimés`
            : `${rows[hovered].bookings} location(s)`)
      : null;

    return scaffold(maxBar, syB, [
      React.createElement('defs', { key: 'defs' },
        React.createElement('pattern', { id: 'projHatch', width: 5, height: 5, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' },
          React.createElement('rect', { width: 5, height: 5, fill: '#dcfce7' }),
          React.createElement('line', { x1: 0, y1: 0, x2: 0, y2: 5, stroke: '#4ade80', strokeWidth: 1.6 }))),
      lastRealIdx >= 0 && lastRealIdx < n - 1 && React.createElement('line', {
        key: 'sep', x1: (sx(lastRealIdx) + sx(lastRealIdx + 1)) / 2, y1: PT,
        x2: (sx(lastRealIdx) + sx(lastRealIdx + 1)) / 2, y2: syB(0),
        stroke: '#94a3b8', strokeWidth: 1, strokeDasharray: '3,3', opacity: 0.6,
      }),
      ...bars,
      tip,
    ].filter(Boolean));
  };

  // ── Line: cumulative realised, continued by the cumulative projection ──
  const renderLine = () => {
    const realPts = cumRows.filter((_, i) => lastRealIdx < 0 || i <= lastRealIdx)
      .map(r => ({ x: sx(r.i), y: syL(r.cum), i: r.i }));
    const projPts = lastRealIdx >= 0 && lastRealIdx < n - 1
      ? cumRows.slice(lastRealIdx).map(r => ({ x: sx(r.i), y: syL(r.cumProj), i: r.i }))
      : [];

    const realPath = bz(realPts);
    const projPath = bz(projPts);
    const f0 = realPts[0], fN = realPts[realPts.length - 1];
    const realArea = realPath && f0 ? `${realPath} L${fN.x},${syL(0)} L${f0.x},${syL(0)} Z` : '';
    const p0 = projPts[0], pN = projPts[projPts.length - 1];
    const projArea = projPath && p0 ? `${projPath} L${pN.x},${syL(0)} L${p0.x},${syL(0)} Z` : '';

    const tip = hovered != null && cumRows[hovered]
      ? tooltip(hovered, syL, hovered <= lastRealIdx ? cumRows[hovered].cum : cumRows[hovered].cumProj,
          hovered <= lastRealIdx ? t('rev.realCumul') : t('rev.projectedCumul'))
      : null;

    return scaffold(maxLine, syL, [
      projArea && React.createElement('path', { key: 'pa', d: projArea, fill: '#4ade8018' }),
      projPath && React.createElement('path', { key: 'pl', d: projPath, fill: 'none', stroke: '#4ade80', strokeWidth: 2.2, strokeDasharray: '8,5', opacity: 0.9 }),
      realArea && React.createElement('path', { key: 'ra', d: realArea, fill: '#05966914' }),
      realPath && React.createElement('path', { key: 'rl', d: realPath, fill: 'none', stroke: '#059669', strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round' }),
      ...rows.map((r, i) => React.createElement('rect', {
        key: 'h' + r.key, x: sx(i) - cW / Math.max(n, 1) / 2, y: PT,
        width: Math.max(6, cW / Math.max(n, 1)), height: cH, fill: 'transparent',
        style: { cursor: 'pointer' },
        onMouseEnter: () => setHovered(i),
        onClick: () => onSelect(r.key === selectedKey ? null : r.key),
      })),
      ...realPts.filter(p => rows[p.i] && rows[p.i].rental > 0).map(p => React.createElement('circle', {
        key: 'd' + p.i, cx: p.x, cy: p.y, r: hovered === p.i ? 5.5 : 3.5,
        fill: '#059669', stroke: '#fff', strokeWidth: 2, style: { pointerEvents: 'none', transition: 'r .12s' },
      })),
      tip,
    ].filter(Boolean));
  };

  // ── Pie: vehicle split over the visible range ──────────────────────────
  const pieAgg = {};
  rows.forEach(r => (r.vehicles || []).forEach(v => { pieAgg[v.name] = (pieAgg[v.name] || 0) + v.value; }));
  const pieData = Object.entries(pieAgg).filter(([, v]) => v > 0).map(([label, value]) => ({ label, value }));

  const legendVehicles = pieData.slice().sort((a, b) => b.value - a.value).slice(0, 6);
  const conf = forecast?.confidence;
  const confLabel = conf === 'high' ? t('rev.confHigh') : conf === 'medium' ? t('rev.confMedium') : t('rev.lowHistory');

  const legend = React.createElement('div', {
    style: { display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 10, fontSize: 11, color: 'var(--muted)' },
  },
    React.createElement('span', { style: { display: 'flex', alignItems: 'center', gap: 5 } },
      React.createElement('span', { style: { width: 10, height: 10, borderRadius: 3, background: '#059669', display: 'inline-block' } }),
      chartType === 'line' ? t('rev.realCumulTitle') : t('rev.realized')),
    forecast && forecast.dailyAverage > 0 && React.createElement('span', { style: { display: 'flex', alignItems: 'center', gap: 5 } },
      React.createElement('svg', { width: 20, height: 8 },
        React.createElement('line', { x1: 0, y1: 4, x2: 20, y2: 4, stroke: '#4ade80', strokeWidth: 2, strokeDasharray: '6,4' })),
      t('rev.projectionPerDay', { amount: KD.fmtDA(forecast.dailyAverage), conf: confLabel })),
    ...(chartType === 'bar' ? legendVehicles.map((v, i) =>
      React.createElement('span', { key: v.label, style: { display: 'flex', alignItems: 'center', gap: 5 } },
        React.createElement('span', { style: { width: 10, height: 10, borderRadius: 3, background: VCOLS[i % VCOLS.length], display: 'inline-block' } }),
        v.label.length > 22 ? v.label.slice(0, 22) + '…' : v.label)) : []),
  );

  return React.createElement(Card, null,
    React.createElement('div', {
      style: { display: 'flex', alignItems: 'center', gap: 12, padding: '14px 18px', borderBottom: '1px solid var(--line)', flexWrap: 'wrap', rowGap: 8 },
    },
      React.createElement('div', { className: 'sc-ico green', style: { width: 36, height: 36, borderRadius: 10 } },
        React.createElement(Icon, { name: 'trendUp', size: 18 })),
      React.createElement('div', null,
        React.createElement('div', { style: { fontSize: 14, fontWeight: 800, color: 'var(--ink)' } }, t('rev.evolution')),
        React.createElement('div', { style: { fontSize: 12, color: 'var(--muted)' } },
          selectedKey ? t('rev.clickToChange') : t('rev.clickToIsolate'))),
      React.createElement('div', { className: 'spacer' }),
      forecast && forecast.pipelineTotal > 0 && React.createElement('div', {
        style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#16a34a', background: '#f0fdf4', padding: '3px 10px', borderRadius: 99, border: '1px solid #bbf7d0' },
      }, React.createElement(Icon, { name: 'check', size: 12 }), t('rev.alreadyBookedAmount', { amount: KD.fmtDA(forecast.pipelineTotal) })),
      React.createElement('div', { className: 'seg' },
        React.createElement('button', { className: chartType === 'bar' ? 'on' : '', onClick: () => { setChartType('bar'); setHovered(null); } },
          React.createElement(Icon, { name: 'barChart', size: 13 }), t('rev.chartBar')),
        React.createElement('button', { className: chartType === 'line' ? 'on' : '', onClick: () => { setChartType('line'); setHovered(null); } },
          React.createElement(Icon, { name: 'trendUp', size: 13 }), t('rev.chartLine')),
        React.createElement('button', { className: chartType === 'pie' ? 'on' : '', onClick: () => { setChartType('pie'); setHovered(null); } },
          React.createElement(Icon, { name: 'pieChart', size: 13 }), t('rev.chartPie'))),
    ),
    React.createElement('div', { className: 'card-pad' },
      n === 0
        ? React.createElement(EmptyState, { variant: 'empty', icon: 'barChart', title: t('rev.noData'), desc: t('rev.noRevenue') })
        : chartType === 'pie'
          ? (pieData.length === 0
            ? React.createElement(EmptyState, { variant: 'empty', icon: 'pieChart', title: t('rev.noData'), desc: t('rev.noRevenue') })
            : React.createElement(PieChart, { data: pieData, height: (height || 280) - 30 }))
          : chartType === 'bar' ? renderBar() : renderLine(),
      n > 0 && chartType !== 'pie' && legend,
    ),
  );
}

// ─── Money rows ───────────────────────────────────────────────────────────
function MoneyRow({ label, value, hint, sign, tone, strong, big, border }) {
  const color = tone === 'neg' ? 'var(--rose-600)' : tone === 'pos' ? 'var(--brand-700)' : 'var(--ink)';
  return (
    <div className="kv kv-money" style={border ? { borderTop: '2px solid var(--line)', paddingTop: 10 } : undefined}>
      <span className="k" style={strong ? { fontWeight: 800, color: 'var(--ink)' } : undefined}>
        {label}
        {hint && <span style={{ display: 'block', fontSize: 11, color: 'var(--muted)', fontWeight: 400, marginTop: 1 }}>{hint}</span>}
      </span>
      <span className="v" style={{ color, fontWeight: strong || big ? 800 : 700, fontSize: big ? 17 : undefined, whiteSpace: 'nowrap' }}>
        {sign === '-' ? '− ' : sign === '+' ? '+ ' : ''}{KD.fmtDA(Math.abs(value || 0))}
      </span>
    </div>
  );
}

function LessorPayments() {
  const t = window.I18N ? I18N.useT() : ((k) => k);
  // ⚠️ Alias indispensable : le tableau des transactions itère en `rows.map(t => …)`, où `t`
  // est la TRANSACTION et masque donc la fonction de traduction. Renommer la variable de
  // boucle toucherait une trentaine de lignes ; l'alias en touche une.
  const tr = t;
  const loc = (window.I18N ? I18N.lang : 'fr') === 'ar' ? 'ar-DZ'
    : (window.I18N ? I18N.lang : 'fr') === 'en' ? 'en-GB' : 'fr-FR';
  const toast = useToast();
  const [period, setPeriod] = useS3('month');
  const [granularity, setGranularity] = useS3('day');
  const [data, setData] = useS3(null);
  const [loading, setLoading] = useS3(true);
  const [err, setErr] = useS3(false);
  // null = the whole period; otherwise a bucket key from the API.
  const [selectedKey, setSelectedKey] = useS3(null);
  // Ancrage de la fenêtre (YYYY-MM). null = période en cours.
  //
  // Sans lui, les quatre périodes sont toutes accrochées à aujourd'hui côté serveur : il était
  // impossible de consulter le mois de mars. Les flèches du carrousel ne déplaçaient que la
  // sélection À L'INTÉRIEUR de la fenêtre, jamais la fenêtre elle-même.
  const [anchor, setAnchor] = useS3(null);

  // Une granularité ne peut pas être plus grossière que la période : « Ce mois » + « Par mois »
  // ne produit qu'une seule pastille, soit un carrousel d'un élément.
  const GRANS = [
    { id: 'day', label: t('rev.byDay') },
    { id: 'week', label: t('rev.byWeek') },
    { id: 'month', label: t('rev.byMonth') },
  ];
  const allowedGrans = (p) => p === 'month' ? ['day', 'week'] : p === 'quarter' ? ['day', 'week', 'month'] : ['week', 'month'];
  const defaultGran = (p) => p === 'month' ? 'day' : p === 'quarter' ? 'week' : 'month';

  const load = (p, g, a) => {
    setLoading(true);
    setErr(false);
    API.lessor.getPayments(p, g, a)
      .then(res => { setData(res); setSelectedKey(null); setLoading(false); })
      .catch(() => { setErr(true); setLoading(false); });
  };

  useE3(() => { load(period, granularity, anchor); }, []);

  const changePeriod = (p) => {
    setPeriod(p);
    // Réaligne automatiquement la granularité plutôt que de laisser l'écran dans un état
    // qui ne peut rien afficher d'utile.
    const g = allowedGrans(p).includes(granularity) ? granularity : defaultGran(p);
    setGranularity(g);
    load(p, g, anchor);
  };
  const changeGranularity = (g) => { setGranularity(g); load(period, g, anchor); };

  /** Décale la fenêtre d'affichage de N périodes (−1 = précédente). */
  const shiftWindow = (dir) => {
    const base = anchor ? new Date(`${anchor}-01T00:00:00`) : new Date();
    const step = period === 'year' || period === 'last12' ? 12 : period === 'quarter' ? 3 : 1;
    base.setMonth(base.getMonth() + dir * step);
    const now = new Date();
    // On ne se projette pas dans le futur : il n'y a rien à y encaisser.
    if (base > now) { setAnchor(null); load(period, granularity, null); return; }
    const next = `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, '0')}`;
    setAnchor(next);
    load(period, granularity, next);
  };

  const backToNow = () => { setAnchor(null); load(period, granularity, null); };

  const exportPayments = (format) => {
    API.lessor.exportPayments(period, granularity, format, anchor)
      .catch(() => toast({ variant: 'error', title: t('book.exportFailed') }));
  };

  const summary = data?.summary || {};
  const transactions = data?.transactions || [];
  const buckets = data?.buckets || [];
  const forecast = data?.forecast || null;
  const selected = selectedKey ? buckets.find(b => b.key === selectedKey) : null;

  // THE view model: every figure below reads from here, so selecting a month in the carousel
  // moves the KPI cards, the summary and the transaction list together instead of leaving the
  // page showing three different periods at once.
  const view = selected
    ? { ...selected, label: selected.label }
    : {
        ...EMPTY_VIEW,
        rental: summary.grossTotal || 0,
        commission: summary.commission || 0,
        serviceFee: summary.serviceFeeTotal || 0,
        discount: summary.discountTotal || 0,
        lessorNet: summary.netTotal || 0,
        clientTotal: summary.collectedFromCustomers || 0,
        dueToPlatform: summary.dueToPlatform || 0,
        bookings: summary.bookingCount != null ? summary.bookingCount : transactions.length,
        label: t('rev.wholePeriod'),
      };

  const commissionPct = view.rental > 0 ? (view.commission / view.rental) * 100 : 0;
  const rows = selectedKey ? transactions.filter(t => t.bucketKey === selectedKey) : transactions;

  // Locations terminées dont l'encaissement n'est pas acté : ce qui transforme une pastille
  // à 0 DA muette en information exploitable.
  const awaitingTotal = selectedKey
    ? (selected?.awaitingSettlement || 0)
    : buckets.reduce((s, b) => s + (b.awaitingSettlement || 0), 0);

  // Libellé de la fenêtre affichée, lu du serveur : c'est lui qui a construit les bornes.
  const windowLabel = (() => {
    if (!data?.rangeStart) return '—';
    const from = new Date(data.rangeStart);
    const to = new Date(data.rangeEnd);
    const opts = { month: 'long', year: 'numeric' };
    const a = from.toLocaleDateString('fr-FR', opts);
    const b = to.toLocaleDateString('fr-FR', opts);
    const label = a === b ? a : `${from.toLocaleDateString('fr-FR', { month: 'short' })} → ${b}`;
    return label.charAt(0).toUpperCase() + label.slice(1);
  })();

  // Vehicle split for the selected scope.
  const vehicleRevenue = (() => {
    const map = {};
    (selected ? [selected] : buckets).forEach(b => (b.vehicles || []).forEach(v => {
      map[v.name] = (map[v.name] || 0) + v.value;
    }));
    return Object.entries(map)
      .map(([name, value], i) => ({ name, value, color: VCOLS[i % VCOLS.length] }))
      .sort((a, b) => b.value - a.value)
      .slice(0, 6);
  })();

  // Period-over-period delta, so a monthly figure is never shown without context.
  const trend = (() => {
    if (!selected) return undefined;
    const i = buckets.findIndex(b => b.key === selected.key);
    const prev = i > 0 ? buckets[i - 1] : null;
    if (!prev || !prev.rental) return undefined;
    const pct = ((selected.rental - prev.rental) / prev.rental) * 100;
    return { dir: pct >= 0 ? 'up' : 'down', value: `${pct >= 0 ? '+' : ''}${pct.toFixed(0)}%` };
  })();

  return (
    <div className="page-wide">
      <div className="toolbar">
        <div>
          <h2 style={{ margin: 0, fontSize: 22, fontWeight: 800, letterSpacing: '-.5px' }}>{t('rev.pageTitle')}</h2>
          <div className="muted" style={{ fontSize: 14, marginTop: 2 }}>
            {t('rev.pageSub')}
          </div>
        </div>
        <div className="spacer" />
        <Select value={granularity} onChange={(e) => changeGranularity(e.target.value)}>
          {GRANS.filter(g => allowedGrans(period).includes(g.id))
                .map(g => <option key={g.id} value={g.id}>{g.label}</option>)}
        </Select>
        <Select value={period} onChange={(e) => changePeriod(e.target.value)}>
          <option value="month">{t('rev.periodMonth')}</option>
          <option value="quarter">{t('rev.periodQuarter')}</option>
          <option value="year">{t('rev.year')}</option>
          <option value="last12">{t('rev.periodLast12')}</option>
        </Select>
        <Dropdown
          trigger={<Button variant="secondary" icon="download">{t('common.export')}</Button>}
          items={[
            { label: t('book.exportExcel'), onClick: () => exportPayments('xlsx') },
            { label: t('book.exportPdf'), onClick: () => exportPayments('pdf') },
          ]}
        />
      </div>

      {/* Navigation dans l'HISTORIQUE : déplace la fenêtre, là où les flèches du carrousel
          ne déplacent que la sélection à l'intérieur de celle-ci. */}
      {!loading && !err && (
        <div className="row" style={{ gap: 8, alignItems: 'center', marginBottom: 12 }}>
          <Button size="sm" variant="secondary" icon="chevLeft" onClick={() => shiftWindow(-1)}>{t('rev.previous')}</Button>
          <div style={{ fontWeight: 700, fontSize: 14 }}>{windowLabel}</div>
          <Button size="sm" variant="secondary" iconRight="chevRight" disabled={!anchor}
                  onClick={() => shiftWindow(1)}>{t('rev.next')}</Button>
          {anchor && <button className="lnk" onClick={backToNow}>{t('rev.backToToday')}</button>}
        </div>
      )}

      {!loading && !err && (
        <PeriodCarousel
          buckets={buckets}
          selectedKey={selectedKey}
          onSelect={setSelectedKey}
          granularity={data?.granularity || granularity}
        />
      )}

      {/* Un zéro doit s'expliquer : « rien ne s'est passé » et « 3 encaissements en attente »
          se ressemblent à l'écran alors qu'ils appellent des actions opposées. */}
      {!loading && !err && awaitingTotal > 0 && (
        <div className="card card-pad" style={{ marginBottom: 16, display: 'flex', gap: 10, alignItems: 'flex-start' }}>
          <Icon name="info" size={17} style={{ color: 'var(--amber-700)', flexShrink: 0, marginTop: 1 }} />
          <div style={{ fontSize: 13.5, lineHeight: 1.5 }}>
            <strong>{awaitingTotal} location(s) terminée(s)</strong> ne sont pas encore comptabilisées :
            leur encaissement n'a pas été confirmé. Elles apparaîtront dans vos revenus une fois
            marquées « Paiement reçu », ou automatiquement 48 h après la fin de la location.
          </div>
        </div>
      )}

      {!loading && !err && (
        <div className="muted" style={{ fontSize: 13, marginBottom: 10 }}>
          Chiffres affichés : <strong style={{ color: 'var(--ink)' }}>{view.label}</strong>
          {' · '}{view.bookings} location(s)
          {selectedKey && (
            <button className="lnk" style={{ marginLeft: 8 }} onClick={() => setSelectedKey(null)}>
              Voir toute la période
            </button>
          )}
        </div>
      )}

      <div className="stat-grid" style={{ marginBottom: 20 }}>
        {loading
          ? [0, 1, 2, 3].map(i => <Sk key={i} w="100%" h={110} r={16} />)
          : err
            ? <div className="card card-pad" style={{ gridColumn: '1/-1' }}><EmptyState variant="err" title={t('rev.loadFailed')} action={<Button variant="secondary" icon="refresh" onClick={() => load(period, granularity, anchor)}>{t('common.retry')}</Button>} /></div>
            : <>
              {/* Ordre revu pour suivre le trajet réel de l'argent : ce que le loueur a en main,
                  puis ce qu'il doit rendre, puis ce qui lui reste. La première carte montrait
                  `view.rental` (base de commission) sous un libellé qui laissait croire à
                  l'encaissement réel — or le client paie en plus les frais de service. */}
              <StatCard icon="banknote" color="green" value={KD.fmtNum(view.clientTotal)} label={t('rev.collected')}
                meta={t('rev.collectedSub')} trend={trend} />
              <StatCard icon="card" color="amber" value={KD.fmtNum(view.serviceFee)} label={t('rev.serviceFees')}
                meta={t('rev.serviceFeesSub')} />
              <StatCard icon="refresh" color="rose" value={KD.fmtNum(view.dueToPlatform)} label={t('rev.toRemit')}
                meta={t('rev.commissionFormula', { pct: commissionPct.toFixed(1) })} />
              <StatCard icon="wallet" color="violet" value={KD.fmtNum(view.lessorNet)} label={t('rev.netForYou')}
                meta={t('rev.netForYouSub')} />
            </>}
      </div>

      <div className="grid-2" style={{ gridTemplateColumns: '1.6fr 1fr', marginBottom: 20 }}>
        {loading
          ? <Card><div className="card-pad"><Sk w="100%" h={280} r={12} /></div></Card>
          : <RevenueChart buckets={buckets} forecast={forecast} selectedKey={selectedKey}
              onSelect={setSelectedKey} height={280} />}

        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <Card>
            <CardHead title={t('rev.breakdown')} sub={t('rev.byVehicle')} icon={{ name: 'car', color: 'blue' }} />
            <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {loading
                ? [0, 1, 2, 3].map(i => <Sk key={i} w="100%" h={28} r={8} />)
                : vehicleRevenue.length === 0
                  ? <EmptyState variant="empty" icon="car" title={t('rev.noData')} />
                  : vehicleRevenue.map((v, i) => {
                      const maxVal = vehicleRevenue[0].value || 1;
                      return (
                        <div key={v.name}>
                          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
                            <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--slate-700)' }}>{v.name}</span>
                            <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)' }}>{KD.fmtNum(v.value)} DA</span>
                          </div>
                          <div style={{ height: 6, background: 'var(--slate-100)', borderRadius: 99 }}>
                            <div style={{ height: '100%', width: `${(v.value / maxVal) * 100}%`, background: v.color, borderRadius: 99, transition: 'width .4s ease' }} />
                          </div>
                        </div>
                      );
                    })}
            </div>
          </Card>

          {/* Prévision — what the model rests on, said out loud. A projection whose basis is
              hidden is a number nobody can argue with, which is the same as a number nobody
              can trust. */}
          {!loading && forecast && (
            <Card>
              <CardHead title={t('rev.forecast')} sub={view.label} icon={{ name: 'trendUp', color: 'green' }} />
              <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                <MoneyRow label={t('rev.forecastSub')} value={forecast.horizonTotal} strong big tone="pos" />
                <MoneyRow label={t('rev.alreadyBooked')} hint={t('rev.asIs')} value={forecast.pipelineTotal} />
                <MoneyRow label={t('rev.dailyAverage')} value={forecast.dailyAverage} />
                <div className="kv kv-money">
                  <span className="k">{t('rev.trend')}</span>
                  <span className="v" style={{ fontWeight: 700, color: forecast.trend >= 1 ? 'var(--brand-700)' : 'var(--rose-600)' }}>
                    {forecast.trend >= 1 ? '↑' : '↓'} {Math.round(Math.abs(forecast.trend - 1) * 100)}%
                  </span>
                </div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', lineHeight: 1.5 }}>
                  {t('rev.basedOn', { days: forecast.basisDays, bookings: forecast.basisBookings })}
                  {forecast.confidence === 'low' && t('rev.lowHistoryNote')}
                  {forecast.weekdayProfile && t('rev.weekdayProfile')}
                </div>
              </div>
            </Card>
          )}
        </div>
      </div>

      {/* ── Transparence du décompte ────────────────────────────────────────
          Three blocks answering three different questions, because merging them is what made
          the old summary read as a 10.8% commission: the "à reverser" line is commission PLUS
          the service fee the renter paid to Uleez through the lessor, and nothing on screen
          said so. */}
      <div className="grid-2" style={{ gridTemplateColumns: '1fr 1fr 1fr', marginBottom: 20 }}>
        <Card>
          <CardHead title={t('rev.yourRevenue')} sub={t('rev.yourRevenueSub')} icon={{ name: 'wallet', color: 'violet' }} />
          <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {loading ? [0, 1, 2].map(i => <Sk key={i} w="100%" h={28} r={8} />) : <>
              <MoneyRow label={t('rev.billedRentals')} hint={t('rev.billedRentalsSub')} value={view.rental} />
              <MoneyRow label={t('rev.commissionUleez', { pct: commissionPct.toFixed(1) })} hint={t('rev.commissionSub')} value={view.commission} sign="-" tone="neg" />
              <MoneyRow label={t('rev.netToReceive')} value={view.lessorNet} strong big tone="pos" border />
              {view.rental > 0 && (
                <div style={{ marginTop: 4 }}>
                  <div style={{ height: 10, borderRadius: 99, background: 'var(--slate-100)', overflow: 'hidden', display: 'flex' }}>
                    <div title={t('rev.netShare', { pct: Math.round((view.lessorNet / view.rental) * 100) })}
                      style={{ width: `${(view.lessorNet / view.rental) * 100}%`, background: '#059669', transition: 'width .4s' }} />
                    <div title={`Commission : ${commissionPct.toFixed(1)}%`}
                      style={{ width: `${(view.commission / view.rental) * 100}%`, background: '#f43f5e', transition: 'width .4s' }} />
                  </div>
                  <div style={{ display: 'flex', gap: 14, marginTop: 6, fontSize: 11, color: 'var(--muted)' }}>
                    <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                      <span style={{ width: 8, height: 8, borderRadius: 99, background: '#059669', display: 'inline-block' }} />
                      Net {Math.round((view.lessorNet / view.rental) * 100)}%
                    </span>
                    <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                      <span style={{ width: 8, height: 8, borderRadius: 99, background: '#f43f5e', display: 'inline-block' }} />
                      Commission {commissionPct.toFixed(1)}%
                    </span>
                  </div>
                </div>
              )}
            </>}
          </div>
        </Card>

        <Card>
          <CardHead title={t('rev.collectedFrom')} sub={t('rev.throughYourHands')} icon={{ name: 'banknote', color: 'green' }} />
          <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {loading ? [0, 1, 2].map(i => <Sk key={i} w="100%" h={28} r={8} />) : <>
              <MoneyRow label={t('rev.billedRentals')} value={view.rental} />
              <MoneyRow label={t('rev.serviceFeeShort')} hint={t('rev.serviceFeeCollected')} value={view.serviceFee} sign="+" />
              <MoneyRow label={t('rev.discountsFunded')} hint={t('rev.discountsNote')} value={view.discount} sign="-" tone="pos" />
              <MoneyRow label={t('rev.totalCollected')} value={view.clientTotal} strong big border />
              <div className="banner-info" style={{ fontSize: 12, padding: '8px 12px', lineHeight: 1.5 }}>
                <Icon name="info" size={14} />
                {t('rev.depositNote')}
              </div>
            </>}
          </div>
        </Card>

        <Card>
          <CardHead title={t('rev.toRemitShort')} sub={t('rev.balanceDetail')} icon={{ name: 'refresh', color: 'rose' }} />
          <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {loading ? [0, 1, 2].map(i => <Sk key={i} w="100%" h={28} r={8} />) : <>
              <MoneyRow label={t('rev.commission')} hint={t('rev.commissionOf', { pct: commissionPct.toFixed(1) })} value={view.commission} />
              <MoneyRow label={t('rev.feesCollected')} hint={t('rev.feesCollectedNote')} value={view.serviceFee} sign="+" />
              <MoneyRow label={t('rev.discountsFunded')} hint={t('rev.discountsDeducted')} value={view.discount} sign="-" tone="pos" />
              <MoneyRow
                label={view.dueToPlatform < 0 ? t('rev.uleezRefunds') : t('rev.toRemitShort')}
                value={view.dueToPlatform} strong big border tone={view.dueToPlatform < 0 ? 'pos' : 'neg'} />
              <div style={{ fontSize: 11.5, color: 'var(--muted)', lineHeight: 1.5 }}>
                {t('rev.checkLine', {
                  collected: KD.fmtDA(view.clientTotal),
                  net: KD.fmtDA(view.lessorNet),
                  due: KD.fmtDA(view.dueToPlatform),
                })}
              </div>
              {summary.pendingCount > 0 && !selectedKey && (
                <div className="banner-info" style={{ fontSize: 12.5, padding: '8px 12px' }}>
                  <Icon name="clock" size={14} />
                  {t('rev.pendingCount', { n: summary.pendingCount, amount: KD.fmtDA(summary.pendingAmount || 0) })}
                </div>
              )}
            </>}
          </div>
        </Card>
      </div>

      <Card>
        <CardHead title={t('rev.transactions')} sub={selected ? selected.label : t('rev.historyTitle')} icon={{ name: 'wallet', color: 'amber' }} />
        {loading
          ? <div className="card-pad"><Sk w="100%" h={180} r={10} /></div>
          : rows.length === 0
            ? <EmptyState variant="empty" icon="wallet" title={t('rev.noCollection')}
                desc={awaitingTotal > 0
                  ? t('rev.awaitingConfirm', { n: awaitingTotal })
                  : t('rev.historyNote')} />
            : <div className="tbl-wrap"><table className="tbl">
                <thead><tr>
                  <th>{t('bill.colRef')}</th><th>{t('book.colVehicle')}</th>
                  {/* La date d'ENCAISSEMENT d'abord : c'est elle qui décide de la période à
                      laquelle la ligne appartient. La date de réservation devient contextuelle. */}
                  <th>{t('rev.colCollectedOn')}</th><th>{t('rev.colRentalPeriod')}</th>
                  <th>{t('rev.colCollection')}</th>
                  <th style={{ textAlign: 'right' }}>{t('rev.colRental')}</th>
                  <th style={{ textAlign: 'right' }}>{t('rev.commission')}</th>
                  <th style={{ textAlign: 'right' }}>{t('rev.serviceFeeShort')}</th>
                  <th style={{ textAlign: 'right' }}>{t('book.colCollected')}</th>
                  <th style={{ textAlign: 'right' }}>{t('book.colNet')}</th>
                </tr></thead>
                <tbody>
                  {rows.map(t => (
                    <tr key={t.id || t.referenceCode}>
                      <td className="mono strong">{t.referenceCode}</td>
                      <td>{t.vehicleName}</td>
                      <td>
                        <div className="cellstack">
                          <div className="nowrap">{t.settledAt ? new Date(t.settledAt).toLocaleDateString(loc, { day: 'numeric', month: 'short', year: '2-digit' }) : '—'}</div>
                          <div className="sub nowrap">{tr('rev.bookedOn', { date: t.createdAt ? new Date(t.createdAt).toLocaleDateString(loc, { day: 'numeric', month: 'short', year: '2-digit' }) : '—' })}</div>
                        </div>
                      </td>
                      <td className="nowrap">{t.pickupAt ? new Date(t.pickupAt).toLocaleDateString(loc, { day: 'numeric', month: 'short' }) : '—'} → {t.returnAt ? new Date(t.returnAt).toLocaleDateString(loc, { day: 'numeric', month: 'short' }) : '—'}</td>
                      <td>
                        {t.paymentPresumed
                          ? <span title={tr('rev.presumedHint')}>
                              <Badge cls="amber">{tr('book.presumed')}</Badge>
                            </span>
                          : <StatusBadge map={KD.PAYMENT_STATUS} value={t.status === 'paid' ? 'paid' : 'unpaid'} />}
                      </td>
                      <td className="cellnum" style={{ textAlign: 'right' }}>{KD.fmtDA(t.totalAmount)}</td>
                      <td className="cellnum" style={{ textAlign: 'right' }}>
                        {t.isWelcome ? <Badge cls="green">Bienvenue (0%)</Badge> : <span className="muted">− {KD.fmtDA(t.commissionAmount)}</span>}
                      </td>
                      {/* The line the lessor asked for: what the renter paid Uleez through them. */}
                      <td className="cellnum" style={{ textAlign: 'right', color: t.serviceFee ? 'var(--amber-700)' : 'var(--muted)' }}>
                        {t.serviceFee ? KD.fmtDA(t.serviceFee) : '—'}
                      </td>
                      <td className="cellnum" style={{ textAlign: 'right' }}>{KD.fmtDA(t.clientTotal != null ? t.clientTotal : t.totalAmount)}</td>
                      <td className="cellnum" style={{ textAlign: 'right', fontWeight: 800, color: 'var(--brand-700)' }}>{KD.fmtDA(t.netAmount)}</td>
                    </tr>
                  ))}
                </tbody>
                <tfoot><tr>
                  <td colSpan={5} style={{ fontWeight: 800 }}>Total · {view.label}</td>
                  <td className="cellnum" style={{ textAlign: 'right', fontWeight: 800 }}>{KD.fmtDA(view.rental)}</td>
                  <td className="cellnum" style={{ textAlign: 'right', fontWeight: 800 }}>− {KD.fmtDA(view.commission)}</td>
                  <td className="cellnum" style={{ textAlign: 'right', fontWeight: 800, color: 'var(--amber-700)' }}>{KD.fmtDA(view.serviceFee)}</td>
                  <td className="cellnum" style={{ textAlign: 'right', fontWeight: 800 }}>{KD.fmtDA(view.clientTotal)}</td>
                  <td className="cellnum" style={{ textAlign: 'right', fontWeight: 800, color: 'var(--brand-700)' }}>{KD.fmtDA(view.lessorNet)}</td>
                </tr></tfoot>
              </table></div>}
      </Card>
    </div>
  );
}

// ============================================================
// PROFILE
// ============================================================
function LessorProfile({ currentUser }) {
  const toast = useToast();
  const t = window.I18N ? I18N.useT() : ((k) => k);
  const [tab, setTab] = useS3('agency');
  const [profile, setProfile] = useS3(null);
  const [loading, setLoading] = useS3(true);
  const [saving, setSaving] = useS3(false);
  const [uploadingLogo, setUploadingLogo] = useS3(false);
  const [logoPreview, setLogoPreview] = useS3(null);
  const logoInputRef = React.useRef(null);
  const [form, setForm] = useS3({
    agencyName: '', type: 'independent', wilaya: 'Alger', rc: '', address: '', description: '',
    phone: '', email: '', website: '',
    bankName: '', rib: '', accountHolder: '',
    currentPassword: '', newPassword: '', confirmPassword: '',
  });

  // Avis clients de TOUTE la flotte. Chargés séparément du profil : l'endpoint a besoin de
  // l'id du loueur, que seul le profil apporte, et un échec ici ne doit pas empêcher
  // l'affichage (ni l'édition) du profil lui-même.
  const [reviews, setReviews] = useS3(null);
  const [reviewsError, setReviewsError] = useS3(false);
  const [reviewFilter, setReviewFilter] = useS3('');

  useE3(() => {
    API.lessor.getProfile()
      .then(data => {
        setProfile(data);
        const typeNorm = data.type === 'agency' ? 'agency' : 'independent';
        setForm(f => ({
          ...f,
          agencyName: data.businessName || data.agencyName || '',
          type: typeNorm,
          wilaya: data.wilaya || 'Alger',
          rc: data.legalIdentifier || data.registrationNumber || '',
          address: data.address || '',
          description: data.description || '',
          // ⚠️ On DÉCOUPE le E.164 renvoyé par l'API. La puce « +213 » était affichée à côté
          // d'une valeur qui contenait déjà « +213… » : l'écran montrait « +213 +213661… », et
          // réenregistrer persistait cette chaîne doublée en base.
          ...(() => {
            const split = API.splitE164(data.phone);
            return split
              ? { dial: split.dial, phone: split.national }
              : { dial: API.DEFAULT_DIAL, phone: String(data.phone || '').replace(/\D/g, '') };
          })(),
          email: data.email || '',
          website: data.website || '',
          rib: data.rib || '',
          accountHolder: data.accountHolder || data.businessName || '',
        }));
        setLoading(false);
      })
      .catch(() => {
        if (currentUser) {
          setForm(f => ({
            ...f,
            agencyName: currentUser.firstName || '',
            email: currentUser.email || '',
            phone: currentUser.phone || '',
          }));
        }
        setLoading(false);
      });
  }, []);

  // Les avis se chargent dans LEUR PROPRE effet, déclenché par l'id du profil.
  //
  // Ils étaient auparavant chargés depuis le `.then` de `getProfile`, et c'était un défaut de
  // conception : une exception dans cet appel secondaire faisait échouer la suite du `.then`,
  // donc `setForm(...)` et `setLoading(false)` ne s'exécutaient jamais. Le formulaire retombait
  // alors sur les valeurs de secours de `currentUser` — un loueur qui enregistrait écrasait son
  // nom d'agence par son prénom. Une section d'affichage secondaire ne doit jamais pouvoir
  // corrompre l'écran qui l'héberge.
  useE3(() => {
    const lessorId = profile && profile.id;
    if (!lessorId) return;
    let cancelled = false;
    // `try` en plus du `.catch` : celui-ci ne rattrape que les promesses rejetées, pas une
    // exception SYNCHRONE — typiquement une méthode d'API absente, ce qui est exactement la
    // faute d'origine (`API.lessor.getLessorReviews` n'existait pas, il était sur `API.admin`).
    // Sans ce garde, l'exception remonte hors de l'effet et emporte l'écran entier.
    try {
      API.lessor.getLessorReviews(lessorId)
        .then(rows => { if (!cancelled) setReviews(Array.isArray(rows) ? rows : []); })
        .catch(() => { if (!cancelled) { setReviews([]); setReviewsError(true); } });
    } catch (e) {
      setReviews([]);
      setReviewsError(true);
    }
    return () => { cancelled = true; };
  }, [profile && profile.id]);

  // Profil chargé mais sans id (ou chargement en échec) : il n'y a pas d'avis à attendre.
  // Sans ça, la section resterait indéfiniment sur son squelette de chargement.
  useE3(() => {
    if (!loading && !(profile && profile.id)) setReviews(r => (r === null ? [] : r));
  }, [loading, profile]);

  const save = async () => {
    setSaving(true);
    try {
      if (tab === 'security') {
        if (!form.currentPassword || !form.newPassword) {
          toast({ variant: 'error', title: t('profile.bothPasswords') });
          setSaving(false); return;
        }
        if (form.newPassword !== form.confirmPassword) {
          toast({ variant: 'error', title: t('profile.passwordMismatch') });
          setSaving(false); return;
        }
        // Dedicated endpoint. This used to POST currentPassword/newPassword to
        // PATCH /lessors/profile/me, whose DTO whitelists neither — and with the global
        // ValidationPipe in forbidNonWhitelisted mode that was a guaranteed 400, so changing
        // a lessor password had never worked.
        //
        // The server bumps passwordChangedAt (killing every existing session) and hands back
        // a fresh pair for THIS tab, otherwise we would log the user out of their own action.
        // Le 3ᵉ argument, `phone`, était OMIS. Or `api.jsx` retombe sur le chemin historique
        // « mot de passe en clair » dès qu'il est absent : le loueur contournait donc la
        // dérivation côté client sur laquelle repose la connexion, et son compte redescendait
        // en kdfVersion 0 — le serveur revoyait son mot de passe en clair, ce que toute
        // l'architecture E2EE cherche à éviter.
        const res = await API.auth.changePassword(
          form.currentPassword, form.newPassword, currentUser && currentUser.phone,
        );
        API.setSession(res);
        setForm(f => ({ ...f, currentPassword: '', newPassword: '', confirmPassword: '' }));
        toast({ variant: 'success', title: t('profile.passwordUpdated'), desc: t('profile.otherSessionsClosed') });
        setSaving(false);
        return;
      } else {
        await API.lessor.updateProfile({
          businessName: form.agencyName,
          type: form.type,
          wilaya: form.wilaya,
          legalIdentifier: form.rc,
          address: form.address,
          description: form.description,
          phone: API.toE164(form.dial, form.phone) || undefined,
          email: form.email,
          rib: form.rib,
          // ⚠️ « Site web », « Banque » et « Titulaire du compte » sont SAISISSABLES à l'écran
          // mais volontairement ABSENTS d'ici : les colonnes correspondantes n'existent pas sur
          // `Lessor`, et `UpdateLessorProfileDto` ne les déclare pas. Comme la ValidationPipe
          // tourne en `forbidNonWhitelisted`, les envoyer ferait échouer TOUT l'enregistrement
          // du profil en 400. Leur saisie reste donc perdue — le défaut est antérieur et sa
          // correction demande une colonne en base, hors périmètre de la V0.9.0.
        });
      }
      toast({ variant: 'success', title: t('profile.saved') });
    } catch (ex) {
      toast({ variant: 'error', title: t('common.error'), desc: ex.message });
    } finally {
      setSaving(false);
    }
  };

  const handleLogoChange = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setLogoPreview(URL.createObjectURL(file));
    setUploadingLogo(true);
    try {
      const updated = await API.lessor.uploadLogo(file);
      setProfile(p => ({ ...p, logoUrl: updated.logoUrl }));
      toast({ variant: 'success', title: t('profile.logoUpdated') });
    } catch (ex) {
      toast({ variant: 'error', title: t('profile.uploadFailed'), desc: ex.message });
      setLogoPreview(null);
    } finally {
      setUploadingLogo(false);
    }
  };

  const f = form;
  const setF = (k) => (e) => setForm(prev => ({ ...prev, [k]: e.target.value }));

  if (loading) return <div className="page-wide"><div className="profile-grid"><Sk w="100%" h={340} r={16} /><Sk w="100%" h={480} r={16} /></div></div>;

  const displayLogo = logoPreview || (profile?.logoUrl ? API.resolveUrl(profile.logoUrl) : null);
  const initials = (f.agencyName || currentUser?.firstName || '?')[0]?.toUpperCase() || '?';

  return (
    <div className="page-wide">
      <div className="toolbar">
        <h2 style={{ margin: 0, fontSize: 22, fontWeight: 800, letterSpacing: '-.5px' }}>{t('profile.title')}</h2>
        <div className="spacer" />
        <Button variant="primary" icon="check" loading={saving} onClick={save}>{t('common.save')}</Button>
      </div>

      <div className="profile-grid">
        <Card className="id-card">
          <div className="id-logo-wrap">
            <div className="id-logo" style={displayLogo ? {} : { background: 'linear-gradient(150deg,var(--brand-400),var(--brand-700))' }}>
              {displayLogo
                ? <img src={displayLogo} alt="Logo" style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 'inherit' }} />
                : <span style={{ fontSize: 38, color: '#fff', fontWeight: 800 }}>{initials}</span>}
              {uploadingLogo && <div className="id-logo-overlay"><div className="spin-sm" /></div>}
            </div>
          </div>
          <div style={{ fontWeight: 800, fontSize: 19 }}>{f.agencyName || t('profile.myAgency')}</div>
          <div className="muted" style={{ fontSize: 13.5, marginTop: 2 }}>{f.type === 'agency' ? t('profile.agency') : t('profile.individual')} · {f.wilaya}</div>
          <div style={{ marginTop: 12, display: 'flex', justifyContent: 'center' }}>
            <Badge cls={profile?.status === 'approved' ? 'green' : 'amber'}>
              <Icon name="shield" size={13} />{profile?.status === 'approved' ? t('profile.verified') : t('profile.pendingVerification')}
            </Badge>
          </div>
          <div style={{ display: 'flex', justifyContent: 'center', gap: 24, marginTop: 20, paddingTop: 18, borderTop: '1px solid var(--line)' }}>
            <div><div style={{ fontWeight: 800, fontSize: 20 }}>{profile?.vehicleCount ?? '—'}</div><div className="muted" style={{ fontSize: 12.5 }}>{t('profile.vehicles')}</div></div>
            <div><div style={{ fontWeight: 800, fontSize: 20 }}>★ {profile?.ratingAverage != null ? Number(profile.ratingAverage).toFixed(1) : '—'}</div><div className="muted" style={{ fontSize: 12.5 }}>{t('profile.rating')}</div></div>
            <div><div style={{ fontWeight: 800, fontSize: 20 }}>{profile?.reviewCount ?? '—'}</div><div className="muted" style={{ fontSize: 12.5 }}>{t('profile.reviews')}</div></div>
          </div>
          <input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleLogoChange} />
          <Button variant="secondary" size="sm" icon="image" className="btn-block" style={{ marginTop: 18 }} onClick={() => logoInputRef.current?.click()} loading={uploadingLogo}>{t('profile.changeLogo')}</Button>
        </Card>

        <Card>
          <div className="card-head" style={{ padding: 0, borderBottom: '1px solid var(--line)' }}>
            <div className="chips" style={{ padding: '14px 18px', gap: 6 }}>
              {[['agency', t('profile.tabAgency')], ['contact', t('profile.tabContact')], ['banking', t('profile.tabBanking')], ['security', t('profile.tabSecurity')], ['preferences', t('profile.tabPreferences')]].map(([k, l]) => (
                <button key={k} className={`chip ${tab === k ? 'on' : ''}`} onClick={() => setTab(k)}>{l}</button>
              ))}
            </div>
          </div>
          <div className="card-pad">
            {tab === 'agency' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
                <Field label={t('prof.agencyName')}><Input value={f.agencyName} onChange={setF('agencyName')} placeholder="Ex : Oran Drive" /></Field>
                {/* Les valeurs sont celles de l'API (`@IsEnum(['agency','independent'])`), pas
                    des libellés francisés. Elles étaient « agence » / « particulier » : aucune
                    option ne correspondait donc à la valeur chargée — le select s'affichait sur
                    la première entrée quelle que soit la réalité, et y toucher faisait échouer
                    l'enregistrement du profil en 400. */}
                <Field label={t('prof.type')}><Select value={f.type} onChange={setF('type')}><option value="agency">Agence</option><option value="independent">Particulier</option></Select></Field>
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
                <Field label={t('prof.wilaya')}><Select value={f.wilaya} onChange={setF('wilaya')}>{KD.WILAYAS.map(w => <option key={w}>{w}</option>)}</Select></Field>
                <Field label={t('prof.rc')}><Input value={f.rc} onChange={setF('rc')} placeholder="16/00-1234567 B 22" /></Field>
              </div>
              <Field label={t('prof.address')}><Input value={f.address} onChange={setF('address')} lead="pin" /></Field>
              <Field label={t('prof.description')} hint={t('prof.descHint')}><Textarea value={f.description} onChange={setF('description')} /></Field>
            </div>}
            {tab === 'contact' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
              <Field
                label={t('prof.phone')}
                error={f.phone && !API.isValidNational(f.dial || API.DEFAULT_DIAL, f.phone)
                  ? t('prof.phoneInvalid', { country: API.dialCodeFor(f.dial).name }) : null}
              >
                <PhoneInput
                  dial={f.dial || API.DEFAULT_DIAL}
                  national={f.phone}
                  onChange={(d, n) => setForm(x => ({ ...x, dial: d, phone: n }))}
                />
              </Field>
              <Field label={t('prof.email')}><Input value={f.email} onChange={setF('email')} lead="mail" /></Field>
              <Field label={t('prof.website')}><Input value={f.website} onChange={setF('website')} /></Field>
              <div className="banner-info"><Icon name="info" size={18} />{t('prof.contactNote')}</div>
            </div>}
            {tab === 'banking' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
              <Field label={t('prof.accountHolder')}><Input value={f.accountHolder} onChange={setF('accountHolder')} /></Field>
              <Field label={t('prof.iban')}><Input value={f.rib} onChange={setF('rib')} lead="card" /></Field>
              <Field label={t('prof.bank')}><Select value={f.bankName} onChange={setF('bankName')}><option>BNA - Banque Nationale d'Algérie</option><option>BEA</option><option>CPA</option><option>Société Générale Algérie</option></Select></Field>
              <div className="banner-info"><Icon name="shield" size={18} />{t('prof.bankNote')}</div>
            </div>}
            {tab === 'security' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
              <Field label={t('prof.currentPassword')}><Input type="password" value={f.currentPassword} onChange={setF('currentPassword')} lead="lock" /></Field>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
                <Field label={t('prof.newPassword')}><Input type="password" value={f.newPassword} onChange={setF('newPassword')} placeholder="••••••••" lead="lock" /></Field>
                <Field label={t('prof.confirmPassword')}><Input type="password" value={f.confirmPassword} onChange={setF('confirmPassword')} placeholder="••••••••" lead="lock" /></Field>
              </div>
              <div className="banner-info" style={{ background: 'var(--amber-bg)', borderColor: 'var(--amber-br)', color: '#b45309' }}><Icon name="shield" size={18} />{t('prof.twoFactorNote')}</div>
            </div>}
            {tab === 'preferences' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
              <Field label={t('common.language')} hint={t('common.languageHint')}>
                <Select
                  value={window.I18N ? I18N.lang : 'fr'}
                  onChange={(e) => {
                    // `setLang` applique la langue ET écrit `preferredLanguage` côté serveur :
                    // c'est ce champ que le backend lit pour choisir la langue des e-mails.
                    I18N.setLang(e.target.value);
                    toast({ variant: 'success', title: t('common.languageSaved') });
                  }}
                >
                  {(window.I18N ? I18N.SUPPORTED : ['fr']).map((code) => (
                    <option key={code} value={code}>{I18N.LANGUAGE_NAMES[code]}</option>
                  ))}
                </Select>
              </Field>
            </div>}
          </div>
        </Card>
      </div>

      <LessorReviews reviews={reviews} error={reviewsError} filter={reviewFilter} onFilter={setReviewFilter} />
    </div>
  );
}

/**
 * « Ce que les clients disent sur vos véhicules ».
 *
 * Les avis existaient en base et alimentaient déjà la note moyenne affichée sur la carte
 * d'identité — mais le loueur ne pouvait lire AUCUN commentaire depuis son espace. Il voyait
 * « ★ 4,2 » sans jamais savoir ce qui, concrètement, lui coûtait les huit dixièmes manquants.
 *
 * Le filtre par véhicule est ce qui rend la section actionnable sur une flotte : un même
 * défaut récurrent sur une seule voiture se voit immédiatement.
 */
function LessorReviews({ reviews, error, filter, onFilter }) {
  const t = window.I18N ? I18N.useT() : ((k) => k);

  if (reviews === null) {
    return <Card style={{ marginTop: 18 }}><div className="card-pad"><Sk w="100%" h={120} r={12} /></div></Card>;
  }

  // Les véhicules réellement présents dans les avis — et non toute la flotte : proposer un
  // filtre qui ne renvoie rien est pire que ne pas le proposer.
  const vehicles = [];
  const seen = {};
  reviews.forEach((r) => {
    if (r.vehicleId && !seen[r.vehicleId]) {
      seen[r.vehicleId] = true;
      vehicles.push({ id: r.vehicleId, name: r.vehicleName || '—' });
    }
  });

  const shown = filter ? reviews.filter((r) => r.vehicleId === filter) : reviews;
  const average = reviews.length
    ? (reviews.reduce((sum, r) => sum + Number(r.rating || 0), 0) / reviews.length)
    : 0;

  return (
    <Card style={{ marginTop: 18 }}>
      <div className="card-head">
        <div>
          <div style={{ fontWeight: 800, fontSize: 16 }}>{t('reviews.title')}</div>
          <div className="muted" style={{ fontSize: 13, marginTop: 2 }}>{t('reviews.subtitle')}</div>
        </div>
        <div className="spacer" />
        {reviews.length > 0 && (
          <div style={{ textAlign: 'center', minWidth: 92 }}>
            <div style={{ fontWeight: 800, fontSize: 22, color: 'var(--brand-700)' }}>★ {average.toFixed(1)}</div>
            <div className="muted" style={{ fontSize: 12 }}>{t('reviews.count', { n: reviews.length })}</div>
          </div>
        )}
      </div>

      {vehicles.length > 1 && (
        <div className="chips" style={{ padding: '12px 18px 0', gap: 6, flexWrap: 'wrap' }}>
          <button className={`chip ${filter === '' ? 'on' : ''}`} onClick={() => onFilter('')}>{t('reviews.allVehicles')}</button>
          {vehicles.map((v) => (
            <button key={v.id} className={`chip ${filter === v.id ? 'on' : ''}`} onClick={() => onFilter(v.id)}>{v.name}</button>
          ))}
        </div>
      )}

      <div className="card-pad">
        {error && <div className="banner-info" style={{ background: 'var(--amber-bg)', borderColor: 'var(--amber-br)', color: '#b45309' }}><Icon name="info" size={18} />{t('reviews.loadFailed')}</div>}
        {!error && shown.length === 0 && (
          <div style={{ textAlign: 'center', padding: '26px 12px' }}>
            <div style={{ fontWeight: 700, fontSize: 15 }}>{t('reviews.empty')}</div>
            <div className="muted" style={{ fontSize: 13.5, marginTop: 4, maxWidth: 420, marginInline: 'auto' }}>{t('reviews.emptyDesc')}</div>
          </div>
        )}
        {shown.map((r) => (
          <div key={r.id} className="review-row">
            <Avatar name={r.customerName || t('reviews.anonymous')} src={r.customerAvatarUrl ? API.resolveUrl(r.customerAvatarUrl) : undefined} size={38} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
                <span style={{ fontWeight: 700, fontSize: 14 }}>{r.customerName || t('reviews.anonymous')}</span>
                <span style={{ color: 'var(--amber-600)', fontSize: 13, fontWeight: 700 }}>{'★'.repeat(Math.round(Number(r.rating) || 0)) || '★'}</span>
                {r.vehicleName && <span className="muted" style={{ fontSize: 12.5 }}>· {r.vehicleName}</span>}
                <span className="spacer" />
                <span className="muted" style={{ fontSize: 12 }}>{r.createdAt ? new Date(r.createdAt).toLocaleDateString() : ''}</span>
              </div>
              <div style={{ fontSize: 13.5, lineHeight: 1.55, marginTop: 4, color: r.comment ? 'var(--ink)' : 'var(--muted)' }}>
                {r.comment || t('reviews.noComment')}
              </div>
            </div>
          </div>
        ))}
      </div>
    </Card>
  );
}

// ============================================================
// VEHICLE FORM (new / edit)
// ============================================================
// Champs acceptés par `CreateVehicleDto` / `UpdateVehicleDto` (backend). Une liste BLANCHE : ajouter
// un champ à ce formulaire ne l'envoie pas tant qu'on ne l'a pas déclaré ici ET côté serveur.
const VEHICLE_API_FIELDS = [
  'brand', 'model', 'year', 'category', 'transmission', 'fuelType', 'seats', 'luggageCount',
  'airConditioning', 'color', 'dailyPriceBase', 'weeklyPriceBase', 'monthlyPriceBase', 'depositAmount',
  'requiresManualApproval', 'deliveryAvailable', 'airportDeliveryAvailable', 'requiredDocuments',
  'pickupAddress', 'pickupCity', 'pickupLat', 'pickupLng', 'mileagePolicy', 'registrationNumber', 'description',
];
function pickVehicleFields(payload, restrictions, initialRestrictions) {
  const body = {};
  VEHICLE_API_FIELDS.forEach((k) => { if (payload[k] !== undefined) body[k] = payload[k]; });
  // `restrictions` n'est envoyé que s'il y a quelque chose à dire (des règles cochées, ou des règles
  // existantes à retirer) : un serveur plus ancien que ce champ refuserait sinon TOUTE création.
  if ((restrictions && restrictions.length) || (initialRestrictions && initialRestrictions.length)) {
    body.restrictions = restrictions || [];
  }
  return body;
}

function VehicleForm({ params, navigate }) {
  const t = window.I18N ? I18N.useT() : ((k) => k);
  const toast = useToast();
  const editing = !!(params && params.id);
  const [loading, setLoading] = useS3(editing);
  const [saving, setSaving] = useS3(false);
  const [uploadingPhoto, setUploadingPhoto] = useS3(false);
  const [photos, setPhotos] = useS3([]);
  const [vehicleId, setVehicleId] = useS3(editing ? params.id : null);
  const fileInputRef = useR3();
  // Règles que le véhicule avait au chargement : sert à décider s'il faut ENVOYER `restrictions`
  // (voir `pickVehicleFields`).
  const initialRestrictions = useR3([]);
  const [form, setForm] = useS3({
    brand: '', model: '', year: '', category: 'Berline', plate: '',
    seats: '5', luggageCount: '2', fuelType: 'Essence', transmission: 'Manuelle',
    airConditioning: true, color: '', mileagePolicy: 'Illimité',
    dailyPriceBase: '', weeklyPriceBase: '', monthlyPriceBase: '', depositAmount: '',
    pickupCity: 'Alger', pickupAddress: '', pickupLat: '', pickupLng: '',
    description: '',
    deliveryAvailable: false, airportDeliveryAvailable: false,
    requiresManualApproval: false,
    requiredDocuments: ['driving_license', 'id_card'],
    // Règles affichées sur la fiche (clés d'un vocabulaire FERMÉ, jamais du texte libre).
    restrictions: [],
  });

  useE3(() => {
    if (!editing) return;
    // La wilaya de l'agence est récupérée EN PARALLÈLE, seulement pour servir de repli si le
    // véhicule n'a lui-même aucune ville enregistrée — jamais pour écraser une vraie valeur
    // existante, même si elle vaut « Alger ».
    Promise.all([API.lessor.getVehicle(params.id), API.lessor.getProfile().catch(() => null)])
      .then(([data, profile]) => {
        const v = API.normalizeVehicle(data);
        setForm({
          brand: v.brand || '',
          model: v.model || '',
          year: String(v.year || ''),
          category: data.category || 'Berline',
          plate: data.plate || data.registrationNumber || '',
          seats: String(v.seats || '5'),
          luggageCount: String(data.luggageCount ?? '2'),
          fuelType: v.fuel || 'Essence',
          transmission: v.trans || 'Manuelle',
          airConditioning: data.airConditioning !== false,
          mileagePolicy: data.mileagePolicy || 'Illimité',
          dailyPriceBase: String(v.price || ''),
          weeklyPriceBase: String(data.weeklyPriceBase || ''),
          monthlyPriceBase: String(data.monthlyPriceBase || ''),
          depositAmount: String(data.depositAmount || ''),
          pickupCity: v.city || (profile && profile.wilaya) || 'Alger',
          pickupAddress: data.pickupAddress || '',
          pickupLat: data.pickupLat != null ? String(data.pickupLat) : '',
          pickupLng: data.pickupLng != null ? String(data.pickupLng) : '',
          description: data.description || '',
          deliveryAvailable: !!data.deliveryAvailable,
          airportDeliveryAvailable: !!data.airportDeliveryAvailable,
          requiresManualApproval: !!data.requiresManualApproval,
          color: data.color || '',
          requiredDocuments: data.requiredDocuments || ['driving_license', 'id_card'],
          restrictions: Array.isArray(data.restrictions) ? data.restrictions : [],
        });
        initialRestrictions.current = Array.isArray(data.restrictions) ? data.restrictions : [];
        if (data.photos) setPhotos(data.photos);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, [editing, params?.id]);

  // Nouveau véhicule : la wilaya de l'agence est un point de départ nettement plus probable que
  // « Alger » en dur (un loueur d'Oran qui ne pense pas à changer ce champ enregistrait sinon
  // chaque véhicule comme situé à Alger). La recherche filtre déjà sur la ville du VÉHICULE, pas
  // celle de l'agence (`vehicles.module.ts`) — c'est ce défaut de formulaire qui les faisait
  // toujours coïncider.
  useE3(() => {
    if (editing) return;
    API.lessor.getProfile()
      .then(data => { if (data && data.wilaya) setForm(f => ({ ...f, pickupCity: data.wilaya })); })
      .catch(() => {});
  }, [editing]);

  const setF = (k) => (e) => setForm(prev => ({ ...prev, [k]: e.target.value }));

  const handlePhotoSelect = async (e) => {
    const files = Array.from(e.target.files);
    if (!files.length) return;
    // For a new unsaved vehicle: queue locally as preview (will upload after create)
    if (!vehicleId) {
      files.forEach(file => {
        const reader = new FileReader();
        reader.onload = (ev) => {
          setPhotos(ps => [...ps, { url: ev.target.result, _file: file, _local: true }]);
        };
        reader.readAsDataURL(file);
      });
      return;
    }
    // For existing vehicle: upload immediately
    setUploadingPhoto(true);
    for (const file of files) {
      try {
        const isFirst = photos.length === 0;
        const photo = await API.lessor.uploadPhoto(vehicleId, file, isFirst);
        setPhotos(ps => [...ps, photo]);
      } catch (ex) {
        toast({ variant: 'error', title: t('veh.uploadError', { file: file.name }), desc: ex.message });
      }
    }
    setUploadingPhoto(false);
    e.target.value = '';
  };

  const removePhoto = async (photo, i) => {
    if (photo._local) { setPhotos(ps => ps.filter((_, j) => j !== i)); return; }
    if (!photo.id) { setPhotos(ps => ps.filter((_, j) => j !== i)); return; }
    try {
      await API.lessor.deletePhoto(vehicleId, photo.id);
      setPhotos(ps => ps.filter((_, j) => j !== i));
    } catch (ex) {
      toast({ variant: 'error', title: t('veh.deleteFailed'), desc: ex.message });
    }
  };

  // Glisser-déposer natif (aucune librairie dans cette app sans étape de build) : le drop
  // déplace la photo à sa nouvelle position localement, et — pour un véhicule déjà créé —
  // persiste l'ordre côté serveur. Pour un véhicule en cours de création, les photos sont
  // encore locales (`_local`, sans id) : rien à persister avant sa sauvegarde.
  const [dragIndex, setDragIndex] = useS3(null);
  const reorderPhotosAt = (from, to) => {
    if (from === to) return;
    setPhotos(ps => {
      const next = [...ps];
      const [moved] = next.splice(from, 1);
      next.splice(to, 0, moved);
      if (vehicleId) {
        const ids = next.filter(p => p.id).map(p => p.id);
        if (ids.length > 1) {
          API.lessor.reorderPhotos(vehicleId, ids)
            .catch(() => toast({ variant: 'error', title: t('common.error'), desc: t('veh.reorderError') }));
        }
      }
      return next;
    });
  };

  const submit = async (draft = false) => {
    setSaving(true);
    try {
      const { plate, ...rest } = form;
      const payload = {
        ...rest,
        year: parseInt(form.year) || undefined,
        seats: parseInt(form.seats) || 5,
        luggageCount: parseInt(form.luggageCount) || 0,
        dailyPriceBase: parseFloat(form.dailyPriceBase) || 0,
        weeklyPriceBase: form.weeklyPriceBase ? parseFloat(form.weeklyPriceBase) : undefined,
        monthlyPriceBase: form.monthlyPriceBase ? parseFloat(form.monthlyPriceBase) : undefined,
        depositAmount: parseFloat(form.depositAmount) || 0,
        pickupAddress: form.pickupAddress || undefined,
        pickupLat: form.pickupLat ? parseFloat(form.pickupLat) : undefined,
        pickupLng: form.pickupLng ? parseFloat(form.pickupLng) : undefined,
        description: form.description || undefined,
        registrationNumber: plate || undefined,
      };
      // Seuls les champs de l'API partent. Le formulaire porte des états d'écran (`plate`, et tout ce
      // qu'on y ajoutera) que le serveur REFUSE — « property … should not exist » — et qui bloquaient
      // la création d'un véhicule, donc sa validation par l'admin.
      const body = pickVehicleFields(payload, form.restrictions, initialRestrictions.current);
      if (editing) {
        await API.lessor.updateVehicle(params.id, body);
        // Upload any locally queued photos
        const localPhotos = photos.filter(p => p._local && p._file);
        if (localPhotos.length > 0) {
          setUploadingPhoto(true);
          for (const p of localPhotos) {
            try { await API.lessor.uploadPhoto(params.id, p._file, false); } catch {}
          }
          setUploadingPhoto(false);
        }
        toast({ variant: 'success', title: t('veh.updated') });
      } else {
        const created = await API.lessor.createVehicle(body);
        const newId = created.id;
        setVehicleId(newId);
        // Upload locally queued photos
        const localPhotos = photos.filter(p => p._local && p._file);
        if (localPhotos.length > 0 && newId) {
          setUploadingPhoto(true);
          for (const p of localPhotos) {
            try { await API.lessor.uploadPhoto(newId, p._file, false); } catch {}
          }
          setUploadingPhoto(false);
        }
        toast({ variant: draft ? 'info' : 'success', title: draft ? t('veh.draftSaved') : t('veh.submitted') });
      }
      navigate('vehicles');
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setSaving(false);
    }
  };

  if (loading) return <div className="page-wide" style={{ maxWidth: 920 }}><Sk w="100%" h={400} r={16} /></div>;

  const f = form;
  return (
    <div className="page-wide" style={{ maxWidth: 920 }}>
      <button className="btn btn-ghost btn-sm" style={{ marginBottom: 14, paddingLeft: 6 }} onClick={() => navigate('vehicles')}>
        <Icon name="arrowLeft" size={16} /> {t('veh.backToFleet')}
      </button>
      <div className="toolbar">
        <h2 style={{ margin: 0, fontSize: 22, fontWeight: 800, letterSpacing: '-.5px' }}>{editing ? t('veh.editTitle', { vehicle: `${f.brand} ${f.model}` }) : t('veh.add')}</h2>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
        <Card>
          <CardHead title={t('veh.photos')} sub={t('veh.photosHint')} icon={{ name: 'image', color: 'blue' }} />
          <div className="card-pad">
            <input ref={fileInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }} onChange={handlePhotoSelect} />
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(150px,1fr))', gap: 12 }}>
              {photos.map((p, i) => (
                <div key={i} draggable
                  onDragStart={() => setDragIndex(i)}
                  onDragOver={(e) => { e.preventDefault(); if (dragIndex !== null && dragIndex !== i) { reorderPhotosAt(dragIndex, i); setDragIndex(i); } }}
                  onDragEnd={() => setDragIndex(null)}
                  style={{ position: 'relative', cursor: 'grab', opacity: dragIndex === i ? 0.5 : 1 }}>
                  <img src={API.resolveUrl(p.url || p)} alt="" style={{ width: '100%', height: 108, objectFit: 'cover', borderRadius: 12 }} />
                  {(i === 0 || p.isCover) && <span className="badge green" style={{ position: 'absolute', top: 8, left: 8, fontSize: 11 }}>Couverture</span>}
                  {p._local && <span className="badge amber" style={{ position: 'absolute', bottom: 8, left: 8, fontSize: 11 }}>Local</span>}
                  <button className="vcard-fav" style={{ top: 8, right: 8 }} onClick={() => removePhoto(p, i)}><Icon name="trash" size={15} /></button>
                </div>
              ))}
              <button
                style={{ height: 108, border: '2px dashed var(--slate-300)', borderRadius: 12, background: 'var(--slate-50)', color: uploadingPhoto ? 'var(--brand-700)' : 'var(--slate-500)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, fontWeight: 700, fontSize: 13, cursor: 'pointer' }}
                disabled={uploadingPhoto}
                onClick={() => fileInputRef.current?.click()}>
                <Icon name={uploadingPhoto ? 'clock' : 'upload'} size={22} />
                {uploadingPhoto ? 'Upload…' : 'Ajouter'}
              </button>
            </div>
          </div>
        </Card>

        <Card>
          <CardHead title={t('veh.generalInfo')} icon={{ name: 'car', color: 'green' }} />
          <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 120px', gap: 14 }}>
              <Field label={t('veh.brand')}><Input value={f.brand} onChange={setF('brand')} placeholder="Ex : Renault" /></Field>
              <Field label={t('veh.model')}><Input value={f.model} onChange={setF('model')} placeholder="Ex : Clio" /></Field>
              <Field label={t('veh.year')}><Input type="number" value={f.year} onChange={setF('year')} placeholder="2022" /></Field>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
              <Field label={t('veh.category')}>
                <Select value={f.category} onChange={setF('category')}>
                  <option>Citadine</option><option>Berline</option><option>SUV</option><option>Utilitaire</option><option>Luxe</option>
                </Select>
              </Field>
              <Field label={t('veh.plate')}><Input value={f.plate} onChange={setF('plate')} placeholder="00000-000-00" className="mono" /></Field>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 14 }}>
              <Field label={t('veh.seats')}>
                <Select value={f.seats} onChange={setF('seats')}>
                  {[2,4,5,7,9].map(n => <option key={n} value={n}>{n}</option>)}
                </Select>
              </Field>
              <Field label={t('veh.luggage')}>
                <Select value={f.luggageCount} onChange={setF('luggageCount')}>
                  {[0,1,2,3,4,5].map(n => <option key={n} value={n}>{n}</option>)}
                </Select>
              </Field>
              <Field label={t('veh.fuel')}>
                <Select value={f.fuelType} onChange={setF('fuelType')}>
                  <option>Essence</option><option>Diesel</option><option>Hybride</option><option>GPL</option>
                </Select>
              </Field>
              <Field label={t('veh.gearbox')}>
                <Select value={f.transmission} onChange={setF('transmission')}>
                  <option>Manuelle</option><option>Auto</option>
                </Select>
              </Field>
            </div>
            <Field label={t('veh.description')}><Textarea value={f.description} onChange={setF('description')} placeholder={t('veh.descPlaceholder')} /></Field>
            <label className="row" style={{ gap: 10, alignItems: 'center', cursor: 'pointer' }}>
              <input type="checkbox" checked={f.airConditioning}
                onChange={(e) => setForm(prev => ({ ...prev, airConditioning: e.target.checked }))} />
              <div style={{ fontWeight: 700, fontSize: 13.5 }}>Climatisation</div>
            </label>
            <Field label={t('veh.color')}>
              <Input value={f.color} onChange={setF('color')} placeholder={t('veh.colorPlaceholder')} maxLength={50} />
            </Field>
          </div>
        </Card>

        <Card>
          <CardHead title={t('veh.pricingTitle')} icon={{ name: 'wallet', color: 'amber' }} />
          <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
              <Field label={t('veh.pricePerDay')}><Input type="number" value={f.dailyPriceBase} onChange={setF('dailyPriceBase')} placeholder="5000" /></Field>
              <Field label={t('veh.pricePerWeek')}><Input type="number" value={f.weeklyPriceBase} onChange={setF('weeklyPriceBase')} placeholder="30000" /></Field>
              <Field label={t('veh.pricePerMonth')}><Input type="number" value={f.monthlyPriceBase} onChange={setF('monthlyPriceBase')} placeholder="100000" /></Field>
            </div>
            <Field label={t('veh.deposit')}><Input type="number" value={f.depositAmount} onChange={setF('depositAmount')} placeholder="30000" /></Field>
            <Field label={t('veh.mileagePolicy')}>
              {/* Les options restent en français À DESSEIN : `mileagePolicy` est une chaîne
                  LIBRE stockée telle quelle et réaffichée au locataire dans l'application
                  mobile. Traduire les libellés ici changerait la DONNÉE écrite en base — un
                  loueur travaillant en anglais enregistrerait « Unlimited », que verrait
                  ensuite un client francophone. Rendre ce champ traduisible demande d'en faire
                  un énuméré côté serveur, avec un affichage dérivé : hors périmètre V0.9.0. */}
              <Select value={f.mileagePolicy} onChange={setF('mileagePolicy')}>
                <option>Illimité</option><option>100 km/jour</option><option>150 km/jour</option><option>200 km/jour</option><option>300 km/jour</option>
              </Select>
            </Field>
            <Field label={t('veh.availWilaya')}>
              <Select value={f.pickupCity} onChange={setF('pickupCity')}>{KD.WILAYAS.map(w => <option key={w}>{w}</option>)}</Select>
            </Field>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
              <Field label={t('veh.pickupAddress')}><Input value={f.pickupAddress} onChange={setF('pickupAddress')} placeholder={t('veh.addressPlaceholder')} /></Field>
              <Field label={t('veh.latitude')}><Input type="number" value={f.pickupLat} onChange={setF('pickupLat')} placeholder="36.7525" /></Field>
              <Field label={t('veh.longitude')}><Input type="number" value={f.pickupLng} onChange={setF('pickupLng')} placeholder="3.0420" /></Field>
            </div>
            <Field label={t('veh.restrictions')}>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
                {['no_smoking', 'pets_allowed', 'no_pets', 'mileage_limited', 'no_offroad', 'min_age_25'].map((k) => (
                  <label key={k} className="row" style={{ gap: 8, alignItems: 'center', cursor: 'pointer' }}>
                    <input type="checkbox" checked={f.restrictions.includes(k)}
                      onChange={(e) => setForm(prev => {
                        let next = e.target.checked ? [...prev.restrictions, k] : prev.restrictions.filter(x => x !== k);
                        // « animaux acceptés » et « non acceptés » s'excluent (le serveur le refuse aussi).
                        if (e.target.checked && k === 'pets_allowed') next = next.filter(x => x !== 'no_pets');
                        if (e.target.checked && k === 'no_pets') next = next.filter(x => x !== 'pets_allowed');
                        return { ...prev, restrictions: next };
                      })} />
                    <span style={{ fontSize: 13.5, fontWeight: 600 }}>{t('veh.restr.' + k)}</span>
                  </label>
                ))}
              </div>
            </Field>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <label className="row" style={{ gap: 10, alignItems: 'center', cursor: 'pointer' }}>
                <input type="checkbox" checked={f.deliveryAvailable}
                  onChange={(e) => setForm(prev => ({ ...prev, deliveryAvailable: e.target.checked }))} />
                <div style={{ fontWeight: 700, fontSize: 13.5 }}>Livraison disponible</div>
              </label>
              <label className="row" style={{ gap: 10, alignItems: 'center', cursor: 'pointer' }}>
                <input type="checkbox" checked={f.airportDeliveryAvailable}
                  onChange={(e) => setForm(prev => ({ ...prev, airportDeliveryAvailable: e.target.checked }))} />
                <div style={{ fontWeight: 700, fontSize: 13.5 }}>{t('veh.airportDelivery')}</div>
              </label>
            </div>
            <label className="row" style={{ gap: 10, alignItems: 'flex-start', cursor: 'pointer' }}>
              <input type="checkbox" style={{ marginTop: 3 }} checked={f.requiresManualApproval}
                onChange={(e) => setForm(prev => ({ ...prev, requiresManualApproval: e.target.checked }))} />
              <div>
                <div style={{ fontWeight: 700, fontSize: 13.5 }}>{t('veh.manualApproval')}</div>
                <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>
                  {t('veh.manualApprovalNote')}
                </div>
              </div>
            </label>
          </div>
        </Card>

        <Card>
          <CardHead title={t('veh.requiredDocs')} sub={t('veh.requiredDocsSub')} icon={{ name: 'clipboard', color: 'violet' }} />
          <div className="card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {/* 'rc'/'insurance' restent dans API.DOCUMENT_TYPES (réutilisé ailleurs pour
                étiqueter un document déjà envoyé, lessor2.jsx) mais n'ont pas de sens à
                exiger d'un LOCATAIRE — un registre de commerce ou une assurance sont des
                documents d'agence, pas de client. */}
            {API.DOCUMENT_TYPES.filter(dt => dt.value !== 'rc' && dt.value !== 'insurance').map(dt => (
              <label key={dt.value} className="row" style={{ gap: 10, alignItems: 'center', cursor: 'pointer' }}>
                <input type="checkbox" checked={f.requiredDocuments.includes(dt.value)}
                  onChange={(e) => setForm(prev => ({
                    ...prev,
                    requiredDocuments: e.target.checked
                      ? [...prev.requiredDocuments, dt.value]
                      : prev.requiredDocuments.filter(d => d !== dt.value),
                  }))} />
                <div style={{ fontWeight: 700, fontSize: 13.5 }}>{dt.label}</div>
              </label>
            ))}
          </div>
        </Card>

        <div className="row" style={{ justifyContent: 'flex-end', gap: 10 }}>
          <Button variant="ghost" onClick={() => navigate('vehicles')}>{t('common.cancel')}</Button>
          {!editing && <Button variant="secondary" loading={saving} onClick={() => submit(true)}>{t('veh.saveDraft')}</Button>}
          <Button variant="primary" icon="check" loading={saving} onClick={() => submit(false)}>{editing ? t('common.save') : t('veh.publish')}</Button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { LessorPayments, LessorProfile, LessorReviews, VehicleForm });
