@tekkare/romulus 0.1.36 → 0.1.38
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/Button.vue +2 -2
- package/dist/runtime/components/Callout.vue +1 -1
- package/dist/runtime/components/ExportPanel.vue +744 -0
- package/dist/runtime/components/SavedViewCard.vue +2 -2
- package/dist/runtime/components/SavedViews.vue +9 -3
- package/dist/runtime/composables/useRmExport.d.ts +29 -0
- package/dist/runtime/composables/useRmExport.js +404 -0
- package/dist/runtime/utils/export.d.ts +199 -0
- package/dist/runtime/utils/export.js +98 -0
- package/package.json +9 -2
|
@@ -204,7 +204,7 @@ function onCardClick() {
|
|
|
204
204
|
</span>
|
|
205
205
|
<RmButton
|
|
206
206
|
v-else
|
|
207
|
-
variant="primary
|
|
207
|
+
variant="primary"
|
|
208
208
|
size="sm"
|
|
209
209
|
@click="emit('activate', view.id)"
|
|
210
210
|
>
|
|
@@ -291,5 +291,5 @@ function onCardClick() {
|
|
|
291
291
|
</template>
|
|
292
292
|
|
|
293
293
|
<style scoped>
|
|
294
|
-
.rm-view{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);padding:14px 18px;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out)}.rm-view:not(.rm-view--active):hover{background:var(--bg-hover);border-color:var(--color-accent-soft)}.rm-view--active{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.08);cursor:default}.rm-view__strip{background:var(--bg-hover);border-bottom:1px solid var(--border-soft);border-radius:7px 7px 0 0;color:var(--color-secondary);font-size:var(--font-11);font-weight:var(--weight-bold);gap:var(--space-8);letter-spacing:.06em;margin:-14px -18px var(--space-12);padding:var(--space-8) var(--space-16);text-transform:uppercase}.rm-view__head,.rm-view__strip{align-items:center;display:flex}.rm-view__head{gap:10px}.rm-view__star{align-items:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;flex:0 0 auto;height:28px;justify-content:center;padding:0;width:28px}.rm-view__star:hover{background:var(--bg-hover)}.rm-view__star--on,.rm-view__star:hover{color:var(--color-warning)}.rm-view__title,.rm-view__title-edit{flex:1;font-size:var(--font-16);font-weight:var(--weight-bold);min-width:0}.rm-view__title-edit{background:var(--bg-surface);border:1px solid var(--color-secondary);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;outline:none;padding:2px var(--space-8)}.rm-view__title-edit:focus{box-shadow:0 0 0 3px rgba(var(--brand-rgb),.12)}.rm-view__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-view__badge{align-items:center;background:var(--color-success-bg);border-radius:var(--radius-pill);color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:3px var(--space-8);text-transform:uppercase}.rm-view__menu-wrap{position:relative}.rm-view__more{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;height:28px;justify-content:center;padding:0;width:28px}.rm-view__more:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-view__menu-shade{inset:0;position:fixed;z-index:60}.rm-view__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);display:flex;flex-direction:column;min-width:200px;padding:var(--space-4);position:absolute;right:0;top:calc(100% + 6px);z-index:61}.rm-view__menu-item{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;width:100%}.rm-view__menu-item:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__menu-item--danger{color:var(--color-danger)}.rm-view__menu-item--danger:hover{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__desc{color:var(--text-secondary);font-size:var(--font-13);margin:var(--space-8) 0 var(--space-12)}.rm-view__config{background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-6);margin:10px 0 var(--space-12);padding:10px 14px}.rm-view__row{align-items:flex-start;display:flex;font-size:var(--font-12);gap:14px}.rm-view__row-key{color:var(--text-muted);display:inline-flex;flex-shrink:0;font-size:var(--font-10);font-weight:var(--weight-semibold);letter-spacing:.04em;min-width:96px;padding-top:4px;text-transform:uppercase}.rm-view__row-key,.rm-view__row-val{align-items:center;gap:var(--space-6)}.rm-view__row-val{color:var(--text-primary);display:flex;flex-wrap:wrap;padding:2px 0}.rm-view__pill{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);display:inline-flex;font-size:var(--font-12);gap:var(--space-4);padding:5px 10px}.rm-view__pill-k{color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-view__pill-v{color:var(--text-primary);font-weight:var(--weight-medium)}.rm-view__foot{border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-16);padding-top:10px}.rm-view__foot span{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-view__confirm{align-items:center;background:var(--color-danger-bg);border:1px solid var(--color-danger);border-radius:var(--radius-md);color:var(--color-danger);display:flex;font-size:var(--font-13);gap:var(--space-12);margin-top:var(--space-12);padding:var(--space-8) var(--space-12)}.rm-view__confirm>span{align-items:center;display:inline-flex;flex:1;gap:var(--space-8)}.rm-view__confirm-actions{display:flex;flex-shrink:0;gap:var(--space-8)}
|
|
294
|
+
.rm-view{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);padding:14px 18px;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out)}.rm-view:not(.rm-view--active):hover{background:var(--bg-hover);border-color:var(--color-accent-soft)}.rm-view--active{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.08);cursor:default}.rm-view__strip{background:var(--bg-hover);border-bottom:1px solid var(--border-soft);border-radius:7px 7px 0 0;color:var(--color-secondary);font-size:var(--font-11);font-weight:var(--weight-bold);gap:var(--space-8);letter-spacing:.06em;margin:-14px -18px var(--space-12);padding:var(--space-8) var(--space-16);text-transform:uppercase}.rm-view__head,.rm-view__strip{align-items:center;display:flex}.rm-view__head{gap:10px}.rm-view__star{align-items:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;flex:0 0 auto;height:28px;justify-content:center;padding:0;width:28px}.rm-view__star:hover{background:var(--bg-hover)}.rm-view__star--on,.rm-view__star:hover{color:var(--color-warning)}.rm-view__title,.rm-view__title-edit{flex:1;font-size:var(--font-16);font-weight:var(--weight-bold);min-width:0}.rm-view__title-edit{background:var(--bg-surface);border:1px solid var(--color-secondary);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;outline:none;padding:2px var(--space-8)}.rm-view__title-edit:focus{box-shadow:0 0 0 3px rgba(var(--brand-rgb),.12)}.rm-view__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-view__badge{align-items:center;background:var(--color-success-bg);border-radius:var(--radius-pill);color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:3px var(--space-8);text-transform:uppercase}.rm-view__menu-wrap{position:relative}.rm-view__more{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;height:28px;justify-content:center;padding:0;width:28px}.rm-view__more:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-view__menu-shade{inset:0;position:fixed;z-index:60}.rm-view__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);display:flex;flex-direction:column;min-width:200px;padding:var(--space-4);position:absolute;right:0;top:calc(100% + 6px);z-index:61}.rm-view__menu-item{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;width:100%}.rm-view__menu-item:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__menu-item--danger{color:var(--color-danger)}.rm-view__menu-item--danger:hover{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__desc{color:var(--text-secondary);font-size:var(--font-13);margin:var(--space-8) 0 var(--space-12)}.rm-view__config{background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-6);margin:10px 0 var(--space-12);padding:10px 14px}.rm-view__row{align-items:flex-start;display:flex;flex-wrap:wrap;font-size:var(--font-12);gap:14px}.rm-view__row-key{color:var(--text-muted);display:inline-flex;flex-shrink:0;font-size:var(--font-10);font-weight:var(--weight-semibold);letter-spacing:.04em;min-width:96px;padding-top:4px;text-transform:uppercase}.rm-view__row-key,.rm-view__row-val{align-items:center;gap:var(--space-6)}.rm-view__row-val{color:var(--text-primary);display:flex;flex:1 1 200px;flex-wrap:wrap;padding:2px 0}.rm-view__pill{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);display:inline-flex;font-size:var(--font-12);gap:var(--space-4);padding:5px 10px}.rm-view__pill-k{color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-view__pill-v{color:var(--text-primary);font-weight:var(--weight-medium)}.rm-view__foot{border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-16);padding-top:10px}.rm-view__foot span{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-view__confirm{align-items:center;background:var(--color-danger-bg);border:1px solid var(--color-danger);border-radius:var(--radius-md);color:var(--color-danger);display:flex;flex-wrap:wrap;font-size:var(--font-13);gap:var(--space-12);margin-top:var(--space-12);padding:var(--space-8) var(--space-12)}.rm-view__confirm>span{align-items:center;display:inline-flex;flex:1 1 220px;gap:var(--space-8)}.rm-view__confirm-actions{display:flex;flex-shrink:0;gap:var(--space-8)}
|
|
295
295
|
</style>
|
|
@@ -49,6 +49,8 @@ const props = withDefaults(
|
|
|
49
49
|
loading?: boolean
|
|
50
50
|
/** Nom de la vue active dont la configuration a change a l'ecran. */
|
|
51
51
|
dirtyTitle?: string
|
|
52
|
+
/** Retire la recherche et les pastilles de filtre. */
|
|
53
|
+
hideFilters?: boolean
|
|
52
54
|
/** Textes de l'etat vide. */
|
|
53
55
|
emptyTitle?: string
|
|
54
56
|
emptyText?: string
|
|
@@ -67,6 +69,7 @@ const props = withDefaults(
|
|
|
67
69
|
starrable: false,
|
|
68
70
|
loading: false,
|
|
69
71
|
dirtyTitle: '',
|
|
72
|
+
hideFilters: false,
|
|
70
73
|
emptyTitle: '',
|
|
71
74
|
emptyText: '',
|
|
72
75
|
emptyActionLabel: '',
|
|
@@ -163,7 +166,7 @@ const visibleTabs = computed(() => (props.tabs.length > 1 ? props.tabs : []))
|
|
|
163
166
|
<h2 v-if="title" class="rm-views__title">{{ title }}</h2>
|
|
164
167
|
<RmButton
|
|
165
168
|
v-if="saveLabel"
|
|
166
|
-
variant="primary
|
|
169
|
+
variant="primary"
|
|
167
170
|
size="sm"
|
|
168
171
|
prefix-icon="BookmarkSimple"
|
|
169
172
|
:disabled="saveDisabled"
|
|
@@ -193,7 +196,10 @@ const visibleTabs = computed(() => (props.tabs.length > 1 ? props.tabs : []))
|
|
|
193
196
|
</button>
|
|
194
197
|
</div>
|
|
195
198
|
|
|
196
|
-
|
|
199
|
+
<!-- La recherche et les pastilles restent posees meme sans vue : la
|
|
200
|
+
reference les montre toujours, et les faire apparaitre a la premiere
|
|
201
|
+
vue enregistree ferait sauter la mise en page du panneau. -->
|
|
202
|
+
<template v-if="!hideFilters">
|
|
197
203
|
<label class="rm-views__search">
|
|
198
204
|
<RmIcon name="MagnifyingGlass" :size="14" />
|
|
199
205
|
<input v-model="search" type="text" :placeholder="T.search">
|
|
@@ -225,7 +231,7 @@ const visibleTabs = computed(() => (props.tabs.length > 1 ? props.tabs : []))
|
|
|
225
231
|
<RmButton variant="secondary" size="sm" @click="emit('save-as')">
|
|
226
232
|
{{ T.saveAs }}
|
|
227
233
|
</RmButton>
|
|
228
|
-
<RmButton variant="primary
|
|
234
|
+
<RmButton variant="primary" size="sm" @click="emit('save')">
|
|
229
235
|
{{ T.save }}
|
|
230
236
|
</RmButton>
|
|
231
237
|
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRmExport - les generateurs de fichiers derriere `RmExportPanel`.
|
|
3
|
+
*
|
|
4
|
+
* Le wizard produit une configuration, la page produit un contenu
|
|
5
|
+
* (`RmExportPayload`) ; ce composable croise les deux et rend un fichier. Il
|
|
6
|
+
* vit dans le paquet et pas dans les apps parce que l'en-tete « OIP Healthcare
|
|
7
|
+
* par Tekkare », la page de garde du classeur et la mise en page A4 doivent
|
|
8
|
+
* etre les memes partout : un export de Hub-R et un export d'une app pays ne
|
|
9
|
+
* peuvent pas se presenter differemment devant un client.
|
|
10
|
+
*
|
|
11
|
+
* Toutes les bibliotheques lourdes (write-excel-file, jsPDF) sont importees a
|
|
12
|
+
* l'appel, pas au chargement de la page : un ecran qui n'exporte rien ne paie
|
|
13
|
+
* pas leur poids.
|
|
14
|
+
*/
|
|
15
|
+
import type { RmExportConfig, RmExportPayload } from '../utils/export.js';
|
|
16
|
+
export interface RmExportResult {
|
|
17
|
+
/** Nom du fichier rendu, extension comprise. Vide pour un permalink. */
|
|
18
|
+
fileName: string;
|
|
19
|
+
/** Nombre de lignes reellement ecrites. */
|
|
20
|
+
rows: number;
|
|
21
|
+
/** Taille du fichier rendu, en octets. */
|
|
22
|
+
bytes: number;
|
|
23
|
+
/** URL, pour le format « Permalink ». */
|
|
24
|
+
url?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function useRmExport(): {
|
|
27
|
+
run: (config: RmExportConfig, payload: RmExportPayload, lang?: "fr" | "en") => Promise<RmExportResult>;
|
|
28
|
+
extensions: Record<string, string>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { RM_EXPORT_EXTENSIONS } from "../utils/export.js";
|
|
2
|
+
const BRAND_TITLE = "OIP Healthcare";
|
|
3
|
+
const BRAND_LINE = "par Tekkare";
|
|
4
|
+
function longDate(lang) {
|
|
5
|
+
return (/* @__PURE__ */ new Date()).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
6
|
+
year: "numeric",
|
|
7
|
+
month: "long",
|
|
8
|
+
day: "numeric"
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function rowsOf(sheet, limit) {
|
|
12
|
+
return limit && sheet.data.length > limit ? sheet.data.slice(0, limit) : sheet.data;
|
|
13
|
+
}
|
|
14
|
+
function columnsOf(sheet, keep) {
|
|
15
|
+
if (!keep.length) return sheet.schema;
|
|
16
|
+
const kept = sheet.schema.filter((column) => keep.includes(column.column));
|
|
17
|
+
return kept.length ? kept : sheet.schema;
|
|
18
|
+
}
|
|
19
|
+
function cellText(value, lang) {
|
|
20
|
+
if (value === null || value === void 0) return "";
|
|
21
|
+
if (value instanceof Date) {
|
|
22
|
+
return value.toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
23
|
+
year: "numeric",
|
|
24
|
+
month: "long",
|
|
25
|
+
day: "numeric"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
function download(blob, fileName) {
|
|
32
|
+
const url = URL.createObjectURL(blob);
|
|
33
|
+
const link = document.createElement("a");
|
|
34
|
+
link.href = url;
|
|
35
|
+
link.download = fileName;
|
|
36
|
+
document.body.appendChild(link);
|
|
37
|
+
link.click();
|
|
38
|
+
link.remove();
|
|
39
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e4);
|
|
40
|
+
return blob.size;
|
|
41
|
+
}
|
|
42
|
+
function svgOf(el) {
|
|
43
|
+
return el instanceof SVGSVGElement ? el : el.querySelector("svg");
|
|
44
|
+
}
|
|
45
|
+
function sizeOf(svg) {
|
|
46
|
+
const box = svg.getBoundingClientRect();
|
|
47
|
+
const width = Math.round(box.width || svg.viewBox?.baseVal?.width || 800);
|
|
48
|
+
const height = Math.round(box.height || svg.viewBox?.baseVal?.height || 400);
|
|
49
|
+
return { width, height };
|
|
50
|
+
}
|
|
51
|
+
function serialize(svg) {
|
|
52
|
+
const clone = svg.cloneNode(true);
|
|
53
|
+
const { width, height } = sizeOf(svg);
|
|
54
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
55
|
+
clone.setAttribute("width", String(width));
|
|
56
|
+
clone.setAttribute("height", String(height));
|
|
57
|
+
if (!clone.getAttribute("viewBox")) clone.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
58
|
+
return { markup: new XMLSerializer().serializeToString(clone), width, height };
|
|
59
|
+
}
|
|
60
|
+
async function raster(markup, width, height, scale = 2) {
|
|
61
|
+
const image = new Image();
|
|
62
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
image.onload = () => resolve();
|
|
65
|
+
image.onerror = () => reject(new Error("SVG illisible"));
|
|
66
|
+
});
|
|
67
|
+
const canvas = document.createElement("canvas");
|
|
68
|
+
canvas.width = width * scale;
|
|
69
|
+
canvas.height = height * scale;
|
|
70
|
+
const context = canvas.getContext("2d");
|
|
71
|
+
context.fillStyle = "#FFFFFF";
|
|
72
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
73
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
74
|
+
return canvas;
|
|
75
|
+
}
|
|
76
|
+
function printable(text) {
|
|
77
|
+
return text.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
|
|
78
|
+
}
|
|
79
|
+
export function useRmExport() {
|
|
80
|
+
function coverSheet(payload, config, lang) {
|
|
81
|
+
const center = { align: "left", alignVertical: "center", fontFamily: "Figtree" };
|
|
82
|
+
const rows = [];
|
|
83
|
+
const line = (value, style = {}) => rows.push([{ ...center, value, ...style }]);
|
|
84
|
+
if (config.options.includes("logo")) {
|
|
85
|
+
line(BRAND_TITLE, { fontSize: 36, fontWeight: "bold", height: 46 });
|
|
86
|
+
line(BRAND_LINE, { fontSize: 14, color: "#6B3FFF" });
|
|
87
|
+
rows.push([]);
|
|
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
|
+
])
|
|
103
|
+
);
|
|
104
|
+
rows.push([]);
|
|
105
|
+
}
|
|
106
|
+
if (payload.filters?.length && config.sections.includes("filters")) {
|
|
107
|
+
line(lang === "en" ? "Active filters" : "Filtres actifs", { fontSize: 13, fontWeight: "bold" });
|
|
108
|
+
payload.filters.forEach(
|
|
109
|
+
(filter) => rows.push([
|
|
110
|
+
{ ...center, value: filter.label, fontSize: 11, color: "#6C6C8A" },
|
|
111
|
+
{ ...center, value: filter.value, fontSize: 11 }
|
|
112
|
+
])
|
|
113
|
+
);
|
|
114
|
+
rows.push([]);
|
|
115
|
+
}
|
|
116
|
+
const withSources = config.sections.includes("sources") || config.options.includes("sources");
|
|
117
|
+
if (payload.sources?.length && withSources) {
|
|
118
|
+
line(lang === "en" ? "Sources" : "Sources", { fontSize: 13, fontWeight: "bold" });
|
|
119
|
+
payload.sources.forEach((source) => {
|
|
120
|
+
const date = source.date ? new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
121
|
+
year: "numeric",
|
|
122
|
+
month: "long",
|
|
123
|
+
day: "numeric"
|
|
124
|
+
}) : null;
|
|
125
|
+
rows.push([
|
|
126
|
+
{ ...center, value: `${source.emoji ? `${source.emoji} ` : ""}${source.name}`, fontSize: 11 },
|
|
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
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return rows;
|
|
137
|
+
}
|
|
138
|
+
async function toXlsx(config, payload, lang) {
|
|
139
|
+
const { default: writeXlsxFile } = await import("write-excel-file");
|
|
140
|
+
const fileName = `${config.fileName}.xlsx`;
|
|
141
|
+
const sheets = payload.sheets;
|
|
142
|
+
const data = [coverSheet(payload, config, lang)];
|
|
143
|
+
const schema = [null];
|
|
144
|
+
const names = [BRAND_TITLE];
|
|
145
|
+
let rowCount = 0;
|
|
146
|
+
sheets.forEach((sheet) => {
|
|
147
|
+
const rows = rowsOf(sheet, payload.limit);
|
|
148
|
+
rowCount += rows.length;
|
|
149
|
+
data.push(rows);
|
|
150
|
+
schema.push(columnsOf(sheet, config.columns));
|
|
151
|
+
names.push(sheet.name.replace(/[[\]:*?/\\]/g, " ").slice(0, 31));
|
|
152
|
+
});
|
|
153
|
+
await writeXlsxFile(data, { schema, sheets: names, fileName, stickyRowsCount: 1 });
|
|
154
|
+
return { fileName, rows: rowCount, bytes: 0 };
|
|
155
|
+
}
|
|
156
|
+
async function toCsv(config, payload, lang) {
|
|
157
|
+
const escape = (text) => /[",\n;]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
158
|
+
const blocks = [];
|
|
159
|
+
let rowCount = 0;
|
|
160
|
+
payload.sheets.forEach((sheet) => {
|
|
161
|
+
const columns = columnsOf(sheet, config.columns);
|
|
162
|
+
const rows = rowsOf(sheet, payload.limit);
|
|
163
|
+
rowCount += rows.length;
|
|
164
|
+
const lines = [];
|
|
165
|
+
if (payload.sheets.length > 1) lines.push(`# ${sheet.name}`);
|
|
166
|
+
lines.push(columns.map((column) => escape(column.column)).join(","));
|
|
167
|
+
rows.forEach(
|
|
168
|
+
(row) => lines.push(columns.map((column) => escape(cellText(column.value(row), lang))).join(","))
|
|
169
|
+
);
|
|
170
|
+
blocks.push(lines.join("\n"));
|
|
171
|
+
});
|
|
172
|
+
const fileName = `${config.fileName}.csv`;
|
|
173
|
+
const blob = new Blob([`\uFEFF${blocks.join("\n\n")}
|
|
174
|
+
`], { type: "text/csv;charset=utf-8" });
|
|
175
|
+
return { fileName, rows: rowCount, bytes: download(blob, fileName) };
|
|
176
|
+
}
|
|
177
|
+
async function toPng(config, payload, lang) {
|
|
178
|
+
const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
|
|
179
|
+
if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
|
|
180
|
+
const scale = 2;
|
|
181
|
+
const pad = 24;
|
|
182
|
+
const head = config.options.includes("logo") ? 72 : 0;
|
|
183
|
+
const parts = charts.map((svg) => serialize(svg));
|
|
184
|
+
const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
|
|
185
|
+
const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
|
|
186
|
+
const canvas = document.createElement("canvas");
|
|
187
|
+
canvas.width = width * scale;
|
|
188
|
+
canvas.height = height * scale;
|
|
189
|
+
const context = canvas.getContext("2d");
|
|
190
|
+
context.scale(scale, scale);
|
|
191
|
+
context.fillStyle = "#FFFFFF";
|
|
192
|
+
context.fillRect(0, 0, width, height);
|
|
193
|
+
if (head) {
|
|
194
|
+
context.fillStyle = "#1A1A2E";
|
|
195
|
+
context.font = "bold 20px Figtree, Helvetica, sans-serif";
|
|
196
|
+
context.fillText(BRAND_TITLE, pad, 32);
|
|
197
|
+
context.fillStyle = "#6C6C8A";
|
|
198
|
+
context.font = "12px Figtree, Helvetica, sans-serif";
|
|
199
|
+
context.fillText(`${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}`, pad, 52);
|
|
200
|
+
}
|
|
201
|
+
let y = head + pad;
|
|
202
|
+
for (const part of parts) {
|
|
203
|
+
const raw = await raster(part.markup, part.width, part.height, scale);
|
|
204
|
+
context.drawImage(raw, pad, y, part.width, part.height);
|
|
205
|
+
y += part.height + pad;
|
|
206
|
+
}
|
|
207
|
+
const blob = await new Promise(
|
|
208
|
+
(resolve, reject) => canvas.toBlob((result) => result ? resolve(result) : reject(new Error("PNG illisible")), "image/png")
|
|
209
|
+
);
|
|
210
|
+
const fileName = `${config.fileName}.png`;
|
|
211
|
+
return { fileName, rows: 0, bytes: download(blob, fileName) };
|
|
212
|
+
}
|
|
213
|
+
async function toSvg(config, payload, lang) {
|
|
214
|
+
const charts = (payload.charts ?? []).map((chart) => svgOf(chart.el)).filter(Boolean);
|
|
215
|
+
if (!charts.length) throw new Error(lang === "en" ? "No chart on this page" : "Aucun graphique sur cette page");
|
|
216
|
+
const pad = 24;
|
|
217
|
+
const head = config.options.includes("logo") ? 72 : 0;
|
|
218
|
+
const parts = charts.map((svg) => serialize(svg));
|
|
219
|
+
const width = Math.max(...parts.map((part) => part.width)) + pad * 2;
|
|
220
|
+
const height = parts.reduce((total, part) => total + part.height + pad, pad) + head;
|
|
221
|
+
let y = head + pad;
|
|
222
|
+
const nested = parts.map((part) => {
|
|
223
|
+
const markup2 = part.markup.replace(/^<svg /, `<svg x="${pad}" y="${y}" `);
|
|
224
|
+
y += part.height + pad;
|
|
225
|
+
return markup2;
|
|
226
|
+
}).join("\n");
|
|
227
|
+
const header = head ? `<text x="${pad}" y="32" font-family="Figtree, Helvetica, sans-serif" font-size="20" font-weight="bold" fill="#1A1A2E">${BRAND_TITLE}</text><text x="${pad}" y="52" font-family="Figtree, Helvetica, sans-serif" font-size="12" fill="#6C6C8A">${payload.title} \xB7 ${BRAND_LINE} \xB7 ${longDate(lang)}</text>` : "";
|
|
228
|
+
const markup = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" fill="#FFFFFF"/>${header}${nested}</svg>`;
|
|
229
|
+
const fileName = `${config.fileName}.svg`;
|
|
230
|
+
const blob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
|
|
231
|
+
return { fileName, rows: 0, bytes: download(blob, fileName) };
|
|
232
|
+
}
|
|
233
|
+
async function toPdf(config, payload, lang) {
|
|
234
|
+
const { default: JsPdf } = await import("jspdf");
|
|
235
|
+
const { default: autoTable } = await import("jspdf-autotable");
|
|
236
|
+
const landscape = config.layout === "a4-landscape";
|
|
237
|
+
const doc = new JsPdf({ orientation: landscape ? "landscape" : "portrait", unit: "mm", format: "a4" });
|
|
238
|
+
const pageWidth = doc.internal.pageSize.getWidth();
|
|
239
|
+
const margin = 14;
|
|
240
|
+
const inner = pageWidth - margin * 2;
|
|
241
|
+
let y = margin;
|
|
242
|
+
const violet = [107, 63, 255];
|
|
243
|
+
const ink = [26, 26, 46];
|
|
244
|
+
const grey = [108, 108, 138];
|
|
245
|
+
if (config.options.includes("logo")) {
|
|
246
|
+
doc.setFillColor(...violet);
|
|
247
|
+
doc.rect(0, 0, pageWidth, 42, "F");
|
|
248
|
+
doc.setTextColor(255, 255, 255);
|
|
249
|
+
doc.setFont("helvetica", "bold");
|
|
250
|
+
doc.setFontSize(22);
|
|
251
|
+
doc.text(BRAND_TITLE, margin, 20);
|
|
252
|
+
doc.setFont("helvetica", "normal");
|
|
253
|
+
doc.setFontSize(11);
|
|
254
|
+
doc.text(BRAND_LINE, margin, 28);
|
|
255
|
+
doc.setFontSize(9);
|
|
256
|
+
doc.text(longDate(lang), pageWidth - margin, 28, { align: "right" });
|
|
257
|
+
if (payload.logoDataUrl) {
|
|
258
|
+
const logoHeight = 8;
|
|
259
|
+
doc.addImage(payload.logoDataUrl, "PNG", pageWidth - margin - logoHeight * 7.6, 10, logoHeight * 7.6, logoHeight);
|
|
260
|
+
}
|
|
261
|
+
y = 54;
|
|
262
|
+
}
|
|
263
|
+
doc.setTextColor(...ink);
|
|
264
|
+
doc.setFont("helvetica", "bold");
|
|
265
|
+
doc.setFontSize(16);
|
|
266
|
+
doc.text(doc.splitTextToSize(printable(payload.title), inner), margin, y);
|
|
267
|
+
y += 8;
|
|
268
|
+
if (payload.subtitle) {
|
|
269
|
+
doc.setFont("helvetica", "normal");
|
|
270
|
+
doc.setFontSize(10);
|
|
271
|
+
doc.setTextColor(...grey);
|
|
272
|
+
const lines = doc.splitTextToSize(printable(payload.subtitle), inner);
|
|
273
|
+
doc.text(lines, margin, y);
|
|
274
|
+
y += lines.length * 5 + 2;
|
|
275
|
+
}
|
|
276
|
+
if (payload.kpi?.length && config.sections.includes("kpi")) {
|
|
277
|
+
const perRow = Math.min(payload.kpi.length, 4);
|
|
278
|
+
const boxWidth = inner / perRow;
|
|
279
|
+
payload.kpi.slice(0, 8).forEach((kpi, index) => {
|
|
280
|
+
const column = index % perRow;
|
|
281
|
+
const row = Math.floor(index / perRow);
|
|
282
|
+
const x = margin + column * boxWidth;
|
|
283
|
+
const top = y + row * 20;
|
|
284
|
+
doc.setFillColor(246, 246, 250);
|
|
285
|
+
doc.roundedRect(x + 1, top, boxWidth - 2, 17, 2, 2, "F");
|
|
286
|
+
doc.setTextColor(...violet);
|
|
287
|
+
doc.setFont("helvetica", "bold");
|
|
288
|
+
doc.setFontSize(14);
|
|
289
|
+
doc.text(printable(String(kpi.value)), x + boxWidth / 2, top + 8, { align: "center" });
|
|
290
|
+
doc.setTextColor(...grey);
|
|
291
|
+
doc.setFont("helvetica", "normal");
|
|
292
|
+
doc.setFontSize(7);
|
|
293
|
+
doc.text(doc.splitTextToSize(printable(kpi.label), boxWidth - 6)[0] ?? "", x + boxWidth / 2, top + 13, { align: "center" });
|
|
294
|
+
});
|
|
295
|
+
y += Math.ceil(Math.min(payload.kpi.length, 8) / perRow) * 20 + 4;
|
|
296
|
+
}
|
|
297
|
+
if (payload.filters?.length && config.sections.includes("filters")) {
|
|
298
|
+
doc.setTextColor(...ink);
|
|
299
|
+
doc.setFont("helvetica", "bold");
|
|
300
|
+
doc.setFontSize(10);
|
|
301
|
+
doc.text(lang === "en" ? "Active filters" : "Filtres actifs", margin, y);
|
|
302
|
+
y += 5;
|
|
303
|
+
doc.setFont("helvetica", "normal");
|
|
304
|
+
doc.setFontSize(9);
|
|
305
|
+
doc.setTextColor(...grey);
|
|
306
|
+
payload.filters.forEach((filter) => {
|
|
307
|
+
const lines = doc.splitTextToSize(printable(`${filter.label} : ${filter.value}`), inner);
|
|
308
|
+
doc.text(lines, margin, y);
|
|
309
|
+
y += lines.length * 4.5;
|
|
310
|
+
});
|
|
311
|
+
y += 4;
|
|
312
|
+
}
|
|
313
|
+
const charts = config.sections.includes("charts") ? payload.charts ?? [] : [];
|
|
314
|
+
for (const chart of charts) {
|
|
315
|
+
const svg = svgOf(chart.el);
|
|
316
|
+
if (!svg) continue;
|
|
317
|
+
const part = serialize(svg);
|
|
318
|
+
const canvas = await raster(part.markup, part.width, part.height, Math.min(2, 1400 / part.width));
|
|
319
|
+
const drawWidth = inner;
|
|
320
|
+
const drawHeight = part.height / part.width * drawWidth;
|
|
321
|
+
if (y + drawHeight > doc.internal.pageSize.getHeight() - margin) {
|
|
322
|
+
doc.addPage();
|
|
323
|
+
y = margin;
|
|
324
|
+
}
|
|
325
|
+
if (chart.title) {
|
|
326
|
+
doc.setTextColor(...ink);
|
|
327
|
+
doc.setFont("helvetica", "bold");
|
|
328
|
+
doc.setFontSize(10);
|
|
329
|
+
doc.text(printable(chart.title), margin, y);
|
|
330
|
+
y += 5;
|
|
331
|
+
}
|
|
332
|
+
doc.addImage(canvas.toDataURL("image/png"), "PNG", margin, y, drawWidth, drawHeight, void 0, "FAST");
|
|
333
|
+
y += drawHeight + 6;
|
|
334
|
+
}
|
|
335
|
+
if (config.sections.includes("table")) {
|
|
336
|
+
let rowCount = 0;
|
|
337
|
+
payload.sheets.forEach((sheet) => {
|
|
338
|
+
const columns = columnsOf(sheet, config.columns);
|
|
339
|
+
const rows = rowsOf(sheet, payload.limit);
|
|
340
|
+
rowCount += rows.length;
|
|
341
|
+
autoTable(doc, {
|
|
342
|
+
startY: y + 2,
|
|
343
|
+
head: [columns.map((column) => printable(column.column))],
|
|
344
|
+
body: rows.map((row) => columns.map((column) => printable(cellText(column.value(row), lang)))),
|
|
345
|
+
styles: { fontSize: 7, cellPadding: 1.6, overflow: "linebreak", textColor: ink },
|
|
346
|
+
headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold" },
|
|
347
|
+
alternateRowStyles: { fillColor: [250, 250, 253] },
|
|
348
|
+
margin: { left: margin, right: margin },
|
|
349
|
+
didDrawPage: () => {
|
|
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
|
+
}
|
|
357
|
+
});
|
|
358
|
+
y = doc.lastAutoTable?.finalY ?? y;
|
|
359
|
+
});
|
|
360
|
+
const withSources = config.sections.includes("sources") || config.options.includes("sources");
|
|
361
|
+
if (payload.sources?.length && withSources) {
|
|
362
|
+
doc.setTextColor(...grey);
|
|
363
|
+
doc.setFont("helvetica", "normal");
|
|
364
|
+
doc.setFontSize(8);
|
|
365
|
+
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 ");
|
|
366
|
+
const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
|
|
367
|
+
doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
|
|
368
|
+
}
|
|
369
|
+
const fileName2 = `${config.fileName}.pdf`;
|
|
370
|
+
const blob2 = doc.output("blob");
|
|
371
|
+
return { fileName: fileName2, rows: rowCount, bytes: download(blob2, fileName2) };
|
|
372
|
+
}
|
|
373
|
+
const fileName = `${config.fileName}.pdf`;
|
|
374
|
+
const blob = doc.output("blob");
|
|
375
|
+
return { fileName, rows: 0, bytes: download(blob, fileName) };
|
|
376
|
+
}
|
|
377
|
+
async function toPermalink(payload) {
|
|
378
|
+
const url = payload.permalink || (typeof window !== "undefined" ? window.location.href : "");
|
|
379
|
+
try {
|
|
380
|
+
await navigator.clipboard.writeText(url);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
return { fileName: "", rows: 0, bytes: 0, url };
|
|
384
|
+
}
|
|
385
|
+
async function run(config, payload, lang = "fr") {
|
|
386
|
+
switch (config.format) {
|
|
387
|
+
case "xlsx":
|
|
388
|
+
return toXlsx(config, payload, lang);
|
|
389
|
+
case "csv":
|
|
390
|
+
return toCsv(config, payload, lang);
|
|
391
|
+
case "png":
|
|
392
|
+
return toPng(config, payload, lang);
|
|
393
|
+
case "svg":
|
|
394
|
+
return toSvg(config, payload, lang);
|
|
395
|
+
case "pdf":
|
|
396
|
+
return toPdf(config, payload, lang);
|
|
397
|
+
case "link":
|
|
398
|
+
return toPermalink(payload);
|
|
399
|
+
default:
|
|
400
|
+
throw new Error(`Format d'export inconnu : ${config.format}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { run, extensions: RM_EXPORT_EXTENSIONS };
|
|
404
|
+
}
|