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
guard(loadWeek)">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()