/** * Roster (Planification) API — appelle targo-hub /roster/*. * Le backend lit les techs/modèles/besoins dans ERPNext facturation et appelle * le solveur OR-Tools. Voir services/targo-hub/lib/roster.js. */ import { HUB_URL as HUB } from 'src/config/hub' async function jget (path) { const r = await fetch(HUB + path) if (!r.ok) throw new Error('Roster API ' + r.status) return r.json() } async function jpost (path, body) { const r = await fetch(HUB + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}), }) if (!r.ok) throw new Error('Roster API ' + r.status) return r.json() } async function jput (path, body) { const r = await fetch(HUB + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) }) if (!r.ok) throw new Error('Roster API ' + r.status) return r.json() } export const listTechnicians = () => jget('/roster/technicians') export const listTemplates = () => jget('/roster/templates') export const createTemplate = (t) => jpost('/roster/templates', t) export const listRequirements = (start, days = 7) => jget(`/roster/requirements?start=${start}&days=${days}`) 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}`) 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}`) export const applyGardeHorizon = (start, weeks, assignments, shifts) => jpost('/roster/garde/apply', { start, weeks, assignments, shifts }) export const setAbsence = (tech, date, type, remove) => jpost('/roster/absence/set', { tech, date, type, remove }) export const generate = (start, days = 7, weights) => jpost('/roster/generate', { start, days, weights }) export const publish = (assignments) => jpost('/roster/publish', { assignments }) export const publishWeek = (start, days, assignments, notify) => jpost('/roster/publish-week', { start, days, assignments, notify }) export const updateTemplate = (name, patch) => jput('/roster/template/' + encodeURIComponent(name), patch) // ── Copilote (Gemini Flash) + politique de reprise ── export const askAssistant = (message, history) => jpost('/roster/assistant', { message, history }) export const getPolicy = () => jget('/roster/policy') export const savePolicy = (policy) => jpost('/roster/policy', policy) export async function deleteShiftTemplate (name) { const r = await fetch(HUB + '/roster/template/' + encodeURIComponent(name), { method: 'DELETE' }) if (!r.ok) throw new Error('Suppression modèle: ' + r.status) return r.json() } export const bulkRequirements = (requirements) => jpost('/roster/requirements/bulk', { requirements }) export const clearRequirements = (start, days = 7) => jpost('/roster/requirements/clear', { start, days }) export async function deleteAssignment (name) { const r = await fetch(HUB + '/roster/assignment/' + encodeURIComponent(name), { method: 'DELETE' }) if (!r.ok) throw new Error('Suppression échouée: ' + r.status) return r.json() } export const listAvailability = (status) => jget('/roster/availability' + (status ? '?status=' + encodeURIComponent(status) : '')) export const requestAvailability = (a) => jpost('/roster/availability', a) export const approveAvailability = (name, body) => jpost('/roster/availability/' + encodeURIComponent(name) + '/approve', body || {}) 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, 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) // 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') // 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') // Write-back legacy : aperçu (0 écriture) puis application (réassigne ticket.assign_to au tech dans osTicket) export const pushLegacyPreview = () => jget('/dispatch/legacy-sync/push-assignments') export const pushLegacyApply = () => jpost('/dispatch/legacy-sync/push-assignments', {}) // Fil complet d'un ticket legacy (osTicket) : messages + réponses, pour l'expand au clic export const ticketThread = (id) => jget('/dispatch/legacy-sync/ticket-thread?id=' + encodeURIComponent(id)) // Fermer un ticket dans le legacy (status=closed + date_closed + closed_by=acteur + réouverture enfants) export const closeLegacyTicket = (ticket) => jpost('/dispatch/legacy-sync/close-ticket?ticket=' + encodeURIComponent(ticket), {}) // Fermeture EN LOT (1 appel pour N tickets) — efficace export const batchCloseLegacy = (ids) => jpost('/dispatch/legacy-sync/batch-close?tickets=' + encodeURIComponent(ids.join(',')), {}) // Assigner un job à un tech (date = case déposée) export const assignJob = (job, tech, date) => jpost('/roster/assign-job', { job, tech, date }) // Fil complet d'un ticket legacy (description + commentaires/réponses des collaborateurs) — read-only export const legacyTicketThread = (id) => jget('/dispatch/legacy-sync/ticket-thread?id=' + encodeURIComponent(id)) // Réordonner / re-prioriser les jobs d'un tech×jour : updates = [{ job, route_order, priority? }] export const reorderJobs = (updates) => jpost('/roster/reorder-jobs', { updates }) // Retirer un job d'un tech (retour au pool non assigné) export const unassignJobRoster = (job) => jpost('/roster/unassign-job', { job }) // Aviser le client d'un report : désassigne + SMS lien /book — { job, phone?, message? } export const notifyReschedule = (body) => jpost('/roster/job/notify-reschedule', body)