From df23f232b3234ddd3a59a673a9dd9c2473f60f43 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Fri, 3 Jul 2026 09:05:32 -0400 Subject: [PATCH] =?UTF-8?q?feat(planif):=20native=20OPS=20job=20creation?= =?UTF-8?q?=20(#17)=20=E2=80=94=20Dispatch=20deprecated,=20create=20from?= =?UTF-8?q?=20Planification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Job/installation creation now lives in Planification (Dispatch board is being retired). Reuses the shared UnifiedCreateModal (work-order mode: subject, job_type, address+RQA geocode, duration, skills/tags, tech, date) so it's the same proven form. - "Créer un job" button (desktop toolbar + mobile ⋮ menu) → modal → createJob → @created refreshes the pool ("À assigner") + occupancy. - "Créneau" button → SuggestSlotsDialog (P1-C, re-homed from Dispatch) → pick tech+date+time → prefills the create modal. - useUnifiedCreate: work-order flow now carries start_time + prefilled coords (from a booked slot) through resetForm → payload. Sync F transitoire: OPS-native jobs have NO legacy_ticket_id, so the F→ OPS sync and purgeStaleOrphans (which only touch legacy_ticket_id jobs) never clobber them — they coexist. No F push for now (billing stays via the sales path). Verified: modal opens in Planification with all 9 work-order fields; button in toolbar (gt-sm) + mobile menu. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/composables/useUnifiedCreate.js | 5 ++++ apps/ops/src/pages/PlanificationPage.vue | 24 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/apps/ops/src/composables/useUnifiedCreate.js b/apps/ops/src/composables/useUnifiedCreate.js index 07b0ff0..98385f3 100644 --- a/apps/ops/src/composables/useUnifiedCreate.js +++ b/apps/ops/src/composables/useUnifiedCreate.js @@ -41,6 +41,7 @@ export function useUnifiedCreate (mode, opts = {}) { address: '', assigned_tech: null, scheduled_date: '', + start_time: '', // heure du créneau (réservation « Trouver un créneau ») — optionnel depends_on: null, // parent dependency (ticket/task/job name) tags: [], // [{ tag, level?, required? }] or ['label', ...] _latitude: null, @@ -217,6 +218,9 @@ export function useUnifiedCreate (mode, opts = {}) { form.address = ctx.address || '' form.assigned_tech = ctx.assigned_tech || null form.scheduled_date = ctx.scheduled_date || '' + form.start_time = ctx.start_time || '' + form._latitude = ctx._latitude != null ? ctx._latitude : (ctx.latitude != null ? ctx.latitude : null) // coords pré-remplies (réservation par créneau) + form._longitude = ctx._longitude != null ? ctx._longitude : (ctx.longitude != null ? ctx.longitude : null) form.depends_on = ctx.depends_on || null form.tags = ctx.tags || [] form._latitude = ctx.latitude || null @@ -280,6 +284,7 @@ export function useUnifiedCreate (mode, opts = {}) { service_location: form.service_location || '', depends_on: form.depends_on || '', scheduled_date: form.scheduled_date || (form.assigned_tech ? nextWeekday() : ''), + start_time: form.start_time || '', // heure du créneau réservé (sinon vide = non planifié à l'heure) notes: form.description || '', assigned_tech: form.assigned_tech || '', latitude: form._latitude || '', diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index ff2aa72..0364e8f 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -19,6 +19,8 @@ Tableau Dispatch Notifier par SMS + Créer un job + Trouver un créneau Publier au legacy Rafraîchir @@ -52,6 +54,9 @@ {{ assignPanel.jobs.length }} Afficher / masquer les jobs à répartir (liste · carte · lasso · suggestion auto) + + Créer un job / une installation (apparaît dans « À assigner ») + Trouver le meilleur créneau (tech + date + heure) puis créer le job réservé @@ -1723,6 +1728,12 @@ + + + + + @@ -1764,6 +1775,8 @@ import TechSelect from 'src/components/shared/TechSelect.vue' import SkillSelect from 'src/components/shared/SkillSelect.vue' import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags partagé (Dispatch) : condensé, création à la volée, couleurs 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 +import UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue' // création de job NATIVE OPS (work-order) — Dispatch déprécié, tout ici +import SuggestSlotsDialog from 'src/modules/dispatch/components/SuggestSlotsDialog.vue' // « Trouver un créneau » → pré-remplit la création (tech+date+heure+adresse) const $q = useQuasar() const router = useRouter() @@ -2011,6 +2024,17 @@ async function fetchKbMatrix (techId) { let _kbMatT = null // le watch déclencheur est enregistré PLUS BAS (après la déclaration de occByTechDay) pour éviter la zone morte temporelle const kbColor = (j) => j.skill ? getTagColor(j.skill) : (j.required_skill ? getTagColor(j.required_skill) : (legacyDeptColor(j.legacy_dept) || '#90a4ae')) async function reloadPool () { try { assignPanel.jobs = (await roster.unassignedJobs()).jobs || [] } catch (e) { /* non bloquant */ } } +// ── Création de job NATIVE OPS (#17) — Dispatch déprécié → tout depuis Planification. Réutilise UnifiedCreateModal (work-order). ── +// Job OPS-natif = PAS de legacy_ticket_id → coexiste avec les jobs synchronisés de F (la sync F ne touche que les jobs à legacy_ticket_id). « Sync F transitoire » = coexistence, pas de push F pour l'instant. +const createJobOpen = ref(false) +const createJobCtx = ref({}) +const showSuggestSlots = ref(false) +function openCreateJob () { createJobCtx.value = { scheduled_date: '' }; createJobOpen.value = true } // pool (non planifié) par défaut +function onSlotSelected (slot) { // un créneau choisi → pré-remplit tech+date+heure+adresse+coords + createJobCtx.value = { assigned_tech: slot.tech_id, scheduled_date: slot.date, start_time: slot.start_time, address: slot._address || '', duration_h: slot._duration || 1, _latitude: slot._latitude, _longitude: slot._longitude } + createJobOpen.value = true +} +async function onJobCreated () { createJobOpen.value = false; await reloadPool(); await reloadOccupancy(); $q.notify({ type: 'positive', icon: 'add_task', message: 'Job créé — visible dans « À assigner »', timeout: 2600 }) } // Drop sur la colonne « À assigner » = renvoyer au pool (désassigner, ERPNext only). async function onKanbanUnassign (ev) { ev.preventDefault()