feat(dispatch): tech-history resolue via identite canonique (dynamique)
Le rapport techHistory groupe desormais par IDENTITE (lib/identity resolveIdentity:
staff_id -> nom), calculee A LA LECTURE -> corriger une identite (onglet Settings
Identites) reclasse le ledger AUSSITOT (pas de valeur figee).
- is_tech = l identite a un tech_id : le staff admin (ex. Megane Liaud, sans tech_id)
n est PLUS compte comme technicien.
- Sortie: par assignee {label,email,is_tech,kind,total,from_pool} + liste unresolved
(noms sans identite, ex. prenom seul) a mapper dynamiquement.
- Detail ?tech= accepte cle identite | courriel | tech_id | staff_id | nom.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
947595f086
commit
60da7a01da
|
|
@ -2573,20 +2573,48 @@ async function techHistoryBackfill ({ batch = 5000, maxBatches = 40, throttleMs
|
|||
}
|
||||
return { ok: true, dryRun, scanned, matched, inserted, batches, watermark: wm, done: batches < maxBatches }
|
||||
}
|
||||
// Rapport : ?tech=TECH-<id> → lignes de ce tech ; sinon → récap par tech (compte + venant du pool + dernier).
|
||||
// Rapport — résolution DYNAMIQUE vers l'IDENTITÉ canonique (lib/identity : SOURCE UNIQUE, éditable via l'onglet
|
||||
// « Identités »). On ne stocke QUE les faits bruts (to_name/to_staff_id) ; le regroupement par personne est calculé
|
||||
// À LA LECTURE via resolveIdentity(staff_id → nom) → corriger une identité (Settings) reclasse le ledger AUSSITÔT.
|
||||
// is_tech = l'identité a un tech_id (⇒ Mégane, staff admin sans tech_id, n'est PAS comptée comme tech). Les noms
|
||||
// non résolus (ex. prénom seul « Mégane ») remontent dans `unresolved` pour être mappés dynamiquement.
|
||||
async function techHistory ({ tech = '', limit = 200 } = {}) {
|
||||
await techHistEnsureSchema()
|
||||
const pg = techHistPg(); const lim = Math.max(1, Math.min(2000, Number(limit) || 200))
|
||||
if (tech) {
|
||||
const r = await pg.query('SELECT legacy_ticket_id, changed_at, to_name, tech_id, from_name, from_pool, dept, changed_by_staff FROM ops_tech_ticket_history WHERE tech_id = $1 OR to_name = $1 ORDER BY changed_at DESC LIMIT $2', [tech, lim])
|
||||
const tot = await pg.query('SELECT COUNT(*)::int n, SUM(CASE WHEN from_pool THEN 1 ELSE 0 END)::int pool FROM ops_tech_ticket_history WHERE tech_id = $1 OR to_name = $1', [tech])
|
||||
return { ok: true, tech, total: tot.rows[0].n, from_pool: tot.rows[0].pool, rows: r.rows }
|
||||
const pg = techHistPg(); const idn = require('./identity'); const lim = Math.max(1, Math.min(5000, Number(limit) || 200))
|
||||
const resolve = (staffId, name) => (staffId != null && idn.resolveIdentity(String(staffId))) || idn.resolveIdentity(name || '') || null
|
||||
if (tech) { // tech = clé identité | courriel | tech_id | staff_id | nom
|
||||
const id = idn.resolveIdentity(tech)
|
||||
const params = []; const conds = []
|
||||
if (id && id.legacy_staff_id) { params.push(Number(id.legacy_staff_id)); conds.push('to_staff_id = $' + params.length) }
|
||||
const names = [...new Set([id && id.label, tech].filter(Boolean))]
|
||||
if (names.length) { params.push(names); conds.push('to_name = ANY($' + params.length + ')') }
|
||||
if (!conds.length) return { ok: true, query: tech, identity: null, total: 0, from_pool: 0, rows: [] }
|
||||
const where = '(' + conds.join(' OR ') + ')'
|
||||
params.push(lim)
|
||||
const r = await pg.query('SELECT legacy_ticket_id, changed_at, to_name, to_staff_id, from_name, from_pool, dept FROM ops_tech_ticket_history WHERE ' + where + ' ORDER BY changed_at DESC LIMIT $' + params.length, params)
|
||||
const tot = await pg.query('SELECT COUNT(*)::int n, SUM(CASE WHEN from_pool THEN 1 ELSE 0 END)::int pool FROM ops_tech_ticket_history WHERE ' + where, params.slice(0, params.length - 1))
|
||||
return { ok: true, query: tech, identity: id ? { key: id.key, label: id.label, email: id.primary_email, tech_id: id.tech_id || '', is_tech: !!id.tech_id, kind: id.kind, active: id.active !== false } : null, total: tot.rows[0].n, from_pool: tot.rows[0].pool, rows: r.rows }
|
||||
}
|
||||
// Récap : on agrège les faits bruts (borné : ~260 couples), puis on résout chacun vers l'identité.
|
||||
const g = await pg.query('SELECT to_staff_id, to_name, COUNT(*)::int total, SUM(CASE WHEN from_pool THEN 1 ELSE 0 END)::int from_pool, MAX(changed_at) last_at FROM ops_tech_ticket_history GROUP BY to_staff_id, to_name')
|
||||
const byKey = new Map()
|
||||
for (const row of g.rows) {
|
||||
const id = resolve(row.to_staff_id, row.to_name)
|
||||
const key = id ? id.key : ('name:' + row.to_name)
|
||||
let e = byKey.get(key)
|
||||
if (!e) { e = { key, label: id ? id.label : row.to_name, email: id ? id.primary_email : '', is_tech: id ? !!id.tech_id : false, kind: id ? id.kind : null, resolved: !!id, total: 0, from_pool: 0, last_at: null, raw_names: [] }; byKey.set(key, e) }
|
||||
e.total += row.total; e.from_pool += row.from_pool
|
||||
if (!e.last_at || row.last_at > e.last_at) e.last_at = row.last_at
|
||||
if (!id) e.raw_names.push({ to_name: row.to_name, to_staff_id: row.to_staff_id })
|
||||
}
|
||||
const all = [...byKey.values()].sort((a, b) => b.total - a.total)
|
||||
const grand = await pg.query('SELECT COUNT(*)::int n FROM ops_tech_ticket_history')
|
||||
return {
|
||||
ok: true, total_rows: grand.rows[0].n, assignees: all.length,
|
||||
techs: all.filter(x => x.is_tech).length, non_tech: all.filter(x => x.resolved && !x.is_tech).length,
|
||||
unresolved: all.filter(x => !x.resolved).map(x => ({ label: x.label, total: x.total, staff_id: (x.raw_names[0] || {}).to_staff_id || null })).slice(0, 50),
|
||||
per_assignee: all.slice(0, lim).map(({ raw_names, ...e }) => e),
|
||||
}
|
||||
const r = await pg.query(`SELECT COALESCE(tech_id, to_name) key, MAX(to_name) name, COUNT(*)::int total,
|
||||
SUM(CASE WHEN from_pool THEN 1 ELSE 0 END)::int from_pool, MAX(changed_at) last_at
|
||||
FROM ops_tech_ticket_history GROUP BY COALESCE(tech_id, to_name) ORDER BY total DESC LIMIT $1`, [lim])
|
||||
const grand = await pg.query('SELECT COUNT(*)::int n, COUNT(DISTINCT COALESCE(tech_id,to_name))::int techs FROM ops_tech_ticket_history')
|
||||
return { ok: true, total_rows: grand.rows[0].n, techs: grand.rows[0].techs, per_tech: r.rows }
|
||||
}
|
||||
|
||||
module.exports = { handle, sync, reimportAddresses, fillMissingCoords, fixGeocoding, purgeStaleOrphans, pushAssignments, returnToPool, postTicketLegacy, ticketThread, watchLegacy, techSyncReport, techSyncApply, mineDurations, legacyTechTickets, legacyWindowLoad, ingestAssigned, reconcileLegacyJobs, backfillServiceLocations, backfillIssueCustomers, backfillSourceIssue, backfillDependsOn, startSync, stopSync, fetchTargoTickets, staleTickets, staleNudge, publishPreview, publishJob, techHistoryBackfill, techHistory, coord, prio, startTime, jobType, inTerritory, geocodeRQA, persistGeocodeToSL, reverseNearestFibre } // parseurs purs exposés pour les tests
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user