@@ -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) {