Reschedule: endpoint aviser-client (lien /book + SMS Twilio + statut 'À reporter') + file 'À reporter' + outil copilote

/roster/job/notify-reschedule (job,phone?) → token /book + SMS + booking_status='À reporter'.
/roster/jobs-to-reschedule → file superviseur. Copilote: outil notifier_client_report.
Testé OK (chemin sans téléphone = sûr). Brique P5 de #55, déclenchable depuis le flux de pause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-04 13:28:45 -04:00
parent 5d371a2a8b
commit 097e0566ec
2 changed files with 41 additions and 1 deletions

View File

@ -117,6 +117,14 @@ const TOOLS = [
parameters: { type: 'object', properties: { job: { type: 'string' }, technicien_id: { type: 'string' } }, required: ['job', 'technicien_id'] },
},
},
{
type: 'function',
function: {
name: 'notifier_client_report',
description: "ACTION : avise le client qu'un rendez-vous doit être reporté — envoie un SMS avec un lien pour qu'il choisisse un nouveau créneau, et passe le job en « À reporter ». Sur instruction explicite. Fournir job (et phone si connu).",
parameters: { type: 'object', properties: { job: { type: 'string' }, phone: { type: 'string' } }, required: ['job'] },
},
},
]
// ── Outils d'ACTION (écriture, sur instruction explicite) ───────────────────
async function tool_marquer_indispo ({ technicien_id, date, type }) {
@ -133,12 +141,22 @@ async function tool_reassigner ({ job, technicien_id }) {
return { ok: true, message: `${job} réassigné à ${technicien_id}` }
}
async function tool_notifier_client ({ job, phone }) {
// Réutilise l'endpoint hub (même process) pour le lien /book + SMS + statut.
const r = await fetch('http://localhost:3300/roster/job/notify-reschedule', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job, phone }),
}).then(x => x.json()).catch(e => ({ ok: false, error: String(e.message || e) }))
return r
}
async function execTool (name, args) {
try {
if (name === 'etat_equipe') return await tool_etat_equipe(args)
if (name === 'jobs_du_technicien') return await tool_jobs_du_technicien(args)
if (name === 'marquer_indisponibilite') return await tool_marquer_indispo(args)
if (name === 'reassigner_job') return await tool_reassigner(args)
if (name === 'notifier_client_report') return await tool_notifier_client(args)
return { error: 'outil inconnu' }
} catch (e) { return { error: String(e.message || e) } }
}
@ -170,7 +188,7 @@ RÈGLES :
- Quand on signale un tech absent/malade : appelle etat_equipe(date) pour repérer le tech (par son nom) et la dispo des autres, puis jobs_du_technicien(id, date) pour l'impact.
- Propose CONCRÈTEMENT : pour chaque job touché, suggère 12 techniciens candidats (Disponible, compétences compatibles, charge faible le même jour) OU un report. Mentionne les ids/noms réels.
- Si tu ne trouves pas le tech nommé, dis-le et liste les techniciens proches.
- Par défaut tu ANALYSES et PROPOSES. Mais tu peux EXÉCUTER sur instruction explicite : marquer_indisponibilite (rendre un tech absent un jour donné, ex « marque Simon malade aujourd'hui ») et reassigner_job (changer le tech d'un RDV, ex « réassigne DJ- à Jean-Pierre »). Confirme TOUJOURS l'action réalisée (id + date). N'invente jamais une action non effectuée. (SMS client de report + escalade superviseur = à venir.)
- Par défaut tu ANALYSES et PROPOSES. Mais tu peux EXÉCUTER sur instruction explicite : marquer_indisponibilite (rendre un tech absent un jour donné, ex « marque Simon malade aujourd'hui »), reassigner_job (changer le tech d'un RDV), et notifier_client_report (aviser le client par SMS qu'un RDV doit être reporté, avec lien pour choisir un nouveau créneau). Confirme TOUJOURS l'action réalisée. N'invente jamais une action non effectuée.
- Réponds en français, bref et actionnable (puces).`
}

View File

@ -483,6 +483,28 @@ async function handle (req, res, method, path, url) {
if (!token) { token = crypto.randomBytes(12).toString('hex'); const r = await retryWrite(() => erp.update('Dispatch Job', b.job, { booking_token: token })); if (!r.ok) return json(res, 500, r) }
return json(res, 200, { ok: true, token, url: (cfg.HUB_PUBLIC_URL || 'https://msg.gigafibre.ca') + '/book?token=' + token })
}
// Aviser le client d'un report : lien /book + SMS Twilio + statut « À reporter »
if (path === '/roster/job/notify-reschedule' && method === 'POST') {
const b = await parseBody(req); if (!b.job) return json(res, 400, { error: 'job requis' })
const job = await erp.get('Dispatch Job', b.job, { fields: ['name', 'booking_token', 'customer', 'customer_name'] })
if (!job) return json(res, 404, { error: 'job introuvable' })
let token = job.booking_token
if (!token) { token = crypto.randomBytes(12).toString('hex'); await retryWrite(() => erp.update('Dispatch Job', b.job, { booking_token: token })) }
const url = (cfg.HUB_PUBLIC_URL || 'https://msg.gigafibre.ca') + '/book?token=' + token
await retryWrite(() => erp.update('Dispatch Job', b.job, { booking_status: 'À reporter' }))
let phone = b.phone
if (!phone && job.customer) { try { const c = await erp.get('Customer', job.customer, { fields: ['mobile_no'] }); phone = c && c.mobile_no } catch (e) {} }
if (!phone) return json(res, 200, { ok: true, url, sms: false, note: 'Statut « À reporter » posé. Aucun téléphone trouvé — fournir "phone" pour envoyer le SMS.' })
const msg = b.message || `Bonjour, votre rendez-vous Gigafibre doit être reporté. Choisissez un nouveau créneau qui vous convient : ${url} Merci de votre compréhension.`
let sid = null
try { sid = await require('./twilio').sendSmsInternal(phone, msg, job.customer) } catch (e) { return json(res, 200, { ok: true, url, sms: false, error: String(e.message || e) }) }
return json(res, 200, { ok: true, url, sms: !!sid, sid, phone })
}
// File « À reporter » (jobs à recontacter) — pour le superviseur
if (path === '/roster/jobs-to-reschedule' && method === 'GET') {
const rows = await erp.list('Dispatch Job', { filters: [['booking_status', '=', 'À reporter']], fields: ['name', 'customer_name', 'service_location', 'service_type', 'scheduled_date', 'assigned_tech', 'booking_token'], limit: 100 })
return json(res, 200, { jobs: rows || [] })
}
// Fit : 3 dispos classées du client → 1er choix tenable, sinon proposer
if (path === '/roster/book/fit' && method === 'POST') {
const b = await parseBody(req)