feat(planif): résumé de Publier complet + groupé par technicien
« Toute modification doit être résumée avant d'être publiée (au cas où on se trompe). » Le résumé était incomplet et groupé par date : - retraits de quarts INVISIBLES (pubSummary.removed codé en dur à 0) alors que le hub SUPPRIME au Publier tout quart absent de la charge (publish-week) — une suppression accidentelle passait donc inaperçue ; - groupé par date, difficile de voir ce qui change POUR UN TECH. pubSummary regroupe désormais PAR TECHNICIEN (avatar + nom) et couvre tout ce que Publier va appliquer : quarts à publier (ajouts/modifs), quarts RETIRÉS (diff contre l'instantané serveur du chargement, serverSet → capte les suppressions), et congés/absences en attente. En-tête : « N quart(s) · R retiré(s) · A congé(s) · L levé(s) ». Un tech sans changement n'apparaît pas. (La garde a son propre bouton « Publier la garde » — hors de ce flux.) Vérifié : repro logique (modif = −ancien/+nouveau, retrait, congés, tech inchangé omis) + aperçu live (Josée-Anne : Congé 14/07 + Maladie 15/07 ; Nathan Morrisseau : quart 8h–16h) — rien écrit en prod (annulé). Build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9d568986fc
commit
947595f086
|
|
@ -1338,13 +1338,15 @@
|
|||
<q-space /><q-btn flat round dense icon="close" v-close-popup />
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pt-sm">
|
||||
<div class="text-body2 q-mb-sm"><b class="text-positive">{{ pubSummary.added }}</b> quart(s) · <b v-if="pubSummary.absAdded" class="text-orange-8">{{ pubSummary.absAdded }}</b><span v-if="pubSummary.absAdded"> congé(s)</span><span v-if="pubSummary.absRemoved"> · <b class="text-teal-7">{{ pubSummary.absRemoved }}</b> congé(s) retiré(s)</span> · <b>{{ notifySms ? pubSummary.added + ' SMS aux techs' : 'sans SMS' }}</b></div>
|
||||
<div class="text-body2 q-mb-sm"><b class="text-positive">{{ pubSummary.added }}</b> quart(s)<span v-if="pubSummary.removed"> · <b class="text-negative">{{ pubSummary.removed }}</b> retiré(s)</span><span v-if="pubSummary.absAdded"> · <b class="text-orange-8">{{ pubSummary.absAdded }}</b> congé(s)</span><span v-if="pubSummary.absRemoved"> · <b class="text-teal-7">{{ pubSummary.absRemoved }}</b> congé(s) levé(s)</span> · <b>{{ notifySms ? pubSummary.added + ' SMS aux techs' : 'sans SMS' }}</b></div>
|
||||
<q-list dense bordered style="border-radius:8px;max-height:44vh;overflow:auto">
|
||||
<template v-for="d in pubSummary.days" :key="d.date">
|
||||
<q-item-label header class="q-py-xs text-weight-medium">{{ fmtDueLabel(d.date) }}</q-item-label>
|
||||
<q-item v-for="(a, i) in d.add" :key="'a' + i" dense><q-item-section avatar style="min-width:24px"><q-icon name="add" color="positive" size="16px" /></q-item-section><q-item-section>{{ a.name }}<span class="text-grey-6"> · {{ a.shift }}</span></q-item-section></q-item>
|
||||
<q-item v-for="(r, i) in d.rem" :key="'r' + i" dense><q-item-section avatar style="min-width:24px"><q-icon name="remove" color="negative" size="16px" /></q-item-section><q-item-section>{{ r.name }}<span class="text-grey-6"> · {{ r.shift }}</span></q-item-section></q-item>
|
||||
<q-item v-for="(ab, i) in d.abs" :key="'ab' + i" dense><q-item-section avatar style="min-width:24px"><q-icon :name="ab.op === 'set' ? 'event_busy' : 'event_available'" :color="ab.op === 'set' ? 'orange-8' : 'teal-7'" size="16px" /></q-item-section><q-item-section>{{ ab.name }}<span class="text-grey-6"> · {{ ab.op === 'set' ? (ab.type || 'Congé') : 'congé retiré' }}</span></q-item-section></q-item>
|
||||
<template v-for="g in pubSummary.techs" :key="g.tech">
|
||||
<q-item-label header class="q-py-xs text-weight-bold row items-center">
|
||||
<UserAvatar :tech-id="g.tech" :name="g.name" :size="18" card class="q-mr-xs" style="flex:0 0 auto" />{{ g.name }}
|
||||
</q-item-label>
|
||||
<q-item v-for="(a, i) in g.add" :key="'a' + i" dense><q-item-section avatar style="min-width:24px"><q-icon name="add" color="positive" size="16px" /></q-item-section><q-item-section>{{ a.shift }}<span class="text-grey-6"> · {{ fmtDueLabel(a.date) }}</span></q-item-section></q-item>
|
||||
<q-item v-for="(r, i) in g.rem" :key="'r' + i" dense><q-item-section avatar style="min-width:24px"><q-icon name="remove" color="negative" size="16px" /></q-item-section><q-item-section><span style="text-decoration:line-through">{{ r.shift }}</span><span class="text-grey-6"> · {{ fmtDueLabel(r.date) }}</span></q-item-section></q-item>
|
||||
<q-item v-for="(ab, i) in g.abs" :key="'ab' + i" dense><q-item-section avatar style="min-width:24px"><q-icon :name="ab.op === 'set' ? 'event_busy' : 'event_available'" :color="ab.op === 'set' ? 'orange-8' : 'teal-7'" size="16px" /></q-item-section><q-item-section>{{ ab.op === 'set' ? (ab.type || 'Congé') : 'congé levé' }}<span class="text-grey-6"> · {{ fmtDueLabel(ab.date) }}</span></q-item-section></q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
|
|
@ -4001,23 +4003,38 @@ function isCellDirty (techId, iso) { return dirtyCells.value.has(techId + '|' +
|
|||
const weekStatus = computed(() => { const st = new Set(unpublished.value.map(a => a.status)); if (st.has('Proposé')) return 'Proposé'; if (st.has('Soumis')) return 'Soumis'; if (st.has('Approuvé')) return 'Approuvé'; return 'Publié' })
|
||||
// #1 — Sommaire AVANT publication : liste les quarts NON PUBLIÉS (à publier) par jour + nb SMS.
|
||||
const pubConfirm = ref(false)
|
||||
// RÉSUMÉ DE PUBLICATION — regroupé PAR TECHNICIEN. Doit refléter TOUT ce que « Publier » va changer (« au cas où on
|
||||
// se trompe ») : quarts à publier (ajouts/modifs), quarts RETIRÉS (le hub supprime tout ce qui n'est plus dans la
|
||||
// charge — cf. roster.js publish-week), et congés/absences en attente. Le retrait est calculé contre l'instantané
|
||||
// SERVEUR pris au chargement (serverSet) → capte aussi une suppression accidentelle.
|
||||
const pubSummary = computed(() => {
|
||||
const nameOf = id => { const t = (techs.value || []).find(x => x.id === id); return (t && t.name) || id }
|
||||
const byDate = {}
|
||||
const bucket = date => (byDate[date] = byDate[date] || { date, add: [], rem: [], abs: [] })
|
||||
for (const a of unpublished.value) {
|
||||
bucket(a.date).add.push({ tech: a.tech, date: a.date, shift: a.shift_name || a.shift, name: nameOf(a.tech), status: a.status })
|
||||
const shiftLabel = s => { const t = tplByName.value[s]; return (t && t.template_name) || s }
|
||||
const byTech = {}
|
||||
const bucket = tid => (byTech[tid] = byTech[tid] || { tech: tid, name: nameOf(tid), add: [], rem: [], abs: [] })
|
||||
// Quarts à publier = état courant non Publié (Proposé/Soumis/Approuvé → deviendront Publié)
|
||||
for (const a of unpublished.value) bucket(a.tech).add.push({ date: a.date, shift: a.shift_name || shiftLabel(a.shift) })
|
||||
// Quarts RETIRÉS = présents au chargement mais plus dans l'état courant → seront supprimés au Publier
|
||||
let removed = 0
|
||||
const cur = currentSet.value
|
||||
for (const k of serverSet.value) {
|
||||
if (cur.has(k)) continue
|
||||
const i1 = k.indexOf('|'); const i2 = k.indexOf('|', i1 + 1); if (i1 < 0 || i2 < 0) continue
|
||||
bucket(k.slice(0, i1)).rem.push({ date: k.slice(i1 + 1, i2), shift: shiftLabel(k.slice(i2 + 1)) }); removed++
|
||||
}
|
||||
// CONGÉS / ABSENCES en attente (pendingAbs) : ils comptent dans « non publié » et sont bien envoyés au Publier
|
||||
// (flushPendingAbsences) — donc ils DOIVENT figurer dans le résumé (sinon un congé indéfini reste invisible, ex. Josée-Anne).
|
||||
// Congés / absences en attente (pendingAbs) — bien envoyés au Publier (flushPendingAbsences) → doivent figurer (ex. Josée-Anne).
|
||||
let absAdded = 0; let absRemoved = 0
|
||||
for (const [k, v] of Object.entries(pendingAbs.value || {})) {
|
||||
const [tid, iso] = k.split('|')
|
||||
for (const [key, v] of Object.entries(pendingAbs.value || {})) {
|
||||
const i1 = key.indexOf('|'); if (i1 < 0) continue
|
||||
const set = v && v.op === 'set'
|
||||
bucket(iso).abs.push({ tech: tid, date: iso, name: nameOf(tid), type: set ? (v.type || 'Congé') : null, op: set ? 'set' : 'remove' })
|
||||
bucket(key.slice(0, i1)).abs.push({ date: key.slice(i1 + 1), type: set ? (v.type || 'Congé') : null, op: set ? 'set' : 'remove' })
|
||||
if (set) absAdded++; else absRemoved++
|
||||
}
|
||||
return { added: unpublished.value.length, removed: 0, absAdded, absRemoved, days: Object.values(byDate).sort((x, y) => String(x.date).localeCompare(String(y.date))) }
|
||||
const list = Object.values(byTech)
|
||||
const byDate = (x, y) => String(x.date).localeCompare(String(y.date))
|
||||
for (const g of list) { g.add.sort(byDate); g.rem.sort(byDate); g.abs.sort(byDate) }
|
||||
list.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
||||
return { added: unpublished.value.length, removed, absAdded, absRemoved, techs: list }
|
||||
})
|
||||
|
||||
const holSet = computed(() => new Set(holidays.value))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user