diff --git a/components/FicheMiniCard.vue b/components/FicheMiniCard.vue new file mode 100644 index 0000000..f527f90 --- /dev/null +++ b/components/FicheMiniCard.vue @@ -0,0 +1,103 @@ + + + {{ fiche.nom }} + {{ fiche.resume }} + + + + + + + Sans localisation + + + + + + diff --git a/composables/useFicheAdapter.ts b/composables/useFicheAdapter.ts new file mode 100644 index 0000000..3b3caa9 --- /dev/null +++ b/composables/useFicheAdapter.ts @@ -0,0 +1,84 @@ +/** + * useFicheAdapter — B3-M2 + * Adaptateur léger Org | StructureV2 → MiniFiche, format commun consommé par + * FicheMiniCard / FichesPanel. Aucune mutation des types sources. + * + * Repli résumé (brief B3, le champ `resume_court` du batch B2b n'existe pas + * encore) : première phrase du premier champ non vide parmi une liste de + * candidats ordonnée par type, tronquée proprement à ~110 caractères sur un + * mot (jamais au milieu). + */ +import type { Org } from '~/types/org' +import type { StructureV2 } from '~/types/structure-v2' + +export interface MiniFiche { + id: string | number + nom: string + resume: string + chips: string[] + echelle?: string + geoloc: boolean + kind: 'org' | 'structure' +} + +const RESUME_MAX = 110 + +/** Première phrase d'un texte (coupe sur ". "/"! "/"? " ou fin de chaîne). */ +function firstSentence(text: string): string { + const match = text.match(/^.*?[.!?](?=\s|$)/) + return (match ? match[0] : text).trim() +} + +/** Tronque proprement sur un mot, jamais au milieu. */ +function truncateOnWord(text: string, max = RESUME_MAX): string { + if (text.length <= max) return text + const slice = text.slice(0, max) + const lastSpace = slice.lastIndexOf(' ') + const cut = lastSpace > max * 0.4 ? slice.slice(0, lastSpace) : slice + return `${cut.trimEnd()}…` +} + +function buildResume(candidates: Array): string { + const source = candidates.find((c) => !!c && c.trim().length > 0) + if (!source) return '' + return truncateOnWord(firstSentence(source.trim())) +} + +function isStructure(source: Org | StructureV2): source is StructureV2 { + return 'famille_principale' in source +} + +function orgFonctions(org: Org): string[] { + return (org.tags_fonction ?? '') + .split(',') + .map((f) => f.trim()) + .filter(Boolean) + .slice(0, 3) +} + +export function useFicheAdapter() { + function toMini(source: Org | StructureV2): MiniFiche { + if (isStructure(source)) { + return { + id: source.id, + nom: source.nom, + resume: buildResume([source.description_courte, source.description_longue]), + chips: (source.hashtags ?? []).slice(0, 3), + geoloc: source.latitude != null && source.longitude != null, + kind: 'structure', + } + } + + return { + id: source.Id, + nom: source.nom, + resume: buildResume([source.description_enrichie, source.description]), + chips: orgFonctions(source), + echelle: source.echelle, + geoloc: source.latitude != null && source.longitude != null, + kind: 'org', + } + } + + return { toMini } +} diff --git a/pages/dev/fiche-mini-card.vue b/pages/dev/fiche-mini-card.vue new file mode 100644 index 0000000..35314c5 --- /dev/null +++ b/pages/dev/fiche-mini-card.vue @@ -0,0 +1,150 @@ + + + + FicheMiniCard — story de test (B3-M2) + + + Org — cas nominal + + + + + + + Structure — cas nominal + + + + + + + Cas limites + + + + + + + + + + Dernier événement « open » + {{ lastOpen ?? '(aucun clic pour le moment)' }} + + + + + + +
{{ lastOpen ?? '(aucun clic pour le moment)' }}