@tekkare/romulus 0.1.38 → 0.1.40
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/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/shell.css +1 -1
- package/dist/runtime/components/Button.vue +13 -4
- package/dist/runtime/components/ChartCard.vue +74 -16
- package/dist/runtime/components/ExportPanel.vue +5 -2
- package/dist/runtime/components/FilterDropdown.vue +56 -5
- package/dist/runtime/components/HybridTable.vue +111 -46
- package/dist/runtime/components/Kpi.vue +3 -0
- package/dist/runtime/components/NumberInput.vue +93 -0
- package/dist/runtime/components/PageHeader.vue +39 -5
- package/dist/runtime/components/RailPanel.vue +14 -0
- package/dist/runtime/components/SidebarLeft.vue +38 -7
- package/dist/runtime/components/SidebarRight.vue +11 -2
- package/dist/runtime/components/SimpleBar.vue +48 -57
- package/dist/runtime/components/Table.vue +1 -1
- package/dist/runtime/composables/useRmExport.js +218 -74
- package/dist/runtime/utils/chart.d.ts +28 -9
- package/dist/runtime/utils/chart.js +20 -9
- package/dist/runtime/utils/export.d.ts +48 -3
- package/dist/runtime/utils/export.js +9 -4
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RM_EXPORT_EXTENSIONS } from "../utils/export.js";
|
|
2
2
|
const BRAND_TITLE = "OIP Healthcare";
|
|
3
|
-
const BRAND_LINE = "
|
|
3
|
+
const BRAND_LINE = "By Tekkare \u{1F49C}";
|
|
4
|
+
const COVER_SHEET = "Tekkare";
|
|
4
5
|
function longDate(lang) {
|
|
5
6
|
return (/* @__PURE__ */ new Date()).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
6
7
|
year: "numeric",
|
|
@@ -16,6 +17,19 @@ function columnsOf(sheet, keep) {
|
|
|
16
17
|
const kept = sheet.schema.filter((column) => keep.includes(column.column));
|
|
17
18
|
return kept.length ? kept : sheet.schema;
|
|
18
19
|
}
|
|
20
|
+
const XLSX_CELL_MAX = 32767;
|
|
21
|
+
function xlsxSafeText(text) {
|
|
22
|
+
return text.length > XLSX_CELL_MAX ? `${text.slice(0, XLSX_CELL_MAX - 1)}\u2026` : text;
|
|
23
|
+
}
|
|
24
|
+
function xlsxColumnsOf(sheet, keep) {
|
|
25
|
+
return columnsOf(sheet, keep).map((column) => ({
|
|
26
|
+
...column,
|
|
27
|
+
value: (row) => {
|
|
28
|
+
const value = column.value(row);
|
|
29
|
+
return typeof value === "string" ? xlsxSafeText(value) : value;
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
19
33
|
function cellText(value, lang) {
|
|
20
34
|
if (value === null || value === void 0) return "";
|
|
21
35
|
if (value instanceof Date) {
|
|
@@ -76,63 +90,101 @@ async function raster(markup, width, height, scale = 2) {
|
|
|
76
90
|
function printable(text) {
|
|
77
91
|
return text.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
|
|
78
92
|
}
|
|
93
|
+
function cardsOf(payload, config, lang) {
|
|
94
|
+
if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ?? 100);
|
|
95
|
+
const sheet = payload.sheets[0];
|
|
96
|
+
if (!sheet) return [];
|
|
97
|
+
const pick = (name) => name ? sheet.schema.find((column) => column.column === name) : void 0;
|
|
98
|
+
const title = pick(sheet.titleColumn);
|
|
99
|
+
const subtitle = pick(sheet.subtitleColumn);
|
|
100
|
+
const columns = columnsOf(sheet, config.columns);
|
|
101
|
+
const head = title ?? columns[0];
|
|
102
|
+
if (!head) return [];
|
|
103
|
+
const fields = columns.filter((column) => column !== head && column !== subtitle);
|
|
104
|
+
return rowsOf(sheet, payload.cardLimit ?? 100).map((row) => ({
|
|
105
|
+
title: cellText(head.value(row), lang) || "\u2014",
|
|
106
|
+
subtitle: subtitle ? cellText(subtitle.value(row), lang) : void 0,
|
|
107
|
+
fields: fields.map((column) => ({ label: column.column, value: cellText(column.value(row), lang) })).filter((field) => field.value !== "")
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
79
110
|
export function useRmExport() {
|
|
80
111
|
function coverSheet(payload, config, lang) {
|
|
81
|
-
const
|
|
112
|
+
const COLUMNS = 40;
|
|
113
|
+
const TEXT_COLUMN = 7;
|
|
114
|
+
const KPI_COLUMNS = [6, 8];
|
|
115
|
+
const base = {
|
|
116
|
+
borderColor: "#FFFFFF",
|
|
117
|
+
align: "center",
|
|
118
|
+
alignVertical: "center",
|
|
119
|
+
fontFamily: "Figtree"
|
|
120
|
+
};
|
|
121
|
+
const empty = { value: "", borderColor: "#FFFFFF" };
|
|
122
|
+
const filler = () => Array.from({ length: COLUMNS }, () => ({ value: "", borderColor: "#FFFFFF", width: 20, height: 30 }));
|
|
123
|
+
const valueStyle = { ...base, fontSize: 18, fontWeight: "bold" };
|
|
124
|
+
const labelStyle = { ...base, fontSize: 18, color: "#b2b2b2", fontStyle: "italic" };
|
|
82
125
|
const rows = [];
|
|
83
|
-
const
|
|
126
|
+
const spread = (cells) => rows.push(Array.from({ length: COLUMNS }, (_, index) => cells[index] ?? empty));
|
|
127
|
+
const line = (value, style = {}) => spread({ [TEXT_COLUMN]: { ...base, value: xlsxSafeText(String(value ?? "")), ...style } });
|
|
128
|
+
rows.push(filler(), filler(), filler());
|
|
84
129
|
if (config.options.includes("logo")) {
|
|
85
|
-
line(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
line(payload.title, { fontSize: 20, fontWeight: "bold" });
|
|
90
|
-
if (payload.subtitle) line(payload.subtitle, { fontSize: 13, color: "#6C6C8A" });
|
|
91
|
-
line(
|
|
92
|
-
lang === "en" ? `Generated on ${longDate(lang)}` : `G\xE9n\xE9r\xE9 le ${longDate(lang)}`,
|
|
93
|
-
{ fontSize: 11, color: "#6C6C8A" }
|
|
94
|
-
);
|
|
95
|
-
rows.push([]);
|
|
96
|
-
if (payload.kpi?.length && config.sections.includes("kpi")) {
|
|
97
|
-
line(lang === "en" ? "Key figures" : "Chiffres cl\xE9s", { fontSize: 13, fontWeight: "bold" });
|
|
98
|
-
payload.kpi.forEach(
|
|
99
|
-
(kpi) => rows.push([
|
|
100
|
-
{ ...center, value: kpi.label, fontSize: 11, color: "#6C6C8A", width: 34 },
|
|
101
|
-
{ ...center, value: String(kpi.value), fontSize: 11, fontWeight: "bold", width: 24 }
|
|
102
|
-
])
|
|
130
|
+
line(
|
|
131
|
+
lang === "en" ? `This document was generated on ${longDate(lang)} by` : `Ce document a \xE9t\xE9 g\xE9n\xE9r\xE9 le ${longDate(lang)} par`,
|
|
132
|
+
{ fontSize: 16 }
|
|
103
133
|
);
|
|
104
|
-
|
|
134
|
+
line(BRAND_TITLE, { fontSize: 72, fontWeight: "bold", width: 60 });
|
|
135
|
+
line(BRAND_LINE, { fontSize: 24, width: 60 });
|
|
136
|
+
} else {
|
|
137
|
+
line(payload.title, { fontSize: 24, fontWeight: "bold" });
|
|
105
138
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
]
|
|
139
|
+
rows.push(filler());
|
|
140
|
+
const kpi = config.sections.includes("kpi") ? payload.kpi ?? [] : [];
|
|
141
|
+
if (kpi.length) {
|
|
142
|
+
line(lang === "en" ? "Your export currently contains :" : "Votre export contient :", { fontSize: 18 });
|
|
143
|
+
rows.push(filler());
|
|
144
|
+
for (let index = 0; index < kpi.length; index += 2) {
|
|
145
|
+
const pair = [kpi[index], kpi[index + 1]];
|
|
146
|
+
spread(
|
|
147
|
+
Object.fromEntries(
|
|
148
|
+
pair.map((item, side) => [
|
|
149
|
+
KPI_COLUMNS[side],
|
|
150
|
+
item ? { ...valueStyle, value: String(item.value) } : empty
|
|
151
|
+
])
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
spread(
|
|
155
|
+
Object.fromEntries(
|
|
156
|
+
pair.map((item, side) => [
|
|
157
|
+
KPI_COLUMNS[side],
|
|
158
|
+
item ? { ...labelStyle, value: String(item.label) } : empty
|
|
159
|
+
])
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
rows.push(filler());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (config.options.includes("logo")) line(payload.title, { fontSize: 20, fontWeight: "bold" });
|
|
166
|
+
if (payload.subtitle) line(payload.subtitle, { fontSize: 16 });
|
|
167
|
+
const filters = config.sections.includes("filters") ? payload.filters ?? [] : [];
|
|
168
|
+
if (filters.length) {
|
|
169
|
+
rows.push(filler());
|
|
170
|
+
filters.forEach(
|
|
171
|
+
(filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
|
|
113
172
|
);
|
|
114
|
-
rows.push([]);
|
|
115
173
|
}
|
|
116
174
|
const withSources = config.sections.includes("sources") || config.options.includes("sources");
|
|
117
175
|
if (payload.sources?.length && withSources) {
|
|
118
|
-
|
|
176
|
+
rows.push(filler());
|
|
119
177
|
payload.sources.forEach((source) => {
|
|
120
178
|
const date = source.date ? new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
121
179
|
year: "numeric",
|
|
122
180
|
month: "long",
|
|
123
181
|
day: "numeric"
|
|
124
182
|
}) : null;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{
|
|
128
|
-
...center,
|
|
129
|
-
value: date ? lang === "en" ? `Last update: ${date}` : `Derni\xE8re mise \xE0 jour : ${date}` : "",
|
|
130
|
-
fontSize: 11,
|
|
131
|
-
color: "#6C6C8A"
|
|
132
|
-
}
|
|
133
|
-
]);
|
|
183
|
+
const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
|
|
184
|
+
line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
|
|
134
185
|
});
|
|
135
186
|
}
|
|
187
|
+
for (let index = 0; index < 15; index += 1) rows.push(filler());
|
|
136
188
|
return rows;
|
|
137
189
|
}
|
|
138
190
|
async function toXlsx(config, payload, lang) {
|
|
@@ -141,13 +193,13 @@ export function useRmExport() {
|
|
|
141
193
|
const sheets = payload.sheets;
|
|
142
194
|
const data = [coverSheet(payload, config, lang)];
|
|
143
195
|
const schema = [null];
|
|
144
|
-
const names = [
|
|
196
|
+
const names = [COVER_SHEET];
|
|
145
197
|
let rowCount = 0;
|
|
146
198
|
sheets.forEach((sheet) => {
|
|
147
199
|
const rows = rowsOf(sheet, payload.limit);
|
|
148
200
|
rowCount += rows.length;
|
|
149
201
|
data.push(rows);
|
|
150
|
-
schema.push(
|
|
202
|
+
schema.push(xlsxColumnsOf(sheet, config.columns));
|
|
151
203
|
names.push(sheet.name.replace(/[[\]:*?/\\]/g, " ").slice(0, 31));
|
|
152
204
|
});
|
|
153
205
|
await writeXlsxFile(data, { schema, sheets: names, fileName, stickyRowsCount: 1 });
|
|
@@ -234,7 +286,12 @@ export function useRmExport() {
|
|
|
234
286
|
const { default: JsPdf } = await import("jspdf");
|
|
235
287
|
const { default: autoTable } = await import("jspdf-autotable");
|
|
236
288
|
const landscape = config.layout === "a4-landscape";
|
|
237
|
-
const doc = new JsPdf({
|
|
289
|
+
const doc = new JsPdf({
|
|
290
|
+
orientation: landscape ? "landscape" : "portrait",
|
|
291
|
+
unit: "mm",
|
|
292
|
+
format: "a4",
|
|
293
|
+
compress: true
|
|
294
|
+
});
|
|
238
295
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
239
296
|
const margin = 14;
|
|
240
297
|
const inner = pageWidth - margin * 2;
|
|
@@ -248,10 +305,10 @@ export function useRmExport() {
|
|
|
248
305
|
doc.setTextColor(255, 255, 255);
|
|
249
306
|
doc.setFont("helvetica", "bold");
|
|
250
307
|
doc.setFontSize(22);
|
|
251
|
-
doc.text(BRAND_TITLE, margin, 20);
|
|
308
|
+
doc.text(printable(BRAND_TITLE), margin, 20);
|
|
252
309
|
doc.setFont("helvetica", "normal");
|
|
253
310
|
doc.setFontSize(11);
|
|
254
|
-
doc.text(BRAND_LINE, margin, 28);
|
|
311
|
+
doc.text(printable(BRAND_LINE), margin, 28);
|
|
255
312
|
doc.setFontSize(9);
|
|
256
313
|
doc.text(longDate(lang), pageWidth - margin, 28, { align: "right" });
|
|
257
314
|
if (payload.logoDataUrl) {
|
|
@@ -332,47 +389,134 @@ export function useRmExport() {
|
|
|
332
389
|
doc.addImage(canvas.toDataURL("image/png"), "PNG", margin, y, drawWidth, drawHeight, void 0, "FAST");
|
|
333
390
|
y += drawHeight + 6;
|
|
334
391
|
}
|
|
392
|
+
let rowCount = 0;
|
|
393
|
+
if (config.sections.includes("cards")) {
|
|
394
|
+
const cards = cardsOf(payload, config, lang);
|
|
395
|
+
rowCount = cards.length;
|
|
396
|
+
const labelWidth = Math.min(46, inner * 0.34);
|
|
397
|
+
const valueWidth = inner - labelWidth - 8;
|
|
398
|
+
const pageHeight = doc.internal.pageSize.getHeight();
|
|
399
|
+
cards.forEach((card) => {
|
|
400
|
+
const titleLines = doc.splitTextToSize(printable(card.title), inner - 8);
|
|
401
|
+
const subtitleLines = card.subtitle ? doc.splitTextToSize(printable(card.subtitle), inner - 8) : [];
|
|
402
|
+
const fields = card.fields.map((field) => ({
|
|
403
|
+
label: doc.splitTextToSize(printable(field.label), labelWidth),
|
|
404
|
+
value: doc.splitTextToSize(printable(field.value), valueWidth)
|
|
405
|
+
}));
|
|
406
|
+
const height = 6 + titleLines.length * 5 + (subtitleLines.length ? subtitleLines.length * 4 + 1 : 0) + (card.badges?.length ? 6 : 0) + fields.reduce((total2, field) => total2 + Math.max(field.label.length, field.value.length) * 4.2, 0) + 5;
|
|
407
|
+
if (y + height > pageHeight - margin) {
|
|
408
|
+
doc.addPage();
|
|
409
|
+
y = margin;
|
|
410
|
+
}
|
|
411
|
+
doc.setDrawColor(232, 232, 240);
|
|
412
|
+
doc.setFillColor(252, 252, 254);
|
|
413
|
+
doc.roundedRect(margin, y, inner, height, 2, 2, "FD");
|
|
414
|
+
let cursor = y + 6;
|
|
415
|
+
doc.setTextColor(...ink);
|
|
416
|
+
doc.setFont("helvetica", "bold");
|
|
417
|
+
doc.setFontSize(11);
|
|
418
|
+
doc.text(titleLines, margin + 4, cursor);
|
|
419
|
+
cursor += titleLines.length * 5;
|
|
420
|
+
if (subtitleLines.length) {
|
|
421
|
+
doc.setTextColor(...grey);
|
|
422
|
+
doc.setFont("helvetica", "normal");
|
|
423
|
+
doc.setFontSize(9);
|
|
424
|
+
doc.text(subtitleLines, margin + 4, cursor + 1);
|
|
425
|
+
cursor += subtitleLines.length * 4 + 1;
|
|
426
|
+
}
|
|
427
|
+
if (card.badges?.length) {
|
|
428
|
+
let badgeX = margin + 4;
|
|
429
|
+
doc.setFontSize(7);
|
|
430
|
+
card.badges.forEach((badge) => {
|
|
431
|
+
const text = printable(badge);
|
|
432
|
+
const width = doc.getTextWidth(text) + 4;
|
|
433
|
+
doc.setFillColor(240, 238, 255);
|
|
434
|
+
doc.roundedRect(badgeX, cursor - 2, width, 5, 1.2, 1.2, "F");
|
|
435
|
+
doc.setTextColor(...violet);
|
|
436
|
+
doc.text(text, badgeX + 2, cursor + 1.6);
|
|
437
|
+
badgeX += width + 2;
|
|
438
|
+
});
|
|
439
|
+
cursor += 6;
|
|
440
|
+
}
|
|
441
|
+
fields.forEach((field) => {
|
|
442
|
+
const lines = Math.max(field.label.length, field.value.length);
|
|
443
|
+
doc.setTextColor(...grey);
|
|
444
|
+
doc.setFont("helvetica", "bold");
|
|
445
|
+
doc.setFontSize(7);
|
|
446
|
+
doc.text(field.label, margin + 4, cursor + 1);
|
|
447
|
+
doc.setTextColor(...ink);
|
|
448
|
+
doc.setFont("helvetica", "normal");
|
|
449
|
+
doc.setFontSize(9);
|
|
450
|
+
doc.text(field.value, margin + 4 + labelWidth, cursor + 1);
|
|
451
|
+
cursor += lines * 4.2;
|
|
452
|
+
});
|
|
453
|
+
y += height + 4;
|
|
454
|
+
});
|
|
455
|
+
const total = payload.sheets[0]?.data.length ?? 0;
|
|
456
|
+
if (total > cards.length) {
|
|
457
|
+
doc.setTextColor(...grey);
|
|
458
|
+
doc.setFont("helvetica", "italic");
|
|
459
|
+
doc.setFontSize(8);
|
|
460
|
+
doc.text(
|
|
461
|
+
printable(
|
|
462
|
+
lang === "en" ? `${cards.length} of ${total} records shown. Export to Excel for the full set.` : `${cards.length} fiches sur ${total}. Passez par l'export Excel pour la totalit\xE9.`
|
|
463
|
+
),
|
|
464
|
+
margin,
|
|
465
|
+
y + 2
|
|
466
|
+
);
|
|
467
|
+
y += 6;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
335
470
|
if (config.sections.includes("table")) {
|
|
336
|
-
|
|
471
|
+
const MAX_COLUMNS = 8;
|
|
337
472
|
payload.sheets.forEach((sheet) => {
|
|
338
|
-
const
|
|
473
|
+
const all = columnsOf(sheet, config.columns);
|
|
474
|
+
const columns = all.slice(0, MAX_COLUMNS);
|
|
339
475
|
const rows = rowsOf(sheet, payload.limit);
|
|
340
|
-
rowCount += rows.length;
|
|
476
|
+
if (!config.sections.includes("cards")) rowCount += rows.length;
|
|
477
|
+
doc.addPage("a4", "landscape");
|
|
478
|
+
const wide = doc.internal.pageSize.getWidth() - margin * 2;
|
|
479
|
+
doc.setTextColor(...ink);
|
|
480
|
+
doc.setFont("helvetica", "bold");
|
|
481
|
+
doc.setFontSize(11);
|
|
482
|
+
doc.text(printable(sheet.name), margin, margin + 2);
|
|
483
|
+
if (all.length > columns.length) {
|
|
484
|
+
doc.setFont("helvetica", "italic");
|
|
485
|
+
doc.setFontSize(8);
|
|
486
|
+
doc.setTextColor(...grey);
|
|
487
|
+
doc.text(
|
|
488
|
+
printable(
|
|
489
|
+
lang === "en" ? `${columns.length} of ${all.length} columns. The Excel export carries them all.` : `${columns.length} colonnes sur ${all.length}. L'export Excel les porte toutes.`
|
|
490
|
+
),
|
|
491
|
+
margin,
|
|
492
|
+
margin + 7
|
|
493
|
+
);
|
|
494
|
+
}
|
|
341
495
|
autoTable(doc, {
|
|
342
|
-
startY:
|
|
496
|
+
startY: margin + 10,
|
|
343
497
|
head: [columns.map((column) => printable(column.column))],
|
|
344
498
|
body: rows.map((row) => columns.map((column) => printable(cellText(column.value(row), lang)))),
|
|
345
|
-
styles: { fontSize:
|
|
346
|
-
headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold" },
|
|
499
|
+
styles: { fontSize: 6.5, cellPadding: 1.4, overflow: "linebreak", textColor: ink, minCellWidth: wide / columns.length / 2 },
|
|
500
|
+
headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold", valign: "middle" },
|
|
347
501
|
alternateRowStyles: { fillColor: [250, 250, 253] },
|
|
348
|
-
margin: { left: margin, right: margin },
|
|
349
|
-
|
|
350
|
-
if (payload.sheets.length > 1) {
|
|
351
|
-
doc.setTextColor(...grey);
|
|
352
|
-
doc.setFont("helvetica", "bold");
|
|
353
|
-
doc.setFontSize(8);
|
|
354
|
-
doc.text(printable(sheet.name), margin, margin - 4);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
502
|
+
margin: { left: margin, right: margin, top: margin, bottom: margin },
|
|
503
|
+
tableWidth: wide
|
|
357
504
|
});
|
|
358
|
-
y = doc.lastAutoTable?.finalY ??
|
|
505
|
+
y = doc.lastAutoTable?.finalY ?? margin;
|
|
359
506
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const fileName2 = `${config.fileName}.pdf`;
|
|
370
|
-
const blob2 = doc.output("blob");
|
|
371
|
-
return { fileName: fileName2, rows: rowCount, bytes: download(blob2, fileName2) };
|
|
507
|
+
}
|
|
508
|
+
const withSources = config.sections.includes("sources") || config.options.includes("sources");
|
|
509
|
+
if (payload.sources?.length && withSources) {
|
|
510
|
+
doc.setTextColor(...grey);
|
|
511
|
+
doc.setFont("helvetica", "normal");
|
|
512
|
+
doc.setFontSize(8);
|
|
513
|
+
const names = payload.sources.map((source) => `${source.name}${source.date ? ` (${new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", { year: "numeric", month: "long", day: "numeric" })})` : ""}`).join(" \xB7 ");
|
|
514
|
+
const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
|
|
515
|
+
doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
|
|
372
516
|
}
|
|
373
517
|
const fileName = `${config.fileName}.pdf`;
|
|
374
518
|
const blob = doc.output("blob");
|
|
375
|
-
return { fileName, rows:
|
|
519
|
+
return { fileName, rows: rowCount, bytes: download(blob, fileName) };
|
|
376
520
|
}
|
|
377
521
|
async function toPermalink(payload) {
|
|
378
522
|
const url = payload.permalink || (typeof window !== "undefined" ? window.location.href : "");
|
|
@@ -94,6 +94,31 @@ export declare function rmBaseOption(theme: RmChartTheme, decal?: boolean): {
|
|
|
94
94
|
borderWidth: number;
|
|
95
95
|
};
|
|
96
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Axe de valeurs, avec son libelle d'unite au-dessus quand il est fourni.
|
|
99
|
+
*
|
|
100
|
+
* `hidden` masque la graduation mais GARDE les lignes de fond : c'est le seul
|
|
101
|
+
* axe qu'on a le droit de cacher, et uniquement quand chaque point porte deja
|
|
102
|
+
* sa valeur en etiquette. Retirer aussi les lignes de fond ferait perdre toute
|
|
103
|
+
* echelle de comparaison entre deux courbes.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* `formatter` s'applique AUSSI a la graduation, pas seulement a l'infobulle :
|
|
107
|
+
* un axe qui affiche 400 000 000 la ou l'infobulle dit 400 M donne deux
|
|
108
|
+
* lectures du meme nombre sur le meme graphe.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Couleur d'une serie a l'indice `index`.
|
|
112
|
+
*
|
|
113
|
+
* La palette categorielle compte six teintes ; au-dela, un simple modulo
|
|
114
|
+
* redonne EXACTEMENT la meme couleur a deux series et rend la legende
|
|
115
|
+
* inutilisable. Chaque tour de palette eclaircit donc la teinte d'un cran :
|
|
116
|
+
* la septieme serie reprend la premiere couleur, en plus clair.
|
|
117
|
+
*
|
|
118
|
+
* Ce n'est pas un remplacement du double encodage : au-dela d'une douzaine de
|
|
119
|
+
* series, un graphe de lignes ne se lit plus, quelle que soit la palette.
|
|
120
|
+
*/
|
|
121
|
+
export declare function rmSeriesColor(palette: string[], index: number): string;
|
|
97
122
|
export interface RmSeriesInput {
|
|
98
123
|
name?: string;
|
|
99
124
|
data: (number | null)[];
|
|
@@ -389,10 +414,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
389
414
|
};
|
|
390
415
|
axisLabel: {
|
|
391
416
|
show: boolean;
|
|
392
|
-
fontSize?: undefined;
|
|
393
|
-
color?: undefined;
|
|
394
|
-
fontFamily?: undefined;
|
|
395
417
|
} | {
|
|
418
|
+
formatter?: ((value: number) => string) | undefined;
|
|
396
419
|
fontSize: number;
|
|
397
420
|
color: string;
|
|
398
421
|
fontFamily: string;
|
|
@@ -432,10 +455,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
432
455
|
};
|
|
433
456
|
axisLabel: {
|
|
434
457
|
show: boolean;
|
|
435
|
-
fontSize?: undefined;
|
|
436
|
-
color?: undefined;
|
|
437
|
-
fontFamily?: undefined;
|
|
438
458
|
} | {
|
|
459
|
+
formatter?: ((value: number) => string) | undefined;
|
|
439
460
|
fontSize: number;
|
|
440
461
|
color: string;
|
|
441
462
|
fontFamily: string;
|
|
@@ -467,10 +488,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
467
488
|
};
|
|
468
489
|
axisLabel: {
|
|
469
490
|
show: boolean;
|
|
470
|
-
fontSize?: undefined;
|
|
471
|
-
color?: undefined;
|
|
472
|
-
fontFamily?: undefined;
|
|
473
491
|
} | {
|
|
492
|
+
formatter?: ((value: number) => string) | undefined;
|
|
474
493
|
fontSize: number;
|
|
475
494
|
color: string;
|
|
476
495
|
fontFamily: string;
|
|
@@ -90,13 +90,24 @@ export function rmBaseOption(theme, decal = false) {
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
export function rmSeriesColor(palette, index) {
|
|
94
|
+
const base = palette[index % palette.length];
|
|
95
|
+
const cycle = Math.floor(index / palette.length);
|
|
96
|
+
if (cycle === 0 || !base.startsWith("#")) return base;
|
|
97
|
+
const hex = base.slice(1);
|
|
98
|
+
const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
|
|
99
|
+
const channels = [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16));
|
|
100
|
+
const ratio = Math.min(cycle * 0.28, 0.7);
|
|
101
|
+
const mixed = channels.map((value) => Math.round(value + (255 - value) * ratio));
|
|
102
|
+
return "#" + mixed.map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
103
|
+
}
|
|
104
|
+
const valueAxis = (theme, label, hidden = false, formatter) => ({
|
|
94
105
|
type: "value",
|
|
95
106
|
name: hidden ? void 0 : label,
|
|
96
107
|
nameLocation: "end",
|
|
97
108
|
nameGap: 12,
|
|
98
109
|
nameTextStyle: { fontSize: 10, color: theme.dimmedColor, fontFamily: FONT, align: "left" },
|
|
99
|
-
axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT },
|
|
110
|
+
axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT, ...formatter ? { formatter } : {} },
|
|
100
111
|
axisLine: hidden ? { show: false } : { lineStyle: { color: theme.axisLine } },
|
|
101
112
|
axisTick: hidden ? { show: false } : void 0,
|
|
102
113
|
splitLine: { lineStyle: { color: theme.splitLine } }
|
|
@@ -117,7 +128,7 @@ export function rmBarVerticalOption(categories, series, theme, palette, decal =
|
|
|
117
128
|
name: s.name,
|
|
118
129
|
stack: s.stack,
|
|
119
130
|
data: s.data,
|
|
120
|
-
itemStyle: { color: s.color ?? palette
|
|
131
|
+
itemStyle: { color: s.color ?? rmSeriesColor(palette, i), borderRadius: [3, 3, 0, 0] }
|
|
121
132
|
}))
|
|
122
133
|
};
|
|
123
134
|
}
|
|
@@ -131,7 +142,7 @@ export function rmBarHorizontalOption(labels, values, theme, palette, decal = fa
|
|
|
131
142
|
type: "bar",
|
|
132
143
|
data: values,
|
|
133
144
|
itemStyle: {
|
|
134
|
-
color: (params) => palette
|
|
145
|
+
color: (params) => rmSeriesColor(palette, params.dataIndex),
|
|
135
146
|
borderRadius: [0, 3, 3, 0]
|
|
136
147
|
},
|
|
137
148
|
label: {
|
|
@@ -192,13 +203,13 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
192
203
|
},
|
|
193
204
|
xAxis: { type: "category", data: categories, ...axisCommon(theme), splitLine: { show: false }, boundaryGap: false },
|
|
194
205
|
yAxis: dual ? [
|
|
195
|
-
{ ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
|
|
206
|
+
{ ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
|
|
196
207
|
// Le second axe ne rejoue pas la grille : deux grilles superposees
|
|
197
208
|
// rendent le fond illisible.
|
|
198
|
-
{ ...valueAxis(theme, axisLabels?.[1], hideValueAxis), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
|
|
199
|
-
] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
|
|
209
|
+
{ ...valueAxis(theme, axisLabels?.[1], hideValueAxis, valueFormatter), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
|
|
210
|
+
] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
|
|
200
211
|
series: series.map((s, i) => {
|
|
201
|
-
const color = s.color ?? palette
|
|
212
|
+
const color = s.color ?? rmSeriesColor(palette, i);
|
|
202
213
|
return {
|
|
203
214
|
type: "line",
|
|
204
215
|
name: s.name,
|
|
@@ -256,7 +267,7 @@ export function rmDonutOption(data, theme, palette, decal = false) {
|
|
|
256
267
|
data: data.map((d, i) => ({
|
|
257
268
|
value: d.value,
|
|
258
269
|
name: d.name,
|
|
259
|
-
itemStyle: { color: palette
|
|
270
|
+
itemStyle: { color: rmSeriesColor(palette, i) }
|
|
260
271
|
})),
|
|
261
272
|
label: { fontSize: 11, color: theme.mutedColor, fontFamily: FONT }
|
|
262
273
|
}]
|
|
@@ -27,6 +27,15 @@ export interface RmExportSheet {
|
|
|
27
27
|
name: string;
|
|
28
28
|
schema: RmExportColumn[];
|
|
29
29
|
data: any[];
|
|
30
|
+
/**
|
|
31
|
+
* Colonne qui identifie une ligne, par son intitule. Elle sert de titre aux
|
|
32
|
+
* fiches du rapport PDF. Sans elle, le titre retombe sur la premiere colonne
|
|
33
|
+
* du schema, qui est souvent la date : une fiche titree « 12 mars 2025 » ne
|
|
34
|
+
* dit pas de quel produit elle parle.
|
|
35
|
+
*/
|
|
36
|
+
titleColumn?: string;
|
|
37
|
+
/** Colonne posee sous le titre de la fiche (le laboratoire, la date...). */
|
|
38
|
+
subtitleColumn?: string;
|
|
30
39
|
}
|
|
31
40
|
/** Chiffre cle repris sur la page de garde. */
|
|
32
41
|
export interface RmExportKpi {
|
|
@@ -44,6 +53,21 @@ export interface RmExportFilter {
|
|
|
44
53
|
label: string;
|
|
45
54
|
value: string;
|
|
46
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Une fiche : ce que la page montre sous forme de carte, rendu tel quel dans
|
|
58
|
+
* le rapport PDF. Une page qui n'en declare pas en obtient quand meme, les
|
|
59
|
+
* fiches etant alors derivees des lignes du tableau (cf. useRmExport).
|
|
60
|
+
*/
|
|
61
|
+
export interface RmExportCard {
|
|
62
|
+
title: string;
|
|
63
|
+
subtitle?: string;
|
|
64
|
+
/** Etiquettes courtes posees sous le titre (SMR, statut, pays...). */
|
|
65
|
+
badges?: string[];
|
|
66
|
+
fields: {
|
|
67
|
+
label: string;
|
|
68
|
+
value: string;
|
|
69
|
+
}[];
|
|
70
|
+
}
|
|
47
71
|
/**
|
|
48
72
|
* Un format d'export.
|
|
49
73
|
*
|
|
@@ -64,15 +88,25 @@ export interface RmExportFormat {
|
|
|
64
88
|
tabular?: boolean;
|
|
65
89
|
/** Le format rend les graphiques. */
|
|
66
90
|
visual?: boolean;
|
|
67
|
-
/** Le format est mis en page :
|
|
91
|
+
/** Le format est mis en page : choix A4 et fiches detaillees. */
|
|
68
92
|
paged?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Le format porte un en-tete de document : page de garde, chiffres cles,
|
|
95
|
+
* filtres actifs, sources. Vrai pour le PDF et le classeur, faux pour un
|
|
96
|
+
* CSV ou une image, qui ne savent porter que leur contenu.
|
|
97
|
+
*/
|
|
98
|
+
document?: boolean;
|
|
69
99
|
/** Format annonce mais pas encore livre : la carte reste visible, grisee. */
|
|
70
100
|
disabled?: boolean;
|
|
71
101
|
/** Raison affichee sur une carte grisee. */
|
|
72
102
|
disabledHint?: string;
|
|
73
103
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Nature d'une section : elle decide de quels formats la section releve.
|
|
106
|
+
* `cards` ne vaut que pour un format mis en page, `meta` que pour un format
|
|
107
|
+
* qui porte un en-tete de document.
|
|
108
|
+
*/
|
|
109
|
+
export type RmExportSectionKind = 'table' | 'visual' | 'meta' | 'cards';
|
|
76
110
|
/** Section proposee a l'etape « Donnees ». */
|
|
77
111
|
export interface RmExportSection {
|
|
78
112
|
key: string;
|
|
@@ -142,6 +176,17 @@ export interface RmExportPayload {
|
|
|
142
176
|
sources?: RmExportSource[];
|
|
143
177
|
filters?: RmExportFilter[];
|
|
144
178
|
sheets: RmExportSheet[];
|
|
179
|
+
/**
|
|
180
|
+
* Fiches du rapport. Absentes, elles sont derivees des lignes du premier
|
|
181
|
+
* onglet : le titre vient de la premiere colonne, les champs des suivantes.
|
|
182
|
+
*/
|
|
183
|
+
cards?: RmExportCard[];
|
|
184
|
+
/**
|
|
185
|
+
* Plafond de fiches dans un rapport. Une fiche fait une dizaine de lignes :
|
|
186
|
+
* mille resultats donneraient un PDF de plusieurs centaines de pages, que
|
|
187
|
+
* personne ne lit et qu'aucune messagerie n'accepte.
|
|
188
|
+
*/
|
|
189
|
+
cardLimit?: number;
|
|
145
190
|
/**
|
|
146
191
|
* Graphiques a rasteriser ou vectoriser. Les charts Romulus sont rendus par
|
|
147
192
|
* ECharts en SVG : on recupere le noeud, pas une capture d'ecran.
|
|
@@ -12,10 +12,11 @@ export const RM_EXPORT_FORMATS = [
|
|
|
12
12
|
label: "PDF",
|
|
13
13
|
icon: "FilePdf",
|
|
14
14
|
color: "var(--color-danger)",
|
|
15
|
-
desc: "Rapport mis en page, pr\xEAt \xE0 partager.
|
|
15
|
+
desc: "Rapport mis en page, pr\xEAt \xE0 partager. Chiffres cl\xE9s, fiches, graphiques, en-t\xEAte OIP Healthcare.",
|
|
16
16
|
tabular: true,
|
|
17
17
|
visual: true,
|
|
18
|
-
paged: true
|
|
18
|
+
paged: true,
|
|
19
|
+
document: true
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
key: "xlsx",
|
|
@@ -24,7 +25,7 @@ export const RM_EXPORT_FORMATS = [
|
|
|
24
25
|
color: "var(--color-success)",
|
|
25
26
|
desc: "Donn\xE9es brutes avec page de garde. Pour analyse en local ou consolidation.",
|
|
26
27
|
tabular: true,
|
|
27
|
-
|
|
28
|
+
document: true
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
31
|
key: "csv",
|
|
@@ -61,7 +62,11 @@ export const RM_EXPORT_FORMATS = [
|
|
|
61
62
|
export const RM_EXPORT_SECTIONS = [
|
|
62
63
|
{ key: "kpi", label: "Chiffres cl\xE9s", kind: "meta", on: true },
|
|
63
64
|
{ key: "charts", label: "Graphiques", kind: "visual", on: true },
|
|
64
|
-
{ key: "
|
|
65
|
+
{ key: "cards", label: "Fiches d\xE9taill\xE9es", kind: "cards", on: true },
|
|
66
|
+
// Decochee : dans un rapport, les fiches disent la meme chose en lisible.
|
|
67
|
+
// La case reste offerte a qui veut le tableau en annexe. Elle ne concerne
|
|
68
|
+
// que le PDF — les onglets du classeur ne dependent pas d'elle.
|
|
69
|
+
{ key: "table", label: "Tableau r\xE9capitulatif", kind: "table" },
|
|
65
70
|
{ key: "filters", label: "Filtres actifs", kind: "meta", on: true },
|
|
66
71
|
{ key: "sources", label: "Sources et mises \xE0 jour", kind: "meta", on: true }
|
|
67
72
|
];
|