diff --git a/apps/ops/src/components/planif/PlanifCellMenu.vue b/apps/ops/src/components/planif/PlanifCellMenu.vue index 01283e1..a8e7828 100644 --- a/apps/ops/src/components/planif/PlanifCellMenu.vue +++ b/apps/ops/src/components/planif/PlanifCellMenu.vue @@ -34,7 +34,7 @@ - Bloquer / job générique…Occupe la journée (durée éditable) · retire du dispatch · ticket + Réserver / bloquer du temps…Bloc (hors dispatch · ticket) ou réserve souple (urgences OK) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 09ebaab..0e08c9d 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -743,7 +743,7 @@
Ouvre l'assistant de quarts (modèles 5×8h · 4×10h · 3×12h + heures/jour + N semaines) → publie les quarts. Ce n'est PAS un bouton « enregistrer ». Applique 5×8h à la semaine affichée (à publier) - Réserver du temps pour une tâche/projet (bloc priorité moyenne) → dé-priorise ce tech au dispatch auto. Réparations/urgences passent quand même (créneau en mode urgence). + Réserver ou bloquer du temps (tâche/projet, formation, réunion…). Choix dans le dialogue : bloc dur (hors dispatch) ou réserve souple (urgences OK).
🏠 Domicile (départ de tournée si plus proche que le dépôt)
@@ -1359,42 +1359,26 @@ - - - - - -
Réserver du temps
- -
- -
{{ resvDlg.tech && resvDlg.tech.name }} — indisponible au dispatch auto pendant ce bloc
-
- - - -
- -
Les réparations & urgences peuvent quand même utiliser ce tech (recherche de créneau en mode urgence).
-
- - - - -
-
- - + - + - -
Bloquer / job générique
+ +
Réserver / bloquer du temps
-
{{ fillerDlg.tech && fillerDlg.tech.name }} — occupé toute la période ⇒ retiré du dispatch régulier
- +
{{ fillerDlg.tech && fillerDlg.tech.name }}
+ + +
+ + + +
+
@@ -1402,14 +1386,17 @@
- + +
+ +
+ +
- - - + @@ -2572,21 +2559,22 @@ const skillMenuTarget = ref(null) // élément cliqué = ancre du popover (près const skillMenuShown = ref(false) function openSkillEditor (t, ev) { skillDialog.value = t; skillMenuTarget.value = (ev && ev.currentTarget) || null; skillMenuShown.value = true; loadTraccarDevices() } // charge les appareils GPS pour la section « Appareil GPS » du volet // #5 — Réserver du temps d'un tech (bloc « Réservation », priorité moyenne). Occupe l'occupation → dé-priorise au dispatch auto + soustrait des créneaux. -const resvDlg = reactive({ open: false, tech: null, date: '', start: '08:00', dur: 2, reason: '', busy: false }) -function openReserve (t) { - if (!t) return - skillMenuShown.value = false - const iso = mobileSelIso.value || kbSelIso.value || start.value || new Date().toLocaleDateString('en-CA', { timeZone: 'America/Toronto' }) - Object.assign(resvDlg, { open: true, tech: { id: t.id, name: t.name }, date: iso, start: '08:00', dur: 2, reason: '', busy: false }) -} -// Job « bouche-trou » (filler) : depuis le menu de cellule (clic case vide / clic droit). Pré-remplit tech+date+durée. -// La durée « journée complète » = longueur du quart de la case s'il existe, sinon 8 h. Occupe l'occupation → hors dispatch. -const fillerDlg = reactive({ open: false, tech: null, date: '', start: '08:00', dur: 8, fullDay: true, shiftH: 8, priority: 'medium', job_type: 'Interne', subject: '', createTicket: true, busy: false }) +// Dialogue UNIQUE « Réserver / bloquer du temps » (fusion des ex- « Réserver du temps » + « Bloquer / job générique »). +// `mode` = SEUL vrai écart entre les deux : 'block' (dur, hors dispatch, ticket optionnel) · 'reserve' (souple, urgences passent). +const fillerDlg = reactive({ open: false, tech: null, date: '', start: '08:00', dur: 8, fullDay: true, shiftH: 8, mode: 'block', priority: 'medium', job_type: 'Interne', subject: '', createTicket: true, busy: false }) function shiftHoursOf (techId, iso) { // longueur cumulée des quarts de la case (0 si aucun) const occ = cellOcc(techId, iso) if (occ && occ.hasReg && occ.shiftE > occ.shiftS) return Math.round((occ.shiftE - occ.shiftS) * 10) / 10 return 0 } +// Ouverture depuis le volet réglages du tech (bouton « Réserver / bloquer… ») : jour = sélection courante, souple par défaut. +function openReserve (t) { + if (!t) return + skillMenuShown.value = false + const iso = selDay.value || mobileSelIso.value || kbSelIso.value || start.value || todayISO() + Object.assign(fillerDlg, { open: true, tech: { id: t.id, name: t.name }, date: iso, start: '08:00', dur: 2, fullDay: false, shiftH: 0, mode: 'reserve', priority: 'medium', job_type: 'Interne', subject: '', createTicket: false, busy: false }) +} +// Ouverture depuis le menu de cellule (clic case) : bloc dur par défaut, pré-rempli tech+date+durée (= longueur du quart, sinon 8 h). function openFillerFromMenu () { if (!menu.tech || !menu.day) return const sh = shiftHoursOf(menu.tech.id, menu.day.iso) @@ -2595,36 +2583,31 @@ function openFillerFromMenu () { Object.assign(fillerDlg, { open: true, tech: { id: menu.tech.id, name: menu.tech.name }, date: menu.day.iso, start: numToTime(startH), dur: sh || 8, fullDay: true, shiftH: sh || 8, - priority: 'medium', job_type: 'Interne', subject: '', createTicket: true, busy: false, + mode: 'block', priority: 'medium', job_type: 'Interne', subject: '', createTicket: true, busy: false, }) menu.show = false } function onFillerFullDay (v) { if (v) { fillerDlg.dur = fillerDlg.shiftH || 8 } } // journée complète → durée = quart -async function doFiller () { - if (!fillerDlg.tech || !fillerDlg.date || !fillerDlg.subject || fillerDlg.busy) return +// Soumission unifiée : réserve souple (/reserve) OU bloc dur + ticket (/filler-job) selon le mode. +async function doBlockOrReserve () { + if (!fillerDlg.tech || !fillerDlg.date || fillerDlg.busy) return + if (fillerDlg.mode === 'block' && !fillerDlg.subject) return fillerDlg.busy = true + const dur = fillerDlg.fullDay ? (fillerDlg.shiftH || 8) : (Number(fillerDlg.dur) || (fillerDlg.mode === 'reserve' ? 2 : 8)) try { - const dur = fillerDlg.fullDay ? (fillerDlg.shiftH || 8) : (Number(fillerDlg.dur) || 8) - const r = await roster.createFillerJob({ - tech: fillerDlg.tech.id, date: fillerDlg.date, start_time: fillerDlg.start, duration_h: dur, - priority: fillerDlg.priority, job_type: fillerDlg.job_type || 'Interne', subject: fillerDlg.subject, - create_ticket: fillerDlg.createTicket, - }) - if (r && r.ok) { - $q.notify({ type: 'positive', icon: 'block', message: fillerDlg.tech.name + ' — ' + dur + 'h bloquées' + (r.issue ? ' · ticket ' + r.issue : '') }) - fillerDlg.open = false - try { await reloadOccupancy() } catch (e) {} - } else $q.notify({ type: 'negative', message: (r && r.error) || 'Création impossible' }) - } catch (e) { $q.notify({ type: 'negative', message: 'Création impossible : ' + (e.message || e) }) } finally { fillerDlg.busy = false } -} -async function doReserve () { - if (!resvDlg.tech || !resvDlg.date || resvDlg.busy) return - resvDlg.busy = true - try { - const r = await roster.reserveTech({ tech: resvDlg.tech.id, date: resvDlg.date, start_time: resvDlg.start, duration_h: resvDlg.dur, reason: resvDlg.reason }) - if (r && r.ok) { $q.notify({ type: 'positive', icon: 'event_busy', message: 'Temps réservé — ' + resvDlg.tech.name + ' · ' + resvDlg.dur + 'h' }); resvDlg.open = false; try { await reloadOccupancy() } catch (e) {} } - else $q.notify({ type: 'negative', message: (r && r.error) || 'Réservation impossible' }) - } catch (e) { $q.notify({ type: 'negative', message: 'Réservation impossible' }) } finally { resvDlg.busy = false } + if (fillerDlg.mode === 'reserve') { + const r = await roster.reserveTech({ tech: fillerDlg.tech.id, date: fillerDlg.date, start_time: fillerDlg.start, duration_h: dur, reason: fillerDlg.subject }) + if (r && r.ok) { $q.notify({ type: 'positive', icon: 'event_busy', message: 'Temps réservé — ' + fillerDlg.tech.name + ' · ' + dur + 'h (urgences OK)' }); fillerDlg.open = false; try { await reloadOccupancy() } catch (e) {} } + else $q.notify({ type: 'negative', message: (r && r.error) || 'Réservation impossible' }) + } else { + const r = await roster.createFillerJob({ + tech: fillerDlg.tech.id, date: fillerDlg.date, start_time: fillerDlg.start, duration_h: dur, + priority: fillerDlg.priority, job_type: fillerDlg.job_type || 'Interne', subject: fillerDlg.subject, create_ticket: fillerDlg.createTicket, + }) + if (r && r.ok) { $q.notify({ type: 'positive', icon: 'block', message: fillerDlg.tech.name + ' — ' + dur + 'h bloquées (hors dispatch)' + (r.issue ? ' · ticket ' + r.issue : '') }); fillerDlg.open = false; try { await reloadOccupancy() } catch (e) {} } + else $q.notify({ type: 'negative', message: (r && r.error) || 'Création impossible' }) + } + } catch (e) { $q.notify({ type: 'negative', message: 'Action impossible : ' + (e.message || e) }) } finally { fillerDlg.busy = false } } // « Voir sa tournée » depuis le volet réglages du tech : bascule en vue Tournées + isole ce tech sur la carte. function viewTechTournee (tech) {