@tekkare/romulus 0.1.210 → 0.1.212
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/dist/runtime/components/Dashboard.vue +290 -0
- package/dist/runtime/components/DashboardWidget.vue +381 -0
- package/dist/runtime/components/DashboardWidgetEditor.vue +474 -0
- package/dist/runtime/components/LineChart.vue +13 -1
- package/dist/runtime/components/ViewPill.vue +2 -2
- package/dist/runtime/composables/useRmDashboard.d.ts +42 -0
- package/dist/runtime/composables/useRmDashboard.js +52 -0
- package/dist/runtime/composables/useRmExport.js +7 -1
- package/dist/runtime/utils/chart.d.ts +10 -0
- package/dist/runtime/utils/chart.js +2 -1
- package/dist/runtime/utils/dashboard.d.ts +232 -0
- package/dist/runtime/utils/dashboard.js +167 -0
- package/package.json +5 -3
|
@@ -229,6 +229,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
229
229
|
legendPosition = "bottom",
|
|
230
230
|
startAtZero = false,
|
|
231
231
|
valueFormatter,
|
|
232
|
+
labelFormatter,
|
|
232
233
|
tooltipFormatter
|
|
233
234
|
} = options;
|
|
234
235
|
const labelled = valueLabels ?? series.length <= RM_LINE_LABEL_MAX_SERIES;
|
|
@@ -310,7 +311,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
310
311
|
backgroundColor: color,
|
|
311
312
|
padding: [3, 5],
|
|
312
313
|
borderRadius: 2,
|
|
313
|
-
formatter: (p) => p.value === null || p.value === void 0 ? "" : format(p.value)
|
|
314
|
+
formatter: (p) => p.value === null || p.value === void 0 ? "" : labelFormatter ? labelFormatter(p.value, p.dataIndex, i) : format(p.value)
|
|
314
315
|
} : void 0,
|
|
315
316
|
lineStyle: { color, width: 2.5 },
|
|
316
317
|
itemStyle: { color },
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tableaux de bord a widgets (RmDashboard) : le modele enregistre, le
|
|
3
|
+
* contrat d'un widget et les regles pures que la coque, l'editeur et
|
|
4
|
+
* l'export partagent.
|
|
5
|
+
*
|
|
6
|
+
* Un widget n'est pas une requete libre : c'est une entree d'un catalogue
|
|
7
|
+
* (`RmWidgetKind`) que l'app declare, reglee par quelques parametres et
|
|
8
|
+
* affichee sous une des visualisations qu'elle permet. L'entree sait aller
|
|
9
|
+
* chercher sa donnee et la rendre sous une des quatre formes normalisees
|
|
10
|
+
* (series, parts, chiffres cles, lignes) ; le paquet ne connait que ces
|
|
11
|
+
* formes, jamais la source. C'est ce qui rend le tableau de bord deployable
|
|
12
|
+
* dans toute app : elle n'ecrit que son catalogue et deux appels d'API.
|
|
13
|
+
*
|
|
14
|
+
* Les marques d'un widget sont celles du contexte par defaut (le projet, la
|
|
15
|
+
* vue...) : un produit ajoute plus tard apparait sans retoucher le tableau.
|
|
16
|
+
* Une liste explicite dans `params.brands` fige le choix.
|
|
17
|
+
*/
|
|
18
|
+
import type { RmExportSheet, RmExportSource } from './export.js';
|
|
19
|
+
export type RmDashboardLang = 'fr' | 'en';
|
|
20
|
+
export type RmDashboardViz = 'line' | 'bar' | 'table' | 'kpi' | 'donut' | 'cards';
|
|
21
|
+
export interface RmDashboardLayout {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
w: number;
|
|
25
|
+
h: number;
|
|
26
|
+
}
|
|
27
|
+
/** Un widget tel qu'il s'enregistre : une entree du catalogue, ses reglages, sa place. */
|
|
28
|
+
export interface RmDashboardWidget {
|
|
29
|
+
id: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
viz: RmDashboardViz;
|
|
32
|
+
/** Surcharge du titre de l'entree. */
|
|
33
|
+
title?: string;
|
|
34
|
+
params: Record<string, unknown>;
|
|
35
|
+
layout: RmDashboardLayout;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Le tableau enregistre, envoye en entier a chaque geste. L'API ne connait
|
|
39
|
+
* pas le catalogue : un `kind` disparu de l'app est ignore a l'affichage et
|
|
40
|
+
* garde sa place dans l'enregistrement.
|
|
41
|
+
*/
|
|
42
|
+
export interface RmDashboardModel {
|
|
43
|
+
version: 1;
|
|
44
|
+
widgets: RmDashboardWidget[];
|
|
45
|
+
}
|
|
46
|
+
export interface RmWidgetSeriesData {
|
|
47
|
+
type: 'series';
|
|
48
|
+
categories: string[];
|
|
49
|
+
series: {
|
|
50
|
+
name: string;
|
|
51
|
+
data: (number | null)[];
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
export interface RmWidgetPartsData {
|
|
55
|
+
type: 'parts';
|
|
56
|
+
parts: {
|
|
57
|
+
name: string;
|
|
58
|
+
value: number;
|
|
59
|
+
}[];
|
|
60
|
+
}
|
|
61
|
+
export interface RmWidgetKpiData {
|
|
62
|
+
type: 'kpi';
|
|
63
|
+
items: {
|
|
64
|
+
label: string;
|
|
65
|
+
value: number;
|
|
66
|
+
previous: number | null;
|
|
67
|
+
trend?: number;
|
|
68
|
+
}[];
|
|
69
|
+
/** « juin 2026 vs juin 2025 » : la periode lue, que la tuile doit dire. */
|
|
70
|
+
period: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `optional` : colonne proposee mais masquee tant qu'on ne la coche pas.
|
|
74
|
+
* `long` : texte de longueur imprevisible (une indication), coupe a trois
|
|
75
|
+
* lignes avec « Lire plus », sur une colonne assez large pour que trois
|
|
76
|
+
* lignes disent quelque chose.
|
|
77
|
+
*/
|
|
78
|
+
export interface RmWidgetColumn {
|
|
79
|
+
key: string;
|
|
80
|
+
label: string;
|
|
81
|
+
numeric?: boolean;
|
|
82
|
+
date?: boolean;
|
|
83
|
+
optional?: boolean;
|
|
84
|
+
long?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Une liste de documents (avis, autorisations, prix). */
|
|
87
|
+
export interface RmWidgetRowsData {
|
|
88
|
+
type: 'rows';
|
|
89
|
+
columns: RmWidgetColumn[];
|
|
90
|
+
rows: Record<string, string | number | null>[];
|
|
91
|
+
/**
|
|
92
|
+
* Lecture en cartes : `kind` nomme le rendu que l'app fournit par le slot
|
|
93
|
+
* `#cards` de RmDashboard, `items` a la forme que ce rendu attend.
|
|
94
|
+
*/
|
|
95
|
+
cards?: {
|
|
96
|
+
kind: string;
|
|
97
|
+
items: unknown[];
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export type RmWidgetData = (RmWidgetSeriesData | RmWidgetPartsData | RmWidgetKpiData | RmWidgetRowsData) & {
|
|
101
|
+
/** Unite des valeurs : « € », ou rien pour un nombre de boites. */
|
|
102
|
+
unit?: string;
|
|
103
|
+
/** Avertissement a poser sous le contenu (troncature, marque sans donnee). */
|
|
104
|
+
note?: string;
|
|
105
|
+
};
|
|
106
|
+
export interface RmWidgetOption {
|
|
107
|
+
value: string;
|
|
108
|
+
label: Record<RmDashboardLang, string>;
|
|
109
|
+
}
|
|
110
|
+
export interface RmWidgetParam {
|
|
111
|
+
key: string;
|
|
112
|
+
label: Record<RmDashboardLang, string>;
|
|
113
|
+
options: RmWidgetOption[];
|
|
114
|
+
}
|
|
115
|
+
/** Ce que l'ecran donne a ses widgets. Une app l'etend avec ce qui lui est propre. */
|
|
116
|
+
export interface RmWidgetContext {
|
|
117
|
+
/** Marques retenues pour ce widget, deja filtrees par `rmWidgetBrands`. */
|
|
118
|
+
brands: string[];
|
|
119
|
+
lang: RmDashboardLang;
|
|
120
|
+
}
|
|
121
|
+
export interface RmWidgetKind<C extends RmWidgetContext = RmWidgetContext> {
|
|
122
|
+
kind: string;
|
|
123
|
+
/**
|
|
124
|
+
* Jeux cites en pied de carte, par identifiant de source (uuid Directus
|
|
125
|
+
* dans les apps OIP). Vide : la donnee est celle de l'ecran lui-meme.
|
|
126
|
+
*/
|
|
127
|
+
sources: string[];
|
|
128
|
+
/** Groupe du catalogue, pour le tiroir d'ajout. */
|
|
129
|
+
group: Record<RmDashboardLang, string>;
|
|
130
|
+
title: Record<RmDashboardLang, string>;
|
|
131
|
+
description: Record<RmDashboardLang, string>;
|
|
132
|
+
/** Visualisations permises, la premiere par defaut. */
|
|
133
|
+
viz: RmDashboardViz[];
|
|
134
|
+
/** Le widget lit les marques : il propose le choix des marques. */
|
|
135
|
+
usesBrands: boolean;
|
|
136
|
+
params: RmWidgetParam[];
|
|
137
|
+
/** Taille de depart dans la grille de 12 colonnes. */
|
|
138
|
+
size: {
|
|
139
|
+
w: number;
|
|
140
|
+
h: number;
|
|
141
|
+
};
|
|
142
|
+
/** Taille en dessous de laquelle le widget ne se lit plus. */
|
|
143
|
+
min?: {
|
|
144
|
+
w: number;
|
|
145
|
+
h: number;
|
|
146
|
+
};
|
|
147
|
+
fetch: (params: Record<string, string>, context: C) => Promise<RmWidgetData>;
|
|
148
|
+
}
|
|
149
|
+
/** La source d'un widget, telle que la carte la cite et que l'export la reprend. */
|
|
150
|
+
export interface RmWidgetSource {
|
|
151
|
+
name: string;
|
|
152
|
+
verified: string | Date | number;
|
|
153
|
+
export?: RmExportSource[];
|
|
154
|
+
}
|
|
155
|
+
export declare const RM_VIZ_LABELS: Record<RmDashboardViz, Record<RmDashboardLang, string>>;
|
|
156
|
+
/** L'icone d'une visualisation, pour le catalogue. */
|
|
157
|
+
export declare const RM_VIZ_ICONS: Record<RmDashboardViz, string>;
|
|
158
|
+
/** Taille en dessous de laquelle aucune tuile ne se lit, a defaut de `min` sur l'entree. */
|
|
159
|
+
export declare const RM_WIDGET_MIN: {
|
|
160
|
+
w: number;
|
|
161
|
+
h: number;
|
|
162
|
+
};
|
|
163
|
+
export declare function rmNewWidgetId(): string;
|
|
164
|
+
export declare function rmWidgetKind<C extends RmWidgetContext>(catalog: RmWidgetKind<C>[], kind: string): RmWidgetKind<C> | undefined;
|
|
165
|
+
/** Les parametres d'un widget completes par les valeurs par defaut de son entree. */
|
|
166
|
+
export declare function rmWidgetParams(entry: RmWidgetKind<any>, params: Record<string, unknown>): Record<string, string>;
|
|
167
|
+
/** Les marques du widget : celles du contexte, sauf liste figee dans ses reglages. */
|
|
168
|
+
export declare function rmWidgetBrands(widget: Pick<RmDashboardWidget, 'params'>, contextBrands: string[]): string[];
|
|
169
|
+
/** La visualisation du widget, ramenee a celles que son entree permet. */
|
|
170
|
+
export declare function rmWidgetViz(widget: Pick<RmDashboardWidget, 'viz'>, entry: RmWidgetKind<any>): RmDashboardViz;
|
|
171
|
+
export declare function rmWidgetTitle(widget: Pick<RmDashboardWidget, 'title'>, entry: RmWidgetKind<any>, lang: RmDashboardLang): string;
|
|
172
|
+
/**
|
|
173
|
+
* Les colonnes affichees d'une liste : celles cochees dans les reglages, dans
|
|
174
|
+
* l'ordre choisi, sinon celles que l'entree montre par defaut (les `optional`
|
|
175
|
+
* restent masquees). Une colonne cochee qui n'existe plus (autre regroupement)
|
|
176
|
+
* est ignoree ; s'il n'en reste aucune, on revient au defaut plutot qu'a un
|
|
177
|
+
* tableau vide. Le widget, l'editeur et l'export lisent la meme regle.
|
|
178
|
+
*/
|
|
179
|
+
export declare function rmShownColumns(columns: RmWidgetColumn[], chosen: unknown): RmWidgetColumn[];
|
|
180
|
+
/** Une serie par marque, alignee sur toutes les periodes rencontrees, dans l'ordre des marques. */
|
|
181
|
+
export declare function rmSeriesByBrand(rows: {
|
|
182
|
+
brand: string;
|
|
183
|
+
period: string;
|
|
184
|
+
value: number | null;
|
|
185
|
+
}[], brands: string[]): RmWidgetSeriesData;
|
|
186
|
+
/**
|
|
187
|
+
* Le reglage « Periode ». « Tout l'historique » en premier : c'est la valeur
|
|
188
|
+
* par defaut, celle des widgets poses avant que le reglage existe.
|
|
189
|
+
*/
|
|
190
|
+
export declare function rmYearsParam(values: string[]): RmWidgetParam;
|
|
191
|
+
/**
|
|
192
|
+
* Les N dernieres annees d'une serie, comptees depuis sa derniere periode et
|
|
193
|
+
* pas depuis l'horloge : une source en retard garde une fenetre pleine. En
|
|
194
|
+
* mensuel, N x 12 mois ; en annuel, N millesimes. Une serie qui n'a plus
|
|
195
|
+
* rien dans la fenetre disparait.
|
|
196
|
+
*/
|
|
197
|
+
export declare function rmLastYears(data: RmWidgetSeriesData, years: string | undefined, monthly: boolean): RmWidgetSeriesData;
|
|
198
|
+
/**
|
|
199
|
+
* Le premier jour retenu par une liste datee sur N ans, ou rien. Une liste de
|
|
200
|
+
* documents ne compare pas deux periodes : la fenetre se compte depuis
|
|
201
|
+
* aujourd'hui.
|
|
202
|
+
*/
|
|
203
|
+
export declare function rmSince(years: string | undefined, today?: Date): string | null;
|
|
204
|
+
/** Les lignes datees depuis `from` ; sans fenetre, toutes, lignes sans date comprises. */
|
|
205
|
+
export declare function rmWithin<T extends Record<string, unknown>>(rows: T[], key: string, from: string | null): T[];
|
|
206
|
+
/** Le message de troncature, ou rien : jamais un graphe partiel sans le dire. */
|
|
207
|
+
export declare function rmTruncationNote(shown: number, total: number, lang: RmDashboardLang): string | undefined;
|
|
208
|
+
/** Les marques demandees qui ne rendent rien, dites plutot que tues. */
|
|
209
|
+
export declare function rmMissingNote(brands: string[], found: Set<string>, lang: RmDashboardLang): string | undefined;
|
|
210
|
+
export declare function rmJoinNotes(...notes: (string | undefined)[]): string | undefined;
|
|
211
|
+
export declare function rmWidgetEmpty(data: RmWidgetData | null): boolean;
|
|
212
|
+
export interface RmWidgetSheet {
|
|
213
|
+
columns: RmWidgetColumn[];
|
|
214
|
+
rows: Record<string, string | number | null>[];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Le tableau d'une donnee, quelle que soit sa forme, en nombres bruts : une
|
|
218
|
+
* serie temporelle fait une ligne par periode, des parts ajoutent leur
|
|
219
|
+
* pourcentage, des chiffres cles leur valeur un an plus tot. Le widget
|
|
220
|
+
* l'affiche mis au format, l'export l'ecrit tel quel : les deux lisent ici.
|
|
221
|
+
*/
|
|
222
|
+
export declare function rmWidgetSheet(data: RmWidgetData, lang: RmDashboardLang, chosenColumns?: unknown): RmWidgetSheet;
|
|
223
|
+
/** L'onglet de classeur d'une feuille. Excel borne le nom a 31 caracteres. */
|
|
224
|
+
export declare function rmWidgetExportSheet(name: string, sheet: RmWidgetSheet): RmExportSheet;
|
|
225
|
+
/**
|
|
226
|
+
* Les onglets de l'export d'un tableau entier : un par widget charge, dans
|
|
227
|
+
* l'ordre de lecture de la grille (haut, puis gauche).
|
|
228
|
+
*/
|
|
229
|
+
export declare function rmDashboardExportSheets(widgets: RmDashboardWidget[], loaded: Record<string, {
|
|
230
|
+
title: string;
|
|
231
|
+
data: RmWidgetData;
|
|
232
|
+
} | null | undefined>, lang: RmDashboardLang): RmExportSheet[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export const RM_VIZ_LABELS = {
|
|
2
|
+
line: { fr: "Courbe", en: "Line" },
|
|
3
|
+
bar: { fr: "Barres", en: "Bars" },
|
|
4
|
+
table: { fr: "Tableau", en: "Table" },
|
|
5
|
+
kpi: { fr: "Chiffres cl\xE9s", en: "Key figures" },
|
|
6
|
+
donut: { fr: "Anneau", en: "Donut" },
|
|
7
|
+
cards: { fr: "Cartes", en: "Cards" }
|
|
8
|
+
};
|
|
9
|
+
export const RM_VIZ_ICONS = {
|
|
10
|
+
line: "ChartLine",
|
|
11
|
+
bar: "ChartBar",
|
|
12
|
+
table: "Table",
|
|
13
|
+
kpi: "Gauge",
|
|
14
|
+
donut: "ChartDonut",
|
|
15
|
+
cards: "SquaresFour"
|
|
16
|
+
};
|
|
17
|
+
export const RM_WIDGET_MIN = { w: 2, h: 3 };
|
|
18
|
+
export function rmNewWidgetId() {
|
|
19
|
+
return globalThis.crypto?.randomUUID?.() ?? `w-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
20
|
+
}
|
|
21
|
+
export function rmWidgetKind(catalog, kind) {
|
|
22
|
+
return catalog.find((entry) => entry.kind === kind);
|
|
23
|
+
}
|
|
24
|
+
export function rmWidgetParams(entry, params) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const param of entry.params) {
|
|
27
|
+
const value = params[param.key];
|
|
28
|
+
out[param.key] = typeof value === "string" && param.options.some((o) => o.value === value) ? value : param.options[0].value;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
export function rmWidgetBrands(widget, contextBrands) {
|
|
33
|
+
const chosen = widget.params.brands;
|
|
34
|
+
return Array.isArray(chosen) ? chosen : contextBrands;
|
|
35
|
+
}
|
|
36
|
+
export function rmWidgetViz(widget, entry) {
|
|
37
|
+
return entry.viz.includes(widget.viz) ? widget.viz : entry.viz[0];
|
|
38
|
+
}
|
|
39
|
+
export function rmWidgetTitle(widget, entry, lang) {
|
|
40
|
+
return widget.title?.trim() || entry.title[lang];
|
|
41
|
+
}
|
|
42
|
+
export function rmShownColumns(columns, chosen) {
|
|
43
|
+
const defaults = columns.filter((c) => !c.optional);
|
|
44
|
+
if (!Array.isArray(chosen)) return defaults;
|
|
45
|
+
const kept = chosen.flatMap((key) => columns.filter((c) => c.key === key));
|
|
46
|
+
return kept.length ? kept : defaults;
|
|
47
|
+
}
|
|
48
|
+
export function rmSeriesByBrand(rows, brands) {
|
|
49
|
+
const categories = [...new Set(rows.map((row) => row.period))].sort();
|
|
50
|
+
const series = brands.filter((brand) => rows.some((row) => row.brand === brand)).map((brand) => {
|
|
51
|
+
const byPeriod = new Map(rows.filter((row) => row.brand === brand).map((row) => [row.period, row.value]));
|
|
52
|
+
return { name: brand, data: categories.map((period) => byPeriod.get(period) ?? null) };
|
|
53
|
+
});
|
|
54
|
+
return { type: "series", categories, series };
|
|
55
|
+
}
|
|
56
|
+
export function rmYearsParam(values) {
|
|
57
|
+
return {
|
|
58
|
+
key: "years",
|
|
59
|
+
label: { fr: "P\xE9riode", en: "Period" },
|
|
60
|
+
options: [
|
|
61
|
+
{ value: "all", label: { fr: "Tout l'historique", en: "Whole history" } },
|
|
62
|
+
...values.map((value) => ({
|
|
63
|
+
value,
|
|
64
|
+
label: value === "1" ? { fr: "Derni\xE8re ann\xE9e", en: "Last year" } : { fr: `${value} derni\xE8res ann\xE9es`, en: `Last ${value} years` }
|
|
65
|
+
}))
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function rmLastYears(data, years, monthly) {
|
|
70
|
+
const n = Number(years);
|
|
71
|
+
if (!n) return data;
|
|
72
|
+
const last = data.categories[data.categories.length - 1];
|
|
73
|
+
if (!last) return data;
|
|
74
|
+
const keep = data.categories.map((period, index) => ({ period, index })).filter(
|
|
75
|
+
({ period, index }) => monthly ? index >= data.categories.length - n * 12 : Number(period) > Number(last.slice(0, 4)) - n
|
|
76
|
+
).map(({ index }) => index);
|
|
77
|
+
return {
|
|
78
|
+
type: "series",
|
|
79
|
+
categories: keep.map((i) => data.categories[i]),
|
|
80
|
+
series: data.series.map((s) => ({ ...s, data: keep.map((i) => s.data[i] ?? null) })).filter((s) => s.data.some((v) => v !== null))
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function rmSince(years, today = /* @__PURE__ */ new Date()) {
|
|
84
|
+
const n = Number(years);
|
|
85
|
+
if (!n) return null;
|
|
86
|
+
const date = new Date(today);
|
|
87
|
+
date.setFullYear(date.getFullYear() - n);
|
|
88
|
+
return date.toISOString().slice(0, 10);
|
|
89
|
+
}
|
|
90
|
+
export function rmWithin(rows, key, from) {
|
|
91
|
+
if (!from) return rows;
|
|
92
|
+
return rows.filter((row) => typeof row[key] === "string" && row[key] >= from);
|
|
93
|
+
}
|
|
94
|
+
export function rmTruncationNote(shown, total, lang) {
|
|
95
|
+
if (total <= shown) return void 0;
|
|
96
|
+
return lang === "en" ? `Truncated results: ${shown} rows out of ${total}.` : `R\xE9sultats tronqu\xE9s : ${shown} lignes sur ${total}.`;
|
|
97
|
+
}
|
|
98
|
+
export function rmMissingNote(brands, found, lang) {
|
|
99
|
+
const absent = brands.filter((brand) => !found.has(brand));
|
|
100
|
+
if (!absent.length) return void 0;
|
|
101
|
+
return lang === "en" ? `No data in this source for ${absent.join(", ")}.` : `Aucune donn\xE9e dans cette source pour ${absent.join(", ")}.`;
|
|
102
|
+
}
|
|
103
|
+
export function rmJoinNotes(...notes) {
|
|
104
|
+
const kept = notes.filter(Boolean);
|
|
105
|
+
return kept.length ? kept.join(" ") : void 0;
|
|
106
|
+
}
|
|
107
|
+
export function rmWidgetEmpty(data) {
|
|
108
|
+
if (!data) return true;
|
|
109
|
+
if (data.type === "series") return !data.series.length;
|
|
110
|
+
if (data.type === "parts") return !data.parts.length;
|
|
111
|
+
if (data.type === "rows") return !data.rows.length;
|
|
112
|
+
return !data.items.length;
|
|
113
|
+
}
|
|
114
|
+
export function rmWidgetSheet(data, lang, chosenColumns) {
|
|
115
|
+
const en = lang === "en";
|
|
116
|
+
if (data.type === "rows") return { columns: rmShownColumns(data.columns, chosenColumns), rows: data.rows };
|
|
117
|
+
if (data.type === "series") {
|
|
118
|
+
return {
|
|
119
|
+
columns: [
|
|
120
|
+
{ key: "period", label: en ? "Period" : "P\xE9riode" },
|
|
121
|
+
...data.series.map((s, i) => ({ key: `s${i}`, label: s.name, numeric: true }))
|
|
122
|
+
],
|
|
123
|
+
rows: data.categories.map((period, row) => ({
|
|
124
|
+
period,
|
|
125
|
+
...Object.fromEntries(data.series.map((s, i) => [`s${i}`, s.data[row] ?? null]))
|
|
126
|
+
}))
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (data.type === "parts") {
|
|
130
|
+
const total = data.parts.reduce((sum, p) => sum + p.value, 0);
|
|
131
|
+
return {
|
|
132
|
+
columns: [
|
|
133
|
+
{ key: "name", label: en ? "Item" : "\xC9l\xE9ment" },
|
|
134
|
+
{ key: "value", label: en ? "Value" : "Valeur", numeric: true },
|
|
135
|
+
{ key: "share", label: en ? "Share (%)" : "Part (%)", numeric: true }
|
|
136
|
+
],
|
|
137
|
+
rows: data.parts.map((p) => ({ name: p.name, value: p.value, share: total ? Math.round(p.value / total * 1e3) / 10 : null }))
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
columns: [
|
|
142
|
+
{ key: "name", label: en ? "Brand" : "Marque" },
|
|
143
|
+
{ key: "value", label: en ? "Value" : "Valeur", numeric: true },
|
|
144
|
+
{ key: "previous", label: en ? "A year earlier" : "Un an plus t\xF4t", numeric: true },
|
|
145
|
+
{ key: "trend", label: en ? "Change (%)" : "\xC9volution (%)", numeric: true }
|
|
146
|
+
],
|
|
147
|
+
rows: data.items.map((i) => ({
|
|
148
|
+
name: i.label,
|
|
149
|
+
value: i.value,
|
|
150
|
+
previous: i.previous,
|
|
151
|
+
trend: i.trend === void 0 ? null : Math.round(i.trend * 10) / 10
|
|
152
|
+
}))
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function rmWidgetExportSheet(name, sheet) {
|
|
156
|
+
return {
|
|
157
|
+
name: name.slice(0, 31),
|
|
158
|
+
schema: sheet.columns.map((c) => ({ column: c.label, value: (row) => row[c.key] ?? null })),
|
|
159
|
+
data: sheet.rows
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function rmDashboardExportSheets(widgets, loaded, lang) {
|
|
163
|
+
return [...widgets].sort((a, b) => a.layout.y - b.layout.y || a.layout.x - b.layout.x).flatMap((widget) => {
|
|
164
|
+
const entry = loaded[widget.id];
|
|
165
|
+
return entry ? [rmWidgetExportSheet(entry.title, rmWidgetSheet(entry.data, lang, widget.params.columns))] : [];
|
|
166
|
+
});
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekkare/romulus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.212",
|
|
4
4
|
"description": "Romulus Design System - Nuxt module by Tekkare",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "nuxt-module-build build",
|
|
29
29
|
"dev": "nuxi dev playground",
|
|
30
|
-
"typecheck": "nuxi typecheck"
|
|
30
|
+
"typecheck": "nuxi typecheck",
|
|
31
|
+
"test": "vitest run"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@nuxt/kit": "^3.16.0",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@nuxt/module-builder": "^0.8.4",
|
|
43
44
|
"nuxt": "^3.16.0",
|
|
44
|
-
"typescript": "^5.7.3"
|
|
45
|
+
"typescript": "^5.7.3",
|
|
46
|
+
"vitest": "^3.2.4"
|
|
45
47
|
}
|
|
46
48
|
}
|