diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index fe41cf3..2aca37f 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -933,6 +933,29 @@ async function handleFieldTech (req, res, method, path, url) { const startTs = j.actual_start || (onSite ? ts : null) return json(res, up.ok ? 200 : 500, { ok: !!up.ok, on_site: onSite, distance_m: dist, minutes: minutesBetween(startTs, onSite ? ts : j.actual_end), actual_start: patch.actual_start || j.actual_start || '', actual_end: patch.actual_end || j.actual_end || '' }) } + // COMMENTAIRE du tech depuis l'app terrain → fil du ticket. Reste sur OPS (plus de renvoi vers store.targo.ca). + // Par défaut = NOTE INTERNE (jamais envoyée au client). Envoyée au client SEULEMENT si public=true (coché). + if (path === '/field/comment' && method === 'POST') { + const b = await parseBody(req); const name = fieldVerify(b.token || token); if (!name) return json(res, 403, { ok: false, error: 'lien invalide' }) + const text = String(b.text || '').trim(); if (!text) return json(res, 400, { ok: false, error: 'texte requis' }) + const isPublic = !!b.public + let posted = false, sentToCustomer = false + try { + const j = await erp.get('Dispatch Job', name, { fields: ['legacy_ticket_id'] }) + const lid = j && j.legacy_ticket_id + if (lid) { + // Fil legacy (osTicket) = ce que le volet job affiche. public=1 → réponse publique (courriel client) ; sinon note interne. + const msg = isPublic ? text : ('🔧 [Note tech — interne]\n' + text) + const r = await require('./legacy-dispatch-sync').postTicketLegacy(lid, msg, isPublic, '') + posted = !!(r && r.ok !== false); sentToCustomer = isPublic && posted + } else { + // Job natif (pas de ticket legacy) → Comment ERPNext sur le Dispatch Job. + await erp.create('Comment', { comment_type: 'Comment', reference_doctype: 'Dispatch Job', reference_name: name, content: (isPublic ? '📣 [Public] ' : '🔧 [Note tech] ') + text }) + posted = true + } + } catch (e) { return json(res, 500, { ok: false, error: e.message }) } + return json(res, 200, { ok: posted, public: isPublic, sentToCustomer }) + } if ((path === '/field' || path === '/field/app') && method === 'GET') return serveFieldApp(res) // PWA technicien (carte + tickets + photos + scan) if (path === '/field/tech' && method === 'GET') { // jobs du jour (+ demain) du tech, via token PAR TECH const tech = techFieldVerify(token); if (!tech) return json(res, 403, { ok: false, error: 'lien invalide' }) diff --git a/services/targo-hub/public/field-app.html b/services/targo-hub/public/field-app.html index 0a0f22f..609eda8 100644 --- a/services/targo-hub/public/field-app.html +++ b/services/targo-hub/public/field-app.html @@ -48,10 +48,18 @@
- 💬 Répondre +
+ @@ -75,6 +83,8 @@ const $=id=>document.getElementById(id); let JOBS=[],CUR=null,map=null,jobMk=nul function toast(m){const t=$('toast');t.textContent=m;t.style.display='block';setTimeout(()=>t.style.display='none',2600)} function badge(j){return j.ended?'terminé'+(j.minutes!=null?' · '+j.minutes+'m':'')+'':j.started?'en cours':'à faire'} async function api(p,o){const r=await fetch(HUB+p,o);return r.json()} +function openComment(){var b=$('cmtbox');b.style.display=(b.style.display==='none'||!b.style.display)?'block':'none';if(b.style.display==='block')$('cmtxt').focus()} +async function sendComment(){var t=($('cmtxt').value||'').trim();if(!t){toast('Écris un commentaire');return}var pub=$('cmpub').checked;var r=await api('/field/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:CUR.token,text:t,public:pub})}).catch(function(){return{ok:false}});if(r&&r.ok){$('cmtxt').value='';$('cmpub').checked=false;$('cmtbox').style.display='none';toast(pub?(r.sentToCustomer?'✓ Ajouté au ticket + envoyé au client':'✓ Ajouté (client non joignable)'):'✓ Note interne ajoutée au ticket')}else{toast('Échec — réessaie')}} function dist(a1,o1,a2,o2){const R=6371000,t=Math.PI/180,dA=(a2-a1)*t,dO=(o2-o1)*t;const x=Math.sin(dA/2)**2+Math.cos(a1*t)*Math.cos(a2*t)*Math.sin(dO/2)**2;return Math.round(R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x)))} async function load(){ const r=await api('/field/tech?t='+encodeURIComponent(T)).catch(()=>({})); @@ -101,7 +111,6 @@ function openDetail(i){ const hasLL=CUR.lat&&Math.abs(+CUR.lat)>0.01; $('ditin').href=hasLL?('https://www.google.com/maps/dir/?api=1&destination='+CUR.lat+','+CUR.lon):'#'; $('dsv').href=hasLL?('https://www.google.com/maps/@?api=1&map_action=pano&viewpoint='+CUR.lat+','+CUR.lon):'#'; - $('areply').href=CUR.reply||'#';$('areply').style.opacity=CUR.reply?1:.4; setChrono();initMap(hasLL);startWatch(hasLL); } function setChrono(){const b=$('chrono');if(CUR.ended){b.className='big doneb';b.textContent='✅ Terminé'+(CUR.minutes!=null?' · '+CUR.minutes+' min':'');b.disabled=true}