diff --git a/scripts/migration/camping_dispatch_backfill.sql b/scripts/migration/camping_dispatch_backfill.sql new file mode 100644 index 0000000..cf43e57 --- /dev/null +++ b/scripts/migration/camping_dispatch_backfill.sql @@ -0,0 +1,19 @@ +-- camping_dispatch_backfill.sql — applique la géoloc FIXE du camping aux Dispatch Jobs DÉJÀ dispatchés. +-- Le pont (legacy-dispatch-sync) ne re-traite que les tickets encore « ouverts + assign_to=3301 » ; les jobs +-- déjà assignés/fermés gardent leurs vieilles coords (résidence). Ce backfill corrige tous les jobs issus du +-- pont dont le SUJET désigne un camping (mots-clés de LIEU sûrs ; pour 'dauphinais', exige aussi « camping »). +-- Idempotent (ne touche que ceux dont la coord diffère). Match via camping_registry. +\timing on +BEGIN; +WITH applied AS ( + UPDATE "tabDispatch Job" dj SET latitude = c.latitude, longitude = c.longitude, modified = NOW() + FROM camping_registry c + WHERE dj.legacy_ticket_id <> '' AND c.active + AND lower(unaccent(coalesce(dj.subject, ''))) LIKE '%' || c.keyword || '%' + AND ( c.keyword IN ('lac des pins','lac de pins','sandysun','sandy sun','frontiere','ensoleill') + OR lower(unaccent(coalesce(dj.subject, ''))) LIKE '%camping%' ) + AND (dj.latitude IS NULL OR abs(coalesce(dj.latitude,0) - c.latitude) > 1e-4 OR abs(coalesce(dj.longitude,0) - c.longitude) > 1e-4) + RETURNING c.name AS camping +) +SELECT camping, count(*) AS jobs FROM applied GROUP BY camping ORDER BY 2 DESC; +COMMIT; diff --git a/services/targo-hub/lib/legacy-dispatch-sync.js b/services/targo-hub/lib/legacy-dispatch-sync.js index ffe7960..ffe7d2e 100644 --- a/services/targo-hub/lib/legacy-dispatch-sync.js +++ b/services/targo-hub/lib/legacy-dispatch-sync.js @@ -25,6 +25,28 @@ const erp = require('./erp') const cfg = require('./config') const { log, json, httpRequest } = require('./helpers') const { searchAddressesRpc } = require('./address-search') // recherche trigram RQA (RPC pg_trgm) — celle de l'autocomplete de dispo +const addrdb = require('./address-db') // pool PG local (camping_registry) + +// Campings : l'adresse de service est un terrain de camping (≠ résidence du client). On force la géoloc +// FIXE du camping (registre camping_registry). Détection robuste : le texte doit contenir « camping » OU +// un mot-clé de LIEU spécifique (évite les faux positifs de patronyme, ex. « Daniel Dauphinais »). +const CAMP_PLACE_KW = ['lac des pins', 'lac de pins', 'sandysun', 'sandy sun', 'frontiere', 'ensoleill'] +let _campings = null; let _campingsAt = 0 +async function getCampings () { + if (_campings && (Date.now() - _campingsAt) < 600000) return _campings // cache 10 min + try { const r = await addrdb.pool().query('SELECT keyword, name, latitude, longitude FROM camping_registry WHERE active'); _campings = r.rows; _campingsAt = Date.now() } catch (e) { _campings = _campings || [] } + return _campings +} +// fields en ORDRE DE PRIORITÉ (sujet d'abord = label de service explicite, puis ville/adresse de delivery). +// Le 1er champ qui contient un signal camping décide → évite qu'une ville de delivery (résidence) écrase le sujet. +function campingFor (campings, fields) { + for (const f of (Array.isArray(fields) ? fields : [fields])) { + const t = norm(f || '') + if (!(t.includes('camping') || CAMP_PLACE_KW.some(k => t.includes(k)))) continue + for (const c of campings) if (c.keyword && t.includes(c.keyword)) return c + } + return null +} let mysql try { mysql = require('mysql2/promise') } catch { /* dépendance optionnelle */ } @@ -222,7 +244,11 @@ async function buildJob (t) { const st = startTime(t.due_time); if (st) payload.start_time = st if (cust) payload.customer = cust.name let coordSrc = null - if (dc) { payload.latitude = dc.lat; payload.longitude = dc.lon; coordSrc = 'delivery' } // source fiable : point de service legacy + // CAMPING (priorité max) : l'adresse de service est un terrain de camping → géoloc FIXE du camping, + // pas la résidence du client. Signal = sujet/ville/adresse de service du ticket. + const camp = campingFor(await getCampings(), [t.subject, t.dv_city, t.dv_addr]) + if (camp) { payload.latitude = camp.latitude; payload.longitude = camp.longitude; coordSrc = 'camping' } + if (!coordSrc && dc) { payload.latitude = dc.lat; payload.longitude = dc.lon; coordSrc = 'delivery' } // point de service legacy if (sl) { payload.service_location = sl.name if (!coordSrc) { const sc = coord(sl.latitude, sl.longitude); if (sc) { payload.latitude = sc.lat; payload.longitude = sc.lon; coordSrc = 'service_location' } } // repli si pas de delivery @@ -281,9 +307,10 @@ async function syncImpl ({ dryRun = false } = {}) { // existantes (souvent issues du Service Location, moins précises). delivery écrase ; SL/RQA non. const hasCoord = (v) => v != null && v !== '' && Math.abs(parseFloat(v)) > 0.0001 const exHas = hasCoord(ex.latitude) && hasCoord(ex.longitude) - const isDeliveryUpgrade = b.matched.coord_src === 'delivery' && exHas && + // delivery (point exact) ET camping (géoloc fixe du camping vs résidence) ÉCRASENT des coords existantes différentes ; SL/RQA/Mapbox non. + const isUpgrade = (b.matched.coord_src === 'delivery' || b.matched.coord_src === 'camping') && exHas && (Math.abs(parseFloat(ex.latitude) - b.payload.latitude) > 1e-5 || Math.abs(parseFloat(ex.longitude) - b.payload.longitude) > 1e-5) - if (b.payload.latitude != null && (!exHas || isDeliveryUpgrade)) { patch.latitude = b.payload.latitude; patch.longitude = b.payload.longitude; coordsFilled++ } + if (b.payload.latitude != null && (!exHas || isUpgrade)) { patch.latitude = b.payload.latitude; patch.longitude = b.payload.longitude; coordsFilled++ } if (!ex.service_location && b.payload.service_location) patch.service_location = b.payload.service_location // backfill lien Service Location if (ex.status === 'open' && !ex.assigned_tech && b.payload.scheduled_date && b.payload.scheduled_date !== ex.scheduled_date) patch.scheduled_date = b.payload.scheduled_date if (!dryRun && Object.keys(patch).length) {