diff --git a/apps/ops/src/api/dispatch.js b/apps/ops/src/api/dispatch.js index bc64096..52ddfee 100644 --- a/apps/ops/src/api/dispatch.js +++ b/apps/ops/src/api/dispatch.js @@ -3,8 +3,20 @@ // Swap BASE_URL in config/erpnext.js to change the target server. // ───────────────────────────────────────────────────────────────────────────── import { BASE_URL } from 'src/config/erpnext' +import { HUB_URL } from 'src/config/hub' import { authFetch } from './auth' +// Créneaux disponibles (hub : gap-finding + tampon de route + classement) pour un nouveau job à une adresse. +// payload = { duration_h, latitude, longitude, after_date?, limit? } → [{ tech_id, tech_name, date, start_time, end_time, travel_min, distance_km, reasons[] }] +export async function suggestSlots (payload) { + const res = await fetch(`${HUB_URL}/dispatch/suggest-slots`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload || {}), + }) + if (!res.ok) throw new Error('suggest-slots ' + res.status) + const data = await res.json() + return data.slots || data || [] +} + async function apiGet (path) { const res = await authFetch(BASE_URL + path) const data = await res.json() diff --git a/apps/ops/src/modules/dispatch/components/SuggestSlotsDialog.vue b/apps/ops/src/modules/dispatch/components/SuggestSlotsDialog.vue new file mode 100644 index 0000000..0ebcc5d --- /dev/null +++ b/apps/ops/src/modules/dispatch/components/SuggestSlotsDialog.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/ops/src/pages/DispatchPage.vue b/apps/ops/src/pages/DispatchPage.vue index d2b38fa..07d5c2e 100644 --- a/apps/ops/src/pages/DispatchPage.vue +++ b/apps/ops/src/pages/DispatchPage.vue @@ -15,6 +15,7 @@ import TimelineRow from 'src/modules/dispatch/components/TimelineRow.vue' import BottomPanel from 'src/modules/dispatch/components/BottomPanel.vue' import JobEditModal from 'src/modules/dispatch/components/JobEditModal.vue' import UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue' +import SuggestSlotsDialog from 'src/modules/dispatch/components/SuggestSlotsDialog.vue' import PublishScheduleModal from 'src/modules/dispatch/components/PublishScheduleModal.vue' import WeekCalendar from 'src/modules/dispatch/components/WeekCalendar.vue' import MonthCalendar from 'src/modules/dispatch/components/MonthCalendar.vue' @@ -396,6 +397,8 @@ async function confirmMove () { const bookingOverlay = ref(null) const woModalOpen = ref(false) const woModalCtx = ref({}) +const showSuggestSlots = ref(false) +const bookingSlot = ref(null) // créneau choisi (porte start_time + coords) — injecté dans confirmWo const publishModalOpen = ref(false) // NLP bar is hidden by default; toggled from the ⋯ menu (Assistant IA). // Showing it eagerly bloats the header on narrow laptops and the @@ -823,23 +826,34 @@ async function saveTechGroup (tech, value) { } function openWoModal (prefillDate = null, prefillTech = null) { + bookingSlot.value = null // création manuelle → pas de créneau réservé woModalCtx.value = { scheduled_date: prefillDate || todayStr, assigned_tech: prefillTech || null } woModalOpen.value = true } +// P1-C : un créneau choisi dans « Trouver un créneau » → pré-remplit la création de job (tech + date + adresse), l'heure/coords injectées au submit. +function onSlotSelected (slot) { + bookingSlot.value = slot + woModalCtx.value = { scheduled_date: slot.date, assigned_tech: slot.tech_id, address: slot._address || '', duration_h: slot._duration || 1 } + woModalOpen.value = true +} async function confirmWo (formData) { - return await store.createJob({ + const slot = bookingSlot.value // réservation via « Trouver un créneau » → heure + coords géocodées de l'adresse + const r = await store.createJob({ subject: formData.subject, address: formData.address, duration_h: formData.duration_h, priority: formData.priority, assigned_tech: formData.assigned_tech || null, scheduled_date: formData.scheduled_date || null, - latitude: formData._latitude || null, - longitude: formData._longitude || null, + start_time: slot ? slot.start_time : null, // heure du créneau réservé (sinon non planifié à l'heure) + latitude: formData._latitude || (slot ? slot._latitude : null), + longitude: formData._longitude || (slot ? slot._longitude : null), note: formData.description || '', tags: (formData.tags || []).map(t => typeof t === 'string' ? { tag: t } : t), depends_on: formData.depends_on || '', }) + bookingSlot.value = null + return r } const { autoDistribute, optimizeRoute: _optimizeRoute } = useAutoDispatch({ @@ -1369,6 +1383,7 @@ onUnmounted(() => { + +