fix(ops): job detail — show all fields (raw pool job), full title, assign tech
Cause du « on ne voit qu'une partie du titre, aucun autre détail » : openJobDetail lisait la forme BLOC (b.customer/skill/detail/legacy_id) mais reçoit un job BRUT du pool quand ouvert depuis une goutte/carrousel (customer_name/required_skill/legacy_detail/legacy_ticket_id) → tous les champs vides sauf le titre. Corrigé : mappe les DEUX formes (client, compétence, adresse, description, n° ticket, date) + charge le fil du billet via le bon id → détails + commentaires s'affichent. - Titre : plus tronqué (ellipsis → retour à la ligne, titre complet). - Assignation DEPUIS le volet : sélecteur « Assigner à… / Réassigner… » (jdAssignTech) ; si d'autres tâches à la MÊME adresse/client/jour → propose de les assigner au même tech (réutilise assignNames + siblingJobs, comme la grille). Vérifié live (goutte carrousel, ticket #253649) : titre complet non tronqué, méta = compétence tv + adresse 44 Carre Morrison + dept + date, section commentaires + sélecteur d'assignation présents ; 0 erreur. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
98184d0a61
commit
92c320d08f
|
|
@ -1357,12 +1357,13 @@
|
||||||
<q-card-section class="row items-center q-pb-sm">
|
<q-card-section class="row items-center q-pb-sm">
|
||||||
<q-icon name="assignment" color="primary" size="22px" class="q-mr-sm" />
|
<q-icon name="assignment" color="primary" size="22px" class="q-mr-sm" />
|
||||||
<div class="col" style="min-width:0">
|
<div class="col" style="min-width:0">
|
||||||
<div class="text-h6 ellipsis">{{ jobDetail.subject }}</div>
|
<div class="text-h6" style="line-height:1.25;word-break:break-word">{{ jobDetail.subject }}</div>
|
||||||
<div class="text-caption text-grey-7"><template v-if="jobDetail.customer">{{ jobDetail.customer }}</template><template v-if="jobDetail.lid"> · Ticket #{{ jobDetail.lid }}</template><template v-if="jobDetail.dept"> · {{ jobDetail.dept }}</template></div>
|
<div class="text-caption text-grey-7"><template v-if="jobDetail.customer">{{ jobDetail.customer }}</template><template v-if="jobDetail.lid"> · Ticket #{{ jobDetail.lid }}</template><template v-if="jobDetail.dept"> · {{ jobDetail.dept }}</template></div>
|
||||||
<!-- QUI fait la job : chip bien visible (highlight demandé) — assigné ou non -->
|
<!-- QUI fait la job : chip visible + assignation DIRECTE depuis le volet (avec regroupement même adresse) -->
|
||||||
<div class="q-mt-xs">
|
<div class="q-mt-xs row items-center q-gutter-xs no-wrap">
|
||||||
<q-chip v-if="jobDetail.techName" dense color="indigo-6" text-color="white" class="text-weight-bold jd-tech-chip"><q-avatar color="indigo-9" text-color="white" size="20px">{{ initials(jobDetail.techName) }}</q-avatar>{{ jobDetail.techName }}</q-chip>
|
<q-chip v-if="jobDetail.techName" dense color="indigo-6" text-color="white" class="text-weight-bold jd-tech-chip"><q-avatar color="indigo-9" text-color="white" size="20px">{{ initials(jobDetail.techName) }}</q-avatar>{{ jobDetail.techName }}</q-chip>
|
||||||
<q-chip v-else dense outline color="grey-7" text-color="grey-8" icon="person_off">Non assigné</q-chip>
|
<q-chip v-else dense outline color="grey-7" text-color="grey-8" icon="person_off">Non assigné</q-chip>
|
||||||
|
<TechSelect v-model="jobDetail.assignTech" :options="jdTeamOptions" :label="jobDetail.techName ? 'Réassigner…' : 'Assigner à…'" style="min-width:170px;max-width:230px" @update:model-value="v => v && jdAssignTech(v)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-space /><q-btn flat round dense icon="close" v-close-popup />
|
<q-space /><q-btn flat round dense icon="close" v-close-popup />
|
||||||
|
|
@ -2192,7 +2193,7 @@ function exportGpx (techId) {
|
||||||
window.open(roster.gpxUrl(techId, from, to), '_blank')
|
window.open(roster.gpxUrl(techId, from, to), '_blank')
|
||||||
}
|
}
|
||||||
// ── Détails d'un job : double-clic sur un bloc → grand volet DROIT (billet + commentaires) ; simple clic = éditeur de jour. ──
|
// ── Détails d'un job : double-clic sur un bloc → grand volet DROIT (billet + commentaires) ; simple clic = éditeur de jour. ──
|
||||||
const jobDetail = reactive({ open: false, name: '', subject: '', customer: '', address: '', skill: '', time: '', detail: '', lid: null, dept: '', techId: '', techName: '', lat: null, lon: null, loading: false, thread: null, canTeam: false, team: [], teamLoading: false, teamAdd: null, geofence: null })
|
const jobDetail = reactive({ open: false, name: '', subject: '', customer: '', address: '', skill: '', time: '', durH: 1, detail: '', lid: null, iso: '', dept: '', techId: '', techName: '', lat: null, lon: null, loading: false, thread: null, canTeam: false, team: [], teamLoading: false, teamAdd: null, assignTech: null, geofence: null })
|
||||||
// Décode les entités HTML (« d'équipement » → « d'équipement ») pour NORMALISER les détails affichés (sujets legacy encodés).
|
// Décode les entités HTML (« d'équipement » → « d'équipement ») pour NORMALISER les détails affichés (sujets legacy encodés).
|
||||||
const _deEntEl = typeof document !== 'undefined' ? document.createElement('textarea') : null
|
const _deEntEl = typeof document !== 'undefined' ? document.createElement('textarea') : null
|
||||||
function deEnt (s) { if (!s || String(s).indexOf('&') < 0 || !_deEntEl) return s || ''; _deEntEl.innerHTML = String(s); return _deEntEl.value }
|
function deEnt (s) { if (!s || String(s).indexOf('&') < 0 || !_deEntEl) return s || ''; _deEntEl.innerHTML = String(s); return _deEntEl.value }
|
||||||
|
|
@ -2200,11 +2201,15 @@ async function openJobDetail (b, t) {
|
||||||
if (!b) return
|
if (!b) return
|
||||||
jdShowTrack.value = false; jdShowMap.value = false
|
jdShowTrack.value = false; jdShowMap.value = false
|
||||||
const durH0 = b.dur != null ? +b.dur : (b.est_min ? +b.est_min / 60 : (b.duration_h != null ? +b.duration_h : 1))
|
const durH0 = b.dur != null ? +b.dur : (b.est_min ? +b.est_min / 60 : (b.duration_h != null ? +b.duration_h : 1))
|
||||||
Object.assign(jobDetail, { open: true, name: b.name || '', subject: deEnt(b.subject || b.name || 'Job'), customer: deEnt(b.customer || ''), address: deEnt(b.address || ''), skill: b.skill || '', time: (b.start || (b.s != null ? fmtH(b.s) : '')) + (b.dur ? ' · ' + Math.round(b.dur * 10) / 10 + 'h' : ''), durH: Math.round((durH0 || 1) * 100) / 100, detail: deEnt(b.detail || ''), lid: b.legacy_id || null, dept: b.dept || '', techId: (t && t.id) || '', techName: (t && t.name) || '', lat: b.lat != null ? +b.lat : null, lon: b.lon != null ? +b.lon : null, loading: !!b.legacy_id, thread: null, canTeam: !!(b.name && !b.legacy), team: [], teamLoading: false, teamAdd: null })
|
// b peut être un BLOC assigné (b.customer/skill/detail/legacy_id) OU un job BRUT du pool (customer_name/required_skill/
|
||||||
|
// legacy_detail/legacy_ticket_id) — ouvert depuis une goutte/carrousel. On mappe les DEUX formes pour toujours montrer
|
||||||
|
// client, compétence, description et le fil du billet.
|
||||||
|
const lid = b.legacy_id || b.legacy_ticket_id || null
|
||||||
|
Object.assign(jobDetail, { open: true, name: b.name || '', subject: deEnt(b.subject || b.name || 'Job'), customer: deEnt(b.customer || b.customer_name || ''), address: deEnt(b.address || b.service_location || ''), skill: b.skill || b.required_skill || '', time: (b.start ? (b.start + (b.dur ? ' · ' + Math.round(b.dur * 10) / 10 + 'h' : '')) : (b.scheduled_date ? fmtDueLabel(b.scheduled_date) : '')), durH: Math.round((durH0 || 1) * 100) / 100, detail: deEnt(b.detail || b.legacy_detail || ''), lid, iso: b.scheduled_date || b.iso || (boardView.value === 'routes' ? routesDay.value : '') || '', dept: b.dept || b.legacy_dept || '', techId: (t && t.id) || b.assigned_tech || '', techName: (t && t.name) || '', lat: b.lat != null ? +b.lat : (b.latitude != null ? +b.latitude : null), lon: b.lon != null ? +b.lon : (b.longitude != null ? +b.longitude : null), loading: !!lid, thread: null, canTeam: !!(b.name && !b.legacy), team: [], teamLoading: false, teamAdd: null, assignTech: null })
|
||||||
// Géofencing (suivi façon colis) : timeline En route → Arrivé → Reparti, non bloquant.
|
// Géofencing (suivi façon colis) : timeline En route → Arrivé → Reparti, non bloquant.
|
||||||
jobDetail.geofence = null
|
jobDetail.geofence = null
|
||||||
if (b.name) roster.jobGeofence(b.name).then(g => { if (jobDetail.name === b.name) jobDetail.geofence = g }).catch(() => {})
|
if (b.name) roster.jobGeofence(b.name).then(g => { if (jobDetail.name === b.name) jobDetail.geofence = g }).catch(() => {})
|
||||||
if (b.legacy_id) { try { jobDetail.thread = await roster.ticketThread(b.legacy_id) } catch (e) { jobDetail.thread = { error: true, messages: [] } } finally { jobDetail.loading = false } }
|
if (lid) { try { jobDetail.thread = await roster.ticketThread(lid) } catch (e) { jobDetail.thread = { error: true, messages: [] } } finally { jobDetail.loading = false } }
|
||||||
if (jobDetail.canTeam) { jobDetail.teamLoading = true; try { const r = await roster.getJobTeam(b.name); jobDetail.team = r.assistants || [] } catch (e) { jobDetail.team = [] } finally { jobDetail.teamLoading = false } }
|
if (jobDetail.canTeam) { jobDetail.teamLoading = true; try { const r = await roster.getJobTeam(b.name); jobDetail.team = r.assistants || [] } catch (e) { jobDetail.team = [] } finally { jobDetail.teamLoading = false } }
|
||||||
}
|
}
|
||||||
// Timeline géofencing pour l'affichage (façon suivi de colis) : 4 étapes + heure atteinte.
|
// Timeline géofencing pour l'affichage (façon suivi de colis) : 4 étapes + heure atteinte.
|
||||||
|
|
@ -2316,6 +2321,24 @@ async function jdSetDuration (h) {
|
||||||
const pj = (assignPanel.jobs || []).find(x => x.name === jobDetail.name)
|
const pj = (assignPanel.jobs || []).find(x => x.name === jobDetail.name)
|
||||||
await patchJob(pj || { name: jobDetail.name }, { duration_h: h }, 'Durée révisée : ' + h + ' h — Ré-optimiser pour recalculer')
|
await patchJob(pj || { name: jobDetail.name }, { duration_h: h }, 'Durée révisée : ' + h + ' h — Ré-optimiser pour recalculer')
|
||||||
}
|
}
|
||||||
|
// Assigner ce job à un tech DEPUIS le volet détail ; si d'autres tâches à la MÊME adresse/client/jour → proposer de
|
||||||
|
// les assigner au même tech (moins de clics). Réutilise assignNames + siblingJobs (même logique que la grille).
|
||||||
|
async function jdAssignTech (techId) {
|
||||||
|
if (!techId || !jobDetail.name) return
|
||||||
|
const tech = techById[techId] || (visibleTechs.value || []).find(x => x.id === techId)
|
||||||
|
if (!tech) { jobDetail.assignTech = null; return }
|
||||||
|
const iso = jobDetail.iso || routesDay.value || todayISO()
|
||||||
|
let names = [jobDetail.name]
|
||||||
|
const sibs = siblingJobs(names)
|
||||||
|
if (sibs.length) {
|
||||||
|
const ex = sibs[0]
|
||||||
|
const yes = await new Promise(res => $q.dialog({ title: 'Assigner le groupe ?', message: `${sibs.length} autre(s) tâche(s) à « ${String(ex.address || ex.service_location || '').slice(0, 60)} » — les assigner aussi à ${tech.name} ?`, cancel: { label: 'Non, juste celle-ci', flat: true }, ok: { label: 'Oui, tout assigner', color: 'primary', unelevated: true } }).onOk(() => res(true)).onCancel(() => res(false)).onDismiss(() => res(false)))
|
||||||
|
if (yes) names = [...new Set([...names, ...sibs.map(s => s.name)])]
|
||||||
|
}
|
||||||
|
await assignNames(names, tech, { iso })
|
||||||
|
jobDetail.techId = tech.id; jobDetail.techName = tech.name; jobDetail.assignTech = null
|
||||||
|
$q.notify({ type: 'positive', message: names.length > 1 ? `${names.length} tâches assignées à ${tech.name}` : `Assigné à ${tech.name}`, timeout: 1800 })
|
||||||
|
}
|
||||||
// Toggle Traccar disponible UNIQUEMENT aujourd'hui ou hier (ET) — sinon Traccar trop lourd/lent.
|
// Toggle Traccar disponible UNIQUEMENT aujourd'hui ou hier (ET) — sinon Traccar trop lourd/lent.
|
||||||
const kbCanTrack = computed(() => {
|
const kbCanTrack = computed(() => {
|
||||||
const iso = kanbanDay.value && kanbanDay.value.iso; const n = nowET.value.iso; if (!iso || !n) return false
|
const iso = kanbanDay.value && kanbanDay.value.iso; const n = nowET.value.iso; if (!iso || !n) return false
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user