/* Uleez — Admin screens part 2: vehicles moderation, bookings, support, settings */
const { useState: useA2, useEffect: useAE2 } = React;

// ============================================================
// ADMIN VEHICLES (moderation)
// ============================================================
function AdminVehicles() {
  const toast = useToast();
  const [rows, setRows] = useA2([]);
  const [loading, setLoading] = useA2(true);
  const [err, setErr] = useA2(false);
  const [tab, setTab] = useA2('all');
  const [reviewing, setReviewing] = useA2(null);
  const [rejecting, setRejecting] = useA2(null);
  const [suspending, setSuspending] = useA2(null);
  const [search, setSearch] = useA2('');
  const [actionLoading, setActionLoading] = useA2(null);

  const load = () => {
    setLoading(true);
    setErr(false);
    API.admin.getVehicles()
      .then(data => {
        setRows(Array.isArray(data) ? data.map(API.normalizeVehicle) : []);
        setLoading(false);
      })
      .catch(() => { setErr(true); setLoading(false); });
  };

  useAE2(() => { load(); }, []);

  const TABS = [
    { id: 'all',          label: 'Tous' },
    { id: 'under_review', label: 'À valider', alert: true },
    { id: 'published',    label: 'En ligne' },
    { id: 'suspended',    label: 'Suspendus' },
    { id: 'rejected',     label: 'Refusés' },
  ];

  // « suspendu » n'est pas une valeur de `status` : la suspension administrative est un
  // drapeau distinct qui se compose avec le statut de modération.
  const matchTab = (v, id) => id === 'all' ? true : id === 'suspended' ? v.adminSuspended : v.status === id;

  const filtered = rows
    .filter(v => matchTab(v, tab))
    .filter(v => {
      if (!search) return true;
      const q = search.toLowerCase();
      return `${v.brand} ${v.model}`.toLowerCase().includes(q)
        || v.plate.toLowerCase().includes(q)
        || (v.lessor?.businessName || '').toLowerCase().includes(q);
    });

  const count = (id) => rows.filter(v => matchTab(v, id)).length;

  const suspend = async (reason) => {
    setActionLoading(suspending.id);
    try {
      await API.admin.suspendVehicle(suspending.id, reason);
      setRows(rs => rs.map(x => x.id === suspending.id
        ? { ...x, adminSuspended: true, adminSuspensionReason: reason, visible: false } : x));
      toast({ variant: 'info', title: 'Annonce retirée de la recherche', desc: `${suspending.brand} ${suspending.model}` });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setActionLoading(null); setSuspending(null); setReviewing(null); }
  };

  const unsuspend = async (v) => {
    setActionLoading(v.id);
    try {
      const res = await API.admin.unsuspendVehicle(v.id);
      setRows(rs => rs.map(x => x.id === v.id
        ? { ...x, adminSuspended: false, adminSuspensionReason: null, visible: res?.visible } : x));
      // Lever la suspension ne republie pas forcément : le loueur peut avoir dépublié
      // l'annonce lui-même, ou son compte être suspendu. Le dire évite un aller-retour.
      toast(res?.visible === false
        ? { variant: 'info', title: 'Annonce réautorisée', desc: "Elle reste hors recherche : le loueur ne l'a pas publiée, ou son compte est suspendu." }
        : { variant: 'success', title: 'Annonce réautorisée', desc: `${v.brand} ${v.model} est de nouveau en ligne.` });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setActionLoading(null); }
  };

  const approve = async (v) => {
    setActionLoading(v.id);
    try {
      await API.admin.approveVehicle(v.id);
      setRows(rs => rs.map(x => x.id === v.id ? { ...x, status: 'published', published: true } : x));
      toast({ variant: 'success', title: 'Véhicule validé', desc: `${v.brand} ${v.model}` });
      setReviewing(null);
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setActionLoading(null);
    }
  };

  const doReject = async (reason) => {
    setActionLoading(rejecting.id);
    try {
      await API.admin.rejectVehicle(rejecting.id, reason);
      setRows(rs => rs.map(x => x.id === rejecting.id ? { ...x, status: 'rejected' } : x));
      toast({ variant: 'error', title: 'Véhicule refusé', desc: `${rejecting.brand} ${rejecting.model}` });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setActionLoading(null);
      setRejecting(null);
      setReviewing(null);
    }
  };

  return (
    <div className="page-wide">
      <div className="toolbar" style={{ marginBottom: 14 }}>
        <div className="chips">
          {TABS.map(t => (
            <Chip key={t.id} active={tab === t.id} count={count(t.id)} alert={t.alert && count(t.id) > 0} onClick={() => setTab(t.id)}>
              {t.label}
            </Chip>
          ))}
        </div>
        <div className="spacer" />
        <div className="header-search" style={{ display: 'flex', width: 260, background: 'var(--surface)', border: '1px solid var(--line)' }}>
          <Icon name="search" size={17} />
          <input placeholder="Marque, modèle, loueur…" value={search} onChange={e => setSearch(e.target.value)} />
        </div>
        <Button variant="secondary" icon="refresh" size="sm" onClick={load}>Actualiser</Button>
      </div>

      <Card>
        {loading && <div className="card-pad"><TableSkeleton cols={7} /></div>}
        {!loading && err && <EmptyState variant="err" title="Impossible de charger les véhicules" action={<Button variant="secondary" icon="refresh" onClick={load}>Réessayer</Button>} />}
        {!loading && !err && (
          <div className="tbl-wrap">
            <table className="tbl">
              <thead>
                <tr><th>Véhicule</th><th>Loueur</th><th>Ville</th><th>Catégorie</th><th>Prix / jour</th><th>Statut</th><th style={{ textAlign: 'right' }}>Actions</th></tr>
              </thead>
              <tbody>
                {filtered.length === 0 && <tr><td colSpan={7} style={{ textAlign: 'center', padding: 32, color: 'var(--muted)' }}>Aucun véhicule trouvé</td></tr>}
                {filtered.map(v => (
                  <tr key={v.id} className="clickable" onClick={() => setReviewing(v)}>
                    <td>
                      <div className="celluser">
                        <VehiclePhoto vehicle={v} w={48} h={36} radius={8} />
                        <div className="cu-meta"><div className="n">{v.brand} {v.model}</div><div className="e mono">{v.plate}</div></div>
                      </div>
                    </td>
                    <td>{v.lessor?.businessName || '—'}</td>
                    <td className="nowrap"><span className="row" style={{ gap: 6 }}><Icon name="pin" size={14} className="muted" />{v.city}</span></td>
                    <td>{v.cat}</td>
                    <td className="cellnum">{KD.fmtDA(v.price)}</td>
                    <td>
                      <div className="cellstack" style={{ gap: 4 }}>
                        <StatusBadge map={KD.VEHICLE_STATUS} value={v.status} />
                        {v.adminSuspended && (
                          <span title={v.adminSuspensionReason || 'Retiré de la recherche par Uleez'}>
                            <Badge cls="rose">Suspendu</Badge>
                          </span>
                        )}
                        {/* Un véhicule publié et validé peut malgré tout être hors recherche
                            si son LOUEUR est suspendu. Sans cette mention, l'admin lit
                            « En ligne » alors que le client ne voit rien. */}
                        {!v.adminSuspended && v.visible === false && v.status === 'published' && (
                          <span title="Le compte du loueur est suspendu">
                            <Badge cls="amber">Loueur suspendu</Badge>
                          </span>
                        )}
                      </div>
                    </td>
                    <td className="cellactions" onClick={(e) => e.stopPropagation()} style={{ textAlign: 'right' }}>
                      {v.status === 'under_review'
                        ? <div className="row" style={{ gap: 7, justifyContent: 'flex-end' }}>
                            <Button size="sm" variant="danger" icon="x" loading={actionLoading === v.id} onClick={() => setRejecting(v)}>Refuser</Button>
                            <Button size="sm" variant="success" icon="check" loading={actionLoading === v.id} onClick={() => approve(v)}>Valider</Button>
                          </div>
                        : <div className="row" style={{ gap: 7, justifyContent: 'flex-end' }}>
                            <Button size="sm" variant="ghost" iconRight="chevRight" onClick={() => setReviewing(v)}>Détail</Button>
                            <Dropdown align="right"
                              trigger={<button className="btn btn-secondary btn-icon sm"><Icon name="moreV" size={16} /></button>}
                              items={[
                                v.adminSuspended
                                  ? { icon: 'check', label: 'Réautoriser l\'annonce', onClick: () => unsuspend(v) }
                                  : { icon: 'ban', label: 'Suspendre l\'annonce', danger: true, onClick: () => setSuspending(v) },
                              ]} />
                          </div>}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </Card>

      {reviewing && (
        <Modal wide title={`${reviewing.brand} ${reviewing.model} ${reviewing.year}`}
          desc={`${reviewing.cat} · ${reviewing.city} · ${reviewing.plate}`}
          icon={{ name: 'car', color: 'green' }} onClose={() => setReviewing(null)}
          footer={reviewing.status === 'under_review' ? <>
            <Button variant="danger" icon="x" loading={actionLoading === reviewing.id} onClick={() => setRejecting(reviewing)}>Refuser</Button>
            <Button variant="primary" icon="check" loading={actionLoading === reviewing.id} onClick={() => approve(reviewing)}>Valider l'annonce</Button>
          </> : <Button variant="secondary" onClick={() => setReviewing(null)}>Fermer</Button>}>

          {/* Photo */}
          <VehiclePhoto vehicle={reviewing} h={200} radius={12} />

          {/* Quick specs row */}
          <div style={{ display: 'flex', gap: 10, marginTop: 14, flexWrap: 'wrap' }}>
            {[
              { icon: 'seats', label: `${reviewing.seats} places` },
              { icon: 'fuel',  label: reviewing.fuel },
              { icon: 'cog',   label: reviewing.trans },
              reviewing.hasAC && { icon: 'wind', label: 'Climatisé' },
              reviewing.instantBooking && { icon: 'check', label: 'Réservation instantanée' },
            ].filter(Boolean).map((s, i) =>
              <span key={i} className="vspec"><Icon name={s.icon} size={14} />{s.label}</span>
            )}
            <span className="vspec" style={{ marginLeft: 'auto', fontWeight: 800, fontSize: 15, color: 'var(--ink)' }}>{KD.fmtDA(reviewing.price)} / jour</span>
          </div>

          {/* Detail grid */}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 24px', marginTop: 16, borderTop: '1px solid var(--line)', paddingTop: 14 }}>
            {/* Left col — identification */}
            <div>
              <div className="flabel" style={{ marginBottom: 8 }}>Identification</div>
              {[
                ['Marque',          reviewing.brand],
                ['Modèle',          reviewing.model],
                ['Année',           reviewing.year],
                ['Immatriculation', reviewing.plate || '—'],
                ['Couleur',         reviewing.color || '—'],
                ['Catégorie',       reviewing.cat],
              ].map(([k, v]) =>
                <div key={k} className="kv"><span className="k">{k}</span><span className="v">{v}</span></div>
              )}
            </div>
            {/* Right col — pricing & policy */}
            <div>
              <div className="flabel" style={{ marginBottom: 8 }}>Tarifs & conditions</div>
              {[
                ['Prix / jour',     KD.fmtDA(reviewing.price)],
                ['Prix / semaine',  reviewing.weeklyPrice ? KD.fmtDA(reviewing.weeklyPrice) : '—'],
                ['Prix / mois',     reviewing.monthlyPrice ? KD.fmtDA(reviewing.monthlyPrice) : '—'],
                ['Caution',         reviewing.deposit ? KD.fmtDA(reviewing.deposit) : '—'],
                ['Kilométrage',     reviewing.mileage || '—'],
                ['Localisation',    reviewing.city || '—'],
              ].map(([k, v]) =>
                <div key={k} className="kv"><span className="k">{k}</span><span className="v">{v}</span></div>
              )}
            </div>
          </div>

          {/* Lessor */}
          {reviewing.lessor && (
            <div style={{ marginTop: 14, padding: '12px 14px', borderRadius: 10, background: 'var(--bg)', border: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 12 }}>
              <div className="sc-ico blue" style={{ width: 36, height: 36, borderRadius: 9, flexShrink: 0 }}><Icon name="building" size={16} /></div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontWeight: 700, fontSize: 13.5 }}>{reviewing.lessor.businessName || '—'}</div>
                <div className="muted" style={{ fontSize: 12 }}>{reviewing.lessor.city || ''}{reviewing.lessor.wilaya ? ` · ${reviewing.lessor.wilaya}` : ''}</div>
              </div>
              <StatusBadge map={KD.LESSOR_STATUS} value={reviewing.lessor.status} />
            </div>
          )}

          {/* Description */}
          {reviewing.description && (
            <div style={{ marginTop: 14 }}>
              <div className="flabel" style={{ marginBottom: 6 }}>Description</div>
              <p style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.6, margin: 0 }}>{reviewing.description}</p>
            </div>
          )}

          {/* Photo gallery thumbnails */}
          {reviewing.photos && reviewing.photos.length > 1 && (
            <div style={{ marginTop: 14 }}>
              <div className="flabel" style={{ marginBottom: 8 }}>Photos ({reviewing.photos.length})</div>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                {reviewing.photos.slice(0, 6).map((p, i) => (
                  <a key={i} href={API.resolveUrl(p.url)} target="_blank" rel="noopener noreferrer"
                    style={{ width: 90, height: 60, borderRadius: 8, overflow: 'hidden', display: 'block', border: '2px solid var(--line)' }}>
                    <img src={API.resolveUrl(p.url)} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                  </a>
                ))}
              </div>
            </div>
          )}

          {reviewing.status === 'under_review' && (
            <div className="banner-info" style={{ marginTop: 14 }}>
              <Icon name="info" size={18} />Vérifiez la conformité des photos, du prix et de la plaque d'immatriculation avant validation.
            </div>
          )}
        </Modal>
      )}
      {rejecting && <RejectModal title="Refuser le véhicule" who={`${rejecting.brand} ${rejecting.model}`} onClose={() => setRejecting(null)} onConfirm={doReject} />}
      {suspending && <SuspendVehicleModal v={suspending} onClose={() => setSuspending(null)} onConfirm={suspend} />}
    </div>
  );
}

// Retirer une annonce de la recherche. Distinct du refus : réversible d'un clic, sans
// repasser par une validation, et sans écraser le choix de publication du loueur.
function SuspendVehicleModal({ v, onClose, onConfirm }) {
  const [reason, setReason] = useA2('');
  const [touched, setTouched] = useA2(false);
  const err = touched && reason.trim().length < 5 ? 'Un motif d\'au moins 5 caractères est obligatoire.' : '';
  return (
    <Modal title="Suspendre l'annonce" desc={`${v.brand} ${v.model} · ${v.lessor?.businessName || ''}`}
      icon={{ name: 'ban', color: 'rose' }} onClose={onClose}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Annuler</Button>
        <Button variant="danger" icon="ban"
          onClick={() => { setTouched(true); if (reason.trim().length >= 5) onConfirm(reason.trim()); }}>
          Retirer de la recherche
        </Button>
      </>}>
      <div className="banner-info" style={{ marginBottom: 12 }}>
        Le véhicule disparaît immédiatement de la recherche et ne peut plus être réservé.
        Les réservations déjà confirmées ne sont pas touchées. Le loueur est prévenu, mais
        ne voit pas le motif ci-dessous et ne peut pas lever la suspension lui-même.
      </div>
      <Field label="Motif interne" error={err}>
        <Textarea placeholder="Photos non conformes, plaque illisible, doute sur la carte grise…"
          value={reason} error={!!err} onChange={(e) => setReason(e.target.value)} />
      </Field>
    </Modal>
  );
}

// ============================================================
// ADMIN BOOKINGS
// ============================================================
const BOOKING_SORTS = [
  ['date_desc',     'Date (récent → ancien)'],
  ['date_asc',      'Date (ancien → récent)'],
  ['amount_desc',   'Montant (élevé → faible)'],
  ['amount_asc',    'Montant (faible → élevé)'],
  ['customer_asc',  'Client (A → Z)'],
  ['customer_desc', 'Client (Z → A)'],
  ['lessor_asc',    'Loueur (A → Z)'],
  ['lessor_desc',   'Loueur (Z → A)'],
];

function AdminBookings() {
  const toast = useToast();
  const [rows, setRows] = useA2([]);
  const [loading, setLoading] = useA2(true);
  const [err, setErr] = useA2(false);
  const [tab, setTab] = useA2('all');
  const [sort, setSort] = useA2('date_desc');
  const [viewing, setViewing] = useA2(null);
  const [detail, setDetail] = useA2(null);        // full dossier for `viewing`
  const [detailLoading, setDetailLoading] = useA2(false);
  const [inspections, setInspections] = useA2([]);
  const [search, setSearch] = useA2('');
  const [query, setQuery] = useA2('');            // debounced, sent to the server
  const [delBusy, setDelBusy] = useA2(null);

  // Approve/reject a lessor's booking-deletion request.
  const respondDeletion = async (approve) => {
    if (!viewing) return;
    setDelBusy(approve ? 'approve' : 'reject');
    try {
      await API.admin.respondBookingDeletion(viewing.id, approve);
      toast({ variant: approve ? 'success' : 'info', title: approve ? 'Suppression approuvée' : 'Suppression refusée', desc: viewing.ref });
      closeDetail();
      load();
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setDelBusy(null);
    }
  };

  const load = () => {
    setLoading(true);
    setErr(false);
    API.admin.getBookings('all', 1, 200, query)
      .then(data => {
        const list = Array.isArray(data) ? data : (data?.data ?? []);
        setRows(list.map(API.normalizeBooking));
        setLoading(false);
      })
      .catch(() => { setErr(true); setLoading(false); });
  };

  useAE2(() => { load(); }, [query]);

  // The list is paged server-side, so filtering the loaded page would hide any older reference.
  // Debounce the input and let the API search the whole table.
  useAE2(() => {
    const t = setTimeout(() => setQuery(search.trim()), 350);
    return () => clearTimeout(t);
  }, [search]);

  const closeDetail = () => { setViewing(null); setDetail(null); setInspections([]); };

  // The row carries only what the list endpoint returns. Everything an admin needs to settle a
  // dispute — contacts, payments, extensions, payout split — comes from the detail endpoint.
  const openDetail = (row) => {
    setViewing(row);
    setDetail(null);
    setInspections([]);
    setDetailLoading(true);
    Promise.all([
      API.admin.getBookingDetail(row.id).then(API.normalizeBooking),
      API.lessor.getInspection(row.id).catch(() => []),
    ])
      .then(([full, insp]) => {
        setDetail(full);
        setInspections(Array.isArray(insp) ? insp : (insp ? [insp] : []));
      })
      .catch((ex) => toast({ variant: 'error', title: 'Détail indisponible', desc: ex.message }))
      .finally(() => setDetailLoading(false));
  };

  const TABS = [['all','Toutes'],['pending','En attente'],['confirmed','Confirmées'],['in_progress','En cours'],['completed','Terminées'],['deletion_requested','Suppr. demandée'],['cancelled','Annulées']];
  const filtered = tab === 'all' ? rows : rows.filter(b => b.status === tab);

  const sorted = [...filtered].sort((a, b) => {
    switch (sort) {
      case 'date_asc':      return new Date(a.pickupAt || 0) - new Date(b.pickupAt || 0);
      case 'date_desc':     return new Date(b.pickupAt || 0) - new Date(a.pickupAt || 0);
      case 'amount_asc':    return (a.amount || 0) - (b.amount || 0);
      case 'amount_desc':   return (b.amount || 0) - (a.amount || 0);
      case 'customer_asc':  return (a.renter?.name || '').localeCompare(b.renter?.name || '');
      case 'customer_desc': return (b.renter?.name || '').localeCompare(a.renter?.name || '');
      case 'lessor_asc':    return (a.lessor?.businessName || '').localeCompare(b.lessor?.businessName || '');
      case 'lessor_desc':   return (b.lessor?.businessName || '').localeCompare(a.lessor?.businessName || '');
      default: return 0;
    }
  });

  return (
    <div className="page-wide">

      <div className="stat-grid" style={{ marginBottom: 20 }}>
        <StatCard icon="clipboard" color="blue" value={rows.length || '—'} label="Total réservations" meta="Toutes périodes" />
        <StatCard icon="banknote" color="green" value={rows.length ? KD.fmtNum(Math.round(rows.reduce((s, b) => s + (b.amount || 0), 0) / 1000)) + ' k' : '—'} label="Volume DA" meta="Brut" />
        <StatCard icon="xCircle" color="rose" value={rows.length ? rows.filter(b => b.status === 'cancelled').length : '—'} label="Annulées" />
        <StatCard icon="checkCircle" color="amber" value={rows.length ? rows.filter(b => b.status === 'completed').length : '—'} label="Terminées" />
      </div>

      <div className="toolbar" style={{ marginBottom: 14 }}>
        <div className="chips">{TABS.map(([id, l]) => <Chip key={id} active={tab === id} onClick={() => setTab(id)}>{l}</Chip>)}</div>
        <div className="spacer" />
        <div className="header-search" style={{ display: 'flex', width: 260, background: 'var(--surface)', border: '1px solid var(--line)' }}>
          <Icon name="search" size={17} />
          <input placeholder="Référence, client, loueur, véhicule…" value={search} onChange={e => setSearch(e.target.value)} />
        </div>
        <Select value={sort} onChange={(e) => setSort(e.target.value)} style={{ width: 'auto' }}>
          {BOOKING_SORTS.map(([id, l]) => <option key={id} value={id}>{l}</option>)}
        </Select>
        <Button variant="secondary" icon="refresh" size="sm" onClick={load}>Actualiser</Button>
      </div>

      <Card>
        {loading && <div className="card-pad"><TableSkeleton cols={8} /></div>}
        {!loading && err && <EmptyState variant="err" title="Impossible de charger les réservations" action={<Button variant="secondary" icon="refresh" onClick={load}>Réessayer</Button>} />}
        {!loading && !err && (
          <div className="tbl-wrap">
            <table className="tbl">
              <thead>
                <tr><th>Référence</th><th>Locataire</th><th>Loueur</th><th>Véhicule</th><th>Date de réservation</th><th>Période de location</th><th>Montant</th><th>Paiement</th><th>Statut</th></tr>
              </thead>
              <tbody>
                {sorted.length === 0 && <tr><td colSpan={9} style={{ textAlign: 'center', padding: 32, color: 'var(--muted)' }}>Aucune réservation</td></tr>}
                {sorted.map(b => {
                  const vName = b.vehicleName || '—';
                  return (
                    <tr key={b.ref || b.id} className="clickable" onClick={() => openDetail(b)}>
                      <td className="mono strong">{b.ref}</td>
                      <td>
                        <div className="celluser">
                          <Avatar name={b.renter?.name || '?'} color={b.renter?.color} size={30} />
                          <div className="cu-meta"><div className="n">{b.renter?.name}</div></div>
                        </div>
                      </td>
                      <td>{b.lessor?.businessName || '—'}</td>
                      <td className="nowrap">{vName}</td>
                      <td className="nowrap">{b.createdAt ? new Date(b.createdAt).toLocaleString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
                      <td className="nowrap">{b.start} → {b.end}</td>
                      <td className="cellnum">{KD.fmtDA(b.amount)}</td>
                      <td><StatusBadge map={KD.PAYMENT_STATUS} value={b.payment} /></td>
                      <td><StatusBadge map={KD.BOOKING_STATUS} value={b.status} /></td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </Card>

      {viewing && (
        <AdminBookingDossier
          row={viewing}
          detail={detail}
          loading={detailLoading}
          inspections={inspections}
          delBusy={delBusy}
          onClose={closeDetail}
          onRespondDeletion={respondDeletion}
          onRefresh={() => openDetail(viewing)}
        />
      )}
    </div>
  );
}

// ============================================================
// ADMIN BOOKING DOSSIER
// ============================================================
/**
 * Everything the platform knows about one rental, in one place.
 *
 * The modal used to show 15 fields taken from the list row, which left an admin arbitrating a
 * dispute without the service fee, the extensions, the promo, the contacts or the money trail —
 * all of it already stored, none of it displayed. `GET /bookings/:id` returns the full record to
 * an admin (and audits the read), so this panel is a rendering problem, not a data one.
 *
 * `row` is the list entry (shown immediately); `detail` is the full dossier once it lands.
 */
function AdminBookingDossier({ row, detail, loading, inspections, delBusy, onClose, onRespondDeletion, onRefresh }) {
  const toast = useToast();
  const [tab, setTab] = useA2('reservation');
  const [voiding, setVoiding] = useA2(null);   // payment being cancelled
  const [receiptBusy, setReceiptBusy] = useA2(false);

  const b = detail || row;
  const acc = b.accounting || null;
  const ext = b.extensions || [];
  const payments = b.payments || [];

  const fmtDT = (d) => (d ? new Date(d).toLocaleString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—');
  const fmtD = (d) => (d ? new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) : '—');
  const kv = (k, v, opts = {}) => (
    <div className="kv"><span className="k">{k}</span><span className={`v${opts.strong ? ' strong' : ''}${opts.mono ? ' mono' : ''}`}>{v == null || v === '' ? '—' : v}</span></div>
  );

  const downloadReceipt = async () => {
    setReceiptBusy(true);
    try {
      await API.admin.getBookingReceipt(b.id, b.ref);
    } catch (ex) {
      toast({ variant: 'error', title: 'Reçu indisponible', desc: ex.message });
    } finally {
      setReceiptBusy(false);
    }
  };

  const TABS = [
    { id: 'reservation', label: 'Réservation', icon: 'calendar' },
    { id: 'parties', label: 'Parties', icon: 'user' },
    { id: 'argent', label: 'Argent', icon: 'wallet' },
    { id: 'historique', label: 'Historique', icon: 'clipboard' },
  ];

  return (
    <Modal wide title={`Réservation ${b.ref}`} desc={b.vehicleName}
      icon={{ name: 'clipboard', color: 'blue' }} onClose={onClose}
      footer={<>
        <Button variant="secondary" onClick={onClose}>Fermer</Button>
        <Button variant="secondary" icon="download" loading={receiptBusy} onClick={downloadReceipt}>Télécharger le reçu</Button>
        {b.status === 'deletion_requested' && <>
          <Button variant="danger" loading={delBusy === 'reject'} onClick={() => onRespondDeletion(false)}>Refuser la suppression</Button>
          <Button variant="primary" icon="trash" loading={delBusy === 'approve'} onClick={() => onRespondDeletion(true)}>Approuver la suppression</Button>
        </>}
      </>}>

      {b.status === 'deletion_requested' && (
        <div className="card card-pad" style={{ marginBottom: 16, borderColor: 'var(--amber-300, #fcd34d)', display: 'flex', gap: 12, alignItems: 'flex-start' }}>
          <Icon name="alert" size={20} className="muted" />
          <div style={{ fontSize: 13.5 }}>
            <strong>Le loueur demande la suppression de cette réservation.</strong> Vérifiez qu'elle n'a jamais commencé ni été payée avant d'approuver (anti-fraude commission).
            {b.deletionReason && <div className="muted" style={{ marginTop: 4 }}>Motif : {b.deletionReason}</div>}
          </div>
        </div>
      )}

      <div className="row" style={{ gap: 10, marginBottom: 14, flexWrap: 'wrap', alignItems: 'center' }}>
        <strong style={{ fontFamily: 'ui-monospace, monospace', fontSize: 15 }}>{b.ref}</strong>
        <StatusBadge map={KD.BOOKING_STATUS} value={b.status} />
        <StatusBadge map={KD.PAYMENT_STATUS} value={b.payment} />
        {loading && <span className="muted" style={{ fontSize: 12.5 }}>Chargement du dossier complet…</span>}
      </div>

      <div className="toolbar" style={{ marginBottom: 14 }}>
        {TABS.map((t) => (
          <button key={t.id} className={`chip ${tab === t.id ? 'active' : ''}`} onClick={() => setTab(t.id)}>
            <Icon name={t.icon} size={14} /> {t.label}
          </button>
        ))}
      </div>

      {tab === 'reservation' && (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
          <div>
            <div className="flabel" style={{ marginBottom: 8 }}>Réservation</div>
            {kv('Référence', b.ref, { mono: true })}
            {kv('Statut', <StatusBadge map={KD.BOOKING_STATUS} value={b.status} />)}
            {kv('Paiement', <StatusBadge map={KD.PAYMENT_STATUS} value={b.payment} />)}
            {kv('Mode de paiement', b.paymentOption === 'on_delivery' ? 'Espèces à la remise' : b.paymentOption)}
            {kv('Validation', b.approvalMode === 'manual' ? 'Manuelle par le loueur' : b.approvalMode === 'instant' ? 'Immédiate' : null)}
            {kv('Date de réservation', fmtDT(b.createdAt))}
            {b.awaitingPayment && kv('Expire le', fmtDT(b.expiresAt))}
            {kv('Période de location', `${b.start} → ${b.end}`)}
            {kv('Durée', `${b.days} jour(s)`)}
            {kv('Conducteur additionnel', b.extraDriver ? 'Oui' : 'Non')}
            {kv('Prise en charge', b.pickupLocation)}
            {kv('Restitution', b.returnLocation)}
            {b.notes && kv('Note client', b.notes)}
          </div>
          <div>
            {/* Coordinates already travel in the booking payload (normalizeBooking) —
                show them on a map like the lessor portal does, not as bare text. */}
            {(b.pickupLat != null || b.returnLat != null) && (
              <>
                <div className="flabel" style={{ marginBottom: 8 }}>Localisation</div>
                <MapView height={220} points={[
                  b.pickupLat != null ? { lat: b.pickupLat, lng: b.pickupLng, label: 'Prise en charge' + (b.pickupLocation ? ' — ' + b.pickupLocation : ''), color: '#059669' } : null,
                  b.returnLat != null ? { lat: b.returnLat, lng: b.returnLng, label: 'Retour' + (b.returnLocation ? ' — ' + b.returnLocation : ''), color: '#2563eb' } : null,
                ].filter(Boolean)} />
              </>
            )}
            <div className="flabel" style={{ marginTop: 16, marginBottom: 8 }}>Véhicule</div>
            {kv('Modèle', b.vehicleName)}
            {kv('Année', b.vehicle?.year)}
            {kv('Immatriculation', b.vehicle?.registrationNumber, { mono: true })}
            {kv('Ville de retrait', b.vehicle?.pickupCity)}
          </div>
        </div>
      )}

      {tab === 'parties' && (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
          <div>
            <div className="flabel" style={{ marginBottom: 8 }}>Locataire</div>
            {kv('Nom', b.renter?.name)}
            {kv('E-mail', b.renter?.email)}
            {kv('Téléphone', b.renter?.phone)}
            {kv('Date de naissance', b.renter?.dateOfBirth ? fmtD(b.renter.dateOfBirth) : null)}
            {kv('Statut KYC', b.renter?.kycStatus)}
            {kv('Identifiant', b.renter?.id, { mono: true })}
          </div>
          <div>
            <div className="flabel" style={{ marginBottom: 8 }}>Loueur</div>
            {kv('Raison sociale', b.lessor?.businessName)}
            {kv('Téléphone', b.lessor?.phone)}
            {kv('E-mail', b.lessor?.email)}
            {kv('Adresse', [b.lessor?.address, b.lessor?.city, b.lessor?.wilaya].filter(Boolean).join(', '))}
            {kv('RC', b.lessor?.legalIdentifier, { mono: true })}
            {kv('NIF', b.lessor?.taxIdentifier, { mono: true })}
            {kv('Taux de commission', b.commissionRate != null ? `${(b.commissionRate * 100).toFixed(1)} %` : null)}
            {kv('Période de bienvenue', b.isWelcomePeriod ? 'Oui (commission 0 %)' : 'Non')}
          </div>
        </div>
      )}

      {tab === 'argent' && (
        <div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
            <div>
              <div className="flabel" style={{ marginBottom: 8 }}>Ce que paie le client</div>
              {kv('Sous-total location', KD.fmtDA(b.subtotal))}
              {b.extra > 0 && kv('Prolongations et extras', KD.fmtDA(b.extra))}
              {kv('Frais de service (payés par le client)', KD.fmtDA(b.serviceFee))}
              {b.discount > 0 && kv(`Remise${b.promoCode ? ` (${b.promoCode})` : ''}`, `− ${KD.fmtDA(b.discount)}`)}
              {b.discount > 0 && kv('Remise financée par', b.discountFundedBy === 'lessor' ? 'Le loueur' : 'La plateforme')}
              {kv('Total client', KD.fmtDA(b.amount), { strong: true })}
              {kv('Caution', KD.fmtDA(b.deposit))}
            </div>
            <div>
              <div className="flabel" style={{ marginBottom: 8 }}>Répartition</div>
              {!acc && <div className="muted" style={{ fontSize: 13 }}>Répartition indisponible.</div>}
              {acc && <>
                {kv('Base commissionnable', KD.fmtDA(acc.rental))}
                {kv(`Commission Uleez${b.commissionRate != null ? ` (${(b.commissionRate * 100).toFixed(1)} %)` : ''}`, KD.fmtDA(acc.commission))}
                {kv('Net loueur', KD.fmtDA(acc.lessorNet))}
                {kv('Net dû à la plateforme', KD.fmtDA(acc.netDueToPlatform), { strong: true })}
              </>}
            </div>
          </div>

          <div className="flabel" style={{ marginTop: 18, marginBottom: 8 }}>Transactions</div>
          {payments.length === 0
            ? <div className="muted" style={{ fontSize: 13 }}>Aucun paiement enregistré.</div>
            : (
              <div className="tbl-wrap">
                <table className="tbl">
                  <thead><tr><th>Type</th><th>Méthode</th><th>Montant</th><th>Statut</th><th>Référence</th><th>Payé le</th><th /></tr></thead>
                  <tbody>
                    {payments.map((p) => (
                      <tr key={p.id}>
                        <td>{p.paymentType === 'deposit' ? 'Caution' : 'Location'}</td>
                        <td>{p.method === 'cash' ? 'Espèces' : p.method}</td>
                        <td className="nowrap">{KD.fmtDA(Number(p.amount))}</td>
                        <td><StatusBadge map={KD.PAYMENT_STATUS} value={p.status} /></td>
                        <td className="mono" style={{ fontSize: 12 }}>{p.transactionReference || '—'}</td>
                        <td className="nowrap">{fmtDT(p.paidAt)}</td>
                        <td>
                          {/* Only a cash record can be undone here: card rails must be reversed
                              at the processor, and the server enforces that too. */}
                          {p.status === 'completed' && p.provider === 'on_site' && (
                            <Button variant="ghost" size="sm" icon="x" onClick={() => setVoiding(p)}>Annuler</Button>
                          )}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
        </div>
      )}

      {tab === 'historique' && (
        <div>
          <div className="flabel" style={{ marginBottom: 8 }}>Prolongations</div>
          {ext.length === 0
            ? <div className="muted" style={{ fontSize: 13 }}>Aucune demande de prolongation.</div>
            : (
              <div className="tbl-wrap">
                <table className="tbl">
                  <thead><tr><th>Statut</th><th>Jours</th><th>Montant</th><th>Retour initial</th><th>Nouveau retour</th><th>Demandée le</th></tr></thead>
                  <tbody>
                    {ext.map((e) => (
                      <tr key={e.id}>
                        <td>{e.status === 'approved' ? 'Approuvée' : e.status === 'pending' ? 'En attente' : e.status === 'rejected' ? 'Refusée' : 'Expirée'}</td>
                        <td>+{e.extraDays}</td>
                        <td className="nowrap">{KD.fmtDA(Number(e.extraAmount))}</td>
                        <td className="nowrap">{fmtD(e.previousReturnAt)}</td>
                        <td className="nowrap">{fmtD(e.newReturnAt)}</td>
                        <td className="nowrap">{fmtDT(e.createdAt)}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}

          <div className="flabel" style={{ marginTop: 18, marginBottom: 8 }}>États des lieux</div>
          {inspections.length === 0
            ? <div className="muted" style={{ fontSize: 13 }}>Aucun état des lieux enregistré.</div>
            : inspections.map((i) => (
                <div key={i.id} className="req" style={{ padding: 10, marginBottom: 6 }}>
                  <div className="req-body">
                    <div style={{ fontWeight: 700, fontSize: 13 }}>
                      {i.mode === 'checkin' ? 'Départ' : 'Retour'} · {(i.photos || []).length} photo(s)
                    </div>
                    <div className="muted" style={{ fontSize: 12 }}>{fmtDT(i.createdAt)}{i.notes ? ` — ${i.notes}` : ''}</div>
                  </div>
                </div>
              ))}
        </div>
      )}

      {voiding && (
        <VoidPaymentModal
          payment={voiding}
          bookingRef={b.ref}
          onClose={() => setVoiding(null)}
          onDone={() => { setVoiding(null); onRefresh(); }}
        />
      )}
    </Modal>
  );
}

/** Undo a cash payment recorded in error. The reason is mandatory — the server stores it in
 *  `audit_logs` alongside the actor, so a correction is never anonymous. */
function VoidPaymentModal({ payment, bookingRef, onClose, onDone }) {
  const toast = useToast();
  const [reason, setReason] = useA2('');
  const [touched, setTouched] = useA2(false);
  const [saving, setSaving] = useA2(false);
  // Mirrors @MinLength(5) on VoidPaymentDto — fail here rather than on a 400 round-trip.
  const err = touched && reason.trim().length < 5 ? 'Indiquez un motif d\'au moins 5 caractères.' : '';

  const submit = async () => {
    setTouched(true);
    if (reason.trim().length < 5) return;
    setSaving(true);
    try {
      await API.admin.voidPayment(payment.id, reason.trim());
      toast({ variant: 'success', title: 'Encaissement annulé', desc: bookingRef });
      onDone();
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setSaving(false);
    }
  };

  return (
    <Modal title="Annuler l'encaissement" desc={`${KD.fmtDA(Number(payment.amount))} · ${bookingRef}`}
      icon={{ name: 'xCircle', color: 'rose' }} onClose={onClose}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Annuler</Button>
        <Button variant="danger" icon="x" loading={saving} onClick={submit}>Confirmer l'annulation</Button>
      </>}>
      <div className="muted" style={{ fontSize: 13, marginBottom: 12 }}>
        La réservation repassera en « non payée » et l'opération sera tracée dans le journal d'audit.
      </div>
      <Field label="Motif de l'annulation" error={err}>
        <Textarea placeholder="Ex. : encaissement saisi sur la mauvaise réservation…" value={reason}
          error={!!err} onChange={(e) => setReason(e.target.value)} />
      </Field>
    </Modal>
  );
}

// ============================================================
// ADMIN ACTIVITY (vue financière globale)
// ============================================================
const ACTIVITY_PERIODS = [
  ['month',   'Mensuel'],
  ['quarter', 'Trimestriel'],
  ['year',    'Annuel'],
];
const ACTIVITY_SORTS = [
  ['date_DESC',     'Date (récent → ancien)'],
  ['date_ASC',      'Date (ancien → récent)'],
  ['amount_DESC',   'Montant (élevé → faible)'],
  ['amount_ASC',    'Montant (faible → élevé)'],
  ['customer_ASC',  'Client (A → Z)'],
  ['customer_DESC', 'Client (Z → A)'],
  ['lessor_ASC',    'Loueur (A → Z)'],
  ['lessor_DESC',   'Loueur (Z → A)'],
];

function AdminActivity({ params, setParams }) {
  const [summary, setSummary] = useA2(null);
  const [activity, setActivity] = useA2(null);
  const [lessors, setLessors] = useA2([]);
  const [loading, setLoading] = useA2(true);
  const [err, setErr] = useA2(false);
  const [period, setPeriod] = useA2('month');
  const [lessorId, setLessorId] = useA2('');
  const [sort, setSort] = useA2('date_DESC');
  const [page, setPage] = useA2(1);
  const [search, setSearch] = useA2('');

  // Le Select choisit la GRANULARITÉ (mois / trimestre / année), l'ancre choisit LAQUELLE.
  // Les deux étaient confondus : on ne pouvait regarder que la période en cours, alors que
  // `getActivity` et `getFinanceSummary` acceptent déjà un paramètre `date`.
  const anchor = /^\d{4}-(0[1-9]|1[0-2])$/.test(params?.anchor || '') ? params.anchor : null;
  const setAnchor = (a) => {
    setPage(1);
    setParams(a ? { ...params, anchor: a } : (({ anchor, ...rest }) => rest)(params || {}));
  };

  const load = () => {
    setLoading(true);
    setErr(false);
    const [sortBy, sortOrder] = sort.split('_');
    const date = anchorToISO(anchor);
    Promise.all([
      API.admin.getFinanceSummary(period, date),
      API.admin.getActivity({ lessorId: lessorId || undefined, period, date, search: search || undefined, sortBy, sortOrder, page, limit: 50 }),
      API.admin.getLessors().catch(() => []),
    ]).then(([fin, act, ls]) => {
      setSummary(fin);
      setActivity(act);
      setLessors(Array.isArray(ls) ? ls.map(API.normalizeLessor) : []);
      setLoading(false);
    }).catch(() => { setErr(true); setLoading(false); });
  };

  useAE2(() => { load(); }, [period, lessorId, sort, page, search, anchor]);

  const rows = activity?.data || [];
  const totalPages = activity ? Math.max(1, Math.ceil(activity.total / activity.limit)) : 1;

  // Ce que les chiffres couvrent réellement — « Sur la période » ne disait pas LAQUELLE.
  const scopeLabel = period === 'month' ? anchorLabel(anchor)
    : period === 'quarter' ? `Trimestre de ${anchorLabel(anchor)}`
      : `Année ${(anchor || currentAnchor()).slice(0, 4)}`;

  return (
    <div className="page-wide">
      <PeriodNavigator
        anchor={anchor}
        onChange={setAnchor}
        disabled={loading}
        sub="Réservations et commissions rattachées à ce mois"
      />

      <div className="stat-grid" style={{ marginBottom: 20 }}>
        {loading
          ? [0,1,2].map(i => <div key={i} className="statcard"><Sk w={46} h={46} r={13} /><Sk w="60%" h={26} style={{ marginTop: 18 }} /><Sk w="80%" h={13} style={{ marginTop: 10 }} /></div>)
          : <>
              <StatCard icon="banknote" color="blue" value={summary ? KD.fmtDA(summary.grossTotal) : '—'} label="Total brut" meta={scopeLabel} />
              <StatCard icon="card" color="amber" value={summary ? KD.fmtDA(summary.commissionTotal) : '—'} label="Commission plateforme" meta={scopeLabel} />
              <StatCard icon="checkCircle" color="green" value={summary ? KD.fmtDA(summary.netTotal) : '—'} label="Net reversé aux loueurs" meta={scopeLabel} />
            </>}
      </div>

      <div className="toolbar" style={{ marginBottom: 14 }}>
        <div className="header-search" style={{ display: 'flex', width: 260, background: 'var(--surface)', border: '1px solid var(--line)' }}>
          <Icon name="search" size={17} />
          <input placeholder="Référence, client, loueur, véhicule…" value={search} onChange={e => { setSearch(e.target.value); setPage(1); }} />
        </div>
        <Select value={period} onChange={(e) => { setPeriod(e.target.value); setPage(1); }} style={{ width: 'auto' }}>
          {ACTIVITY_PERIODS.map(([id, l]) => <option key={id} value={id}>{l}</option>)}
        </Select>
        <Select value={lessorId} onChange={(e) => { setLessorId(e.target.value); setPage(1); }} style={{ width: 'auto' }}>
          <option value="">Tous les loueurs</option>
          {lessors.map(l => <option key={l.id} value={l.id}>{l.name}</option>)}
        </Select>
        <div className="spacer" />
        <Select value={sort} onChange={(e) => setSort(e.target.value)} style={{ width: 'auto' }}>
          {ACTIVITY_SORTS.map(([id, l]) => <option key={id} value={id}>{l}</option>)}
        </Select>
        <Button variant="secondary" icon="refresh" size="sm" onClick={load}>Actualiser</Button>
      </div>

      {summary && summary.byLessor?.length > 0 && (
        <Card style={{ marginBottom: 20 }}>
          <CardHead title="Répartition par loueur" sub={scopeLabel} icon={{ name: 'building', color: 'violet' }} />
          <div className="tbl-wrap">
            <table className="tbl">
              <thead><tr><th>Loueur</th><th className="cellnum">Brut</th><th className="cellnum">Commission</th><th className="cellnum">Net</th></tr></thead>
              <tbody>
                {summary.byLessor.map(l => (
                  <tr key={l.lessorId}>
                    <td>{l.businessName}</td>
                    <td className="cellnum">{KD.fmtDA(l.gross)}</td>
                    <td className="cellnum">{KD.fmtDA(l.commission)}</td>
                    <td className="cellnum">{KD.fmtDA(l.net)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </Card>
      )}

      <Card>
        {loading && <div className="card-pad"><TableSkeleton cols={10} /></div>}
        {!loading && err && <EmptyState variant="err" title="Impossible de charger l'activité" action={<Button variant="secondary" icon="refresh" onClick={load}>Réessayer</Button>} />}
        {!loading && !err && (
          <div className="tbl-wrap">
            <table className="tbl">
              <thead>
                <tr><th>Référence</th><th>Client</th><th>Loueur</th><th>Véhicule</th><th>Date de réservation</th><th>Période de location</th><th className="cellnum">Montant</th><th className="cellnum">Remise</th><th>Statut</th><th className="cellnum">Commission</th></tr>
              </thead>
              <tbody>
                {rows.length === 0 && <tr><td colSpan={10} style={{ textAlign: 'center', padding: 32, color: 'var(--muted)' }}>Aucune activité sur cette période</td></tr>}
                {rows.map(b => (
                  <tr key={b.id}>
                    <td className="mono strong">{b.referenceCode}</td>
                    <td>{b.customerName}</td>
                    <td>{b.lessorName}</td>
                    <td className="nowrap">{b.vehicleName}</td>
                    <td className="nowrap">{b.createdAt ? new Date(b.createdAt).toLocaleString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
                    <td className="nowrap">
                      {b.pickupAt ? new Date(b.pickupAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) : '—'}
                      {' → '}
                      {b.returnAt ? new Date(b.returnAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) : '—'}
                    </td>
                    <td className="cellnum">{KD.fmtDA(b.totalAmount)}</td>
                    <td className="cellnum" style={{ color: b.discountAmount > 0 ? 'var(--rose-600)' : undefined }}>
                      {b.discountAmount > 0 ? `- ${KD.fmtDA(b.discountAmount)}` : '—'}
                    </td>
                    <td><StatusBadge map={KD.BOOKING_STATUS} value={API.mapBookingStatus(b.status)} /></td>
                    <td className="cellnum">
                      {KD.fmtDA(b.commissionAmount)}
                      <div className="muted" style={{ fontSize: 11 }}>{(b.commissionRate * 100).toFixed(1)}% de la location</div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
        {!loading && !err && totalPages > 1 && (
          <div className="card-pad" style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, alignItems: 'center' }}>
            <Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>Précédent</Button>
            <span className="muted" style={{ fontSize: 13 }}>Page {page} / {totalPages}</span>
            <Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>Suivant</Button>
          </div>
        )}
      </Card>
    </div>
  );
}

// ============================================================
// ADMIN SUPPORT
// ============================================================
/**
 * Unlocks the shared support-desk private key for this session.
 *
 * Conversations are end-to-end encrypted; the desk holds its own sealed copy of every
 * conversation key so staff can handle disputes and moderation. That copy is sealed under
 * a passphrase the SERVER NEVER SEES — so an agent has to type it once per session, and a
 * database dump does not hand anyone the ability to read customer threads.
 */
// Politique de vérification du numéro (off / review / strict) + état du canal SMS.
//
// `strict` est NEUTRALISÉ tant que le canal SMS est fermé (le disjoncteur bascule sur `review`) :
// c'est ce qui empêche « plus de crédits → plus de réservations ». Le réglage reste `strict` en
// base et reprend seul son effet — l'écran montre donc la politique CONFIGURÉE et l'EFFECTIVE côte
// à côte, sinon un admin croirait `strict` sans effet pendant une panne.
const PHONE_POLICIES = {
  off:    { label: 'Désactivée',  desc: 'Aucun contrôle : tout le monde peut réserver.' },
  review: { label: 'Revue',       desc: 'Les numéros non prouvés sont listés dans Clients → Numéros à vérifier, et signalés au loueur. Rien n\'est bloqué.' },
  strict: { label: 'Stricte',     desc: 'Un numéro non prouvé ne peut pas réserver. Neutralisée automatiquement si le canal SMS est fermé.' },
};
function PhonePolicyCard() {
  const toast = useToast();
  const [ch, setCh] = useA2(null);
  const [policy, setPolicy] = useA2('off');
  const [saving, setSaving] = useA2(false);
  const load = () => API.admin.getSmsChannel().then(c => { setCh(c); if (c && c.configuredPolicy) setPolicy(c.configuredPolicy); }).catch(() => {});
  useAE2(() => { load(); }, []);
  const save = async () => {
    setSaving(true);
    try {
      await API.admin.updateSettings({ phoneVerificationPolicy: policy });
      toast({ variant: 'success', title: 'Politique enregistrée', desc: PHONE_POLICIES[policy].label });
      await load();
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setSaving(false); }
  };
  const open = ch && ch.health && ch.health.healthy === false;
  const neutralized = ch && ch.configuredPolicy === 'strict' && ch.effectivePolicy !== 'strict';
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div className="req" style={{ padding: 16 }}>
        <div className="req-body">
          <div style={{ fontWeight: 700 }}>Canal SMS</div>
          <div className="muted" style={{ fontSize: 13, lineHeight: 1.5 }}>
            {!ch ? 'Chargement…'
              : <>
                  <b style={{ color: open ? 'var(--rose-600, #be123c)' : 'var(--green-600, #15803d)' }}>{open ? 'Fermé (repli sur l\'e-mail)' : 'Opérationnel'}</b>
                  {' · '}crédits : {ch.credits != null ? ch.credits : 'inconnu'}
                  {ch.daysRemaining != null ? ` (≈ ${ch.daysRemaining} j au rythme actuel)` : ''}
                </>}
          </div>
        </div>
      </div>
      <div>
        <div className="flabel" style={{ marginBottom: 8 }}>Politique de vérification du numéro</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {Object.entries(PHONE_POLICIES).map(([k, v]) => (
            <label key={k} className="req" style={{ padding: 14, cursor: 'pointer', alignItems: 'flex-start', gap: 12, border: policy === k ? '1px solid var(--brand-600)' : undefined }}>
              <input type="radio" name="phone-policy" checked={policy === k} onChange={() => setPolicy(k)} style={{ marginTop: 3 }} />
              <div className="req-body">
                <div style={{ fontWeight: 700 }}>{v.label}{ch && ch.effectivePolicy === k ? '  · en vigueur' : ''}</div>
                <div className="muted" style={{ fontSize: 13, lineHeight: 1.5 }}>{v.desc}</div>
              </div>
            </label>
          ))}
        </div>
        {neutralized && (
          <div style={{ marginTop: 10, fontSize: 13, color: 'var(--amber-700, #b45309)', fontWeight: 600 }}>
            ⚠ « Stricte » est configurée mais NEUTRALISÉE : le canal SMS est fermé, la politique en vigueur est « Revue ». Elle reprendra seule dès la réouverture du canal.
          </div>
        )}
      </div>
      <div className="between">
        <span className="muted" style={{ fontSize: 12.5 }}>Un numéro n'est jamais réécrit depuis le back-office : seul le titulaire peut le corriger.</span>
        <Button variant="primary" icon="check" loading={saving} onClick={save}>Enregistrer</Button>
      </div>
    </div>
  );
}

function SupportDeskUnlock({ onUnlocked }) {
  const toast = useToast();
  const [pass, setPass] = useA2('');
  const [busy, setBusy] = useA2(false);

  const submit = async (e) => {
    e.preventDefault();
    setBusy(true);
    try {
      await E2E.unlockSupportDesk(pass);
      setPass('');
      toast({ variant: 'success', title: 'Bureau support déverrouillé' });
      onUnlocked();
    } catch (_) {
      toast({ variant: 'error', title: 'Passphrase incorrecte' });
    } finally { setBusy(false); }
  };

  return (
    <form onSubmit={submit} className="card card-pad" style={{ marginBottom: 16, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
      <div className="sc-ico amber" style={{ width: 36, height: 36, borderRadius: 10 }}>
        <Icon name="shield" size={18} />
      </div>
      <div style={{ flex: 1, minWidth: 220 }}>
        <div style={{ fontWeight: 800, fontSize: 14 }}>Conversations chiffrées</div>
        <div className="muted" style={{ fontSize: 12.5 }}>
          Saisissez la passphrase du bureau support pour lire les fils de cette session.
        </div>
      </div>
      <input type="password" value={pass} onChange={e => setPass(e.target.value)}
             placeholder="Passphrase du bureau support" autoComplete="off"
             style={{ width: 260, padding: '9px 11px', borderRadius: 10, border: '1px solid var(--line)', background: 'var(--surface)', color: 'var(--ink)' }} />
      <Button variant="primary" loading={busy} disabled={!pass}>Déverrouiller</Button>
    </form>
  );
}

// ============================================================
// SUPPORT — booking file behind a ticket
// ============================================================
// A support agent has no bookings screen and `GET /bookings/:id` 403s them: they are neither
// admin, nor the customer, nor the owning lessor. That made a dispute about a rental
// impossible to settle — the agent could read the conversation but knew nothing about the
// rental it was about.
//
// This panel is the whole of that access, and it opens ONLY from a ticket. The server serves
// a booking exclusively while some ticket references it (no list, no search) and audits every
// read, so an agent can see the rentals people asked support about and nothing else.
function SupportBookingPanel({ bookingId, onClose }) {
  const toast = useToast();
  const [data, setData] = useA2(null);
  const [loading, setLoading] = useA2(true);
  const [tab, setTab] = useA2('reservation');

  useAE2(() => {
    let cancelled = false;
    setLoading(true);
    API.support.getStaffBooking(bookingId)
      .then((d) => { if (!cancelled) { setData(d); setLoading(false); } })
      .catch((e) => {
        if (cancelled) return;
        setLoading(false);
        toast({ variant: 'error', title: 'Réservation indisponible', desc: e.message });
      });
    return () => { cancelled = true; };
  }, [bookingId]);

  const fmtD = (d) => (d ? new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) : '—');
  const fmtDT = (d) => (d ? new Date(d).toLocaleString('fr-FR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '—');
  const money = (n) => (n == null ? '—' : `${Number(n).toLocaleString('fr-FR')} DA`);

  const TABS = [
    { id: 'reservation', label: 'Réservation', icon: 'calendar' },
    { id: 'locataire', label: 'Locataire', icon: 'user' },
    { id: 'paiement', label: 'Paiement', icon: 'wallet' },
    { id: 'documents', label: 'Documents', icon: 'clipboard' },
  ];

  const b = data?.booking;
  const kv = (k, v) => <div className="kv"><span className="k">{k}</span><span className="v">{v ?? '—'}</span></div>;

  return (
    <Modal wide onClose={onClose} title="Détail de la réservation"
           desc="Informations et pièces utiles au traitement du litige"
           icon={{ name: 'calendar', color: 'blue' }}>
      {loading && <Sk w="100%" h={220} r={10} />}
      {!loading && !data && <EmptyState icon="alert" title="Réservation indisponible" desc="Aucun ticket ne référence cette réservation." />}
      {!loading && data && (
        <div>
          <div className="row" style={{ gap: 10, marginBottom: 14, flexWrap: 'wrap' }}>
            <strong style={{ fontFamily: 'ui-monospace, monospace', fontSize: 15 }}>{b.referenceCode}</strong>
            <StatusBadge map={KD.BOOKING_STATUS} value={b.status} />
            {/* Valeur serveur brute (« not_paid ») → clé d'affichage, même dérivation que normalizeBooking. */}
            <StatusBadge map={KD.PAYMENT_STATUS} value={
              (['cancelled', 'rejected', 'deleted'].includes(b.status) && (b.paymentStatus || 'not_paid') === 'not_paid')
                ? 'none' : API.mapPaymentStatus(b.paymentStatus)} />
          </div>

          <div className="toolbar" style={{ marginBottom: 14 }}>
            {TABS.map((t) => (
              <button key={t.id} className={`chip ${tab === t.id ? 'active' : ''}`} onClick={() => setTab(t.id)}>
                <Icon name={t.icon} size={14} /> {t.label}
              </button>
            ))}
          </div>

          {tab === 'reservation' && (
            <div>
              {kv('Véhicule', b.vehicle ? `${b.vehicle.name}${b.vehicle.year ? ` (${b.vehicle.year})` : ''}` : '—')}
              {kv('Immatriculation', b.vehicle?.plateNumber)}
              {kv('Départ', `${fmtD(b.pickupAt)} · ${b.pickupLocation || '—'}`)}
              {kv('Retour', `${fmtD(b.returnAt)} · ${b.returnLocation || '—'}`)}
              {kv('Montant total', money(b.totalAmount))}
              {kv('Créée le', fmtD(b.createdAt))}
              {kv('Loueur', data.lessor ? data.lessor.businessName : '—')}
              {kv('Contact loueur', data.lessor ? `${data.lessor.phone || '—'} · ${data.lessor.email || '—'}` : '—')}
              {kv('Adresse loueur', data.lessor ? [data.lessor.address, data.lessor.city, data.lessor.wilaya].filter(Boolean).join(', ') : '—')}
              <div style={{ marginTop: 14 }}>
                <div className="flabel" style={{ marginBottom: 6 }}>États des lieux</div>
                {data.inspections.length === 0
                  ? <div className="muted" style={{ fontSize: 13 }}>Aucun état des lieux enregistré.</div>
                  : data.inspections.map((i) => (
                      <div key={i.id} className="req" style={{ padding: 10, marginBottom: 6 }}>
                        <div className="req-body">
                          <div style={{ fontWeight: 700, fontSize: 13 }}>{i.mode === 'checkin' ? 'Départ' : 'Retour'} · {i.photoCount} photo(s)</div>
                          <div className="muted" style={{ fontSize: 12 }}>{fmtDT(i.createdAt)}{i.notes ? ` — ${i.notes}` : ''}</div>
                          {i.photos && i.photos.length > 0 && (
                            <div className="row" style={{ gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
                              {i.photos.map((p) => (
                                <AuthedImg key={p.id} path={p.downloadPath} alt={p.position}
                                           fetcher={API.support.staffInspectionPhotoObjectUrl}
                                           style={{ width: 72, height: 72, objectFit: 'cover', borderRadius: 8, border: '1px solid var(--line)' }} />
                              ))}
                            </div>
                          )}
                        </div>
                      </div>
                    ))}
              </div>
            </div>
          )}

          {tab === 'locataire' && (
            <div>
              {kv('Nom', data.customer?.name)}
              {kv('Email', data.customer?.email)}
              {kv('Téléphone', data.customer?.phone)}
            </div>
          )}

          {tab === 'paiement' && (
            data.payments.length === 0
              ? <div className="muted" style={{ fontSize: 13 }}>Aucun paiement enregistré.</div>
              : (
                <table className="table">
                  <thead><tr><th>Type</th><th>Méthode</th><th>Montant</th><th>Statut</th><th>Référence</th><th>Payé le</th></tr></thead>
                  <tbody>
                    {data.payments.map((p) => (
                      <tr key={p.id}>
                        <td>{p.paymentType === 'deposit' ? 'Caution' : 'Location'}</td>
                        <td>{p.method}</td>
                        <td className="nowrap">{money(p.amount)}</td>
                        <td><StatusBadge map={KD.PAYMENT_STATUS} value={p.status} /></td>
                        <td style={{ fontFamily: 'ui-monospace, monospace', fontSize: 12 }}>{p.transactionReference || '—'}</td>
                        <td className="nowrap">{fmtDT(p.paidAt)}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              )
          )}

          {tab === 'documents' && (
            data.documents.length === 0
              ? <div className="muted" style={{ fontSize: 13 }}>Aucun document.</div>
              : (
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 12 }}>
                  {data.documents.map((d) => {
                    const label = (API.DOCUMENT_TYPES.find((t) => t.value === d.documentType) || {}).label || d.documentType;
                    const isPdf = (d.mimeType || '') === 'application/pdf';
                    return (
                      <div key={d.id}>
                        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 6 }}>
                          <span style={{ fontWeight: 700, fontSize: 13 }}>{label}</span>
                          <StatusBadge map={KD.DOCUMENT_STATUS} value={d.hasContent ? d.verificationStatus : 'pending'} />
                        </div>
                        {/* Bytes never come from a URL: identity documents are encrypted at rest
                            and only GET /documents/:id/content (bearer token + audit row) can
                            serve them — hence AuthedImg's fetch → blob → object URL. */}
                        {!d.hasContent
                          ? <div className="muted" style={{ fontSize: 12.5, padding: 14, borderRadius: 10, border: '1px dashed var(--line)' }}>
                              En attente : le locataire doit renvoyer ce document.
                            </div>
                          : isPdf
                            ? <div className="row" style={{ gap: 10, padding: '18px 14px', cursor: 'pointer', borderRadius: 10, border: '1px solid var(--line)', background: 'var(--slate-100)' }}
                                   onClick={() => API.docs.download(d, `${d.documentType}.pdf`).catch(() => toast({ variant: 'error', title: 'Téléchargement impossible' }))}>
                                <Icon name="clipboard" size={22} className="muted" />
                                <div style={{ fontWeight: 700, fontSize: 13 }}>Document PDF chiffré</div>
                              </div>
                            : <div style={{ borderRadius: 10, overflow: 'hidden', border: '1px solid var(--line)', background: 'var(--slate-100)' }}>
                                <AuthedImg path={d} alt={label} fetcher={API.docs.objectUrl}
                                           style={{ width: '100%', height: 150, objectFit: 'cover', display: 'block' }} />
                              </div>}
                      </div>
                    );
                  })}
                </div>
              )
          )}
        </div>
      )}
    </Modal>
  );
}

function AdminSupport() {
  const toast = useToast();
  const [tickets, setTickets] = useA2([]);
  const [loading, setLoading] = useA2(true);
  const [err, setErr] = useA2(false);
  const [active, setActive] = useA2(null);
  const [reply, setReply] = useA2('');
  const [sending, setSending] = useA2(false);
  const [messages, setMessages] = useA2([]);
  const [search, setSearch] = useA2('');
  const [statusFilter, setStatusFilter] = useA2('all');
  const [replyTo, setReplyTo] = useA2(null);
  // Typing indicator, both directions. The desk was the only chat surface without one, so a
  // customer waiting on an emergency had no sign anyone was answering.
  const [peerTyping, setPeerTyping] = useA2(false);
  // bookingId whose detail panel is open, or null.
  const [bookingPanel, setBookingPanel] = useA2(null);
  const peerTypingRef = React.useRef(null);
  const typingRef = React.useRef(null);
  if (!typingRef.current) typingRef.current = createTypingSignal();
  useAE2(() => () => typingRef.current && typingRef.current.dispose(), []);
  // Desk key state: locked until an agent types the passphrase, and only relevant once a
  // desk key has actually been enrolled (older deployments have none).
  const [deskUnlocked, setDeskUnlocked] = useA2(() => !!(window.E2E && E2E.isStaffUnlocked()));
  const [deskEnrolled, setDeskEnrolled] = useA2(false);
  // `deskChecked` : on ne sait pas encore si une clé existe. Sans lui, la bannière « non enrôlé »
  // clignoterait à chaque ouverture, avant même la réponse du serveur.
  const [deskChecked, setDeskChecked] = useA2(false);

  useAE2(() => {
    let cancelled = false;
    API.keys.supportDeskPublic()
      .then(() => { if (!cancelled) { setDeskEnrolled(true); setDeskChecked(true); } })
      .catch(() => { if (!cancelled) { setDeskEnrolled(false); setDeskChecked(true); } });
    return () => { cancelled = true; };
  }, []);

  useAE2(() => {
    // Staff queue endpoint — works for admin AND support_agent, returns minimized data.
    API.support.getStaffTickets()
      .then(data => {
        // Le SERVEUR est l'autorité : `getStaffTickets` ne renvoie que les tickets d'assistance et les
        // SOS (jamais une conversation loueur↔client). Le filtre client qui doublait cette règle
        // (`!bookingId && category !== 'booking'`) faisait disparaître de la file tout ticket rattaché
        // à une réservation — c'est l'une des trois raisons pour lesquelles « les messages ne
        // remontaient pas ». Deux règles qui divergent finissent toujours par mentir.
        const list = Array.isArray(data) ? data : [];
        setTickets(list);
        if (list.length > 0) setActive(list[0].id);
        setLoading(false);
      })
      .catch(() => { setErr(true); setLoading(false); });
  }, []);

  // Messages of the active ticket, over the socket — this used to re-GET the whole thread
  // every 4 seconds, which both lagged behind an emergency and re-fetched every attachment.
  // The one-shot load below is the initial fill; everything after arrives pushed.
  useAE2(() => {
    if (!active) { setMessages([]); return; }
    let cancelled = false;
    setPeerTyping(false); // a stale bubble must not carry over to another ticket
    typingRef.current.stop(); // …nor must our own signal follow us to the new one
    API.support.getMessages(active)
      .then(data => { if (!cancelled) setMessages(Array.isArray(data) ? data : []); })
      .catch(() => {});

    API.Realtime.connect();
    API.Realtime.joinConversation(active);
    const offMsg = API.Realtime.on('message:new', async (raw) => {
      if (cancelled || !raw || raw.ticketId !== active) return;
      // Staff decrypt through the support-desk grant (see E2E.conversationKey). If the desk
      // key is still locked, the message arrives flagged `undecryptable` and renders as such.
      const m = await API.support.decryptMessage(active, raw);
      if (cancelled) return;
      setPeerTyping(false); // the message landed — whatever they were typing is now sent
      setMessages(prev => (prev.some(x => x.id && x.id === m.id) ? prev : [...prev, m]));
    });
    const offReact = API.Realtime.on('message:reaction', (e) => {
      if (cancelled || !e) return;
      setMessages(prev => prev.map(m => (m.id === e.messageId
        ? { ...m, reactions: API.support.mergeReactions(m.reactions, e) }
        : m)));
    });
    const offTyping = API.Realtime.on('conversation:typing', (e) => {
      if (cancelled || !e || e.ticketId !== active) return;
      const typing = e.typing !== false;
      setPeerTyping(typing);
      if (peerTypingRef.current) clearTimeout(peerTypingRef.current);
      if (typing) peerTypingRef.current = setTimeout(() => setPeerTyping(false), TYPING_EXPIRY_MS);
    });
    return () => {
      cancelled = true;
      offMsg(); offReact(); offTyping();
      if (peerTypingRef.current) clearTimeout(peerTypingRef.current);
      API.Realtime.leaveConversation(active);
    };
  }, [active]);

  // Toggle a reaction, then reconcile with the server's authoritative summary.
  const react = async (messageId, emoji) => {
    const previous = messages;
    try {
      const res = await API.support.react(messageId, emoji);
      const reactions = (res && res.reactions) || [];
      setMessages(ms => ms.map(m => (m.id === messageId ? { ...m, reactions } : m)));
    } catch (ex) {
      setMessages(previous);
      toast({ variant: 'error', title: 'Réaction impossible' });
    }
  };

  const t = tickets.find(x => x.id === active);
  useAE2(() => { setReplyTo(null); }, [active]);
  const PRIO = { critical: { label: '🆘 Urgence', cls: 'rose' }, high: { label: 'Urgent', cls: 'rose' }, medium: { label: 'Moyen', cls: 'amber' }, low: { label: 'Faible', cls: 'gray' }, med: { label: 'Moyen', cls: 'amber' } };
  const TST  = { open: { label: 'Ouvert', cls: 'blue' }, pending: { label: 'En attente', cls: 'amber' }, resolved: { label: 'Résolu', cls: 'green' }, in_progress: { label: 'En cours', cls: 'blue' } };
  // Helper: extract display info from a ticket (real or mock shape)
  const tSubject = (tk) => tk.subject || tk.category || tk.message?.slice(0, 60) || 'Ticket';
  const tFrom    = (tk) => tk.from?.name || tk.openedBy?.firstName || tk.openedByUser?.firstName || 'Utilisateur';
  const tTime    = (tk) => tk.time || (tk.createdAt ? new Date(tk.createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) : '—');

  const sendMsg = async () => {
    if (!reply.trim() || !t) return;
    typingRef.current.stop(); // the message is on its way — stop claiming we're still typing
    setSending(true);
    try {
      await API.support.sendMessage(t.id, reply, replyTo ? replyTo.id : undefined);
      setReply('');
      setReplyTo(null);
      const data = await API.support.getMessages(t.id).catch(() => null);
      if (Array.isArray(data)) setMessages(data);
      toast({ variant: 'success', title: 'Réponse envoyée' });
    } catch {
      toast({ variant: 'error', title: 'Erreur d\'envoi' });
    } finally {
      setSending(false);
    }
  };

  const sendFile = async (file) => {
    if (!file || !t) return;
    setSending(true);
    try {
      await API.support.sendAttachment(t.id, file);
      const data = await API.support.getMessages(t.id).catch(() => null);
      if (Array.isArray(data)) setMessages(data);
    } catch (ex) {
      toast({ variant: 'error', title: 'Envoi impossible', desc: ex.message });
    } finally {
      setSending(false);
    }
  };

  // Several photos at once → ONE message (an album). Re-reads the thread like sendFile does.
  const sendFiles = async (files) => {
    if (!files || files.length === 0 || !t) return;
    setSending(true);
    try {
      await API.support.sendAlbum(t.id, files);
      const data = await API.support.getMessages(t.id).catch(() => null);
      if (Array.isArray(data)) setMessages(data);
    } catch (ex) {
      toast({ variant: 'error', title: 'Envoi impossible', desc: ex.message });
    } finally {
      setSending(false);
    }
  };

  const resolve = async () => {
    if (!t) return;
    try {
      await API.support.resolveStaffTicket(t.id);
      setTickets(ts => ts.map(x => x.id === active ? { ...x, status: 'resolved' } : x));
      toast({ variant: 'success', title: 'Ticket résolu' });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    }
  };

  if (loading) return <div className="page-wide"><Card style={{ padding: 48 }}><EmptyState variant="empty" icon="lifebuoy" title="Chargement…" /></Card></div>;
  if (err) return <div className="page-wide"><Card style={{ padding: 48 }}><EmptyState variant="err" title="Impossible de charger les tickets" /></Card></div>;

  return (
    <div className="page-wide" style={{ paddingBottom: 0 }}>
      {deskChecked && !deskEnrolled && (
        // Sans clé de bureau enrôlée, le formulaire de déverrouillage ne s'affichait JAMAIS et
        // rien n'expliquait pourquoi les fils chiffrés restaient illisibles : l'état est visible.
        <div className="card card-pad" style={{ marginBottom: 16, display: 'flex', gap: 12, alignItems: 'center' }}>
          <div className="sc-ico rose" style={{ width: 36, height: 36, borderRadius: 10, flex: 'none' }}>
            <Icon name="shield" size={18} />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 800, fontSize: 14 }}>Clé du bureau support non enrôlée</div>
            <div className="muted" style={{ fontSize: 12.5 }}>
              Aucune clé de bureau n'est enregistrée : les conversations chiffrées de nouveaux clients ne peuvent pas
              être lues ici. Enrôlez la clé du bureau (voir CRYPTO_ARCHITECTURE.md), puis rechargez cette page.
            </div>
          </div>
        </div>
      )}
      {deskEnrolled && !deskUnlocked && (
        <SupportDeskUnlock onUnlocked={() => {
          setDeskUnlocked(true);
          // Re-fetch the open thread: what is on screen was decrypted (or not) before the
          // key existed, so it has to be read again now that it can be opened.
          if (active) API.support.getMessages(active).then(setMessages).catch(() => {});
        }} />
      )}
      <Card style={{ overflow: 'hidden', padding: 0 }}>
        <div className="chat-wrap">
          <div className="conv-list">
            {/* Statuts + comptes : sans onglets, un ticket résolu depuis des semaines reste mélangé
                aux nouveaux, et rien ne dit d'un coup d'œil où en est la file. Calculé côté client
                (pas de nouvel endpoint) : `getStaffTickets` renvoie déjà `status` par ticket. */}
            <div className="chips" style={{ padding: '10px 12px 0', flexWrap: 'wrap', gap: 6 }}>
              {[
                ['all', 'Tous'], ['open', 'Ouverts'], ['in_progress', 'En cours'], ['resolved', 'Résolus'],
              ].map(([key, label]) => (
                <Chip key={key} active={statusFilter === key} count={key === 'all' ? tickets.length : tickets.filter(tk => tk.status === key).length}
                  onClick={() => setStatusFilter(key)}>{label}</Chip>
              ))}
            </div>
            <div className="conv-search">
              <div className="header-search" style={{ display: 'flex', width: '100%', background: 'var(--slate-100)', border: '1px solid transparent' }}>
                <Icon name="search" size={16} /><input placeholder="Rechercher un ticket…" value={search} onChange={e => setSearch(e.target.value)} />
              </div>
            </div>
            {(() => {
              const visible = tickets.filter(tk => statusFilter === 'all' || tk.status === statusFilter);
              return visible.length === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Aucun ticket</div>;
            })()}
            {tickets.filter(tk => statusFilter === 'all' || tk.status === statusFilter).filter(tk => {
              if (!search) return true;
              const q = search.toLowerCase();
              return tSubject(tk).toLowerCase().includes(q) || tFrom(tk).toLowerCase().includes(q);
            }).slice().sort((a, b) => {
              // Open emergencies first — the server orders by recency, which can bury a live
              // SOS under ordinary tickets.
              const rank = (tk) => (tk.category === 'sos' && tk.status !== 'resolved' && tk.status !== 'closed' ? 0 : 1);
              return rank(a) - rank(b);
            }).map(tk => {
              const p = PRIO[tk.priority] || PRIO.low;
              return (
                <div key={tk.id} className={`conv ${active === tk.id ? 'active' : ''}`} onClick={() => setActive(tk.id)} style={{ alignItems: 'flex-start' }}>
                  <div className={`sc-ico ${p.cls}`} style={{ width: 38, height: 38, borderRadius: 10, flex: 'none' }}>
                    <Icon name="lifebuoy" size={18} />
                  </div>
                  <div className="conv-meta">
                    <div className="conv-top"><span className="n" style={{ fontSize: 13.5 }}>{tFrom(tk)}</span><span className="t">{tTime(tk)}</span></div>
                    <div style={{ fontWeight: 600, fontSize: 13.5, margin: '3px 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{tSubject(tk)}</div>
                    <div className="row" style={{ gap: 6 }}>
                      <Badge cls={p.cls} dot={false}>{p.label}</Badge>
                      <Badge cls={(TST[tk.status] || TST.open).cls} dot={false}>{(TST[tk.status] || TST.open).label}</Badge>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>

          <div className="thread">
            {t ? <>
              <div className="thread-head">
                <div className="th-meta" style={{ flex: 1 }}>
                  <div className="n">{tSubject(t)}</div>
                  <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>{tFrom(t)} · {t.category || t.role || ''}</div>
                </div>
                {/* The only way into a booking from the desk — and the only bookings an agent
                    can reach at all. See SupportBookingPanel. */}
                {t.bookingId && (
                  <Button variant="secondary" icon="calendar" onClick={() => setBookingPanel(t.bookingId)}>
                    Voir la réservation
                  </Button>
                )}
                <Badge cls={(PRIO[t.priority] || PRIO.low).cls}>{(PRIO[t.priority] || PRIO.low).label}</Badge>
                <Badge cls={(TST[t.status] || TST.open).cls}>{(TST[t.status] || TST.open).label}</Badge>
              </div>
              <div className="thread-body">
                <div className="bubble-row them">
                  <div className="row" style={{ gap: 8, marginBottom: 6 }}>
                    <Avatar name={tFrom(t)} size={28} />
                    <span style={{ fontWeight: 700, fontSize: 13 }}>{tFrom(t)}</span>
                  </div>
                  <div className="bubble" style={{ maxWidth: '100%' }}>{t.message || tSubject(t)}</div>
                  <div className="bubble-time">{tTime(t)}</div>
                </div>
                {/* groupAlbums merges the rows of a multi-photo message into one bubble. */}
                {groupAlbums(messages).map((m, i) => (
                  <ChatBubble
                    key={m.id || i}
                    msg={{
                      id: m.id,
                      from: ['admin', 'support', 'support_agent'].includes(m.senderRole) ? 'me' : 'them',
                      text: m.body,
                      // Sans ce drapeau, un message que la clé du bureau ne sait pas ouvrir arrive
                      // avec `body: ''` et ChatBubble ne rend RIEN pour un texte vide : la bulle
                      // disparaissait, sans même un cadenas. lessor3.jsx le fait déjà.
                      undecryptable: m.undecryptable || false,
                      location: messageLocation(m),
                      attachment: m.attachment || null,
                      attachments: m.attachments || null,
                      replyTo: m.replyTo || null,
                      reactions: m.reactions || [],
                      t: m.createdAt ? new Date(m.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '—',
                    }}
                    quotedSource={(() => {
                      if (!m.replyTo) return null;
                      // Le fil admin est en forme SERVEUR : on ramène le message cité à la
                      // forme attendue par l'aperçu (`text`, `attachment`, `location`).
                      const src = messages.find((x) => x.id === m.replyTo.id);
                      return src ? {
                        text: src.body,
                        attachment: src.attachment || null,
                        location: messageLocation(src),
                        undecryptable: src.undecryptable || false,
                      } : null;
                    })()}
                    peerLabel={tFrom(t)}
                    onReply={setReplyTo}
                    // `ChatBubble` appelle `onReact(m, emoji)` avec le MESSAGE ENTIER (voir
                    // ui.jsx) : câbler `react` directement lui faisait recevoir l'objet en guise
                    // de `messageId`, d'où une URL du type `/support/messages/[object Object]/
                    // reactions`, un 404, et le toast « Réaction impossible ». lessor2.jsx et
                    // lessor3.jsx faisaient déjà le bon geste.
                    onReact={(m, emoji) => react(m.id, emoji)}
                  />
                ))}
                {peerTyping ? <TypingBubble /> : null}
              </div>
              {replyTo && (
                <div className="reply-banner">
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="rb-author">{replyTo.from === 'me' ? 'Vous' : tFrom(t)}</div>
                    <div className="rb-text">{replyTo.text || (replyTo.attachment ? '📎 Pièce jointe' : '📍 Position')}</div>
                  </div>
                  <button className="rb-close" onClick={() => setReplyTo(null)} title="Annuler la réponse">
                    <Icon name="x" size={15} />
                  </button>
                </div>
              )}
              <div className="thread-input">
                <label className="btn btn-secondary btn-icon sm" style={{ cursor: 'pointer', margin: 0 }} title="Joindre une image ou un PDF">
                  <Icon name="image" size={18} />
                  <input type="file" multiple accept="image/jpeg,image/png,image/webp,application/pdf" style={{ display: 'none' }}
                    onChange={(e) => {
                  // One file keeps the single-attachment path (so a lone photo still has a
                  // NULL group and renders exactly as before); several become an album.
                  const fs = Array.from(e.target.files || []);
                  e.target.value = '';
                  if (fs.length === 1) sendFile(fs[0]);
                  else if (fs.length > 1) sendFiles(fs);
                }} />
                </label>
                <div className="ti-box">
                  <Textarea placeholder="Répondre au ticket…" value={reply}
                    onChange={(e) => { setReply(e.target.value); typingRef.current.update(active, !!e.target.value.trim()); }}
                    style={{ minHeight: 44, maxHeight: 110 }} />
                </div>
                <Button variant="secondary" onClick={resolve} icon="check">Résoudre</Button>
                <Button variant="primary" loading={sending} onClick={sendMsg} style={{ height: 44, width: 44, padding: 0 }}>
                  <Icon name="send" size={18} />
                </Button>
              </div>
            </> : <EmptyState variant="empty" icon="lifebuoy" title="Sélectionnez un ticket" />}
          </div>
        </div>
      </Card>
      {bookingPanel && <SupportBookingPanel bookingId={bookingPanel} onClose={() => setBookingPanel(null)} />}
    </div>
  );
}

// ============================================================
// ADMIN SETTINGS
// ============================================================
/**
 * One-time enrolment of the support-desk key pair.
 *
 * Generated IN THIS BROWSER. The public half goes to the server so every new conversation
 * can seal a copy of its key for the desk; the private half is sealed under a passphrase
 * the server never receives, so a database dump does not grant read access to customer
 * conversations.
 *
 * Deliberately not rotatable from here: replacing the key would orphan the desk's sealed
 * copy in every existing conversation, silently making the whole support history
 * unreadable. A rotation needs a migration that re-seals, not a second button.
 */
function SupportDeskSettings() {
  const toast = useToast();
  const [enrolled, setEnrolled] = useA2(null); // null = still checking
  const [pass, setPass] = useA2('');
  const [confirm, setConfirm] = useA2('');
  const [busy, setBusy] = useA2(false);

  useAE2(() => {
    let cancelled = false;
    API.keys.supportDeskPublic()
      .then(() => { if (!cancelled) setEnrolled(true); })
      .catch(() => { if (!cancelled) setEnrolled(false); });
    return () => { cancelled = true; };
  }, []);

  const enrol = async () => {
    if (pass.length < 12) { toast({ variant: 'error', title: 'Passphrase trop courte (12 caractères minimum)' }); return; }
    if (pass !== confirm) { toast({ variant: 'error', title: 'Les deux saisies diffèrent' }); return; }
    setBusy(true);
    try {
      await E2E.enrolSupportDesk(pass);
      setEnrolled(true); setPass(''); setConfirm('');
      toast({ variant: 'success', title: 'Bureau support activé' });
    } catch (ex) {
      toast({ variant: 'error', title: 'Activation impossible', desc: ex.message });
    } finally { setBusy(false); }
  };

  if (enrolled === null) return <Sk w="100%" h={120} r={12} />;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div className="flabel">Clé de chiffrement du bureau support</div>
      <p className="muted" style={{ fontSize: 13, lineHeight: 1.65, margin: 0 }}>
        Les conversations entre clients et loueurs sont chiffrées de bout en bout : nos serveurs
        ne peuvent pas les lire. Pour que l'équipe support puisse traiter un litige ou une demande
        d'urgence, chaque conversation scelle également une copie de sa clé pour le bureau support.
        Cette copie n'est ouvrable qu'avec la passphrase ci-dessous, <strong>que le serveur ne
        reçoit jamais</strong>. Chaque lecture d'un fil par un agent est journalisée.
      </p>

      {enrolled ? (
        <div className="req" style={{ padding: 14 }}>
          <div className="sc-ico green" style={{ width: 34, height: 34, borderRadius: 9 }}>
            <Icon name="checkCircle" size={16} />
          </div>
          <div className="req-body">
            <div style={{ fontWeight: 700, fontSize: 13.5 }}>Bureau support activé</div>
            <div className="muted" style={{ fontSize: 12.5 }}>
              Les agents saisissent la passphrase une fois par session depuis l'écran Support.
              La clé ne peut pas être remplacée ici : cela rendrait tout l'historique illisible.
            </div>
          </div>
        </div>
      ) : (
        <>
          <div style={{ padding: 12, borderRadius: 10, border: '1px solid var(--amber-600, #d97706)', background: 'var(--amber-bg, #fffbeb)', fontSize: 12.5, lineHeight: 1.6 }}>
            <strong>À conserver hors ligne.</strong> Cette passphrase n'est stockée nulle part.
            Si elle est perdue, aucun agent ne pourra plus lire les conversations chiffrées, et
            il n'existe aucun moyen de la retrouver.
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <Field label="Passphrase du bureau support">
              <Input type="password" value={pass} onChange={e => setPass(e.target.value)} autoComplete="new-password" />
            </Field>
            <Field label="Confirmation">
              <Input type="password" value={confirm} onChange={e => setConfirm(e.target.value)} autoComplete="new-password" />
            </Field>
          </div>
          <div>
            <Button variant="primary" icon="shield" loading={busy} onClick={enrol}>Activer le bureau support</Button>
          </div>
        </>
      )}
    </div>
  );
}

function AdminSettings() {
  const toast = useToast();
  const [tab, setTab] = useA2('general');
  const [commission, setCommission] = useA2(10);
  const [savingSettings, setSavingSettings] = useA2(false);
  // Tarifs d'abonnement loueur. Semés côté serveur à des ordres de grandeur, PAS des prix :
  // tant que `pricesConfirmedAt` est null, aucune facture ni relance ne peut partir.
  const [prices, setPrices] = useA2({ monthly: '', quarterly: '', yearly: '' });
  const [pricesConfirmedAt, setPricesConfirmedAt] = useA2(null);
  const [remindersEnabled, setRemindersEnabled] = useA2(false);
  const Toggle = ({ on, onChange }) => (
    <button onClick={() => onChange(!on)} style={{ width: 46, height: 26, borderRadius: 99, border: 'none', background: on ? 'var(--brand-700)' : 'var(--slate-300)', position: 'relative', transition: '.16s', flex: 'none' }}>
      <span style={{ position: 'absolute', top: 3, left: on ? 23 : 3, width: 20, height: 20, borderRadius: 99, background: '#fff', transition: '.16s', boxShadow: 'var(--sh-sm)' }} />
    </button>
  );
  const [toggles, setToggles] = useA2({ autoApprove: false, emailNotif: true, smsNotif: true, maintenance: false });
  // Taux € affiché aux clients hors d'Algérie (l'app mobile le lit via GET /app-config).
  // 1 = lundi … 7 = dimanche, comme le serveur. L'index 0 reste vide volontairement : un
  // décalage ici ferait partir la campagne le mauvais jour, sans rien casser de visible.
  const WEEKDAYS = ['', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'];

  const [eurRate, setEurRate] = useA2('');
  const [eurRateAt, setEurRateAt] = useA2(null);
  // Promotion hebdomadaire : la seule campagne qui s'adresse à TOUS les clients. Pilotée
  // d'ici pour pouvoir l'arrêter un soir sans redéploiement.
  const [promo, setPromo] = useA2({ enabled: false, weekday: 2, hour: 10, max: 2000, lastSentAt: null });
  const [savingPromo, setSavingPromo] = useA2(false);
  const [savingRate, setSavingRate] = useA2(false);
  const set = (k, v) => setToggles(s => ({ ...s, [k]: v }));

  // Support team management (real backend, admin-only).
  const [team, setTeam] = useA2([]);
  const [teamLoading, setTeamLoading] = useA2(false);
  const [inviting, setInviting] = useA2(false);
  const [invite, setInvite] = useA2({ firstName: '', lastName: '', email: '', phone: '' });
  const [inviteSaving, setInviteSaving] = useA2(false);
  const [createdCreds, setCreatedCreds] = useA2(null); // {name, email, phone, password} shown after creation
  const nameOfM = (m) => `${m.firstName || ''} ${m.lastName || ''}`.trim() || (m.email || 'Membre');

  useAE2(() => {
    API.admin.getSettings()
      .then(s => {
        if (!s) return;
        if (s.commissionRateDefault != null) setCommission(Math.round(Number(s.commissionRateDefault) * 1000) / 10);
        setPrices({
          monthly: s.subscriptionPriceMonthly != null ? String(Math.round(Number(s.subscriptionPriceMonthly))) : '',
          quarterly: s.subscriptionPriceQuarterly != null ? String(Math.round(Number(s.subscriptionPriceQuarterly))) : '',
          yearly: s.subscriptionPriceYearly != null ? String(Math.round(Number(s.subscriptionPriceYearly))) : '',
        });
        setPricesConfirmedAt(s.subscriptionPricesConfirmedAt || null);
        setRemindersEnabled(!!s.billingRemindersEnabled);
        setEurRate(s.eurRateDzd != null ? String(Number(s.eurRateDzd)) : '');
        setEurRateAt(s.eurRateUpdatedAt || null);
        setPromo({
          enabled: !!s.weeklyPromoEnabled,
          weekday: s.weeklyPromoWeekday != null ? Number(s.weeklyPromoWeekday) : 2,
          hour: s.weeklyPromoHour != null ? Number(s.weeklyPromoHour) : 10,
          max: s.weeklyPromoMaxRecipients != null ? Number(s.weeklyPromoMaxRecipients) : 2000,
          lastSentAt: s.weeklyPromoLastSentAt || null,
        });
      })
      .catch(() => {});
  }, []);

  useAE2(() => {
    if (tab !== 'team') return;
    setTeamLoading(true);
    API.admin.getTeam().then(list => setTeam(Array.isArray(list) ? list : [])).catch(() => {}).finally(() => setTeamLoading(false));
  }, [tab]);

  const doInvite = async () => {
    if (!invite.firstName.trim() || !invite.email.trim() || !invite.phone.trim()) {
      toast({ variant: 'info', title: 'Champs requis', desc: 'Prénom, email et téléphone' });
      return;
    }
    setInviteSaving(true);
    try {
      const created = await API.admin.createTeamMember({
        firstName: invite.firstName.trim(),
        lastName: invite.lastName.trim(),
        email: invite.email.trim(),
        phone: invite.phone.trim(),
      });
      setTeam(t => [...t, created]);
      setCreatedCreds({
        name: nameOfM(created),
        email: created.email,
        phone: created.phone,
        password: created.tempPassword || 'Support@2026',
      });
      toast({ variant: 'success', title: 'Agent support créé', desc: `${nameOfM(created)} · mot de passe : ${created.tempPassword || 'Support@2026'}` });
      setInviting(false);
      setInvite({ firstName: '', lastName: '', email: '', phone: '' });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setInviteSaving(false); }
  };

  const doRemove = async (m) => {
    try {
      await API.admin.removeTeamMember(m.id);
      setTeam(t => t.filter(x => x.id !== m.id));
      toast({ variant: 'info', title: 'Membre désactivé', desc: nameOfM(m) });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    }
  };

  const saveSettings = async () => {
    setSavingSettings(true);
    try {
      // N'envoie JAMAIS les tarifs d'abonnement : le serveur considère la présence d'un champ
      // de prix comme une confirmation explicite. Enregistrer la commission ne doit pas valider
      // par ricochet des tarifs que personne n'a regardés — ils partiraient ensuite en facture.
      await API.admin.updateSettings({ commissionRateDefault: commission / 100 });
      toast({ variant: 'success', title: 'Paramètres enregistrés' });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setSavingSettings(false);
    }
  };

  // Champ vide = plus de conversion : l'app repasse aux dinars partout.
  const saveRate = async () => {
    const raw = String(eurRate).trim().replace(',', '.');
    const value = raw === '' ? null : Number(raw);
    if (value !== null && (!Number.isFinite(value) || value < 1)) {
      toast({ variant: 'error', title: 'Taux invalide', desc: 'Saisissez le nombre de dinars pour 1 €, par exemple 250.' });
      return;
    }
    setSavingRate(true);
    try {
      const s = await API.admin.updateSettings({ eurRateDzd: value });
      setEurRateAt(s?.eurRateUpdatedAt || new Date().toISOString());
      toast({ variant: 'success', title: value === null ? 'Conversion désactivée' : 'Taux enregistré',
        desc: value === null ? 'Les prix restent affichés en dinars.' : `1 € = ${value} DA — visible dans l'app sous 24 h au plus.` });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setSavingRate(false); }
  };

  const savePromo = async () => {
    setSavingPromo(true);
    try {
      const s = await API.admin.updateSettings({
        weeklyPromoEnabled: promo.enabled,
        weeklyPromoWeekday: Number(promo.weekday),
        weeklyPromoHour: Number(promo.hour),
        weeklyPromoMaxRecipients: Number(promo.max),
      });
      setPromo(p => ({ ...p, lastSentAt: s?.weeklyPromoLastSentAt ?? p.lastSentAt }));
      toast({
        variant: 'success',
        title: promo.enabled ? 'Campagne activée' : 'Campagne désactivée',
        desc: promo.enabled
          ? `Envoi chaque ${WEEKDAYS[Number(promo.weekday)] || ''} à ${String(promo.hour).padStart(2, '0')} h (heure d'Alger).`
          : 'Plus aucun envoi hebdomadaire ne partira.',
      });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setSavingPromo(false); }
  };

  // Action dédiée : c'est elle, et elle seule, qui confirme les tarifs côté serveur.
  const savePrices = async () => {
    const nums = {
      subscriptionPriceMonthly: Number(prices.monthly),
      subscriptionPriceQuarterly: Number(prices.quarterly),
      subscriptionPriceYearly: Number(prices.yearly),
    };
    const bad = Object.entries(nums).find(([, v]) => !Number.isFinite(v) || v < 0);
    if (bad) {
      toast({ variant: 'error', title: 'Tarif invalide', desc: 'Saisissez un montant en dinars pour les trois formules.' });
      return;
    }
    setSavingSettings(true);
    try {
      const s = await API.admin.updateSettings({ ...nums, billingRemindersEnabled: remindersEnabled });
      setPricesConfirmedAt(s?.subscriptionPricesConfirmedAt || new Date().toISOString());
      toast({ variant: 'success', title: 'Tarifs confirmés', desc: 'La facturation des abonnements est débloquée.' });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally {
      setSavingSettings(false);
    }
  };

  return (
    <div className="page-wide" style={{ maxWidth: 920 }}>
      <div className="toolbar">
        <h2 style={{ margin: 0, fontSize: 22, fontWeight: 800, letterSpacing: '-.5px' }}>Paramètres de la plateforme</h2>
        <div className="spacer" />
        <Button variant="primary" icon="check" loading={savingSettings} onClick={saveSettings}>Enregistrer</Button>
      </div>

      <Card>
        <div className="card-head" style={{ padding: 0 }}>
          <div className="chips" style={{ padding: '14px 18px', gap: 6 }}>
            {[['general', 'Général'], ['commission', 'Commission'], ['facturation', 'Facturation'], ['notifications', 'Notifications'], ['team', 'Équipe'], ['chiffrement', 'Chiffrement'], ['verification', 'Vérification du numéro']].map(([k, l]) => (
              <button key={k} className={`chip ${tab === k ? 'on' : ''}`} onClick={() => setTab(k)}>{l}</button>
            ))}
          </div>
        </div>
        <div className="card-pad">
          {tab === 'general' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
              <Field label="Nom de la plateforme"><Input defaultValue="Uleez" /></Field>
              <Field label="Email de support"><Input defaultValue="support@kerya.dz" lead="mail" /></Field>
            </div>
            <Field label="Devise"><Select><option>DZD — Dinar algérien (DA)</option></Select></Field>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 14, alignItems: 'end' }}>
              <Field label="Taux affiché aux clients hors d'Algérie (DA pour 1 €)"
                hint={eurRateAt
                  ? `Mis à jour le ${new Date(eurRateAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}. Affichage seulement : le paiement reste en dinars.`
                  : "Vide = pas de conversion. Affichage seulement : le paiement reste en dinars."}>
                <Input type="number" min="1" step="0.01" placeholder="ex. 250" value={eurRate}
                  onChange={e => setEurRate(e.target.value)} />
              </Field>
              <Button variant="secondary" icon="check" loading={savingRate} onClick={saveRate}>Enregistrer le taux</Button>
            </div>
            <div className="req" style={{ padding: 16 }}>
              <div className="req-body"><div style={{ fontWeight: 700 }}>Mode maintenance</div><div className="muted" style={{ fontSize: 13 }}>Désactive temporairement l'accès à la plateforme</div></div>
              <Toggle on={toggles.maintenance} onChange={(v) => set('maintenance', v)} />
            </div>

            {/* Promotion hebdomadaire. Volontairement DÉSACTIVÉE par défaut : un envoi à
                l'ensemble des clients ne doit pas démarrer tout seul après un déploiement. */}
            <div className="req" style={{ padding: 16 }}>
              <div className="req-body">
                <div style={{ fontWeight: 700 }}>Promotion hebdomadaire</div>
                <div className="muted" style={{ fontSize: 13 }}>
                  Une notification par semaine à tous les clients, avec la meilleure promotion
                  de leur wilaya (ou la meilleure du pays, à défaut).
                  {promo.lastSentAt
                    ? ` Dernier envoi : ${new Date(promo.lastSentAt).toLocaleString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}.`
                    : ' Jamais envoyée.'}
                </div>
              </div>
              <Toggle on={promo.enabled} onChange={(v) => setPromo(p => ({ ...p, enabled: v }))} />
            </div>
            {promo.enabled && (
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr auto', gap: 14, alignItems: 'end' }}>
                <Field label="Jour">
                  <Select value={promo.weekday} onChange={e => setPromo(p => ({ ...p, weekday: e.target.value }))}>
                    {[1, 2, 3, 4, 5, 6, 7].map(d => <option key={d} value={d}>{WEEKDAYS[d]}</option>)}
                  </Select>
                </Field>
                <Field label="Heure (Alger)">
                  <Select value={promo.hour} onChange={e => setPromo(p => ({ ...p, hour: e.target.value }))}>
                    {Array.from({ length: 24 }, (_, h) => <option key={h} value={h}>{String(h).padStart(2, '0')} h</option>)}
                  </Select>
                </Field>
                <Field label="Destinataires max." hint="Garde-fou, pas un objectif.">
                  <Input type="number" min="1" value={promo.max}
                    onChange={e => setPromo(p => ({ ...p, max: e.target.value }))} />
                </Field>
                <Button variant="secondary" icon="check" loading={savingPromo} onClick={savePromo}>Enregistrer</Button>
              </div>
            )}
          </div>}
          {tab === 'commission' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <Field label={`Taux de commission : ${commission}%`} hint="Pourcentage prélevé sur chaque location">
              <input type="range" min="0" max="25" value={commission} onChange={(e) => setCommission(+e.target.value)} style={{ width: '100%', accentColor: 'var(--brand-700)' }} />
            </Field>
            <div className="req" style={{ padding: 16 }}>
              <div className="req-body"><div style={{ fontWeight: 700 }}>Approbation automatique des véhicules</div><div className="muted" style={{ fontSize: 13 }}>Les loueurs vérifiés publient sans validation manuelle</div></div>
              <Toggle on={toggles.autoApprove} onChange={(v) => set('autoApprove', v)} />
            </div>
            <Field label="Délai de versement aux loueurs"><Select><option>Hebdomadaire</option><option>Bi-mensuel</option><option>Mensuel</option></Select></Field>
          </div>}
          {/* Tarifs d'abonnement loueur. Écran volontairement distinct du bouton « Enregistrer »
              global : côté serveur, la seule présence d'un champ de prix vaut confirmation, et
              débloque l'envoi des factures. Ce geste doit donc être délibéré. */}
          {tab === 'facturation' && <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            {!pricesConfirmedAt ? (
              <div className="req" style={{ padding: 14, border: '1px solid var(--amber-300, #fcd34d)', background: 'var(--amber-50, #fffbeb)' }}>
                <Icon name="alert" size={18} style={{ color: 'var(--amber-700)', flex: 'none' }} />
                <div className="req-body">
                  <div style={{ fontWeight: 700 }}>Tarifs non confirmés</div>
                  <div className="muted" style={{ fontSize: 13, lineHeight: 1.5 }}>
                    Les montants ci-dessous sont des valeurs indicatives, pas vos prix. Tant qu'ils
                    ne sont pas confirmés, <strong>aucune facture d'abonnement ni relance ne peut
                    être envoyée</strong> à un loueur.
                  </div>
                </div>
              </div>
            ) : (
              <div className="req" style={{ padding: 14, border: '1px solid var(--brand-600)', background: 'var(--brand-50)' }}>
                <Icon name="checkCircle" size={18} style={{ color: 'var(--brand-700)', flex: 'none' }} />
                <div className="req-body">
                  <div style={{ fontWeight: 700 }}>Tarifs confirmés</div>
                  <div className="muted" style={{ fontSize: 13 }}>
                    Le {new Date(pricesConfirmedAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}.
                    La facturation des abonnements est active.
                  </div>
                </div>
              </div>
            )}

            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
              <Field label="Mensuel (DA)" hint="Prélevé chaque mois">
                <Input type="number" min="0" value={prices.monthly}
                  onChange={e => setPrices(p => ({ ...p, monthly: e.target.value }))} />
              </Field>
              <Field label="Trimestriel (DA)" hint="Période de 3 mois">
                <Input type="number" min="0" value={prices.quarterly}
                  onChange={e => setPrices(p => ({ ...p, quarterly: e.target.value }))} />
              </Field>
              <Field label="Annuel (DA)" hint="Période de 12 mois">
                <Input type="number" min="0" value={prices.yearly}
                  onChange={e => setPrices(p => ({ ...p, yearly: e.target.value }))} />
              </Field>
            </div>

            <div className="req" style={{ padding: 16 }}>
              <div className="req-body">
                <div style={{ fontWeight: 700 }}>Relances automatiques par e-mail</div>
                <div className="muted" style={{ fontSize: 13, lineHeight: 1.5 }}>
                  Facture de commission à l'émission puis J+3 / J+7 / J+14 après échéance ;
                  renouvellement d'abonnement à J-15 / J-3 puis J+1. Laissez désactivé tant que
                  vous n'avez pas relu un e-mail de test depuis l'écran Facturation.
                </div>
              </div>
              <Toggle on={remindersEnabled} onChange={setRemindersEnabled} />
            </div>

            <div className="between">
              <span className="muted" style={{ fontSize: 12.5 }}>
                Enregistrer ici confirme les tarifs et débloque la facturation.
              </span>
              <Button variant="primary" icon="check" loading={savingSettings} onClick={savePrices}>
                {pricesConfirmedAt ? 'Mettre à jour les tarifs' : 'Confirmer les tarifs'}
              </Button>
            </div>
          </div>}
          {tab === 'verification' && <PhonePolicyCard />}
          {tab === 'notifications' && <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div className="req" style={{ padding: 16 }}><div className="req-body"><div style={{ fontWeight: 700 }}>Notifications par email</div><div className="muted" style={{ fontSize: 13 }}>Nouvelles inscriptions, tickets, alertes</div></div><Toggle on={toggles.emailNotif} onChange={(v) => set('emailNotif', v)} /></div>
            <div className="req" style={{ padding: 16 }}><div className="req-body"><div style={{ fontWeight: 700 }}>Notifications SMS</div><div className="muted" style={{ fontSize: 13 }}>Alertes urgentes par SMS</div></div><Toggle on={toggles.smsNotif} onChange={(v) => set('smsNotif', v)} /></div>
          </div>}
          {tab === 'chiffrement' && <SupportDeskSettings />}
          {tab === 'team' && <div>
            <div className="between" style={{ marginBottom: 14 }}>
              <span className="flabel">Membres de l'équipe</span>
              <Button size="sm" variant="secondary" icon="plus" onClick={() => setInviting(true)}>Inviter un agent support</Button>
            </div>
            {createdCreds && (
              <div style={{ marginBottom: 14, padding: 14, borderRadius: 12, border: '1px solid var(--brand-600)', background: 'var(--brand-50)' }}>
                <div className="between" style={{ marginBottom: 6 }}>
                  <span style={{ fontWeight: 700, color: 'var(--ink)' }}>Identifiants de {createdCreds.name}</span>
                  <button className="btn btn-ghost btn-icon sm" onClick={() => setCreatedCreds(null)} title="Fermer"><Icon name="x" size={15} /></button>
                </div>
                <div className="muted" style={{ fontSize: 13, marginBottom: 8 }}>Communiquez ces accès à l'agent. Il pourra se connecter à l'espace support.</div>
                <div className="row" style={{ gap: 8, marginBottom: 4 }}><span className="muted" style={{ fontSize: 12.5, minWidth: 90 }}>Identifiant</span><b style={{ fontSize: 13.5 }}>{createdCreds.email || createdCreds.phone}</b></div>
                <div className="row" style={{ gap: 8 }}><span className="muted" style={{ fontSize: 12.5, minWidth: 90 }}>Mot de passe</span><code style={{ fontSize: 13.5, fontWeight: 700, background: 'var(--surface)', padding: '2px 8px', borderRadius: 6, border: '1px solid var(--line)' }}>{createdCreds.password}</code></div>
              </div>
            )}
            {teamLoading && <Sk w="100%" h={60} r={8} />}
            {!teamLoading && team.length === 0 && <div className="muted" style={{ fontSize: 13 }}>Aucun membre.</div>}
            {!teamLoading && team.map(m => {
              const isAdmin = m.role === 'admin';
              const active = m.isActive !== false;
              return (
                <div key={m.id} className="req" style={{ padding: 13, marginBottom: 9, opacity: active ? 1 : 0.55 }}>
                  <Avatar name={nameOfM(m)} size={40} />
                  <div className="req-body">
                    <div style={{ fontWeight: 700 }}>{nameOfM(m)}</div>
                    <div className="muted" style={{ fontSize: 13 }}>{m.email || '—'}{!active ? ' • désactivé' : ''}</div>
                  </div>
                  <Badge cls={isAdmin ? 'violet' : 'green'}>{isAdmin ? 'Administrateur' : 'Support'}</Badge>
                  {!isAdmin && active && (
                    <Button size="sm" variant="danger" icon="x" onClick={() => doRemove(m)} style={{ marginLeft: 8 }}>Désactiver</Button>
                  )}
                </div>
              );
            })}
          </div>}
        </div>
      </Card>

      {inviting && (
        <Modal title="Inviter un agent support" desc="Accès limité aux conversations de support" icon={{ name: 'user', color: 'emerald' }} onClose={() => setInviting(false)}
          footer={<>
            <Button variant="primary" icon="check" loading={inviteSaving} onClick={doInvite}>Créer l'agent</Button>
            <Button variant="secondary" onClick={() => setInviting(false)}>Annuler</Button>
          </>}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <Field label="Prénom"><Input value={invite.firstName} onChange={e => setInvite(v => ({ ...v, firstName: e.target.value }))} /></Field>
            <Field label="Nom"><Input value={invite.lastName} onChange={e => setInvite(v => ({ ...v, lastName: e.target.value }))} /></Field>
            <Field label="Email"><Input type="email" value={invite.email} onChange={e => setInvite(v => ({ ...v, email: e.target.value }))} /></Field>
            <Field label="Téléphone"><Input value={invite.phone} onChange={e => setInvite(v => ({ ...v, phone: e.target.value }))} placeholder="+213…" /></Field>
          </div>
          <div className="muted" style={{ fontSize: 12, marginTop: 10 }}>Un mot de passe temporaire lui sera envoyé par email. Il n'aura accès qu'aux conversations de support.</div>
        </Modal>
      )}
    </div>
  );
}

// ============================================================
// ADMIN — CLIENTS (mobile app users)
// ============================================================
const CUSTOMER_TABS = [
  { id: 'all',         label: 'Tous' },
  { id: 'kyc_pending', label: 'À vérifier' },
  // File de travail du cran « review » de la politique de vérification des numéros.
  { id: 'phone',       label: 'Numéros à vérifier' },
  { id: 'active',      label: 'Actifs' },
  { id: 'banned',      label: 'Bannis' },
  // Comptes fermés par leur titulaire (« Supprimer mon compte ») — en attente de purge ou purgés.
  { id: 'deleted',     label: 'Supprimés' },
];
const CUSTOMER_STATUS = {
  closing: { label: 'Suppression demandée', cls: 'amber' },
  deleted: { label: 'Supprimé', cls: 'gray' },
  active: { label: 'Actif',  cls: 'green' },
  banned: { label: 'Banni',  cls: 'rose' },
};

function AdminCustomers({ params }) {
  const toast = useToast();
  const [rows, setRows] = useA2([]);
  const [loading, setLoading] = useA2(true);
  const [err, setErr] = useA2(false);
  // The dashboard's "Clients à approuver" card lands here on the review queue directly.
  const [tab, setTab] = useA2(
    CUSTOMER_TABS.some(t => t.id === params?.tab) ? params.tab : 'all');
  const [search, setSearch] = useA2('');
  const [viewing, setViewing] = useA2(null);          // customer summary in the detail modal
  const [detail, setDetail] = useA2(null);            // {customer, bookings, documents}
  const [detailLoading, setDetailLoading] = useA2(false);
  const [banning, setBanning] = useA2(null);          // customer pending ban confirmation
  const [banReason, setBanReason] = useA2('');
  const [actionLoading, setActionLoading] = useA2(null);
  const [kyc, setKyc] = useA2({ dob: '', lic: '', licExp: '', idnum: '', reason: '' }); // KYC review form
  const [kycLoading, setKycLoading] = useA2(false);

  // Prefill the KYC form (self-declared DOB) once the customer detail loads.
  useAE2(() => {
    if (detail?.customer) {
      const dob = detail.customer.dateOfBirth ? String(detail.customer.dateOfBirth).split('T')[0] : '';
      setKyc({ dob, lic: '', licExp: '', idnum: '', reason: '' });
    }
  }, [detail]);

  const load = () => {
    setLoading(true); setErr(false);
    const status = (tab === 'all' || tab === 'phone') ? undefined : tab;
    API.admin.getCustomers({ status, phoneVerified: tab === 'phone' ? 'false' : undefined, limit: 50 })
      .then(res => {
        setRows(Array.isArray(res) ? res : (res.items || []));
        setLoading(false);
      })
      .catch(() => { setErr(true); setLoading(false); });
  };
  useAE2(() => { load(); }, [tab]);

  // Un compte fermé par son titulaire n'est PAS « banni » : il n'a rien à débannir (le réactiver
  // ferait revivre un compte dont la suppression a été demandée — le serveur le refuse aussi).
  const statusOf = (c) => (c.deletedAt ? 'deleted'
    : c.deletionRequestedAt ? 'closing'
    : c.isActive === false ? 'banned' : 'active');
  // Date de purge prévue (J+30) d'un compte en cours de suppression.
  const purgeDateOf = (c) => c.deletionRequestedAt
    ? new Date(new Date(c.deletionRequestedAt).getTime() + 30 * 86400000).toLocaleDateString('fr-FR')
    : null;
  const nameOf = (c) => `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Client';

  const filtered = rows.filter(c => {
    if (!search) return true;
    const q = search.toLowerCase();
    return nameOf(c).toLowerCase().includes(q)
      || (c.email || '').toLowerCase().includes(q)
      || (c.phone || '').toLowerCase().includes(q);
  });

  const openDetail = (c) => {
    setViewing(c); setDetail(null); setDetailLoading(true);
    API.admin.getCustomer(c.id).then(setDetail).catch(() => {}).finally(() => setDetailLoading(false));
  };

  const doBan = async () => {
    const c = banning; if (!c) return;
    setActionLoading(c.id);
    try {
      await API.admin.banCustomer(c.id, banReason.trim() || undefined);
      setRows(rs => rs.map(x => x.id === c.id ? { ...x, isActive: false, bannedReason: banReason.trim() || null } : x));
      toast({ variant: 'info', title: 'Client banni', desc: nameOf(c) });
      setBanning(null); setBanReason(''); setViewing(null);
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setActionLoading(null); }
  };

  // Atteste qu'un numéro est authentique. Ne modifie JAMAIS le numéro : le téléphone est le sel de la
  // connexion du client, le réécrire casserait son mot de passe sans le moindre message d'erreur.
  const doMarkPhone = async (c) => {
    setActionLoading(c.id);
    try {
      const updated = await API.admin.markPhoneVerified(c.id);
      // Dans l'onglet « Numéros à vérifier », le client quitte la file ; ailleurs, on met la ligne à jour.
      setRows(rs => tab === 'phone' ? rs.filter(x => x.id !== c.id) : rs.map(x => x.id === c.id ? { ...x, phoneVerifiedAt: updated.phoneVerifiedAt } : x));
      setViewing(v => (v && v.id === c.id ? { ...v, phoneVerifiedAt: updated.phoneVerifiedAt } : v));
      toast({ variant: 'success', title: 'Numéro marqué comme vérifié', desc: nameOf(c) });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setActionLoading(null); }
  };

  const doUnban = async (c) => {
    setActionLoading(c.id);
    try {
      await API.admin.unbanCustomer(c.id);
      setRows(rs => rs.map(x => x.id === c.id ? { ...x, isActive: true, bannedReason: null } : x));
      toast({ variant: 'success', title: 'Client réactivé', desc: nameOf(c) });
      setViewing(null);
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setActionLoading(null); }
  };

  const applyKycResult = (updated) => {
    setDetail(d => d ? { ...d, customer: { ...d.customer, ...updated } } : d);
    setRows(rs => rs.map(x => x.id === (viewing && viewing.id) ? { ...x, isVerified: updated.isVerified, kycStatus: updated.kycStatus } : x));
  };

  const doKycApprove = async () => {
    if (!viewing) return;
    setKycLoading(true);
    try {
      const updated = await API.admin.reviewKyc(viewing.id, {
        action: 'approve',
        dateOfBirth: kyc.dob || undefined,
        drivingLicenseNumber: kyc.lic.trim() || undefined,
        drivingLicenseExpiry: kyc.licExp || undefined,
        idCardNumber: kyc.idnum.trim() || undefined,
      });
      applyKycResult(updated);
      toast({ variant: 'success', title: 'Identité validée', desc: nameOf(viewing) });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setKycLoading(false); }
  };

  const doKycReject = async () => {
    if (!viewing) return;
    setKycLoading(true);
    try {
      const updated = await API.admin.reviewKyc(viewing.id, { action: 'reject', rejectionReason: kyc.reason.trim() || undefined });
      applyKycResult(updated);
      toast({ variant: 'info', title: 'Identité refusée', desc: nameOf(viewing) });
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setKycLoading(false); }
  };

  return (
    <div className="page-wide">
      <div className="toolbar" style={{ marginBottom: 14 }}>
        <div className="chips">
          {CUSTOMER_TABS.map(t => (
            <Chip key={t.id} active={tab === t.id} onClick={() => setTab(t.id)}>{t.label}</Chip>
          ))}
        </div>
        <div className="spacer" />
        <div className="header-search" style={{ display: 'flex', width: 260, background: 'var(--surface)', border: '1px solid var(--line)' }}>
          <Icon name="search" size={17} />
          <input placeholder="Nom, e-mail, téléphone…" value={search} onChange={e => setSearch(e.target.value)} />
        </div>
        <Button variant="secondary" icon="refresh" size="sm" onClick={load}>Actualiser</Button>
      </div>

      <Card>
        {loading && <div className="card-pad"><TableSkeleton cols={6} /></div>}
        {!loading && err && <EmptyState variant="err" title="Impossible de charger les clients" action={<Button variant="secondary" icon="refresh" onClick={load}>Réessayer</Button>} />}
        {!loading && !err && (
          <div className="tbl-wrap">
            <table className="tbl">
              <thead><tr>
                <th>Client</th><th>Téléphone</th><th>Inscription</th><th>Vérifié</th><th>Statut</th>
                <th style={{ textAlign: 'right' }}>Actions</th>
              </tr></thead>
              <tbody>
                {filtered.length === 0 && (
                  <tr><td colSpan={6} style={{ textAlign: 'center', padding: 32, color: 'var(--muted)' }}>Aucun client trouvé</td></tr>
                )}
                {filtered.map(c => (
                  <tr key={c.id} className="clickable" onClick={() => openDetail(c)}>
                    <td>
                      <div className="celluser">
                        <Avatar name={nameOf(c)} src={c.avatarUrl || undefined} size={36} />
                        <div className="cu-meta"><div className="n">{nameOf(c)}</div><div className="e">{c.email || '—'}</div></div>
                      </div>
                    </td>
                    <td className="nowrap">
                      {c.phone || '—'}
                      {!c.phoneVerifiedAt && <div className="muted" style={{ fontSize: 11 }}>non vérifié</div>}
                    </td>
                    <td className="nowrap">{c.createdAt ? new Date(c.createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) : '—'}</td>
                    <td>{c.isVerified ? <Icon name="check" size={15} style={{ color: 'var(--green-500)' }} /> : '—'}</td>
                    <td><StatusBadge map={CUSTOMER_STATUS} value={statusOf(c)} /></td>
                    <td onClick={(e) => e.stopPropagation()} style={{ textAlign: 'right' }}>
                      {!c.phoneVerifiedAt && (
                        <Button size="sm" variant="secondary" icon="check" loading={actionLoading === c.id} onClick={() => doMarkPhone(c)} style={{ marginRight: 6 }}>Marquer vérifié</Button>
                      )}
                      {statusOf(c) === 'active'
                        ? <Button size="sm" variant="danger" icon="ban" loading={actionLoading === c.id} onClick={() => { setBanning(c); setBanReason(''); }}>Bannir</Button>
                        : statusOf(c) === 'banned'
                          ? <Button size="sm" variant="success" icon="check" loading={actionLoading === c.id} onClick={() => doUnban(c)}>Débannir</Button>
                          : null}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </Card>

      {viewing && (
        <Modal wide title={nameOf(viewing)} desc={viewing.email || ''} icon={{ name: 'user', color: 'blue' }} onClose={() => setViewing(null)}
          footer={<>
            {statusOf(viewing) === 'active'
              ? <Button variant="danger" icon="ban" onClick={() => { setBanning(viewing); setBanReason(''); }}>Bannir le client</Button>
              : statusOf(viewing) === 'banned'
                ? <Button variant="success" icon="check" loading={actionLoading === viewing.id} onClick={() => doUnban(viewing)}>Débannir</Button>
                : null}
            <Button variant="secondary" onClick={() => setViewing(null)}>Fermer</Button>
          </>}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
            <div>
              <div className="kv"><span className="k">Email</span><span className="v">{viewing.email || '—'}</span></div>
              <div className="kv"><span className="k">Téléphone</span><span className="v">{viewing.phone || '—'}</span></div>
              <div className="kv"><span className="k">Numéro</span><span className="v">{viewing.phoneVerifiedAt
                ? <span className="row" style={{ gap: 6 }}><Icon name="check" size={14} style={{ color: 'var(--green-500)' }} />Vérifié le {new Date(viewing.phoneVerifiedAt).toLocaleDateString('fr-FR')}</span>
                : <Button size="sm" variant="secondary" icon="check" loading={actionLoading === viewing.id} onClick={() => doMarkPhone(viewing)}>Marquer comme vérifié</Button>}</span></div>
              <div className="kv"><span className="k">Inscription</span><span className="v">{viewing.createdAt ? new Date(viewing.createdAt).toLocaleDateString('fr-FR') : '—'}</span></div>
              <div className="kv"><span className="k">Statut</span><span className="v"><StatusBadge map={CUSTOMER_STATUS} value={statusOf(viewing)} /></span></div>
              {statusOf(viewing) === 'closing' && (
                <div className="kv"><span className="k">Purge prévue</span><span className="v">{purgeDateOf(viewing)}</span></div>
              )}
              {statusOf(viewing) === 'banned' && (detail?.customer?.bannedReason || viewing.bannedReason) && (
                <div className="kv"><span className="k">Motif</span><span className="v">{detail?.customer?.bannedReason || viewing.bannedReason}</span></div>
              )}
            </div>
            <div>
              <div className="flabel" style={{ marginBottom: 8 }}>Documents</div>
              {detailLoading && <Sk w="100%" h={120} r={8} />}
              {!detailLoading && (!detail || detail.documents.length === 0) && <div className="muted" style={{ fontSize: 13 }}>Aucun document.</div>}
              {!detailLoading && detail && detail.documents.map(d => {
                // Identity documents are encrypted at rest and are no longer served from
                // the public /uploads prefix — where anyone with the URL could read them.
                // The only way in is GET /documents/:id/content, which needs the bearer
                // token, so the preview goes through AuthedImg (fetch → blob → object URL)
                // rather than a plain <img src>.
                const isPdf = (d.mimeType || '') === 'application/pdf';
                const label = (API.DOCUMENT_TYPES.find(t => t.value === d.documentType) || {}).label || d.documentType;
                return (
                  <div key={d.id} style={{ marginBottom: 12 }}>
                    <div className="row" style={{ justifyContent: 'space-between', marginBottom: 6 }}>
                      <span className="row" style={{ gap: 8, fontSize: 13, fontWeight: 600 }}><Icon name="clipboard" size={15} className="muted" />{label}</span>
                      {/* A row with no file was flipped to 'rejected' by the v0.7.4 migration,
                          but nobody rejected it — show it as awaiting resubmission (amber)
                          instead of a red "Refusé" that reads like the customer failed KYC. */}
                      <StatusBadge map={KD.DOCUMENT_STATUS} value={d.hasContent ? d.verificationStatus : 'pending'} />
                    </div>
                    {/* The server now returns only the CURRENT document of each type, so this
                        no longer appears under an already-approved one — it means the customer
                        genuinely has not re-sent this document yet. Phrased and styled as a
                        pending state, because that is what it is: nothing went wrong. */}
                    {!d.hasContent
                      ? <div className="muted" style={{ fontSize: 12.5, padding: '12px 14px', borderRadius: 10, border: '1px dashed var(--line)' }}>
                          En attente : le client doit renvoyer ce document (les fichiers d'avant le
                          chiffrement ont été supprimés).
                        </div>
                      : isPdf
                        ? <div className="row" onClick={() => API.docs.download(d, `${d.documentType}.pdf`).catch(() => toast({ variant: 'error', title: 'Téléchargement impossible' }))}
                               style={{ gap: 10, padding: '16px 14px', color: 'var(--ink)', cursor: 'pointer', borderRadius: 10, border: '1px solid var(--line)', background: 'var(--slate-100)' }}>
                            <Icon name="clipboard" size={22} className="muted" />
                            <div><div style={{ fontWeight: 700, fontSize: 13 }}>Document PDF chiffré</div><div className="muted" style={{ fontSize: 12 }}>Cliquer pour télécharger</div></div>
                            <div className="spacer" style={{ flex: 1 }} /><Icon name="download" size={16} className="muted" />
                          </div>
                        : <div style={{ borderRadius: 10, overflow: 'hidden', border: '1px solid var(--line)', background: 'var(--slate-100)' }}>
                            <AuthedImg path={d} alt={label} fetcher={API.docs.objectUrl}
                                       style={{ width: '100%', maxHeight: 200, objectFit: 'cover', display: 'block' }} />
                          </div>}
                  </div>
                );
              })}
            </div>
          </div>
          <div className="flabel" style={{ margin: '18px 0 8px' }}>Vérification d'identité (KYC)</div>
          <div className="row" style={{ gap: 8, marginBottom: 10 }}>
            <StatusBadge map={KD.KYC_STATUS} value={(detail?.customer?.kycStatus) || 'not_submitted'} />
            {detail?.customer?.kycStatus === 'rejected' && detail?.customer?.kycRejectionReason && (
              <span className="muted" style={{ fontSize: 13 }}>Motif : {detail.customer.kycRejectionReason}</span>
            )}
            {detail?.customer?.drivingLicenseExpiry && (() => {
              const exp = new Date(detail.customer.drivingLicenseExpiry);
              const days = Math.ceil((exp - new Date()) / 86400000);
              const soon = days <= 30;
              return (
                <span className={`badge ${days < 0 ? 'rose' : soon ? 'amber' : 'slate'}`} style={{ fontSize: 12 }}>
                  Permis {days < 0 ? 'expiré le' : 'valide jusqu\'au'} {exp.toLocaleDateString('fr-FR')}
                  {soon && days >= 0 ? ` (${days} j)` : ''}
                </span>
              );
            })()}
          </div>
          {!detailLoading && detail?.customer?.kycStatus === 'pending' && (
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
              <Field label="Date de naissance (confirmer)"><Input type="date" value={kyc.dob} onChange={e => setKyc(k => ({ ...k, dob: e.target.value }))} /></Field>
              <Field label="N° de permis"><Input value={kyc.lic} onChange={e => setKyc(k => ({ ...k, lic: e.target.value }))} /></Field>
              <Field label="Expiration du permis"><Input type="date" value={kyc.licExp} onChange={e => setKyc(k => ({ ...k, licExp: e.target.value }))} /></Field>
              <Field label="N° CNI"><Input value={kyc.idnum} onChange={e => setKyc(k => ({ ...k, idnum: e.target.value }))} /></Field>
              <div style={{ gridColumn: '1 / -1' }}>
                <Field label="Motif de rejet (si refus)"><Input value={kyc.reason} onChange={e => setKyc(k => ({ ...k, reason: e.target.value }))} placeholder="Optionnel" /></Field>
              </div>
              <div style={{ gridColumn: '1 / -1', display: 'flex', gap: 8 }}>
                <Button variant="success" icon="check" loading={kycLoading} onClick={doKycApprove}>Valider l'identité</Button>
                <Button variant="danger" icon="x" loading={kycLoading} onClick={doKycReject}>Rejeter</Button>
              </div>
            </div>
          )}

          <div className="flabel" style={{ margin: '18px 0 8px' }}>Réservations ({detail?.bookings?.length || 0})</div>
          {detailLoading && <Sk w="100%" h={80} r={8} />}
          {!detailLoading && (!detail || detail.bookings.length === 0) && <div className="muted" style={{ fontSize: 13 }}>Aucune réservation.</div>}
          {!detailLoading && detail && detail.bookings.length > 0 && (
            <div className="tbl-wrap">
              <table className="tbl">
                <thead><tr><th>Réf.</th><th>Véhicule</th><th>Dates</th><th>Montant</th><th>Statut</th></tr></thead>
                <tbody>
                  {detail.bookings.map(b => (
                    <tr key={b.id}>
                      <td className="nowrap">{b.referenceCode || '—'}</td>
                      <td>{b.vehicleName || '—'}</td>
                      <td className="nowrap">{b.pickupAt ? new Date(b.pickupAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) : ''} → {b.returnAt ? new Date(b.returnAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) : ''}</td>
                      <td className="nowrap">{b.totalAmount ? `${Number(b.totalAmount).toLocaleString('fr-FR')} DA` : '—'}</td>
                      <td><StatusBadge map={KD.BOOKING_STATUS} value={API.mapBookingStatus(b.status)} /></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </Modal>
      )}

      {banning && (
        <Modal title="Bannir le client" desc={nameOf(banning)} icon={{ name: 'ban', color: 'rose' }} onClose={() => setBanning(null)}
          footer={<>
            <Button variant="secondary" onClick={() => setBanning(null)}>Annuler</Button>
            <Button variant="danger" icon="ban" loading={actionLoading === banning.id} onClick={doBan}>Confirmer le bannissement</Button>
          </>}>
          <p className="muted" style={{ fontSize: 13.5, marginBottom: 12 }}>
            Le client sera immédiatement déconnecté et ne pourra plus se connecter (application mobile incluse) tant qu'il reste banni.
          </p>
          <div className="flabel" style={{ marginBottom: 6 }}>Motif (optionnel)</div>
          <textarea className="input" rows={3} value={banReason} maxLength={300} onChange={e => setBanReason(e.target.value)} placeholder="Raison du bannissement…" style={{ width: '100%', resize: 'vertical' }} />
        </Modal>
      )}
    </div>
  );
}

// ============================================================
// ADMIN PROMO CODES (campaign codes applied at checkout)
// ============================================================
function AdminPromoCodes() {
  const toast = useToast();
  const [rows, setRows] = useA2([]);
  const [loading, setLoading] = useA2(true);
  const [err, setErr] = useA2(false);
  const [editing, setEditing] = useA2(null);   // null | 'new' | row (edit)
  const [saving, setSaving] = useA2(false);
  const [busy, setBusy] = useA2(null);
  const [deleting, setDeleting] = useA2(null);  // row pending delete confirmation
  const [detail, setDetail] = useA2(null);      // row whose redemptions are shown
  const [detailData, setDetailData] = useA2(null);
  const [detailLoading, setDetailLoading] = useA2(false);
  const [q, setQ] = useA2('');
  const [statusFilter, setStatusFilter] = useA2('all'); // all | active | scheduled | expired | inactive
  const empty = { code: '', discountType: 'percent', discountValue: '', validFrom: '', validUntil: '', maxUses: '', perUserLimit: '1', minSubtotal: '0' };
  const [form, setForm] = useA2(empty);
  const setF = (k, v) => setForm(s => ({ ...s, [k]: v }));

  const TYPE_LABEL = { percent: '%', fixed: 'DA', free_days: 'jour(s)' };
  const TYPE_BADGE = { percent: 'violet', fixed: 'blue', free_days: 'amber' };
  const TYPE_SHORT = { percent: 'Pourcentage', fixed: 'Montant fixe', free_days: 'Jours offerts' };
  const fmtVal = (r) => r.discountType === 'percent' ? `-${r.discountValue} %`
    : r.discountType === 'fixed' ? `-${KD.fmtDA(r.discountValue)}`
    : `${r.discountValue} jour(s) offert(s)`;
  const fmtDate = (d) => d ? new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) : null;
  const toDateInput = (d) => d ? new Date(d).toISOString().slice(0, 10) : '';
  const DAY = 86400000;

  // Statut réel du code : « actif » ne suffit pas, une campagne peut être programmée,
  // expirée ou épuisée alors que le drapeau active est toujours à true.
  const lifecycle = (r) => {
    if (!r.active) return { key: 'inactive', label: 'Désactivé', cls: 'gray' };
    const now = Date.now();
    if (r.validFrom && new Date(r.validFrom).getTime() > now) return { key: 'scheduled', label: 'Programmé', cls: 'blue' };
    if (r.validUntil && new Date(r.validUntil).getTime() < now) return { key: 'expired', label: 'Expiré', cls: 'rose' };
    if (r.maxUses && (r.usedCount || 0) >= r.maxUses) return { key: 'expired', label: 'Épuisé', cls: 'amber' };
    return { key: 'active', label: 'Actif', cls: 'green' };
  };

  const validityNote = (r) => {
    const now = Date.now();
    if (r.validFrom && new Date(r.validFrom).getTime() > now) {
      const days = Math.ceil((new Date(r.validFrom).getTime() - now) / DAY);
      return `Démarre dans ${days} j`;
    }
    if (!r.validUntil) return 'Sans date de fin';
    const left = Math.ceil((new Date(r.validUntil).getTime() - now) / DAY);
    if (left < 0) return 'Expiré';
    if (left === 0) return "Expire aujourd'hui";
    return `Expire dans ${left} j`;
  };

  const load = () => {
    setLoading(true); setErr(false);
    API.admin.getPromoCodes()
      .then(list => { setRows(Array.isArray(list) ? list : []); setLoading(false); })
      .catch(() => { setErr(true); setLoading(false); });
  };
  useAE2(() => { load(); }, []);

  const openCreate = () => { setForm(empty); setEditing('new'); };
  const openEdit = (r) => {
    setForm({
      code: r.code, discountType: r.discountType, discountValue: String(r.discountValue ?? ''),
      validFrom: toDateInput(r.validFrom), validUntil: toDateInput(r.validUntil),
      maxUses: r.maxUses == null ? '' : String(r.maxUses),
      perUserLimit: String(r.perUserLimit ?? '1'), minSubtotal: String(r.minSubtotal ?? '0'),
    });
    setEditing(r);
  };

  const openDetail = (r) => {
    setDetail(r); setDetailData(null); setDetailLoading(true);
    API.admin.getPromoRedemptions(r.id)
      .then(d => { setDetailData(d); setDetailLoading(false); })
      .catch(() => { setDetailData(null); setDetailLoading(false); });
  };

  const doSave = async () => {
    const code = form.code.trim().toUpperCase();
    const value = parseInt(form.discountValue, 10);
    if (!code || !(value > 0)) {
      toast({ variant: 'info', title: 'Champs requis', desc: 'Code et valeur (> 0) obligatoires' });
      return;
    }
    if (form.discountType === 'percent' && value > 100) {
      toast({ variant: 'info', title: 'Valeur invalide', desc: 'Un pourcentage ne peut pas dépasser 100 %' });
      return;
    }
    if (form.validFrom && form.validUntil && form.validFrom > form.validUntil) {
      toast({ variant: 'info', title: 'Dates incohérentes', desc: 'La date de fin précède la date de début' });
      return;
    }
    const body = {
      discountType: form.discountType,
      discountValue: value,
      validFrom: form.validFrom || undefined,
      validUntil: form.validUntil || undefined,
      maxUses: form.maxUses === '' ? undefined : parseInt(form.maxUses, 10),
      perUserLimit: form.perUserLimit === '' ? undefined : parseInt(form.perUserLimit, 10),
      minSubtotal: form.minSubtotal === '' ? undefined : parseInt(form.minSubtotal, 10),
    };
    setSaving(true);
    try {
      if (editing === 'new') {
        await API.admin.createPromoCode({ code, ...body });
        toast({ variant: 'success', title: 'Code promo créé', desc: code });
      } else {
        await API.admin.updatePromoCode(editing.id, body);
        toast({ variant: 'success', title: 'Code promo modifié', desc: code });
      }
      setEditing(null); setForm(empty); load();
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setSaving(false); }
  };

  const toggleActive = async (r) => {
    setBusy(r.id);
    try {
      await API.admin.updatePromoCode(r.id, { active: !r.active });
      setRows(rs => rs.map(x => x.id === r.id ? { ...x, active: !r.active } : x));
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setBusy(null); }
  };

  const doDelete = async () => {
    if (!deleting) return;
    setBusy(deleting.id);
    try {
      await API.admin.deletePromoCode(deleting.id);
      toast({ variant: 'success', title: 'Code désactivé', desc: deleting.code });
      setRows(rs => rs.map(x => x.id === deleting.id ? { ...x, active: false } : x));
      setDeleting(null);
    } catch (ex) {
      toast({ variant: 'error', title: 'Erreur', desc: ex.message });
    } finally { setBusy(null); }
  };

  const counts = rows.reduce((acc, r) => {
    const k = lifecycle(r).key;
    acc[k] = (acc[k] || 0) + 1;
    return acc;
  }, {});
  const stats = {
    total: rows.length,
    active: counts.active || 0,
    uses: rows.reduce((s, r) => s + (r.usedCount || 0), 0),
  };
  const filtered = rows.filter(r => {
    if (statusFilter !== 'all' && lifecycle(r).key !== statusFilter) return false;
    if (q.trim() && !r.code.toLowerCase().includes(q.trim().toLowerCase())) return false;
    return true;
  });

  // Aperçu live dans la modale : rend concret ce que le code retirera au client.
  const previewOn = 15000;
  const preview = (() => {
    const v = parseInt(form.discountValue, 10);
    if (!(v > 0)) return null;
    if (form.discountType === 'percent') return Math.min(previewOn, Math.round(previewOn * v / 100));
    if (form.discountType === 'fixed') return Math.min(previewOn, v);
    return null;
  })();

  return (
    <div className="page-wide">
      <div className="stat-grid" style={{ marginBottom: 18 }}>
        <StatCard icon="gift" color="green" value={stats.total} label="Codes au total"
          meta={`${stats.active} en cours de validité`} />
        <StatCard icon="users" color="violet" value={KD.fmtNum(stats.uses)} label="Utilisations cumulées"
          meta={stats.total ? `${(stats.uses / stats.total).toFixed(1)} par code en moyenne` : '—'} />
        <StatCard icon="clock" color="blue" value={counts.scheduled || 0} label="Campagnes programmées"
          meta={`${(counts.expired || 0)} expirée(s) ou épuisée(s)`} />
      </div>

      <div className="toolbar" style={{ marginBottom: 14 }}>
        <div className="chips">
          {[['all', 'Tous'], ['active', 'Actifs'], ['scheduled', 'Programmés'], ['expired', 'Expirés'], ['inactive', 'Désactivés']].map(t => (
            <Chip key={t[0]} count={t[0] === 'all' ? rows.length : (counts[t[0]] || 0)}
              active={statusFilter === t[0]} onClick={() => setStatusFilter(t[0])}>{t[1]}</Chip>
          ))}
        </div>
        <div className="spacer" />
        <div style={{ width: 260 }}>
          <Input lead="search" value={q} onChange={e => setQ(e.target.value)} placeholder="Rechercher un code…" />
        </div>
        <Button variant="secondary" icon="refresh" onClick={load}>Actualiser</Button>
        <Button variant="primary" icon="plus" onClick={openCreate}>Nouveau code</Button>
      </div>

      <Card>
        <CardHead title="Campagnes de réduction" icon={{ name: 'gift', color: 'green' }}
          sub="Les remises sont financées par Uleez et recalculées côté serveur au paiement"
          right={<span className="muted" style={{ fontSize: 12.5 }}>{filtered.length} code(s)</span>} />
        {loading && <div className="card-pad"><TableSkeleton cols={7} /></div>}
        {!loading && err && <EmptyState variant="err" title="Impossible de charger les codes promo"
          action={<Button variant="secondary" icon="refresh" onClick={load}>Réessayer</Button>} />}
        {!loading && !err && rows.length === 0 && (
          <EmptyState icon="gift" title="Aucun code promo"
            desc="Créez votre première campagne pour offrir une réduction au paiement."
            action={<Button variant="primary" icon="plus" onClick={openCreate}>Nouveau code</Button>} />
        )}
        {!loading && !err && rows.length > 0 && filtered.length === 0 && (
          <EmptyState icon="search" title="Aucun résultat" desc="Aucun code ne correspond à cette recherche." />
        )}
        {!loading && !err && filtered.length > 0 && (
          <div className="tbl-wrap">
            <table className="tbl">
              <thead>
                <tr>
                  <th>Code</th>
                  <th>Réduction</th>
                  <th>Validité</th>
                  <th style={{ minWidth: 150 }}>Utilisations</th>
                  <th className="cellnum">Par client</th>
                  <th className="cellnum">Sous-total min.</th>
                  <th>Statut</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {filtered.map(r => {
                  const st = lifecycle(r);
                  const used = r.usedCount || 0;
                  const pct = r.maxUses ? Math.min(100, Math.round((used / r.maxUses) * 100)) : null;
                  return (
                    <tr key={r.id}>
                      <td>
                        <div className="mono strong" style={{ fontSize: 13.5, letterSpacing: '.3px' }}>{r.code}</div>
                        <div className="muted" style={{ fontSize: 11.5, marginTop: 2 }}>{TYPE_SHORT[r.discountType] || r.discountType}</div>
                      </td>
                      <td>
                        <Badge cls={TYPE_BADGE[r.discountType] || 'gray'} dot={false}>{fmtVal(r)}</Badge>
                      </td>
                      <td className="nowrap">
                        <div style={{ fontSize: 13 }}>
                          {fmtDate(r.validFrom) || 'Immédiat'} → {fmtDate(r.validUntil) || '∞'}
                        </div>
                        <div className="muted" style={{ fontSize: 11.5, marginTop: 2 }}>{validityNote(r)}</div>
                      </td>
                      <td>
                        <div style={{ fontSize: 13, fontWeight: 600 }}>
                          {KD.fmtNum(used)}{r.maxUses ? ` / ${KD.fmtNum(r.maxUses)}` : ' · illimité'}
                        </div>
                        {pct !== null && (
                          <div style={{ height: 5, borderRadius: 99, background: 'var(--slate-100)', marginTop: 5, overflow: 'hidden' }}>
                            <div style={{ height: '100%', width: `${pct}%`, borderRadius: 99, background: pct >= 100 ? 'var(--amber-500)' : 'var(--brand-500)', transition: 'width .3s' }} />
                          </div>
                        )}
                      </td>
                      <td className="cellnum">{r.perUserLimit}</td>
                      <td className="cellnum">{r.minSubtotal ? KD.fmtDA(r.minSubtotal) : '—'}</td>
                      <td><Badge cls={st.cls}>{st.label}</Badge></td>
                      <td style={{ textAlign: 'right' }}>
                        <div className="row" style={{ gap: 7, justifyContent: 'flex-end' }}>
                          <Button size="sm" variant="ghost" icon="eye" onClick={() => openDetail(r)}>Détail</Button>
                          <Dropdown trigger={<button className="btn btn-icon btn-sm" aria-label="Actions"><Icon name="moreV" size={16} /></button>}
                            items={[
                              { label: 'Modifier', icon: 'edit', onClick: () => openEdit(r) },
                              { label: r.active ? 'Désactiver' : 'Réactiver', icon: r.active ? 'ban' : 'checkCircle', onClick: () => toggleActive(r) },
                              { sep: true },
                              { label: 'Supprimer', icon: 'trash', danger: true, onClick: () => setDeleting(r) },
                            ]} />
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </Card>

      {editing && (
        <Modal wide title={editing === 'new' ? 'Nouveau code promo' : `Modifier ${editing.code}`}
          desc="La réduction est recalculée et vérifiée côté serveur au paiement."
          icon={{ name: 'gift', color: 'green' }}
          onClose={() => setEditing(null)}
          footer={<>
            <Button variant="secondary" onClick={() => setEditing(null)}>Annuler</Button>
            <Button variant="primary" icon="check" loading={saving} onClick={doSave}>{editing === 'new' ? 'Créer la campagne' : 'Enregistrer'}</Button>
          </>}>

          <div className="form-section">
            <div className="form-section-title">Identité de la campagne</div>
            <div className="form-grid-2">
              <Field label="Code" hint={editing === 'new' ? 'Saisi par le client au paiement' : 'Non modifiable après création'}>
                <Input value={form.code} disabled={editing !== 'new'} onChange={e => setF('code', e.target.value.toUpperCase())} placeholder="Ex. WELCOME900" />
              </Field>
              <Field label="Type de réduction">
                <Select value={form.discountType} onChange={e => setF('discountType', e.target.value)}>
                  <option value="percent">Pourcentage (%)</option>
                  <option value="fixed">Montant fixe (DZD)</option>
                  <option value="free_days">Jours offerts</option>
                </Select>
              </Field>
            </div>
            <div className="form-grid-2" style={{ marginTop: 14 }}>
              <Field label={`Valeur (${TYPE_LABEL[form.discountType]})`}>
                <Input type="number" min="1" max={form.discountType === 'percent' ? '100' : undefined}
                  value={form.discountValue} onChange={e => setF('discountValue', e.target.value)} placeholder="10" />
              </Field>
              <div className="promo-preview">
                <div className="muted" style={{ fontSize: 11.5, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.4px' }}>Aperçu</div>
                {preview !== null ? (
                  <div style={{ marginTop: 5, fontSize: 13.5 }}>
                    Sur une location de <strong>{KD.fmtDA(previewOn)}</strong>, le client paie{' '}
                    <strong style={{ color: 'var(--brand-700)' }}>{KD.fmtDA(previewOn - preview)}</strong>
                    <span className="muted"> (− {KD.fmtDA(preview)})</span>
                  </div>
                ) : (
                  <div className="muted" style={{ marginTop: 5, fontSize: 13 }}>
                    {form.discountType === 'free_days'
                      ? 'La remise vaut le prix journalier × le nombre de jours offerts, plafonnée à la location.'
                      : 'Saisissez une valeur pour voir l\'effet sur une location type.'}
                  </div>
                )}
              </div>
            </div>
          </div>

          <div className="form-section">
            <div className="form-section-title">Fenêtre de validité</div>
            <div className="form-grid-2">
              <Field label="Valide à partir du" hint="Vide = actif immédiatement">
                <Input type="date" value={form.validFrom} onChange={e => setF('validFrom', e.target.value)} />
              </Field>
              <Field label="Expire le" hint="Vide = sans date de fin">
                <Input type="date" value={form.validUntil} onChange={e => setF('validUntil', e.target.value)} />
              </Field>
            </div>
          </div>

          <div className="form-section">
            <div className="form-section-title">Limites d'usage</div>
            <div className="form-grid-3">
              <Field label="Usages max" hint="Vide = illimité">
                <Input type="number" min="0" value={form.maxUses} onChange={e => setF('maxUses', e.target.value)} placeholder="∞" />
              </Field>
              <Field label="Par client" hint="Nombre de fois par compte">
                <Input type="number" min="1" value={form.perUserLimit} onChange={e => setF('perUserLimit', e.target.value)} />
              </Field>
              <Field label="Sous-total min. (DA)" hint="0 = aucun minimum">
                <Input type="number" min="0" value={form.minSubtotal} onChange={e => setF('minSubtotal', e.target.value)} />
              </Field>
            </div>
          </div>

          <div className="banner-info" style={{ fontSize: 12.5 }}>
            <Icon name="info" size={15} />
            La remise est financée par Uleez : elle est déduite de ce que le loueur reverse, jamais de son net.
          </div>
        </Modal>
      )}

      {detail && (
        <Modal wide title={`Utilisations · ${detail.code}`}
          desc={detailData ? `${detailData.redemptions.length} utilisation(s) · ${KD.fmtDA(detailData.totalDiscounted)} de remises accordées` : 'Chargement…'}
          icon={{ name: 'users', color: 'violet' }}
          onClose={() => setDetail(null)}
          footer={<Button variant="secondary" onClick={() => setDetail(null)}>Fermer</Button>}>
          {detailLoading && <TableSkeleton cols={4} />}
          {!detailLoading && !detailData && <EmptyState variant="err" title="Impossible de charger les utilisations" />}
          {!detailLoading && detailData && detailData.redemptions.length === 0 && (
            <EmptyState icon="users" title="Jamais utilisé" desc="Ce code n'a encore été appliqué à aucune réservation." />
          )}
          {!detailLoading && detailData && detailData.redemptions.length > 0 && (
            <div className="tbl-wrap">
              <table className="tbl">
                <thead><tr><th>Client</th><th>Réservation</th><th className="cellnum">Remise</th><th>Date</th></tr></thead>
                <tbody>
                  {detailData.redemptions.map(x => (
                    <tr key={x.id}>
                      <td>{x.customerName}</td>
                      <td className="mono strong">{x.referenceCode}</td>
                      <td className="cellnum" style={{ color: 'var(--brand-700)' }}>- {KD.fmtDA(x.amountDiscounted)}</td>
                      <td className="nowrap">{new Date(x.createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </Modal>
      )}

      {deleting && (
        <Modal title="Supprimer ce code promo ?"
          desc={`Le code « ${deleting.code} » sera désactivé et ne pourra plus être appliqué.`}
          icon={{ name: 'trash', color: 'rose' }}
          onClose={() => setDeleting(null)}
          footer={<>
            <Button variant="secondary" onClick={() => setDeleting(null)}>Annuler</Button>
            <Button variant="danger" icon="trash" loading={busy === deleting.id} onClick={doDelete}>Supprimer</Button>
          </>}>
          <p className="muted" style={{ margin: 0 }}>
            Les réservations déjà passées avec ce code ne sont pas affectées : la trace des
            utilisations est conservée pour la comptabilité.
          </p>
        </Modal>
      )}
    </div>
  );
}

Object.assign(window, { AdminVehicles, AdminBookings, AdminBookingDossier, VoidPaymentModal, AdminActivity, AdminSupport, AdminSettings, AdminCustomers, AdminPromoCodes });
