planif : assignation ticket façon courriel To/Cc + décodage entités à la source du pool
Simplification (panneau détail desktop) — demande 2026-07-07 : - UN seul champ d'assignation « façon To/Cc » (fusionne « Assigner à… » + section « Équipe/renfort ») : chips À = assigné (principal, retirable → renvoie au pool) + Cc = assistants (renfort) ; le champ ajoute : 1er tech = assigné (À), suivants (job natif) = assistants (Cc). Handlers jdAssignOrAssist (routage) + jdUnassign ; réutilise jdAssignTech/jdAddAssistant/jdRemoveAssistant. - Timeline de l'assistant GRISÉE (au lieu de hachurée lilas) : .kbb-blk.assist + .tl-blk-assist (opacité + gris) dans le kanban ET la grille semaine. - ⚠️ RecipientSelect est COURRIEL-only (pas réutilisable pour des techs) → on réutilise le PATRON chips+autosuggest avec TechSelect. - Décodage entités HTML à la SOURCE du pool : capPickups décode subject/customer_name/address/ service_location/legacy_detail (deEnt) → fini « l'église » dans le pool, la feuille, l'assignation. Vérifié : build + parse OK, 0 erreur console. ⚠️ rendu visuel du panneau détail NON confirmé en aperçu (l'environnement d'aperçu bloquait l'ouverture du volet ce tour) — à valider sur desktop réel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
92ed0a0734
commit
915a898c04
|
|
@ -485,7 +485,7 @@
|
|||
<div class="tl">
|
||||
<div v-if="cellOcc(t.id, d.iso) && cellOcc(t.id, d.iso).hasReg" class="tl-shift tl-shift-click" :class="{ over: cellOcc(t.id, d.iso).over }" :style="pos(cellOcc(t.id, d.iso).shiftS, cellOcc(t.id, d.iso).shiftE)" @click.stop="openDayFromCell(t, d)"></div>
|
||||
<div v-for="(b, bi) in cellGardeBands(t.id, d.iso)" :key="'g' + bi" class="tl-shift tl-shift-click oncall" :style="{ left: b.left, width: b.width }" @click.stop="openDayFromCell(t, d)"></div>
|
||||
<div v-for="(b, bi) in cellBlocks(t.id, d.iso)" :key="'j' + bi" class="tl-blk tl-blk-click" :class="{ 'tl-blk-legacy': b.legacy, 'tl-blk-done': b.done }" :style="blockStyle(b, cellPct(t.id, d.iso))" @click.stop="openDayFromCell(t, d)" @mousedown.stop><q-icon v-if="b.done || (b.e - b.s) >= (dayMode ? 0.75 : 2.5)" :name="blkIcon(b)" :style="{ color: blkColor(b) }" size="12px" /><q-tooltip class="bg-grey-9" :delay="350" style="font-size:11px"><template v-if="b.assist">👥 <b>Assistant (renfort)</b> · ≈{{ Math.round((b.e - b.s) * 10) / 10 }}h</template><template v-else><b>{{ b.subject || b.skill || 'Job' }}</b><br>{{ b.legacy ? (b.legacy_dept || b.dept || 'Ticket') : (b.skill || 'Job') }} · ≈{{ Math.round((b.e - b.s) * 10) / 10 }}h<span v-if="blkOpsOnly(b)"><br><span class="text-warning">⚠ Dispatché OPS — pas publié dans F</span></span><br><span class="text-info">clic = détails du jour (carte + trajet) · clic droit = renvoyer au pool</span></template></q-tooltip><q-menu v-if="b.job && !b.legacy && !b.assist" context-menu touch-position auto-close><q-list dense style="min-width:180px"><q-item clickable @click="unassignOne(b)"><q-item-section avatar><q-icon name="inbox" color="grey-7" /></q-item-section><q-item-section>Renvoyer au pool</q-item-section></q-item></q-list></q-menu></div>
|
||||
<div v-for="(b, bi) in cellBlocks(t.id, d.iso)" :key="'j' + bi" class="tl-blk tl-blk-click" :class="{ 'tl-blk-legacy': b.legacy, 'tl-blk-done': b.done, 'tl-blk-assist': b.assist }" :style="blockStyle(b, cellPct(t.id, d.iso))" @click.stop="openDayFromCell(t, d)" @mousedown.stop><q-icon v-if="b.done || (b.e - b.s) >= (dayMode ? 0.75 : 2.5)" :name="blkIcon(b)" :style="{ color: blkColor(b) }" size="12px" /><q-tooltip class="bg-grey-9" :delay="350" style="font-size:11px"><template v-if="b.assist">👥 <b>Assistant (renfort)</b> · ≈{{ Math.round((b.e - b.s) * 10) / 10 }}h</template><template v-else><b>{{ b.subject || b.skill || 'Job' }}</b><br>{{ b.legacy ? (b.legacy_dept || b.dept || 'Ticket') : (b.skill || 'Job') }} · ≈{{ Math.round((b.e - b.s) * 10) / 10 }}h<span v-if="blkOpsOnly(b)"><br><span class="text-warning">⚠ Dispatché OPS — pas publié dans F</span></span><br><span class="text-info">clic = détails du jour (carte + trajet) · clic droit = renvoyer au pool</span></template></q-tooltip><q-menu v-if="b.job && !b.legacy && !b.assist" context-menu touch-position auto-close><q-list dense style="min-width:180px"><q-item clickable @click="unassignOne(b)"><q-item-section avatar><q-icon name="inbox" color="grey-7" /></q-item-section><q-item-section>Renvoyer au pool</q-item-section></q-item></q-list></q-menu></div>
|
||||
<!-- Aperçu d'occupation projetée pendant le drag : barre fantôme + delta -->
|
||||
<div v-if="isDropTarget(t.id, d.iso) && projPct(t.id, d.iso) != null" class="tl-proj" :style="{ width: Math.min(100, projPct(t.id, d.iso)) + '%', background: occColor(projPct(t.id, d.iso)) }"></div>
|
||||
<!-- pas de bulle sur le FOND de cellule : clic sur un bloc = son ticket · clic ailleurs dans la cellule = éditeur de jour (liste + timeline + carte) -->
|
||||
|
|
@ -1394,13 +1394,21 @@
|
|||
<div class="col" style="min-width:0">
|
||||
<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>
|
||||
<!-- QUI fait la job : chip visible + assignation DIRECTE depuis le volet (avec regroupement même adresse) -->
|
||||
<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-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)" />
|
||||
<TicketStatusControl v-if="jobDetail.canTeam" doctype="Dispatch Job" :docname="jobDetail.name" :status="jobDetail.status" :scheduled-date="jobDetail.iso"
|
||||
@changed="p => { if (p && p.status) jobDetail.status = p.status; reloadOccupancy(); reloadPool && reloadPool() }" />
|
||||
<!-- Assignation façon courriel To/Cc — UN champ : À = assigné (principal) · Cc = assistants (renfort, créneau grisé). -->
|
||||
<div class="q-mt-xs jd-assign">
|
||||
<div class="row items-center q-gutter-xs">
|
||||
<q-chip v-if="jobDetail.techName" dense removable @remove="jdUnassign" 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>
|
||||
<template v-if="jobDetail.canTeam">
|
||||
<q-chip v-for="a in jobDetail.team" :key="a.tech_id" dense removable @remove="jdRemoveAssistant(a.tech_id)" outline color="blue-grey-6" text-color="blue-grey-8" icon="group">Cc · {{ jdAssistantName(a) }}<q-tooltip class="bg-grey-9" style="font-size:11px">Assistant (renfort) — son créneau est réservé et grisé (anti double-booking)</q-tooltip></q-chip>
|
||||
<q-spinner v-if="jobDetail.teamLoading" size="14px" color="blue-grey-6" />
|
||||
</template>
|
||||
<q-space />
|
||||
<TicketStatusControl v-if="jobDetail.canTeam" doctype="Dispatch Job" :docname="jobDetail.name" :status="jobDetail.status" :scheduled-date="jobDetail.iso"
|
||||
@changed="p => { if (p && p.status) jobDetail.status = p.status; reloadOccupancy(); reloadPool && reloadPool() }" />
|
||||
</div>
|
||||
<TechSelect v-model="jobDetail.assignTech" :options="jdTeamOptions" :label="jobDetail.techName ? (jobDetail.canTeam ? 'Ajouter un assistant (Cc)…' : 'Réassigner…') : 'Assigner à… (À)'" style="min-width:170px;flex:1" class="q-mt-xs" @update:model-value="v => v && jdAssignOrAssist(v)" />
|
||||
<div v-if="jobDetail.canTeam" class="text-caption text-grey-6 q-mt-xs">{{ jobDetail.techName ? 'Ajout = assistant (Cc / renfort) : son créneau est réservé et grisé. Retirer « À » = désassigner.' : 'Choisir un tech = l’assigner (À). Les ajouts suivants deviennent des assistants (Cc).' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-space /><q-btn flat round dense icon="close" v-close-popup />
|
||||
|
|
@ -1452,17 +1460,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="jobDetail.canTeam" class="jd-team">
|
||||
<div class="text-subtitle2 row items-center q-mb-xs"><q-icon name="group" size="18px" class="q-mr-xs" /> Équipe / renfort<q-space /><q-spinner v-if="jobDetail.teamLoading" size="16px" color="deep-purple" /></div>
|
||||
<div v-if="jobDetail.team && jobDetail.team.length" class="row items-center q-gutter-xs q-mb-xs">
|
||||
<q-chip v-for="a in jobDetail.team" :key="a.tech_id" dense removable @remove="jdRemoveAssistant(a.tech_id)" color="deep-purple-1" text-color="deep-purple-9" icon="group">{{ jdAssistantName(a) }}</q-chip>
|
||||
</div>
|
||||
<div v-else class="text-caption text-grey-6 q-mb-xs">Aucun assistant. Ajoutez un renfort → un bloc <b>hachuré</b> sera réservé dans son horaire (anti double‑booking).</div>
|
||||
<div class="row items-center no-wrap q-gutter-xs">
|
||||
<TechSelect v-model="jobDetail.teamAdd" :options="jdTeamOptions" label="Ajouter un assistant (nom · compétence)" style="flex:1;min-width:0" />
|
||||
<q-btn dense unelevated color="deep-purple" icon="group_add" label="Ajouter" :disable="!jobDetail.teamAdd" @click="jdAddAssistant" no-caps />
|
||||
</div>
|
||||
</div>
|
||||
<!-- « Équipe / renfort » fusionnée dans le champ d'assignation To/Cc en haut du volet (À = assigné · Cc = assistants). -->
|
||||
<div class="text-subtitle2 q-mt-md q-mb-xs row items-center">Commentaires / fil du billet<span v-if="jdMessages.length" class="text-grey-5 q-ml-sm" style="font-size:10px">· plus récent en haut</span></div>
|
||||
<div v-if="jobDetail.loading" class="text-center q-pa-md"><q-spinner size="26px" color="primary" /><div class="text-caption text-grey-6 q-mt-sm">Lecture du ticket…</div></div>
|
||||
<template v-else-if="jdMessages.length">
|
||||
|
|
@ -2470,6 +2468,18 @@ async function jdRemoveAssistant (techId) {
|
|||
if (!jobDetail.name) return
|
||||
try { await roster.removeAssistant(jobDetail.name, techId); const r = await roster.getJobTeam(jobDetail.name); jobDetail.team = r.assistants || []; await reloadOccupancy(); $q.notify({ type: 'info', message: 'Assistant retiré', timeout: 1600 }) } catch (e) { err(e) }
|
||||
}
|
||||
// Champ unique d'assignation façon courriel To/Cc : 1er tech choisi = assigné (À) ; ajouts suivants (job natif) = assistants (Cc / renfort).
|
||||
function jdAssignOrAssist (techId) {
|
||||
if (!techId) return
|
||||
if (jobDetail.techName && jobDetail.canTeam) { jobDetail.teamAdd = techId; jdAddAssistant(); jobDetail.teamAdd = null } // déjà un assigné + job natif → assistant
|
||||
else jdAssignTech(techId) // sinon → assigner / réassigner (vaut aussi pour les tickets legacy, sans assistant)
|
||||
jobDetail.assignTech = null // vider le champ après le choix
|
||||
}
|
||||
// Retirer le chip « À » (assigné) → renvoyer le job au pool (non assigné).
|
||||
async function jdUnassign () {
|
||||
if (!jobDetail.name) return
|
||||
try { await roster.unassignJobRoster(jobDetail.name); jobDetail.techId = ''; jobDetail.techName = ''; await reloadOccupancy(); if (reloadPool) await reloadPool(); $q.notify({ type: 'info', message: 'Renvoyé au pool (non assigné)', timeout: 1800 }) } catch (e) { err(e) }
|
||||
}
|
||||
let _kbClickT = null
|
||||
function kbBlockClick (t) { clearTimeout(_kbClickT); _kbClickT = setTimeout(() => { if (kanbanDay.value) openDayFromCell(t, kanbanDay.value) }, 230) } // simple clic (différé) = éditeur de jour
|
||||
function kbBlockDbl (b, t) { clearTimeout(_kbClickT); openJobDetail(b, t) } // double-clic = volet détails
|
||||
|
|
@ -4658,8 +4668,21 @@ const PICKUP_RE = /ramm?ass|r[ée]cup[ée]ration/i
|
|||
const PICKUP_MIN = 5
|
||||
function isPickup (j) { return !!j && PICKUP_RE.test(String(j.subject || j.service_type || j.job_type || '')) }
|
||||
function jobDur (j) { if (isPickup(j)) return PICKUP_MIN / 60; return (j && (j.est_min ? j.est_min / 60 : Number(j.duration_h || 1))) || 1 }
|
||||
// Cap la durée AFFICHÉE des ramassages à 5 min (le temps affiché suit alors jobDur partout : pool, carte, occupation).
|
||||
function capPickups (jobs) { return (jobs || []).map(j => isPickup(j) ? { ...j, est_min: PICKUP_MIN } : j) }
|
||||
// Normalise les jobs du pool à l'entrée (SOURCE UNIQUE, appelé aux 3 points de chargement) :
|
||||
// (1) décode les entités HTML des champs texte (adresses legacy « l'église » → « l'église ») via deEnt,
|
||||
// (2) cap la durée AFFICHÉE des ramassages à 5 min (le temps suit jobDur partout : pool, carte, occupation).
|
||||
function capPickups (jobs) {
|
||||
return (jobs || []).map(j => {
|
||||
if (!j) return j
|
||||
const d = { ...j }
|
||||
if (d.subject) d.subject = deEnt(d.subject)
|
||||
if (d.customer_name) d.customer_name = deEnt(d.customer_name)
|
||||
if (d.address) d.address = deEnt(d.address)
|
||||
if (d.service_location) d.service_location = deEnt(d.service_location)
|
||||
if (d.legacy_detail) d.legacy_detail = deEnt(d.legacy_detail)
|
||||
return isPickup(d) ? { ...d, est_min: PICKUP_MIN } : d
|
||||
})
|
||||
}
|
||||
// Construit un plan job→tech×jour SANS rien écrire (proposition). Chaque job garde sa date si elle est dans la fenêtre.
|
||||
function buildSuggestion () {
|
||||
const useSel = selectedNames.value.length > 0
|
||||
|
|
@ -6363,7 +6386,8 @@ tr.res-hidden .hide-eye { opacity: 1; }
|
|||
.kbb-leg-t { font-size: 9px; font-weight: 700; color: #5b6b80; white-space: nowrap; }
|
||||
.kbb-blk { position: absolute; top: 7px; bottom: 7px; border-radius: 5px; background: #fff; color: #1c2533; border: 1.5px solid #cfd6e0; border-left-width: 4px; display: flex; flex-direction: column; justify-content: center; gap: 1px; padding: 2px 7px; cursor: grab; box-shadow: 0 1px 3px rgba(0,0,0,.12); overflow: hidden; } /* carte BLANCHE, contour + icône colorés (skill), texte noir — style Gaiia */
|
||||
.kbb-blk:hover { box-shadow: 0 2px 8px rgba(0,0,0,.22); z-index: 2; }
|
||||
.kbb-blk.assist { border-style: dashed; background: #faf8ff; cursor: default; }
|
||||
.kbb-blk.assist { border-style: dashed !important; background: #eceff1 !important; color: #90a4ae; opacity: .6; cursor: default; } /* assistant (renfort) = créneau GRISÉ (réservé, secondaire) */
|
||||
.tl-blk.tl-blk-assist { opacity: .5; filter: grayscale(0.55); border: 1px dashed rgba(120,144,176,.7) !important; } /* idem dans la grille semaine : bloc assistant grisé */
|
||||
.kbb-blk-l1 { display: flex; align-items: center; gap: 3px; font-size: 11px; font-weight: 700; line-height: 1.2; }
|
||||
.kbb-blk-l2 { font-size: 9.5px; font-weight: 500; color: #6a7686; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.kbb-blk-t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user