@tekkare/romulus 0.1.111 → 0.1.113
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
CHANGED
|
@@ -78,6 +78,10 @@ const FR = {
|
|
|
78
78
|
refusal: {
|
|
79
79
|
too_broad: (n: string, max: string) =>
|
|
80
80
|
`Cette vue est trop large pour être surveillée : ${n} documents, alors qu’une alerte en suit ${max} au maximum. Resserrez le périmètre de la vue, sur une marque ou une molécule par exemple.`,
|
|
81
|
+
too_many_brands: (n: string, max: string) =>
|
|
82
|
+
`Cette vue couvre ${n} marques, alors qu'une alerte en suit ${max} au maximum. Une veille se fait sur un portefeuille ou sur des concurrents : resserrez la vue sur les marques qui vous intéressent.`,
|
|
83
|
+
too_heavy: (n: string) =>
|
|
84
|
+
`Ce périmètre demande trop de mémoire pour être suivi champ par champ (${n} Mo). Réduisez la vue, ou choisissez les champs à suivre plutôt que « n'importe quel changement ».`,
|
|
81
85
|
collapse: (n: string) =>
|
|
82
86
|
`Les résultats se sont effondrés (${n} documents restants) : c’est un incident sur les données, pas une nouvelle. Rien n’a été signalé, l’alerte reprendra au prochain passage sain.`,
|
|
83
87
|
growth: (n: string) =>
|
|
@@ -131,6 +135,10 @@ const EN = {
|
|
|
131
135
|
refusal: {
|
|
132
136
|
too_broad: (n: string, max: string) =>
|
|
133
137
|
`This view is too wide to watch: ${n} documents, where an alert tracks at most ${max}. Narrow the view, to one brand or molecule for instance.`,
|
|
138
|
+
too_many_brands: (n: string, max: string) =>
|
|
139
|
+
`This view covers ${n} brands, where an alert tracks at most ${max}. Monitoring is done on a portfolio or on competitors: narrow the view to the brands you care about.`,
|
|
140
|
+
too_heavy: (n: string) =>
|
|
141
|
+
`This scope needs too much memory to be tracked field by field (${n} MB). Narrow the view, or pick the fields to watch instead of "any change".`,
|
|
134
142
|
collapse: (n: string) =>
|
|
135
143
|
`Results collapsed (${n} documents left): that is a data incident, not news. Nothing was reported; the alert resumes on the next healthy run.`,
|
|
136
144
|
growth: (n: string) =>
|
|
@@ -169,9 +177,11 @@ const refusal = computed<string | null>(() => {
|
|
|
169
177
|
if (typeof sentence === 'string') return sentence
|
|
170
178
|
|
|
171
179
|
const count = number(props.alert.lastErrorCount ?? 0)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
// Deux plafonds se disent avec leur valeur : le nombre de documents et le
|
|
181
|
+
// nombre de marques.
|
|
182
|
+
if (kind === 'too_broad') return sentence(count, number(props.alert.maxTracked ?? 10000))
|
|
183
|
+
if (kind === 'too_many_brands') return sentence(count, number(props.alert.maxBrands ?? 20))
|
|
184
|
+
return sentence(count)
|
|
175
185
|
})
|
|
176
186
|
|
|
177
187
|
/** Les etats qui demandent une explication, pas seulement une pastille. */
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { computed, ref, watch } from 'vue'
|
|
21
21
|
import type {
|
|
22
22
|
RmAlertCadence,
|
|
23
|
+
RmAlertKind,
|
|
23
24
|
RmAlertDraft,
|
|
24
25
|
RmAlertField,
|
|
25
26
|
RmAlertTrigger,
|
|
@@ -43,6 +44,19 @@ const props = withDefaults(
|
|
|
43
44
|
canSaveScreen?: boolean
|
|
44
45
|
/** Champs surveillables de la page. Vide : seule l'apparition compte. */
|
|
45
46
|
fields?: RmAlertField[]
|
|
47
|
+
/**
|
|
48
|
+
* Marques proposees au choix, pour une alerte de produit. Elles arrivent
|
|
49
|
+
* du serveur a chaque saisie : le formulaire ne filtre pas lui-meme.
|
|
50
|
+
*/
|
|
51
|
+
brandOptions?: string[]
|
|
52
|
+
brandsLoading?: boolean
|
|
53
|
+
/** Plafond de marques, tel que l'API l'impose. */
|
|
54
|
+
maxBrands?: number
|
|
55
|
+
/**
|
|
56
|
+
* L'ecran permet de partir d'une vue. Faux depuis un endroit qui n'en a
|
|
57
|
+
* pas : le choix se reduit alors a l'alerte de produit.
|
|
58
|
+
*/
|
|
59
|
+
canWatchView?: boolean
|
|
46
60
|
/** Nombre d'elements que la vue tient a l'ecran, s'il est connu. */
|
|
47
61
|
scopeCount?: number | null
|
|
48
62
|
submitting?: boolean
|
|
@@ -64,6 +78,10 @@ const props = withDefaults(
|
|
|
64
78
|
{
|
|
65
79
|
views: () => [],
|
|
66
80
|
canSaveScreen: false,
|
|
81
|
+
brandOptions: () => [],
|
|
82
|
+
brandsLoading: false,
|
|
83
|
+
maxBrands: 20,
|
|
84
|
+
canWatchView: true,
|
|
67
85
|
fields: () => [],
|
|
68
86
|
scopeCount: null,
|
|
69
87
|
submitting: false,
|
|
@@ -83,9 +101,21 @@ const emit = defineEmits<{
|
|
|
83
101
|
* la vue surveillee doivent dire la meme chose au moment de la capture.
|
|
84
102
|
*/
|
|
85
103
|
'select-view': [id: string]
|
|
104
|
+
/** Saisie du selecteur de marques, a porter jusqu'a l'index. */
|
|
105
|
+
'search-brands': [value: string]
|
|
86
106
|
}>()
|
|
87
107
|
|
|
88
108
|
const FR = {
|
|
109
|
+
kind: 'Que surveiller',
|
|
110
|
+
kindView: 'Une vue',
|
|
111
|
+
kindViewHint: 'Un écran filtré, aussi fin que vous voulez, mais limité à cette page.',
|
|
112
|
+
kindProduct: 'Un produit',
|
|
113
|
+
kindProductHint: 'Une à {max} marques, suivies partout : prix, avis HAS, indications, ANSM, PUT, MITM, Journal officiel.',
|
|
114
|
+
brands: 'Marques surveillées',
|
|
115
|
+
brandsHint: 'Cherchez une marque et ajoutez-la. L’alerte couvre tout le hub pour chacune.',
|
|
116
|
+
brandsRequired: 'Choisissez au moins une marque.',
|
|
117
|
+
brandsTooMany: 'Une alerte suit {max} marques au maximum.',
|
|
118
|
+
productFields: 'Tous les champs sont suivis : une alerte de produit ne se restreint pas, elle rapporte tout ce qui bouge sur ces marques.',
|
|
89
119
|
screenOption: 'Écran actuel (une vue sera créée)',
|
|
90
120
|
viewSelect: 'Vue à surveiller',
|
|
91
121
|
viewApplied: 'La vue choisie est appliquée à l’écran : c’est lui qui fournit la requête surveillée.',
|
|
@@ -128,6 +158,16 @@ const FR = {
|
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
const EN = {
|
|
161
|
+
kind: 'What to watch',
|
|
162
|
+
kindView: 'A view',
|
|
163
|
+
kindViewHint: 'A filtered screen, as precise as you like, but tied to that page.',
|
|
164
|
+
kindProduct: 'A product',
|
|
165
|
+
kindProductHint: 'One to {max} brands, watched everywhere: prices, HAS opinions, indications, ANSM, PUT, MITM, official journal.',
|
|
166
|
+
brands: 'Watched brands',
|
|
167
|
+
brandsHint: 'Search a brand and add it. The alert covers the whole hub for each one.',
|
|
168
|
+
brandsRequired: 'Pick at least one brand.',
|
|
169
|
+
brandsTooMany: 'An alert watches at most {max} brands.',
|
|
170
|
+
productFields: 'Every field is watched: a product alert does not narrow down, it reports whatever moves on those brands.',
|
|
131
171
|
screenOption: 'Current screen (a view will be created)',
|
|
132
172
|
viewSelect: 'View to watch',
|
|
133
173
|
viewApplied: 'The chosen view is applied to the screen: the screen is what provides the watched query.',
|
|
@@ -189,6 +229,16 @@ const selectedView = computed(
|
|
|
189
229
|
|
|
190
230
|
const editing = computed(() => props.mode === 'edit')
|
|
191
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Ce que l'alerte surveille. Le choix ne se pose qu'a la creation : changer
|
|
234
|
+
* une alerte de vue en alerte de produit, ce n'est plus la meme alerte.
|
|
235
|
+
*/
|
|
236
|
+
const kind = ref<RmAlertKind>(
|
|
237
|
+
(props.initial?.kind as RmAlertKind) ?? (props.canWatchView ? 'view' : 'product')
|
|
238
|
+
)
|
|
239
|
+
const brands = ref<string[]>(props.initial?.brands ?? [])
|
|
240
|
+
const watchesProduct = computed(() => kind.value === 'product')
|
|
241
|
+
|
|
192
242
|
const title = ref(props.initial?.title ?? props.viewTitle)
|
|
193
243
|
const triggers = ref<RmAlertTrigger[]>(props.initial?.triggers ?? ['appeared'])
|
|
194
244
|
const watchedFields = ref<string[]>(
|
|
@@ -212,6 +262,22 @@ watch(
|
|
|
212
262
|
},
|
|
213
263
|
)
|
|
214
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Le titre suit ce qu'on surveille, tant que l'utilisateur n'y a pas touche.
|
|
267
|
+
*
|
|
268
|
+
* En passant sur « un produit », garder le nom d'une vue donnerait une alerte
|
|
269
|
+
* « Suivi prix anticoagulants » qui suit en realite Opdivo.
|
|
270
|
+
*/
|
|
271
|
+
watch([kind, brands], () => {
|
|
272
|
+
if (titleTouched.value) return
|
|
273
|
+
if (watchesProduct.value) {
|
|
274
|
+
title.value = brands.value.length ? `Veille ${brands.value.join(', ')}` : ''
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
const view = props.views.find((entry) => entry.id === selectedViewId.value)
|
|
278
|
+
title.value = view?.title ?? props.viewTitle
|
|
279
|
+
})
|
|
280
|
+
|
|
215
281
|
watch(selectedViewId, (id) => {
|
|
216
282
|
const view = props.views.find((entry) => entry.id === id)
|
|
217
283
|
if (view && !titleTouched.value) title.value = view.title
|
|
@@ -263,8 +329,19 @@ const cadenceLabel = computed(
|
|
|
263
329
|
() => cadenceOptions.value.find((option) => option.value === cadence.value)?.label ?? '',
|
|
264
330
|
)
|
|
265
331
|
|
|
332
|
+
const brandsInvalid = computed(
|
|
333
|
+
() =>
|
|
334
|
+
watchesProduct.value &&
|
|
335
|
+
(brands.value.length === 0 || brands.value.length > props.maxBrands),
|
|
336
|
+
)
|
|
337
|
+
|
|
266
338
|
const valid = computed(
|
|
267
|
-
() =>
|
|
339
|
+
() =>
|
|
340
|
+
title.value.trim().length > 0 &&
|
|
341
|
+
triggers.value.length > 0 &&
|
|
342
|
+
!brandsInvalid.value &&
|
|
343
|
+
// Une alerte de produit suit tout : la question des champs ne se pose pas.
|
|
344
|
+
(watchesProduct.value || !missingFields.value),
|
|
268
345
|
)
|
|
269
346
|
|
|
270
347
|
/**
|
|
@@ -286,15 +363,21 @@ function submit() {
|
|
|
286
363
|
if (!valid.value) return
|
|
287
364
|
emit('submit', {
|
|
288
365
|
title: title.value.trim(),
|
|
289
|
-
|
|
366
|
+
kind: kind.value,
|
|
367
|
+
brands: watchesProduct.value ? brands.value : [],
|
|
368
|
+
viewId: watchesProduct.value ? null : selectedViewId.value,
|
|
290
369
|
triggers: triggers.value,
|
|
291
370
|
// Les champs ne servent qu'au declencheur « changement » : les garder
|
|
292
371
|
// sans lui ferait porter a l'alerte une promesse qu'elle ne tient pas.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
:
|
|
372
|
+
// Une alerte de produit traverse sept index : une liste de champs nommes
|
|
373
|
+
// n'y voudrait rien dire, ils different d'un index a l'autre.
|
|
374
|
+
watchedFields: watchesProduct.value
|
|
375
|
+
? [RM_ALERT_ANY_FIELD]
|
|
376
|
+
: watchesChanges.value
|
|
377
|
+
? anyField.value
|
|
378
|
+
? [RM_ALERT_ANY_FIELD]
|
|
379
|
+
: watchedFields.value
|
|
380
|
+
: [],
|
|
298
381
|
channels: emailOn.value ? ['in_app', 'email'] : ['in_app'],
|
|
299
382
|
cadence: cadence.value,
|
|
300
383
|
// Le partage viendra plus tard : le schema le porte deja, l'ecran non.
|
|
@@ -315,7 +398,49 @@ function submit() {
|
|
|
315
398
|
"
|
|
316
399
|
/>
|
|
317
400
|
|
|
318
|
-
<
|
|
401
|
+
<fieldset v-if="!editing && canWatchView" class="rm-alert-form__group">
|
|
402
|
+
<legend class="rm-alert-form__label">{{ T.kind }}</legend>
|
|
403
|
+
<p class="rm-alert-form__hint">
|
|
404
|
+
{{
|
|
405
|
+
watchesProduct
|
|
406
|
+
? T.kindProductHint.replace('{max}', String(maxBrands))
|
|
407
|
+
: T.kindViewHint
|
|
408
|
+
}}
|
|
409
|
+
</p>
|
|
410
|
+
<RmSegmented
|
|
411
|
+
v-model="kind"
|
|
412
|
+
block
|
|
413
|
+
variant="soft"
|
|
414
|
+
:options="[
|
|
415
|
+
{ value: 'view', label: T.kindView },
|
|
416
|
+
{ value: 'product', label: T.kindProduct },
|
|
417
|
+
]"
|
|
418
|
+
/>
|
|
419
|
+
</fieldset>
|
|
420
|
+
|
|
421
|
+
<fieldset v-if="watchesProduct" class="rm-alert-form__group">
|
|
422
|
+
<legend class="rm-alert-form__label">{{ T.brands }}</legend>
|
|
423
|
+
<p class="rm-alert-form__hint">{{ T.brandsHint }}</p>
|
|
424
|
+
<RmMultipleSelect
|
|
425
|
+
v-model="brands"
|
|
426
|
+
label=""
|
|
427
|
+
search
|
|
428
|
+
:filter-search="false"
|
|
429
|
+
:options="brandOptions"
|
|
430
|
+
:loading="brandsLoading"
|
|
431
|
+
:show-amount="true"
|
|
432
|
+
:lang="lang"
|
|
433
|
+
@search="emit('search-brands', $event)"
|
|
434
|
+
/>
|
|
435
|
+
<p v-if="brands.length > maxBrands" class="rm-alert-form__error">
|
|
436
|
+
{{ T.brandsTooMany.replace('{max}', String(maxBrands)) }}
|
|
437
|
+
</p>
|
|
438
|
+
<p v-else-if="brands.length === 0" class="rm-alert-form__hint">
|
|
439
|
+
{{ T.brandsRequired }}
|
|
440
|
+
</p>
|
|
441
|
+
</fieldset>
|
|
442
|
+
|
|
443
|
+
<div v-else-if="!editing" class="rm-alert-form__view">
|
|
319
444
|
<p class="rm-alert-form__label">{{ T.view }}</p>
|
|
320
445
|
<p class="rm-alert-form__hint">
|
|
321
446
|
<!-- L'avertissement sur l'application ne vaut que pour une vue deja
|
|
@@ -344,9 +469,9 @@ function submit() {
|
|
|
344
469
|
<p v-if="!selectedViewId" class="rm-alert-form__hint">{{ T.screenNote }}</p>
|
|
345
470
|
</div>
|
|
346
471
|
|
|
347
|
-
<p v-else class="rm-alert-form__view-name">
|
|
348
|
-
<RmIcon name="BookmarkSimple" :size="14" />
|
|
349
|
-
{{ T.
|
|
472
|
+
<p v-else-if="editing" class="rm-alert-form__view-name">
|
|
473
|
+
<RmIcon :name="watchesProduct ? 'Tag' : 'BookmarkSimple'" :size="14" />
|
|
474
|
+
{{ watchesProduct ? `${T.brands} : ${brands.join(', ')}` : `${T.view} : ${viewTitle}` }}
|
|
350
475
|
</p>
|
|
351
476
|
|
|
352
477
|
<fieldset class="rm-alert-form__group">
|
|
@@ -366,7 +491,11 @@ function submit() {
|
|
|
366
491
|
/>
|
|
367
492
|
</fieldset>
|
|
368
493
|
|
|
369
|
-
<
|
|
494
|
+
<p v-if="watchesProduct && watchesChanges" class="rm-alert-form__hint">
|
|
495
|
+
{{ T.productFields }}
|
|
496
|
+
</p>
|
|
497
|
+
|
|
498
|
+
<fieldset v-if="watchesChanges && !watchesProduct" class="rm-alert-form__group">
|
|
370
499
|
<legend class="rm-alert-form__label">{{ T.fields }}</legend>
|
|
371
500
|
<p class="rm-alert-form__hint">{{ anyField ? T.anyFieldHint : T.fieldsHint }}</p>
|
|
372
501
|
<template v-if="fields.length">
|
|
@@ -54,6 +54,8 @@ export interface RmAlert {
|
|
|
54
54
|
lastErrorCount?: number | null;
|
|
55
55
|
/** Plafond de documents qu'une alerte peut suivre. */
|
|
56
56
|
maxTracked?: number | null;
|
|
57
|
+
/** Plafond de marques distinctes qu'une alerte peut suivre. */
|
|
58
|
+
maxBrands?: number | null;
|
|
57
59
|
lastRunAt?: string | null;
|
|
58
60
|
/** Date de la derniere remise a zero de la reference, s'il y en a eu une. */
|
|
59
61
|
reseededAt?: string | null;
|
|
@@ -94,9 +96,19 @@ export interface RmAlertViewOption {
|
|
|
94
96
|
id: string;
|
|
95
97
|
title: string;
|
|
96
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Ce qu'une alerte surveille.
|
|
101
|
+
*
|
|
102
|
+
* `view` fige le perimetre d'un ecran : fin, mais lie a une page. `product`
|
|
103
|
+
* nomme des marques et vaut pour tout le hub, sans dependre d'aucune page.
|
|
104
|
+
*/
|
|
105
|
+
export type RmAlertKind = 'view' | 'product';
|
|
97
106
|
/** Ce que le formulaire de creation rend a l'appelant. */
|
|
98
107
|
export interface RmAlertDraft {
|
|
99
108
|
title: string;
|
|
109
|
+
kind: RmAlertKind;
|
|
110
|
+
/** Marques surveillees, pour une alerte de produit. */
|
|
111
|
+
brands: string[];
|
|
100
112
|
/**
|
|
101
113
|
* Vue a surveiller. `null` : l'ecran actuel, que l'appelant enregistrera
|
|
102
114
|
* comme vue avant de creer l'alerte.
|