(async () => {
  "use strict";

  const STORAGE_KEY = "refone-atelier-os-workspace-v2";
  const LEGACY_STORAGE_KEYS = [
    "refone-atelier-os-simple-v1",
    "refone-atelier-os-v1"
  ];
  const SYNC_CODE_KEY = "refone-atelier-os-sync-code-v5";
  const SYNC_SCHEMA = "refone-atelier-os-workspace-v2";
  const FACTURE_SYNC_SCHEMA = "refone-facture-os-workspace-v1";
  const FILE_SCHEMA = "refone-atelier-os-file-v1";
  const FILE_DB_NAME = "refone-atelier-os-files-v1";
  const FILE_STORE_NAME = "files";
  const REMOTE_ORIGIN = "https://refonetech.com";
  const DEFAULT_SYNC_CODE = "RF-ERWANN";
  const MIN_SYNC_CODE_LENGTH = 8;
  const AUTH_SESSION_KEY = "refone-atelier-os-auth-v1";
  const AUTH_USERNAME = "admin";
  const AUTH_PASSWORD_HASH = "430e6d6ab5bad93eaba486226da10ac5ce0f98ecd4c51f04877e4d2c9eb436bc";
  const MAX_PDF_BYTES = 6 * 1024 * 1024;
  const SYNC_POLL_INTERVAL_MS = 120000;
  const SYNC_PUSH_DEBOUNCE_MS = 400;
  const STAGES = [
    { id: "intake", label: "Recu atelier" },
    { id: "diagnostic", label: "Diagnostic" },
    { id: "quote", label: "Attente client" },
    { id: "repair", label: "Reparation" },
    { id: "testing", label: "Tests finaux" },
    { id: "ready", label: "Pret" }
  ];
  const PRIORITY_LABELS = {
    normal: "Normale",
    high: "Haute",
    urgent: "Urgente"
  };
  const CUSTOMER_TYPE_LABELS = {
    individual: "Particulier",
    business: "Professionnel"
  };
  const QUOTE_REQUEST_STATUS = new Set(["pending", "accepted", "dismissed"]);
  const PAGE_META = {
    workshop: {
      kicker: "DOSSIERS EN COURS",
      title: "Vue atelier",
      copy: "Retrouvez ici les dossiers actifs a traiter, modifiez-les et recherchez instantanement par client ou appareil."
    },
    archives: {
      kicker: "ARCHIVES",
      title: "Dossiers archives",
      copy: "Tous les dossiers marques comme prets arrivent ici automatiquement avec leur historique et leurs PDFs."
    }
  };

  const ui = {
    search: "",
    filterStage: "all",
    page: "workshop"
  };

  const elements = {
    authGate: document.querySelector("[data-auth-gate]"),
    authShell: document.querySelector("[data-auth-shell]"),
    authForm: document.querySelector("[data-auth-form]"),
    authUsername: document.querySelector("[data-auth-username]"),
    authPassword: document.querySelector("[data-auth-password]"),
    authError: document.querySelector("[data-auth-error]"),
    lockSession: document.querySelector("[data-lock-session]"),
    pageKicker: document.querySelector("[data-panel-kicker]"),
    pageTitle: document.querySelector("[data-panel-title]"),
    pageCopy: document.querySelector("[data-panel-copy]"),
    filterGroup: document.querySelector("[data-filter-group]"),
    searchInput: document.querySelector("[data-search-input]"),
    ticketList: document.querySelector("[data-ticket-list]"),
    quoteQueue: document.querySelector("[data-quote-queue]"),
    ticketCount: document.querySelector("[data-ticket-count]"),
    detail: document.querySelector("[data-ticket-detail]"),
    stats: document.querySelector("[data-stats]"),
    stockList: document.querySelector("[data-stock-list]"),
    messageOutput: document.querySelector("[data-message-output]"),
    syncSummary: document.querySelector("[data-sync-summary]"),
    syncStatus: document.querySelector("[data-sync-status]"),
    syncModal: document.querySelector("[data-sync-modal]"),
    syncInput: document.querySelector("[data-sync-code]"),
    resetPanel: document.querySelector("[data-reset-panel]"),
    createModal: document.querySelector("[data-create-modal]"),
    customerDatalist: document.querySelector("#atelierKnownCustomers"),
    pdfModal: document.querySelector("[data-pdf-modal]"),
    pdfFrame: document.querySelector("[data-pdf-frame]"),
    pdfTitle: document.querySelector("[data-pdf-title]"),
    pdfStatus: document.querySelector("[data-pdf-status]"),
    toast: document.querySelector("[data-toast]")
  };

  const syncRuntime = {
    apiUrl: resolveApiUrl("/api/atelier-os-sync"),
    factureApiUrl: resolveApiUrl("/api/facture-os-sync"),
    fileApiUrl: resolveApiUrl("/api/atelier-os-file"),
    lastRemoteUpdatedAt: "",
    eventSource: null,
    streamConnected: false,
    pollTimer: 0,
    pushTimer: 0,
    isApplyingRemote: false,
    isPushing: false
  };

  let fileDbPromise = null;
  let isBooted = false;
  let activePdfUrl = "";
  let activePdfName = "";
  const state = loadState();

  function createEmptyWorkspace() {
    return {
      selectedTicketId: "",
      messageKind: "update",
      quoteRequests: [],
      tickets: [],
      stock: []
    };
  }

  async function sha256Hex(value) {
    const digest = await crypto.subtle.digest(
      "SHA-256",
      new TextEncoder().encode(String(value || ""))
    );
    return [...new Uint8Array(digest)]
      .map((item) => item.toString(16).padStart(2, "0"))
      .join("");
  }

  function isSessionUnlocked() {
    try {
      return sessionStorage.getItem(AUTH_SESSION_KEY) === "ok";
    } catch {
      return false;
    }
  }

  function storeSessionUnlocked() {
    try {
      sessionStorage.setItem(AUTH_SESSION_KEY, "ok");
    } catch {}
  }

  function clearUnlockedSession() {
    try {
      sessionStorage.removeItem(AUTH_SESSION_KEY);
    } catch {}
  }

  function setAuthError(message = "") {
    if (!elements.authError) return;
    elements.authError.textContent = message;
    elements.authError.hidden = !message;
  }

  function showAuthGate(message = "") {
    document.body.classList.add("is-locked");
    if (elements.authGate) {
      elements.authGate.hidden = false;
    }
    if (elements.authShell) {
      elements.authShell.hidden = true;
    }
    if (elements.authUsername && !elements.authUsername.value) {
      elements.authUsername.value = AUTH_USERNAME;
    }
    if (elements.authPassword) {
      elements.authPassword.value = "";
    }
    setAuthError(message);
    window.setTimeout(() => {
      if (elements.authPassword && elements.authUsername?.value) {
        elements.authPassword.focus();
        return;
      }
      elements.authUsername?.focus();
    }, 0);
  }

  function hideAuthGate() {
    document.body.classList.remove("is-locked");
    if (elements.authGate) {
      elements.authGate.hidden = true;
    }
    if (elements.authShell) {
      elements.authShell.hidden = false;
    }
    if (elements.authPassword) {
      elements.authPassword.value = "";
    }
    setAuthError("");
  }

  function clone(value) {
    return JSON.parse(JSON.stringify(value));
  }

  function purgeLegacyState() {
    LEGACY_STORAGE_KEYS.forEach((key) => {
      try {
        localStorage.removeItem(key);
      } catch {}
    });
  }

  function normalizeCode(value) {
    return String(value || "")
      .toUpperCase()
      .replace(/[^A-Z0-9-]/g, "")
      .slice(0, 64);
  }

  function normalizeStage(value) {
    return STAGES.some((stage) => stage.id === value) ? value : "intake";
  }

  function normalizePriority(value) {
    return value === "urgent" || value === "high" ? value : "normal";
  }

  function normalizeCustomerType(value) {
    return CUSTOMER_TYPE_LABELS[value] ? value : "individual";
  }

  function normalizeMessageKind(value) {
    return ["update", "quote", "ready", "relance"].includes(value) ? value : "update";
  }

  function normalizeQuoteRequestStatus(value) {
    return QUOTE_REQUEST_STATUS.has(value) ? value : "pending";
  }

  function normalizePage(value) {
    return value === "archives" ? "archives" : "workshop";
  }

  function normalizeAttachmentMeta(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    return {
      id: String(input.id || "").slice(0, 120),
      name: String(input.name || "Document.pdf").slice(0, 220),
      size: Math.max(0, Number(input.size || 0)),
      mimeType: String(input.mimeType || "application/pdf").slice(0, 120),
      uploadedAt: String(input.uploadedAt || new Date().toISOString()).slice(0, 40)
    };
  }

  function normalizeActivity(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    return {
      title: String(input.title || "Note atelier").slice(0, 120),
      note: String(input.note || "").slice(0, 1500),
      date: String(input.date || new Date().toISOString()).slice(0, 40),
      tag: String(input.tag || "Atelier").slice(0, 60)
    };
  }

  function normalizeTicket(raw, index) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const code = String(input.code || input.id || `RF-ITEM-${index + 1}`).slice(0, 120);
    const stage = normalizeStage(input.stage);
    const archivedAt = String(input.archivedAt || "").slice(0, 40);
    const restoreStage = normalizeStage(
      input.restoreStage || (stage === "ready" ? "testing" : stage)
    );

    return {
      id: String(input.id || code).slice(0, 120),
      code,
      customerName: String(input.customerName || "Client").slice(0, 160),
      customerEmail: String(input.customerEmail || "").slice(0, 220),
      customerPhone: String(input.customerPhone || "").slice(0, 80),
      customerAddress: String(input.customerAddress || "").slice(0, 400),
      customerType: normalizeCustomerType(input.customerType),
      device: String(input.device || "Appareil").slice(0, 180),
      issue: String(input.issue || "").slice(0, 2000),
      stage,
      priority: normalizePriority(input.priority),
      technician: String(input.technician || "").slice(0, 120),
      estimate: Number.isFinite(Number(input.estimate)) ? Number(input.estimate) : 0,
      deposit: Number.isFinite(Number(input.deposit)) ? Number(input.deposit) : 0,
      dueAt: String(input.dueAt || "").slice(0, 40),
      receivedAt: String(input.receivedAt || new Date().toISOString()).slice(0, 40),
      serial: String(input.serial || "").slice(0, 160),
      sourceDocumentId: String(input.sourceDocumentId || "").slice(0, 120),
      sourceDocumentNumber: String(input.sourceDocumentNumber || "").slice(0, 120),
      archivedAt,
      restoreStage,
      attachments: Array.isArray(input.attachments)
        ? input.attachments.slice(0, 24).map(normalizeAttachmentMeta).filter((attachment) => attachment.id)
        : [],
      activities: Array.isArray(input.activities)
        ? input.activities.slice(-80).map(normalizeActivity)
        : []
    };
  }

  function normalizeQuoteRequest(raw, index = 0) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const sourceDocumentId = String(input.sourceDocumentId || input.id || `quote-${index + 1}`).slice(0, 120);
    const createdAt = String(input.createdAt || new Date().toISOString()).slice(0, 40);
    const updatedAt = String(input.updatedAt || createdAt).slice(0, 40);
    return {
      id: String(input.id || `quote-request-${sourceDocumentId}`).slice(0, 140),
      sourceDocumentId,
      sourceDocumentNumber: String(input.sourceDocumentNumber || "").slice(0, 120),
      title: String(input.title || "Devis accepte").slice(0, 220),
      customerName: String(input.customerName || "Client").slice(0, 160),
      customerEmail: String(input.customerEmail || "").slice(0, 220),
      customerPhone: String(input.customerPhone || "").slice(0, 80),
      customerAddress: String(input.customerAddress || "").slice(0, 400),
      customerType: normalizeCustomerType(input.customerType),
      device: String(input.device || "Appareil").slice(0, 180),
      issue: String(input.issue || "").slice(0, 2000),
      estimate: Number.isFinite(Number(input.estimate)) ? Number(input.estimate) : 0,
      status: normalizeQuoteRequestStatus(input.status),
      sourceUpdatedAt: String(input.sourceUpdatedAt || "").slice(0, 40),
      createdAt,
      updatedAt
    };
  }

  function normalizeStockItem(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const idSeed = String(input.id || input.name || "").trim();

    return {
      id: idSeed ? idSeed.slice(0, 120) : createId("stock"),
      name: String(input.name || "Piece").slice(0, 180),
      qty: Math.max(0, Math.round(Number(input.qty || 0))),
      target: Math.max(0, Math.round(Number(input.target || 0))),
      zone: String(input.zone || "").slice(0, 120)
    };
  }

  function normalizeWorkspace(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    const tickets = Array.isArray(input.tickets) ? input.tickets.slice(0, 500).map(normalizeTicket) : [];
    const quoteRequests = Array.isArray(input.quoteRequests) ? input.quoteRequests.slice(0, 500).map(normalizeQuoteRequest) : [];
    const stock = Array.isArray(input.stock) ? input.stock.slice(0, 250).map(normalizeStockItem) : [];
    const selectedTicketId = String(input.selectedTicketId || "").slice(0, 120);

    return {
      selectedTicketId: tickets.some((ticket) => ticket.id === selectedTicketId) ? selectedTicketId : (tickets[0]?.id || ""),
      messageKind: normalizeMessageKind(input.messageKind),
      quoteRequests,
      tickets,
      stock
    };
  }

  function loadState() {
    purgeLegacyState();

    try {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (saved) {
        return normalizeWorkspace(JSON.parse(saved));
      }
    } catch (error) {
      console.warn("Impossible de charger Atelier OS", error);
    }

    return createEmptyWorkspace();
  }

  function saveState() {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizeWorkspace(state)));
    } catch (error) {
      console.warn("Impossible d'enregistrer Atelier OS", error);
    }
  }

  function resolveApiUrl(path) {
    const host = String(window.location.hostname || "").toLowerCase();
    const isRemoteHttp = /^https?:$/.test(window.location.protocol) && host && host !== "127.0.0.1" && host !== "localhost";
    const base = isRemoteHttp ? window.location.origin : REMOTE_ORIGIN;
    return new URL(path, base).toString();
  }

  function createId(prefix) {
    if (crypto.randomUUID) {
      return `${prefix}-${crypto.randomUUID()}`;
    }
    return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
  }

  function openFileDb() {
    if (fileDbPromise) return fileDbPromise;
    if (!("indexedDB" in window)) {
      fileDbPromise = Promise.resolve(null);
      return fileDbPromise;
    }

    fileDbPromise = new Promise((resolve) => {
      const request = indexedDB.open(FILE_DB_NAME, 1);
      request.onupgradeneeded = () => {
        if (!request.result.objectStoreNames.contains(FILE_STORE_NAME)) {
          request.result.createObjectStore(FILE_STORE_NAME, { keyPath: "id" });
        }
      };
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => resolve(null);
    });

    return fileDbPromise;
  }

  async function putLocalFile(fileRecord) {
    const db = await openFileDb();
    if (!db) return false;

    return new Promise((resolve) => {
      const transaction = db.transaction(FILE_STORE_NAME, "readwrite");
      transaction.objectStore(FILE_STORE_NAME).put(fileRecord);
      transaction.oncomplete = () => resolve(true);
      transaction.onerror = () => resolve(false);
    });
  }

  async function getLocalFile(id) {
    const db = await openFileDb();
    if (!db) return null;

    return new Promise((resolve) => {
      const transaction = db.transaction(FILE_STORE_NAME, "readonly");
      const request = transaction.objectStore(FILE_STORE_NAME).get(id);
      request.onsuccess = () => resolve(request.result || null);
      request.onerror = () => resolve(null);
    });
  }

  async function deleteLocalFile(id) {
    const db = await openFileDb();
    if (!db) return false;

    return new Promise((resolve) => {
      const transaction = db.transaction(FILE_STORE_NAME, "readwrite");
      transaction.objectStore(FILE_STORE_NAME).delete(id);
      transaction.oncomplete = () => resolve(true);
      transaction.onerror = () => resolve(false);
    });
  }

  function pageHashValue(page) {
    return normalizePage(page) === "archives" ? "#archives" : "#atelier";
  }

  function pageFromHash() {
    const hash = String(window.location.hash || "")
      .replace(/^#/, "")
      .trim()
      .toLowerCase();
    return hash === "archives" ? "archives" : "workshop";
  }

  function syncHashForPage(replace = false) {
    const nextHash = pageHashValue(ui.page);
    if (window.location.hash === nextHash) return;
    const nextUrl = `${window.location.pathname}${window.location.search}${nextHash}`;

    if (replace && window.history?.replaceState) {
      window.history.replaceState(null, "", nextUrl);
      return;
    }

    if (window.history?.pushState) {
      window.history.pushState(null, "", nextUrl);
      return;
    }

    window.location.hash = nextHash;
  }

  function setPage(nextPage, options = {}) {
    ui.page = normalizePage(nextPage);
    if (ui.page === "archives") {
      ui.filterStage = "all";
    }
    if (options.syncHash !== false) {
      syncHashForPage(Boolean(options.replaceHash));
    }
    if (options.render !== false) {
      render();
    }
  }

  function selectedPool() {
    return ui.page === "archives"
      ? state.tickets.filter((ticket) => ticket.archivedAt)
      : state.tickets.filter((ticket) => !ticket.archivedAt);
  }

  function normalizeSelection(pool = selectedPool()) {
    if (!pool.some((ticket) => ticket.id === state.selectedTicketId)) {
      state.selectedTicketId = pool[0]?.id || "";
    }
  }

  function selectedTicket() {
    return state.tickets.find((ticket) => ticket.id === state.selectedTicketId) || null;
  }

  function normalizeCustomerLookupValue(value) {
    return String(value || "")
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .toLowerCase()
      .replace(/\s+/g, " ")
      .trim();
  }

  function customerProfiles() {
    const profiles = new Map();
    const tickets = clone(state.tickets).sort((left, right) => {
      const leftTime = new Date(left.archivedAt || left.receivedAt || 0).getTime();
      const rightTime = new Date(right.archivedAt || right.receivedAt || 0).getTime();
      return rightTime - leftTime;
    });

    tickets.forEach((ticket) => {
      const name = String(ticket.customerName || "").trim();
      const key = normalizeCustomerLookupValue(name);
      if (!key) return;

      const existing = profiles.get(key);
      const snapshot = {
        key,
        name,
        phone: String(ticket.customerPhone || "").trim(),
        email: String(ticket.customerEmail || "").trim(),
        address: String(ticket.customerAddress || "").trim(),
        customerType: normalizeCustomerType(ticket.customerType),
        device: String(ticket.device || "").trim(),
        code: String(ticket.code || "").trim()
      };

      if (!existing) {
        profiles.set(key, snapshot);
        return;
      }

      if (!existing.phone && snapshot.phone) existing.phone = snapshot.phone;
      if (!existing.email && snapshot.email) existing.email = snapshot.email;
      if (!existing.address && snapshot.address) existing.address = snapshot.address;
      if (!existing.customerType && snapshot.customerType) existing.customerType = snapshot.customerType;
      if (!existing.device && snapshot.device) existing.device = snapshot.device;
      if (!existing.code && snapshot.code) existing.code = snapshot.code;
    });

    return [...profiles.values()].sort((left, right) => left.name.localeCompare(right.name, "fr"));
  }

  function syncCustomerDatalist() {
    if (!elements.customerDatalist) return;
    const profiles = customerProfiles();
    elements.customerDatalist.innerHTML = profiles
      .map((profile) => `<option value="${escapeHtml(profile.name)}"></option>`)
      .join("");
  }

  function resolveCustomerProfile(rawValue) {
    const query = normalizeCustomerLookupValue(rawValue);
    if (!query) return null;

    const profiles = customerProfiles();
    const exact = profiles.find((profile) => profile.key === query);
    if (exact) return exact;

    const prefixMatches = profiles.filter((profile) => profile.key.startsWith(query));
    if (prefixMatches.length === 1) return prefixMatches[0];

    return null;
  }

  function customerFormFields(form) {
    if (!form) return null;
    return {
      name: form.querySelector('[name="customerName"]'),
      phone: form.querySelector('[name="customerPhone"]'),
      email: form.querySelector('[name="customerEmail"]'),
      address: form.querySelector('[name="customerAddress"]'),
      customerType: form.querySelector('[name="customerType"]'),
      status: form.querySelector("[data-customer-lookup-status]")
    };
  }

  function fillFieldFromProfile(field, nextValue, options = {}) {
    if (!field || !nextValue) return;
    const currentValue = String(field.value || "").trim();
    const previousAutofill = String(field.dataset.autofillValue || "").trim();
    if (!options.force && currentValue && currentValue !== previousAutofill) return;
    field.value = nextValue;
    field.dataset.autofillValue = nextValue;
  }

  function updateCustomerLookup(form, rawValue, options = {}) {
    const fields = customerFormFields(form);
    if (!fields?.name) return;

    const profile = resolveCustomerProfile(rawValue);
    if (!profile) {
      if (fields.status) {
        fields.status.textContent = rawValue
          ? "Continuez a saisir ou choisissez un client connu pour pre remplir ses coordonnees."
          : "Les clients deja connus se proposeront automatiquement.";
      }
      return;
    }

    if (options.commit) {
      fields.name.value = profile.name;
    }
    fillFieldFromProfile(fields.phone, profile.phone, options);
    fillFieldFromProfile(fields.email, profile.email, options);
    fillFieldFromProfile(fields.address, profile.address, options);
    if (fields.customerType && profile.customerType) {
      fields.customerType.value = normalizeCustomerType(profile.customerType);
    }

    if (fields.status) {
      const helperParts = [
        profile.phone || "",
        profile.email || "",
        profile.device ? `Dernier appareil: ${profile.device}` : ""
      ].filter(Boolean);
      fields.status.textContent = helperParts.length
        ? `Client connu · ${helperParts.join(" · ")}`
        : "Client connu dans Atelier OS.";
    }
  }

  function syncVisibleCustomerForms() {
    document.querySelectorAll('form [name="customerName"]').forEach((input) => {
      if (!(input instanceof HTMLInputElement)) return;
      updateCustomerLookup(input.closest("form"), input.value, { force: false, commit: false });
    });
  }

  function stageMeta(stageId) {
    return STAGES.find((stage) => stage.id === stageId) || STAGES[0];
  }

  function supportsLiveSync() {
    return false;
  }

  function liveSyncUrl(code) {
    const url = new URL(syncRuntime.apiUrl);
    url.searchParams.set("code", code);
    url.searchParams.set("stream", "1");
    return url.toString();
  }

  function closeLiveSync() {
    if (syncRuntime.eventSource) {
      syncRuntime.eventSource.close();
      syncRuntime.eventSource = null;
    }
    syncRuntime.streamConnected = false;
    updateSyncIndicator();
  }

  function formatSyncMoment(value) {
    if (!value) return "";

    try {
      return new Intl.DateTimeFormat("fr-FR", {
        day: "2-digit",
        month: "2-digit",
        hour: "2-digit",
        minute: "2-digit"
      }).format(new Date(value));
    } catch {
      return "";
    }
  }

  function currency(value) {
    const amount = Number(value || 0);
    return new Intl.NumberFormat("fr-FR", {
      style: "currency",
      currency: "EUR"
    }).format(amount);
  }

  function bytesLabel(value) {
    if (!value) return "0 Ko";
    if (value < 1024) return `${value} o`;
    if (value < 1024 * 1024) return `${Math.round(value / 102.4) / 10} Ko`;
    return `${Math.round(value / 104857.6) / 10} Mo`;
  }

  function dateLabel(value) {
    if (!value) return "Non renseigne";

    try {
      return new Intl.DateTimeFormat("fr-FR", {
        dateStyle: "medium",
        timeStyle: "short"
      }).format(new Date(value));
    } catch {
      return "Non renseigne";
    }
  }

  function toDateTimeLocalValue(value) {
    if (!value) return "";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) {
      return String(value).slice(0, 16);
    }
    const pad = (number) => String(number).padStart(2, "0");
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
  }

  function relativeDueLabel(value) {
    if (!value) return "Sans echeance";

    const delta = new Date(value).getTime() - Date.now();
    const hours = Math.round(delta / 36e5);

    if (hours < 0) return "En retard";
    if (hours <= 6) return "Tres proche";
    if (hours <= 24) return "Aujourd'hui / demain";
    return "Planifie";
  }

  function escapeHtml(value) {
    return String(value ?? "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  function readSyncCode() {
    return normalizeCode(elements.syncInput?.value || localStorage.getItem(SYNC_CODE_KEY) || DEFAULT_SYNC_CODE);
  }

  function storeSyncCode(code) {
    const normalized = normalizeCode(code);
    if (elements.syncInput) {
      elements.syncInput.value = normalized;
    }
    try {
      localStorage.setItem(SYNC_CODE_KEY, normalized);
    } catch {}
    updateSyncIndicator();
    restartSyncTransport();
    return normalized;
  }

  function validateSyncCode() {
    const code = storeSyncCode(readSyncCode());
    if (code.length < MIN_SYNC_CODE_LENGTH) {
      setSyncStatus(`Le code prive doit contenir au moins ${MIN_SYNC_CODE_LENGTH} caracteres.`, true);
      elements.syncInput?.focus();
      return "";
    }
    return code;
  }

  function generateSyncCode() {
    const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
    const bytes = new Uint8Array(15);
    crypto.getRandomValues(bytes);
    const raw = Array.from(bytes, (value) => alphabet[value % alphabet.length]).join("");
    const code = `RF-${raw.match(/.{1,5}/g).join("-")}`;
    storeSyncCode(code);
    setSyncStatus("Code cree. Reprenez exactement ce code sur chaque support.");
    showToast("Code de synchro cree");
  }

  function setSyncStatus(message, isError = false) {
    if (!elements.syncStatus) return;
    elements.syncStatus.textContent = message;
    elements.syncStatus.classList.toggle("is-error", isError);
  }

  function updateSyncIndicator() {
    const hasCode = readSyncCode().length >= MIN_SYNC_CODE_LENGTH;
    document.querySelectorAll("[data-sync-dot]").forEach((dot) => dot.classList.toggle("is-ready", hasCode));
    document.querySelectorAll("[data-sync-label]").forEach((label) => {
      label.textContent = hasCode ? "Sync active" : "Synchroniser";
    });

    if (!elements.syncSummary) return;

    if (!hasCode) {
      elements.syncSummary.textContent = "Base locale uniquement.";
      return;
    }

    if (syncRuntime.streamConnected && syncRuntime.lastRemoteUpdatedAt) {
      elements.syncSummary.textContent = `Sync active - derniere base visible ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}.`;
      return;
    }

    if (syncRuntime.streamConnected) {
      elements.syncSummary.textContent = "Sync active - en attente de la premiere base.";
      return;
    }

    if (syncRuntime.lastRemoteUpdatedAt) {
      elements.syncSummary.textContent = `Sync active - derniere base visible ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}.`;
      return;
    }

    elements.syncSummary.textContent = "Code actif - liaison en cours.";
  }

  function currentSyncPayload() {
    return {
      schema: SYNC_SCHEMA,
      version: 2,
      source: window.location.protocol === "file:" ? "atelier-os-desktop" : "atelier-os-web",
      updatedAt: new Date().toISOString(),
      workspace: normalizeWorkspace(state)
    };
  }

  function factureDocumentTotal(documentItem) {
    const lines = Array.isArray(documentItem?.lines) ? documentItem.lines : [];
    return lines.reduce((sum, line) => {
      const qty = Number(line.qty || 1);
      const unitHt = Number(line.unitHt || 0);
      const vatRate = Number(line.vatRate || 0);
      if (!Number.isFinite(qty) || !Number.isFinite(unitHt)) return sum;
      const totalHt = unitHt * qty;
      const totalVat = Number.isFinite(vatRate) ? totalHt * (vatRate / 100) : 0;
      return Math.round((sum + totalHt + totalVat) * 100) / 100;
    }, 0);
  }

  function factureDocumentDevice(documentItem) {
    const title = String(documentItem?.title || "").trim();
    const firstLine = Array.isArray(documentItem?.lines) ? String(documentItem.lines[0]?.description || "").trim() : "";
    const candidate = title || firstLine;
    if (!candidate) return "Appareil";
    return candidate
      .replace(/^(devis|facture|bon de commande)\s*[-:]\s*/i, "")
      .replace(/^diagnostic et intervention atelier\s*[-:]\s*/i, "")
      .replace(/^intervention atelier\s*[-:]\s*/i, "")
      .slice(0, 180) || "Appareil";
  }

  function quoteRequestFromFactureDocument(documentItem) {
    const sourceDocumentId = String(documentItem.id || "").slice(0, 120);
    return normalizeQuoteRequest({
      id: `quote-request-${sourceDocumentId}`,
      sourceDocumentId,
      sourceDocumentNumber: documentItem.number || "DEVIS",
      title: documentItem.title || "Devis accepte",
      customerName: documentItem.customerName || "Client",
      customerEmail: documentItem.customerEmail || "",
      customerPhone: documentItem.customerPhone || "",
      customerAddress: documentItem.customerAddress || "",
      customerType: documentItem.customerType || "individual",
      device: factureDocumentDevice(documentItem),
      issue: [documentItem.title, documentItem.noteToCustomer].filter(Boolean).join("\n"),
      estimate: factureDocumentTotal(documentItem),
      status: "pending",
      sourceUpdatedAt: documentItem.updatedAt || "",
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    });
  }

  function mergeFactureQuoteRequest(request) {
    if (!request.sourceDocumentId) return false;
    const existingTicket = state.tickets.find((ticket) => ticket.sourceDocumentId === request.sourceDocumentId);
    if (existingTicket) return false;

    const existing = state.quoteRequests.find((item) => item.sourceDocumentId === request.sourceDocumentId);
    if (existing) {
      if (existing.status !== "pending") return false;
      existing.sourceDocumentNumber = request.sourceDocumentNumber || existing.sourceDocumentNumber;
      existing.title = request.title || existing.title;
      existing.customerName = request.customerName || existing.customerName;
      existing.customerEmail = request.customerEmail || existing.customerEmail;
      existing.customerPhone = request.customerPhone || existing.customerPhone;
      existing.customerAddress = request.customerAddress || existing.customerAddress;
      existing.customerType = request.customerType || existing.customerType;
      existing.device = request.device || existing.device;
      existing.issue = request.issue || existing.issue;
      existing.estimate = request.estimate || existing.estimate;
      existing.sourceUpdatedAt = request.sourceUpdatedAt || existing.sourceUpdatedAt;
      existing.updatedAt = new Date().toISOString();
      return true;
    }

    state.quoteRequests.unshift(request);
    return true;
  }

  async function syncAcceptedQuotesFromFacture(options = {}) {
    const code = readSyncCode();
    if (code.length < MIN_SYNC_CODE_LENGTH) return;

    try {
      const response = await fetch(`${syncRuntime.factureApiUrl}?code=${encodeURIComponent(code)}`, {
        headers: { accept: "application/json" }
      });
      const result = await response.json();
      if (!response.ok) {
        if (!options.silent && response.status !== 404) {
          showToast(result.error || "Import FactureOS impossible");
        }
        return;
      }

      const payload = result.snapshot?.payload;
      if (!payload || payload.schema !== FACTURE_SYNC_SCHEMA) return;
      const documents = Array.isArray(payload.workspace?.documents) ? payload.workspace.documents : [];
      let changed = false;
      documents
        .filter((documentItem) => documentItem.type === "quote" && documentItem.status === "approved")
        .forEach((documentItem) => {
          if (mergeFactureQuoteRequest(quoteRequestFromFactureDocument(documentItem))) {
            changed = true;
          }
        });

      if (changed) {
        render();
        queueAutoPush();
        if (!options.silent) {
          showToast("File devis acceptes mise a jour");
        }
      }
    } catch (error) {
      if (!options.silent) {
        showToast(error.message || "Import FactureOS impossible");
      }
    }
  }

  function normalizeSyncPayload(raw) {
    const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
    return {
      schema: String(input.schema || SYNC_SCHEMA),
      version: Number(input.version || 2),
      source: String(input.source || "unknown").slice(0, 60),
      updatedAt: String(input.updatedAt || new Date().toISOString()).slice(0, 40),
      workspace: normalizeWorkspace(input.workspace || input.payload || input.state || {})
    };
  }

  function applyWorkspace(workspace) {
    const normalized = normalizeWorkspace(workspace);
    state.selectedTicketId = normalized.selectedTicketId;
    state.messageKind = normalized.messageKind;
    state.quoteRequests = normalized.quoteRequests;
    state.tickets = normalized.tickets;
    state.stock = normalized.stock;
  }

  function applyRemoteSnapshot(snapshot, statusMessage) {
    const payload = normalizeSyncPayload(snapshot?.payload);
    syncRuntime.isApplyingRemote = true;
    applyWorkspace(payload.workspace);
    syncRuntime.lastRemoteUpdatedAt = String(snapshot?.updatedAt || payload.updatedAt || new Date().toISOString());
    render();
    syncRuntime.isApplyingRemote = false;
    updateSyncIndicator();
    setSyncStatus(`${statusMessage} - ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}`);
  }

  async function pushSync(options = {}) {
    const code = validateSyncCode();
    if (!code || syncRuntime.isApplyingRemote) return;

    if (!options.silent) {
      setSyncStatus("Envoi de la base atelier...");
    }

    syncRuntime.isPushing = true;
    try {
      const response = await fetch(syncRuntime.apiUrl, {
        method: "PUT",
        headers: {
          "content-type": "application/json"
        },
        body: JSON.stringify({
          code,
          payload: currentSyncPayload()
        }),
        keepalive: options.keepalive === true
      });
      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error || "Synchronisation impossible.");
      }

      syncRuntime.lastRemoteUpdatedAt = String(result.updatedAt || new Date().toISOString());
      updateSyncIndicator();
      setSyncStatus(`Base envoyee - ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}`);
      if (!options.silent) {
        showToast("Base atelier synchronisee");
      }
    } catch (error) {
      if (!options.silent) {
        setSyncStatus(error.message || "Synchronisation impossible.", true);
      }
    } finally {
      syncRuntime.isPushing = false;
    }
  }

  async function pullSync(options = {}) {
    const code = validateSyncCode();
    if (!code) return;

    if (!options.silent) {
      setSyncStatus("Recuperation de la base distante...");
    }

    try {
      const response = await fetch(`${syncRuntime.apiUrl}?code=${encodeURIComponent(code)}`, {
        headers: {
          accept: "application/json"
        }
      });
      const result = await response.json();
      if (!response.ok) {
        if (options.allowMissing && response.status === 404) {
          if (!options.silent) {
            setSyncStatus("Aucune base distante pour ce code pour le moment.");
          }
          return;
        }
        throw new Error(result.error || "Recuperation impossible.");
      }

      const incomingAt = String(result.snapshot?.updatedAt || "");
      if (!options.force && incomingAt && syncRuntime.lastRemoteUpdatedAt && incomingAt <= syncRuntime.lastRemoteUpdatedAt) {
        if (!options.silent) {
          setSyncStatus(`Base deja a jour - ${formatSyncMoment(syncRuntime.lastRemoteUpdatedAt)}`);
        }
        return;
      }

      applyRemoteSnapshot(result.snapshot, "Base recue");
      await syncAcceptedQuotesFromFacture({ silent: true });
      if (!options.silent) {
        showToast("Base atelier recuperee");
      }
    } catch (error) {
      if (!options.silent) {
        setSyncStatus(error.message || "Recuperation impossible.", true);
      }
    }
  }

  function startLiveSync() {
    const code = readSyncCode();
    if (!supportsLiveSync() || code.length < MIN_SYNC_CODE_LENGTH) return false;

    closeLiveSync();
    const source = new EventSource(liveSyncUrl(code));
    syncRuntime.eventSource = source;

    source.onopen = () => {
      if (syncRuntime.eventSource !== source) return;
      syncRuntime.streamConnected = true;
      updateSyncIndicator();
      setSyncStatus("Connexion live active.");
    };

    source.addEventListener("snapshot", (event) => {
      if (syncRuntime.eventSource !== source) return;
      try {
        const data = JSON.parse(event.data || "{}");
        const snapshot = data?.snapshot;
        const incomingAt = String(snapshot?.updatedAt || "");
        if (!incomingAt) return;

        if (syncRuntime.lastRemoteUpdatedAt && incomingAt <= syncRuntime.lastRemoteUpdatedAt) {
          syncRuntime.streamConnected = true;
          updateSyncIndicator();
          return;
        }

        applyRemoteSnapshot(snapshot, "Base recue en direct");
      } catch (error) {
        console.warn("Flux live illisible", error);
      }
    });

    source.addEventListener("keepalive", () => {
      if (syncRuntime.eventSource !== source) return;
      syncRuntime.streamConnected = true;
      updateSyncIndicator();
    });

    source.onerror = () => {
      if (syncRuntime.eventSource !== source) return;
      syncRuntime.streamConnected = false;
      updateSyncIndicator();
    };

    return true;
  }

  function queueAutoPush() {
    const code = readSyncCode();
    if (code.length < MIN_SYNC_CODE_LENGTH || syncRuntime.isApplyingRemote) return;
    window.clearTimeout(syncRuntime.pushTimer);
    syncRuntime.pushTimer = window.setTimeout(() => {
      pushSync({ silent: true });
    }, SYNC_PUSH_DEBOUNCE_MS);
  }

  function restartSyncTransport() {
    window.clearInterval(syncRuntime.pollTimer);
    closeLiveSync();
    const code = readSyncCode();
    if (code.length < MIN_SYNC_CODE_LENGTH) {
      updateSyncIndicator();
      return;
    }

    if (startLiveSync()) {
      return;
    }

    syncRuntime.pollTimer = window.setInterval(() => {
      if (document.visibilityState !== "visible") return;
      pullSync({ silent: true, allowMissing: true });
    }, SYNC_POLL_INTERVAL_MS);
    updateSyncIndicator();
  }

  function statsModel() {
    const active = state.tickets.filter((ticket) => !ticket.archivedAt);
    const archived = state.tickets.filter((ticket) => ticket.archivedAt);
    const waiting = active.filter((ticket) => ticket.stage === "quote").length;
    const urgent = active.filter((ticket) => ticket.priority === "urgent").length;
    const pendingQuotes = state.quoteRequests.filter((request) => request.status === "pending").length;

    return [
      {
        label: "Dossiers en cours",
        value: active.length,
        meta: "Tous les dossiers encore a traiter."
      },
      {
        label: "Archives",
        value: archived.length,
        meta: "Dossiers termines et ranges."
      },
      {
        label: "Attente client",
        value: waiting,
        meta: "Devis ou validation en attente."
      },
      {
        label: "Devis acceptes",
        value: pendingQuotes,
        meta: "A valider avant creation atelier."
      },
      {
        label: "Urgences",
        value: urgent,
        meta: "Dossiers prioritaires dans l'atelier."
      }
    ];
  }

  function filteredTickets() {
    const pool = selectedPool();
    const query = ui.search.trim().toLowerCase();

    return clone(pool)
      .filter((ticket) => ui.page === "archives" || ui.filterStage === "all" || ticket.stage === ui.filterStage)
      .filter((ticket) => {
        if (!query) return true;

        return [
          ticket.code,
          ticket.customerName,
          ticket.customerEmail,
          ticket.customerPhone,
          ticket.customerAddress,
          ticket.device,
          ticket.issue,
          ticket.technician,
          ticket.serial
        ]
          .filter(Boolean)
          .join(" ")
          .toLowerCase()
          .includes(query);
      })
      .sort((left, right) => {
        const priorityWeight = { urgent: 0, high: 1, normal: 2 };
        const priorityDelta = priorityWeight[left.priority] - priorityWeight[right.priority];
        if (!left.archivedAt && !right.archivedAt && priorityDelta !== 0) return priorityDelta;

        const leftTime = new Date(left.archivedAt || left.dueAt || left.receivedAt).getTime();
        const rightTime = new Date(right.archivedAt || right.dueAt || right.receivedAt).getTime();
        return ui.page === "archives" ? rightTime - leftTime : leftTime - rightTime;
      });
  }

  function renderStats() {
    elements.stats.innerHTML = statsModel()
      .map((stat) => `
        <article class="atelier-stat-card">
          <span class="atelier-stat-label">${escapeHtml(stat.label)}</span>
          <p class="atelier-stat-value">${escapeHtml(stat.value)}</p>
          <p class="atelier-stat-meta">${escapeHtml(stat.meta)}</p>
        </article>
      `)
      .join("");
  }

  function renderQuoteQueue() {
    if (!elements.quoteQueue) return;
    const requests = state.quoteRequests
      .filter((request) => request.status === "pending")
      .sort((left, right) => new Date(right.sourceUpdatedAt || right.updatedAt).getTime() - new Date(left.sourceUpdatedAt || left.updatedAt).getTime());

    if (ui.page !== "workshop" || !requests.length) {
      elements.quoteQueue.innerHTML = "";
      elements.quoteQueue.hidden = true;
      return;
    }

    elements.quoteQueue.hidden = false;
    elements.quoteQueue.innerHTML = `
      <article class="atelier-quote-card">
        <div class="atelier-panel-head">
          <div>
            <span class="atelier-panel-kicker">DEVIS ACCEPTES</span>
            <h2>File d'attente FactureOS</h2>
            <p class="atelier-empty-text">Ces devis sont marques comme acceptes. Creez seulement les dossiers utiles en atelier.</p>
          </div>
        </div>
        <div class="atelier-quote-list">
          ${requests.map((request) => `
            <article class="atelier-ticket-card">
              <div class="atelier-ticket-top">
                <div class="atelier-ticket-main">
                  <span class="atelier-card-code">${escapeHtml(request.sourceDocumentNumber || "DEVIS")}</span>
                  <span class="atelier-ticket-title">${escapeHtml(request.customerName)}</span>
                  <span class="atelier-ticket-meta">${escapeHtml(request.device || request.title)}</span>
                </div>
                <span class="atelier-alert-chip">${escapeHtml(currency(request.estimate || 0))}</span>
              </div>
              <div class="atelier-ticket-bottom">
                <span class="atelier-ticket-meta">${escapeHtml(CUSTOMER_TYPE_LABELS[request.customerType] || CUSTOMER_TYPE_LABELS.individual)}</span>
                <span class="atelier-ticket-meta">${escapeHtml(request.customerPhone || request.customerEmail || "Coordonnees a verifier")}</span>
              </div>
              <div class="atelier-form-actions">
                <button type="button" class="atelier-primary-button" data-accept-quote-request="${escapeHtml(request.id)}">Creer le dossier</button>
                <button type="button" class="atelier-secondary-button" data-dismiss-quote-request="${escapeHtml(request.id)}">Ignorer</button>
              </div>
            </article>
          `).join("")}
        </div>
      </article>
    `;
  }

  function renderViewSwitch() {
    const meta = PAGE_META[ui.page] || PAGE_META.workshop;

    document.querySelectorAll("[data-page-view]").forEach((button) => {
      button.classList.toggle("is-active", button.dataset.pageView === ui.page);
    });
    document.querySelectorAll("[data-filter-stage]").forEach((button) => {
      button.classList.toggle("is-active", button.dataset.filterStage === ui.filterStage);
    });

    if (elements.pageKicker) elements.pageKicker.textContent = meta.kicker;
    if (elements.pageTitle) elements.pageTitle.textContent = meta.title;
    if (elements.pageCopy) elements.pageCopy.textContent = meta.copy;
    if (elements.filterGroup) {
      elements.filterGroup.hidden = ui.page === "archives";
    }
  }

  function renderTickets(tickets = filteredTickets()) {
    const label = ui.page === "archives" ? "archives" : "dossiers";
    elements.ticketCount.textContent = `${tickets.length} ${tickets.length > 1 ? label : label.slice(0, -1)}`;

    if (!tickets.length) {
      elements.ticketList.innerHTML = `
        <article class="atelier-empty-card">
          <h3>${ui.page === "archives" ? "Aucune archive pour le moment" : "Aucun dossier pour le moment"}</h3>
          <p class="atelier-empty-text">${ui.page === "archives" ? "Les dossiers marques comme prets viendront ici automatiquement." : "Ajoutez votre premier dossier atelier, avec toutes les infos utiles des la creation."}</p>
          ${ui.page === "workshop" ? `
            <div class="atelier-inline-actions">
              <button type="button" class="atelier-primary-button" data-open-create>Creer le premier dossier</button>
            </div>
          ` : ""}
        </article>
      `;
      return;
    }

    elements.ticketList.innerHTML = tickets
      .map((ticket) => `
        <button type="button" class="atelier-ticket-card ${ticket.id === state.selectedTicketId ? "is-selected" : ""}" data-select-ticket="${escapeHtml(ticket.id)}">
          <div class="atelier-ticket-top">
            <div class="atelier-ticket-main">
              <span class="atelier-card-code">${escapeHtml(ticket.code)}</span>
              <span class="atelier-ticket-title">${escapeHtml(ticket.customerName)}</span>
              <span class="atelier-ticket-meta">${escapeHtml(ticket.device)}</span>
            </div>
            <div class="atelier-detail-badges">
              <span class="atelier-chip" data-stage="${escapeHtml(ticket.stage)}">${escapeHtml(stageMeta(ticket.stage).label)}</span>
              <span class="atelier-priority-chip" data-priority="${escapeHtml(ticket.priority)}">${escapeHtml(PRIORITY_LABELS[ticket.priority])}</span>
            </div>
          </div>
          <div class="atelier-ticket-bottom">
            <span class="atelier-ticket-meta">${escapeHtml(ui.page === "archives" ? `Archive le ${dateLabel(ticket.archivedAt)}` : relativeDueLabel(ticket.dueAt))}</span>
            <span class="atelier-ticket-meta">${escapeHtml(currency(ticket.estimate || 0))}</span>
          </div>
        </button>
      `)
      .join("");
  }

  function renderDetail() {
    const ticket = selectedTicket();

    if (!ticket) {
      elements.detail.innerHTML = `
        <article class="atelier-empty-card">
          <h3>${ui.page === "archives" ? "Selectionnez une archive" : "Base propre, prete a travailler"}</h3>
          <p class="atelier-empty-text">${ui.page === "archives" ? "Choisissez un dossier archive pour revoir ses infos ou le restaurer." : "Aucun faux dossier n'est charge. Ajoutez seulement vos vrais dossiers atelier."}</p>
          ${ui.page === "workshop" ? `
            <div class="atelier-inline-actions">
              <button type="button" class="atelier-primary-button" data-open-create>Ajouter un dossier</button>
            </div>
          ` : ""}
        </article>
      `;
      return;
    }

    const notes = clone(ticket.activities).sort((left, right) => new Date(right.date) - new Date(left.date));
    const attachments = ticket.attachments || [];

    elements.detail.innerHTML = `
      <div class="atelier-detail-stack">
        <header class="atelier-detail-head">
          <span class="atelier-card-code">${escapeHtml(ticket.code)}</span>
          <h2 class="atelier-detail-title">${escapeHtml(ticket.customerName)} - ${escapeHtml(ticket.device)}</h2>
          <p class="atelier-detail-subtitle">${escapeHtml(ticket.issue || "Aucune panne renseignee.")}</p>
          <div class="atelier-detail-badges">
            <span class="atelier-chip" data-stage="${escapeHtml(ticket.stage)}">${escapeHtml(stageMeta(ticket.stage).label)}</span>
            <span class="atelier-priority-chip" data-priority="${escapeHtml(ticket.priority)}">${escapeHtml(PRIORITY_LABELS[ticket.priority])}</span>
            <span class="atelier-alert-chip">${escapeHtml(relativeDueLabel(ticket.dueAt))}</span>
            ${ticket.archivedAt ? `<span class="atelier-alert-chip">Archive</span>` : ""}
          </div>
        </header>

        <div class="atelier-detail-grid three-columns">
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Telephone</span>
            <strong>${escapeHtml(ticket.customerPhone || "Non renseigne")}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Email</span>
            <strong>${escapeHtml(ticket.customerEmail || "Non renseigne")}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Technicien</span>
            <strong>${escapeHtml(ticket.technician || "Non renseigne")}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Entree atelier</span>
            <strong>${escapeHtml(dateLabel(ticket.receivedAt))}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Echeance</span>
            <strong>${escapeHtml(dateLabel(ticket.dueAt))}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">IMEI / Serie</span>
            <strong>${escapeHtml(ticket.serial || "Non renseigne")}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Montant estime</span>
            <strong>${escapeHtml(currency(ticket.estimate || 0))}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Depot</span>
            <strong>${escapeHtml(currency(ticket.deposit || 0))}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Type client</span>
            <strong>${escapeHtml(CUSTOMER_TYPE_LABELS[ticket.customerType] || CUSTOMER_TYPE_LABELS.individual)}</strong>
          </article>
          <article class="atelier-detail-box">
            <span class="atelier-meta-label">Adresse postale</span>
            <strong>${escapeHtml(ticket.customerAddress || "Non renseignee").replace(/\n/g, "<br>")}</strong>
          </article>
        </div>

        <form class="atelier-detail-form" data-ticket-form>
          <div class="atelier-form-grid">
            <label class="atelier-field">
              <span>Nom client</span>
              <input type="text" name="customerName" value="${escapeHtml(ticket.customerName)}" required list="atelierKnownCustomers" autocomplete="off" spellcheck="false">
              <small class="atelier-help-text" data-customer-lookup-status>Les clients deja connus peuvent re remplir leurs coordonnees automatiquement.</small>
            </label>
            <label class="atelier-field">
              <span>Telephone</span>
              <input type="text" name="customerPhone" value="${escapeHtml(ticket.customerPhone)}">
            </label>
            <label class="atelier-field">
              <span>Email</span>
              <input type="email" name="customerEmail" value="${escapeHtml(ticket.customerEmail)}">
            </label>
            <label class="atelier-field">
              <span>Type client</span>
              <select name="customerType">
                <option value="individual" ${ticket.customerType === "individual" ? "selected" : ""}>Particulier</option>
                <option value="business" ${ticket.customerType === "business" ? "selected" : ""}>Professionnel</option>
              </select>
            </label>
            <label class="atelier-field">
              <span>Date d'entree atelier</span>
              <input type="datetime-local" name="receivedAt" value="${escapeHtml(toDateTimeLocalValue(ticket.receivedAt))}">
            </label>
            <label class="atelier-field atelier-field-wide">
              <span>Adresse postale</span>
              <textarea name="customerAddress" rows="2">${escapeHtml(ticket.customerAddress)}</textarea>
            </label>
            <label class="atelier-field">
              <span>Appareil</span>
              <input type="text" name="device" value="${escapeHtml(ticket.device)}" required>
            </label>
            <label class="atelier-field">
              <span>IMEI / Serie</span>
              <input type="text" name="serial" value="${escapeHtml(ticket.serial)}">
            </label>
            <label class="atelier-field atelier-field-wide">
              <span>Panne / demande</span>
              <textarea name="issue" rows="4" required>${escapeHtml(ticket.issue)}</textarea>
            </label>
            <label class="atelier-field">
              <span>Etape</span>
              <select name="stage">
                ${STAGES.map((stage) => `<option value="${escapeHtml(stage.id)}" ${stage.id === ticket.stage ? "selected" : ""}>${escapeHtml(stage.label)}</option>`).join("")}
              </select>
            </label>
            <label class="atelier-field">
              <span>Priorite</span>
              <select name="priority">
                ${Object.entries(PRIORITY_LABELS).map(([key, label]) => `<option value="${escapeHtml(key)}" ${key === ticket.priority ? "selected" : ""}>${escapeHtml(label)}</option>`).join("")}
              </select>
            </label>
            <label class="atelier-field">
              <span>Technicien</span>
              <input type="text" name="technician" value="${escapeHtml(ticket.technician)}">
            </label>
            <label class="atelier-field">
              <span>Montant estime</span>
              <input type="number" name="estimate" min="0" step="0.01" value="${escapeHtml(ticket.estimate || 0)}">
            </label>
            <label class="atelier-field">
              <span>Depot client</span>
              <input type="number" name="deposit" min="0" step="0.01" value="${escapeHtml(ticket.deposit || 0)}">
            </label>
            <label class="atelier-field">
              <span>Echeance</span>
              <input type="datetime-local" name="dueAt" value="${escapeHtml(toDateTimeLocalValue(ticket.dueAt))}">
            </label>
          </div>

          <div class="atelier-form-actions">
            <button type="submit" class="atelier-primary-button">Enregistrer</button>
            ${ticket.archivedAt
              ? `<button type="button" class="atelier-secondary-button" data-restore-ticket>Restaurer</button>`
              : `<button type="button" class="atelier-secondary-button" data-ticket-ready>Marquer pret et archiver</button>`}
            <button type="button" class="atelier-danger-button" data-delete-ticket>Supprimer</button>
          </div>
        </form>

        <section class="atelier-detail-stack">
          <div class="atelier-panel-head">
            <div>
              <span class="atelier-panel-kicker">PDFS</span>
              <h2>Documents du dossier</h2>
            </div>
          </div>
          <div class="atelier-attachment-list">
            ${attachments.length ? attachments.map((attachment) => `
              <article class="atelier-attachment-card">
                <div class="atelier-attachment-top">
                  <div>
                    <strong class="atelier-attachment-name">${escapeHtml(attachment.name)}</strong>
                    <div class="atelier-attachment-meta">${escapeHtml(bytesLabel(attachment.size))} - ${escapeHtml(dateLabel(attachment.uploadedAt))}</div>
                  </div>
                  <div class="atelier-attachment-actions">
                    <button type="button" class="atelier-link-button" data-open-attachment="${escapeHtml(attachment.id)}">Ouvrir PDF</button>
                    <button type="button" class="atelier-stock-button" data-remove-attachment="${escapeHtml(attachment.id)}">Supprimer</button>
                  </div>
                </div>
              </article>
            `).join("") : `
              <article class="atelier-empty-card">
                <p class="atelier-empty-text">Aucun PDF sur ce dossier pour le moment.</p>
              </article>
            `}
          </div>

          <form class="atelier-field" data-attachment-form>
            <span>Ajouter des PDFs</span>
            <input type="file" name="attachments" accept="application/pdf" multiple>
            <div class="atelier-form-actions">
              <button type="submit" class="atelier-secondary-button">Ajouter les PDFs</button>
            </div>
          </form>
        </section>

        <section class="atelier-detail-stack">
          <div class="atelier-panel-head">
            <div>
              <span class="atelier-panel-kicker">NOTES</span>
              <h2>Notes internes</h2>
            </div>
          </div>
          <div class="atelier-note-list">
            ${notes.length ? notes.map((note) => `
              <article class="atelier-note-card">
                <div class="atelier-note-top">
                  <div>
                    <strong class="atelier-note-title">${escapeHtml(note.title)}</strong>
                    <div class="atelier-note-caption">${escapeHtml(note.tag || "Atelier")}</div>
                  </div>
                  <span class="atelier-note-date">${escapeHtml(dateLabel(note.date))}</span>
                </div>
                <p class="atelier-note-text">${escapeHtml(note.note || "")}</p>
              </article>
            `).join("") : `
              <article class="atelier-empty-card">
                <p class="atelier-empty-text">Aucune note interne sur ce dossier.</p>
              </article>
            `}
          </div>

          <form class="atelier-field" data-note-form>
            <span>Ajouter une note</span>
            <textarea name="note" rows="3" placeholder="Exemple: client rappelle a 16 h, attente validation devis..."></textarea>
            <div class="atelier-form-actions">
              <button type="submit" class="atelier-secondary-button">Ajouter la note</button>
            </div>
          </form>
        </section>
      </div>
    `;
  }

  function renderStock() {
    const items = clone(state.stock).sort((left, right) => left.name.localeCompare(right.name, "fr"));

    if (!items.length) {
      elements.stockList.innerHTML = `
        <article class="atelier-empty-card">
          <h3>Stock vide</h3>
          <p class="atelier-empty-text">Ajoutez seulement les pieces utiles a l'atelier. Pas de contenu fictif.</p>
        </article>
      `;
      return;
    }

    elements.stockList.innerHTML = items
      .map((item) => `
        <article class="atelier-stock-card">
          <div class="atelier-stock-top">
            <div>
              <strong class="atelier-stock-name">${escapeHtml(item.name)}</strong>
              <div class="atelier-stock-caption">Zone ${escapeHtml(item.zone || "Non renseignee")} - Seuil ${escapeHtml(item.target)}</div>
            </div>
            <span class="atelier-alert-chip">${escapeHtml(item.qty)} u.</span>
          </div>
          <div class="atelier-stock-actions">
            <button type="button" class="atelier-step-button" data-stock-step="-1" data-stock-id="${escapeHtml(item.id)}">-</button>
            <span class="atelier-stock-qty">${escapeHtml(item.qty)}</span>
            <button type="button" class="atelier-step-button" data-stock-step="1" data-stock-id="${escapeHtml(item.id)}">+</button>
            <button type="button" class="atelier-stock-button" data-remove-stock="${escapeHtml(item.id)}">Supprimer</button>
          </div>
        </article>
      `)
      .join("");
  }

  function buildClientMessage(ticket, kind) {
    if (!ticket) {
      return "Selectionnez un dossier pour preparer un message client.";
    }

    const intro = `Bonjour ${ticket.customerName},`;
    const deviceLine = `${ticket.device} - ${ticket.code}`;
    const signature = "\n\nReFone";

    if (kind === "quote") {
      return `${intro}\n\nVotre devis pour ${deviceLine} est pret.\nMontant estime: ${currency(ticket.estimate || 0)}.\n\nVous pouvez nous repondre directement pour valider la suite.${signature}`;
    }

    if (kind === "ready") {
      return `${intro}\n\nVotre ${deviceLine} est pret.\nVous pouvez passer au point de retrait ou nous repondre pour nous prevenir.${signature}`;
    }

    if (kind === "relance") {
      return `${intro}\n\nJe reviens vers vous au sujet de votre dossier ${deviceLine}.\nNous attendons votre retour pour poursuivre dans de bonnes conditions.${signature}`;
    }

    return `${intro}\n\nPetit point atelier pour votre dossier ${deviceLine}.\nStatut actuel: ${stageMeta(ticket.stage).label}.\n${ticket.issue}\n\nNous revenons vers vous des que l'etape suivante est terminee.${signature}`;
  }

  function renderMessageOutput() {
    if (!elements.messageOutput) return;
    elements.messageOutput.value = buildClientMessage(selectedTicket(), state.messageKind);
    document.querySelectorAll("[data-message-kind]").forEach((button) => {
      button.classList.toggle("is-active", button.dataset.messageKind === state.messageKind);
    });
  }

  function render() {
    const tickets = filteredTickets();
    normalizeSelection(tickets);
    syncCustomerDatalist();
    renderViewSwitch();
    renderStats();
    renderQuoteQueue();
    renderTickets(tickets);
    renderDetail();
    renderStock();
    renderMessageOutput();
    syncVisibleCustomerForms();
    saveState();
    updateSyncIndicator();
  }

  function nextTicketCode() {
    const now = new Date();
    const stamp = `${String(now.getFullYear()).slice(-2)}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
    const count = state.tickets.filter((ticket) => ticket.code.includes(`RF-${stamp}`)).length + 1;
    return `RF-${stamp}-${String(count).padStart(3, "0")}`;
  }

  function readFileAsDataUrl(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(String(reader.result || ""));
      reader.onerror = () => reject(new Error("Lecture PDF impossible."));
      reader.readAsDataURL(file);
    });
  }

  async function uploadRemoteAttachment(code, metadata, dataUrl) {
    const response = await fetch(syncRuntime.fileApiUrl, {
      method: "PUT",
      headers: {
        "content-type": "application/json"
      },
      body: JSON.stringify({
        code,
        file: {
          schema: FILE_SCHEMA,
          ...metadata,
          dataUrl
        }
      })
    });
    const result = await response.json();
    if (!response.ok) {
      throw new Error(result.error || "Upload PDF impossible.");
    }
    return result;
  }

  async function fetchRemoteAttachment(code, id) {
    const response = await fetch(`${syncRuntime.fileApiUrl}?code=${encodeURIComponent(code)}&id=${encodeURIComponent(id)}`, {
      headers: {
        accept: "application/json"
      }
    });
    const result = await response.json();
    if (!response.ok) {
      throw new Error(result.error || "Document distant introuvable.");
    }
    return result.file;
  }

  async function removeRemoteAttachment(code, id) {
    const response = await fetch(syncRuntime.fileApiUrl, {
      method: "DELETE",
      headers: {
        "content-type": "application/json"
      },
      body: JSON.stringify({ code, id })
    });
    if (!response.ok) {
      const result = await response.json().catch(() => ({}));
      throw new Error(result.error || "Suppression distante impossible.");
    }
  }

  async function attachFilesToTicket(ticketId, files, options = {}) {
    const ticket = state.tickets.find((item) => item.id === ticketId);
    if (!ticket || !files.length) return;

    const code = readSyncCode();
    let uploadedCount = 0;

    for (const file of files) {
      if (!file || !file.name || file.size <= 0) continue;
      if (file.type && file.type !== "application/pdf") {
        showToast(`"${file.name}" n'est pas un PDF`);
        continue;
      }
      if (file.size > MAX_PDF_BYTES) {
        showToast(`"${file.name}" depasse la limite PDF`);
        continue;
      }

      const attachment = normalizeAttachmentMeta({
        id: createId("pdf"),
        name: file.name,
        size: file.size,
        mimeType: "application/pdf",
        uploadedAt: new Date().toISOString()
      });
      const dataUrl = await readFileAsDataUrl(file);
      const localSaved = await putLocalFile({
        id: attachment.id,
        name: attachment.name,
        size: attachment.size,
        mimeType: attachment.mimeType,
        uploadedAt: attachment.uploadedAt,
        dataUrl
      });

      if (!localSaved) {
        showToast(`Stockage local impossible pour "${file.name}"`);
      }

      if (code.length >= MIN_SYNC_CODE_LENGTH) {
        try {
          await uploadRemoteAttachment(code, attachment, dataUrl);
        } catch (error) {
          showToast(error.message || `Upload impossible pour "${file.name}"`);
        }
      }

      ticket.attachments.unshift(attachment);
      ticket.activities.push(normalizeActivity({
        title: "PDF ajoute",
        note: `${attachment.name} ajoute au dossier.`,
        date: new Date().toISOString(),
        tag: "Documents"
      }));
      uploadedCount += 1;
    }

    ticket.activities = ticket.activities.slice(-80);
    render();
    queueAutoPush();

    if (uploadedCount && !options.silent) {
      showToast(uploadedCount > 1 ? `${uploadedCount} PDFs ajoutes` : "PDF ajoute");
    }
  }

  async function openAttachment(attachmentId) {
    const ticket = selectedTicket();
    if (!ticket) return;
    const attachment = (ticket.attachments || []).find((item) => item.id === attachmentId);
    if (!attachment) return;

    let fileRecord = await getLocalFile(attachment.id);
    if (!fileRecord && readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
      try {
        const remoteFile = await fetchRemoteAttachment(readSyncCode(), attachment.id);
        fileRecord = {
          id: remoteFile.id,
          name: remoteFile.name,
          size: remoteFile.size,
          mimeType: remoteFile.mimeType,
          uploadedAt: remoteFile.uploadedAt,
          dataUrl: remoteFile.dataUrl
        };
        await putLocalFile(fileRecord);
      } catch (error) {
        showToast(error.message || "PDF introuvable");
        return;
      }
    }

    if (!fileRecord?.dataUrl) {
      showToast("PDF indisponible sur cet appareil");
      return;
    }

    try {
      const response = await fetch(fileRecord.dataUrl);
      const blob = await response.blob();
      const objectUrl = URL.createObjectURL(blob);
      openPdfModal(attachment.name, objectUrl);
    } catch (error) {
      showToast(error.message || "Ouverture PDF impossible");
    }
  }

  async function removeAttachment(attachmentId) {
    const ticket = selectedTicket();
    if (!ticket) return;

    const attachment = (ticket.attachments || []).find((item) => item.id === attachmentId);
    if (!attachment) return;

    if (!window.confirm(`Supprimer "${attachment.name}" ?`)) return;

    ticket.attachments = ticket.attachments.filter((item) => item.id !== attachmentId);
    ticket.activities.push(normalizeActivity({
      title: "PDF supprime",
      note: `${attachment.name} retire du dossier.`,
      date: new Date().toISOString(),
      tag: "Documents"
    }));
    ticket.activities = ticket.activities.slice(-80);
    await deleteLocalFile(attachmentId);

    const code = readSyncCode();
    if (code.length >= MIN_SYNC_CODE_LENGTH) {
      try {
        await removeRemoteAttachment(code, attachmentId);
      } catch (error) {
        showToast(error.message || "Suppression distante impossible");
      }
    }

    render();
    queueAutoPush();
    showToast("PDF supprime");
  }

  async function removeTicketAttachments(ticket) {
    const attachments = ticket.attachments || [];
    const code = readSyncCode();

    for (const attachment of attachments) {
      await deleteLocalFile(attachment.id);
      if (code.length >= MIN_SYNC_CODE_LENGTH) {
        try {
          await removeRemoteAttachment(code, attachment.id);
        } catch {}
      }
    }
  }

  function createTicketPayload(values) {
    const stage = normalizeStage(values.stage);
    const archivedAt = stage === "ready" ? new Date().toISOString() : "";
    const code = nextTicketCode();
    const restoreStage = stage === "ready" ? "testing" : stage;
    return normalizeTicket({
      id: code,
      code,
      customerName: String(values.customerName || "").trim(),
      customerEmail: String(values.customerEmail || "").trim(),
      customerPhone: String(values.customerPhone || "").trim(),
      customerAddress: String(values.customerAddress || "").trim(),
      customerType: normalizeCustomerType(values.customerType),
      device: String(values.device || "").trim(),
      issue: String(values.issue || "").trim(),
      stage,
      priority: normalizePriority(values.priority),
      technician: String(values.technician || "").trim(),
      estimate: Number(values.estimate || 0),
      deposit: Number(values.deposit || 0),
      dueAt: String(values.dueAt || "").trim(),
      receivedAt: String(values.receivedAt || new Date().toISOString()).trim() || new Date().toISOString(),
      serial: String(values.serial || "").trim(),
      sourceDocumentId: String(values.sourceDocumentId || "").trim(),
      sourceDocumentNumber: String(values.sourceDocumentNumber || "").trim(),
      archivedAt,
      restoreStage,
      attachments: [],
      activities: [
        {
          title: archivedAt ? "Dossier cree et archive" : "Dossier cree",
          note: archivedAt ? "Le dossier a ete cree directement dans les archives." : "Le dossier a ete cree dans Atelier OS.",
          date: new Date().toISOString(),
          tag: "Creation"
        }
      ]
    }, state.tickets.length);
  }

  async function createTicket(values, files) {
    const ticket = createTicketPayload(values);
    state.tickets.unshift(ticket);
    setPage(ticket.archivedAt ? "archives" : "workshop", { render: false, replaceHash: true });
    state.selectedTicketId = ticket.id;
    state.messageKind = ticket.archivedAt ? "ready" : "update";
    render();
    if (files.length) {
      await attachFilesToTicket(ticket.id, files, { silent: true });
    } else {
      queueAutoPush();
    }
    closeCreateModal();
    showToast("Dossier ajoute");
  }

  function createTicketFromQuoteRequest(requestId) {
    const request = state.quoteRequests.find((item) => item.id === requestId);
    if (!request) return;
    if (state.tickets.some((ticket) => ticket.sourceDocumentId === request.sourceDocumentId)) {
      request.status = "accepted";
      request.updatedAt = new Date().toISOString();
      render();
      queueAutoPush();
      showToast("Dossier deja cree");
      return;
    }

    const ticket = createTicketPayload({
      customerName: request.customerName,
      customerEmail: request.customerEmail,
      customerPhone: request.customerPhone,
      customerAddress: request.customerAddress,
      customerType: request.customerType,
      device: request.device,
      issue: request.issue || request.title,
      stage: "intake",
      priority: "normal",
      technician: "Erwann",
      estimate: request.estimate,
      deposit: 0,
      dueAt: "",
      receivedAt: new Date().toISOString(),
      serial: "",
      sourceDocumentId: request.sourceDocumentId,
      sourceDocumentNumber: request.sourceDocumentNumber
    });

    ticket.activities.unshift(normalizeActivity({
      title: "Cree depuis FactureOS",
      note: `Dossier cree depuis le devis accepte ${request.sourceDocumentNumber || request.sourceDocumentId}.`,
      date: new Date().toISOString(),
      tag: "FactureOS"
    }));
    ticket.activities = ticket.activities.slice(-80);

    request.status = "accepted";
    request.updatedAt = new Date().toISOString();
    state.tickets.unshift(ticket);
    state.selectedTicketId = ticket.id;
    state.messageKind = "update";
    setPage("workshop", { render: false, replaceHash: true });
    render();
    queueAutoPush();
    showToast("Dossier atelier cree depuis le devis");
  }

  function dismissQuoteRequest(requestId) {
    const request = state.quoteRequests.find((item) => item.id === requestId);
    if (!request) return;
    if (!window.confirm(`Ignorer le devis ${request.sourceDocumentNumber || request.title} ?`)) return;
    request.status = "dismissed";
    request.updatedAt = new Date().toISOString();
    render();
    queueAutoPush();
    showToast("Devis ignore");
  }

  function updateSelectedTicket(values) {
    const ticket = selectedTicket();
    if (!ticket) return;
    const previousStage = ticket.stage;
    const wasArchived = Boolean(ticket.archivedAt);
    const nextStage = normalizeStage(values.stage);

    ticket.customerName = String(values.customerName || "").trim();
    ticket.customerPhone = String(values.customerPhone || "").trim();
    ticket.customerEmail = String(values.customerEmail || "").trim();
    ticket.customerAddress = String(values.customerAddress || "").trim();
    ticket.customerType = normalizeCustomerType(values.customerType);
    ticket.device = String(values.device || "").trim();
    ticket.serial = String(values.serial || "").trim();
    ticket.issue = String(values.issue || "").trim();
    ticket.stage = nextStage;
    ticket.priority = normalizePriority(values.priority);
    ticket.technician = String(values.technician || "").trim();
    ticket.estimate = Number.isFinite(Number(values.estimate)) ? Number(values.estimate) : 0;
    ticket.deposit = Number.isFinite(Number(values.deposit)) ? Number(values.deposit) : 0;
    ticket.dueAt = String(values.dueAt || "").trim();
    ticket.receivedAt = String(values.receivedAt || new Date().toISOString()).trim();

    if (nextStage === "ready") {
      const fallbackRestoreStage = previousStage !== "ready" ? previousStage : ticket.restoreStage;
      ticket.restoreStage = normalizeStage(fallbackRestoreStage || "testing");
      if (!wasArchived) {
        ticket.archivedAt = new Date().toISOString();
      }
    } else {
      ticket.restoreStage = nextStage;
    }

    ticket.activities.push(normalizeActivity({
      title: !wasArchived && nextStage === "ready" ? "Dossier archive" : "Dossier mis a jour",
      note: !wasArchived && nextStage === "ready"
        ? "Le dossier a ete passe sur Pret puis archive."
        : "Les informations du dossier ont ete modifiees.",
      date: new Date().toISOString(),
      tag: !wasArchived && nextStage === "ready" ? "Archive" : "Edition"
    }));
    ticket.activities = ticket.activities.slice(-80);

    if (!wasArchived && nextStage === "ready") {
      state.messageKind = "ready";
      setPage("archives", { render: false, replaceHash: true });
      state.selectedTicketId = ticket.id;
    }

    render();
    queueAutoPush();
    showToast(!wasArchived && nextStage === "ready" ? "Dossier archive" : "Dossier enregistre");
  }

  function archiveSelectedTicket() {
    const ticket = selectedTicket();
    if (!ticket) return;

    const restoreStage = ticket.stage !== "ready" ? ticket.stage : ticket.restoreStage || "testing";
    ticket.restoreStage = normalizeStage(restoreStage);
    ticket.stage = "ready";
    ticket.archivedAt = new Date().toISOString();
    ticket.activities.push(normalizeActivity({
      title: "Dossier archive",
      note: "Le dossier a ete marque pret puis archive.",
      date: ticket.archivedAt,
      tag: "Archive"
    }));
    ticket.activities = ticket.activities.slice(-80);
    state.messageKind = "ready";
    setPage("archives", { render: false, replaceHash: true });
    state.selectedTicketId = ticket.id;
    render();
    queueAutoPush();
    showToast("Dossier archive");
  }

  function restoreSelectedTicket() {
    const ticket = selectedTicket();
    if (!ticket) return;

    ticket.stage = normalizeStage(ticket.restoreStage || "testing");
    ticket.archivedAt = "";
    ticket.activities.push(normalizeActivity({
      title: "Dossier restaure",
      note: "Le dossier est revenu dans la liste des dossiers en cours.",
      date: new Date().toISOString(),
      tag: "Archive"
    }));
    ticket.activities = ticket.activities.slice(-80);
    state.messageKind = "update";
    setPage("workshop", { render: false, replaceHash: true });
    state.selectedTicketId = ticket.id;
    render();
    queueAutoPush();
    showToast("Dossier restaure");
  }

  function addNote(noteValue) {
    const ticket = selectedTicket();
    const note = String(noteValue || "").trim();
    if (!ticket || !note) return;

    ticket.activities.push(normalizeActivity({
      title: "Note interne",
      note,
      date: new Date().toISOString(),
      tag: "Interne"
    }));
    ticket.activities = ticket.activities.slice(-80);
    render();
    queueAutoPush();
    showToast("Note ajoutee");
  }

  async function deleteSelectedTicket() {
    const ticket = selectedTicket();
    if (!ticket) return;

    if (!window.confirm(`Supprimer le dossier ${ticket.code} ?`)) return;

    await removeTicketAttachments(ticket);
    state.tickets = state.tickets.filter((item) => item.id !== ticket.id);
    render();
    queueAutoPush();
    showToast("Dossier supprime");
  }

  function addStockItem(values) {
    const name = String(values.name || "").trim();
    if (!name) return;

    state.stock.unshift(normalizeStockItem({
      id: createId("stock"),
      name,
      qty: Number(values.qty || 0),
      target: Number(values.target || 0),
      zone: String(values.zone || "").trim()
    }));
    render();
    queueAutoPush();
    showToast("Piece ajoutee");
  }

  function stepStock(itemId, delta) {
    const item = state.stock.find((stockItem) => stockItem.id === itemId);
    if (!item) return;
    item.qty = Math.max(0, item.qty + delta);
    render();
    queueAutoPush();
  }

  function removeStockItem(itemId) {
    const item = state.stock.find((stockItem) => stockItem.id === itemId);
    if (!item) return;

    if (!window.confirm(`Supprimer la piece "${item.name}" ?`)) return;

    state.stock = state.stock.filter((stockItem) => stockItem.id !== itemId);
    render();
    queueAutoPush();
    showToast("Piece supprimee");
  }

  async function resetWorkspace() {
    if (!(await confirmWorkspaceReset())) return;

    const allTickets = clone(state.tickets);
    for (const ticket of allTickets) {
      await removeTicketAttachments(ticket);
    }

    applyWorkspace(createEmptyWorkspace());
    syncRuntime.lastRemoteUpdatedAt = "";
    setPage("workshop", { render: false, replaceHash: true });
    closePdfModal();
    closeSyncModal();
    render();
    queueAutoPush();
    setSyncStatus("Base vide appliquee.");
    showToast("Base atelier vide");
  }

  function openCreateModal() {
    if (!elements.createModal) return;
    elements.createModal.hidden = false;
    const createForm = elements.createModal.querySelector("[data-create-form]");
    const createFields = customerFormFields(createForm);
    if (createFields?.status) {
      createFields.status.textContent = "Les clients deja connus se proposeront automatiquement.";
    }
    ["customerPhone", "customerEmail", "customerAddress"].forEach((name) => {
      const field = createForm?.querySelector(`[name="${name}"]`);
      if (field) {
        delete field.dataset.autofillValue;
      }
    });
    const receivedInput = elements.createModal.querySelector('input[name="receivedAt"]');
    if (receivedInput && !receivedInput.value) {
      receivedInput.value = toDateTimeLocalValue(new Date().toISOString());
    }
  }

  function closeCreateModal() {
    if (elements.createModal) {
      elements.createModal.hidden = true;
    }
  }

  function openSyncModal() {
    if (elements.syncModal) {
      elements.syncModal.hidden = false;
    }
    setResetPanel(false);
  }

  function closeSyncModal() {
    if (elements.syncModal) {
      elements.syncModal.hidden = true;
    }
    setResetPanel(false);
  }

  function setResetPanel(isOpen) {
    if (!elements.resetPanel) return;
    elements.resetPanel.hidden = !isOpen;
  }

  function toggleResetPanel() {
    if (!elements.resetPanel) return;
    setResetPanel(elements.resetPanel.hidden);
  }

  function resetPdfPreview() {
    if (elements.pdfFrame) {
      elements.pdfFrame.removeAttribute("src");
    }
    if (elements.pdfTitle) {
      elements.pdfTitle.textContent = "Document atelier";
    }
    if (elements.pdfStatus) {
      elements.pdfStatus.textContent = "Chargement du PDF...";
      elements.pdfStatus.classList.remove("is-error");
    }
    if (activePdfUrl) {
      window.URL.revokeObjectURL(activePdfUrl);
      activePdfUrl = "";
    }
    activePdfName = "";
  }

  function openPdfModal(name, objectUrl) {
    resetPdfPreview();
    activePdfUrl = objectUrl;
    activePdfName = name || "document.pdf";

    if (elements.pdfTitle) {
      elements.pdfTitle.textContent = activePdfName;
    }
    if (elements.pdfFrame) {
      elements.pdfFrame.src = `${objectUrl}#toolbar=0&navpanes=0&view=FitH`;
    }
    if (elements.pdfStatus) {
      elements.pdfStatus.textContent = "Apercu integre charge. Si besoin, utilisez le telechargement.";
      elements.pdfStatus.classList.remove("is-error");
    }
    if (elements.pdfModal) {
      elements.pdfModal.hidden = false;
    }
  }

  function closePdfModal() {
    if (elements.pdfModal) {
      elements.pdfModal.hidden = true;
    }
    resetPdfPreview();
  }

  function downloadActivePdf() {
    if (!activePdfUrl) {
      showToast("Aucun PDF ouvert");
      return;
    }
    const link = document.createElement("a");
    link.href = activePdfUrl;
    link.download = activePdfName || "document.pdf";
    document.body.appendChild(link);
    link.click();
    link.remove();
  }

  async function confirmWorkspaceReset() {
    if (!window.confirm("Cette action efface tous les dossiers, archives, stock et PDFs sur tous les appareils synchronises. Continuer ?")) {
      return false;
    }

    const confirmation = window.prompt('Pour confirmer, tapez exactement SUPPRIMER');
    if (String(confirmation || "").trim().toUpperCase() !== "SUPPRIMER") {
      showToast("Reinitialisation annulee");
      return false;
    }

    return window.confirm("Derniere verification : vous voulez vraiment reinitialiser toute la base Atelier OS ?");
  }

  function legacyCopyText(text) {
    const field = document.createElement("textarea");
    field.value = text;
    field.setAttribute("readonly", "");
    field.style.position = "fixed";
    field.style.opacity = "0";
    document.body.appendChild(field);
    field.select();
    field.setSelectionRange(0, field.value.length);
    let copied = false;
    try {
      copied = document.execCommand("copy");
    } catch {}
    field.remove();
    return copied;
  }

  async function copyText(text, successMessage) {
    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(text);
      } else if (!legacyCopyText(text)) {
        throw new Error("copy failed");
      }
      showToast(successMessage);
    } catch {
      if (legacyCopyText(text)) {
        showToast(successMessage);
        return;
      }
      showToast("Copie impossible pour le moment");
    }
  }

  function showToast(message) {
    if (!elements.toast) return;
    elements.toast.textContent = message;
    elements.toast.hidden = false;
    window.clearTimeout(showToast.timer);
    showToast.timer = window.setTimeout(() => {
      elements.toast.textContent = "";
      elements.toast.hidden = true;
    }, 2600);
  }

  function handleDocumentClick(event) {
    const ticketButton = event.target.closest("[data-select-ticket]");
    if (ticketButton) {
      state.selectedTicketId = ticketButton.dataset.selectTicket;
      render();
      return;
    }

    const pageButton = event.target.closest("[data-page-view]");
    if (pageButton) {
      setPage(pageButton.dataset.pageView);
      return;
    }

    const acceptQuoteButton = event.target.closest("[data-accept-quote-request]");
    if (acceptQuoteButton) {
      createTicketFromQuoteRequest(acceptQuoteButton.dataset.acceptQuoteRequest);
      return;
    }

    const dismissQuoteButton = event.target.closest("[data-dismiss-quote-request]");
    if (dismissQuoteButton) {
      dismissQuoteRequest(dismissQuoteButton.dataset.dismissQuoteRequest);
      return;
    }

    if (event.target.closest("[data-open-create]")) {
      openCreateModal();
      return;
    }

    if (event.target.closest("[data-close-create]")) {
      closeCreateModal();
      return;
    }

    if (event.target.closest("[data-open-sync]")) {
      openSyncModal();
      return;
    }

    if (event.target.closest("[data-close-sync]")) {
      closeSyncModal();
      return;
    }

    if (event.target.closest("[data-close-pdf]")) {
      closePdfModal();
      return;
    }

    if (event.target.closest("[data-copy-message]")) {
      copyText(elements.messageOutput?.value || "", "Message client copie");
      return;
    }

    const filterButton = event.target.closest("[data-filter-stage]");
    if (filterButton) {
      ui.filterStage = filterButton.dataset.filterStage;
      render();
      return;
    }

    const messageKindButton = event.target.closest("[data-message-kind]");
    if (messageKindButton) {
      state.messageKind = messageKindButton.dataset.messageKind;
      renderMessageOutput();
      saveState();
      return;
    }

    if (event.target.closest("[data-generate-sync]")) {
      generateSyncCode();
      return;
    }

    if (event.target.closest("[data-sync-pull]")) {
      pullSync({ force: true, allowMissing: true });
      return;
    }

    if (event.target.closest("[data-sync-push]")) {
      pushSync();
      return;
    }

    if (event.target.closest("[data-toggle-reset]")) {
      toggleResetPanel();
      return;
    }

    if (event.target.closest("[data-ticket-ready]")) {
      archiveSelectedTicket();
      return;
    }

    if (event.target.closest("[data-restore-ticket]")) {
      restoreSelectedTicket();
      return;
    }

    if (event.target.closest("[data-delete-ticket]")) {
      deleteSelectedTicket();
      return;
    }

    const stockStepButton = event.target.closest("[data-stock-step]");
    if (stockStepButton) {
      stepStock(stockStepButton.dataset.stockId, Number(stockStepButton.dataset.stockStep || 0));
      return;
    }

    const removeStockButton = event.target.closest("[data-remove-stock]");
    if (removeStockButton) {
      removeStockItem(removeStockButton.dataset.removeStock);
      return;
    }

    const attachmentButton = event.target.closest("[data-open-attachment]");
    if (attachmentButton) {
      openAttachment(attachmentButton.dataset.openAttachment);
      return;
    }

    const removeAttachmentButton = event.target.closest("[data-remove-attachment]");
    if (removeAttachmentButton) {
      removeAttachment(removeAttachmentButton.dataset.removeAttachment);
      return;
    }

    if (event.target.closest("[data-reset-workspace]")) {
      resetWorkspace();
      return;
    }

    if (event.target.closest("[data-download-pdf]")) {
      downloadActivePdf();
    }
  }

  async function handleSubmit(event) {
    if (event.target.matches("[data-create-form]")) {
      event.preventDefault();
      const formData = new FormData(event.target);
      const files = formData.getAll("attachments").filter((file) => file && file.name);
      const values = Object.fromEntries([...formData.entries()].filter(([key, value]) => key !== "attachments" && typeof value === "string"));
      await createTicket(values, files);
      event.target.reset();
      return;
    }

    if (event.target.matches("[data-ticket-form]")) {
      event.preventDefault();
      updateSelectedTicket(Object.fromEntries(new FormData(event.target).entries()));
      return;
    }

    if (event.target.matches("[data-note-form]")) {
      event.preventDefault();
      const note = new FormData(event.target).get("note");
      addNote(note);
      event.target.reset();
      return;
    }

    if (event.target.matches("[data-stock-form]")) {
      event.preventDefault();
      addStockItem(Object.fromEntries(new FormData(event.target).entries()));
      event.target.reset();
      return;
    }

    if (event.target.matches("[data-attachment-form]")) {
      event.preventDefault();
      const ticket = selectedTicket();
      if (!ticket) return;
      const files = new FormData(event.target).getAll("attachments").filter((file) => file && file.name);
      await attachFilesToTicket(ticket.id, files);
      event.target.reset();
    }
  }

  async function bootAtelier() {
    if (!isBooted) {
      await openFileDb();
      initEvents();
      isBooted = true;
    }

    render();
    restartSyncTransport();

    if (readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
      setSyncStatus("Code actif. Recuperation de la derniere base...");
      await pullSync({ silent: true, allowMissing: true, force: true });
      await syncAcceptedQuotesFromFacture({ silent: true });
    } else {
      setSyncStatus("Aucune synchronisation active.");
    }
  }

  async function unlockAtelier(username, password) {
    const normalizedUsername = String(username || "").trim().toLowerCase();
    const passwordHash = await sha256Hex(password);

    if (normalizedUsername !== AUTH_USERNAME || passwordHash !== AUTH_PASSWORD_HASH) {
      showAuthGate("Identifiant ou mot de passe incorrect.");
      return false;
    }

    storeSessionUnlocked();
    hideAuthGate();
    await bootAtelier();
    return true;
  }

  function lockAtelier() {
    clearUnlockedSession();
    window.clearInterval(syncRuntime.pollTimer);
    window.clearTimeout(syncRuntime.pushTimer);
    closeLiveSync();
    closeCreateModal();
    closeSyncModal();
    closePdfModal();
    showAuthGate();
  }

  function initAuth() {
    if (elements.authUsername && !elements.authUsername.value) {
      elements.authUsername.value = AUTH_USERNAME;
    }

    elements.authForm?.addEventListener("submit", async (event) => {
      event.preventDefault();
      const formData = new FormData(event.target);
      await unlockAtelier(formData.get("username"), formData.get("password"));
    });

    elements.lockSession?.addEventListener("click", () => {
      lockAtelier();
    });
  }

  function initEvents() {
    document.addEventListener("click", handleDocumentClick);
    document.addEventListener("submit", (event) => {
      handleSubmit(event);
    });
    document.addEventListener("input", (event) => {
      const target = event.target instanceof HTMLInputElement ? event.target : null;
      if (!target || target.name !== "customerName") return;
      const form = target.closest("form");
      updateCustomerLookup(form, target.value, { force: false, commit: false });
    });
    document.addEventListener("change", (event) => {
      const target = event.target;
      if (target instanceof HTMLInputElement && target.name === "customerName") {
        updateCustomerLookup(target.closest("form"), target.value, { force: true, commit: true });
      }
    });

    elements.searchInput?.addEventListener("input", (event) => {
      ui.search = event.target.value || "";
      render();
    });

    elements.syncInput?.addEventListener("input", () => {
      storeSyncCode(elements.syncInput.value);
      if (readSyncCode().length < MIN_SYNC_CODE_LENGTH) {
        setSyncStatus("Saisissez le meme code prive sur chaque support.");
      }
    });

    window.addEventListener("pagehide", () => {
      if (readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
        pushSync({ silent: true, keepalive: true });
      }
      closeLiveSync();
    });

    window.addEventListener("online", () => {
      restartSyncTransport();
      if (readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
        pullSync({ silent: true, allowMissing: true, force: true }).then(() => syncAcceptedQuotesFromFacture({ silent: true }));
      }
    });

    window.addEventListener("offline", () => {
      syncRuntime.streamConnected = false;
      updateSyncIndicator();
      setSyncStatus("Hors ligne. La synchro reprendra des le retour reseau.");
    });

    window.addEventListener("hashchange", () => {
      const nextPage = pageFromHash();
      if (nextPage !== ui.page) {
        setPage(nextPage, { syncHash: false });
      }
    });

    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState !== "visible") return;
      if (readSyncCode().length >= MIN_SYNC_CODE_LENGTH) {
        restartSyncTransport();
        pullSync({ silent: true, allowMissing: true, force: true }).then(() => syncAcceptedQuotesFromFacture({ silent: true }));
      }
    });
  }

  if (elements.syncInput) {
    elements.syncInput.value = normalizeCode(localStorage.getItem(SYNC_CODE_KEY) || DEFAULT_SYNC_CODE);
  }

  ui.page = pageFromHash();

  initAuth();

  if (isSessionUnlocked()) {
    hideAuthGate();
    await bootAtelier();
  } else {
    showAuthGate();
  }
})();
