import React, { useState, useEffect, useMemo, useRef } from "react";
import { createRoot } from "react-dom/client";
import { createClient } from "@supabase/supabase-js";

/* ------------------------------------------------------------------ */
/* IDHOUSECONCEPT — Compte-rendu de visite, version mobile / tablette  */
/* Projet séparé de l'appli web. Même base Supabase, même structure.   */
/* Étape 1e : enregistrement du compte-rendu (PDF + fiche du chantier).  */
/* Écrit en base uniquement au clic « Enregistrer le CR ».             */
/* ------------------------------------------------------------------ */
const VERSION = "Étape 1f · accueil, transmission";

const SUPABASE_URL = "https://xhjzgjgumcczqeecmpuo.supabase.co";
const SUPABASE_PUBLISHABLE_KEY = "sb_publishable_X4k2DyMkUEgI1TcdrGj80w_CdTvVlV0";
const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);

/* Check-list d'origine — COPIE CONFORME de l'appli web (app.jsx).
   Les réponses d'un compte-rendu sont rangées par position dans cette
   liste : ne jamais en changer l'ordre ici. */
const CHECKLISTE_CR_PAR_LOT_DEFAUT = {
  "DEMOLITION": [
    { id: "demolition_g0", titre: "", items: ["Préparer le site pour garantir la sécurité des opérations.", "Démolition des structures principales selon le plan établi.", "Nettoyage final de la zone après la démolition."], avecSuivi: true },
  ],
  "TERRASSEMENT": [
    { id: "terrassement_g0", titre: "Plateforme + Fondations + Accès chantier :", items: ["Préparer la plateforme pour les futures constructions.", "Creuser les fondations selon les plans fournis.", "Evacuation des terres (HORS LOT)", "Assurer un accès sécurisé et dégagé au chantier."], avecSuivi: true },
    { id: "terrassement_g1", titre: "Tranchée technique :", items: ["Creuser des tranchées pour le passage des réseaux d'eau et d'électricité.", "Installer les conduits nécessaires. (eau, electricité, télécommunications)"], avecSuivi: true },
    { id: "terrassement_g2", titre: "Finition chantier :", items: ["Stabiliser le terrain.", "Mettre en place des systèmes de drainage pour éviter l'accumulation d'eau.", "Etalage des terres,"], avecSuivi: true },
  ],
  "FONDATIONS SPECIALES": [
    { id: "fondations_speciales_g0", titre: "", items: ["Mise en œuvre des fondations spéciales", "Forage pour la pose de pieux, micropieux, ou puits.", "Injection de résines pour renforcer les sols friables ou instables.", "Mise en place de techniques de soutènement ou de radier selon les besoins."], avecSuivi: true },
    { id: "fondations_speciales_g1", titre: "Finalisation des fondations :", items: ["Vérifier la stabilité et l’alignement des structures mises en place.", "Effectuer les raccords nécessaires avec les autres éléments du chantier (terrassement, maçonnerie).", "Réaliser un contrôle qualité pour garantir la durabilité et la sécurité des fondations."], avecSuivi: true },
  ],
  "MACONNERIE": [
    { id: "maconnerie_g0", titre: "RESERVATION PARTIE HABITABLE :", items: [], avecSuivi: true },
    { id: "maconnerie_g1", titre: "RESERVATION SOL GARAGE :", items: [], avecSuivi: true },
    { id: "maconnerie_g2", titre: "Implantation :", items: ["Définir et marquer précisément les emplacements des fondations et des murs."], avecSuivi: true },
    { id: "maconnerie_g3", titre: "Soubassement :", items: ["Réaliser les fondations."], avecSuivi: true },
    { id: "maconnerie_g4", titre: "Choix des matériaux pour le soubassement :", items: ["Mettre en place le hérisson ou autre base drainante.", "Poser la polyane antithermite (obligatoire)", "Poser les premières assises de blocs ou briques pour le soubassement, si applicable"], avecSuivi: true },
    { id: "maconnerie_g5", titre: "Coulage de la dalle :", items: ["Préparer le terrain pour accueillir la dalle.", "Effectuer le ferraillage selon le plan de béton armé (BA).", "Couler la dalle selon les spécifications techniques."], avecSuivi: true },
    { id: "maconnerie_g6", titre: "Élévation :", items: ["Construire les murs et les structures verticales conformément aux plans."], avecSuivi: true },
    { id: "maconnerie_g7", titre: "Seuils et appuis :", items: ["Installer les seuils et les appuis de fenêtres et de portes pour assurer l'étanchéité et la finition."], avecSuivi: true },
  ],
  "CHARPENTE": [
    { id: "charpente_g0", titre: "Livraison de la charpente :", items: ["Assurer la réception des éléments de charpente sur le chantier, en vérifiant la conformité avec les", "plans."], avecSuivi: true },
    { id: "charpente_g1", titre: "Pose de la charpente :", items: ["Installer les éléments de charpente selon les spécifications techniques et s'assurer de leur solidité."], avecSuivi: true },
  ],
  "COUVERTURE": [
    { id: "couverture_g0", titre: "COULEUR TUILES :", items: [], avecSuivi: true },
    { id: "couverture_g1", titre: "Pose de la couverture :", items: ["Installer les éléments de couverture selon les spécifications techniques, en veillant à l'étanchéité et à la", "résistance aux intempéries."], avecSuivi: true },
  ],
  "ENDUIT": [
    { id: "enduit_g0", titre: "Dégrossi :", items: ["Appliquer un enduit de dégrossi pour préparer les surfaces avant la finition."], avecSuivi: true },
    { id: "enduit_g1", titre: "Enduit de finition :", items: [], avecSuivi: true },
    { id: "enduit_g2", titre: "COULEUR ENDUIT :", items: [], avecSuivi: true },
    { id: "enduit_g3", titre: "FINITION :", items: ["Poser l'enduit de finition pour assurer une surface lisse et esthétique, conforme aux attentes."], avecSuivi: true },
  ],
  "MENSUISERIE EXTERIEURE": [
    { id: "mensuiserie_exterieure_g0", titre: "TAPEE :", items: [], avecSuivi: true },
    { id: "mensuiserie_exterieure_g1", titre: "COULEUR MENUISERIES :", items: [], avecSuivi: true },
    { id: "mensuiserie_exterieure_g2", titre: "Pose des menuiseries :", items: ["Installer les fenêtres, portes et autres éléments de menuiserie extérieure conformément aux spécifications."], avecSuivi: true },
    { id: "mensuiserie_exterieure_g3", titre: "Réglages des menuiseries et volets :", items: ["Effectuer les réglages nécessaires pour assurer le bon fonctionnement des menuiseries et des volets."], avecSuivi: true },
  ],
  "DESCENTES / GOUTTIERES": [
    { id: "descentes_gouttieres_g0", titre: "FORME :", items: [], avecSuivi: true },
    { id: "descentes_gouttieres_g1", titre: "COULEUR :", items: [], avecSuivi: true },
    { id: "descentes_gouttieres_g2", titre: "Pose des gouttières :", items: ["Installer les gouttières pour assurer un drainage efficace des eaux pluviales, conformément aux normes", "en vigueur."], avecSuivi: true },
  ],
  "PLATRERIE": [
    { id: "platrerie_g0", titre: "Railage plafond et isolation doublage :", items: ["Installer le railage pour le plafond et mettre en place l'isolation pour le doublage."], avecSuivi: true },
    { id: "platrerie_g1", titre: "Plaquage des plafonds et doublage :", items: ["Poser les plaques de plâtre pour les plafonds et les murs en doublage, en veillant à la continuité de", "l'isolation."], avecSuivi: true },
    { id: "platrerie_g2", titre: "Première peau cloison distribution :", items: ["Installer la première peau des cloisons de distribution pour délimiter les espaces intérieurs."], avecSuivi: true },
    { id: "platrerie_g3", titre: "Fermeture de cloison :", items: ["Finaliser la fermeture des cloisons pour assurer l'intégrité et la finition des murs."], avecSuivi: true },
    { id: "platrerie_g4", titre: "Bandes :", items: ["Appliquer les bandes à joints pour préparer les surfaces avant peinture ou finition."], avecSuivi: true },
  ],
  "MENUISERIE INTERIEURE": [
    { id: "menuiserie_interieure_g0", titre: "CHOIX PORTE :", items: [], avecSuivi: true },
    { id: "menuiserie_interieure_g1", titre: "CHOIX POIGNEE :", items: [], avecSuivi: true },
    { id: "menuiserie_interieure_g2", titre: "Pose d'huisserie :", items: ["Installer les huisseries pour assurer un bon encadrement des portes et fenêtres intérieures."], avecSuivi: true },
    { id: "menuiserie_interieure_g3", titre: "Pose des portes :", items: ["Installer les portes intérieures conformément aux spécifications, en vérifiant l'ajustement et l'esthétique."], avecSuivi: true },
    { id: "menuiserie_interieure_g4", titre: "Pose des poignées et réglages :", items: ["Installer les poignées sur les portes et effectuer les réglages nécessaires pour garantir un fonctionnement", "fluide."], avecSuivi: true },
  ],
  "ELECTRICITE / VMC": [
    { id: "electricite_vmc_g0", titre: "Incorporation sous dallage :", items: ["Préparer et intégrer les éléments électriques dans le dallage avant le coulage."], avecSuivi: true },
    { id: "electricite_vmc_g1", titre: "Pose de la pieuvre électrique et VMC :", items: ["Installer le pieuvre électrique ainsi que le système de ventilation mécanique contrôlée (VMC) selon les", "normes."], avecSuivi: true },
    { id: "electricite_vmc_g2", titre: "Descente de filerie et sortie de câbles :", items: ["Mettre en place les descentes de filerie et prévoir les sorties de câbles pour les installations électriques."], avecSuivi: true },
    { id: "electricite_vmc_g3", titre: "Pose de la GTL :", items: ["Installer le tableau de gestion technique de logement (GTL) pour la distribution électrique."], avecSuivi: true },
    { id: "electricite_vmc_g4", titre: "Pose de l’appareillage :", items: ["Installer les appareils électriques (interrupteurs, prises, etc.) en vérifiant leur bon fonctionnement."], avecSuivi: true },
  ],
  "PLOMBERIE": [
    { id: "plomberie_g0", titre: "Incorporation sous dallage :", items: ["Installer les conduits et éléments de plomberie sous le dallage avant le coulage."], avecSuivi: true },
    { id: "plomberie_g1", titre: "Préparation des doublages et cloisons :", items: ["Mettre en place les préparations nécessaires pour intégrer les installations de plomberie dans les", "doublages et cloisons."], avecSuivi: true },
    { id: "plomberie_g2", titre: "Pose des équipements sanitaires :", items: ["Installer les équipements sanitaires (lavabos, douches, WC, etc.) conformément aux plans et aux normes", "en vigueur."], avecSuivi: true },
  ],
  "CHAUFFAGE": [
    { id: "chauffage_g0", titre: "Pose du système :", items: ["Installer le système de chauffage conformément aux spécifications techniques, en assurant une intégration", "optimale et une conformité avec les normes en vigueur."], avecSuivi: true },
    { id: "chauffage_g1", titre: "Mise en service :", items: ["Réaliser la mise en service, incluant les ajustements et contrôles nécessaires pour garantir une", "performance optimale."], avecSuivi: true },
  ],
  "ISOLATION SOL": [
    { id: "isolation_sol_g0", titre: "Pose du TMS :", items: [], avecSuivi: true },
    { id: "isolation_sol_g1", titre: "Epaisseur :", items: ["Installer le TMS pour assurer une isolation efficace du sol, garantissant ainsi le confort thermique et la", "protection contre l'humidité."], avecSuivi: true },
  ],
  "CHAPE": [
    { id: "chape_g0", titre: "Coulage de la chape :", items: ["Réaliser le coulage de la chape en respectant les normes de nivellement et d'épaisseur."], avecSuivi: true },
    { id: "chape_g1", titre: "Chape sous receveur :", items: ["Préparer et couler la chape sous le receveur de douche pour assurer une bonne évacuation de l'eau."], avecSuivi: true },
  ],
  "CARRELAGE": [
    { id: "carrelage_g0", titre: "Pose de carrelage :", items: ["Installer le carrelage au sol en veillant à respecter les motifs et les jointures."], avecSuivi: true },
    { id: "carrelage_g1", titre: "Pose de faïence :", items: ["Installer la faïence sur les murs, en s'assurant d'une finition soignée et conforme aux normes."], avecSuivi: true },
  ],
  "PEINTURE": [
    { id: "peinture_g0", titre: "Ponçage + Impression mur :", items: ["Effectuer le ponçage des surfaces murales, suivi de l’application d’une couche d'impression pour préparer", "les murs à la finition."], avecSuivi: true },
    { id: "peinture_g1", titre: "Finition plafond :", items: ["Appliquer la finition sur le plafond, en veillant à un rendu uniforme et propre."], avecSuivi: true },
    { id: "peinture_g2", titre: "Finition mur :", items: ["Réaliser la finition des murs, en utilisant la peinture choisie pour obtenir l’esthétique souhaitée."], avecSuivi: true },
  ],
  "SOL SOUPLE": [
    { id: "sol_souple_g0", titre: "Pose de sol :", items: ["Installer le revêtement de sol souple en respectant les méthodes appropriées pour garantir une surface", "uniforme et durable."], avecSuivi: true },
  ],
  "CUISINE": [
    { id: "cuisine_g0", titre: "Pose de la cuisine :", items: ["Installer les éléments de la cuisine (meubles, plans de travail, appareils électroménagers) en suivant les", "plans et les normes pour assurer fonctionnalité et esthétique."], avecSuivi: true },
  ],
  "ISOLATION PLAFOND": [
    { id: "isolation_plafond_g0", titre: "Réalisation :", items: ["Effectuer l'isolation des combles en utilisant des matériaux appropriés pour garantir une performance", "thermique optimale et éviter les déperditions de chaleur."], avecSuivi: true },
  ],
  "TEST RE2020": [
    { id: "test_re2020_g0", titre: "Réalisation :", items: ["Effectuer le test de conformité RE2020 pour vérifier que le bâtiment respecte les exigences en matière de", "performance énergétique et de confort."], avecSuivi: true },
  ],
  "RECEPTION": [
    { id: "reception_g0", titre: "Semaine de réception de l'ouvrage :", items: [], avecSuivi: true },
    { id: "reception_g1", titre: "Pré-réception :", items: [], avecSuivi: true },
  ],
};

// Même règle que le web : la liste personnalisée (Paramètres) remplace
// celle d'origine lot par lot.
function checklisteCRActive(perso) {
  if (!perso || typeof perso !== "object") return CHECKLISTE_CR_PAR_LOT_DEFAUT;
  const fusion = { ...CHECKLISTE_CR_PAR_LOT_DEFAUT };
  Object.keys(perso).forEach(nom => { if (Array.isArray(perso[nom])) fusion[nom] = perso[nom]; });
  return fusion;
}
function groupeAvecSemaine(g) { return g.avecSemaine !== undefined ? g.avecSemaine !== false : g.avecSuivi !== false; }
function groupeAvecConforme(g) { return g.avecConforme !== undefined ? g.avecConforme !== false : g.avecSuivi !== false; }

/* ------------------------------ Lecture -------------------------------- */
// On ne lit que ce qui sert au compte-rendu (pas toute la fiche du
// chantier), pour rester léger sur un réseau de chantier.
async function chargerChantiers() {
  const leger = "id, nom, agence, statut, created_at, clients(nom), comptesRendus:fiche_details->comptesRendus, interne:fiche_details->interne, transmissionsCR:fiche_details->transmissionsCR";
  let { data, error } = await supabase.from("projets").select(leger).order("created_at", { ascending: false });
  if (error) {
    // Repli : lecture complète, si la lecture ciblée n'est pas acceptée.
    const r = await supabase.from("projets").select("id, nom, agence, statut, created_at, clients(nom), fiche_details").order("created_at", { ascending: false });
    if (r.error) throw r.error;
    data = r.data.map(p => ({ ...p, comptesRendus: p.fiche_details?.comptesRendus, interne: p.fiche_details?.interne, transmissionsCR: p.fiche_details?.transmissionsCR }));
  }
  return (data || []).map(p => ({
    id: p.id, nom: p.nom || "Sans nom", agence: p.agence || "", statut: p.statut || "", client: p.clients?.nom || "",
    transmissionsCR: Array.isArray(p.transmissionsCR) ? p.transmissionsCR : [],
    conducteur: p.interne?.conducteurTravaux || "",
    comptesRendus: Array.isArray(p.comptesRendus) ? p.comptesRendus : [],
  }));
}
// Lecture seule de la check-list personnalisée. (Le web, lui, crée les
// réglages manquants au chargement : le mobile ne le fait jamais.)
async function chargerChecklistePerso() {
  const { data, error } = await supabase.from("parametres").select("valeurs").eq("cle", "checklisteCR").maybeSingle();
  if (error) throw error;
  const v = data?.valeurs;
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
}

/* ------------------------------ Outils --------------------------------- */
const MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"];
// "2026-10-04" → "4 octobre 2026", sans décalage de fuseau horaire.
function dateLongue(s) {
  const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s || "");
  if (!m) return s || "Date inconnue";
  return `${Number(m[3])} ${MOIS[Number(m[2]) - 1]} ${m[1]}`;
}
function dateCourte(s) {
  const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s || "");
  return m ? `${m[3]}/${m[2]}/${m[1]}` : "";
}
function normaliser(s) {
  return (s || "").toString().normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
}
function crTries(chantier) {
  return [...chantier.comptesRendus].sort((a, b) => (b.date || "").localeCompare(a.date || "") || (b.numero || 0) - (a.numero || 0));
}
function dernierCR(chantier) { return crTries(chantier)[0] || null; }

function useLargeur() {
  const [l, setL] = useState(() => window.innerWidth);
  useEffect(() => {
    const f = () => setL(window.innerWidth);
    window.addEventListener("resize", f);
    return () => window.removeEventListener("resize", f);
  }, []);
  return l;
}
function useStockageLocal(cle, initial) {
  const [v, setV] = useState(() => {
    try { const s = localStorage.getItem(cle); return s === null ? initial : JSON.parse(s); } catch (e) { return initial; }
  });
  useEffect(() => { try { localStorage.setItem(cle, JSON.stringify(v)); } catch (e) { /* pas bloquant */ } }, [cle, v]);
  return [v, setV];
}


// Lots du chantier et offres — mêmes lectures que le web (lecture seule).
async function chargerLotsEtOffres(projetId) {
  const [l, o] = await Promise.all([
    supabase.from("lots").select("*, intervenants(id, nom)").eq("projet_id", projetId),
    supabase.from("offres").select("*, intervenants(nom), lots!inner(projet_id)").eq("lots.projet_id", projetId),
  ]);
  if (l.error) throw l.error;
  if (o.error) throw o.error;
  const lots = (l.data || []).map(x => ({ id: x.id, nom: x.nom, intervenantId: x.intervenant_id, numero: x.numero }));
  const offres = (o.data || []).map(x => ({
    id: x.id, lotId: x.lot_id, intervenantId: x.intervenant_id,
    intervenantNom: x.intervenants?.nom || x.entreprise_libre || "", retenu: x.retenu,
  }));
  return { lots, offres };
}
// Copies conformes des règles du web : quelle entreprise est retenue pour
// un corps de métier donné sur ce chantier.
function lotCorrespondCorpsEtat(nomLot, corpsEtat) {
  if (!nomLot || !corpsEtat) return false;
  const a = normaliser(nomLot);
  const b = normaliser(corpsEtat);
  if (a.includes(b) || b.includes(a)) return true;
  const motsA = a.split(/[\s/]+/).filter(m => m.length > 3);
  const motsB = b.split(/[\s/]+/).filter(m => m.length > 3);
  return motsA.some(m => motsB.some(m2 => m === m2 || m.includes(m2) || m2.includes(m)));
}
function intervenantUniquePourCorpsEtat(corpsEtat, lots, offres) {
  if (!corpsEtat) return "";
  const lotsTrouves = lots.filter(l => lotCorrespondCorpsEtat(l.nom, corpsEtat));
  if (lotsTrouves.length !== 1) return "";
  const retenue = offres.find(o => o.lotId === lotsTrouves[0].id && o.retenu);
  if (retenue?.intervenantNom) return retenue.intervenantNom;
  const attribue = lotsTrouves[0].intervenantId ? offres.find(o => o.intervenantId === lotsTrouves[0].intervenantId)?.intervenantNom : "";
  return attribue || "";
}

/* ------------------------- Brouillons (sur l'appareil) ----------------- */
// Un brouillon par chantier, gardé dans le navigateur de l'appareil.
// Mêmes noms de champs que le formulaire web, pour que l'enregistrement
// final (étape 1e) produise exactement la même structure.
const CLE_BROUILLON = id => `idh-mobile-brouillon-${id}`;
function lireBrouillon(projetId) {
  try { const s = localStorage.getItem(CLE_BROUILLON(projetId)); return s ? JSON.parse(s) : null; } catch (e) { return null; }
}
function ecrireBrouillon(projetId, b) {
  try { localStorage.setItem(CLE_BROUILLON(projetId), JSON.stringify(b)); return true; } catch (e) { return false; }
}
function supprimerBrouillon(projetId) {
  try { localStorage.removeItem(CLE_BROUILLON(projetId)); } catch (e) { /* pas bloquant */ }
}
function aujourdHui() {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function brouillonVide(chantier) {
  return {
    projetId: chantier.id, dateVisite: aujourdHui(), auteur: chantier.conducteur || "",
    lotsChoisis: [], detailParLot: {},
    commentairesGeneraux: "", prochaineVisite: "", indicationsProchaineVisite: "", photosGenerales: [],
    majLe: null,
  };
}
function brouillonEstVide(b) {
  return !b.lotsChoisis?.length && !b.commentairesGeneraux && !b.indicationsProchaineVisite && !(b.photosGenerales || []).length;
}
// Nouveau brouillon rempli à partir d'un compte-rendu enregistré : lots,
// réponses, lignes ajoutées, corrections, textes et photos (gardant leur lien
// en ligne). Ne reprend pas : date de visite, auteur, date de prochaine visite.
// Les photos déjà prises dans le brouillon existant sont gardées.
function brouillonDepuisCR(chantier, cr, existant) {
  const base = existant || brouillonVide(chantier);
  const copiePhoto = p => ({ idLocal: null, nom: p.nom, url: p.url, ...(p.annotee ? { annotee: true, formes: p.formes || [] } : {}) });
  const photosDuJour = liste => (liste || []).filter(p => p.idLocal);
  const detail = { ...(base.detailParLot || {}) };
  (cr.lots || []).forEach(l => {
    detail[l.nom] = {
      champs: l.champs || {}, groupes: l.groupes || [], groupesPerso: l.groupesPerso || [],
      corrections: l.corrections || "",
      photos: [...(l.photos || []).filter(p => p.url).map(copiePhoto), ...photosDuJour(base.detailParLot?.[l.nom]?.photos)],
    };
  });
  const nbPhotos = (cr.photosGenerales || []).length + (cr.lots || []).reduce((n, l) => n + (l.photos || []).length, 0);
  return {
    ...base,
    lotsChoisis: (cr.lots || []).map(l => l.nom),
    detailParLot: detail,
    commentairesGeneraux: cr.commentairesGeneraux || "",
    indicationsProchaineVisite: cr.indicationsProchaineVisite || "",
    photosGenerales: [...(cr.photosGenerales || []).filter(x => x.url).map(copiePhoto), ...photosDuJour(base.photosGenerales)],
    departChoisi: true,
    repriseDe: { numero: cr.numero, date: cr.date, nbLots: (cr.lots || []).length, nbPhotos },
    majLe: new Date().toISOString(),
  };
}
function nouveauDetailLot(nom, checkliste) {
  const def = checkliste[nom] || [];
  return { champs: {}, groupes: def.map(() => ({ conforme: "", semaine: "", retire: false })), groupesPerso: [], corrections: "", photos: [] };
}
function heureCourte(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  return `${dateCourte(iso.slice(0, 10)).slice(0, 5)} à ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}


/* ------------------------------ Photos (étape 1c) ---------------------- */
// Chaque photo est d'abord réduite puis gardée sur l'appareil (IndexedDB),
// puis envoyée au même endroit que l'appli web : espace « APS »,
// dossier cr-chantier/{chantier}, lien signé valable 5 ans.
// Dans le brouillon, une photo = { idLocal, nom, url } ; url reste vide tant
// qu'elle n'est pas envoyée. À l'enregistrement final (1e), seuls url et nom
// sont gardés, comme sur le web.
function ouvrirBaseLocale() {
  return new Promise((res, rej) => {
    const r = indexedDB.open("idh-mobile", 1);
    r.onupgradeneeded = () => { if (!r.result.objectStoreNames.contains("photos")) r.result.createObjectStore("photos"); };
    r.onsuccess = () => res(r.result);
    r.onerror = () => rej(r.error);
  });
}
async function baseLocale(mode, action) {
  const db = await ouvrirBaseLocale();
  return new Promise((res, rej) => {
    const tx = db.transaction("photos", mode);
    const req = action(tx.objectStore("photos"));
    tx.oncomplete = () => { res(req?.result); db.close(); };
    tx.onerror = () => { rej(tx.error); db.close(); };
  });
}
const photoLocaleEcrire = (cle, blob) => baseLocale("readwrite", s => s.put(blob, cle));
const photoLocaleLire = cle => baseLocale("readonly", s => s.get(cle));
const photoLocaleSupprimer = cle => baseLocale("readwrite", s => s.delete(cle)).catch(() => {});

// Réduit la photo (côté le plus long : 1600 px, JPEG) : une photo de
// téléphone passe de 4-8 Mo à environ 300-500 Ko.
async function reduirePhoto(fichier, max = 1600, qualite = 0.82) {
  const lien = URL.createObjectURL(fichier);
  try {
    const img = await new Promise((res, rej) => {
      const i = new Image();
      i.onload = () => res(i);
      i.onerror = () => rej(new Error("Ce format d'image n'est pas lisible sur cet appareil."));
      i.src = lien;
    });
    const ratio = Math.min(1, max / Math.max(img.naturalWidth, img.naturalHeight));
    const l = Math.round(img.naturalWidth * ratio), h = Math.round(img.naturalHeight * ratio);
    const c = document.createElement("canvas");
    c.width = l; c.height = h;
    c.getContext("2d").drawImage(img, 0, 0, l, h);
    return await new Promise((res, rej) => c.toBlob(b => b ? res(b) : rej(new Error("Réduction de la photo impossible.")), "image/jpeg", qualite));
  } finally {
    URL.revokeObjectURL(lien);
  }
}

function toutesPhotos(b) {
  if (!b) return [];
  return [...(b.photosGenerales || []), ...Object.values(b.detailParLot || {}).flatMap(d => d?.photos || [])];
}
function majPhoto(b, idLocal, champs) {
  const maj = liste => (liste || []).map(p => p.idLocal === idLocal ? { ...p, ...champs } : p);
  const detailParLot = {};
  Object.keys(b.detailParLot || {}).forEach(n => { detailParLot[n] = { ...b.detailParLot[n], photos: maj(b.detailParLot[n].photos) }; });
  return { ...b, photosGenerales: maj(b.photosGenerales), detailParLot };
}

const envoisEnCours = new Map(); // idLocal -> envoi en cours (promesse)
const envoisEchoues = new Set();
function signalerEtatPhotos() { window.dispatchEvent(new Event("idh-photo-etat")); }

function envoyerPhoto(projetId, idLocal, nom) {
  if (envoisEnCours.has(idLocal)) return envoisEnCours.get(idLocal);
  let fin;
  envoisEnCours.set(idLocal, new Promise(r => { fin = r; }));
  envoisEchoues.delete(idLocal);
  signalerEtatPhotos();
  envoyerPhotoMaintenant(projetId, idLocal, nom).finally(() => fin());
  return envoisEnCours.get(idLocal);
}
async function envoyerPhotoMaintenant(projetId, idLocal, nom) {
  try {
    const blob = await photoLocaleLire(idLocal);
    if (!blob) throw new Error("photo introuvable sur l'appareil");
    const chemin = `cr-chantier/${projetId}/${Date.now()}-${nom}`;
    const { error } = await supabase.storage.from("APS").upload(chemin, blob, { contentType: "image/jpeg" });
    if (error) throw error;
    const { data, error: err2 } = await supabase.storage.from("APS").createSignedUrl(chemin, 60 * 60 * 24 * 365 * 5);
    if (err2) throw err2;
    // Le brouillon gardé sur l'appareil est mis à jour même si le formulaire
    // a été fermé entre-temps.
    const b = lireBrouillon(projetId);
    if (b) ecrireBrouillon(projetId, majPhoto(b, idLocal, { url: data.signedUrl }));
    window.dispatchEvent(new CustomEvent("idh-photo-envoyee", { detail: { projetId, idLocal, url: data.signedUrl } }));
  } catch (e) {
    envoisEchoues.add(idLocal);
  } finally {
    envoisEnCours.delete(idLocal);
    signalerEtatPhotos();
  }
}
function relancerEnvois(projetId) {
  if (!navigator.onLine) return;
  toutesPhotos(lireBrouillon(projetId)).filter(p => p.idLocal && !p.url).forEach(p => envoyerPhoto(projetId, p.idLocal, p.nom));
}

// Affiche une photo : la copie gardée sur l'appareil si elle existe
// (instantané, sans réseau), sinon le lien en ligne.
function ImagePhoto({ photo, className, alt }) {
  const [src, setSrc] = useState(photo.idLocal ? null : photo.url);
  useEffect(() => {
    if (!photo.idLocal) { setSrc(photo.url); return; }
    let lien = null, annule = false;
    photoLocaleLire(photo.idLocal).then(blob => {
      if (annule) return;
      if (blob) { lien = URL.createObjectURL(blob); setSrc(lien); } else setSrc(photo.url || null);
    }).catch(() => { if (!annule) setSrc(photo.url || null); });
    return () => { annule = true; if (lien) URL.revokeObjectURL(lien); };
  }, [photo.idLocal, photo.url]);
  if (!src) return <span className={"photo-absente " + (className || "")}>Photo indisponible</span>;
  return <img src={src} alt={alt || photo.nom || ""} className={className} loading="lazy" />;
}

function ZonePhotos({ photos, projetId, prefixeNom, onAjouter, onRetirer, onOuvrir, onAnnoter }) {
  const [traitement, setTraitement] = useState(0);
  const [erreur, setErreur] = useState("");
  const refAppareil = useRef(null), refGalerie = useRef(null);

  async function recevoir(fichiers) {
    setErreur("");
    const liste = Array.from(fichiers || []);
    if (liste.length === 0) return;
    try { navigator.storage?.persist?.(); } catch (e) { /* pas bloquant */ }
    setTraitement(n => n + liste.length);
    for (const f of liste) {
      try {
        const blob = await reduirePhoto(f);
        const idLocal = `ph-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        const nom = `${prefixeNom}_${aujourdHui()}_${idLocal.slice(-6)}.jpg`;
        await photoLocaleEcrire(idLocal, blob);
        onAjouter({ idLocal, nom, url: null });
        if (navigator.onLine) envoyerPhoto(projetId, idLocal, nom);
      } catch (e) {
        setErreur(e.message || "La photo n'a pas pu être ajoutée.");
      } finally {
        setTraitement(n => n - 1);
      }
    }
  }
  const enAttente = photos.filter(p => p.idLocal && !p.url);
  const echecs = enAttente.filter(p => envoisEchoues.has(p.idLocal) && !envoisEnCours.has(p.idLocal));

  return (
    <div className="zone-photos">
      {photos.length > 0 && (
        <div className="photos">
          {photos.map((p, i) => {
            const etat = p.url ? null : envoisEnCours.has(p.idLocal) ? "Envoi…" : envoisEchoues.has(p.idLocal) ? "Non envoyée" : "En attente";
            return (
              <div className="photo-carte" key={p.idLocal || p.url || i}>
                <div className="photo photo-saisie">
                  <button type="button" className="photo-ouvrir" onClick={() => onOuvrir(photos, i)} aria-label={`Agrandir la photo ${i + 1}`}>
                    <ImagePhoto photo={p} />
                  </button>
                  {etat && <span className={"photo-etat" + (etat === "Non envoyée" ? " photo-etat-echec" : "")}>{etat}</span>}
                  <button type="button" className="photo-retirer" aria-label="Retirer la photo" onClick={() => { if (window.confirm("Retirer cette photo du compte-rendu ?")) onRetirer(p); }}>×</button>
                </div>
                {p.idLocal && <button type="button" className="photo-annoter" onClick={() => onAnnoter(p)}>{p.annotee ? "Ré-annoter" : "Annoter"}</button>}
              </div>
            );
          })}
        </div>
      )}
      {traitement > 0 && <p className="note">Préparation de la photo…</p>}
      {erreur && <p className="erreur">{erreur}</p>}
      {enAttente.length > 0 && (
        <p className="note">
          {enAttente.length} photo{enAttente.length > 1 ? "s" : ""} gardée{enAttente.length > 1 ? "s" : ""} sur l'appareil, pas encore envoyée{enAttente.length > 1 ? "s" : ""}.
          {" "}{!navigator.onLine ? "Envoi automatique au retour du réseau." : echecs.length > 0 ? <button type="button" className="lien" onClick={() => relancerEnvois(projetId)}>Réessayer l'envoi</button> : ""}
        </p>
      )}
      <div className="boutons-photo">
        <button type="button" className="btn-principal btn-photo" onClick={() => refAppareil.current?.click()}>Prendre une photo</button>
        <button type="button" className="btn-secondaire btn-photo" onClick={() => refGalerie.current?.click()}>Depuis la galerie</button>
      </div>
      <input ref={refAppareil} type="file" accept="image/*" capture="environment" hidden onChange={e => { recevoir(e.target.files); e.target.value = ""; }} />
      <input ref={refGalerie} type="file" accept="image/*" multiple hidden onChange={e => { recevoir(e.target.files); e.target.value = ""; }} />
    </div>
  );
}


/* --------------------------- Annotation (étape 1d) --------------------- */
// Réécriture tactile de l'annotateur du web (PlanAnnotable) : doigt, stylet
// ou souris. Mêmes outils et MÊME FORMAT de formes que le web :
//   flèche / cercle : { id, type, x1, y1, x2, y2 }
//   texte           : { id, type: "texte", x, y, texte, taille }
// en pixels de l'image. Les traits libres sont « figés » dans l'image.
// Avec un stylet, le doigt (ou la paume) ne dessine pas : il sert à
// déplacer et zoomer. Deux doigts = zoom, dans tous les cas.
const ROUGE_ANNOT = "#E11D48", ACIER_ANNOT = "#2F5A73";

function dessinerTraits(ctx, traits, e) {
  ctx.strokeStyle = ROUGE_ANNOT;
  ctx.fillStyle = ROUGE_ANNOT;
  ctx.lineWidth = 4 * e;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  traits.forEach(pts => {
    if (!pts || pts.length === 0) return;
    if (pts.length === 1) {
      ctx.beginPath(); ctx.arc(pts[0].x, pts[0].y, 2 * e, 0, Math.PI * 2); ctx.fill();
      return;
    }
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
    ctx.stroke();
  });
}
function mesurerTexte(ctx, f, e) {
  const taille = f.taille || Math.round(34 * e);
  ctx.font = `bold ${taille}px Arial`;
  return { taille, largeur: ctx.measureText(f.texte || "").width };
}
// Mêmes rendus que le web ; seules les poignées grossissent pour le doigt.
function dessinerFormeAnnot(ctx, f, selectionnee, e, rayonPoignee) {
  ctx.strokeStyle = ROUGE_ANNOT;
  ctx.fillStyle = ROUGE_ANNOT;
  ctx.lineWidth = 5 * e;
  ctx.lineCap = "round";
  if (f.type === "fleche") {
    const angle = Math.atan2(f.y2 - f.y1, f.x2 - f.x1);
    const tete = 22 * e;
    ctx.beginPath();
    ctx.moveTo(f.x1, f.y1);
    ctx.lineTo(f.x2, f.y2);
    ctx.lineTo(f.x2 - tete * Math.cos(angle - Math.PI / 6), f.y2 - tete * Math.sin(angle - Math.PI / 6));
    ctx.moveTo(f.x2, f.y2);
    ctx.lineTo(f.x2 - tete * Math.cos(angle + Math.PI / 6), f.y2 - tete * Math.sin(angle + Math.PI / 6));
    ctx.stroke();
  } else if (f.type === "cercle") {
    ctx.beginPath();
    ctx.arc(f.x1, f.y1, Math.hypot(f.x2 - f.x1, f.y2 - f.y1), 0, Math.PI * 2);
    ctx.stroke();
  } else if (f.type === "texte") {
    const { taille, largeur } = mesurerTexte(ctx, f, e);
    ctx.fillText(f.texte, f.x, f.y);
    if (selectionnee) {
      ctx.strokeStyle = ACIER_ANNOT;
      ctx.lineWidth = 2 * e;
      ctx.strokeRect(f.x - 4, f.y - taille, largeur + 8, taille * 1.3);
      ctx.fillStyle = ACIER_ANNOT;
      ctx.beginPath();
      ctx.arc(f.x + largeur + 4, f.y + taille * 0.3, rayonPoignee, 0, Math.PI * 2);
      ctx.fill();
    }
  }
  if (selectionnee && (f.type === "fleche" || f.type === "cercle")) {
    ctx.fillStyle = ACIER_ANNOT;
    [{ x: f.x1, y: f.y1 }, { x: f.x2, y: f.y2 }].forEach(p => {
      ctx.beginPath(); ctx.arc(p.x, p.y, rayonPoignee, 0, Math.PI * 2); ctx.fill();
    });
  }
}
function canvasVersBlob(c, qualite) {
  return new Promise((res, rej) => {
    try { c.toBlob(b => b ? res(b) : rej(new Error("export vide")), "image/jpeg", qualite); }
    catch (e) { rej(e); }
  });
}
const distPts = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);

function AnnotateurTactile({ photo, onEnregistrer, onFermer }) {
  const sceneRef = useRef(null), canvasRef = useRef(null), imageRef = useRef(null);
  const [pret, setPret] = useState(false);
  const [erreur, setErreur] = useState("");
  const [mode, setMode] = useState("dessin");
  // Formes reprises seulement si l'on repart de l'image sans formes (base).
  const [formes, setFormes] = useState(() => (photo.annotee && photo.idBase ? (photo.formes || []) : []));
  const [traits, setTraits] = useState([]);
  const [selectionId, setSelectionId] = useState(null);
  const [vue, setVue] = useState({ s: 1, tx: 0, ty: 0 });
  const [cadre, setCadre] = useState({ l: 0, h: 0, x: 0, y: 0 });
  const [saisieTexte, setSaisieTexte] = useState(null);
  const [texte, setTexte] = useState("");
  const [enregistrement, setEnregistrement] = useState(false);
  const [nbHisto, setNbHisto] = useState(0);
  const [penVu, setPenVu] = useState(false);

  const formesR = useRef(formes), traitsR = useRef(traits), selR = useRef(selectionId), vueR = useRef(vue), cadreR = useRef(cadre);
  formesR.current = formes; traitsR.current = traits; selR.current = selectionId; vueR.current = vue; cadreR.current = cadre;
  const histo = useRef([]);
  const penVuR = useRef(false);
  const pointeurs = useRef(new Map());
  const action = useRef(null);
  const raf = useRef(0);

  const echelle = () => Math.max((canvasRef.current?.width || 1000) / 900, 1);
  // Nombre de pixels de l'image pour un pixel de l'écran (zoom compris).
  const pxImage = () => {
    const c = canvasRef.current; if (!c) return 1;
    const r = c.getBoundingClientRect();
    return r.width ? c.width / r.width : 1;
  };
  const rayonPoignee = () => Math.max(12 * echelle(), 14 * pxImage());
  const tolerance = () => Math.max(20 * echelle(), 24 * pxImage());

  function ajuster() {
    const sc = sceneRef.current, img = imageRef.current;
    if (!sc || !img) return;
    const r = sc.getBoundingClientRect();
    const ratio = Math.min(r.width / img.naturalWidth, r.height / img.naturalHeight);
    const l = img.naturalWidth * ratio, h = img.naturalHeight * ratio;
    setCadre({ l, h, x: (r.width - l) / 2, y: (r.height - h) / 2 });
    setVue({ s: 1, tx: 0, ty: 0 });
  }

  useEffect(() => {
    let annule = false, lien = null;
    (async () => {
      try {
        const cle = photo.idBase || photo.idLocal;
        const blob = cle ? await photoLocaleLire(cle).catch(() => null) : null;
        let src = photo.url;
        if (blob) { lien = URL.createObjectURL(blob); src = lien; }
        if (!src) throw new Error("Photo introuvable sur l'appareil.");
        const img = new Image();
        img.crossOrigin = "anonymous";
        await new Promise((res, rej) => { img.onload = res; img.onerror = () => rej(new Error("La photo n'a pas pu être ouverte.")); img.src = src; });
        if (annule) return;
        imageRef.current = img;
        canvasRef.current.width = img.naturalWidth;
        canvasRef.current.height = img.naturalHeight;
        ajuster();
        setPret(true);
      } catch (e) { if (!annule) setErreur(e.message); }
    })();
    window.addEventListener("resize", ajuster);
    return () => { annule = true; window.removeEventListener("resize", ajuster); if (lien) URL.revokeObjectURL(lien); };
  }, []);

  function dessiner(avecSelection = true) {
    const c = canvasRef.current, img = imageRef.current;
    if (!c || !img) return;
    const ctx = c.getContext("2d");
    const e = echelle();
    ctx.clearRect(0, 0, c.width, c.height);
    ctx.drawImage(img, 0, 0);
    dessinerTraits(ctx, traitsR.current, e);
    const a = action.current;
    if (a?.type === "trait") dessinerTraits(ctx, [a.points], e);
    const liste = [...formesR.current];
    if (a?.type === "forme" && a.apercu) liste.push(a.apercu);
    liste.forEach(f => dessinerFormeAnnot(ctx, f, avecSelection && f.id === selR.current, e, rayonPoignee()));
  }
  const redessiner = () => { cancelAnimationFrame(raf.current); raf.current = requestAnimationFrame(() => dessiner()); };
  useEffect(() => { if (pret) dessiner(); }, [pret, formes, traits, selectionId, vue, cadre]);

  function pousserHisto() {
    histo.current.push({ formes: formesR.current, traits: traitsR.current });
    if (histo.current.length > 60) histo.current.shift();
    setNbHisto(histo.current.length);
  }
  function defaire() {
    const h = histo.current.pop();
    setNbHisto(histo.current.length);
    if (!h) return;
    setFormes(h.formes); setTraits(h.traits); setSelectionId(null);
  }

  function versImage(ev) {
    const c = canvasRef.current, r = c.getBoundingClientRect();
    return { x: (ev.clientX - r.left) * c.width / r.width, y: (ev.clientY - r.top) * c.height / r.height };
  }
  function versScene(p) {
    const r = sceneRef.current.getBoundingClientRect();
    return { x: p.x - r.left, y: p.y - r.top };
  }
  function formeTouchee(pt) {
    const tol = tolerance();
    const ctx = canvasRef.current.getContext("2d");
    const liste = formesR.current;
    for (let i = liste.length - 1; i >= 0; i--) {
      const f = liste[i];
      if (f.type === "texte") {
        const { taille, largeur } = mesurerTexte(ctx, f, echelle());
        if (pt.x > f.x - tol && pt.x < f.x + largeur + tol && pt.y > f.y - taille - tol && pt.y < f.y + taille * 0.3 + tol) return f;
      }
      if (f.type === "cercle") {
        const rayon = Math.hypot(f.x2 - f.x1, f.y2 - f.y1);
        if (Math.abs(distPts(pt, { x: f.x1, y: f.y1 }) - rayon) < tol) return f;
      }
      if (f.type === "fleche") {
        const A = { x: f.x1, y: f.y1 }, B = { x: f.x2, y: f.y2 };
        const l2 = distPts(A, B) ** 2 || 1;
        let t = ((pt.x - A.x) * (B.x - A.x) + (pt.y - A.y) * (B.y - A.y)) / l2;
        t = Math.max(0, Math.min(1, t));
        if (distPts(pt, { x: A.x + t * (B.x - A.x), y: A.y + t * (B.y - A.y) }) < tol) return f;
      }
    }
    return null;
  }

  function demarrerDeplacementVue(ev) {
    action.current = { type: "vue", id: ev.pointerId, depart: { x: ev.clientX, y: ev.clientY }, vue0: { ...vueR.current } };
  }
  function debuterOutil(ev) {
    const pt = versImage(ev);
    if (mode === "dessin") {
      action.current = { type: "trait", id: ev.pointerId, points: [pt] };
      redessiner();
    } else if (mode === "fleche" || mode === "cercle") {
      action.current = { type: "forme", id: ev.pointerId, depart: pt, ecran: { x: ev.clientX, y: ev.clientY }, apercu: null };
    } else if (mode === "texte") {
      action.current = { type: "tap-texte", id: ev.pointerId, pt, ecran: { x: ev.clientX, y: ev.clientY } };
    } else if (mode === "selection") {
      const sel = formesR.current.find(f => f.id === selR.current);
      const tolP = rayonPoignee() * 1.8;
      if (sel && (sel.type === "fleche" || sel.type === "cercle")) {
        if (distPts(pt, { x: sel.x1, y: sel.y1 }) < tolP) { action.current = { type: "edition", poignee: "debut", id: ev.pointerId, forme: sel.id }; return; }
        if (distPts(pt, { x: sel.x2, y: sel.y2 }) < tolP) { action.current = { type: "edition", poignee: "fin", id: ev.pointerId, forme: sel.id }; return; }
      }
      if (sel && sel.type === "texte") {
        const { taille, largeur } = mesurerTexte(canvasRef.current.getContext("2d"), sel, echelle());
        if (distPts(pt, { x: sel.x + largeur + 4, y: sel.y + taille * 0.3 }) < tolP) {
          action.current = { type: "edition", poignee: "taille", id: ev.pointerId, forme: sel.id, depart: pt, tailleDepart: taille };
          return;
        }
      }
      const f = formeTouchee(pt);
      setSelectionId(f ? f.id : null);
      if (f) action.current = { type: "edition", poignee: "deplacer", id: ev.pointerId, forme: f.id, depart: pt, formeDepart: { ...f } };
      else demarrerDeplacementVue(ev);
    }
  }

  function surAppui(ev) {
    if (!pret || saisieTexte || enregistrement) return;
    ev.preventDefault();
    if (ev.pointerType === "pen" && !penVuR.current) { penVuR.current = true; setPenVu(true); }
    try { sceneRef.current.setPointerCapture(ev.pointerId); } catch (e) { /* pas bloquant */ }
    pointeurs.current.set(ev.pointerId, { x: ev.clientX, y: ev.clientY, type: ev.pointerType });
    const doigts = [...pointeurs.current.values()].filter(p => p.type === "touch");
    if (ev.pointerType === "touch" && doigts.length >= 2) {
      // Deux doigts : on abandonne le geste en cours et on zoome.
      action.current = null;
      const [a, b] = doigts;
      action.current = { type: "pincer", d0: distPts(a, b) || 1, m0: versScene({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }), vue0: { ...vueR.current } };
      redessiner();
      return;
    }
    if (action.current) return;
    if (ev.pointerType === "touch" && penVuR.current) { demarrerDeplacementVue(ev); return; }
    debuterOutil(ev);
  }

  function surMouvement(ev) {
    if (!pointeurs.current.has(ev.pointerId)) return;
    pointeurs.current.set(ev.pointerId, { x: ev.clientX, y: ev.clientY, type: ev.pointerType });
    const a = action.current;
    if (!a) return;
    if (a.type === "pincer") {
      const doigts = [...pointeurs.current.values()].filter(p => p.type === "touch");
      if (doigts.length < 2) return;
      const [p1, p2] = doigts;
      const m = versScene({ x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 });
      const s = Math.min(6, Math.max(1, a.vue0.s * distPts(p1, p2) / a.d0));
      const k = cadreR.current;
      const cx = (a.m0.x - k.x - a.vue0.tx) / a.vue0.s, cy = (a.m0.y - k.y - a.vue0.ty) / a.vue0.s;
      setVue(s <= 1.01 ? { s: 1, tx: 0, ty: 0 } : { s, tx: m.x - k.x - cx * s, ty: m.y - k.y - cy * s });
      return;
    }
    if (a.id !== ev.pointerId) return;
    if (a.type === "vue") {
      if (a.vue0.s <= 1) return;
      setVue({ ...a.vue0, tx: a.vue0.tx + ev.clientX - a.depart.x, ty: a.vue0.ty + ev.clientY - a.depart.y });
      return;
    }
    const pt = versImage(ev);
    if (a.type === "trait") {
      const evs = ev.getCoalescedEvents ? ev.getCoalescedEvents() : [];
      (evs.length ? evs.map(versImage) : [pt]).forEach(p => a.points.push(p));
      redessiner();
    } else if (a.type === "forme") {
      a.apercu = { id: "_apercu", type: mode, x1: a.depart.x, y1: a.depart.y, x2: pt.x, y2: pt.y };
      redessiner();
    } else if (a.type === "tap-texte") {
      if (Math.hypot(ev.clientX - a.ecran.x, ev.clientY - a.ecran.y) > 12) action.current = null;
    } else if (a.type === "edition") {
      if (!a.historise) { pousserHisto(); a.historise = true; }
      setFormes(prev => prev.map(f => {
        if (f.id !== a.forme) return f;
        if (a.poignee === "debut") return { ...f, x1: pt.x, y1: pt.y };
        if (a.poignee === "fin") return { ...f, x2: pt.x, y2: pt.y };
        if (a.poignee === "taille") return { ...f, taille: Math.max(12, Math.round(a.tailleDepart + (pt.x - a.depart.x))) };
        const dx = pt.x - a.depart.x, dy = pt.y - a.depart.y, b = a.formeDepart;
        if (f.type === "texte") return { ...f, x: b.x + dx, y: b.y + dy };
        return { ...f, x1: b.x1 + dx, y1: b.y1 + dy, x2: b.x2 + dx, y2: b.y2 + dy };
      }));
    }
  }

  function surRelache(ev) {
    pointeurs.current.delete(ev.pointerId);
    const a = action.current;
    if (!a) return;
    if (a.type === "pincer") {
      if ([...pointeurs.current.values()].filter(p => p.type === "touch").length < 2) action.current = null;
      return;
    }
    if (a.id !== ev.pointerId) return;
    action.current = null;
    if (ev.type === "pointercancel") { redessiner(); return; }
    if (a.type === "trait") {
      pousserHisto();
      setTraits(prev => [...prev, a.points]);
    } else if (a.type === "forme") {
      if (a.apercu && Math.hypot(ev.clientX - a.ecran.x, ev.clientY - a.ecran.y) > 8) {
        pousserHisto();
        setFormes(prev => [...prev, { ...a.apercu, id: `f-${Date.now()}` }]);
      } else redessiner();
    } else if (a.type === "tap-texte") {
      setTexte("");
      setSaisieTexte(a.pt);
    }
  }

  function validerTexte() {
    const t = texte.trim();
    if (t) {
      pousserHisto();
      setFormes(prev => [...prev, { id: `f-${Date.now()}`, type: "texte", x: saisieTexte.x, y: saisieTexte.y, texte: t, taille: Math.round(34 * echelle()) }]);
    }
    setSaisieTexte(null);
  }
  function supprimerSelection() {
    pousserHisto();
    setFormes(prev => prev.filter(f => f.id !== selectionId));
    setSelectionId(null);
  }
  function choisirOutil(m) { setMode(m); if (m !== "selection") setSelectionId(null); }

  async function enregistrer() {
    setEnregistrement(true);
    try {
      const c = canvasRef.current, e = echelle();
      // Base : photo + traits libres (sans les formes, qui restent modifiables).
      const base = document.createElement("canvas");
      base.width = c.width; base.height = c.height;
      const bctx = base.getContext("2d");
      bctx.drawImage(imageRef.current, 0, 0);
      dessinerTraits(bctx, traitsR.current, e);
      const blobBase = await canvasVersBlob(base, 0.9);
      // Image complète, telle qu'elle apparaîtra dans le compte-rendu.
      const comp = document.createElement("canvas");
      comp.width = c.width; comp.height = c.height;
      const cctx = comp.getContext("2d");
      cctx.drawImage(base, 0, 0);
      formesR.current.forEach(f => dessinerFormeAnnot(cctx, f, false, e, 0));
      const blobAnnotee = await canvasVersBlob(comp, 0.85);
      await onEnregistrer(blobAnnotee, blobBase, formesR.current);
    } catch (err) {
      setErreur("L'annotation n'a pas pu être enregistrée : " + (err.message || "erreur inconnue"));
      setEnregistrement(false);
    }
  }
  function fermer() {
    if (nbHisto > 0 && !window.confirm("Quitter sans enregistrer l'annotation ?")) return;
    onFermer();
  }

  const outils = [["dessin", "Crayon"], ["fleche", "Flèche"], ["cercle", "Cercle"], ["texte", "Texte"], ["selection", "Modifier"]];
  return (
    <div className="annotateur" role="dialog" aria-label="Annoter la photo">
      <div className="annot-haut">
        <button type="button" className="annot-btn" onClick={fermer}>Fermer</button>
        <button type="button" className="annot-btn" onClick={defaire} disabled={nbHisto === 0}>Défaire</button>
        <span className="annot-espace" />
        <button type="button" className="btn-principal annot-enreg" onClick={enregistrer} disabled={!pret || enregistrement}>{enregistrement ? "Enregistrement…" : "Enregistrer"}</button>
      </div>
      <div
        ref={sceneRef} className="annot-scene"
        onPointerDown={surAppui} onPointerMove={surMouvement} onPointerUp={surRelache} onPointerCancel={surRelache}
      >
        <div className="annot-cadre" style={{ left: cadre.x, top: cadre.y, width: cadre.l, height: cadre.h, transform: `translate(${vue.tx}px, ${vue.ty}px) scale(${vue.s})` }}>
          <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />
        </div>
        {!pret && !erreur && <p className="annot-message">Ouverture de la photo…</p>}
        {erreur && <p className="annot-message erreur-claire">{erreur}</p>}
      </div>
      <div className="annot-bas">
        <div className="annot-outils" role="group" aria-label="Outils">
          {outils.map(([m, l]) => (
            <button type="button" key={m} className={"annot-outil" + (mode === m ? " actif" : "")} aria-pressed={mode === m} onClick={() => choisirOutil(m)}>{l}</button>
          ))}
        </div>
        <div className="annot-infos">
          {mode === "selection" && selectionId
            ? <button type="button" className="annot-btn annot-suppr" onClick={supprimerSelection}>Supprimer l'élément</button>
            : <span>{mode === "texte" ? "Touchez la photo là où écrire." : mode === "selection" ? "Touchez une flèche, un cercle ou un texte." : penVu ? "Le stylet dessine, le doigt déplace. Deux doigts pour zoomer." : "Deux doigts pour zoomer."}</span>}
          {vue.s > 1 && <button type="button" className="annot-btn" onClick={() => setVue({ s: 1, tx: 0, ty: 0 })}>Recadrer</button>}
        </div>
      </div>
      {saisieTexte && (
        <div className="annot-texte">
          <input autoFocus className="champ" placeholder="Texte à écrire sur la photo" value={texte} onChange={e => setTexte(e.target.value)} onKeyDown={e => { if (e.key === "Enter") validerTexte(); }} />
          <div className="annot-texte-boutons">
            <button type="button" className="btn-secondaire" onClick={() => setSaisieTexte(null)}>Annuler</button>
            <button type="button" className="btn-principal" onClick={validerTexte}>Ajouter</button>
          </div>
        </div>
      )}
    </div>
  );
}


/* ------------------------- Enregistrement (étape 1e) ------------------- */
// Reproduit genererCompteRendu() de l'appli web : même PDF, même dépôt
// (documents/{chantier}/08-chantier/comptes-rendus), même ajout dans la
// fiche, même ligne d'historique. Différence voulue : la fiche est RELUE
// juste avant d'écrire, et on n'y ajoute que le compte-rendu, pour ne
// jamais écraser ce que le bureau a modifié entre-temps.
async function chargerParametresPDF() {
  const { data, error } = await supabase.from("parametres").select("cle, valeurs").in("cle", ["societe", "logoContrat"]);
  if (error) throw error;
  const v = cle => (data || []).find(d => d.cle === cle)?.valeurs || null;
  return { societe: v("societe"), logoContrat: v("logoContrat") };
}
function echapper(t) {
  return String(t ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
const texteMultiligne = t => echapper(t).replace(/\n/g, "<br/>");
const dateFr = s => s ? new Date(s).toLocaleDateString("fr-FR") : "";

// Une étape qui ne répond plus finit en erreur claire au lieu de bloquer.
function avecDelai(promesse, ms, message) {
  let minuteur;
  return Promise.race([
    promesse,
    new Promise((_, rej) => { minuteur = setTimeout(() => rej(new Error(message)), ms); }),
  ]).finally(() => clearTimeout(minuteur));
}

/* PDF dessiné directement avec jsPDF (texte, cadres, photos), sans passer
   par la conversion d'une page HTML : celle-ci se bloque sur iPhone.
   Mise en page reprise du PDF du web : mêmes blocs, mêmes couleurs, mêmes
   tailles (les tailles du web sont en pixels d'une page de 794 px de large,
   ramenées à 515 points de largeur utile, comme le fait le web). */
const ECH_PDF = 515 / 794;
const pxPdf = n => n * ECH_PDF;
const WINANSI_EXTRA = new Set("ŒœŠšŸŽžƒˆ˜–—‘’‚“”„†‡•…‰‹›€™".split(""));
// Les polices de base du PDF ne connaissent que l'alphabet occidental :
// on retire ce qu'elles ne savent pas afficher (émojis…).
function textePdf(t) {
  return Array.from(String(t ?? "").replace(/\r/g, "").replace(/\t/g, " ")).filter(c => {
    const k = c.codePointAt(0);
    return k === 10 || (k >= 32 && k <= 126) || (k >= 160 && k <= 255) || WINANSI_EXTRA.has(c);
  }).join("");
}
function chargerImage(src) {
  return new Promise((res, rej) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.onload = () => res(img);
    img.onerror = () => rej(new Error("image illisible"));
    img.src = src;
  });
}
// Recadre comme « object-fit: cover » du web et réduit à la taille utile.
async function imagePourPdf(src, largeurMax, ratio, fond) {
  const img = await avecDelai(chargerImage(src), 20000, "image trop longue à charger");
  let sx = 0, sy = 0, sw = img.naturalWidth, sh = img.naturalHeight;
  if (ratio) {
    if (sh / sw > ratio) { sh = sw * ratio; sy = (img.naturalHeight - sh) / 2; }
    else { sw = sh / ratio; sx = (img.naturalWidth - sw) / 2; }
  }
  const l = Math.round(Math.min(largeurMax, sw)), h = Math.round(l * sh / sw);
  const c = document.createElement("canvas");
  c.width = l; c.height = h;
  const ctx = c.getContext("2d");
  if (fond) { ctx.fillStyle = fond; ctx.fillRect(0, 0, l, h); }
  ctx.drawImage(img, sx, sy, sw, sh, 0, 0, l, h);
  return { data: c.toDataURL("image/jpeg", 0.85), l, h };
}

function creerPdfCompteRendu({ jsPDF, fd, nomProjet, numero, b, checkliste, entrepriseDe, societe, logo, imagesPhotos }) {
  const doc = new jsPDF({ orientation: "p", unit: "pt", format: "a4" });
  const PH = doc.internal.pageSize.getHeight(), PW = doc.internal.pageSize.getWidth();
  const MX = 40, MT = 40, BAS = PH - 50, W = PW - 2 * MX;
  let y = MT;
  const ENCRE = "#1A1D23";
  const police = (taille, gras, couleur = ENCRE) => { doc.setFont("helvetica", gras ? "bold" : "normal"); doc.setFontSize(pxPdf(taille)); doc.setTextColor(couleur); };
  const ih = taille => pxPdf(taille) * 1.5;
  const decouper = (t, taille, gras, larg) => { police(taille, gras); return doc.splitTextToSize(textePdf(t), larg); };
  const ecrire = (ligne, x, yy, taille, opts) => doc.text(ligne, x, yy + (ih(taille) - pxPdf(taille)) / 2, { baseline: "top", ...opts });
  const nouvellePage = () => { doc.addPage(); y = MT; };
  const espace = h => ({ h, draw() {} });

  function paragraphe(t, { taille = 11, gras = false, couleur = ENCRE, align = "left", larg = W, marge = 0 } = {}) {
    const ls = decouper(t, taille, gras, larg);
    return { h: ls.length * ih(taille) + marge, draw(x, yy) {
      police(taille, gras, couleur);
      ls.forEach((l, i) => ecrire(l, align === "center" ? x + larg / 2 : x, yy + i * ih(taille), taille, align === "center" ? { align: "center" } : {}));
    } };
  }
  // « Libellé : valeur » sur la même ligne, la valeur revenant à la ligne si besoin.
  function libelleValeur(libelle, valeur, { taille = 12, larg = W, marge = 0 } = {}) {
    police(taille, true);
    const lw = doc.getTextWidth(textePdf(libelle) + " ");
    const v = textePdf(valeur);
    police(taille, false);
    const premiere = doc.splitTextToSize(v, Math.max(20, larg - lw));
    const l1 = premiere[0] || "";
    const reste = v.startsWith(l1) ? v.slice(l1.length).replace(/^\s+/, "") : premiere.slice(1).join(" ");
    const suite = reste ? doc.splitTextToSize(reste, larg) : [];
    return { h: (1 + suite.length) * ih(taille) + marge, draw(x, yy) {
      police(taille, true); ecrire(textePdf(libelle), x, yy, taille);
      police(taille, false); ecrire(l1, x + lw, yy, taille);
      suite.forEach((l, i) => ecrire(l, x, yy + (i + 1) * ih(taille), taille));
    } };
  }
  // Libellé en gras, puis le texte en dessous (« Commentaires : » + retour à la ligne).
  function libelleTexte(libelle, texte, { taille = 11, larg = W, marge = 0 } = {}) {
    const a = paragraphe(libelle, { taille, gras: true, larg }), c = paragraphe(texte, { taille, larg });
    return { h: a.h + c.h + marge, draw(x, yy) { a.draw(x, yy); c.draw(x, yy + a.h); } };
  }
  const couleurConf = c => c === "oui" ? "#2E7D32" : c === "non" ? "#B5552F" : "#999999";
  const libelleConf = c => c === "oui" ? "OUI" : c === "non" ? "NON" : "Non renseigné";
  // Un groupe de la check-list : titre à gauche, « Semaine — Conforme » à droite, puis les points.
  function boiteGroupe({ titre, segments, puces, texte, fond, larg }) {
    const padV = pxPdf(6), padH = pxPdf(8), marge = pxPdf(6), interieur = larg - 2 * padH;
    let statutL = 0;
    segments.forEach(s => { police(10, s.gras); statutL += doc.getTextWidth(textePdf(s.t)); });
    const titreLs = titre ? decouper(titre, 10.5, true, segments.length ? interieur - statutL - pxPdf(8) : interieur) : [];
    const tete = Math.max(titreLs.length * ih(10.5), segments.length ? ih(10) : 0);
    const pucesLs = (puces || []).map(p => decouper(p, 10, false, interieur - pxPdf(16)));
    const pucesH = pucesLs.reduce((s, ls) => s + ls.length * ih(10), 0) + (pucesLs.length ? pxPdf(2) : 0);
    const texteLs = texte ? decouper(texte, 10, false, interieur) : [];
    const texteH = texteLs.length ? texteLs.length * ih(10) + pxPdf(2) : 0;
    const hBoite = 2 * padV + tete + pucesH + texteH;
    return { h: hBoite + 2 * marge, draw(x, yy) {
      const top = yy + marge;
      doc.setFillColor(fond);
      doc.roundedRect(x, top, larg, hBoite, pxPdf(4), pxPdf(4), "F");
      let cy = top + padV;
      police(10.5, true);
      titreLs.forEach((l, i) => ecrire(l, x + padH, cy + i * ih(10.5), 10.5));
      if (segments.length) {
        let sx = x + larg - padH - statutL;
        segments.forEach(s => { police(10, s.gras, s.c || ENCRE); const t = textePdf(s.t); ecrire(t, sx, cy, 10); sx += doc.getTextWidth(t); });
      }
      cy += tete + (pucesLs.length || texteLs.length ? pxPdf(2) : 0);
      police(10, false, "#444444");
      pucesLs.forEach(ls => {
        ecrire("•", x + padH + pxPdf(5), cy, 10);
        ls.forEach(l => { ecrire(l, x + padH + pxPdf(16), cy, 10); cy += ih(10); });
      });
      texteLs.forEach(l => { ecrire(l, x + padH, cy, 10); cy += ih(10); });
    } };
  }
  // Photos en ligne, comme sur le web : 120×90 (annotée : 220×165), marges 4/6 px.
  function lignesPhotos(photos, larg, margeHaut = 0) {
    const rangs = []; let rang = [], l = 0;
    photos.forEach(p => {
      const img = imagesPhotos.get(p);
      if (!img) return;
      const w = pxPdf(p.annotee ? 220 : 120), h = pxPdf(p.annotee ? 165 : 90);
      if (rang.length && l + w > larg) { rangs.push(rang); rang = []; l = 0; }
      rang.push({ img, w, h }); l += w + pxPdf(6);
    });
    if (rang.length) rangs.push(rang);
    return rangs.map((r, i) => {
      const hr = Math.max(...r.map(e => e.h)) + pxPdf(10) + (i === 0 ? margeHaut : 0);
      return { h: hr, draw(x, yy) {
        let cx = x;
        const top = yy + pxPdf(4) + (i === 0 ? margeHaut : 0);
        r.forEach(e => { doc.addImage(e.img.data, "JPEG", cx, top, e.w, e.h); cx += e.w + pxPdf(6); });
      } };
    });
  }
  function placer(e) {
    if (y + e.h > BAS && y > MT) nouvellePage();
    e.draw(MX, y); y += e.h;
  }
  // Bloc encadré ; « page-break-inside: avoid » comme sur le web.
  // « avant » : un titre à garder sur la même page que le bloc qui le suit.
  function placerBloc(enfants, { pad, fond, bord, margeBas, avant }) {
    const h = enfants.reduce((s, e) => s + e.h, 0) + 2 * pad;
    const hAvant = avant ? avant.h : 0;
    if (y + hAvant + h > BAS && hAvant + h <= BAS - MT && y > MT) nouvellePage();
    else if (avant && y + hAvant + Math.min(h, 120) > BAS && y > MT) nouvellePage();
    if (avant) { avant.draw(MX, y); y += hAvant; }
    if (y + h <= BAS) {
      if (fond) doc.setFillColor(fond);
      if (bord) { doc.setDrawColor(bord); doc.setLineWidth(pxPdf(1)); }
      doc.roundedRect(MX, y, W, h, pxPdf(6), pxPdf(6), fond && bord ? "FD" : fond ? "F" : "S");
      let yy = y + pad;
      enfants.forEach(e => { e.draw(MX + pad, yy); yy += e.h; });
      y += h + margeBas;
    } else {
      // Bloc plus long qu'une page : il continue sur la suivante, sans cadre.
      y += pad;
      enfants.forEach(e => { if (y + e.h > BAS && y > MT) nouvellePage(); e.draw(MX + pad, y); y += e.h; });
      y += pad + margeBas;
    }
  }

  // ---- En-tête (bandeau noir, comme le web) ----
  const s = societe || {};
  const lignesAdresse = [s.adresse || "", s.codePostalVille || "", [s.telephone ? `Tél. ${s.telephone}` : "", s.email ? `E-mail ${s.email}` : ""].filter(Boolean).join(" — ")].filter(Boolean);
  {
    const pad = pxPdf(10), padH = pxPdf(16);
    const logoH = logo ? pxPdf(34) : 0, logoW = logo ? logoH * logo.l / logo.h : 0;
    const texteH = ih(13) + lignesAdresse.length * ih(9);
    const h = 2 * pad + Math.max(logoH, texteH);
    doc.setFillColor("#0d0d0d");
    doc.rect(MX, y, W, h, "F");
    let tx = MX + padH;
    if (logo) { doc.addImage(logo.data, "JPEG", tx, y + (h - logoH) / 2, logoW, logoH); tx += logoW + pxPdf(14); }
    let ty = y + (h - texteH) / 2;
    police(13, true, "#FFFFFF"); ecrire(textePdf(s.raisonSociale || ""), tx, ty, 13); ty += ih(13);
    police(9, false, "#C9A227"); lignesAdresse.forEach(l => { ecrire(textePdf(l), tx, ty, 9); ty += ih(9); });
    y += h + pxPdf(18);
  }

  // ---- Titre et informations du chantier ----
  const mo1 = fd.mo1 || {}, mo2 = fd.mo2 || {};
  const nomsMO = [mo1.nom ? `${mo1.civilite || ""} ${mo1.nom} ${mo1.prenom || ""}`.trim() : "", fd.mo2Actif && mo2.nom ? `${mo2.civilite || ""} ${mo2.nom} ${mo2.prenom || ""}`.trim() : ""].filter(Boolean).join(" & ");
  const t = fd.terrain || {};
  const adresseChantier = [t.adresseProjet, t.codePostal, t.ville].filter(Boolean).join(" ");
  const conducteur = fd.interne?.conducteurTravaux || "";
  placer(paragraphe(`COMPTE RENDU VISITE DE CHANTIER N°${numero}`, { taille: 16, gras: true, align: "center", marge: pxPdf(4) }));
  placer(paragraphe(`${nomProjet} — visite du ${dateFr(b.dateVisite)}`, { taille: 12, couleur: "#666666", align: "center", marge: pxPdf(16) }));
  placer(libelleValeur("Maître(s) d'ouvrage :", nomsMO || "—", { marge: pxPdf(4) }));
  placer(libelleValeur("Adresse du chantier :", adresseChantier || "—", { marge: pxPdf(4) }));
  placer(libelleValeur("Conducteur de travaux :", conducteur || b.auteur || "—", { marge: pxPdf(16) }));

  // ---- Compte rendu général ----
  const padBloc = pxPdf(12), interieur = W - 2 * padBloc;
  const general = [paragraphe("COMPTE RENDU GÉNÉRAL DE LA VISITE", { taille: 13, gras: true, larg: interieur, marge: pxPdf(8) })];
  if (b.commentairesGeneraux) general.push(libelleTexte("Commentaires :", b.commentairesGeneraux, { larg: interieur, marge: pxPdf(8) }));
  if (b.prochaineVisite) general.push(libelleValeur("Prochaine visite de chantier prévue le :", dateFr(b.prochaineVisite), { taille: 11, larg: interieur, marge: pxPdf(6) }));
  general.push(...lignesPhotos(b.photosGenerales || [], interieur));
  if (b.indicationsProchaineVisite) general.push(libelleTexte("Indications pour la prochaine visite (artisans à convoquer…) :", b.indicationsProchaineVisite, { larg: interieur }));
  placerBloc(general, { pad: padBloc, fond: "#FBF9F6", bord: "#DDDDDD", margeBas: pxPdf(16) });

  // ---- Observations par corps de métier ----
  const titreObservations = paragraphe("OBSERVATIONS PAR CORPS DE MÉTIER", { taille: 13, gras: true, marge: pxPdf(8) });
  b.lotsChoisis.forEach((nom, indexLot) => {
    const d = b.detailParLot[nom] || {};
    const entreprise = entrepriseDe(nom);
    const enfants = [paragraphe(`${nom}${entreprise ? ` — ${entreprise}` : ""}`, { taille: 13, gras: true, larg: interieur })];
    (checkliste[nom] || []).forEach((g, i) => {
      const r = d.groupes?.[i] || {};
      if (r.retire) return;
      const segments = [];
      if (groupeAvecSemaine(g)) segments.push({ t: "Semaine : " }, { t: r.semaine || "—", gras: true });
      if (groupeAvecConforme(g)) {
        if (segments.length) segments.push({ t: " — " });
        segments.push({ t: "Conforme : " }, { t: libelleConf(r.conforme), gras: true, c: couleurConf(r.conforme) });
      }
      const puces = (r.itemsModifies || g.items || []).filter(it => (it || "").trim());
      enfants.push(boiteGroupe({ titre: g.titre, segments, puces, fond: "#FAFAFA", larg: interieur }));
    });
    (d.groupesPerso || []).filter(g => (g.item || "").trim()).forEach(g => {
      enfants.push(boiteGroupe({
        titre: g.titre, texte: g.item, fond: "#F5F0E8", larg: interieur,
        segments: [{ t: "Semaine : " }, { t: g.semaine || "—", gras: true }, { t: " — Conforme : " }, { t: libelleConf(g.conforme), gras: true, c: couleurConf(g.conforme) }],
      }));
    });
    if (d.corrections) enfants.push(espace(pxPdf(8)), libelleTexte("Corrections à apporter / à porter à la connaissance de l'entreprise :", d.corrections, { larg: interieur }));
    enfants.push(...lignesPhotos(d.photos || [], interieur, pxPdf(4)));
    placerBloc(enfants, { pad: padBloc, bord: "#DDDDDD", margeBas: pxPdf(18), avant: indexLot === 0 ? titreObservations : null });
  });

  // ---- Pied (bandeau noir) ----
  const piedTexte = [s.formeJuridique || "", s.raisonSociale || "", s.codeApe ? `Activités d'architecture ${s.codeApe}` : "", s.rcs ? `RCS ${s.rcs}` : "", s.siret ? `SIRET ${s.siret}` : ""].filter(Boolean).join(" — ");
  {
    const ls = decouper(piedTexte || " ", 9, false, W - 2 * pxPdf(16));
    const h = 2 * pxPdf(8) + ls.length * pxPdf(9) * 1.4;
    y += pxPdf(24);
    if (y + h > BAS) nouvellePage();
    doc.setFillColor("#0d0d0d");
    doc.rect(MX, y, W, h, "F");
    police(9, false, "#999999");
    ls.forEach((l, i) => doc.text(l, MX + W / 2, y + pxPdf(8) + i * pxPdf(9) * 1.4, { baseline: "top", align: "center" }));
  }
  return doc.output("blob");
}

async function genererPdf(donnees, photos, logoUrl, etape) {
  etape("Création du PDF : chargement de l'outil…");
  const { jsPDF } = await avecDelai(import("jspdf"), 45000, "L'outil de création du PDF n'a pas pu être chargé (réseau trop faible ?).");
  etape("Création du PDF : préparation des photos…");
  const imagesPhotos = new Map();
  for (const { photo, src } of photos) {
    try {
      // Taille utile : environ 2,5 fois la taille imprimée, largement suffisant.
      imagesPhotos.set(photo, await imagePourPdf(src, photo.annotee ? 360 : 240, 0.75));
    } catch (e) { /* photo illisible : on continue sans elle */ }
  }
  let logo = null;
  if (logoUrl) { try { logo = await imagePourPdf(logoUrl, 400, null, "#0d0d0d"); } catch (e) { /* sans logo */ } }
  etape("Création du PDF : mise en page…");
  await new Promise(r => setTimeout(r, 30)); // laisse l'écran se mettre à jour
  return creerPdfCompteRendu({ jsPDF, ...donnees, logo, imagesPhotos });
}

async function deposerBlobMobile(blob, nomFichier, dossier, type) {
  const chemin = `${dossier}/${Date.now()}-${nomFichier}`;
  const { error } = await supabase.storage.from("APS").upload(chemin, blob, { contentType: type });
  if (error) throw error;
  const { data, error: err2 } = await supabase.storage.from("APS").createSignedUrl(chemin, 60 * 60 * 24 * 365 * 5);
  if (err2) throw err2;
  return { url: data.signedUrl, nom: nomFichier };
}
// Photo telle qu'enregistrée dans le compte-rendu : même forme que le web.
function photoFinale(p) {
  const f = { url: p.url, nom: p.nom };
  if (p.annotee) { f.annotee = true; f.formes = p.formes || []; }
  return f;
}

async function enregistrerCompteRendu({ chantier, b, checkliste, infosLots, etape }) {
  if (!navigator.onLine) throw new Error("Pas de réseau. Le compte-rendu reste en brouillon sur l'appareil : enregistrez-le dès que le réseau revient.");
  if (b.lotsChoisis.length === 0) throw new Error("Cochez au moins un lot concerné par cette visite.");

  etape("Envoi des photos…");
  await Promise.all(toutesPhotos(b).filter(p => p.idLocal && !p.url).map(p => envoyerPhoto(chantier.id, p.idLocal, p.nom)));
  const bb = lireBrouillon(chantier.id) || b;
  const manquantes = toutesPhotos(bb).filter(p => !p.url);
  if (manquantes.length) throw new Error(`${manquantes.length} photo(s) n'ont pas pu être envoyées. Vérifiez le réseau et réessayez.`);

  etape("Lecture de la fiche du chantier…");
  const lecture = await supabase.from("projets").select("id, nom, fiche_details").eq("id", chantier.id).single();
  if (lecture.error) throw lecture.error;
  const fd = lecture.data.fiche_details || {};
  const numero = (fd.comptesRendus || []).length + 1;
  const infos = infosLots || await chargerLotsEtOffres(chantier.id);
  const entrepriseDe = nom => intervenantUniquePourCorpsEtat(nom, infos.lots, infos.offres);
  const { societe, logoContrat } = await chargerParametresPDF().catch(() => ({ societe: null, logoContrat: null }));

  etape("Création du PDF…");
  // Dans le PDF, les photos viennent de l'appareil (plus rapide, moins de données).
  const liens = [];
  const photos = [];
  for (const p of toutesPhotos(bb)) {
    const blob = p.idLocal ? await photoLocaleLire(p.idLocal).catch(() => null) : null;
    const src = blob ? URL.createObjectURL(blob) : p.url;
    if (blob) liens.push(src);
    photos.push({ photo: p, src });
  }
  let pdf;
  try {
    pdf = await genererPdf({
      fd, nomProjet: lecture.data.nom || chantier.nom, numero, b: bb, checkliste, entrepriseDe, societe,
    }, photos, logoContrat?.url, etape);
  } finally {
    liens.forEach(u => URL.revokeObjectURL(u));
  }

  etape("Dépôt du PDF…");
  const nomFichier = `CR_N${numero}_${bb.dateVisite}_${(lecture.data.nom || "chantier").replace(/[^a-zA-Z0-9_-]+/g, "_")}.pdf`;
  const fichier = await deposerBlobMobile(pdf, nomFichier, `documents/${chantier.id}/08-chantier/comptes-rendus`, "application/pdf");

  etape("Enregistrement dans la fiche du chantier…");
  const nouveauCr = {
    id: `cr-${Date.now()}`, numero, date: bb.dateVisite, auteur: bb.auteur || fd.interne?.conducteurTravaux || "",
    lots: bb.lotsChoisis.map(nom => {
      const d = bb.detailParLot[nom] || {};
      return { nom, ...d, photos: (d.photos || []).map(photoFinale) };
    }),
    commentairesGeneraux: bb.commentairesGeneraux, prochaineVisite: bb.prochaineVisite,
    indicationsProchaineVisite: bb.indicationsProchaineVisite, photosGenerales: (bb.photosGenerales || []).map(photoFinale),
    fichierUrl: fichier.url, fichierNom: fichier.nom,
  };
  // Relecture au dernier moment : on part de la fiche telle qu'elle est en
  // base à cet instant et on n'y ajoute que le compte-rendu et son PDF.
  const fraiche = await supabase.from("projets").select("fiche_details, historique").eq("id", chantier.id).single();
  if (fraiche.error) throw fraiche.error;
  const fd2 = fraiche.data.fiche_details || {};
  const arbo = fd2.documentsArbo || {};
  const nouvelleFiche = {
    ...fd2,
    documentsArbo: { ...arbo, "08-chantier/comptes-rendus": [...(arbo["08-chantier/comptes-rendus"] || []), { ...fichier, date: bb.dateVisite }] },
    comptesRendus: [...(fd2.comptesRendus || []), nouveauCr],
  };
  const maj = await supabase.from("projets").update({ fiche_details: nouvelleFiche }).eq("id", chantier.id);
  if (maj.error) throw maj.error;

  // Historique du dossier : secondaire, ne bloque pas (comme sur le web).
  try {
    const historique = [...(fraiche.data.historique || []), {
      date: new Date().toISOString().slice(0, 10), action: "Compte-rendu de visite généré",
      detail: `${dateFr(bb.dateVisite)} — ${bb.lotsChoisis.join(", ")}`,
    }];
    await supabase.from("projets").update({ historique }).eq("id", chantier.id);
  } catch (e) { /* pas bloquant */ }

  // Nettoyage de l'appareil : copies des photos et brouillon.
  toutesPhotos(bb).forEach(p => { if (p.idLocal) photoLocaleSupprimer(p.idLocal); if (p.idBase) photoLocaleSupprimer(p.idBase); });
  supprimerBrouillon(chantier.id);
  return nouveauCr;
}

/* ------------------------------ Connexion ------------------------------ */
function EcranConnexion() {
  const [email, setEmail] = useState("");
  const [motDePasse, setMotDePasse] = useState("");
  const [enCours, setEnCours] = useState(false);
  const [erreur, setErreur] = useState("");

  async function seConnecter(e) {
    e.preventDefault();
    setErreur("");
    setEnCours(true);
    const { error } = await supabase.auth.signInWithPassword({ email: email.trim(), password: motDePasse });
    setEnCours(false);
    if (error) {
      setErreur(/invalid/i.test(error.message)
        ? "E-mail ou mot de passe incorrect. Ce sont les mêmes que pour l'appli web."
        : /fetch|network/i.test(error.message)
          ? "Pas de connexion réseau. La première connexion doit se faire avec du réseau."
          : "Connexion refusée : " + error.message);
    }
  }

  return (
    <div className="connexion">
      <form className="connexion-carte" onSubmit={seConnecter}>
        <div className="marque">IDHOUSECONCEPT</div>
        <h1>Comptes-rendus de visite</h1>
        <label>
          <span>E-mail</span>
          <input type="email" autoComplete="username" inputMode="email" value={email} onChange={e => setEmail(e.target.value)} required />
        </label>
        <label>
          <span>Mot de passe</span>
          <input type="password" autoComplete="current-password" value={motDePasse} onChange={e => setMotDePasse(e.target.value)} required />
        </label>
        {erreur && <p className="erreur">{erreur}</p>}
        <button className="btn-principal" type="submit" disabled={enCours}>{enCours ? "Connexion…" : "Se connecter"}</button>
        <p className="aide">Mot de passe oublié : réinitialisez-le depuis l'appli web.</p>
      </form>
    </div>
  );
}

/* ------------------------------ Accueil -------------------------------- */
// Écran de base : « Nouveau compte-rendu », les brouillons en cours, puis
// tous les comptes-rendus (tous chantiers), comme l'écran du web.
function EcranAccueil({ chantiers, onNouveau, onBrouillon, onOuvrirCR }) {
  const [recherche, setRecherche] = useState("");
  const [conducteur, setConducteur] = useStockageLocal("idh-mobile-conducteur", "");
  const [nbAffiches, setNbAffiches] = useState(40);
  const brouillons = chantiers.map(c => ({ c, b: lireBrouillon(c.id) })).filter(x => x.b)
    .sort((x, y) => (y.b.majLe || "").localeCompare(x.b.majLe || ""));
  const tous = useMemo(() => {
    const l = [];
    chantiers.forEach(c => c.comptesRendus.forEach(cr => l.push({ cr, c })));
    return l.sort((a, b) => (b.cr.date || "").localeCompare(a.cr.date || "") || (b.cr.numero || 0) - (a.cr.numero || 0));
  }, [chantiers]);
  const auteurs = useMemo(() => [...new Set([...tous.map(x => x.cr.auteur), ...chantiers.map(c => c.conducteur)].filter(Boolean))].sort(), [tous, chantiers]);
  const visibles = tous.filter(({ cr, c }) => {
    if (conducteur && cr.auteur !== conducteur && c.conducteur !== conducteur) return false;
    if (!recherche.trim()) return true;
    return normaliser(`${c.nom} ${c.client} ${(cr.lots || []).map(l => l.nom).join(" ")}`).includes(normaliser(recherche.trim()));
  });
  return (
    <div className="colonne">
      <button className="btn-principal btn-large btn-nouveau btn-accueil" onClick={onNouveau}>Nouveau compte-rendu</button>

      {brouillons.length > 0 && (
        <section className="section-accueil">
          <h2 className="titre-section">Brouillons en cours</h2>
          <ul className="liste">
            {brouillons.map(({ c, b }) => (
              <li key={c.id}>
                <button className="ligne ligne-brouillon" onClick={() => onBrouillon(c.id)}>
                  <span className="ligne-titre">{c.nom}</span>
                  <span className="ligne-sous">{[c.client, `visite du ${dateLongue(b.dateVisite)}`].filter(Boolean).join(", ")}</span>
                  <span className="lots-resume">{b.lotsChoisis?.length ? `${b.lotsChoisis.length} corps de métier` : "Rien de saisi"}{b.majLe ? `. Modifié le ${heureCourte(b.majLe)}.` : ""}</span>
                </button>
              </li>
            ))}
          </ul>
        </section>
      )}

      <section className="section-accueil">
        <h2 className="titre-section">Comptes-rendus enregistrés</h2>
        <div className="filtres">
          <input className="recherche" type="search" placeholder="Rechercher un chantier, un client, un lot" value={recherche} onChange={e => setRecherche(e.target.value)} />
          {auteurs.length > 0 && (
            <select value={conducteur} onChange={e => setConducteur(e.target.value)} aria-label="Conducteur de travaux">
              <option value="">Tous les conducteurs</option>
              {auteurs.map(n => <option key={n} value={n}>{n}</option>)}
            </select>
          )}
        </div>
        {visibles.length === 0 ? (
          <p className="vide">{tous.length === 0 ? "Aucun compte-rendu enregistré pour l'instant." : "Aucun compte-rendu ne correspond. Videz la recherche ou choisissez « Tous les conducteurs »."}</p>
        ) : (
          <ul className="liste">
            {visibles.slice(0, nbAffiches).map(({ cr, c }) => {
              const tr = c.transmissionsCR.filter(t => t.crId === cr.id);
              return (
                <li key={c.id + (cr.id || cr.numero)}>
                  <button className="ligne" onClick={() => onOuvrirCR(c.id, cr.id)}>
                    <span className="ligne-titre">{c.nom}, CR n°{cr.numero || "?"}</span>
                    <span className="ligne-sous">{dateLongue(cr.date)}{cr.auteur ? `, par ${cr.auteur}` : ""}{c.client ? ` · ${c.client}` : ""}</span>
                    <span className="lots-resume">{(cr.lots || []).map(l => l.nom).join(", ") || "Aucun lot"}</span>
                    {tr.length > 0 && <span className="pastille pastille-ok">Transmis le {dateCourte(tr[tr.length - 1].date)}</span>}
                  </button>
                </li>
              );
            })}
          </ul>
        )}
        {visibles.length > nbAffiches && <button className="btn-secondaire btn-large" onClick={() => setNbAffiches(n => n + 40)}>Afficher plus</button>}
      </section>
    </div>
  );
}

/* --------------------------- Choix du chantier ------------------------- */
function ChoixChantier({ chantiers, onChoisir }) {
  const [recherche, setRecherche] = useState("");
  const [conducteur, setConducteur] = useStockageLocal("idh-mobile-conducteur", "");
  const conducteurs = useMemo(() => [...new Set(chantiers.map(c => c.conducteur).filter(Boolean))].sort(), [chantiers]);
  const visibles = useMemo(() => {
    let liste = chantiers;
    if (conducteur) liste = liste.filter(c => c.conducteur === conducteur);
    if (recherche.trim()) {
      const r = normaliser(recherche.trim());
      liste = liste.filter(c => normaliser(`${c.nom} ${c.client} ${c.agence} ${c.conducteur}`).includes(r));
    }
    // Brouillons d'abord, puis les chantiers visités le plus récemment.
    return [...liste].sort((a, b) => (lireBrouillon(b.id) ? 1 : 0) - (lireBrouillon(a.id) ? 1 : 0)
      || (dernierCR(b)?.date || "").localeCompare(dernierCR(a)?.date || ""));
  }, [chantiers, recherche, conducteur]);
  return (
    <div className="colonne">
      <h2 className="titre-zone">Pour quel chantier ?</h2>
      <div className="filtres">
        <input className="recherche" type="search" placeholder="Nom du chantier ou du client" value={recherche} onChange={e => setRecherche(e.target.value)} />
        {conducteurs.length > 0 && (
          <select value={conducteur} onChange={e => setConducteur(e.target.value)} aria-label="Conducteur de travaux">
            <option value="">Tous les conducteurs</option>
            {conducteurs.map(n => <option key={n} value={n}>{n}</option>)}
          </select>
        )}
      </div>
      {visibles.length === 0 ? (
        <p className="vide">Aucun chantier ne correspond. Videz la recherche ou choisissez « Tous les conducteurs ».</p>
      ) : (
        <ul className="liste">
          {visibles.map(c => {
            const d = dernierCR(c);
            return (
              <li key={c.id}>
                <button className="ligne" onClick={() => onChoisir(c.id)}>
                  <span className="ligne-titre">{c.nom}</span>
                  <span className="ligne-sous">{[c.client, c.agence, c.conducteur].filter(Boolean).join(", ")}</span>
                  <span className="pastilles">
                    <span className={"pastille" + (d ? "" : " pastille-vide")}>{d ? `Dernier CR : n°${d.numero || "?"} du ${dateCourte(d.date)}` : "Aucun compte-rendu"}</span>
                    {lireBrouillon(c.id) && <span className="pastille pastille-brouillon">Brouillon en cours</span>}
                  </span>
                </button>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}

/* ------------------------- Détail d'un compte-rendu -------------------- */
function Conformite({ valeur }) {
  if (valeur === "oui") return <span className="conf conf-oui">Conforme</span>;
  if (valeur === "non") return <span className="conf conf-non">Non conforme</span>;
  return <span className="conf conf-vide">Non renseigné</span>;
}

function Photos({ photos, onOuvrir }) {
  if (!photos || photos.length === 0) return null;
  return (
    <div className="photos">
      {photos.map((p, i) => (
        <button key={(p.url || "") + i} className="photo" onClick={() => onOuvrir(photos, i)} aria-label={`Agrandir la photo ${i + 1}`}>
          <ImagePhoto photo={p} />
          {p.annotee && <span className="photo-marque">Annotée</span>}
        </button>
      ))}
    </div>
  );
}

function BlocLot({ lot, checkliste, onOuvrirPhoto }) {
  const def = checkliste[lot.nom];
  const groupes = def || [];
  return (
    <section className="bloc">
      <h3>{lot.nom}</h3>
      {!def && <p className="note">Ce lot n'existe plus dans la check-list des Paramètres : seuls les points ajoutés, les corrections et les photos sont affichés.</p>}
      {groupes.map((g, i) => {
        const rep = lot.groupes?.[i] || {};
        if (rep.retire) return null;
        const items = (rep.itemsModifies || g.items || []).filter(it => (it || "").trim());
        const avecS = groupeAvecSemaine(g), avecC = groupeAvecConforme(g);
        return (
          <div className="groupe" key={g.id || i}>
            {g.titre && <div className="groupe-titre">{g.titre}</div>}
            {items.length > 0 && <ul className="items">{items.map((it, k) => <li key={k}>{it}</li>)}</ul>}
            {(avecS || avecC) && (
              <div className="suivi">
                {avecS && <span>Semaine : <strong>{rep.semaine || "—"}</strong></span>}
                {avecC && <Conformite valeur={rep.conforme} />}
              </div>
            )}
          </div>
        );
      })}
      {(lot.groupesPerso || []).filter(g => (g.item || "").trim()).map((g, i) => (
        <div className="groupe groupe-perso" key={"p" + i}>
          {g.titre && <div className="groupe-titre">{g.titre}</div>}
          <p className="items-texte">{g.item}</p>
          <div className="suivi">
            <span>Semaine : <strong>{g.semaine || "—"}</strong></span>
            <Conformite valeur={g.conforme} />
          </div>
        </div>
      ))}
      {lot.corrections && (
        <div className="texte-bloc">
          <div className="etiquette">Corrections à apporter</div>
          <p>{lot.corrections}</p>
        </div>
      )}
      <Photos photos={lot.photos} onOuvrir={onOuvrirPhoto} />
    </section>
  );
}

function DetailCR({ cr, checkliste, onOuvrirPhoto, sansBoutonPdf }) {
  const general = cr.commentairesGeneraux || cr.prochaineVisite || cr.indicationsProchaineVisite || (cr.photosGenerales || []).length > 0;
  return (
    <div className="detail">
      <div className="detail-tete">
        <h2>Compte-rendu n°{cr.numero || "?"}</h2>
        <p>Visite du {dateLongue(cr.date)}{cr.auteur ? `, par ${cr.auteur}` : ""}</p>
        {cr.fichierUrl && !sansBoutonPdf && <a className="btn-secondaire" href={cr.fichierUrl} target="_blank" rel="noopener">Ouvrir le PDF</a>}
      </div>
      {general && (
        <section className="bloc bloc-general">
          <h3>La visite en général</h3>
          {cr.commentairesGeneraux && <div className="texte-bloc"><div className="etiquette">Commentaires</div><p>{cr.commentairesGeneraux}</p></div>}
          {cr.prochaineVisite && <div className="texte-bloc"><div className="etiquette">Prochaine visite prévue</div><p>{dateLongue(cr.prochaineVisite)}</p></div>}
          {cr.indicationsProchaineVisite && <div className="texte-bloc"><div className="etiquette">Pour la prochaine visite</div><p>{cr.indicationsProchaineVisite}</p></div>}
          <Photos photos={cr.photosGenerales} onOuvrir={onOuvrirPhoto} />
        </section>
      )}
      {(cr.lots || []).map((lot, i) => <BlocLot key={lot.nom + i} lot={lot} checkliste={checkliste} onOuvrirPhoto={onOuvrirPhoto} />)}
    </div>
  );
}

function VisionneusePhoto({ photos, index, onIndex, onFermer }) {
  const p = photos[index];
  useEffect(() => {
    const f = e => {
      if (e.key === "Escape") onFermer();
      if (e.key === "ArrowRight" && index < photos.length - 1) onIndex(index + 1);
      if (e.key === "ArrowLeft" && index > 0) onIndex(index - 1);
    };
    window.addEventListener("keydown", f);
    return () => window.removeEventListener("keydown", f);
  }, [index, photos.length]);
  return (
    <div className="visionneuse" role="dialog" aria-label="Photo agrandie">
      <ImagePhoto photo={p} className="visionneuse-image" />
      <div className="visionneuse-barre">
        <button onClick={() => onIndex(index - 1)} disabled={index === 0}>Précédente</button>
        <span>{index + 1} / {photos.length}</span>
        <button onClick={() => onIndex(index + 1)} disabled={index === photos.length - 1}>Suivante</button>
        <button className="fermer" onClick={onFermer}>Fermer</button>
      </div>
    </div>
  );
}

/* -------------- Actions sur un compte-rendu enregistré ----------------- */
// Transmettre : même fonctionnement et mêmes données que le web (e-mail
// prérempli avec le lien du PDF, clients en destinataires, artisans en
// copie cachée ; puis enregistrement dans « transmissionsCR » et historique).
async function chargerDestinataires(chantierId) {
  const [f, o] = await Promise.all([
    supabase.from("projets").select("fiche_details").eq("id", chantierId).single(),
    supabase.from("offres").select("*, intervenants(nom), lots!inner(projet_id, nom, numero)").eq("lots.projet_id", chantierId),
  ]);
  if (f.error) throw f.error;
  if (o.error) throw o.error;
  return { fd: f.data.fiche_details || {}, offres: o.data || [] };
}
function destinatairesPossibles(fd, offres, cr) {
  const nomPersonne = (p, defaut) => [p.civilite, p.nom, p.prenom].filter(Boolean).join(" ") || defaut;
  const clients = [
    fd.mo1?.email ? { cle: "client-1", type: "client", nom: nomPersonne(fd.mo1, "Client"), email: fd.mo1.email } : null,
    fd.mo2Actif && fd.mo2?.email ? { cle: "client-2", type: "client", nom: nomPersonne(fd.mo2, "Client 2"), email: fd.mo2.email } : null,
  ].filter(Boolean);
  // Comme le web, mais sans tenir compte des accents (MACONNERIE = Maçonnerie).
  const lotsDuCr = (cr.lots || []).map(l => normaliser(l.nom));
  const vus = new Set(), artisans = [];
  offres.filter(o => o.retenu && !o.verrouille && o.email).forEach(o => {
    const nom = o.intervenants?.nom || o.entreprise_libre || "";
    if (vus.has(nom)) return;
    vus.add(nom);
    const lot = o.lots?.nom || "";
    artisans.push({ cle: `art-${o.id}`, type: "artisan", nom, email: o.email, lot, concerne: lotsDuCr.includes(normaliser(lot)) });
  });
  return { clients, artisans };
}
async function ajouterHistorique(chantierId, action, detail) {
  try {
    const { data, error } = await supabase.from("projets").select("historique").eq("id", chantierId).single();
    if (error) throw error;
    const historique = [...(data.historique || []), { date: new Date().toISOString().slice(0, 10), action, detail }];
    await supabase.from("projets").update({ historique }).eq("id", chantierId);
  } catch (e) { /* secondaire, comme sur le web */ }
}

function FenetreTransmission({ chantier, cr, auteur, onFermer, onTransmis }) {
  const [donnees, setDonnees] = useState(null);
  const [erreur, setErreur] = useState("");
  const [choisis, setChoisis] = useState(new Set());
  const [objet, setObjet] = useState(`Compte-rendu de chantier n°${cr.numero || ""} — ${chantier.nom}`);
  const [corps, setCorps] = useState(`Bonjour,\n\nVous trouverez ci-dessous le lien vers le compte-rendu de chantier n°${cr.numero || ""} du ${cr.date ? dateFr(cr.date) : ""} concernant le chantier ${chantier.nom} :\n${cr.fichierUrl}\n\nNous restons à votre disposition pour tout complément.\n\nCordialement,`);
  const [etape, setEtape] = useState("redaction"); // redaction | confirmation | enregistrement
  useEffect(() => {
    chargerDestinataires(chantier.id).then(({ fd, offres }) => {
      const d = destinatairesPossibles(fd, offres, cr);
      setDonnees(d);
      setChoisis(new Set([...d.clients.map(x => x.cle), ...d.artisans.filter(a => a.concerne).map(a => a.cle)]));
    }).catch(() => setErreur("Impossible de charger les destinataires. Vérifiez le réseau."));
  }, []);
  const bascule = cle => setChoisis(prev => { const n = new Set(prev); n.has(cle) ? n.delete(cle) : n.add(cle); return n; });
  const dest = donnees ? [...donnees.clients, ...donnees.artisans].filter(d => choisis.has(d.cle)) : [];

  function ouvrirMessagerie() {
    if (dest.length === 0) { setErreur("Sélectionnez au moins un destinataire."); return; }
    setErreur("");
    const to = dest.filter(d => d.type === "client").map(d => d.email);
    const bcc = dest.filter(d => d.type === "artisan").map(d => d.email);
    window.location.href = `mailto:${to.map(encodeURIComponent).join(",")}?${bcc.length ? `bcc=${encodeURIComponent(bcc.join(","))}&` : ""}subject=${encodeURIComponent(objet)}&body=${encodeURIComponent(corps)}`;
    setEtape("confirmation");
  }
  async function enregistrerTransmission() {
    setEtape("enregistrement");
    try {
      const { data, error } = await supabase.from("projets").select("fiche_details").eq("id", chantier.id).single();
      if (error) throw error;
      const fd = data.fiche_details || {};
      const entree = { id: `tr-${Date.now()}`, crId: cr.id, date: new Date().toISOString().slice(0, 10), destinataires: dest.map(d => ({ nom: d.nom, type: d.type, email: d.email })), auteur: auteur || "" };
      const maj = await supabase.from("projets").update({ fiche_details: { ...fd, transmissionsCR: [...(fd.transmissionsCR || []), entree] } }).eq("id", chantier.id);
      if (maj.error) throw maj.error;
      await ajouterHistorique(chantier.id, "Compte-rendu transmis", `CR n°${cr.numero || ""} → ${dest.map(d => d.nom).join(", ")}`);
      onTransmis();
    } catch (e) {
      setErreur("La transmission n'a pas pu être enregistrée : " + (e.message || "erreur inconnue"));
      setEtape("confirmation");
    }
  }

  const Case = ({ d }) => (
    <label className="case-dest">
      <input type="checkbox" checked={choisis.has(d.cle)} onChange={() => bascule(d.cle)} />
      <span><strong>{d.nom}</strong>{d.lot ? `, ${d.lot}` : ""}<br /><span className="note">{d.email}</span></span>
    </label>
  );
  return (
    <div className="voile-enreg" role="dialog" aria-label="Transmettre le compte-rendu">
      <div className="voile-carte fenetre">
        <h3>Transmettre le CR n°{cr.numero || "?"}</h3>
        {etape === "redaction" && (
          <>
            {!donnees && !erreur && <p className="note">Chargement des destinataires…</p>}
            {donnees && (
              <>
                <div className="etiquette">Client(s)</div>
                {donnees.clients.length === 0 ? <p className="note">Aucun e-mail client renseigné dans la fiche du dossier.</p> : donnees.clients.map(d => <Case key={d.cle} d={d} />)}
                <div className="etiquette espace-haut">Artisans retenus (cochés : ceux des lots du compte-rendu, en copie cachée)</div>
                {donnees.artisans.length === 0 ? <p className="note">Aucun artisan retenu avec une adresse e-mail sur ce dossier.</p> : donnees.artisans.map(d => <Case key={d.cle} d={d} />)}
                <label className="champ-etiquete"><span>Objet</span><input className="champ" value={objet} onChange={e => setObjet(e.target.value)} /></label>
                <div className="texte-bloc"><div className="etiquette">Message</div><ZoneTexte lignes={7} valeur={corps} onChange={setCorps} /></div>
              </>
            )}
            {erreur && <p className="erreur">{erreur}</p>}
            <div className="boutons-fenetre">
              <button className="btn-secondaire" onClick={onFermer}>Annuler</button>
              <button className="btn-principal" onClick={ouvrirMessagerie} disabled={!donnees}>Ouvrir dans la messagerie</button>
            </div>
          </>
        )}
        {etape !== "redaction" && (
          <>
            <p>L'e-mail est prérempli dans votre messagerie, avec le lien du PDF. Les artisans sont en copie cachée.</p>
            <p><strong>L'avez-vous envoyé ?</strong> Si oui, il sera enregistré comme transmis dans le dossier.</p>
            {erreur && <p className="erreur">{erreur}</p>}
            <div className="boutons-fenetre">
              <button className="btn-secondaire" onClick={onFermer} disabled={etape === "enregistrement"}>Non, pas envoyé</button>
              <button className="btn-principal" onClick={enregistrerTransmission} disabled={etape === "enregistrement"}>{etape === "enregistrement" ? "Enregistrement…" : "Oui, envoyé"}</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// Suppression : n'existe pas sur le web. Pour ne jamais créer deux CR avec
// le même numéro (le web numérote « nombre de CR + 1 »), seul le DERNIER
// compte-rendu d'un chantier peut être supprimé. Le PDF est retiré du
// dossier ; les photos ne sont pas effacées (elles peuvent servir à
// d'autres comptes-rendus repris).
function cheminDepuisLienSigne(url) {
  const m = /\/object\/sign\/APS\/([^?]+)/.exec(url || "");
  return m ? decodeURIComponent(m[1]) : null;
}
async function supprimerCompteRendu(chantier, cr) {
  const { data, error } = await supabase.from("projets").select("fiche_details").eq("id", chantier.id).single();
  if (error) throw error;
  const fd = data.fiche_details || {};
  const liste = fd.comptesRendus || [];
  if (!liste.length || liste[liste.length - 1].id !== cr.id) throw new Error("Ce n'est plus le dernier compte-rendu du chantier (un autre a été ajouté entre-temps). Suppression annulée.");
  const arbo = fd.documentsArbo || {};
  const cle = "08-chantier/comptes-rendus";
  const nouvelleFiche = {
    ...fd,
    comptesRendus: liste.slice(0, -1),
    documentsArbo: { ...arbo, [cle]: (arbo[cle] || []).filter(d => d.url !== cr.fichierUrl) },
    transmissionsCR: (fd.transmissionsCR || []).filter(t => t.crId !== cr.id),
  };
  const maj = await supabase.from("projets").update({ fiche_details: nouvelleFiche }).eq("id", chantier.id);
  if (maj.error) throw maj.error;
  const chemin = cheminDepuisLienSigne(cr.fichierUrl);
  if (chemin) { try { await supabase.storage.from("APS").remove([chemin]); } catch (e) { /* pas bloquant */ } }
  await ajouterHistorique(chantier.id, "Compte-rendu supprimé", `CR n°${cr.numero || ""} du ${dateFr(cr.date)} (depuis le mobile)`);
}

/* ------------------------- Saisie d'un compte-rendu -------------------- */
function BoutonsConforme({ valeur, onChange }) {
  // Toucher à nouveau le choix actif l'efface (retour à « non renseigné »).
  return (
    <div className="choix-conf" role="group" aria-label="Conforme">
      <button type="button" className={"choix choix-oui" + (valeur === "oui" ? " actif" : "")} aria-pressed={valeur === "oui"} onClick={() => onChange(valeur === "oui" ? "" : "oui")}>Conforme</button>
      <button type="button" className={"choix choix-non" + (valeur === "non" ? " actif" : "")} aria-pressed={valeur === "non"} onClick={() => onChange(valeur === "non" ? "" : "non")}>Non conforme</button>
    </div>
  );
}
function ChampSemaine({ valeur, onChange }) {
  return (
    <label className="champ-semaine">
      <span>Semaine</span>
      <input type="text" inputMode="numeric" value={valeur || ""} onChange={e => onChange(e.target.value)} />
    </label>
  );
}
function ZoneTexte({ valeur, onChange, placeholder, lignes = 3 }) {
  return <textarea className="zone-texte" rows={lignes} placeholder={placeholder} value={valeur || ""} onChange={e => onChange(e.target.value)} />;
}


function resumeLot(d, def) {
  let oui = 0, non = 0;
  def.forEach((g, i) => {
    const r = d.groupes?.[i];
    if (!r || r.retire) return;
    if (r.conforme === "oui") oui++;
    if (r.conforme === "non") non++;
  });
  (d.groupesPerso || []).forEach(g => { if (g.conforme === "oui") oui++; if (g.conforme === "non") non++; });
  const parts = [];
  if (oui) parts.push(`${oui} conforme${oui > 1 ? "s" : ""}`);
  if (non) parts.push(`${non} non conforme${non > 1 ? "s" : ""}`);
  return parts.join(", ") || "Rien de renseigné";
}

function SaisieLot({ nom, entreprise, d, def, ouvert, onBasculer, maj, projetId, onOuvrirPhoto, onRetirerPhoto, onAnnoterPhoto }) {
  const [edition, setEdition] = useState(null); // index du groupe dont on modifie les points
  const majGroupe = (i, champs) => maj(p => {
    const groupes = [...(p.groupes || [])];
    groupes[i] = { ...(groupes[i] || { conforme: "", semaine: "", retire: false }), ...champs };
    return { ...p, groupes };
  });
  const majPerso = (i, champs) => maj(p => {
    const groupesPerso = [...(p.groupesPerso || [])];
    groupesPerso[i] = { ...groupesPerso[i], ...champs };
    return { ...p, groupesPerso };
  });
  const retires = def.map((g, i) => ({ g, i })).filter(({ i }) => d.groupes?.[i]?.retire);

  return (
    <section className={"lot-saisie" + (ouvert ? " ouvert" : "")}>
      <button type="button" className="lot-tete" onClick={onBasculer} aria-expanded={ouvert}>
        <span className="lot-nom">{nom}</span>
        {entreprise && <span className="lot-entreprise">{entreprise}</span>}
        <span className="lot-resume">{resumeLot(d, def)}</span>
        <span className="lot-chevron" aria-hidden="true">{ouvert ? "−" : "+"}</span>
      </button>
      {ouvert && (
        <div className="lot-corps">
          {def.length === 0 && <p className="note">Pas de check-list pour ce lot dans les Paramètres : ajoutez vos lignes ci-dessous.</p>}
          {def.map((g, i) => {
            const r = d.groupes?.[i] || {};
            if (r.retire) return null;
            const items = r.itemsModifies || g.items || [];
            const avecS = groupeAvecSemaine(g), avecC = groupeAvecConforme(g);
            return (
              <div className="groupe-saisie" key={g.id || i}>
                {g.titre && <div className="groupe-titre">{g.titre}</div>}
                {edition === i ? (
                  <ZoneTexte lignes={4} valeur={items.join("\n")} onChange={v => majGroupe(i, { itemsModifies: v.split("\n") })} />
                ) : (
                  items.filter(it => (it || "").trim()).length > 0 && <ul className="items">{items.filter(it => (it || "").trim()).map((it, k) => <li key={k}>{it}</li>)}</ul>
                )}
                {(avecS || avecC) && (
                  <div className="ligne-controles">
                    {avecS && <ChampSemaine valeur={r.semaine} onChange={v => majGroupe(i, { semaine: v })} />}
                    {avecC && <BoutonsConforme valeur={r.conforme} onChange={v => majGroupe(i, { conforme: v })} />}
                  </div>
                )}
                <div className="ligne-actions">
                  <button type="button" className="lien" onClick={() => setEdition(edition === i ? null : i)}>{edition === i ? "Terminer la modification" : "Modifier les points"}</button>
                  <button type="button" className="lien lien-discret" onClick={() => { majGroupe(i, { retire: true }); if (edition === i) setEdition(null); }}>Retirer pour cette visite</button>
                </div>
              </div>
            );
          })}

          {(d.groupesPerso || []).map((g, i) => (
            <div className="groupe-saisie groupe-perso" key={"p" + i}>
              <input className="champ" placeholder="Titre de la ligne (facultatif)" value={g.titre || ""} onChange={e => majPerso(i, { titre: e.target.value })} />
              <ZoneTexte lignes={2} placeholder="Observation" valeur={g.item} onChange={v => majPerso(i, { item: v })} />
              <div className="ligne-controles">
                <ChampSemaine valeur={g.semaine} onChange={v => majPerso(i, { semaine: v })} />
                <BoutonsConforme valeur={g.conforme} onChange={v => majPerso(i, { conforme: v })} />
              </div>
              <div className="ligne-actions">
                <button type="button" className="lien lien-danger" onClick={() => maj(p => ({ ...p, groupesPerso: (p.groupesPerso || []).filter((_, k) => k !== i) }))}>Supprimer cette ligne</button>
              </div>
            </div>
          ))}
          <button type="button" className="btn-secondaire btn-large" onClick={() => maj(p => ({ ...p, groupesPerso: [...(p.groupesPerso || []), { titre: "", item: "", conforme: "", semaine: "" }] }))}>Ajouter une ligne</button>

          <div className="texte-bloc">
            <div className="etiquette">Corrections à apporter / à porter à la connaissance de l'entreprise</div>
            <ZoneTexte valeur={d.corrections} onChange={v => maj(p => ({ ...p, corrections: v }))} />
          </div>
          <div className="texte-bloc"><div className="etiquette">Photos du lot</div></div>
          <ZonePhotos
            photos={d.photos || []} projetId={projetId} prefixeNom={nom.replace(/[^A-Za-z0-9]+/g, "_")}
            onAjouter={ph => maj(p => ({ ...p, photos: [...(p.photos || []), ph] }))}
            onRetirer={ph => onRetirerPhoto(ph, () => maj(p => ({ ...p, photos: (p.photos || []).filter(x => x !== ph && (!ph.idLocal || x.idLocal !== ph.idLocal)) })))}
            onOuvrir={onOuvrirPhoto} onAnnoter={onAnnoterPhoto}
          />

          {retires.length > 0 && (
            <div className="retires">
              <div className="etiquette">Lignes retirées pour cette visite</div>
              {retires.map(({ g, i }) => (
                <div className="retire" key={i}>
                  <span>{g.titre || (g.items || [])[0] || `Ligne ${i + 1}`}</span>
                  <button type="button" className="lien" onClick={() => majGroupe(i, { retire: false })}>Remettre</button>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </section>
  );
}

function FormulaireCR({ chantier, checkliste, onFermer, onBrouillonChange, onOuvrirPhoto, onEnregistre }) {
  const [b, setB] = useState(() => lireBrouillon(chantier.id) || brouillonVide(chantier));
  const [infosLots, setInfosLots] = useState(null); // { lots, offres } une fois chargé
  const [erreurLots, setErreurLots] = useState("");
  const [ouvert, setOuvert] = useState(null);
  const [echecSauvegarde, setEchecSauvegarde] = useState(false);
  const premierPassage = useRef(true);

  // Sauvegarde automatique du brouillon sur l'appareil à chaque saisie.
  useEffect(() => {
    if (premierPassage.current) { premierPassage.current = false; return; }
    const ok = ecrireBrouillon(chantier.id, b);
    setEchecSauvegarde(!ok);
    onBrouillonChange();
  }, [b]);
  const modifier = f => setB(p => ({ ...f(p), majLe: new Date().toISOString() }));

  // Photos : suivi des envois, relance automatique au retour du réseau.
  const [, setTicPhotos] = useState(0);
  useEffect(() => {
    const envoyee = e => { if (e.detail.projetId === chantier.id) setB(p => majPhoto(p, e.detail.idLocal, { url: e.detail.url })); };
    const etat = () => setTicPhotos(t => t + 1);
    const reseau = () => { etat(); relancerEnvois(chantier.id); };
    window.addEventListener("idh-photo-envoyee", envoyee);
    window.addEventListener("idh-photo-etat", etat);
    window.addEventListener("online", reseau);
    window.addEventListener("offline", etat);
    relancerEnvois(chantier.id);
    return () => {
      window.removeEventListener("idh-photo-envoyee", envoyee);
      window.removeEventListener("idh-photo-etat", etat);
      window.removeEventListener("online", reseau);
      window.removeEventListener("offline", etat);
    };
  }, [chantier.id]);
  function retirerPhoto(ph, retirerDuBrouillon) {
    retirerDuBrouillon();
    if (ph.idLocal) photoLocaleSupprimer(ph.idLocal);
    if (ph.idBase) photoLocaleSupprimer(ph.idBase);
  }

  // Annotation : la photo annotée remplace l'originale dans le brouillon,
  // avec ses formes (format du web). On garde aussi sur l'appareil la
  // « base » (photo + traits libres) pour ré-annoter sans doublon.
  const [annotation, setAnnotation] = useState(null); // idLocal de la photo
  const photoAnnotee = annotation ? toutesPhotos(b).find(p => p.idLocal === annotation) : null;
  async function enregistrerAnnotation(blobAnnotee, blobBase, formes) {
    const ancienne = photoAnnotee;
    const suffixe = Math.random().toString(36).slice(2, 8);
    const idLocal = `ph-${Date.now()}-${suffixe}`, idBase = `pb-${Date.now()}-${suffixe}`;
    await photoLocaleEcrire(idLocal, blobAnnotee);
    await photoLocaleEcrire(idBase, blobBase);
    const nom = (ancienne.nom || "Photo.jpg").replace(/(_annotee)?\.jpg$/i, "") + "_annotee.jpg";
    const nouvelle = { idLocal, idBase, nom, url: null, annotee: true, formes };
    const remplacer = liste => (liste || []).map(p => p.idLocal === ancienne.idLocal ? nouvelle : p);
    modifier(p => {
      const detailParLot = {};
      Object.keys(p.detailParLot || {}).forEach(n => { detailParLot[n] = { ...p.detailParLot[n], photos: remplacer(p.detailParLot[n].photos) }; });
      return { ...p, photosGenerales: remplacer(p.photosGenerales), detailParLot };
    });
    photoLocaleSupprimer(ancienne.idLocal);
    if (ancienne.idBase) photoLocaleSupprimer(ancienne.idBase);
    setAnnotation(null);
    if (navigator.onLine) envoyerPhoto(chantier.id, idLocal, nom);
  }

  async function chargerLots() {
    setErreurLots("");
    try { setInfosLots(await chargerLotsEtOffres(chantier.id)); }
    catch (e) { setErreurLots("Impossible de charger les entreprises retenues sur ce chantier. Vérifiez le réseau, puis réessayez."); }
  }
  useEffect(() => { chargerLots(); }, [chantier.id]);

  const retenus = useMemo(() => {
    if (!infosLots) return [];
    return Object.keys(checkliste)
      .map(nom => ({ nom, entreprise: intervenantUniquePourCorpsEtat(nom, infosLots.lots, infosLots.offres) }))
      .filter(x => x.entreprise);
  }, [infosLots, checkliste]);
  const entrepriseDe = nom => retenus.find(x => x.nom === nom)?.entreprise || "";
  // Les lots déjà cochés restent visibles même s'ils ne sont plus « retenus ».
  const proposes = [...retenus.map(x => x.nom), ...b.lotsChoisis.filter(n => !retenus.some(x => x.nom === n))];

  function basculerLot(nom) {
    const dejaChoisi = b.lotsChoisis.includes(nom);
    modifier(p => ({
      ...p,
      lotsChoisis: dejaChoisi ? p.lotsChoisis.filter(x => x !== nom) : [...p.lotsChoisis, nom],
      detailParLot: p.detailParLot[nom] ? p.detailParLot : { ...p.detailParLot, [nom]: nouveauDetailLot(nom, checkliste) },
    }));
    setOuvert(dejaChoisi ? (ouvert === nom ? null : ouvert) : nom);
  }
  // Reprise du dernier compte-rendu : proposée d'emblée sur un brouillon vide.
  const dernier = dernierCR(chantier);
  function reprendreDernier() {
    if (!dernier) return;
    if (!brouillonEstVide(b) && !window.confirm("Remplacer ce qui est déjà saisi par le contenu du dernier compte-rendu ? Les photos déjà prises aujourd'hui sont gardées.")) return;
    setB(p => brouillonDepuisCR(chantier, dernier, p));
    setOuvert(null);
    setTimeout(copierPhotosReprisesSurAppareil, 50);
  }
  // Copie sur l'appareil les photos reprises (pour l'annotation et le hors-ligne).
  async function copierPhotosReprisesSurAppareil() {
    const aCopier = toutesPhotos(lireBrouillon(chantier.id)).filter(p => !p.idLocal && p.url);
    for (const p of aCopier) {
      try {
        const rep = await avecDelai(fetch(p.url), 30000, "délai dépassé");
        if (!rep.ok) continue;
        const blob = await rep.blob();
        const idLocal = `ph-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        await photoLocaleEcrire(idLocal, blob);
        setB(prev => {
          const maj = liste => (liste || []).map(x => (!x.idLocal && x.url === p.url) ? { ...x, idLocal } : x);
          const detailParLot = {};
          Object.keys(prev.detailParLot || {}).forEach(n => { detailParLot[n] = { ...prev.detailParLot[n], photos: maj(prev.detailParLot[n].photos) }; });
          return { ...prev, photosGenerales: maj(prev.photosGenerales), detailParLot };
        });
      } catch (e) { /* sans réseau : la photo reste affichée par son lien */ }
    }
  }
  useEffect(() => { setTimeout(copierPhotosReprisesSurAppareil, 300); }, []);
  const messageReprise = b.repriseDe
    ? `Repris du compte-rendu n°${b.repriseDe.numero || "?"} du ${dateLongue(b.repriseDe.date)} : ${b.repriseDe.nbLots} corps de métier, textes et ${b.repriseDe.nbPhotos} photo${b.repriseDe.nbPhotos > 1 ? "s" : ""}. Mettez à jour ce qui a changé et la date de prochaine visite.`
    : "";
  const proposerReprise = dernier && !b.departChoisi && brouillonEstVide(b);
  const [enregistrementCR, setEnregistrementCR] = useState(null); // { etape, erreur, debut }
  const [, setTicChrono] = useState(0);
  useEffect(() => {
    if (!enregistrementCR || enregistrementCR.erreur) return;
    const t = setInterval(() => setTicChrono(x => x + 1), 1000);
    return () => clearInterval(t);
  }, [!!enregistrementCR && !enregistrementCR.erreur]);
  async function enregistrer() {
    if (enregistrementCR) return;
    if (!window.confirm(`Enregistrer le compte-rendu du ${dateLongue(b.dateVisite)} ? Le PDF sera créé et ajouté au dossier du chantier.`)) return;
    const debut = Date.now();
    let derniereEtape = "Préparation…";
    setEnregistrementCR({ etape: derniereEtape, debut });
    try {
      const cr = await enregistrerCompteRendu({ chantier, b, checkliste, infosLots, etape: e => { derniereEtape = e; setEnregistrementCR({ etape: e, debut }); } });
      onBrouillonChange();
      onEnregistre(cr.id);
    } catch (e) {
      setEnregistrementCR({ erreur: e.message || "erreur inconnue", etapeBloquee: derniereEtape, duree: Math.round((Date.now() - debut) / 1000) });
    }
  }
  function abandonner() {
    if (!window.confirm("Abandonner ce brouillon ? Tout ce qui a été saisi pour cette visite sera effacé de l'appareil.")) return;
    toutesPhotos(b).forEach(p => { if (p.idLocal) photoLocaleSupprimer(p.idLocal); if (p.idBase) photoLocaleSupprimer(p.idBase); });
    supprimerBrouillon(chantier.id);
    onBrouillonChange();
    onFermer();
  }

  return (
    <div className="formulaire">
      <div className="detail-tete">
        <h2>Nouveau compte-rendu</h2>
        <p>{[chantier.nom, chantier.client].filter(Boolean).join(", ")}</p>
      </div>

      {proposerReprise && (
        <section className="carte-reprise">
          <div className="ligne-titre">Partir du dernier compte-rendu ?</div>
          <p className="ligne-sous">CR n°{dernier.numero || "?"} du {dateLongue(dernier.date)} : {(dernier.lots || []).length} corps de métier. Tout est repris (réponses, lignes, corrections, textes, photos) ; vous n'avez plus qu'à mettre à jour.</p>
          <button type="button" className="btn-principal btn-large" onClick={reprendreDernier}>Tout reprendre</button>
          <button type="button" className="btn-secondaire btn-large" onClick={() => modifier(p => ({ ...p, departChoisi: true }))}>Partir de zéro</button>
        </section>
      )}

      <section className="bloc">
        <h3 className="titre-acier">La visite</h3>
        <div className="deux-champs">
          <label className="champ-etiquete"><span>Date de la visite</span><input className="champ" type="date" value={b.dateVisite} onChange={e => modifier(p => ({ ...p, dateVisite: e.target.value }))} /></label>
          <label className="champ-etiquete"><span>Auteur du compte-rendu</span><input className="champ" value={b.auteur} onChange={e => modifier(p => ({ ...p, auteur: e.target.value }))} /></label>
        </div>
        {dernier && !proposerReprise && <button type="button" className="btn-secondaire btn-large" onClick={reprendreDernier}>Reprendre le dernier compte-rendu (n°{dernier.numero || "?"})</button>}
        {messageReprise && <p className="note message-reprise">{messageReprise}</p>}
      </section>

      <section className="bloc">
        <h3 className="titre-acier">La visite en général</h3>
        <div className="texte-bloc"><div className="etiquette">Commentaires généraux</div><ZoneTexte valeur={b.commentairesGeneraux} onChange={v => modifier(p => ({ ...p, commentairesGeneraux: v }))} /></div>
        <label className="champ-etiquete"><span>Prochaine visite de chantier prévue le</span><input className="champ" type="date" value={b.prochaineVisite} onChange={e => modifier(p => ({ ...p, prochaineVisite: e.target.value }))} /></label>
        <div className="texte-bloc"><div className="etiquette">Indications pour la prochaine visite (artisans à convoquer…)</div><ZoneTexte valeur={b.indicationsProchaineVisite} onChange={v => modifier(p => ({ ...p, indicationsProchaineVisite: v }))} /></div>
        <div className="texte-bloc"><div className="etiquette">Photos générales de la visite</div></div>
        <ZonePhotos
          photos={b.photosGenerales || []} projetId={chantier.id} prefixeNom="Generale"
          onAjouter={ph => modifier(p => ({ ...p, photosGenerales: [...(p.photosGenerales || []), ph] }))}
          onRetirer={ph => retirerPhoto(ph, () => modifier(p => ({ ...p, photosGenerales: (p.photosGenerales || []).filter(x => x !== ph && (!ph.idLocal || x.idLocal !== ph.idLocal)) })))}
          onOuvrir={onOuvrirPhoto} onAnnoter={ph => setAnnotation(ph.idLocal)}
        />
      </section>

      <section className="bloc">
        <h3 className="titre-acier">Corps de métier de la visite</h3>
        {!infosLots && !erreurLots && <p className="note">Chargement des entreprises retenues…</p>}
        {erreurLots && <p className="erreur">{erreurLots} <button type="button" className="lien" onClick={chargerLots}>Réessayer</button></p>}
        {infosLots && proposes.length === 0 && <p className="note">Aucune entreprise retenue identifiée pour l'instant sur ce chantier (appel d'offres pas encore complété).</p>}
        <div className="puces">
          {proposes.map(nom => (
            <button type="button" key={nom} className={"puce" + (b.lotsChoisis.includes(nom) ? " active" : "")} aria-pressed={b.lotsChoisis.includes(nom)} onClick={() => basculerLot(nom)}>
              <span className="puce-nom">{nom}</span>
              {entrepriseDe(nom) && <span className="puce-ent">{entrepriseDe(nom)}</span>}
            </button>
          ))}
        </div>
      </section>

      {b.lotsChoisis.map(nom => (
        <SaisieLot
          key={nom} nom={nom} entreprise={entrepriseDe(nom)}
          d={b.detailParLot[nom] || nouveauDetailLot(nom, checkliste)} def={checkliste[nom] || []}
          ouvert={ouvert === nom} onBasculer={() => setOuvert(ouvert === nom ? null : nom)}
          maj={f => modifier(p => ({ ...p, detailParLot: { ...p.detailParLot, [nom]: f(p.detailParLot[nom] || nouveauDetailLot(nom, checkliste)) } }))}
          projetId={chantier.id} onOuvrirPhoto={onOuvrirPhoto} onRetirerPhoto={retirerPhoto} onAnnoterPhoto={ph => setAnnotation(ph.idLocal)}
        />
      ))}

      <button type="button" className="lien lien-danger abandon" onClick={abandonner}>Abandonner ce brouillon</button>

      <div className="barre-enregistrer">
        <span className={"etat-brouillon" + (echecSauvegarde ? " erreur" : "")}>
          {echecSauvegarde ? "Le brouillon n'a pas pu être gardé sur l'appareil (mémoire pleine ?)." : b.majLe ? `Brouillon gardé sur l'appareil, ${heureCourte(b.majLe)}` : "Le brouillon se garde automatiquement sur l'appareil."}
        </span>
        <button type="button" className="btn-principal" onClick={enregistrer} disabled={!!enregistrementCR || b.lotsChoisis.length === 0}>Enregistrer le CR</button>
      </div>
      {enregistrementCR && (
        <div className="voile-enreg" role="alertdialog" aria-live="polite">
          <div className="voile-carte">
            {enregistrementCR.erreur ? (
              <>
                <h3>Le compte-rendu n'est pas enregistré</h3>
                <p>{enregistrementCR.erreur}</p>
                <p className="note">Étape : {enregistrementCR.etapeBloquee} (après {enregistrementCR.duree} s). {VERSION}.</p>
                <p className="note">Rien n'est enregistré dans la fiche du chantier, et rien n'est perdu : le brouillon et les photos restent sur l'appareil.</p>
                <button type="button" className="btn-principal btn-large" onClick={() => setEnregistrementCR(null)}>Revenir au brouillon</button>
              </>
            ) : (
              <>
                <h3>Enregistrement du compte-rendu</h3>
                <p>{enregistrementCR.etape}</p>
                <p className="chrono">{Math.round((Date.now() - enregistrementCR.debut) / 1000)} s</p>
                <p className="note">Gardez l'appli ouverte jusqu'à la fin.</p>
              </>
            )}
          </div>
        </div>
      )}
      {photoAnnotee && (
        <AnnotateurTactile key={photoAnnotee.idLocal} photo={photoAnnotee} onEnregistrer={enregistrerAnnotation} onFermer={() => setAnnotation(null)} />
      )}
    </div>
  );
}

/* ------------------------------ Écran principal ------------------------ */
// Navigation : accueil → choix du chantier → saisie ; accueil → compte-rendu.
function EcranPrincipal({ email }) {
  const [chantiers, setChantiers] = useState(null);
  const [checklistePerso, setChecklistePerso] = useState(null);
  const [erreur, setErreur] = useState("");
  const [chargement, setChargement] = useState(false);
  const [ecran, setEcran] = useState({ type: "accueil" }); // accueil | choix | saisie | cr
  const [visionneuse, setVisionneuse] = useState(null);
  const [transmission, setTransmission] = useState(false);
  const [suppression, setSuppression] = useState(null); // { enCours, erreur }
  const [, setVersionBrouillons] = useState(0);
  const brouillonChange = () => setVersionBrouillons(v => v + 1);
  const [conducteurChoisi] = useStockageLocal("idh-mobile-conducteur", "");

  async function charger() {
    setChargement(true);
    setErreur("");
    try {
      const [liste, perso] = await Promise.all([chargerChantiers(), chargerChecklistePerso().catch(() => null)]);
      setChantiers(liste);
      setChecklistePerso(perso);
    } catch (e) {
      setErreur("Impossible de charger les chantiers. Vérifiez le réseau, puis touchez « Actualiser ». (" + (e.message || "erreur inconnue") + ")");
    } finally {
      setChargement(false);
    }
  }
  useEffect(() => { charger(); }, []);
  useEffect(() => { window.scrollTo(0, 0); }, [ecran.type, ecran.chantierId, ecran.crId]);

  const checkliste = useMemo(() => checklisteCRActive(checklistePerso), [checklistePerso]);
  const chantier = chantiers?.find(c => c.id === ecran.chantierId) || null;
  const cr = ecran.type === "cr" ? chantier?.comptesRendus.find(x => x.id === ecran.crId) || null : null;
  const ouvrirPhoto = (photos, index) => setVisionneuse({ photos, index });

  const aller = e => { setTransmission(false); setSuppression(null); setEcran(e); };
  async function apresEnregistrement(id) {
    await charger();
    aller({ type: "cr", chantierId: ecran.chantierId, crId: id });
  }
  function reprendreCeCR() {
    const existant = lireBrouillon(chantier.id);
    if (existant && !brouillonEstVide(existant) && !window.confirm("Un brouillon est déjà en cours sur ce chantier. Le remplacer par le contenu de ce compte-rendu ? Les photos déjà prises aujourd'hui sont gardées.")) return;
    ecrireBrouillon(chantier.id, brouillonDepuisCR(chantier, cr, existant));
    brouillonChange();
    aller({ type: "saisie", chantierId: chantier.id });
  }
  async function supprimer() {
    const transmis = chantier.transmissionsCR.some(t => t.crId === cr.id);
    if (!window.confirm(`Supprimer définitivement le compte-rendu n°${cr.numero || "?"} du ${dateLongue(cr.date)} ? Il disparaîtra aussi de l'appli web, et son PDF sera retiré du dossier.${transmis ? " Attention : il a déjà été transmis, le lien envoyé ne fonctionnera plus." : ""}`)) return;
    setSuppression({ enCours: true });
    try {
      await supprimerCompteRendu(chantier, cr);
      await charger();
      aller({ type: "accueil" });
    } catch (e) {
      setSuppression({ erreur: e.message || "erreur inconnue" });
    }
  }

  const titres = { accueil: "", choix: "Nouveau compte-rendu", saisie: "Saisie", cr: cr ? `CR n°${cr.numero || "?"}` : "Compte-rendu" };
  const barre = (
    <header className="barre">
      {ecran.type !== "accueil"
        ? <button className="barre-retour" onClick={() => aller({ type: "accueil" })} aria-label="Retour à l'accueil">‹ Accueil</button>
        : <span className="barre-marque">IDHOUSECONCEPT</span>}
      <span className="barre-titre">{titres[ecran.type]}</span>
      <button className="barre-action" onClick={charger} disabled={chargement}>{chargement ? "…" : "Actualiser"}</button>
    </header>
  );
  const pied = (
    <footer className="pied">
      <span>{email}</span>
      <span>Check-list : {checklistePerso ? "personnalisée (Paramètres)" : "d'origine"}</span>
      <span>{VERSION}</span>
      <button className="lien" onClick={() => supabase.auth.signOut()}>Se déconnecter</button>
    </footer>
  );

  let contenu;
  if (erreur && !chantiers) contenu = <p className="vide erreur">{erreur}</p>;
  else if (!chantiers) contenu = <p className="vide">Chargement…</p>;
  else if (ecran.type === "choix") {
    contenu = <ChoixChantier chantiers={chantiers} onChoisir={id => aller({ type: "saisie", chantierId: id })} />;
  } else if (ecran.type === "saisie" && chantier) {
    contenu = <FormulaireCR key={chantier.id} chantier={chantier} checkliste={checkliste} onFermer={() => aller({ type: "accueil" })} onBrouillonChange={brouillonChange} onOuvrirPhoto={ouvrirPhoto} onEnregistre={apresEnregistrement} />;
  } else if (ecran.type === "cr" && chantier && cr) {
    const estDernier = chantier.comptesRendus[chantier.comptesRendus.length - 1]?.id === cr.id;
    const tr = chantier.transmissionsCR.filter(t => t.crId === cr.id);
    contenu = (
      <>
        <div className="actions-cr">
          {cr.fichierUrl && <a className="btn-principal btn-action" href={cr.fichierUrl} target="_blank" rel="noopener">Ouvrir le PDF</a>}
          {cr.fichierUrl && <button className="btn-secondaire btn-action" onClick={() => setTransmission(true)}>Transmettre</button>}
          <button className="btn-secondaire btn-action" onClick={reprendreCeCR}>Reprendre</button>
        </div>
        {tr.length > 0 && (
          <p className="note transmis">
            {tr.map(t => `Transmis le ${dateCourte(t.date)} à ${(t.destinataires || []).map(d => d.nom).join(", ")}`).join(". ")}.
          </p>
        )}
        <DetailCR cr={cr} checkliste={checkliste} onOuvrirPhoto={ouvrirPhoto} sansBoutonPdf />
        <div className="zone-suppression">
          {estDernier
            ? <button className="lien lien-danger" onClick={supprimer} disabled={suppression?.enCours}>{suppression?.enCours ? "Suppression…" : "Supprimer ce compte-rendu"}</button>
            : <p className="note">Seul le dernier compte-rendu d'un chantier peut être supprimé, pour ne pas créer deux comptes-rendus avec le même numéro.</p>}
          {suppression?.erreur && <p className="erreur">La suppression a échoué : {suppression.erreur}</p>}
        </div>
      </>
    );
  } else {
    contenu = (
      <EcranAccueil
        chantiers={chantiers}
        onNouveau={() => aller({ type: "choix" })}
        onBrouillon={id => aller({ type: "saisie", chantierId: id })}
        onOuvrirCR={(chantierId, crId) => aller({ type: "cr", chantierId, crId })}
      />
    );
  }

  return (
    <div className="appli">
      {barre}
      {erreur && chantiers && <p className="bandeau-erreur">{erreur}</p>}
      <main className="une-colonne">
        {contenu}
        {ecran.type !== "saisie" && pied}
      </main>
      {transmission && cr && (
        <FenetreTransmission
          chantier={chantier} cr={cr} auteur={conducteurChoisi || email}
          onFermer={() => setTransmission(false)}
          onTransmis={async () => { setTransmission(false); await charger(); }}
        />
      )}
      {visionneuse && (
        <VisionneusePhoto
          photos={visionneuse.photos} index={visionneuse.index}
          onIndex={i => setVisionneuse(v => ({ ...v, index: i }))}
          onFermer={() => setVisionneuse(null)}
        />
      )}
    </div>
  );
}

function Appli() {
  const [session, setSession] = useState(undefined);
  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => setSession(data.session));
    const { data: ecoute } = supabase.auth.onAuthStateChange((_e, s) => setSession(s));
    return () => ecoute.subscription.unsubscribe();
  }, []);
  if (session === undefined) return <p className="vide">Ouverture…</p>;
  if (!session) return <EcranConnexion />;
  return <EcranPrincipal email={session.user?.email || ""} />;
}

createRoot(document.getElementById("root")).render(<Appli />);
