@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,215 @@
1
+ /**
2
+ * viewTemplateCriteria - les criteres d'un template, tels qu'ils sont ranges.
3
+ *
4
+ * Un template publie par Tekkare est une vue enregistree, et une vue
5
+ * enregistree stocke des LIBELLES : « Insuffisant », « Oncologie: true »,
6
+ * « 30 derniers jours ». Ces libelles sont ecrits dans la langue de qui a
7
+ * pose les filtres. Range tel quel, un template ecrit en francais ouvre un
8
+ * ecran vide chez un lecteur anglais.
9
+ *
10
+ * On range donc la CLE plutot que le libelle a chaque fois qu'elle existe.
11
+ * Quatre mecanismes du catalogue en dur (`~/utils/viewTemplates`) traduisent
12
+ * leur valeur, et tous les quatre la tirent de l'i18n :
13
+ *
14
+ * | mecanisme | ce qui est traduit | ce qu'on range |
15
+ * |----------------------|-----------------------------|----------------|
16
+ * | periode relative | « 30 derniers jours » | `last30` |
17
+ * | filtre booleen | « Oncologie: true » | `oncology` |
18
+ * | onglet des avis HAS | « Indications » | `indic` |
19
+ * | page Prix | le NOM du filtre | sa cle i18n |
20
+ *
21
+ * Le reste des valeurs est neutre : « Liste en sus », « Commercialisée »,
22
+ * « Brand », les marques, les codes ATC, les laboratoires. Elles viennent des
23
+ * index, sortis de sources francaises, et un lecteur anglais les voit deja
24
+ * telles quelles aujourd'hui. Rien a faire pour elles.
25
+ *
26
+ * Reste une poignee de filtres dont les valeurs existent vraiment en deux
27
+ * orthographes, parce que l'index porte un champ `_fr` et un champ `_en`
28
+ * (cf. `LANGUAGE_BOUND`). Leur paire ne vit ni dans l'i18n ni dans le code :
29
+ * la resoudre demande une requete au cluster. La v1 ne la resout pas, elle
30
+ * range la valeur telle quelle avec la langue d'ecriture, et le dialogue de
31
+ * publication previent. Le jour ou le resolveur arrive, il remplit le cote
32
+ * manquant sans toucher une seule ligne deja enregistree : c'est tout
33
+ * l'interet de ranger une forme, et pas une chaine.
34
+ */
35
+ import {
36
+ HAS_TABS,
37
+ LANGUAGE_BOUND,
38
+ PERIOD_FILTER,
39
+ PERIOD_SHORTCUTS,
40
+ TRANSLATED_FILTER_NAMES,
41
+ } from "#oip-app/views";
42
+ import { VIEW_PREFS_FILTER } from "../composables/useViews";
43
+
44
+ export { LANGUAGE_BOUND };
45
+
46
+ /** Langue d'ecriture d'un template. */
47
+ export type TemplateLang = "fr" | "en";
48
+
49
+ export interface TemplateCriteriaContext {
50
+ lang: TemplateLang;
51
+ t: (key: string) => string;
52
+ te: (key: string) => boolean;
53
+ /** Prefixe i18n des libelles de filtres de la page, sans le point final. */
54
+ namespace: string;
55
+ }
56
+
57
+ /**
58
+ * Un critere range. `kind` dit comment le relire :
59
+ * - `values` : les valeurs telles quelles, eventuellement dans les deux
60
+ * langues quand la paire est connue ;
61
+ * - `check` / `tab` : une cle i18n, dont on refait le libelle a la lecture ;
62
+ * - `period` : un raccourci de periode, qui reste glissant.
63
+ */
64
+ export interface StoredCriterion {
65
+ filter: string;
66
+ /** Cle i18n du NOM du filtre, quand c'est lui qui est traduit (page Prix). */
67
+ filterKey?: string;
68
+ kind: "values" | "check" | "tab" | "period";
69
+ /** `kind: "values"` sans paire connue. */
70
+ values?: string[];
71
+ /** `kind: "values"` avec sa paire, quand elle est connue. */
72
+ localized?: Record<TemplateLang, string[]>;
73
+ /** `kind` autre que `values` : la cle a relire. */
74
+ key?: string;
75
+ /** Langue d'ecriture, pour les valeurs qu'on ne sait pas encore traduire. */
76
+ lang?: TemplateLang;
77
+ }
78
+
79
+ // Filtres et libelles propres a l'app (periode, onglets, filtres traduits,
80
+ // filtres lies a la langue) : contrat #oip-app/views.
81
+
82
+
83
+ /** Les valeurs d'un critere, que la periode les garde en chaine ou non. */
84
+ function valuesOf(item: any): string[] {
85
+ if (Array.isArray(item?.values)) return item.values.map(String);
86
+ return item?.values !== undefined && item?.values !== null ? [String(item.values)] : [];
87
+ }
88
+
89
+ /** La cle dont la traduction vaut `label`, parmi `keys`. */
90
+ function keyOf(
91
+ keys: string[],
92
+ prefix: string,
93
+ label: string,
94
+ ctx: TemplateCriteriaContext,
95
+ ): string | null {
96
+ return (
97
+ keys.find((key) => ctx.te(`${prefix}${key}`) && ctx.t(`${prefix}${key}`) === label) ?? null
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Range les criteres d'un ecran, dans la langue de qui les a poses.
103
+ *
104
+ * Ce qui n'est reconnu par aucun des quatre mecanismes part en valeurs
105
+ * telles quelles : c'est le cas de la grande majorite, et c'est le bon
106
+ * defaut — une valeur neutre rangee comme neutre se relit a l'identique.
107
+ */
108
+ export function encodeCriteria(
109
+ criteria: any[],
110
+ ctx: TemplateCriteriaContext,
111
+ ): StoredCriterion[] {
112
+ return (criteria ?? []).map((item) => {
113
+ const filter = String(item?.filter ?? "");
114
+ const values = valuesOf(item);
115
+
116
+ // Les preferences de cartes ne sont pas un filtre : elles voyagent dans
117
+ // les criteres faute d'un autre champ libre, et ne se traduisent pas.
118
+ if (filter === VIEW_PREFS_FILTER) {
119
+ return { filter, kind: "values" as const, values };
120
+ }
121
+
122
+ // Le NOM du filtre est traduit : on range sa cle, pas son libelle.
123
+ const filterKey = TRANSLATED_FILTER_NAMES.find(
124
+ (key) => ctx.te(key) && ctx.t(key) === filter,
125
+ );
126
+ if (filterKey) {
127
+ return { filter, filterKey, kind: "values" as const, values, lang: ctx.lang };
128
+ }
129
+
130
+ // Periode relative : le raccourci reste glissant, les deux dates non.
131
+ if (filter === PERIOD_FILTER && values.length === 1) {
132
+ const key = keyOf(PERIOD_SHORTCUTS, "heart.period.", values[0]!, ctx);
133
+ if (key) return { filter, kind: "period" as const, key };
134
+ }
135
+
136
+ // Filtre booleen : la vue stocke « <libelle du filtre>: true ».
137
+ if (values.length === 1 && values[0]!.endsWith(": true")) {
138
+ const label = values[0]!.slice(0, -": true".length);
139
+ const key = `${ctx.namespace}.${filter}`;
140
+ if (ctx.te(key) && ctx.t(key) === label) {
141
+ return { filter, kind: "check" as const, key };
142
+ }
143
+ }
144
+
145
+ // Onglet des avis HAS.
146
+ if (filter === "Type" && values.length === 1) {
147
+ const key = keyOf(HAS_TABS, "heart.tabs.", values[0]!, ctx);
148
+ if (key) return { filter, kind: "tab" as const, key };
149
+ }
150
+
151
+ return {
152
+ filter,
153
+ kind: "values" as const,
154
+ values,
155
+ ...(LANGUAGE_BOUND.has(filter) && { lang: ctx.lang }),
156
+ };
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Relit des criteres ranges, dans la langue de l'ecran.
162
+ *
163
+ * La periode rend une CHAINE et non un tableau : c'est ce que `rangePicker`
164
+ * attend, et c'est ce qui la garde relative.
165
+ */
166
+ export function decodeCriteria(
167
+ stored: StoredCriterion[],
168
+ ctx: TemplateCriteriaContext,
169
+ ): Array<{ filter: string; values: string[] | string }> {
170
+ return (stored ?? []).map((item) => {
171
+ const filter = item.filterKey && ctx.te(item.filterKey)
172
+ ? ctx.t(item.filterKey)
173
+ : item.filter;
174
+
175
+ switch (item.kind) {
176
+ case "period":
177
+ return {
178
+ filter,
179
+ values: item.key && ctx.te(`heart.period.${item.key}`)
180
+ ? ctx.t(`heart.period.${item.key}`)
181
+ : (item.values?.[0] ?? ""),
182
+ };
183
+ case "check":
184
+ return {
185
+ filter,
186
+ values: [`${item.key && ctx.te(item.key) ? ctx.t(item.key) : filter}: true`],
187
+ };
188
+ case "tab":
189
+ return {
190
+ filter,
191
+ values: [
192
+ item.key && ctx.te(`heart.tabs.${item.key}`)
193
+ ? ctx.t(`heart.tabs.${item.key}`)
194
+ : (item.values?.[0] ?? ""),
195
+ ],
196
+ };
197
+ default:
198
+ return { filter, values: item.localized?.[ctx.lang] ?? item.values ?? [] };
199
+ }
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Les filtres qui resteront dans la langue d'ecriture, parmi des criteres
205
+ * ranges. Ce que le dialogue de publication annonce avant d'enregistrer.
206
+ */
207
+ export function languageBoundFilters(stored: StoredCriterion[]): string[] {
208
+ return [
209
+ ...new Set(
210
+ (stored ?? [])
211
+ .filter((item) => item.kind === "values" && !item.localized && LANGUAGE_BOUND.has(item.filter))
212
+ .map((item) => item.filter),
213
+ ),
214
+ ];
215
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * viewTemplates - les vues prêtes a l'emploi fournies par Tekkare.
3
+ *
4
+ * Un template est une vue que personne n'a enregistree : un jeu de criteres
5
+ * ecrit ici, applique comme une vue temporaire (sans identifiant, cf.
6
+ * `isTemporaryView`). L'utilisateur le demarre, le retouche, et l'enregistre
7
+ * dans ses vues s'il veut le garder. Rien ne part en base tant qu'il ne le
8
+ * demande pas.
9
+ *
10
+ * Les criteres se construisent avec la langue de l'ecran, jamais en dur :
11
+ * - les filtres tapent `status_fr` ou `status_en` selon la locale, et la vue
12
+ * stocke le libelle affiche (« Insuffisant » / « Insufficient ») ;
13
+ * - un filtre booleen (`CheckFilter`) stocke `"<libelle du filtre>: true"`,
14
+ * donc « Oncologie: true » en francais et « Oncology: true » en anglais ;
15
+ * - sur la page Prix, c'est le NOM du filtre qui est traduit.
16
+ * D'ou le contexte `{ lang, t }` passe a chaque `criteria()`.
17
+ *
18
+ * Les valeurs sont celles des index : elles ont ete relevees par agregation,
19
+ * pas devinees (cf. la page projet du vault). Une valeur inventee ne filtre
20
+ * rien et le template ouvre un ecran vide.
21
+ */
22
+
23
+ import { VIEW_TEMPLATES } from "#oip-app/views";
24
+
25
+ export type ViewTemplateTone = "primary" | "success" | "warning" | "danger" | "info";
26
+
27
+ export interface ViewTemplateContext {
28
+ lang: "fr" | "en";
29
+ t: (key: string) => string;
30
+ /**
31
+ * Une cle i18n existe-t-elle. Les templates en dur n'en ont pas besoin
32
+ * (leurs cles sont ecrites ici, donc sures) ; ceux publies depuis l'app,
33
+ * si : ils relisent une cle rangee en base, qu'un renommage a pu perimer.
34
+ */
35
+ te: (key: string) => boolean;
36
+ }
37
+
38
+ /** Un critere de vue. `values` est un tableau, sauf pour la periode. */
39
+ export interface ViewTemplateCriterion {
40
+ filter: string;
41
+ values: string[] | string;
42
+ }
43
+
44
+ export interface ViewTemplate {
45
+ /** Identifiant stable : il sert de cle de carte, jamais d'id de vue. */
46
+ id: string;
47
+ /** Chemin de la page, sans prefixe de langue ni barre finale. */
48
+ page: string;
49
+ /** Icone Phosphor de la pastille. */
50
+ icon: string;
51
+ tone?: ViewTemplateTone;
52
+ title: Record<"fr" | "en", string>;
53
+ description: Record<"fr" | "en", string>;
54
+ criteria: (ctx: ViewTemplateContext) => ViewTemplateCriterion[];
55
+ /**
56
+ * Le template se demarre par un lien profond plutot qu'en posant les
57
+ * criteres a l'ecran.
58
+ *
59
+ * Les pages du portail « Medicaments France » ne portent pas des filtres
60
+ * mais une selection de produits, et personne ne la resout dans la page :
61
+ * c'est le middleware `03.scope-from-query` qui expanse un code ATC en
62
+ * liste de marques, remplit les listes par source (`medicamBrandList`,
63
+ * `susBrandList`...) et pose la vue temporaire. Un template de panier est
64
+ * donc l'URL qui le declenche, ce qui evite d'y figer une liste de marques
65
+ * qui vieillirait a chaque nouvelle AMM.
66
+ */
67
+ deepLink?: boolean;
68
+ /**
69
+ * Publie pour l'espace du lecteur nommement, et non pour tout le monde :
70
+ * la carte le dit (« Créé pour vous »).
71
+ */
72
+ forYou?: boolean;
73
+ }
74
+
75
+ /**
76
+ * Parametre d'URL qui dit de quel template vient le perimetre pose. Le
77
+ * middleware du lien profond le recopie sur la vue temporaire, la pastille de
78
+ * la barre nomme alors le template plutot que de dire « Vue temporaire », et la
79
+ * navigation le garde avec les autres cles de perimetre (cf. useDrugsNav).
80
+ */
81
+ export const VIEW_TEMPLATE_QUERY_PARAM = "template";
82
+
83
+ /**
84
+ * Critere de vue vers parametre d'URL (template en lien profond) et
85
+ * parametres a nettoyer avant d'en poser de nouveaux : metier de l'app, cf.
86
+ * contrat #oip-app/views.
87
+ */
88
+ export { VIEW_TEMPLATE_QUERY_KEYS, VIEW_TEMPLATE_QUERY_RESET } from "#oip-app/views";
89
+
90
+ /**
91
+ * Chemin comparable : sans prefixe de langue (`/en/price`), sans barre finale.
92
+ * Les pages declarent `route.path`, qui porte les deux.
93
+ */
94
+ export function normalizePage(path: string): string {
95
+ return String(path ?? "")
96
+ .replace(/^\/(?:en|fr)(?=\/|$)/, "")
97
+ .replace(/\/$/, "");
98
+ }
99
+
100
+ /**
101
+ * Un template du catalogue par identifiant, quelle que soit sa page : la
102
+ * pastille nomme le template dont vient la vue, et la vue suit le lecteur
103
+ * d'une page du portail a l'autre alors que l'identifiant porte la page de
104
+ * depart.
105
+ */
106
+ export function viewTemplateById(id: string): ViewTemplate | undefined {
107
+ return VIEW_TEMPLATES.find((template) => template.id === id);
108
+ }
109
+
110
+ /** Les templates d'une page, dans l'ordre du catalogue. */
111
+ export function viewTemplatesFor(page: string): ViewTemplate[] {
112
+ const wanted = normalizePage(page);
113
+ return VIEW_TEMPLATES.filter(
114
+ (template) => template.page === wanted,
115
+ );
116
+ }
@@ -8,7 +8,16 @@
8
8
  // pas dans `modules` : l'app les enregistre deja avec ses options (romulus
9
9
  // sans baseStyles, ses locales), et un second enregistrement par le layer
10
10
  // les reinstallerait avec les options par defaut.
11
+ import { fileURLToPath } from "node:url";
12
+
11
13
  export default defineNuxtConfig({
14
+ // Contrat avec l'app pour ce qui lui est propre (client API, bandeau,
15
+ // tables metier des vues et des alertes). Par defaut, les valeurs neutres
16
+ // du layer ; une app pointe `#oip-app` vers son dossier `app/oip` dans son
17
+ // propre nuxt.config, et sa valeur l'emporte.
18
+ alias: {
19
+ "#oip-app": fileURLToPath(new URL("./app/oip-defaults", import.meta.url)),
20
+ },
12
21
  future: {
13
22
  compatibilityVersion: 4,
14
23
  },
@@ -146,7 +146,7 @@ export default defineEventHandler(async (event): Promise<ApiNote[]> => {
146
146
  // tout le portail. Sans sujet de cette app, la note n'est pas la sienne.
147
147
  const paths = (row.linked_subjects ?? [])
148
148
  .map((link) => link.OipSubject_subject_id)
149
- .filter((subject) => appUrls.includes(subject?.app?.prod_url))
149
+ .filter((subject) => appUrls.includes(subject?.app?.prod_url ?? ""))
150
150
  .map((subject) => (subject!.relative_path ?? "").trim())
151
151
  .filter(Boolean);
152
152
  const sources = (row.linked_sources ?? [])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.242",
3
+ "version": "0.1.244",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,9 @@
17
17
  "types": "./dist/runtime/utils/*.d.ts",
18
18
  "import": "./dist/runtime/utils/*.js"
19
19
  },
20
- "./layer": "./layer/nuxt.config.ts"
20
+ "./layer": "./layer/nuxt.config.ts",
21
+ "./layer-utils/*": "./layer/app/utils/*.ts",
22
+ "./layer-composables/*": "./layer/app/composables/*.ts"
21
23
  },
22
24
  "main": "./dist/module.cjs",
23
25
  "module": "./dist/module.mjs",