@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.
Files changed (45) hide show
  1. package/dist/module.json +1 -1
  2. package/layer/app/components/AppUpdateToast.vue +157 -0
  3. package/layer/app/components/OipShell.vue +2 -0
  4. package/layer/app/components/auth/SplitLayout.vue +261 -0
  5. package/layer/app/components/layout/alertBrandSync.vue +143 -0
  6. package/layer/app/components/layout/firstVisitVideo.vue +165 -0
  7. package/layer/app/components/layout/tool/adminTemplates.vue +261 -0
  8. package/layer/app/components/layout/tool/alerts.vue +969 -0
  9. package/layer/app/components/layout/tool/export.vue +590 -0
  10. package/layer/app/components/layout/tool/views.vue +933 -0
  11. package/layer/app/components/layout/viewEmptyState.vue +278 -0
  12. package/layer/app/components/layout/viewPill.vue +550 -0
  13. package/layer/app/components/panels/OipPanelProjects.vue +94 -0
  14. package/layer/app/composables/useAdminAccess.ts +86 -0
  15. package/layer/app/composables/useAlertBrandSync.ts +85 -0
  16. package/layer/app/composables/useAlertSource.ts +210 -0
  17. package/layer/app/composables/useAlerts.ts +345 -0
  18. package/layer/app/composables/useExportSource.ts +236 -0
  19. package/layer/app/composables/useHubrEmbedded.ts +13 -0
  20. package/layer/app/composables/usePermalink.ts +108 -0
  21. package/layer/app/composables/useSavedExports.ts +286 -0
  22. package/layer/app/composables/useViewCardActions.ts +500 -0
  23. package/layer/app/composables/useViewEditing.ts +11 -0
  24. package/layer/app/composables/useViewTemplates.ts +369 -0
  25. package/layer/app/composables/useViews.ts +574 -0
  26. package/layer/app/oip-defaults/alerts.ts +34 -0
  27. package/layer/app/oip-defaults/api.ts +18 -0
  28. package/layer/app/oip-defaults/export.ts +23 -0
  29. package/layer/app/oip-defaults/ui.ts +21 -0
  30. package/layer/app/oip-defaults/views.ts +59 -0
  31. package/layer/app/utils/alertFieldLabels.ts +191 -0
  32. package/layer/app/utils/countries.ts +30 -0
  33. package/layer/app/utils/exportPage.ts +418 -0
  34. package/layer/app/utils/headerTabs.ts +15 -0
  35. package/layer/app/utils/hybridRows.ts +22 -0
  36. package/layer/app/utils/oipScopeStore.ts +112 -0
  37. package/layer/app/utils/oipScopes.ts +376 -0
  38. package/layer/app/utils/postLoginRedirect.ts +58 -0
  39. package/layer/app/utils/valueTable.ts +126 -0
  40. package/layer/app/utils/viewPages.ts +30 -0
  41. package/layer/app/utils/viewTemplateCriteria.ts +215 -0
  42. package/layer/app/utils/viewTemplates.ts +116 -0
  43. package/layer/nuxt.config.ts +9 -0
  44. package/layer/server/api/oip/notes.get.ts +1 -1
  45. package/package.json +4 -2
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Contrat `#oip-app/views` : le metier des vues propre a l'app. Valeurs
3
+ * neutres par defaut ; une app qui sert les panneaux Vues et Admin templates
4
+ * fournit les siennes (cf. fr_drugs_app/app/oip/views.ts).
5
+ */
6
+ import type { ViewTemplate } from "../utils/viewTemplates";
7
+
8
+ /** Libelles des filtres sans entree dans les traductions de la page. */
9
+ export const FILTER_LABELS: Record<string, { fr: string; en: string }> = {};
10
+ /** Filtres ignores a la comparaison de deux vues. */
11
+ export const IGNORED_FILTERS: string[] = ["Take"];
12
+ /** Filtres qui ne comptent pas comme pose a la main (tri, pagination). */
13
+ export const HAND_FILTER_EXCLUDED: string[] = ["Take", "Sort"];
14
+ /** Parametres d'URL d'un lien profond qui l'emportent sur la vue par defaut. */
15
+ export const DEEP_LINK_SCOPE_KEYS: string[] = [];
16
+ /** Filtres qui portent une selection de produits. */
17
+ export const VIEW_BRAND_FILTERS: string[] = ["brands"];
18
+
19
+ /** Familles de pages dont les vues valent l'une pour l'autre. */
20
+ export const VIEW_PAGE_FAMILIES: string[][] = [];
21
+ /** Anciennes cles de page, vers la forme du jour. */
22
+ export const LEGACY_VIEW_PAGES: Record<string, string> = {};
23
+
24
+ /** Catalogue des modeles de vues. */
25
+ export const VIEW_TEMPLATES: ViewTemplate[] = [];
26
+ export const VIEW_TEMPLATE_QUERY_KEYS: Record<string, string> = {};
27
+ export const VIEW_TEMPLATE_QUERY_RESET: string[] = ["template"];
28
+ /** Namespace i18n des noms de filtres des modeles publies. */
29
+ export const PUBLISHED_TEMPLATE_NAMESPACE = "filters";
30
+ /** Resout la selection d'un modele en lien profond vers sa liste de marques. */
31
+ export async function resolveTemplateBrands(
32
+ _selection: { brands: string[]; atc5: string[]; atc4: string[]; companies: string[] },
33
+ _key: string,
34
+ ): Promise<string[]> {
35
+ return [];
36
+ }
37
+
38
+ /** Encodage des criteres des modeles. */
39
+ export const PERIOD_FILTER = "periodRange";
40
+ export const PERIOD_SHORTCUTS: string[] = [];
41
+ export const HAS_TABS: string[] = [];
42
+ export const TRANSLATED_FILTER_NAMES: string[] = [];
43
+ export const LANGUAGE_BOUND = new Set<string>();
44
+
45
+ /** Options d'affichage des cartes (« Personnaliser »), par famille de page. */
46
+ export type CardPrefScope = string;
47
+ export function useCardPrefs() {
48
+ return {
49
+ on: (_scope: string): Record<string, boolean> => ({}),
50
+ anyOn: (_scope: string) => false,
51
+ toggle: (_scope: string, _key: string) => {},
52
+ reset: (_scope: string) => {},
53
+ };
54
+ }
55
+
56
+ /** Le composable de vues enregistrees par defaut (createOipScopes). */
57
+ export function useDefaultScopes(): any {
58
+ throw new Error("[oip] useDefaultScopes n'est pas fourni par #oip-app/views.");
59
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Un chemin de champ Elasticsearch, dit comme l'app le dit.
3
+ *
4
+ * Le moteur d'alertes ne connait que les noms de la donnee : il rapporte
5
+ * « reimbursement_history · 2026-09-15 · announce_date ». Les traductions
6
+ * n'existent nulle part ailleurs que dans l'app (les index ne portent aucune
7
+ * metadonnee, `_meta` est vide partout), donc c'est ici, et seulement ici,
8
+ * qu'un chemin redevient lisible.
9
+ *
10
+ * Une alerte de MARQUE surveille `*` sur onze index : la liste des champs
11
+ * possibles est ouverte, et la fermer serait mentir. Un champ absent du
12
+ * catalogue garde donc son nom brut, a dessein : il se repere a l'ecran et se
13
+ * complete ici.
14
+ */
15
+
16
+ /**
17
+ * Champs sans portee metier : cles techniques, identifiants, empreintes.
18
+ *
19
+ * Le moteur ne les rapporte plus (meme liste cote virtual-lab-api), mais les
20
+ * evenements deja journalises les portent encore : un lecteur qui ouvre le
21
+ * panneau aujourd'hui verrait toujours « index_id : fr_3400930295083_1
22
+ * devient fr_3400930318959_1 » sous un produit. Recopiee ici a dessein, les
23
+ * deux depots ne partagent pas de code.
24
+ */
25
+ import {
26
+ ALERT_ENTRY_MARKERS as ENTRY_MARKERS,
27
+ ALERT_FIELD_NESTED as NESTED,
28
+ ALERT_FIELD_TOP_LEVEL as TOP_LEVEL,
29
+ } from "#oip-app/alerts";
30
+
31
+ const NOISE_LEAVES = new Set([
32
+ "id",
33
+ "uuid",
34
+ "guid",
35
+ "key",
36
+ "hash",
37
+ "checksum",
38
+ "version",
39
+ "index_id",
40
+ "local_drug_id",
41
+ "doc_id",
42
+ "document_id",
43
+ ]);
44
+
45
+ const NOISE_SUFFIXES = ["_id", "_ids", "_uuid", "_hash", "_key"];
46
+
47
+ /** Ce changement ne se montre pas : il ne dit rien a qui fait de l'acces au marche. */
48
+ export function isAlertNoiseField(field: string): boolean {
49
+ const leaf = String(field ?? "").split(".").pop()?.toLowerCase() ?? "";
50
+ if (!leaf) return false;
51
+ // Le compteur de debordement du moteur, pas un champ.
52
+ if (leaf.startsWith("__")) return false;
53
+ if (leaf.startsWith("_")) return true;
54
+ if (NOISE_LEAVES.has(leaf)) return true;
55
+ return NOISE_SUFFIXES.some((suffix) => leaf.endsWith(suffix));
56
+ }
57
+
58
+
59
+
60
+
61
+
62
+ /** Une entree rangee sous l'empreinte de son contenu : elle ne nomme rien. */
63
+ const OPAQUE_KEY = /^[0-9a-f]{12}$/;
64
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
65
+
66
+ /** Midi pour lire la date : minuit UTC recule d'un jour a l'ouest. */
67
+ function asDate(value: string, locale: string): string {
68
+ return new Date(`${value}T12:00:00`).toLocaleDateString(
69
+ locale === "en" ? "en-GB" : "fr-FR",
70
+ { day: "numeric", month: "short", year: "numeric" },
71
+ );
72
+ }
73
+
74
+
75
+
76
+ /**
77
+ * Le chemin d'un changement, coupe en deux : ce qu'il touche, et le champ.
78
+ *
79
+ * `reimbursement_history.2026-09-15.announce_date` rend
80
+ * `{ group: "Historique du prix · 15 sept. 2026", label: "Date JO" }`. Le
81
+ * premier segment est un champ du document, le dernier un champ de l'entree,
82
+ * ceux du milieu la clef de l'entree dans sa collection : une date se met en
83
+ * forme, une empreinte se tait, le reste se garde (un identifiant d'avis situe
84
+ * la ligne).
85
+ *
86
+ * La coupe existe pour l'affichage : deux champs d'une meme entree portent le
87
+ * meme groupe, montre une seule fois au-dessus d'eux. Un champ de tete n'a pas
88
+ * de groupe, il se suffit a lui-meme.
89
+ */
90
+ export function alertFieldLabel(
91
+ field: string,
92
+ serverLabel: string | null | undefined,
93
+ t: (key: string) => string,
94
+ locale: string,
95
+ ): { group: string | null; label: string } {
96
+ // Le compteur de debordement n'est pas un champ : son libelle est deja une
97
+ // phrase.
98
+ if (!field || field.startsWith("__")) {
99
+ return { group: null, label: serverLabel || field };
100
+ }
101
+
102
+ const segments = field.split(".");
103
+ const parts = segments
104
+ .map((part, index) => {
105
+ if (index === 0) return translate(TOP_LEVEL, part, t);
106
+ if (OPAQUE_KEY.test(part)) return "";
107
+ if (ISO_DATE.test(part)) return asDate(part, locale);
108
+ return translate(NESTED, part, t);
109
+ })
110
+ .filter(Boolean);
111
+
112
+ // Une entree entiere qui entre ou qui sort : le chemin ne nomme aucun champ,
113
+ // c'est la marque qui dit ce qui s'est passe. Elle remplace alors le champ,
114
+ // et le cote vide du changement n'est plus montre : « Apparition : ... » dit
115
+ // deja qu'il n'y avait rien avant.
116
+ const marker = Object.keys(ENTRY_MARKERS).find((entry) =>
117
+ (serverLabel ?? "").endsWith(entry),
118
+ );
119
+ if (marker) {
120
+ return { group: parts.join(" · ") || null, label: t(ENTRY_MARKERS[marker]!) };
121
+ }
122
+
123
+ // Champ inconnu du catalogue : le libelle que la page a envoye avec l'alerte
124
+ // (`fieldLabels`) vaut mieux que le nom brut. Pas quand le moteur n'a fait
125
+ // que recopier le chemin : il n'apprendrait rien de plus.
126
+ const leaf = segments[segments.length - 1]!;
127
+ const catalog = segments.length === 1 ? TOP_LEVEL : NESTED;
128
+ const known = !!catalog[leaf] || OPAQUE_KEY.test(leaf) || ISO_DATE.test(leaf);
129
+ const fallback = serverLabel && !looksLikePath(serverLabel, field) ? serverLabel : null;
130
+ if (!known && fallback && parts.length > 0) parts[parts.length - 1] = fallback;
131
+
132
+ const label = parts.pop() ?? field;
133
+ return { group: parts.join(" · ") || null, label };
134
+ }
135
+
136
+ /**
137
+ * Le libelle du moteur n'est que le chemin du champ, tel quel ou avec ses
138
+ * segments separes par « · » : il ne remplace pas le nom brut.
139
+ */
140
+ function looksLikePath(label: string, field: string): boolean {
141
+ const normalized = label.trim();
142
+ return normalized === field || normalized === field.split(".").join(" · ");
143
+ }
144
+
145
+ /** `clef valeur, clef valeur` : une entree resumee par le moteur. */
146
+ const ENTRY_SUMMARY = /^([a-z0-9_]+) (.+)$/;
147
+
148
+ /**
149
+ * Une valeur, dite avec les mots de l'app.
150
+ *
151
+ * Deux formes passent ici : une valeur seule, et le resume d'une entiere
152
+ * entree, que le moteur ecrit `clef valeur, clef valeur`. Le resume n'est
153
+ * reecrit que si sa premiere clef est connue : sans ce garde-fou, une phrase
154
+ * qui commence par un mot en minuscules se ferait couper en deux.
155
+ */
156
+ export function alertChangeValue(
157
+ value: string,
158
+ t: (key: string) => string,
159
+ locale: string,
160
+ ): string {
161
+ if (!value) return value;
162
+ if (ISO_DATE.test(value)) return asDate(value, locale);
163
+
164
+ const chunks = value.split(", ");
165
+ const first = chunks[0]?.match(ENTRY_SUMMARY);
166
+ if (!first || !NESTED[first[1]!]) return value;
167
+
168
+ return chunks
169
+ .map((chunk) => {
170
+ const parts = chunk.match(ENTRY_SUMMARY);
171
+ const key = parts?.[1];
172
+ if (!key || !NESTED[key]) return chunk;
173
+ const read = parts![2]!;
174
+ return `${translate(NESTED, key, t)} : ${ISO_DATE.test(read) ? asDate(read, locale) : read}`;
175
+ })
176
+ .join(", ");
177
+ }
178
+
179
+ /** Le libelle du catalogue, ou le nom brut quand il n'y en a pas. */
180
+ function translate(
181
+ catalog: Record<string, string>,
182
+ field: string,
183
+ t: (key: string) => string,
184
+ ): string {
185
+ const key = catalog[field];
186
+ if (!key) return field;
187
+ const label = t(key);
188
+ // `t` rend la clef quand la traduction manque : le nom brut se lit mieux
189
+ // qu'un chemin i18n.
190
+ return label === key ? field : label;
191
+ }
@@ -0,0 +1,30 @@
1
+ // ISO 3166-1 alpha-2 codes → localized country names via Intl.DisplayNames.
2
+ // Used by the onboarding country picker (searchable select).
3
+ const ISO_ALPHA2 = [
4
+ "AF","AX","AL","DZ","AS","AD","AO","AI","AQ","AG","AR","AM","AW","AU","AT","AZ",
5
+ "BS","BH","BD","BB","BY","BE","BZ","BJ","BM","BT","BO","BA","BW","BR","IO","BN",
6
+ "BG","BF","BI","KH","CM","CA","CV","KY","CF","TD","CL","CN","CO","KM","CG","CD",
7
+ "CK","CR","CI","HR","CU","CY","CZ","DK","DJ","DM","DO","EC","EG","SV","GQ","ER",
8
+ "EE","SZ","ET","FJ","FI","FR","GF","PF","GA","GM","GE","DE","GH","GI","GR","GL",
9
+ "GD","GP","GU","GT","GG","GN","GW","GY","HT","HN","HK","HU","IS","IN","ID","IR",
10
+ "IQ","IE","IM","IL","IT","JM","JP","JE","JO","KZ","KE","KI","KP","KR","KW","KG",
11
+ "LA","LV","LB","LS","LR","LY","LI","LT","LU","MO","MG","MW","MY","MV","ML","MT",
12
+ "MH","MQ","MR","MU","MX","FM","MD","MC","MN","ME","MS","MA","MZ","MM","NA","NR",
13
+ "NP","NL","NC","NZ","NI","NE","NG","MK","NO","OM","PK","PW","PS","PA","PG","PY",
14
+ "PE","PH","PL","PT","PR","QA","RE","RO","RU","RW","KN","LC","VC","WS","SM","ST",
15
+ "SA","SN","RS","SC","SL","SG","SK","SI","SB","SO","ZA","SS","ES","LK","SD","SR",
16
+ "SE","CH","SY","TW","TJ","TZ","TH","TL","TG","TO","TT","TN","TR","TM","TC","TV",
17
+ "UG","UA","AE","GB","US","UY","UZ","VU","VA","VE","VN","YE","ZM","ZW",
18
+ ];
19
+
20
+ export function getCountryNames(locale = "en"): string[] {
21
+ try {
22
+ const dn = new Intl.DisplayNames([locale], { type: "region" });
23
+ return ISO_ALPHA2.map((c) => dn.of(c) || c)
24
+ .filter((n): n is string => !!n)
25
+ .sort((a, b) => a.localeCompare(b, locale));
26
+ } catch {
27
+ // Intl.DisplayNames unavailable — fall back to the raw codes
28
+ return [...ISO_ALPHA2].sort();
29
+ }
30
+ }