Campings : gestion du registre + réapplication self-service depuis Ops
Rend le mécanisme réutilisable (« faire de même pour tous les campings ») :
- Hub (address-conformity.js) : GET /address/conformity/campings (registre + nb lots par camping),
POST /campings (upsert {keyword,name,address,lat,lon} → applique direct), POST /campings/apply (réappliquer).
applyCampings() = UPDATE des lots (match ville normalisée) → géoloc fixe du camping.
- Ops (page Conformité adresses) : section « Campings — géoloc de remplacement fixe » : table du registre
(nom, adresse principale, GPS→Google Maps, nb lots) + formulaire d'ajout (nom/mot-clé/adresse/lat/lon)
qui ajoute ET applique, + bouton « réappliquer ». api/address.js : campingsList/Upsert/Apply.
→ Pour un nouveau camping : on saisit son adresse principale + GPS, tous ses lots pointent dessus (le tech
navigue au camping). Registre seedé : Lac des Pins, Dauphinais, SandySun, Frontière, Ensoleillé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6453757d1
commit
ec6a317933
|
|
@ -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', {})
|
||||
|
|
|
|||
|
|
@ -20,6 +20,33 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campings : géoloc de remplacement fixe (le lot pointe vers l'adresse principale du camping) -->
|
||||
<q-expansion-item icon="cottage" label="Campings — géoloc de remplacement fixe" class="q-mb-sm" header-class="text-weight-medium">
|
||||
<q-card flat bordered class="q-pa-sm">
|
||||
<div class="text-caption text-grey-7 q-mb-sm">Pour un lot de camping, l'adresse est souvent la <b>résidence</b> du client, pas le terrain. On force la position du <b>camping</b> (le tech y navigue, puis trouve le terrain). Ajoute un camping → appliqué à tous ses lots (match sur la ville).</div>
|
||||
<q-markup-table flat dense wrap-cells class="q-mb-sm">
|
||||
<thead><tr><th class="text-left">Camping (mot-clé)</th><th class="text-left">Adresse principale</th><th>GPS</th><th>Lots</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="c in campings" :key="c.id">
|
||||
<td class="text-left">{{ c.name }} <span class="text-grey-5">({{ c.keyword }})</span></td>
|
||||
<td class="text-left">{{ c.address }}</td>
|
||||
<td class="text-center"><a :href="'https://www.google.com/maps?q=' + c.latitude + ',' + c.longitude" target="_blank">{{ (+c.latitude).toFixed(4) }}, {{ (+c.longitude).toFixed(4) }}</a></td>
|
||||
<td class="text-center">{{ campingLots[c.name] || 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
<div class="row q-col-gutter-xs items-center">
|
||||
<q-input class="col-3" dense outlined v-model="newCamp.name" label="Nom" />
|
||||
<q-input class="col-2" dense outlined v-model="newCamp.keyword" label="Mot-clé (ville)" />
|
||||
<q-input class="col-3" dense outlined v-model="newCamp.address" label="Adresse principale" />
|
||||
<q-input class="col" dense outlined v-model.number="newCamp.latitude" label="Lat" />
|
||||
<q-input class="col" dense outlined v-model.number="newCamp.longitude" label="Lon" />
|
||||
<q-btn dense unelevated color="primary" icon="add" :disable="!campValid" :loading="campSaving" @click="addCamping"><q-tooltip>Ajouter + appliquer</q-tooltip></q-btn>
|
||||
<q-btn dense flat color="indigo" icon="sync" :loading="campSaving" @click="applyCampings"><q-tooltip>Réappliquer tous les campings</q-tooltip></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<!-- Filtres par type -->
|
||||
<div class="row items-center q-gutter-sm q-mb-sm">
|
||||
<q-chip v-for="t in typeChips" :key="t.key" clickable :selected="filter.type === t.key"
|
||||
|
|
@ -203,5 +230,29 @@ function openGps (row) { gps.row = row; gps.lat = row.latitude ? +row.latitude :
|
|||
function parsePaste () { const m = String(gps.paste).match(/(-?\d+\.\d+)[ ,]+(-?\d+\.\d+)/); if (m) { gps.lat = +m[1]; gps.lon = +m[2] } }
|
||||
function saveGps () { if (!gpsValid.value) return; resolve(gps.row, 'gps', { latitude: gps.lat, longitude: gps.lon }); gps.open = false }
|
||||
|
||||
onMounted(() => { 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() })
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user