UPG Neighbor: print a prayer packet of a reached group's unreached neighbours, on the shared packet renderer
From a green group's brief: the same list as the map, the CSV and the letter (this E-level, closest first, the closest 60, as the cover says), printed by library/composables/usePosterPrints.printPacket with the renderer UPG-100 uses, not a copy. The group set is upg100's pg_slugs shape. Agreed with upg100: the lightweight variant (the %-evangelical page is DOXA-wide today), a triangle of its own (all, unreached, these N; not DOXA's 2,107, which these are not a subset of), and one explicit record adapter (ISO3 country code, the language as a plain label, IMB's GSEC for the how-reached sentence, photos joined from the doxa list by slug when an API is configured).
This commit is contained in:
parent
0428f279c1
commit
5b6ed0d0f8
1 changed files with 68 additions and 1 deletions
|
|
@ -13,7 +13,8 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
|||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||
import { whenStyleReady } from './whenStyleReady.js'
|
||||
import { useNeighborT } from './composables/useNeighborT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
import { tStatic, getRuntimeLocale } from '@map/i18n/index.js'
|
||||
import FUNNEL from '../../../data/people-groups/vision-funnel.json'
|
||||
import { loadNearData, E_COLORS, UNREACHED, REACHED, SELECTED, NAVY, fold, fmtNum, downloadCsv, downloadText } from './nearData.js'
|
||||
import { buildLetter } from './letter.js'
|
||||
|
||||
|
|
@ -193,6 +194,71 @@ function onLetterKey(e) {
|
|||
}
|
||||
watch(() => nbState.pgid, () => { letterOpen.value = false })
|
||||
|
||||
// ─── The neighbours prayer packet (phase 4): the SHARED packet, not a copy ─────
|
||||
// library/composables/usePosterPrints.printPacket with library/packet's renderer, the one UPG-100 prints
|
||||
// with. The group set is upg100's shape (a pg_slugs selector); the members are the same list as the map,
|
||||
// the CSV and the letter (this E-level, closest first), capped at PACKET_CAP, and the cover label says so.
|
||||
// Decisions agreed with upg100 (2026-09-29):
|
||||
// · variant 'lightweight': the %-evangelical page is DOXA-wide today and would describe DOXA, not these
|
||||
// neighbours; lightweight prints map · triangle · profiles · wheel · points.
|
||||
// · the triangle is told its own steps: all → unreached → these N. Its default puts DOXA's 2,107 directly
|
||||
// above N, which would read as "these are a subset of DOXA's list"; they are not.
|
||||
// · one explicit record adapter (below), because the renderer and describeGroup read their own field
|
||||
// names and a mismatch prints blank, never fails.
|
||||
const PACKET_CAP = 60
|
||||
const packetBusy = ref(false)
|
||||
const packetSlug = (g) => g.slug || g.pgid
|
||||
// Photos: joined from the doxa people-group list by slug, when an API is configured (config-only and
|
||||
// fail-closed, as upg100: no default host). No photo prints the renderer's own "no photo" tile.
|
||||
async function doxaPhotos(groups) {
|
||||
const base = (typeof window !== 'undefined' && window.MAP_APP_API_URL) || import.meta.env.VITE_API_BASE_URL || ''
|
||||
const want = new Set(groups.map((g) => g.slug).filter(Boolean))
|
||||
if (!base || !want.size) return new Map()
|
||||
try {
|
||||
const r = await fetch(`${base}/api/people-groups/list?fields=slug,image_url`)
|
||||
if (!r.ok) return new Map()
|
||||
const j = await r.json()
|
||||
const list = Array.isArray(j) ? j : (j.data || j.people_groups || j.posts || [])
|
||||
const url = (v) => (v && typeof v === 'object' ? v.value || v.url || '' : v || '')
|
||||
return new Map(list.filter((x) => want.has(x.slug) && url(x.image_url)).map((x) => [x.slug, url(x.image_url)]))
|
||||
} catch (_) { return new Map() }
|
||||
}
|
||||
const packetRecord = (g, photos) => ({
|
||||
slug: packetSlug(g), name: g.name, population: g.pop, religion: g.religion,
|
||||
country_code: g.iso3, // the ISO3: describeGroup localizes the country name from it
|
||||
country_code_label: g.country, // the next rung: printed if the code cannot be resolved
|
||||
primary_language_label: g.langName, // a plain string; describeGroup reads this rung first
|
||||
imb_gsec: Number.isInteger(g.imb_gsec) ? g.imb_gsec : null, // 0-6: the "how reached" sentence
|
||||
image_url: photos.get(g.slug) || '',
|
||||
})
|
||||
async function printNeighbourPacket() {
|
||||
const g = selected.value
|
||||
if (!g || !g.reached || packetBusy.value || !neighbors.value.length) return
|
||||
packetBusy.value = true
|
||||
try {
|
||||
const all = neighbors.value, members = all.slice(0, PACKET_CAP).map((x) => x.g)
|
||||
const level = nt(E_SHORT[eLevel.value])
|
||||
const label = all.length > PACKET_CAP
|
||||
? nt('neighbor.packet.labelCapped', { group: g.name, n: PACKET_CAP, total: all.length.toLocaleString('en-US'), level })
|
||||
: nt('neighbor.packet.label', { group: g.name, level })
|
||||
const zone = { id: `neighbours-${g.pgid}-E${eLevel.value}`, label, selector: { type: 'pg_slugs', values: members.map(packetSlug) } }
|
||||
const [{ usePosterPrints }, photos] = await Promise.all([import('@map/composables/usePosterPrints.js'), doxaPhotos(members)])
|
||||
const records = members.map((m) => packetRecord(m, photos))
|
||||
const steps = (FUNNEL?.steps || []).filter((s) => s.id === 'all' || s.id === 'unreached')
|
||||
const prints = usePosterPrints({
|
||||
records: () => records,
|
||||
lang: () => String(getRuntimeLocale() || 'en').split('-')[0],
|
||||
label: (z) => z?.label || '',
|
||||
downloadPrefix: 'doxa-upg-neighbor',
|
||||
})
|
||||
await prints.printPacket(zone, { funnel: steps.length ? { steps } : { steps: [] }, variant: 'lightweight' })
|
||||
} catch (e) {
|
||||
console.warn('[upg-neighbor] packet failed:', e?.message || e)
|
||||
} finally {
|
||||
packetBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openBridge() { if (selected.value) { nbState.lang = selected.value.lang; nbState.view = 'campaigns' } }
|
||||
const isBridge = computed(() => !!selected.value && !!data.value?.bridges.some((b) => b.lang === selected.value.lang))
|
||||
|
||||
|
|
@ -470,6 +536,7 @@ useShadowStyles(`
|
|||
|
||||
<div class="mb-actions">
|
||||
<button v-if="isReachedSel && neighbors.length" ref="letterBtn" class="mb-btn" @click="openLetter">{{ $t('neighbor.letter.button') }}</button>
|
||||
<button v-if="isReachedSel && neighbors.length" type="button" class="mb-btn ghost" :disabled="packetBusy" :aria-busy="packetBusy ? 'true' : 'false'" @click="printNeighbourPacket">{{ $t('neighbor.packet.button') }}</button>
|
||||
<button class="mb-btn" :class="{ ghost: isReachedSel && neighbors.length }" @click="exportSegments">{{ $t('neighbor.mob.export') }}</button>
|
||||
<button v-if="isBridge" class="mb-btn ghost" @click="openBridge">{{ $t('neighbor.mob.openBridge', { lang: selected.langName }) }}</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue