@tekkare/romulus 0.1.243 → 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/layout/alertBrandSync.vue +143 -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/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/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/oipScopeStore.ts +112 -0
- package/layer/app/utils/oipScopes.ts +376 -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 +3 -2
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Etat vide d'une page d'analyse : aucune vue n'est active, donc aucun produit
|
|
3
|
+
n'est selectionne et il n'y a rien a interroger.
|
|
4
|
+
|
|
5
|
+
Le message seul renvoyait l'utilisateur vers le tiroir de selection, meme
|
|
6
|
+
quand il avait deja des vues gardees pour cette page : il devait les aller
|
|
7
|
+
chercher dans la tuile « Mes vues » du rail alors que l'ecran lui demandait
|
|
8
|
+
d'en composer une. Les vues applicables ici sont donc listees sous le
|
|
9
|
+
message, activables d'un clic (`RmSavedViewCard`, les memes cartes que le
|
|
10
|
+
panneau du rail).
|
|
11
|
+
|
|
12
|
+
Les templates Tekkare de la page viennent ensuite : un panier pret a
|
|
13
|
+
l'emploi depanne quand l'utilisateur n'a rien garde, mais ses propres vues
|
|
14
|
+
passent devant, comme dans le panneau du rail ou l'onglet « Mes vues » est le
|
|
15
|
+
premier. Leur mise en carte et leur demarrage viennent de
|
|
16
|
+
`useViewTemplates`.
|
|
17
|
+
|
|
18
|
+
Le composant lit la page declaree par `useViews` : c'est elle qui porte le
|
|
19
|
+
magasin de scopes de l'app affichee, pas ce composant.
|
|
20
|
+
-->
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { VIEW_BRAND_FILTERS } from "#oip-app/views";
|
|
23
|
+
import { sameViewPage } from "../../utils/viewPages";
|
|
24
|
+
|
|
25
|
+
const { locale } = useI18n();
|
|
26
|
+
const route = useRoute();
|
|
27
|
+
const views = useViews();
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{ create: [] }>();
|
|
30
|
+
|
|
31
|
+
const fr = computed(() => locale.value !== "en");
|
|
32
|
+
const source = computed(() => views.current.value);
|
|
33
|
+
|
|
34
|
+
/** Page cote API : les vues y sont rangees, comme dans le panneau du rail. */
|
|
35
|
+
const page = computed(() => (source.value?.page ?? route.path).replace(/\/$/, ""));
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Le magasin de scopes de la page affichee. Un appel par acces : ces
|
|
39
|
+
* composables creent leurs refs a chaque fois, donc rien ne se garde ici.
|
|
40
|
+
*/
|
|
41
|
+
const store = () => views.scopes.value();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Chemins acceptes. Les vues des delivrances hospitalieres enregistrees par
|
|
45
|
+
* l'ancienne page de selection portent son chemin (`/doutpatient/drug/scope`)
|
|
46
|
+
* et non celui de l'analyse : sans les deux, elles disparaitraient de la liste
|
|
47
|
+
* (cf. le tiroir de selection, meme lecture).
|
|
48
|
+
*/
|
|
49
|
+
const pages = computed(() => [page.value, `${page.value}/scope`]);
|
|
50
|
+
/** La vue vaut ici : meme page, meme famille (cf. viewPages), ou ancienne page de selection. */
|
|
51
|
+
const forThisPage = (scope: any) =>
|
|
52
|
+
sameViewPage(scope.page, page.value) ||
|
|
53
|
+
pages.value.includes(String(scope.page).replace(/\/$/, ""));
|
|
54
|
+
|
|
55
|
+
const { cards: templates, start, save: saveTemplate } = useViewTemplates(() => page.value);
|
|
56
|
+
const filterLabel = useViewFilterLabels(() => source.value?.filterNamespace);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Menu des cartes (renommer, alerte, export, partager...) : le meme que dans
|
|
60
|
+
* le panneau « Mes vues », sans devoir aller l'y chercher.
|
|
61
|
+
*/
|
|
62
|
+
const {
|
|
63
|
+
menuOf,
|
|
64
|
+
onAction,
|
|
65
|
+
rename,
|
|
66
|
+
sharing,
|
|
67
|
+
shareTarget,
|
|
68
|
+
shareMembers,
|
|
69
|
+
isSuperAdmin,
|
|
70
|
+
workspaces,
|
|
71
|
+
templateNote,
|
|
72
|
+
onShareSearch,
|
|
73
|
+
confirmShare,
|
|
74
|
+
} = useViewCardActions(apply);
|
|
75
|
+
|
|
76
|
+
function onTemplateAction(key: string, id: string) {
|
|
77
|
+
if (key === "save-template") void saveTemplate(id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Les criteres qui portent des produits : cites en premier. */
|
|
81
|
+
const PRODUCT_FILTERS = VIEW_BRAND_FILTERS;
|
|
82
|
+
|
|
83
|
+
const dateFormat = computed(
|
|
84
|
+
() => new Intl.DateTimeFormat(locale.value === "en" ? "en-GB" : "fr-FR", { dateStyle: "medium" }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ce que porte la vue, valeurs comprises : les marques une a une, puis la
|
|
89
|
+
* classe ou le laboratoire qui l'ont composee. Meme lecture que les templates
|
|
90
|
+
* juste en dessous ; un nombre de produits ne disait pas lesquels.
|
|
91
|
+
*/
|
|
92
|
+
function shownCriteria(scope: any): any[] {
|
|
93
|
+
const criteria = parseViewCriteria(scope).filter(
|
|
94
|
+
(item: any) => item?.filter !== VIEW_PREFS_FILTER && viewFilledValues(item).length > 0,
|
|
95
|
+
);
|
|
96
|
+
return [
|
|
97
|
+
...criteria.filter((item: any) => PRODUCT_FILTERS.includes(item?.filter)),
|
|
98
|
+
...criteria.filter((item: any) => !PRODUCT_FILTERS.includes(item?.filter)),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const savedViews = computed(() =>
|
|
103
|
+
store()
|
|
104
|
+
.scopeList.value.filter(forThisPage)
|
|
105
|
+
.map((scope: any) => {
|
|
106
|
+
const criteria = shownCriteria(scope);
|
|
107
|
+
const created = scope.createdAt ? new Date(scope.createdAt) : null;
|
|
108
|
+
return {
|
|
109
|
+
id: scope.id,
|
|
110
|
+
title: scope.title,
|
|
111
|
+
description: scope.description,
|
|
112
|
+
// Meme pastille pour toutes les vues gardees : elles n'ont pas de
|
|
113
|
+
// sujet declare, contrairement aux templates dont l'icone dit la
|
|
114
|
+
// classe traitee. Le signet reprend celle de l'etat vide au dessus, et
|
|
115
|
+
// le ton de marque les distingue des templates, chacun dans sa
|
|
116
|
+
// couleur.
|
|
117
|
+
icon: "BookmarkSimple",
|
|
118
|
+
tone: "primary",
|
|
119
|
+
isDefault: scope.isDefault === true,
|
|
120
|
+
menu: menuOf(scope),
|
|
121
|
+
renamable: scope.isOwner !== false,
|
|
122
|
+
starred: scope.starred === true,
|
|
123
|
+
// Les vues qu'on nous a partagees s'activent ici comme les autres : la
|
|
124
|
+
// carte dit seulement d'ou elles viennent, pour qu'on ne cherche pas
|
|
125
|
+
// qui a pose ces filtres.
|
|
126
|
+
sharedBy: scope.isOwner === false ? (scope.ownerName ?? undefined) : undefined,
|
|
127
|
+
config: criteria.length
|
|
128
|
+
? [
|
|
129
|
+
{
|
|
130
|
+
icon: "Funnel",
|
|
131
|
+
key: `${fr.value ? "Filtres" : "Filters"} (${criteria.length})`,
|
|
132
|
+
pills: criteria.map((item: any) => ({
|
|
133
|
+
k: filterLabel(item.filter),
|
|
134
|
+
v: viewFilledValues(item).join(", "),
|
|
135
|
+
})),
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
: [],
|
|
139
|
+
meta:
|
|
140
|
+
created && !Number.isNaN(created.getTime())
|
|
141
|
+
? [
|
|
142
|
+
{
|
|
143
|
+
icon: "Clock",
|
|
144
|
+
label: (fr.value ? "Créée le " : "Created on ") + dateFormat.value.format(created),
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
: [],
|
|
148
|
+
};
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Le « Ou » ne se dit que s'il y a bien une premiere liste au dessus : sans
|
|
154
|
+
* vue gardee, les templates ouvrent l'ecran et l'alternative n'existe pas.
|
|
155
|
+
*/
|
|
156
|
+
const templatesLabel = computed(() => {
|
|
157
|
+
if (savedViews.value.length)
|
|
158
|
+
return fr.value ? "Ou démarrer avec une vue prête à l'emploi" : "Or start from a ready-made view";
|
|
159
|
+
return fr.value ? "Démarrer avec une vue prête à l'emploi" : "Start from a ready-made view";
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Active une vue : meme chemin que le panneau du rail (copie volontaire, les
|
|
164
|
+
* watchers de filtres suivent la reference de `selectedScope`).
|
|
165
|
+
*
|
|
166
|
+
* Le chemin de la vue est ramene a celui de la page : une vue venue de
|
|
167
|
+
* l'ancienne page de selection ne se dirait pas active ici, et la pastille
|
|
168
|
+
* resterait muette sur la vue qu'on vient d'appliquer.
|
|
169
|
+
*/
|
|
170
|
+
function apply(id: string) {
|
|
171
|
+
const scope = store().scopeList.value.find((item: any) => item.id === id);
|
|
172
|
+
if (!scope) return;
|
|
173
|
+
const applied = { ...scope, page: page.value };
|
|
174
|
+
store().setSelectedScope(applied);
|
|
175
|
+
views.applyPrefs(applied, source.value?.prefsScope);
|
|
176
|
+
source.value?.onApply?.(applied);
|
|
177
|
+
}
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<template>
|
|
181
|
+
<div class="vempty">
|
|
182
|
+
<RmEmptyState
|
|
183
|
+
icon="BookmarkSimple"
|
|
184
|
+
tone="primary"
|
|
185
|
+
framed
|
|
186
|
+
:title="fr ? 'Aucune vue active' : 'No active view'"
|
|
187
|
+
:text="fr
|
|
188
|
+
? 'Une vue définit les produits de l’analyse (marque, molécule, classe ATC4 ou fabricant). Créez-en une pour commencer, vous l’enregistrerez si elle vous sert.'
|
|
189
|
+
: 'A view defines the products the analysis covers (brand, molecule, ATC4 class or company). Create one to start; save it if it proves useful.'"
|
|
190
|
+
:action-label="fr ? 'Créer une vue' : 'Create a view'"
|
|
191
|
+
action-icon="Plus"
|
|
192
|
+
@action="emit('create')"
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
<section v-if="savedViews.length" class="vempty_saved">
|
|
196
|
+
<p class="vempty_label">
|
|
197
|
+
{{ fr ? "Reprendre une vue enregistrée" : "Resume a saved view" }}
|
|
198
|
+
</p>
|
|
199
|
+
<div class="vempty_grid">
|
|
200
|
+
<RmSavedViewCard
|
|
201
|
+
v-for="view in savedViews"
|
|
202
|
+
:key="view.id"
|
|
203
|
+
:view="view"
|
|
204
|
+
:lang="fr ? 'fr' : 'en'"
|
|
205
|
+
@activate="apply"
|
|
206
|
+
@action="onAction"
|
|
207
|
+
@rename="rename"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
<section v-if="templates.length" class="vempty_saved">
|
|
213
|
+
<p class="vempty_label">
|
|
214
|
+
{{ templatesLabel }}
|
|
215
|
+
</p>
|
|
216
|
+
<div class="vempty_grid">
|
|
217
|
+
<RmSavedViewCard
|
|
218
|
+
v-for="template in templates"
|
|
219
|
+
:key="template.id"
|
|
220
|
+
:view="template"
|
|
221
|
+
:lang="fr ? 'fr' : 'en'"
|
|
222
|
+
@activate="start"
|
|
223
|
+
@action="onTemplateAction"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
|
|
228
|
+
<RmShareDialog
|
|
229
|
+
v-model="sharing"
|
|
230
|
+
:title="shareTarget?.title ?? ''"
|
|
231
|
+
:members="shareMembers"
|
|
232
|
+
:visibility="shareTarget?.visibility ?? 'private'"
|
|
233
|
+
:shared-with="shareTarget?.sharedWith ?? []"
|
|
234
|
+
:can-template="isSuperAdmin"
|
|
235
|
+
:is-template="shareTarget?.isTemplate ?? false"
|
|
236
|
+
:target-workspaces="shareTarget?.targetWorkspaces ?? []"
|
|
237
|
+
:workspaces="workspaces"
|
|
238
|
+
:template-note="templateNote"
|
|
239
|
+
:lang="fr ? 'fr' : 'en'"
|
|
240
|
+
@search="onShareSearch"
|
|
241
|
+
@save="confirmShare"
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
</template>
|
|
245
|
+
|
|
246
|
+
<style scoped>
|
|
247
|
+
.vempty {
|
|
248
|
+
display: flex;
|
|
249
|
+
flex-direction: column;
|
|
250
|
+
gap: 24px;
|
|
251
|
+
width: 100%;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.vempty_saved {
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
gap: 12px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Mosaique : une carte de vue ne porte qu'un nom, ses criteres et une date,
|
|
261
|
+
une pleine largeur par vue faisait defiler l'ecran pour rien. A 280px, un
|
|
262
|
+
titre long partageait sa ligne avec « Demarrer » sur trois ou quatre
|
|
263
|
+
lignes. */
|
|
264
|
+
.vempty_grid {
|
|
265
|
+
display: grid;
|
|
266
|
+
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
267
|
+
gap: 12px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.vempty_label {
|
|
271
|
+
margin: 0;
|
|
272
|
+
color: var(--text-muted);
|
|
273
|
+
font-size: var(--font-11);
|
|
274
|
+
font-weight: var(--weight-semibold);
|
|
275
|
+
letter-spacing: 0.04em;
|
|
276
|
+
text-transform: uppercase;
|
|
277
|
+
}
|
|
278
|
+
</style>
|