Logo CLEANMYCLIM
Mon compte Discuter sur WhatsApp

Service Report

Vos données restent en mémoire sur l’appareil au moins 5 heures afin de vous laisser le temps d'intervenir..

Brouillon : —

AFFAIRE – Identification

Le chronomètre démarre au clic. Sa valeur n’est pas affichée mais sera transmise avec le rapport.

Aucun équipement

Cliquez sur “Ajouter un équipement” pour commencer.

`; container.appendChild(empty); return; } state.equipments.forEach(function (equip, idx) { container.appendChild(equipmentNode(equip, idx)); }); } function addEquipment() { state.equipments.push(newEquip()); saveDraft(); renderEquipments(); } function removeEquipment(equipId) { state.equipments = state.equipments.filter(function (e) { return e.id !== equipId; }); fileStore.delete(equipId); saveDraft(); renderEquipments(); } function canStartTimer() { return !!String(state.affaire.numero || "").trim(); } function startTimer() { if (!canStartTimer()) return; if (!state.timer.started) { state.timer.started = true; state.timer.startedAtMs = now(); saveDraft(); } else { } } function getElapsedSeconds() { if (!state.timer.started || !state.timer.startedAtMs) return 0; return Math.max(0, Math.round((now() - state.timer.startedAtMs) / 1000)); } function renderIntervenantPhotoUI() { const meta = state.affaire.photoIntervenant || { name: "", size: 0, type: "" }; $("#photoIntervenantLabel").value = meta.name ? (meta.name + " (" + Math.round((meta.size||0)/1024) + " Ko)") : ""; $("#btnPhotoIntervenant").disabled = !String(state.affaire.numero || "").trim(); const thumbWrap = $("#photoIntervenantThumb"); const img = $("#photoIntervenantImg"); const metaLine = $("#photoIntervenantMeta"); if (!intervenantPhotoFile) { thumbWrap.classList.add("hidden"); img.src = ""; metaLine.textContent = ""; return; } fileToDataUrl(intervenantPhotoFile).then(function(url){ img.src = url; metaLine.textContent = clampText(intervenantPhotoFile.name, 60) + " – " + Math.round(intervenantPhotoFile.size/1024) + " Ko"; thumbWrap.classList.remove("hidden"); }).catch(function(){ }); } function setSubmitStatus(msg, tone) { const el = $("#submitStatus"); el.textContent = msg || ""; el.className = "text-xs " + (tone === "err" ? "text-rose-700" : tone === "ok" ? "text-emerald-700" : tone === "warn"? "text-amber-700" : "text-slate-500"); } function refreshButtonsState() { const affaireOk = !!String(state.affaire.numero || "").trim(); $("#btnAddEquip").disabled = !affaireOk; $("#btnStartTimer").disabled = !affaireOk; $("#btnPhotoIntervenant").disabled = !affaireOk; } function validateBeforeSubmit() { const errors = []; if (!String(state.affaire.numero || "").trim()) errors.push("Numéro d’affaire obligatoire."); if (!state.timer.started) errors.push("Cliquez sur “C’est parti” pour démarrer l’intervention."); if (!intervenantPhotoFile) errors.push("Photo de l’intervenant (tenue CleanMyClim visible) requise."); if (!state.equipments.length) errors.push("Au moins 1 équipement est requis."); state.equipments.forEach(function (e, i) { if (!e.typeMateriel) errors.push(equipLabel(i) + " : type de matériel obligatoire."); if (!e.clotureIntervention) errors.push(equipLabel(i) + " : clôture de l’intervention obligatoire."); }); return errors; } function openSecretModal() { $("#secretModal").classList.remove("hidden"); $("#secretInput").value = secretInMemory || ""; $("#secretInput").focus(); } function closeSecretModal() { $("#secretModal").classList.add("hidden"); $("#secretInput").value = ""; $("#rememberSecretInMemory").checked = false; } async function doSubmitWithSecret(secret) { setSubmitStatus("Envoi en cours…", "warn"); const affaireNumero = String(state.affaire.numero || "").trim(); const safeAffaire = toSafeSlug(affaireNumero); const submitTs = now(); const dt = formatDateTimeForFilename(submitTs); let photoIndex = 1; // index global de nommage const payload = { affaire: { numero: affaireNumero, cleSecrete: String(secret || "").trim() }, access: { granted: true }, timer: { started: !!state.timer.started, startedAtMs: state.timer.startedAtMs || 0, elapsedSeconds: getElapsedSeconds() }, serviceReport: { type: "service-report", version: "v1", createdAt: new Date(state.meta.createdAt).toISOString(), updatedAt: new Date().toISOString() }, intervenant: { photoMeta: state.affaire.photoIntervenant || { name: "", size: 0, type: "" } }, equipments: state.equipments.map(function (e) { return { id: e.id, typeMateriel: e.typeMateriel, etatGeneralExterieur: e.etatGeneralExterieur, testDynamiqueMode: e.testDynamiqueMode, temperatureRelevee: e.temperatureRelevee, testDynamiqueResultat: e.testDynamiqueResultat, observationParticulieres: e.observationParticulieres, nettoyageRealise: e.nettoyageRealise, mesureTemperatureApresMode: e.mesureTemperatureApresMode, temperatureApres: e.temperatureApres, clotureIntervention: e.clotureIntervention, observations: e.observations, photosMeta: e.photos }; }), client: { userAgent: navigator.userAgent, submittedAt: new Date().toISOString() } }; const fd = new FormData(); fd.append("payload", JSON.stringify(payload)); if (intervenantPhotoFile) { const ext = "jpg"; // selon votre demande const newName = safeAffaire + "_" + dt + "_" + (photoIndex++) + "." + ext; fd.append("photo_intervenant", intervenantPhotoFile, newName); } state.equipments.forEach(function (e, eqIdx) { const files = fileStore.get(e.id) || {}; PHOTO_FIELDS.forEach(function (p) { const f = files[p.key]; if (!f) return; const ext = "jpg"; const newName = safeAffaire + "_" + dt + "_" + (photoIndex++) + "." + ext; fd.append("equipments[" + eqIdx + "][" + p.key + "]", f, newName); }); }); try { const res = await fetch(ENDPOINT_URL, { method: "POST", body: fd }); if (!res.ok) { const text = await res.text().catch(function () { return ""; }); setSubmitStatus("Échec HTTP " + res.status + " " + res.statusText + " – " + clampText(text, 220), "err"); return; } let respText = ""; try { respText = await res.text(); } catch (e) { respText = ""; } setSubmitStatus("Transmission OK. Réponse: " + (clampText(respText, 220) || "OK"), "ok"); clearDraft(); } catch (err) { setSubmitStatus("Erreur réseau: " + (err && err.message ? err.message : String(err)), "err"); } } function setAccessStatus(msg, tone) { const el = $("#accessKeyStatus"); el.textContent = msg || ""; el.className = "mt-2 text-xs " + (tone === "err" ? "text-rose-700" : tone === "ok" ? "text-emerald-700" : tone === "warn"? "text-amber-700" : "text-slate-500"); } async function validateAccessKey(key) { key = String(key || "").trim(); if (!key) return { ok: false, message: "Clé d’accès obligatoire." }; if (key === "test2026") return { ok: true, message: "Accès autorisé (mode test)." }; if (!ACCESS_VALIDATE_URL) { return { ok: false, message: "Clé invalide (mode test uniquement). Configurez le webservice de validation." }; } try { const res = await fetch(ACCESS_VALIDATE_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ accessKey: key }) }); if (!res.ok) return { ok: false, message: "Validation refusée (HTTP " + res.status + ")." }; const data = await res.json().catch(function(){ return null; }); const ok = !!(data && (data.ok || data.authorized)); return ok ? { ok: true, message: "Accès autorisé." } : { ok: false, message: "Clé invalide." }; } catch (e) { return { ok: false, message: "Erreur réseau lors de la validation." }; } } function unlockApp() { accessGranted = true; $("#accessModal").classList.add("hidden"); $("#appRoot").classList.remove("hidden"); renderAll(); refreshButtonsState(); } $("#affaireNumero").addEventListener("input", function (e) { state.affaire.numero = e.target.value || ""; saveDraft(); renderIntervenantPhotoUI(); }); $("#btnStartTimer").addEventListener("click", function () { startTimer(); }); $("#btnPhotoIntervenant").addEventListener("click", function () { if (!String(state.affaire.numero || "").trim()) return; $("#photoIntervenantInput").click(); }); $("#photoIntervenantInput").addEventListener("change", function () { const file = this.files && this.files[0]; if (!file) return; intervenantPhotoFile = file; state.affaire.photoIntervenant = { name: file.name, size: file.size, type: file.type }; saveDraft(); renderIntervenantPhotoUI(); }); $("#btnAddEquip").addEventListener("click", function () { if (!String(state.affaire.numero || "").trim()) { setSubmitStatus("Renseignez d’abord le numéro d’affaire.", "err"); return; } addEquipment(); }); $("#btnClearDraft").addEventListener("click", function () { clearDraft(); }); $("#btnSubmit").addEventListener("click", function () { setSubmitStatus("", "muted"); const errors = validateBeforeSubmit(); if (errors.length) { setSubmitStatus("Erreurs: " + errors.join(" | "), "err"); return; } openSecretModal(); }); $("#btnCloseSecretModal").addEventListener("click", closeSecretModal); $("#btnCancelSecret").addEventListener("click", closeSecretModal); $("#btnConfirmSecret").addEventListener("click", function () { const secret = ($("#secretInput").value || "").trim(); if (!secret) { setSubmitStatus("Clé secrète obligatoire pour transmettre.", "err"); return; } if ($("#rememberSecretInMemory").checked) secretInMemory = secret; else secretInMemory = ""; closeSecretModal(); doSubmitWithSecret(secret); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { const secretModal = $("#secretModal"); if (!secretModal.classList.contains("hidden")) closeSecretModal(); } }); window.addEventListener("beforeunload", function () { saveDraft(); }); $("#btnAccessValidate").addEventListener("click", async function () { setAccessStatus("Validation…", "warn"); const key = $("#accessKeyInput").value || ""; const result = await validateAccessKey(key); if (!result.ok) { setAccessStatus(result.message || "Accès refusé.", "err"); return; } setAccessStatus(result.message || "Accès autorisé.", "ok"); unlockApp(); }); function renderAll() { $("#affaireNumero").value = state.affaire.numero || ""; refreshButtonsState(); renderIntervenantPhotoUI(); renderEquipments(); renderDraftStatus(); } loadDraft(); })();