@tekkare/romulus 0.1.242 → 0.1.244
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/layer/app/components/AppUpdateToast.vue +157 -0
- package/layer/app/components/OipShell.vue +2 -0
- package/layer/app/components/auth/SplitLayout.vue +261 -0
- package/layer/app/components/layout/alertBrandSync.vue +143 -0
- package/layer/app/components/layout/firstVisitVideo.vue +165 -0
- package/layer/app/components/layout/tool/adminTemplates.vue +261 -0
- package/layer/app/components/layout/tool/alerts.vue +969 -0
- package/layer/app/components/layout/tool/export.vue +590 -0
- package/layer/app/components/layout/tool/views.vue +933 -0
- package/layer/app/components/layout/viewEmptyState.vue +278 -0
- package/layer/app/components/layout/viewPill.vue +550 -0
- package/layer/app/components/panels/OipPanelProjects.vue +94 -0
- package/layer/app/composables/useAdminAccess.ts +86 -0
- package/layer/app/composables/useAlertBrandSync.ts +85 -0
- package/layer/app/composables/useAlertSource.ts +210 -0
- package/layer/app/composables/useAlerts.ts +345 -0
- package/layer/app/composables/useExportSource.ts +236 -0
- package/layer/app/composables/useHubrEmbedded.ts +13 -0
- package/layer/app/composables/usePermalink.ts +108 -0
- package/layer/app/composables/useSavedExports.ts +286 -0
- package/layer/app/composables/useViewCardActions.ts +500 -0
- package/layer/app/composables/useViewEditing.ts +11 -0
- package/layer/app/composables/useViewTemplates.ts +369 -0
- package/layer/app/composables/useViews.ts +574 -0
- package/layer/app/oip-defaults/alerts.ts +34 -0
- package/layer/app/oip-defaults/api.ts +18 -0
- package/layer/app/oip-defaults/export.ts +23 -0
- package/layer/app/oip-defaults/ui.ts +21 -0
- package/layer/app/oip-defaults/views.ts +59 -0
- package/layer/app/utils/alertFieldLabels.ts +191 -0
- package/layer/app/utils/countries.ts +30 -0
- package/layer/app/utils/exportPage.ts +418 -0
- package/layer/app/utils/headerTabs.ts +15 -0
- package/layer/app/utils/hybridRows.ts +22 -0
- package/layer/app/utils/oipScopeStore.ts +112 -0
- package/layer/app/utils/oipScopes.ts +376 -0
- package/layer/app/utils/postLoginRedirect.ts +58 -0
- package/layer/app/utils/valueTable.ts +126 -0
- package/layer/app/utils/viewPages.ts +30 -0
- package/layer/app/utils/viewTemplateCriteria.ts +215 -0
- package/layer/app/utils/viewTemplates.ts +116 -0
- package/layer/nuxt.config.ts +9 -0
- package/layer/server/api/oip/notes.get.ts +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ce que l'ecran montre, relu dans le DOM pour le panneau d'export.
|
|
3
|
+
*
|
|
4
|
+
* Un rapport PDF doit ressembler a la page : le graphique quand la vue est un
|
|
5
|
+
* graphique, le tableau quand c'est un tableau, avec les memes titres et les
|
|
6
|
+
* memes chiffres cles. Plutot que de faire redeclarer a chaque page ce qu'elle
|
|
7
|
+
* affiche deja (et de laisser les deux diverger a la premiere retouche), on
|
|
8
|
+
* relit le rendu au moment de l'export.
|
|
9
|
+
*
|
|
10
|
+
* Trois regles communes a tout ce module :
|
|
11
|
+
*
|
|
12
|
+
* - seul le visible compte. Les onglets d'une page vivent en `v-show` : ils
|
|
13
|
+
* restent dans le DOM, et un rapport ne doit pas porter les trois onglets que
|
|
14
|
+
* le lecteur n'a pas ouverts ;
|
|
15
|
+
* - le titre vient de la carte qui porte le contenu, donc il porte deja les
|
|
16
|
+
* filtres que la page a mis dans son intitule (« TOP 10 brands for ABBVIE ») ;
|
|
17
|
+
* - les valeurs sont prises telles qu'elles sont ecrites a l'ecran, mises en
|
|
18
|
+
* forme comprises. C'est l'export Excel qui porte les nombres bruts.
|
|
19
|
+
*/
|
|
20
|
+
import type { RmExportCard, RmExportKpi, RmExportSheet, RmExportTag } from "@tekkare/romulus/utils/export";
|
|
21
|
+
import { rmCardTags } from "@tekkare/romulus/utils/cardData";
|
|
22
|
+
|
|
23
|
+
/** Une carte de graphique telle que le panneau d'export la consomme. */
|
|
24
|
+
export interface PageChart {
|
|
25
|
+
title?: string;
|
|
26
|
+
el: Element;
|
|
27
|
+
/** Etiquettes de perimetre de la carte, redessinees sous le titre. */
|
|
28
|
+
tags?: RmExportTag[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Cartes du hub qui peuvent porter un graphique ou un tableau. */
|
|
32
|
+
const CARD_SELECTOR = ".chart-card, .rm-table-card";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ou lire : le contenu de la page, pas le cadre.
|
|
36
|
+
*
|
|
37
|
+
* Les panneaux du rail portent eux aussi des cartes et des tableaux (le
|
|
38
|
+
* tiroir de selection de produits en est un), et le panneau d'export est
|
|
39
|
+
* ouvert au moment de la lecture. Sans cette racine, un rapport sortait avec
|
|
40
|
+
* le tableau du tiroir voisin, que la page ne montre pas.
|
|
41
|
+
*/
|
|
42
|
+
function pageRoot(): ParentNode {
|
|
43
|
+
return document.querySelector("main.rm-shell__main") ?? document;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Texte d'un noeud, espaces normalises : le DOM en met partout. */
|
|
47
|
+
function cellText(el: Element | null | undefined) {
|
|
48
|
+
return (el?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Titre d'une carte, quel que soit le composant qui la rend.
|
|
53
|
+
*
|
|
54
|
+
* Le contexte (l'annee, le secteur) est repris avec le titre : trois cartes de
|
|
55
|
+
* classement ne different que par lui, et un rapport qui les nommerait toutes
|
|
56
|
+
* « Ranking: TOP 10 brands for ABBVIE » ne se lirait pas.
|
|
57
|
+
*/
|
|
58
|
+
function cardTitle(card: Element) {
|
|
59
|
+
const title =
|
|
60
|
+
cellText(card.querySelector(".chart-card-title")) ||
|
|
61
|
+
cellText(card.querySelector(".rm-table-card__title")) ||
|
|
62
|
+
"";
|
|
63
|
+
const context = cellText(card.querySelector(".chart-card-context"));
|
|
64
|
+
if (!title) return "";
|
|
65
|
+
return context ? `${title} (${context})` : title;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Une carte est-elle a l'ecran ? `offsetParent` vaut `null` des qu'un parent
|
|
70
|
+
* est en `display: none`, ce qui est exactement l'etat d'un onglet non actif.
|
|
71
|
+
*/
|
|
72
|
+
function isVisible(card: HTMLElement) {
|
|
73
|
+
return card.offsetParent !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function visibleCards(root: ParentNode): HTMLElement[] {
|
|
77
|
+
return [...root.querySelectorAll<HTMLElement>(CARD_SELECTOR)].filter(isVisible);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Le conteneur d'un graphe Romulus (RmChart pose `.rm-chart > .echart`).
|
|
82
|
+
*
|
|
83
|
+
* Viser ce noeud, et non « la carte qui contient un `<svg>` » : une carte de
|
|
84
|
+
* tableau en porte aussi (les fleches de tri, les icones d'en-tete). Le
|
|
85
|
+
* rapport sortait alors une page « Market data » avec une icone de 20 px
|
|
86
|
+
* etiree sur toute la largeur.
|
|
87
|
+
*/
|
|
88
|
+
const CHART_SELECTOR = ".rm-chart .echart";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Graphiques affiches. Les charts Romulus sont rendus par ECharts en SVG : le
|
|
92
|
+
* generateur reprend le noeud tel quel, il n'y a rien a capturer.
|
|
93
|
+
*/
|
|
94
|
+
export function visibleExportCharts(root?: ParentNode): PageChart[] {
|
|
95
|
+
if (typeof document === "undefined") return [];
|
|
96
|
+
|
|
97
|
+
// Les memes cartes que celles que le rapport garde une a une : un graphique
|
|
98
|
+
// pris dans un bloc capture entier est deja dans l'image du bloc, et le
|
|
99
|
+
// vectoriser en plus le posait deux fois dans le document.
|
|
100
|
+
return keptCards(root ?? pageRoot()).flatMap((card) =>
|
|
101
|
+
[...card.querySelectorAll<HTMLElement>(CHART_SELECTOR)]
|
|
102
|
+
// Un graphe dessine dans une cellule (courbe de tendance d'une ligne) est
|
|
103
|
+
// une decoration du tableau : il ne fait pas une page de rapport.
|
|
104
|
+
.filter((holder) => holder.querySelector("svg") && !holder.closest("table"))
|
|
105
|
+
.map((holder) => ({ title: cardTitle(card) || undefined, el: holder as Element, tags: rmCardTags(card) })),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Une carte de l'ecran, telle que le rapport mis en page la capture. */
|
|
110
|
+
export interface PageNode {
|
|
111
|
+
kind: "kpi" | "chart" | "table" | "card";
|
|
112
|
+
el: Element;
|
|
113
|
+
title?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Toutes les cartes du design system sous un noeud, la plus englobante d'abord. */
|
|
117
|
+
function cardsIn(root: ParentNode): HTMLElement[] {
|
|
118
|
+
const selector = `.rm-kpibar, ${CARD_SELECTOR}, ${RESULT_CARD_SELECTOR}`;
|
|
119
|
+
const found = [...root.querySelectorAll<HTMLElement>(selector)];
|
|
120
|
+
// `querySelectorAll` ne rend que les descendants : un bloc qui EST une carte
|
|
121
|
+
// ne se trouvait pas lui-meme, et sortait capture comme un bloc anonyme.
|
|
122
|
+
if (root instanceof HTMLElement && root.matches(selector)) found.unshift(root);
|
|
123
|
+
return (
|
|
124
|
+
found
|
|
125
|
+
.filter(isVisible)
|
|
126
|
+
// Une carte nichee dans une autre partirait deux fois dans le rapport.
|
|
127
|
+
.filter((el, _index, all) => !all.some((other) => other !== el && other.contains(el)))
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Part de la hauteur d'un bloc occupee par ses cartes. */
|
|
132
|
+
const CARDS_COVER = 0.6;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Le bloc est-il fait de ses cartes, ou en porte-t-il seulement une ?
|
|
136
|
+
*
|
|
137
|
+
* Une page de resultats est une pile de fiches : la capturer fiche par fiche
|
|
138
|
+
* fait tomber les coupures de pages entre deux fiches. Un ecran qui pose une
|
|
139
|
+
* barre de chiffres cles au-dessus de son propre markup, lui, n'est pas cette
|
|
140
|
+
* barre : sur la page des prix, elle fait 82 px sur 675, et le rapport sortait
|
|
141
|
+
* le bandeau sans la liste.
|
|
142
|
+
*/
|
|
143
|
+
function mostlyCards(block: HTMLElement, cards: HTMLElement[]) {
|
|
144
|
+
if (!cards.length) return false;
|
|
145
|
+
const height = block.getBoundingClientRect().height;
|
|
146
|
+
if (!height) return false;
|
|
147
|
+
const covered = cards.reduce((total, card) => total + card.getBoundingClientRect().height, 0);
|
|
148
|
+
return covered / height >= CARDS_COVER;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Les cartes que le rapport capture une a une, blocs anonymes exclus. */
|
|
152
|
+
function keptCards(root: ParentNode): HTMLElement[] {
|
|
153
|
+
const blocks = visiblePageBlocks(root);
|
|
154
|
+
// Hors d'une page du hub (un rendu de test, un autre cadre) : on s'en tient
|
|
155
|
+
// aux cartes, il n'y a pas de conteneur de page ou descendre.
|
|
156
|
+
if (!blocks.length) return cardsIn(root);
|
|
157
|
+
return blocks.flatMap((block) => {
|
|
158
|
+
const cards = cardsIn(block);
|
|
159
|
+
return mostlyCards(block, cards) ? cards : [];
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Le role d'une carte, pour les cases a cocher du panneau. */
|
|
164
|
+
function nodeOf(el: HTMLElement): PageNode {
|
|
165
|
+
return {
|
|
166
|
+
kind: el.matches(".rm-kpibar")
|
|
167
|
+
? "kpi"
|
|
168
|
+
: el.matches(RESULT_CARD_SELECTOR)
|
|
169
|
+
? "card"
|
|
170
|
+
: el.querySelector(CHART_SELECTOR)
|
|
171
|
+
? "chart"
|
|
172
|
+
: "table",
|
|
173
|
+
el,
|
|
174
|
+
title: cardTitle(el) || undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Ce que le rapport mis en page capture, dans l'ordre de la page.
|
|
180
|
+
*
|
|
181
|
+
* Il reconstruisait ses tableaux et ses fiches a partir de la donnee : colonnes
|
|
182
|
+
* ecretees, mise en forme differente de l'ecran, pages a moitie vides. Il
|
|
183
|
+
* capture desormais le rendu tel quel.
|
|
184
|
+
*
|
|
185
|
+
* Bloc par bloc, et non carte par carte : une page dont le design system ne
|
|
186
|
+
* reconnait qu'une partie (une barre de chiffres cles au-dessus d'un ecran au
|
|
187
|
+
* markup propre) sortait reduite a cette partie. Un bloc fait de ses cartes est
|
|
188
|
+
* rendu carte par carte, pour que les coupures de pages tombent entre elles ;
|
|
189
|
+
* les autres partent entiers.
|
|
190
|
+
*
|
|
191
|
+
* Les graphiques des cartes gardent leur chemin vectoriel (cf.
|
|
192
|
+
* `visibleExportCharts`), ils sont deja nets.
|
|
193
|
+
*/
|
|
194
|
+
export function visibleExportNodes(root?: ParentNode): PageNode[] {
|
|
195
|
+
if (typeof document === "undefined") return [];
|
|
196
|
+
|
|
197
|
+
const where = root ?? pageRoot();
|
|
198
|
+
const blocks = visiblePageBlocks(where);
|
|
199
|
+
if (!blocks.length) return cardsIn(where).map(nodeOf);
|
|
200
|
+
|
|
201
|
+
return blocks.flatMap((block) => {
|
|
202
|
+
const cards = cardsIn(block);
|
|
203
|
+
return mostlyCards(block, cards) ? cards.map(nodeOf) : [{ kind: "card" as const, el: block }];
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Le composant de page du hub, pose par le layout autour de `<NuxtPage>`. */
|
|
208
|
+
const PAGE_SELECTOR = ".oip_container";
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* L'en-tete de page appartient au cadre, pas a l'analyse : son titre est deja
|
|
212
|
+
* sur la page de garde, et ses onglets et son bouton d'export n'ont rien a
|
|
213
|
+
* faire dans un document.
|
|
214
|
+
*/
|
|
215
|
+
// `.arg_header` : bandeau Auriga des pages pas encore passees sur Romulus.
|
|
216
|
+
const PAGE_HEADER_SELECTOR = ".rm-page-header, .arg_header";
|
|
217
|
+
|
|
218
|
+
/** Les enfants qui portent quelque chose, en-tete de page exclu. */
|
|
219
|
+
function contentChildren(el: Element): HTMLElement[] {
|
|
220
|
+
return [...el.children]
|
|
221
|
+
.filter((child): child is HTMLElement => child instanceof HTMLElement)
|
|
222
|
+
.filter(isVisible)
|
|
223
|
+
.filter((child) => !child.matches(PAGE_HEADER_SELECTOR))
|
|
224
|
+
.filter((child) => cellText(child) !== "" || !!child.querySelector("svg, canvas, img"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Les blocs de la page, pour une capture fidele.
|
|
229
|
+
*
|
|
230
|
+
* On descend tant qu'un seul enfant porte le contenu : les pages du hub
|
|
231
|
+
* s'emboitent dans deux ou trois conteneurs de mise en page avant d'arriver au
|
|
232
|
+
* niveau que le lecteur voit comme des blocs. On s'arrete au premier niveau
|
|
233
|
+
* qui en compte plusieurs de vraie hauteur : c'est la que les coupures de
|
|
234
|
+
* pages du rapport tombent au bon endroit, entre deux blocs et non au milieu
|
|
235
|
+
* d'un tableau.
|
|
236
|
+
*
|
|
237
|
+
* Un bloc plus haut qu'une page n'est pas reduit pour tenir : le generateur le
|
|
238
|
+
* pose en bandes sur les pages suivantes.
|
|
239
|
+
*/
|
|
240
|
+
export function visiblePageBlocks(root?: ParentNode): HTMLElement[] {
|
|
241
|
+
if (typeof document === "undefined") return [];
|
|
242
|
+
|
|
243
|
+
const page = (root ?? pageRoot()).querySelector<HTMLElement>(PAGE_SELECTOR);
|
|
244
|
+
if (!page || !isVisible(page) || cellText(page) === "") return [];
|
|
245
|
+
|
|
246
|
+
let node: HTMLElement = page;
|
|
247
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
248
|
+
const kids = contentChildren(node);
|
|
249
|
+
if (!kids.length) break;
|
|
250
|
+
// Un titre seul au-dessus de son tableau ne fait pas deux blocs : sans ce
|
|
251
|
+
// seuil, le rapport sortait une image par ligne de texte.
|
|
252
|
+
if (kids.filter((kid) => kid.getBoundingClientRect().height > 80).length > 1) return kids;
|
|
253
|
+
if (kids.length > 1) break;
|
|
254
|
+
node = kids[0]!;
|
|
255
|
+
}
|
|
256
|
+
return [node];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Tableaux affiches, dans la forme attendue par les generateurs. Le schema est
|
|
261
|
+
* celui de l'entete du tableau, et une ligne est un tableau de chaines : les
|
|
262
|
+
* cellules sont lues dans l'ordre des colonnes, comme a l'ecran.
|
|
263
|
+
*
|
|
264
|
+
* Sert au rapport PDF, pas au classeur : Excel doit sortir la donnee complete
|
|
265
|
+
* (10 000 lignes), la page n'en montre que le haut.
|
|
266
|
+
*/
|
|
267
|
+
export function visibleExportSheets(root?: ParentNode): RmExportSheet[] {
|
|
268
|
+
if (typeof document === "undefined") return [];
|
|
269
|
+
|
|
270
|
+
const sheets: RmExportSheet[] = [];
|
|
271
|
+
|
|
272
|
+
visibleCards(root ?? pageRoot()).forEach((card) => {
|
|
273
|
+
const table = card.querySelector("table");
|
|
274
|
+
if (!table) return;
|
|
275
|
+
|
|
276
|
+
const head = [...table.querySelectorAll("thead th")].map((th) => cellText(th));
|
|
277
|
+
const rows = [...table.querySelectorAll("tbody tr")]
|
|
278
|
+
.map((tr) => [...tr.querySelectorAll("th, td")].map((cell) => cellText(cell)))
|
|
279
|
+
.filter((cells) => cells.some((cell) => cell !== ""));
|
|
280
|
+
if (!rows.length) return;
|
|
281
|
+
|
|
282
|
+
// Tableau sans entete (rare, mais un rapport sans nom de colonne est
|
|
283
|
+
// illisible) : on numerote plutot que de laisser des colonnes muettes.
|
|
284
|
+
const columns = head.length ? head : rows[0]!.map((_, index) => `Col. ${index + 1}`);
|
|
285
|
+
|
|
286
|
+
sheets.push({
|
|
287
|
+
name: cardTitle(card) || `Table ${sheets.length + 1}`,
|
|
288
|
+
schema: columns.map((column, index) => ({
|
|
289
|
+
column,
|
|
290
|
+
value: (row: any) => row?.[index] ?? null,
|
|
291
|
+
})),
|
|
292
|
+
data: rows,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return sheets;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Chiffres cles affiches (RmKpiBar), dans l'ordre de la barre. */
|
|
300
|
+
export function visibleExportKpis(root?: ParentNode): RmExportKpi[] {
|
|
301
|
+
if (typeof document === "undefined") return [];
|
|
302
|
+
|
|
303
|
+
return [...(root ?? pageRoot()).querySelectorAll<HTMLElement>(".rm-kpibar__item")]
|
|
304
|
+
.filter(isVisible)
|
|
305
|
+
.map((item) => ({
|
|
306
|
+
label: [
|
|
307
|
+
cellText(item.querySelector(".rm-kpibar__legend")),
|
|
308
|
+
cellText(item.querySelector(".rm-kpibar__subtitle")),
|
|
309
|
+
]
|
|
310
|
+
.filter(Boolean)
|
|
311
|
+
.join(", "),
|
|
312
|
+
value: cellText(item.querySelector(".rm-kpibar__value")),
|
|
313
|
+
}))
|
|
314
|
+
.filter((kpi) => kpi.value !== "");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Une fiche de resultat (RmResultCard). Les pages de recherche du hub empilent
|
|
319
|
+
* ces cartes : un rapport tire de ces pages doit les reprendre telles quelles,
|
|
320
|
+
* et non redecouper la donnee brute en fiches qui ne ressemblent a rien de ce
|
|
321
|
+
* qui est a l'ecran.
|
|
322
|
+
*/
|
|
323
|
+
// `.rm-section-card` : fiches de resultats construites sur RmSectionCard.
|
|
324
|
+
const RESULT_CARD_SELECTOR = ".rm-rcard, .rm-section-card";
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Le compteur de la carte, celui qu'elle pose en gros a gauche. Absent des
|
|
328
|
+
* cartes qui n'en portent pas (une fiche de texte reglementaire).
|
|
329
|
+
*/
|
|
330
|
+
function cardCount(card: Element) {
|
|
331
|
+
const value = cellText(card.querySelector(".rm-rcard__count-value"));
|
|
332
|
+
if (!value) return undefined;
|
|
333
|
+
return {
|
|
334
|
+
value,
|
|
335
|
+
label: cellText(card.querySelector(".rm-rcard__count-label")) || undefined,
|
|
336
|
+
extra: cellText(card.querySelector(".rm-rcard__count-extra")) || undefined,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Corps d'une fiche : les lignes libres (dernier avis, resume, indication)
|
|
342
|
+
* puis les rangees « Personnaliser ».
|
|
343
|
+
*
|
|
344
|
+
* Les lignes libres sortent sans intitule : la carte ne leur en donne pas, et
|
|
345
|
+
* le rapport n'a pas a en inventer un. C'est aussi ce qui leur vaut toute la
|
|
346
|
+
* largeur dans le rapport, comme a l'ecran.
|
|
347
|
+
*/
|
|
348
|
+
function cardFields(card: Element): RmExportCard["fields"] {
|
|
349
|
+
const fields: RmExportCard["fields"] = [];
|
|
350
|
+
|
|
351
|
+
card
|
|
352
|
+
.querySelectorAll(
|
|
353
|
+
".rm-rcard__meta > *, .rm-rcard__text, .rm-rcard__lead, .rm-rcard__desc",
|
|
354
|
+
)
|
|
355
|
+
.forEach((node) => {
|
|
356
|
+
const value = cellText(node);
|
|
357
|
+
if (!value) return;
|
|
358
|
+
// La classe dit le role de la ligne, et le rapport lui rend sa graisse :
|
|
359
|
+
// l'entete de date en petites capitales, la reference sur son fond gris,
|
|
360
|
+
// le chapo en gras. C'est la seule chose qui distingue ces lignes a
|
|
361
|
+
// l'ecran, leur texte ne le dit pas.
|
|
362
|
+
const kind = node.classList.contains("rm-rcard__ref")
|
|
363
|
+
? "ref"
|
|
364
|
+
: node.classList.contains("rm-rcard__lead")
|
|
365
|
+
? "lead"
|
|
366
|
+
: node.classList.contains("rm-rcard__desc")
|
|
367
|
+
? "desc"
|
|
368
|
+
: node.closest(".rm-rcard__meta")
|
|
369
|
+
? "meta"
|
|
370
|
+
: "text";
|
|
371
|
+
fields.push({ label: "", value, kind });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
card.querySelectorAll(".pref_row").forEach((row) => {
|
|
375
|
+
// Tag par tag : lire le conteneur d'un coup colle les etiquettes entre
|
|
376
|
+
// elles (« ImportantModere »), le DOM ne mettant aucun texte entre deux
|
|
377
|
+
// elements voisins.
|
|
378
|
+
const value = [...row.querySelectorAll(".pref_row_tags > *")]
|
|
379
|
+
.map((tag) => cellText(tag))
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
.join(" · ");
|
|
382
|
+
if (value) fields.push({ label: cellText(row.querySelector(".pref_row_label")), value });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return fields;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Fiches affichees, dans l'ordre de la liste. */
|
|
389
|
+
export function visibleExportCards(root?: ParentNode): RmExportCard[] {
|
|
390
|
+
if (typeof document === "undefined") return [];
|
|
391
|
+
|
|
392
|
+
return [...(root ?? pageRoot()).querySelectorAll<HTMLElement>(RESULT_CARD_SELECTOR)]
|
|
393
|
+
.filter(isVisible)
|
|
394
|
+
.map((card) => ({
|
|
395
|
+
// Une RmSectionCard n'a pas de classe de titre : son premier intitule en tient lieu.
|
|
396
|
+
title: cellText(card.querySelector(".rm-rcard__title") ?? card.querySelector("h1, h2, h3, .oip_heading")),
|
|
397
|
+
subtitle: cellText(card.querySelector(".rm-rcard__subtitle")) || undefined,
|
|
398
|
+
badges: [...card.querySelectorAll(".rm-rcard__head .badge")]
|
|
399
|
+
.map((badge) => cellText(badge))
|
|
400
|
+
.filter(Boolean),
|
|
401
|
+
count: cardCount(card),
|
|
402
|
+
fields: cardFields(card),
|
|
403
|
+
}))
|
|
404
|
+
// Une carte sans titre est une coquille en cours de chargement.
|
|
405
|
+
.filter((card) => card.title !== "");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Tout ce que l'ecran porte, en une lecture : c'est ce que `ExportSource.screen`
|
|
410
|
+
* attend, et ce que le panneau envoie aux formats mis en page.
|
|
411
|
+
*/
|
|
412
|
+
export function visibleExportScreen(root?: ParentNode) {
|
|
413
|
+
return {
|
|
414
|
+
sheets: visibleExportSheets(root),
|
|
415
|
+
kpi: visibleExportKpis(root),
|
|
416
|
+
cards: visibleExportCards(root),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Onglets numerotes pour RmPageHeader.
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// `arg-header` prenait une liste de libelles et renvoyait l'indice de l'onglet
|
|
4
|
+
// clique, a partir de 1. RmPageHeader travaille sur des cles. Les pages qui
|
|
5
|
+
// gardent leur onglet dans un indice passent par ici plutot que de reecrire la
|
|
6
|
+
// conversion a chaque fois : la cle est l'indice en toutes lettres.
|
|
7
|
+
|
|
8
|
+
export type IndexedTab = { key: string; label: string; active: boolean };
|
|
9
|
+
|
|
10
|
+
export const indexedTabs = (labels: readonly string[], active: number): IndexedTab[] =>
|
|
11
|
+
labels.map((label, index) => ({
|
|
12
|
+
key: String(index + 1),
|
|
13
|
+
label,
|
|
14
|
+
active: active === index + 1,
|
|
15
|
+
}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hybridRows - un classement Auriga rendu par RmHybridTable.
|
|
3
|
+
*
|
|
4
|
+
* Les analyses du portail construisent leurs classements comme
|
|
5
|
+
* `arg-hybrid-chart` les attendait : des lignes `{ values: [libelle,
|
|
6
|
+
* { value, percentage }, ...colonnes de fin] }`. RmHybridTable, lui, lit des
|
|
7
|
+
* lignes nommees. La conversion est mecanique : elle vit ici plutot que
|
|
8
|
+
* recopiee dans chaque composant, et le calcul des valeurs reste ou il est.
|
|
9
|
+
*/
|
|
10
|
+
export function hybridRows(contents: any[]) {
|
|
11
|
+
return (Array.isArray(contents) ? contents : [])
|
|
12
|
+
.filter((row: any) => Array.isArray(row?.values))
|
|
13
|
+
.map((row: any) => {
|
|
14
|
+
const [label, measure, ...extra] = row.values;
|
|
15
|
+
return {
|
|
16
|
+
label: String(label),
|
|
17
|
+
value: Number(measure?.value ?? 0),
|
|
18
|
+
percentage: measure?.percentage,
|
|
19
|
+
extra,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineStore } from "pinia";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Le store d'une famille de vues (portail, Hub-R, PHMEV...) : la vue
|
|
5
|
+
* appliquee, la liste, les filtres de l'app. Une fabrique : chaque famille a
|
|
6
|
+
* le sien, sous son propre id (`scopeStore`, `heart:scopeStore`...), et la
|
|
7
|
+
* forme est la meme pour toutes (cf. le middleware qui y ecrit une vue venue
|
|
8
|
+
* d'un lien partage).
|
|
9
|
+
*/
|
|
10
|
+
export function defineOipScopeStore(id: string) {
|
|
11
|
+
return defineStore(id, {
|
|
12
|
+
state: () => {
|
|
13
|
+
return {
|
|
14
|
+
scopeFilter: [] as Array<any>,
|
|
15
|
+
selectedScope: null as any,
|
|
16
|
+
scoped: false as boolean,
|
|
17
|
+
setState: [] as Array<any>,
|
|
18
|
+
appFilters: [] as Array<any>,
|
|
19
|
+
list: [] as any,
|
|
20
|
+
selected: null,
|
|
21
|
+
error: null,
|
|
22
|
+
loading: false,
|
|
23
|
+
selectedFilters: [],
|
|
24
|
+
isSet: false,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
actions: {
|
|
28
|
+
setList(payload: any[]) {
|
|
29
|
+
this.list = []
|
|
30
|
+
this.list = payload;
|
|
31
|
+
},
|
|
32
|
+
setSelected(payload: null) {
|
|
33
|
+
this.selected = payload;
|
|
34
|
+
},
|
|
35
|
+
setScoped(payload: boolean) {
|
|
36
|
+
this.scoped = payload;
|
|
37
|
+
},
|
|
38
|
+
setError(payload: null) {
|
|
39
|
+
this.error = payload;
|
|
40
|
+
},
|
|
41
|
+
setLoading(payload: boolean) {
|
|
42
|
+
this.loading = payload;
|
|
43
|
+
},
|
|
44
|
+
setIsSet(payload: boolean) {
|
|
45
|
+
this.isSet = payload;
|
|
46
|
+
},
|
|
47
|
+
setScopeFilter(info: any[]) {
|
|
48
|
+
this.scopeFilter = info;
|
|
49
|
+
},
|
|
50
|
+
setSelectedScope(info: Object) {
|
|
51
|
+
this.selectedScope = info;
|
|
52
|
+
},
|
|
53
|
+
clearScope() {
|
|
54
|
+
this.selectedScope = null;
|
|
55
|
+
this.clearAppFilters()
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// STORED FILTERS
|
|
59
|
+
setAppFilters(info: { filter: any; page: any; values: any }) {
|
|
60
|
+
const selectedIndex = this.appFilters.findIndex((b) => {
|
|
61
|
+
return b.filter === info.filter && b.page === info.page;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (selectedIndex !== -1) {
|
|
65
|
+
this.appFilters[selectedIndex] = info;
|
|
66
|
+
} else {
|
|
67
|
+
this.appFilters.push(info);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
resetAppFilters(query: { filter: any; page: any; info: any }) {
|
|
72
|
+
const selectedIndex = this.appFilters.findIndex((b) => {
|
|
73
|
+
return b.filter === query.filter && b.page === query.page;
|
|
74
|
+
});
|
|
75
|
+
if (selectedIndex !== -1) {
|
|
76
|
+
const index = this.appFilters[selectedIndex].values.indexOf(query.info);
|
|
77
|
+
if (index !== -1) {
|
|
78
|
+
this.appFilters[selectedIndex].values.splice(index, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
clearAppFilters(){
|
|
84
|
+
this.appFilters = []
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
resetSelectedFilters(query: { filter: any; page: any }) {
|
|
88
|
+
const selectedIndex = this.appFilters.findIndex((b) => {
|
|
89
|
+
return b.filter === query.filter && b.page === query.page;
|
|
90
|
+
});
|
|
91
|
+
if (selectedIndex !== -1) {
|
|
92
|
+
this.appFilters.splice(selectedIndex, 1);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
removeAllFromPage(query: any) {
|
|
97
|
+
this.appFilters = [...this.appFilters.filter((x) => x.page !== query)];
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getStoreFromPage() {
|
|
101
|
+
return this.appFilters.filter((x) => x.page === window.location.href);
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
findValuesStore(filter: string) {
|
|
105
|
+
return this.getStoreFromPage().find((x) => x.filter === filter)
|
|
106
|
+
? this.getStoreFromPage().find((x) => x.filter === filter)
|
|
107
|
+
: [];
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
getters: {},
|
|
111
|
+
});
|
|
112
|
+
}
|