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:
parent
5d371a2a8b
commit
097e0566ec
|
|
@ -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 1–2 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).`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user