Conformité : repli « centre du code postal / ville » pour les unmatched restants (statut 'area')
Dernier recours quand l'adresse exacte est introuvable : placer le Service Location au CENTROÏDE (rqa_addresses) de son code postal (préféré) sinon de sa ville → le job apparaît dans le bon secteur. - Hub : applyAreaFallback() (CTE centroïdes CP/ville, index-friendly) + POST /address/conformity/apply-area. Statut 'area', linked_address '≈ centre <CP/ville>'. Hors-QC/junk (absents de rqa_addresses) restent unmatched. - Ops : carte stat « ≈ Secteur (CP/ville) » + bouton « ≈ Centre CP/ville (reste) » dans la page Conformité. Exécuté : 317 placés (303 par code postal, 14 par ville) → unmatched 365 → 48 (Toronto/boîtes postales/junk). État final : validated 16 257 · review 489 · area 317 · unmatched 48 → 99,7 % des services ont une position. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ec6a317933
commit
2b9a863d39
|
|
@ -25,3 +25,5 @@ export const conformityResolve = (body) => jpost('/address/conformity/resolve',
|
||||||
export const campingsList = () => jget('/address/conformity/campings')
|
export const campingsList = () => jget('/address/conformity/campings')
|
||||||
export const campingsUpsert = (body) => jpost('/address/conformity/campings', body) // {keyword,name,address,latitude,longitude}
|
export const campingsUpsert = (body) => jpost('/address/conformity/campings', body) // {keyword,name,address,latitude,longitude}
|
||||||
export const campingsApply = () => jpost('/address/conformity/campings/apply', {})
|
export const campingsApply = () => jpost('/address/conformity/campings/apply', {})
|
||||||
|
// Dernier repli : placer les unmatched restants au centre du code postal (sinon ville) → statut 'area'
|
||||||
|
export const conformityApplyArea = () => jpost('/address/conformity/apply-area', {})
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="row items-center q-mb-sm">
|
<div class="row items-center q-mb-sm">
|
||||||
<div class="text-h6 text-weight-bold">Conformité des adresses</div>
|
<div class="text-h6 text-weight-bold">Conformité des adresses</div>
|
||||||
<q-space />
|
<q-space />
|
||||||
|
<q-btn flat dense no-caps size="sm" icon="my_location" label="≈ Centre CP/ville (reste)" color="blue-grey" :loading="areaSaving" @click="applyArea"><q-tooltip>Placer les adresses non matchées restantes au centre de leur code postal (sinon ville) — dernier repli</q-tooltip></q-btn>
|
||||||
<q-btn flat dense round icon="refresh" @click="reload" :loading="loading"><q-tooltip>Rafraîchir</q-tooltip></q-btn>
|
<q-btn flat dense round icon="refresh" @click="reload" :loading="loading"><q-tooltip>Rafraîchir</q-tooltip></q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-grey-7 q-mb-md">
|
<div class="text-caption text-grey-7 q-mb-md">
|
||||||
|
|
@ -171,8 +172,9 @@ const typeN = (k) => { const x = stats.by_type.find(s => s.t === k); return x ?
|
||||||
const statusCards = computed(() => [
|
const statusCards = computed(() => [
|
||||||
{ key: 'validated', label: 'Validées (conformes)', n: statN('validated'), cls: 'text-positive' },
|
{ key: 'validated', label: 'Validées (conformes)', n: statN('validated'), cls: 'text-positive' },
|
||||||
{ key: 'review', label: 'À confirmer', n: statN('review'), cls: 'text-amber-8' },
|
{ key: 'review', label: 'À confirmer', n: statN('review'), cls: 'text-amber-8' },
|
||||||
|
{ key: 'area', label: '≈ Secteur (CP/ville)', n: statN('area'), cls: 'text-blue-grey-7' },
|
||||||
{ key: 'unmatched', label: 'Non matchées', n: statN('unmatched'), cls: 'text-deep-orange' },
|
{ key: 'unmatched', label: 'Non matchées', n: statN('unmatched'), cls: 'text-deep-orange' },
|
||||||
{ key: 'no_address', label: 'Sans adresse (rejetées)', n: statN('no_address'), cls: 'text-grey-7' },
|
{ key: 'no_address', label: 'Rejetées (sans adresse)', n: statN('no_address'), cls: 'text-grey-7' },
|
||||||
])
|
])
|
||||||
const typeChips = computed(() => [
|
const typeChips = computed(() => [
|
||||||
{ key: '', label: 'Tout', n: statN('review') + statN('unmatched') },
|
{ key: '', label: 'Tout', n: statN('review') + statN('unmatched') },
|
||||||
|
|
@ -254,5 +256,16 @@ async function applyCampings () {
|
||||||
catch (e) { $q.notify({ type: 'negative', message: e.message }) } finally { campSaving.value = false }
|
catch (e) { $q.notify({ type: 'negative', message: e.message }) } finally { campSaving.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Dernier repli : centre du code postal / ville pour les unmatched restants ──
|
||||||
|
const areaSaving = ref(false)
|
||||||
|
async function applyArea () {
|
||||||
|
areaSaving.value = true
|
||||||
|
try {
|
||||||
|
const r = await addr.conformityApplyArea()
|
||||||
|
$q.notify({ type: 'positive', message: `${r.total || 0} placés au secteur (${r.by_postal || 0} par code postal, ${r.by_city || 0} par ville)`, timeout: 2600 })
|
||||||
|
reload()
|
||||||
|
} catch (e) { $q.notify({ type: 'negative', message: 'Échec: ' + e.message }) } finally { areaSaving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => { loadStats(); loadList(); loadCampings() })
|
onMounted(() => { loadStats(); loadList(); loadCampings() })
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,33 @@ async function applyCampings (p) {
|
||||||
return r.rows
|
return r.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dernier repli : pour les 'unmatched' qu'on n'a pas pu géocoder, placer au CENTRE (centroïde rqa_addresses)
|
||||||
|
// du code postal (préféré, plus précis) sinon de la ville → le job apparaît au moins dans le bon secteur.
|
||||||
|
// Statut 'area' (approximatif). Les hors-QC/junk (pas dans rqa_addresses) restent 'unmatched'.
|
||||||
|
async function applyAreaFallback (p) {
|
||||||
|
const r = await p.query(`
|
||||||
|
WITH um AS (
|
||||||
|
SELECT name, replace(upper(coalesce(postal_code,'')),' ','') cp, lower(unaccent(coalesce(city,''))) city,
|
||||||
|
postal_code cp_raw, city city_raw
|
||||||
|
FROM "tabService Location" WHERE address_validation_status='unmatched'
|
||||||
|
),
|
||||||
|
pc AS (SELECT replace(upper(code_postal),' ','') cp, avg(latitude) lat, avg(longitude) lon FROM rqa_addresses
|
||||||
|
WHERE replace(upper(code_postal),' ','') IN (SELECT cp FROM um WHERE cp ~ '^[A-Z][0-9][A-Z][0-9]') GROUP BY 1),
|
||||||
|
cc AS (SELECT lower(unaccent(ville)) city, avg(latitude) lat, avg(longitude) lon FROM rqa_addresses
|
||||||
|
WHERE lower(unaccent(ville)) IN (SELECT city FROM um WHERE city NOT IN ('','n/a','ville','x','-')) GROUP BY 1),
|
||||||
|
upd AS (
|
||||||
|
UPDATE "tabService Location" sl SET
|
||||||
|
latitude=COALESCE(pc.lat,cc.lat), longitude=COALESCE(pc.lon,cc.lon),
|
||||||
|
linked_address='≈ centre ' || COALESCE(NULLIF(CASE WHEN pc.lat IS NOT NULL THEN um.cp_raw END,''), um.city_raw),
|
||||||
|
address_validation_status='area', address_validated_at=NOW(), modified=NOW()
|
||||||
|
FROM um LEFT JOIN pc ON pc.cp=um.cp LEFT JOIN cc ON cc.city=um.city
|
||||||
|
WHERE sl.name=um.name AND (pc.lat IS NOT NULL OR cc.lat IS NOT NULL)
|
||||||
|
RETURNING (pc.lat IS NOT NULL) AS by_postal)
|
||||||
|
SELECT count(*)::int total, count(*) FILTER (WHERE by_postal)::int by_postal,
|
||||||
|
count(*) FILTER (WHERE NOT by_postal)::int by_city FROM upd`)
|
||||||
|
return r.rows[0]
|
||||||
|
}
|
||||||
|
|
||||||
// Type d'adresse (pour trier la file) : camping (sobriquet de lot), civique à corriger, non-adresse, review standard.
|
// Type d'adresse (pour trier la file) : camping (sobriquet de lot), civique à corriger, non-adresse, review standard.
|
||||||
const TYPE_SQL = `CASE
|
const TYPE_SQL = `CASE
|
||||||
WHEN sl.city ILIKE '%camping%' OR sl.address_line ILIKE '%camping%' THEN 'camping'
|
WHEN sl.city ILIKE '%camping%' OR sl.address_line ILIKE '%camping%' THEN 'camping'
|
||||||
|
|
@ -137,6 +164,10 @@ async function handle (req, res, method, path) {
|
||||||
if (path === '/address/conformity/campings/apply' && method === 'POST') {
|
if (path === '/address/conformity/campings/apply' && method === 'POST') {
|
||||||
cors(res); return json(res, 200, { ok: true, applied: await applyCampings(pool()) })
|
cors(res); return json(res, 200, { ok: true, applied: await applyCampings(pool()) })
|
||||||
}
|
}
|
||||||
|
// Repli centroïde (centre du CP/ville) pour les unmatched restants
|
||||||
|
if (path === '/address/conformity/apply-area' && method === 'POST') {
|
||||||
|
cors(res); return json(res, 200, { ok: true, ...(await applyAreaFallback(pool())) })
|
||||||
|
}
|
||||||
|
|
||||||
cors(res); return json(res, 404, { ok: false, error: 'route conformité inconnue' })
|
cors(res); return json(res, 404, { ok: false, error: 'route conformité inconnue' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user