From 43c67e3a18c082e529997c99b44c3b458d8b7feb Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 4 Jun 2026 14:28:14 -0400 Subject: [PATCH] =?UTF-8?q?Ops=20RDV+Copilote:=20vue=20agent=20(semaine/jo?= =?UTF-8?q?ur=20+=20hold),=20file=20=C3=80=20recontacter,=20r=C3=A9glages?= =?UTF-8?q?=20#56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RendezVousPage: - Vue segmentée À planifier / À recontacter / Tous. - Créneaux proposés groupés Semaine → Jour (se situer dans le temps, comme /book). - Hold à la sélection: bookHold(date,start,10min) → bloque les autres; libéré à la confirmation ou au changement de job (onBeforeUnmount). - File À recontacter (jobs À reporter) + actions: Lien client (copie URL self-serve), Aviser par SMS (notify-reschedule: désassigne + SMS lien /book). CopilotePage: carte réglages des créneaux offerts (#56) — lead_hours, plage horaire, horizon, max/jour, hold, jours offerts (chips) → savePolicy({booking}). api/roster.js: bookHold, bookLink, jobsToReschedule, notifyReschedule. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/api/roster.js | 8 ++ apps/ops/src/pages/CopilotePage.vue | 51 +++++++- apps/ops/src/pages/RendezVousPage.vue | 169 ++++++++++++++++++++------ 3 files changed, 187 insertions(+), 41 deletions(-) diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index 28a7391..6a0cd4e 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -67,3 +67,11 @@ export const bookJobs = () => jget('/roster/book/jobs') export const bookSlots = (p) => jget('/roster/book/slots?' + new URLSearchParams(p).toString()) export const bookFit = (body) => jpost('/roster/book/fit', body) export const bookConfirm = (body) => jpost('/roster/book/confirm', body) +// Hold temporaire d'une fenêtre (agent qui sélectionne) — { date, start, minutes } ou { date, start, release:true } +export const bookHold = (body) => jpost('/roster/book/hold', body) +// Lien client (token) pour un job → { token, url } +export const bookLink = (job) => jpost('/roster/book/link', { job }) +// File « À recontacter » (jobs À reporter) +export const jobsToReschedule = () => jget('/roster/jobs-to-reschedule') +// Aviser le client d'un report : désassigne + SMS lien /book — { job, phone?, message? } +export const notifyReschedule = (body) => jpost('/roster/job/notify-reschedule', body) diff --git a/apps/ops/src/pages/CopilotePage.vue b/apps/ops/src/pages/CopilotePage.vue index 63a9fd6..4e688e8 100644 --- a/apps/ops/src/pages/CopilotePage.vue +++ b/apps/ops/src/pages/CopilotePage.vue @@ -23,6 +23,33 @@
✓ Politique enregistrée
+ + +
Créneaux offerts à la prise de RDV
+
+ Contrôle ce qui est proposé sur la page client et dans la vue agent. Les créneaux restent + dérivés des shifts du roster — ces réglages les bornent (délai, heures, jours, plafond). +
+
+
+ +
+
+
+
Jours offerts
+ {{ d.l }} +
+
+ + +
+
✓ Réglages enregistrés
+
+
@@ -66,24 +93,46 @@ const loadingPolicy = ref(false) const savingPolicy = ref(false) const policySaved = ref(false) +// #56 — réglages des créneaux offerts +const booking = reactive({ lead_hours: 24, day_start: 8, day_end: 18, days_offered: [1, 2, 3, 4, 5], horizon_days: 21, max_per_day: 0, hold_minutes: 10 }) +const bookingFields = ref([]) +const weekdays = ref([]) +const savingBooking = ref(false) +const bookingSaved = ref(false) + onMounted(async () => { loadingPolicy.value = true try { const d = await getPolicy() Object.assign(policy, d.policy || {}) + if (d.policy?.booking) Object.assign(booking, d.policy.booking) opts.reschedule = d.options?.reschedule || [] opts.escalation = d.options?.escalation || [] + bookingFields.value = d.options?.booking_fields || [] + weekdays.value = d.options?.weekdays || [] } catch (e) { /* defaults */ } loadingPolicy.value = false }) async function doSavePolicy () { savingPolicy.value = true; policySaved.value = false - try { await savePolicy({ ...policy }); policySaved.value = true; setTimeout(() => { policySaved.value = false }, 2500) } + try { await savePolicy({ reschedule: policy.reschedule, sms_enabled: policy.sms_enabled, sms_quiet_hours: policy.sms_quiet_hours, escalation: policy.escalation }); policySaved.value = true; setTimeout(() => { policySaved.value = false }, 2500) } catch (e) { /* ignore */ } savingPolicy.value = false } +function toggleDay (v) { + if (!Array.isArray(booking.days_offered)) booking.days_offered = [] + const i = booking.days_offered.indexOf(v) + if (i >= 0) booking.days_offered.splice(i, 1); else booking.days_offered.push(v) +} +async function doSaveBooking () { + savingBooking.value = true; bookingSaved.value = false + try { await savePolicy({ booking: { ...booking, days_offered: [...(booking.days_offered || [])] } }); bookingSaved.value = true; setTimeout(() => { bookingSaved.value = false }, 2500) } + catch (e) { /* ignore */ } + savingBooking.value = false +} + async function scrollDown () { await nextTick(); if (scrollEl.value) scrollEl.value.scrollTop = scrollEl.value.scrollHeight } async function send () { diff --git a/apps/ops/src/pages/RendezVousPage.vue b/apps/ops/src/pages/RendezVousPage.vue index 0cb1fee..c649b75 100644 --- a/apps/ops/src/pages/RendezVousPage.vue +++ b/apps/ops/src/pages/RendezVousPage.vue @@ -3,7 +3,11 @@
Rendez-vous clients
- +
@@ -11,14 +15,17 @@
- Jobs ({{ filteredJobs.length }}) + + {{ view === 'reschedule' ? 'À recontacter' : 'Jobs' }} ({{ filteredJobs.length }}) + - {{ j.name }} + {{ j.customer_name || j.name }} {{ j.service_location || '—' }} · {{ j.duration_h || 1 }}h - + + Aucun job. @@ -29,9 +36,20 @@
Sélectionne un job à gauche pour prendre rendez-vous. - -
{{ sel.name }}
-
{{ sel.service_location || '—' }} · durée {{ params.duration }}h · tech actuel: {{ sel.assigned_tech || '—' }}
+ +
+
{{ sel.customer_name || sel.name }}
+
{{ sel.service_location || '—' }} · durée {{ params.duration }}h · tech actuel : {{ sel.assigned_tech || '—' }}
+
+ +
+ + + + + ⚠️ Client à recontacter. Propose un créneau ci-dessous (réassignation directe), ou laisse le client choisir : + + @@ -43,13 +61,35 @@ - + + + + +
+
{{ slots.length }} créneaux — clique pour réserver (bloqué {{ holdMinutes }} min pour les autres) :
+
+
{{ wk.label }}
+
+
{{ dayLabel(day.date) }}
+
+
+
{{ s.start }}–{{ s.end }}
+
{{ s.tech_name }}
+
+
+
+
+ +
+
Aucun créneau — élargis la période, la zone ou la compétence (le roster doit être publié, et la politique de créneaux peut filtrer).
+
+ - +
Saisis les 3 disponibilités du client, en ordre de préférence. On place dans le 1er tenable.
{{ i + 1 }} @@ -60,7 +100,7 @@
- ✅ Choix #{{ fit.chosen.rank }} retenu : {{ frDate(fit.chosen.date) }} {{ fit.chosen.start }}–{{ fit.chosen.end }} · {{ fit.chosen.tech_name }} + ✅ Choix #{{ fit.chosen.rank }} retenu : {{ dayLabel(fit.chosen.date) }} {{ fit.chosen.start }}–{{ fit.chosen.end }} · {{ fit.chosen.tech_name }} @@ -73,23 +113,6 @@
- - - - -
-
{{ slots.length }} créneaux — clique pour sélectionner :
-
-
-
{{ frDate(s.date) }}
-
{{ s.start }}–{{ s.end }}
-
{{ s.tech_name }}
-
-
- -
-
Aucun créneau — élargis la période, la zone ou la compétence (le roster doit être publié).
-
@@ -97,30 +120,63 @@