diff --git a/apps/ops/src/api/address.js b/apps/ops/src/api/address.js index 5347925..7bc28b3 100644 --- a/apps/ops/src/api/address.js +++ b/apps/ops/src/api/address.js @@ -20,3 +20,8 @@ export const conformityList = (p) => jget('/address/conformity/list?' + new URLS export const conformityCandidates = (q) => jget('/address/conformity/candidates?q=' + encodeURIComponent(q)) // action: approve | correct | gps | reject (+ aq_address_id/linked_address/latitude/longitude selon l'action) export const conformityResolve = (body) => jpost('/address/conformity/resolve', body) + +// ── Registre des campings (géoloc de remplacement fixe par camping) ── +export const campingsList = () => jget('/address/conformity/campings') +export const campingsUpsert = (body) => jpost('/address/conformity/campings', body) // {keyword,name,address,latitude,longitude} +export const campingsApply = () => jpost('/address/conformity/campings/apply', {}) diff --git a/apps/ops/src/pages/AddressConformityPage.vue b/apps/ops/src/pages/AddressConformityPage.vue index 97dfd28..c87ad3b 100644 --- a/apps/ops/src/pages/AddressConformityPage.vue +++ b/apps/ops/src/pages/AddressConformityPage.vue @@ -20,6 +20,33 @@ + + + +
Pour un lot de camping, l'adresse est souvent la résidence du client, pas le terrain. On force la position du camping (le tech y navigue, puis trouve le terrain). Ajoute un camping → appliqué à tous ses lots (match sur la ville).
+ + Camping (mot-clé)Adresse principaleGPSLots + + + {{ c.name }} ({{ c.keyword }}) + {{ c.address }} + {{ (+c.latitude).toFixed(4) }}, {{ (+c.longitude).toFixed(4) }} + {{ campingLots[c.name] || 0 }} + + + +
+ + + + + + Ajouter + appliquer + Réappliquer tous les campings +
+
+
+
{ loadStats(); loadList() }) +// ── Campings (géoloc de remplacement fixe) ── +const campings = ref([]) +const campingLots = ref({}) +const campSaving = ref(false) +const newCamp = reactive({ name: '', keyword: '', address: '', latitude: null, longitude: null }) +const campValid = computed(() => !!newCamp.name && !!newCamp.keyword && isFinite(newCamp.latitude) && isFinite(newCamp.longitude)) +async function loadCampings () { try { const r = await addr.campingsList(); campings.value = r.campings || []; campingLots.value = r.lots || {} } catch (e) {} } +function campApplied (r) { return (r.applied || []).reduce((s, x) => s + (x.n || 0), 0) } +async function addCamping () { + if (!campValid.value) return + campSaving.value = true + try { + const r = await addr.campingsUpsert({ name: newCamp.name, keyword: newCamp.keyword, address: newCamp.address, latitude: newCamp.latitude, longitude: newCamp.longitude }) + $q.notify({ type: 'positive', message: `Camping enregistré + appliqué (${campApplied(r)} lots)`, timeout: 2200 }) + Object.assign(newCamp, { name: '', keyword: '', address: '', latitude: null, longitude: null }) + loadCampings(); reload() + } catch (e) { $q.notify({ type: 'negative', message: 'Échec: ' + e.message }) } finally { campSaving.value = false } +} +async function applyCampings () { + campSaving.value = true + try { const r = await addr.campingsApply(); $q.notify({ type: 'positive', message: `Appliqué : ${campApplied(r)} lots`, timeout: 2200 }); loadCampings(); reload() } + catch (e) { $q.notify({ type: 'negative', message: e.message }) } finally { campSaving.value = false } +} + +onMounted(() => { loadStats(); loadList(); loadCampings() }) diff --git a/services/targo-hub/lib/address-conformity.js b/services/targo-hub/lib/address-conformity.js index bba7b9e..edfc81d 100644 --- a/services/targo-hub/lib/address-conformity.js +++ b/services/targo-hub/lib/address-conformity.js @@ -14,6 +14,21 @@ */ const { json, parseBody, log, cors } = require('./helpers') const { searchRaw, pool } = require('./address-db') // réutilise le pool LOCAL partagé (rqa_addresses + fiber) +const { norm } = require('./util/text') + +// Application de la géoloc de remplacement des campings sur leurs lots (match ville normalisée). Réutilisée +// par /campings/apply ET appelée après l'ajout/édition d'un camping. Retourne le décompte par camping. +async function applyCampings (p) { + const r = await p.query(`WITH applied AS ( + UPDATE "tabService Location" sl SET latitude=c.latitude, longitude=c.longitude, + linked_address=c.name||' — '||COALESCE(c.address,''), aq_address_id=NULL, + address_validation_status='validated', address_validated_at=NOW(), modified=NOW() + FROM camping_registry c + WHERE c.active AND lower(unaccent(coalesce(sl.city,''))) LIKE '%'||c.keyword||'%' + RETURNING c.name AS camping) + SELECT camping, count(*)::int n FROM applied GROUP BY camping ORDER BY 2 DESC`) + return r.rows +} // Type d'adresse (pour trier la file) : camping (sobriquet de lot), civique à corriger, non-adresse, review standard. const TYPE_SQL = `CASE @@ -98,6 +113,31 @@ async function handle (req, res, method, path) { cors(res); return json(res, 200, { ok: true, updated: r.rowCount, name, action: b.action }) } + // ── Registre des campings (géoloc de remplacement fixe) ── + if (path === '/address/conformity/campings' && method === 'GET') { + const p = pool() + const campings = (await p.query('SELECT id, keyword, name, address, latitude, longitude, active FROM camping_registry ORDER BY name, keyword')).rows + const counts = (await p.query(`SELECT c.name, count(sl.name)::int n FROM camping_registry c + LEFT JOIN "tabService Location" sl ON c.active AND lower(unaccent(coalesce(sl.city,''))) LIKE '%'||c.keyword||'%' + GROUP BY c.name`)).rows + const lots = {}; for (const x of counts) lots[x.name] = x.n + cors(res); return json(res, 200, { ok: true, campings, lots }) + } + if (path === '/address/conformity/campings' && method === 'POST') { + const b = await parseBody(req) + if (!b.keyword || !b.name || b.latitude == null || b.longitude == null) { cors(res); return json(res, 400, { ok: false, error: 'keyword/name/latitude/longitude requis' }) } + const p = pool() + await p.query(`INSERT INTO camping_registry (keyword,name,address,latitude,longitude,active) + VALUES ($1,$2,$3,$4,$5,true) + ON CONFLICT (keyword) DO UPDATE SET name=$2, address=$3, latitude=$4, longitude=$5, active=true`, + [norm(b.keyword), b.name, b.address || '', Number(b.latitude), Number(b.longitude)]) + const applied = await applyCampings(p) // applique direct le nouveau/maj camping + cors(res); return json(res, 200, { ok: true, applied }) + } + 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, 404, { ok: false, error: 'route conformité inconnue' }) } catch (e) { log('address-conformity error:', e.message)