From f4a6d07a8b67817d0ccd21dee56b871126225526 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Wed, 8 Jul 2026 18:33:30 -0400 Subject: [PATCH] =?UTF-8?q?feat(ops):=20reason=20=E2=86=92=20skill=20?= =?UTF-8?q?=E2=86=92=20skill-aware=20availability=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge reason → required skill → availability so the occupancy denominator reflects the skill a job needs ("70 h installation" vs "150 h all techs"). Hub: - lib/skill-resolver.js (NEW, SOURCE UNIQUE): shared techHasSkill(s) predicate, deptToSkill (moved here, re-exported from roster.js — no cycle), DEPARTMENT_SKILLS bridge, resolveSkills({text,department,jobType,useAI}) (chip → rule → Gemini fallback, reusing classifyEmail pattern). - roster.js: capacityByDay(start,days,skills) filters denominator to qualified techs (+ guards used/due). New POST /roster/resolve-skills, /roster/capacity?skill=. Backward-compatible (no skill = prior behavior). - dispatch.js: techOccupancy/suggestSlots reuse the shared predicate. SPA: - config/departments.js: skills[]+dur per department + skillsForDepartment(); added Réparation reason. - components/shared/OccupancyBands.vue (NEW): vertical-strip grid extracted from SuggestSlotsDialog (now reuses it). - components/shared/AvailabilityByReason.vue (NEW): reason chips + dictate→AI → resolved skill → skill-filtered bands + adjusted-denominator summary. Mounted in ClientDetailPage, IssueDetail, ConversationPanel. - api/roster.js: getCapacity(start,days,skill) + resolveSkills(). - Planif + Dashboard capacity bands pass skill to /roster/capacity. Co-Authored-By: Claude Opus 4.8 --- apps/ops/src/api/roster.js | 10 +- .../shared/AvailabilityByReason.vue | 176 ++++++++++++++++++ .../components/shared/ConversationPanel.vue | 12 +- .../src/components/shared/OccupancyBands.vue | 117 ++++++++++++ .../shared/detail-sections/IssueDetail.vue | 21 ++- apps/ops/src/config/departments.js | 31 ++- .../components/SuggestSlotsDialog.vue | 80 +------- apps/ops/src/pages/ClientDetailPage.vue | 30 ++- apps/ops/src/pages/DashboardPage.vue | 15 +- apps/ops/src/pages/PlanificationPage.vue | 21 ++- services/targo-hub/lib/dispatch.js | 7 +- services/targo-hub/lib/roster.js | 43 +++-- services/targo-hub/lib/skill-resolver.js | 140 ++++++++++++++ 13 files changed, 581 insertions(+), 122 deletions(-) create mode 100644 apps/ops/src/components/shared/AvailabilityByReason.vue create mode 100644 apps/ops/src/components/shared/OccupancyBands.vue create mode 100644 services/targo-hub/lib/skill-resolver.js diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index 3117bc7..4cc3dc3 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -34,8 +34,14 @@ export const listRequirements = (start, days = 7) => jget(`/roster/requirements? export const createRequirement = (r) => jpost('/roster/requirements', r) export const listAssignments = (start, days = 7) => jget(`/roster/assignments?start=${start}&days=${days}`) export const getCoverage = (start, days = 7) => jget(`/roster/coverage?start=${start}&days=${days}`) -// Dispo restante par jour × segment (AM/PM/Soir) : capacité (quarts) − jobs placés, + charge due -export const getCapacity = (start, days = 7) => jget(`/roster/capacity?start=${start}&days=${days}`) +// Dispo restante par jour × segment (AM/PM/Soir) : capacité (quarts) − jobs placés, + charge due. +// skill (chaîne ou tableau) → dénominateur FILTRÉ : ne compte que les techs qui ont cette/ces compétence(s). +export const getCapacity = (start, days = 7, skill = '') => { + const s = (Array.isArray(skill) ? skill : (skill ? [skill] : [])).filter(Boolean) + return jget(`/roster/capacity?start=${start}&days=${days}${s.length ? '&skill=' + encodeURIComponent(s.join(',')) : ''}`) +} +// Résolveur « raison → compétence(s) » : { text?, department?, jobType?, useAI? } → { skills, primary, confidence, source, reason }. +export const resolveSkills = (body) => jpost('/roster/resolve-skills', body || {}) export const getStats = (start, days = 7) => jget(`/roster/stats?start=${start}&days=${days}`) export const getOccupancy = (start, days = 7) => jget(`/roster/occupancy?start=${start}&days=${days}`) export const getAbsences = (start, days = 7) => jget(`/roster/absences?start=${start}&days=${days}`) diff --git a/apps/ops/src/components/shared/AvailabilityByReason.vue b/apps/ops/src/components/shared/AvailabilityByReason.vue new file mode 100644 index 0000000..72023a1 --- /dev/null +++ b/apps/ops/src/components/shared/AvailabilityByReason.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/apps/ops/src/components/shared/ConversationPanel.vue b/apps/ops/src/components/shared/ConversationPanel.vue index 51a805f..aa5256d 100644 --- a/apps/ops/src/components/shared/ConversationPanel.vue +++ b/apps/ops/src/components/shared/ConversationPanel.vue @@ -96,7 +96,7 @@ {{ ticketTitle(t) }} {{ ticketCategory(t) }} - {{ t.customer || '—' }} + {{ t.customer }} @@ -145,6 +145,9 @@ Ajouter ce fil à une tâche (existante ou nouvelle) + + Dispo par motif : compétence probable de cette demande → disponibilité ajustée + Re-synchroniser depuis Gmail — récupère les réponses envoyées hors hub + les courriels manqués (panne / sortis de la boîte) @@ -1010,6 +1013,11 @@ + + + @@ -1030,6 +1038,7 @@ import { useAuthStore } from 'src/stores/auth' import { useRouter } from 'vue-router' import AddToTaskDialog from 'src/components/shared/AddToTaskDialog.vue' import EmailExpandDialog from 'src/components/shared/conversation/EmailExpandDialog.vue' +import AvailabilityByReason from 'src/components/shared/AvailabilityByReason.vue' // raison → compétence → disponibilité (dénominateur filtré) import { emailSrcdoc } from 'src/composables/useEmailRender' const $q = useQuasar() @@ -1048,6 +1057,7 @@ const { } = useConversations() // ── Identité d'expédition « De : » — défaut = groupe (anonymisé), modifiable par message ── +const availReasonOpen = ref(false) // « Dispo par motif » depuis la conversation const senders = ref({ default: '', identities: [] }) const replySendAs = ref('') const composeSendAs = ref('') diff --git a/apps/ops/src/components/shared/OccupancyBands.vue b/apps/ops/src/components/shared/OccupancyBands.vue new file mode 100644 index 0000000..f8701ac --- /dev/null +++ b/apps/ops/src/components/shared/OccupancyBands.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/apps/ops/src/components/shared/detail-sections/IssueDetail.vue b/apps/ops/src/components/shared/detail-sections/IssueDetail.vue index 9c1a728..0a58a8f 100644 --- a/apps/ops/src/components/shared/detail-sections/IssueDetail.vue +++ b/apps/ops/src/components/shared/detail-sections/IssueDetail.vue @@ -12,7 +12,7 @@