From 2396c10f14eff090a906d6a2b67f1758135a06ae Mon Sep 17 00:00:00 2001 From: louispaulb Date: Tue, 7 Jul 2026 22:20:45 -0400 Subject: [PATCH] =?UTF-8?q?planif=20:=20extraction=20#4/3=20=E2=80=94=20di?= =?UTF-8?q?alogue=20Types=20de=20shift=20=E2=86=92=20components/planif/Shi?= =?UTF-8?q?ftTypesDialog.vue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3e extraction. Couplage traité proprement : le dialogue édite les Shift Template et la liste `templates` est PARTAGÉE (grille/garde/demande) → le composant émet `changed` et le parent recharge via refreshTemplates. - components/planif/ShiftTypesDialog.vue (NOUVEAU) : editTpls + newTpl + newTplRange + save/add/del déplacés ; semé depuis la prop `templates` à l'ouverture (watch) ; après add → refetch local ; helpers copiés (calcHours/fmtH/hToNum/numToTime/chip) ; CSS .demand-tbl + .code-chip recopiés - PlanificationPage : dialogue + état + 4 fonctions retirés ; `chip()` supprimé (seul consommateur déplacé) ; helpers partagés (calcHours/fmtH/hToNum/numToTime) CONSERVÉS ; bouton « Types de shift » → `showShiftEditor = true` ; `` - Vérifié en aperçu : 20 modèles rendus (Jour/Soir/Matinal…), pastille couleur stylée, 0 erreur Co-Authored-By: Claude Opus 4.8 --- .../components/planif/ShiftTypesDialog.vue | 86 +++++++++++++++++++ apps/ops/src/pages/PlanificationPage.vue | 53 ++---------- 2 files changed, 94 insertions(+), 45 deletions(-) create mode 100644 apps/ops/src/components/planif/ShiftTypesDialog.vue diff --git a/apps/ops/src/components/planif/ShiftTypesDialog.vue b/apps/ops/src/components/planif/ShiftTypesDialog.vue new file mode 100644 index 0000000..60cd881 --- /dev/null +++ b/apps/ops/src/components/planif/ShiftTypesDialog.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index dfe3b2d..e533a85 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -182,7 +182,7 @@
Demande — effectif requis par créneau
- +
@@ -618,41 +618,8 @@ - - - -
Types de shift
- -
- - - - - - - - - - - - - - -
NomDébutFinHeuresCouleur🛡️ Garde
{{ (t.template_name||'?')[0].toUpperCase() }} {{ t.template_name }}{{ calcHours(t.start, t.end) }} hQuart de garde (urgences) — non offert au bookingEnregistrer
- -
- -
- -
- {{ fmtH(newTplRange.min) }}h–{{ fmtH(newTplRange.max) }}h · {{ calcHours(newTpl.start, newTpl.end) }} h - - - -
-
-
-
+ + @@ -1988,6 +1955,7 @@ import { useAuthStore } from 'src/stores/auth' // email de l'agent (X-Authentik- import TechSelect from 'src/components/shared/TechSelect.vue' import LeaveDialog from 'src/components/planif/LeaveDialog.vue' // Congés & disponibilités (extrait — décomposition #4) import TechSyncDialog from 'src/components/planif/TechSyncDialog.vue' // Synchroniser les techniciens (extrait — décomposition #4) +import ShiftTypesDialog from 'src/components/planif/ShiftTypesDialog.vue' // Types de shift (extrait — décomposition #4) import TicketStatusControl from 'src/components/shared/TicketStatusControl.vue' import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags/compétences PARTAGÉ (chips colorées, création + palette, niveaux) — SOURCE UNIQUE : jobs ET techs import RouteMap from 'src/components/shared/RouteMap.vue' // carte de tournées réutilisable (revue dispatch auto + onglet Tournées) : OSRM réel, arrêts numérotés, fitTo @@ -2115,7 +2083,7 @@ const hiddenTechs = ref([]); const showHidden = ref(false) function isHidden (id) { return hiddenTechs.value.includes(id) } function toggleHidden (id) { const i = hiddenTechs.value.indexOf(id); if (i >= 0) hiddenTechs.value.splice(i, 1); else hiddenTechs.value.push(id); localStorage.setItem('roster-hidden-techs-v1', JSON.stringify(hiddenTechs.value)) } const hiddenCount = computed(() => techs.value.filter(t => isHidden(t.id)).length) -const showShiftEditor = ref(false); const editTpls = ref([]) +const showShiftEditor = ref(false) // v-model de (editTpls/newTpl/fonctions déplacés dans le composant) const showTeamEditor = ref(false); const editTechs = ref([]) const notifySms = ref(false) // ── Notifier les techs de leur tournée (SMS/Email + lien token) : aperçu d'abord (compose sans envoi), puis envoi. ── @@ -2161,10 +2129,8 @@ async function saveNotifyContact (m) { } catch (e) { err(e) } finally { m.saving = false } } const showLeave = ref(false) // ouverture de (état leaveRows/leaveFilter/newLeave + fonctions déplacés dans le composant) -const newTpl = reactive({ template_name: '', start: '08:00', end: '16:00', color: '#1976d2', on_call: 0 }) +// numToTime : heure décimale → HH:MM (partagé — aussi utilisé par le glisser-créer de quart). newTpl/newTplRange déplacés dans ShiftTypesDialog. function numToTime (h) { const hh = Math.floor(h); const mm = Math.round((h - hh) * 60); return String(hh).padStart(2, '0') + ':' + String(mm).padStart(2, '0') } -// Slider à 2 poignées pour le nouveau modèle (heures custom) ↔ newTpl.start/end -const newTplRange = computed({ get: () => ({ min: hToNum(newTpl.start) || 8, max: hToNum(newTpl.end) || 16 }), set: (v) => { newTpl.start = numToTime(v.min); newTpl.end = numToTime(v.max) } }) const LS_DEMAND = 'roster-demand-v1'; const LS_HOL = 'roster-holidays-v1'; const LS_TPL = 'roster-week-templates-v1'; const LS_GARDE = 'roster-garde-rules-v1'; const LS_GARDE_MANUAL = 'roster-garde-manual-v1' @@ -2705,7 +2671,7 @@ const gardeEffective = computed(() => { function onGarde (techId, iso) { return !!gardeEffective.value[techId + '|' + iso] } // Nb de techs de garde par jour (garde EFFECTIVE) → ligne de pied cohérente avec la grille const gardeCountByDate = computed(() => { const m = {}; for (const k in gardeEffective.value) { const iso = k.split('|')[1]; m[iso] = (m[iso] || 0) + 1 } return m }) -function chip (color) { return { background: color || '#1976d2', color: '#fff' } } +// chip() (pastille couleur du type de shift) déplacé dans ShiftTypesDialog — seul consommateur. // techs visibles (recherche + groupe + tri) const groupOptions = computed(() => { const s = new Set(); for (const t of techs.value) if (t.group) s.add(t.group); return [...s].sort().map(g => ({ label: g, value: g })) }) @@ -5719,10 +5685,7 @@ async function saveCost (t) { try { await roster.setTechCost(t.id, { salary: t.s // éditeur de types de shift (intervalle d'heures) function calcHours (st, et) { if (!st || !et) return 0; const [h1, m1] = st.split(':').map(Number); const [h2, m2] = et.split(':').map(Number); let mins = (h2 * 60 + m2) - (h1 * 60 + m1); if (mins < 0) mins += 1440; return Math.round(mins / 60 * 100) / 100 } -function openShiftEditor () { editTpls.value = templates.value.map(t => ({ name: t.name, template_name: t.template_name, start: (t.start_time || '08:00:00').slice(0, 5), end: (t.end_time || '16:00:00').slice(0, 5), color: t.color || '#1976d2', on_call: t.on_call ? 1 : 0 })); showShiftEditor.value = true } -async function saveShiftTpl (t) { try { await roster.updateTemplate(t.name, { start_time: t.start + ':00', end_time: t.end + ':00', hours: calcHours(t.start, t.end), color: t.color, on_call: t.on_call ? 1 : 0 }); await refreshTemplates(); $q.notify({ type: 'positive', message: t.template_name + ' enregistré (' + calcHours(t.start, t.end) + ' h)' }) } catch (e) { err(e) } } -async function addShiftTpl () { const nm = (newTpl.template_name || '').trim() || (fmtH(hToNum(newTpl.start)) + 'h–' + fmtH(hToNum(newTpl.end)) + 'h'); try { await roster.createTemplate({ template_name: nm, start_time: newTpl.start + ':00', end_time: newTpl.end + ':00', hours: calcHours(newTpl.start, newTpl.end), color: newTpl.color, default_required: 1, on_call: newTpl.on_call ? 1 : 0 }); newTpl.template_name = ''; newTpl.on_call = 0; await refreshTemplates(); openShiftEditor(); $q.notify({ type: 'positive', message: 'Type « ' + nm + ' » ajouté' }) } catch (e) { err(e) } } -async function delShiftTpl (t) { if (!window.confirm('Supprimer le type « ' + t.template_name + ' » ?')) return; try { await roster.deleteShiftTemplate(t.name); await refreshTemplates(); editTpls.value = editTpls.value.filter(x => x.name !== t.name); $q.notify({ type: 'info', message: 'Type supprimé' }) } catch (e) { err(e) } } +// Types de shift → extrait dans components/planif/ShiftTypesDialog.vue (état + fonctions déplacés ; @changed → refreshTemplates). function snapshotServer (list) { serverSet.value = new Set(list.map(a => a.tech + '|' + a.date + '|' + a.shift)) } async function loadWeek () { loading.value = true