From f1204ed4597a4b99a42eed46900beec1e108408e Mon Sep 17 00:00:00 2001 From: louispaulb Date: Fri, 5 Jun 2026 15:50:17 -0400 Subject: [PATCH] roster(planif): assignation drag-drop + timeline ressource + occupation + nettoyage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Panneau « jobs à assigner » v2 : multi-sélection (cases), groupes parent-enfant surlignés, heuristique terrain/à distance (activation/netadmin pré-décochés), pré-total d'heures, aperçu d'occupation PROJETÉE au survol (barre fantôme + badge). Fix barre d'occupation figée après drop : /roster/assign-job pose désormais un start_time (premier-trou-libre dans le shift) + garantit duration_h, sinon le job compte dans les heures mais n'affiche aucun bloc. Nouvel endpoint /roster/backfill-start-times (idempotent) pour rattraper l'historique. Infobulle de cellule : nb de jobs + liste triée par priorité (occupancyByTechDay renvoie jobs[]). Timeline contextuelle par ressource (dialogue, 0 appel réseau). Lisibilité du drag : fantôme compact semi-transparent décalé sous le curseur (ne masque plus l'aperçu) + source estompée. Scoring de priorité : hook proximité (neutre — secteur géré manuellement), réservé à 20% du score quand la géoloc arrivera. Refactor hub : helper partagé firstFitStart (assign-job + backfill). Nettoyage : retrait code mort (onDeleteRosterTag, projUsedH), carte des sections en tête de PlanificationPage. Doc : docs/features/roster.md + index. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/api/roster.js | 18 +- apps/ops/src/components/shared/TagEditor.vue | 6 +- apps/ops/src/pages/CopilotePage.vue | 26 +- apps/ops/src/pages/PlanificationPage.vue | 1064 +++++++++++++++--- apps/ops/src/pages/RendezVousPage.vue | 27 +- docs/features/README.md | 1 + docs/features/roster.md | 86 ++ services/targo-hub/lib/roster-assistant.js | 4 +- services/targo-hub/lib/roster.js | 206 +++- 9 files changed, 1230 insertions(+), 208 deletions(-) create mode 100644 docs/features/roster.md diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index e694424..5939d6b 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -64,10 +64,12 @@ export const approveAvailability = (name, body) => jpost('/roster/availability/' export const pauseTechnician = (id, paused, reason) => jpost(`/roster/technician/${encodeURIComponent(id)}/pause`, { paused, reason }) export const setTechEfficiency = (id, efficiency) => jpost(`/roster/technician/${encodeURIComponent(id)}/efficiency`, { efficiency }) export const setTechCost = (id, body) => jpost(`/roster/technician/${encodeURIComponent(id)}/cost`, body) -export const setTechSkills = (id, skills) => jpost(`/roster/technician/${encodeURIComponent(id)}/skills`, { skills }) +export const setTechSkills = (id, skills, skillLevels, skillEff) => { const body = { skills }; if (skillLevels !== undefined) body.skill_levels = skillLevels; if (skillEff !== undefined) body.skill_eff = skillEff; return jpost(`/roster/technician/${encodeURIComponent(id)}/skills`, body) } // ── Prise de RDV ── export const bookJobs = () => jget('/roster/book/jobs') +// Méta de l'éditeur type→compétence : { service_types, skills, skill_by_type } +export const bookMeta = () => jget('/roster/book/meta') 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) @@ -77,5 +79,19 @@ export const bookHold = (body) => jpost('/roster/book/hold', body) export const bookLink = (job) => jpost('/roster/book/link', { job }) // File « À recontacter » (jobs À reporter) export const jobsToReschedule = () => jget('/roster/jobs-to-reschedule') +// Impact d'un retrait de compétence : jobs assignés exigeant cette compétence +export const skillImpact = (tech, skill) => jget('/roster/skill-impact?tech=' + encodeURIComponent(tech) + '&skill=' + encodeURIComponent(skill)) +// Impact d'une absence : jobs assignés au tech sur ces dates (CSV) +export const absenceImpact = (tech, dates) => jget('/roster/absence-impact?tech=' + encodeURIComponent(tech) + '&dates=' + encodeURIComponent((dates || []).join(','))) +// Redistribuer ces jobs : mode 'auto' (re-match) ou 'requeue' (À recontacter) +export const redistributeSkillJobs = (jobs, skill, mode) => jpost('/roster/skill-impact/redistribute', { jobs, skill, mode }) +// Candidats classés pour reprendre un job (techs qualifiés + libres au créneau) +export const jobCandidates = (job, exclude) => jget('/roster/job-candidates?job=' + encodeURIComponent(job) + '&exclude=' + encodeURIComponent(exclude || '')) +// Plan explicite : [{ job, tech } | { job, requeue:true }] +export const redistributePlan = (plan) => jpost('/roster/skill-impact/redistribute', { plan }) +// Jobs non assignés (+ groupe/dépendances) pour le panneau glisser-déposer +export const unassignedJobs = () => jget('/roster/unassigned-jobs') +// Assigner un job à un tech (date = case déposée) +export const assignJob = (job, tech, date) => jpost('/roster/assign-job', { job, tech, date }) // 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/components/shared/TagEditor.vue b/apps/ops/src/components/shared/TagEditor.vue index 10d9ee4..03e3116 100644 --- a/apps/ops/src/components/shared/TagEditor.vue +++ b/apps/ops/src/components/shared/TagEditor.vue @@ -10,7 +10,7 @@ * - Create new tags on-the-fly with color picker * - Reusable everywhere: jobs, techs, sidebar filter */ -import { ref, computed, nextTick, watch } from 'vue' +import { ref, computed, nextTick, watch, onMounted } from 'vue' import { useQuasar } from 'quasar' const $q = useQuasar() @@ -28,6 +28,7 @@ const props = defineProps({ levelHint: { type: String, default: '1 = base · 5 = expert' }, showRequired:{ type: Boolean, default: false }, // show required pin per tag (jobs) compact: { type: Boolean, default: false }, // smaller chips + autofocus: { type: Boolean, default: false }, // focus l'input au montage (ouvre la liste direct) }) const emit = defineEmits(['update:modelValue', 'create', 'update-tag', 'rename-tag', 'delete-tag']) @@ -179,6 +180,9 @@ function onBlur () { function onEditBlur () { setTimeout(() => closeEdit(), 250) } + +// Autofocus optionnel : ouvre la liste d'autocomplétion dès l'apparition (ergonomie popover près du clic). +onMounted(() => { if (props.autofocus) nextTick(() => { inputEl.value?.focus(); focused.value = true }) })