@tekkare/romulus 0.1.132 → 0.1.134
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
|
@@ -110,7 +110,7 @@ const FR = {
|
|
|
110
110
|
kindView: 'Une vue',
|
|
111
111
|
kindViewHint: 'Un écran filtré, aussi fin que vous voulez, mais limité à cette page.',
|
|
112
112
|
kindProduct: 'Une marque',
|
|
113
|
-
kindProductHint: 'Une à {max} marques, suivies partout : prix, avis HAS, indications, ANSM, PUT, MITM, Journal officiel.',
|
|
113
|
+
kindProductHint: 'Une à {max} marques, suivies partout : prix, avis HAS, indications, ANSM, PUT, MITM, Journal officiel, ventes ville et hospitalières.',
|
|
114
114
|
brands: 'Marques surveillées',
|
|
115
115
|
brandsSearch: 'Rechercher',
|
|
116
116
|
brandsHint: 'Cherchez par marque, molécule, classe ATC ou laboratoire. L’alerte couvre tout le hub pour chaque marque retenue.',
|
|
@@ -163,7 +163,7 @@ const EN = {
|
|
|
163
163
|
kindView: 'A view',
|
|
164
164
|
kindViewHint: 'A filtered screen, as precise as you like, but tied to that page.',
|
|
165
165
|
kindProduct: 'A brand',
|
|
166
|
-
kindProductHint: 'One to {max} brands, watched everywhere: prices, HAS opinions, indications, ANSM, PUT, MITM, official journal.',
|
|
166
|
+
kindProductHint: 'One to {max} brands, watched everywhere: prices, HAS opinions, indications, ANSM, PUT, MITM, official journal, retail and hospital sales.',
|
|
167
167
|
brands: 'Watched brands',
|
|
168
168
|
brandsSearch: 'Search',
|
|
169
169
|
brandsHint: 'Search by brand, molecule, ATC class or company. The alert covers the whole hub for each brand kept.',
|
|
@@ -2,6 +2,8 @@ import { RM_EXPORT_EXTENSIONS } from "../utils/export.js";
|
|
|
2
2
|
const BRAND_TITLE = "OIP Healthcare";
|
|
3
3
|
const BRAND_LINE = "By Tekkare \u{1F49C}";
|
|
4
4
|
const COVER_SHEET = "Tekkare";
|
|
5
|
+
const CARD_PAGES_MAX = 10;
|
|
6
|
+
const CARD_COUNT_MAX = 2e3;
|
|
5
7
|
function longDate(lang) {
|
|
6
8
|
return (/* @__PURE__ */ new Date()).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
|
|
7
9
|
year: "numeric",
|
|
@@ -105,7 +107,7 @@ function printable(text) {
|
|
|
105
107
|
return substituted.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
|
|
106
108
|
}
|
|
107
109
|
function cardsOf(payload, config, lang) {
|
|
108
|
-
if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ??
|
|
110
|
+
if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ?? CARD_COUNT_MAX);
|
|
109
111
|
const sheet = payload.sheets[0];
|
|
110
112
|
if (!sheet) return [];
|
|
111
113
|
const pick = (name) => name ? sheet.schema.find((column) => column.column === name) : void 0;
|
|
@@ -115,7 +117,7 @@ function cardsOf(payload, config, lang) {
|
|
|
115
117
|
const head = title ?? columns[0];
|
|
116
118
|
if (!head) return [];
|
|
117
119
|
const fields = columns.filter((column) => column !== head && column !== subtitle);
|
|
118
|
-
return rowsOf(sheet, payload.cardLimit ??
|
|
120
|
+
return rowsOf(sheet, payload.cardLimit ?? CARD_COUNT_MAX).map((row) => ({
|
|
119
121
|
title: cellText(head.value(row), lang) || "\u2014",
|
|
120
122
|
subtitle: subtitle ? cellText(subtitle.value(row), lang) : void 0,
|
|
121
123
|
fields: fields.map((column) => ({ label: column.column, value: cellText(column.value(row), lang) })).filter((field) => field.value !== "")
|
|
@@ -405,40 +407,100 @@ export function useRmExport() {
|
|
|
405
407
|
let rowCount = 0;
|
|
406
408
|
if (config.sections.includes("cards")) {
|
|
407
409
|
const cards = cardsOf(payload, config, lang);
|
|
408
|
-
rowCount = cards.length;
|
|
409
|
-
const labelWidth = Math.min(46, inner * 0.34);
|
|
410
|
-
const valueWidth = inner - labelWidth - 8;
|
|
411
410
|
const pageHeight = doc.internal.pageSize.getHeight();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
411
|
+
const PAD = 6;
|
|
412
|
+
const COUNT_WIDTH = 32;
|
|
413
|
+
const COUNT_GAP = 6;
|
|
414
|
+
const LABEL_WIDTH = 26;
|
|
415
|
+
const firstCardPage = doc.getNumberOfPages();
|
|
416
|
+
let drawn = 0;
|
|
417
|
+
for (const card of cards) {
|
|
418
|
+
const hasCount = !!card.count?.value;
|
|
419
|
+
const bodyX = margin + PAD + (hasCount ? COUNT_WIDTH + COUNT_GAP : 0);
|
|
420
|
+
const bodyWidth = inner - PAD * 2 - (hasCount ? COUNT_WIDTH + COUNT_GAP : 0);
|
|
421
|
+
doc.setFont("helvetica", "bold");
|
|
422
|
+
doc.setFontSize(12);
|
|
423
|
+
const titleLines = doc.splitTextToSize(printable(card.title), inner - PAD * 2);
|
|
424
|
+
doc.setFont("helvetica", "normal");
|
|
425
|
+
doc.setFontSize(9);
|
|
426
|
+
const subtitleLines = card.subtitle ? doc.splitTextToSize(printable(card.subtitle), inner - PAD * 2) : [];
|
|
427
|
+
const TYPO = {
|
|
428
|
+
meta: { size: 7, bold: true, grey: true, lh: 4, caps: true },
|
|
429
|
+
// Monospace comme a l'ecran : une reference (CT-20451, un code CIP)
|
|
430
|
+
// se lit caractere par caractere, et se recopie.
|
|
431
|
+
ref: { size: 7.5, bold: false, grey: true, lh: 4, pill: true, mono: true },
|
|
432
|
+
lead: { size: 10, bold: true, grey: false, lh: 4.8 },
|
|
433
|
+
text: { size: 9, bold: false, grey: false, lh: 4.4 },
|
|
434
|
+
desc: { size: 8.5, bold: false, grey: true, lh: 4.2 }
|
|
435
|
+
};
|
|
436
|
+
const free = card.fields.filter((field) => !printable(field.label));
|
|
437
|
+
const rows = card.fields.filter((field) => printable(field.label));
|
|
438
|
+
const inline = [];
|
|
439
|
+
let taken = 0;
|
|
440
|
+
for (const field of free) {
|
|
441
|
+
const kind = field.kind ?? "text";
|
|
442
|
+
if (kind !== "meta" && kind !== "ref") break;
|
|
443
|
+
const typo = TYPO[kind];
|
|
444
|
+
const text = kind === "meta" ? printable(field.value).toUpperCase() : printable(field.value);
|
|
445
|
+
doc.setFont(typo.mono ? "courier" : "helvetica", typo.bold ? "bold" : "normal");
|
|
446
|
+
doc.setFontSize(typo.size);
|
|
447
|
+
const width = doc.getTextWidth(text) + (typo.pill ? 3 : 0);
|
|
448
|
+
const line = inline[inline.length - 1];
|
|
449
|
+
const used = line ? line.reduce((total2, item) => total2 + item.width + 3, 0) : 0;
|
|
450
|
+
if (line && used + width <= bodyWidth) line.push({ text, pill: !!typo.pill, width });
|
|
451
|
+
else inline.push([{ text, pill: !!typo.pill, width }]);
|
|
452
|
+
taken += 1;
|
|
453
|
+
}
|
|
454
|
+
const stacked = free.slice(taken).map((field) => {
|
|
455
|
+
const typo = TYPO[field.kind ?? "text"] ?? TYPO.text;
|
|
456
|
+
doc.setFont(typo.mono ? "courier" : "helvetica", typo.bold ? "bold" : "normal");
|
|
457
|
+
doc.setFontSize(typo.size);
|
|
458
|
+
return {
|
|
459
|
+
typo,
|
|
460
|
+
lines: doc.splitTextToSize(
|
|
461
|
+
typo.caps ? printable(field.value).toUpperCase() : printable(field.value),
|
|
462
|
+
bodyWidth
|
|
463
|
+
)
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
const fields = rows.map((field) => {
|
|
467
|
+
doc.setFont("helvetica", "bold");
|
|
468
|
+
doc.setFontSize(6.5);
|
|
469
|
+
const label = doc.splitTextToSize(printable(field.label).toUpperCase(), LABEL_WIDTH);
|
|
470
|
+
doc.setFont("helvetica", "normal");
|
|
471
|
+
doc.setFontSize(9);
|
|
472
|
+
return {
|
|
473
|
+
label,
|
|
474
|
+
value: doc.splitTextToSize(printable(field.value), bodyWidth - LABEL_WIDTH - 4)
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
const headHeight = titleLines.length * 5.2 + (subtitleLines.length ? subtitleLines.length * 4.2 + 1 : 0) + (card.badges?.length ? 6 : 0);
|
|
478
|
+
const bodyHeight = inline.length * 4.8 + stacked.reduce((total2, block) => total2 + block.lines.length * block.typo.lh + 1, 0) + fields.reduce((total2, field) => total2 + Math.max(field.label.length, field.value.length) * 4.4, 0);
|
|
479
|
+
const countHeight = hasCount ? 9 + (card.count?.label ? 4 : 0) + (card.count?.extra ? 4.5 : 0) : 0;
|
|
480
|
+
const height = PAD + headHeight + 3 + Math.max(bodyHeight, countHeight) + PAD;
|
|
420
481
|
if (y + height > pageHeight - margin) {
|
|
482
|
+
if (doc.getNumberOfPages() - firstCardPage + 1 >= CARD_PAGES_MAX) break;
|
|
421
483
|
doc.addPage();
|
|
422
484
|
y = margin;
|
|
423
485
|
}
|
|
424
486
|
doc.setDrawColor(232, 232, 240);
|
|
425
|
-
doc.setFillColor(
|
|
426
|
-
doc.roundedRect(margin, y, inner, height,
|
|
427
|
-
let cursor = y +
|
|
487
|
+
doc.setFillColor(255, 255, 255);
|
|
488
|
+
doc.roundedRect(margin, y, inner, height, 3, 3, "FD");
|
|
489
|
+
let cursor = y + PAD + 3;
|
|
428
490
|
doc.setTextColor(...ink);
|
|
429
491
|
doc.setFont("helvetica", "bold");
|
|
430
|
-
doc.setFontSize(
|
|
431
|
-
doc.text(titleLines, margin +
|
|
432
|
-
cursor += titleLines.length * 5;
|
|
492
|
+
doc.setFontSize(12);
|
|
493
|
+
doc.text(titleLines, margin + PAD, cursor);
|
|
494
|
+
cursor += titleLines.length * 5.2;
|
|
433
495
|
if (subtitleLines.length) {
|
|
434
496
|
doc.setTextColor(...grey);
|
|
435
497
|
doc.setFont("helvetica", "normal");
|
|
436
498
|
doc.setFontSize(9);
|
|
437
|
-
doc.text(subtitleLines, margin +
|
|
438
|
-
cursor += subtitleLines.length * 4 + 1;
|
|
499
|
+
doc.text(subtitleLines, margin + PAD, cursor + 1);
|
|
500
|
+
cursor += subtitleLines.length * 4.2 + 1;
|
|
439
501
|
}
|
|
440
502
|
if (card.badges?.length) {
|
|
441
|
-
let badgeX = margin +
|
|
503
|
+
let badgeX = margin + PAD;
|
|
442
504
|
doc.setFontSize(7);
|
|
443
505
|
card.badges.forEach((badge) => {
|
|
444
506
|
const text = printable(badge);
|
|
@@ -451,28 +513,91 @@ export function useRmExport() {
|
|
|
451
513
|
});
|
|
452
514
|
cursor += 6;
|
|
453
515
|
}
|
|
516
|
+
const bodyTop = cursor + 3;
|
|
517
|
+
if (hasCount) {
|
|
518
|
+
const centre = margin + PAD + COUNT_WIDTH / 2;
|
|
519
|
+
doc.setTextColor(...violet);
|
|
520
|
+
doc.setFont("helvetica", "bold");
|
|
521
|
+
doc.setFontSize(20);
|
|
522
|
+
doc.text(printable(String(card.count.value)), centre, bodyTop + 7, { align: "center" });
|
|
523
|
+
let countCursor = bodyTop + 11;
|
|
524
|
+
if (card.count.label) {
|
|
525
|
+
doc.setTextColor(...grey);
|
|
526
|
+
doc.setFont("helvetica", "bold");
|
|
527
|
+
doc.setFontSize(6.5);
|
|
528
|
+
doc.text(printable(card.count.label).toUpperCase(), centre, countCursor, { align: "center" });
|
|
529
|
+
countCursor += 4;
|
|
530
|
+
}
|
|
531
|
+
if (card.count.extra) {
|
|
532
|
+
doc.setTextColor(...violet);
|
|
533
|
+
doc.setFont("helvetica", "normal");
|
|
534
|
+
doc.setFontSize(8);
|
|
535
|
+
doc.text(
|
|
536
|
+
doc.splitTextToSize(printable(card.count.extra), COUNT_WIDTH),
|
|
537
|
+
centre,
|
|
538
|
+
countCursor + 0.5,
|
|
539
|
+
{ align: "center" }
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
doc.setDrawColor(232, 232, 240);
|
|
543
|
+
doc.line(
|
|
544
|
+
margin + PAD + COUNT_WIDTH + COUNT_GAP / 2,
|
|
545
|
+
bodyTop,
|
|
546
|
+
margin + PAD + COUNT_WIDTH + COUNT_GAP / 2,
|
|
547
|
+
y + height - PAD
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
cursor = bodyTop;
|
|
551
|
+
inline.forEach((line) => {
|
|
552
|
+
let itemX = bodyX;
|
|
553
|
+
line.forEach((item) => {
|
|
554
|
+
if (item.pill) {
|
|
555
|
+
doc.setFillColor(244, 244, 248);
|
|
556
|
+
doc.roundedRect(itemX, cursor - 2.6, item.width, 4.6, 1, 1, "F");
|
|
557
|
+
doc.setFont("courier", "normal");
|
|
558
|
+
doc.setFontSize(7.5);
|
|
559
|
+
doc.setTextColor(...grey);
|
|
560
|
+
doc.text(item.text, itemX + 1.5, cursor + 0.5);
|
|
561
|
+
} else {
|
|
562
|
+
doc.setFont("helvetica", "bold");
|
|
563
|
+
doc.setFontSize(7);
|
|
564
|
+
doc.setTextColor(...grey);
|
|
565
|
+
doc.text(item.text, itemX, cursor + 0.5);
|
|
566
|
+
}
|
|
567
|
+
itemX += item.width + 3;
|
|
568
|
+
});
|
|
569
|
+
cursor += 4.8;
|
|
570
|
+
});
|
|
571
|
+
stacked.forEach((block) => {
|
|
572
|
+
doc.setFont(block.typo.mono ? "courier" : "helvetica", block.typo.bold ? "bold" : "normal");
|
|
573
|
+
doc.setFontSize(block.typo.size);
|
|
574
|
+
doc.setTextColor(...block.typo.grey ? grey : ink);
|
|
575
|
+
doc.text(block.lines, bodyX, cursor + 1);
|
|
576
|
+
cursor += block.lines.length * block.typo.lh + 1;
|
|
577
|
+
});
|
|
454
578
|
fields.forEach((field) => {
|
|
455
|
-
const lines = Math.max(field.label.length, field.value.length);
|
|
456
579
|
doc.setTextColor(...grey);
|
|
457
580
|
doc.setFont("helvetica", "bold");
|
|
458
|
-
doc.setFontSize(
|
|
459
|
-
doc.text(field.label,
|
|
581
|
+
doc.setFontSize(6.5);
|
|
582
|
+
doc.text(field.label, bodyX, cursor + 1);
|
|
460
583
|
doc.setTextColor(...ink);
|
|
461
584
|
doc.setFont("helvetica", "normal");
|
|
462
585
|
doc.setFontSize(9);
|
|
463
|
-
doc.text(field.value,
|
|
464
|
-
cursor +=
|
|
586
|
+
doc.text(field.value, bodyX + LABEL_WIDTH + 4, cursor + 1);
|
|
587
|
+
cursor += Math.max(field.label.length, field.value.length) * 4.4;
|
|
465
588
|
});
|
|
466
589
|
y += height + 4;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
590
|
+
drawn += 1;
|
|
591
|
+
}
|
|
592
|
+
rowCount = drawn;
|
|
593
|
+
const total = payload.cards?.length || (payload.sheets[0]?.data.length ?? 0);
|
|
594
|
+
if (total > drawn) {
|
|
470
595
|
doc.setTextColor(...grey);
|
|
471
596
|
doc.setFont("helvetica", "italic");
|
|
472
597
|
doc.setFontSize(8);
|
|
473
598
|
doc.text(
|
|
474
599
|
printable(
|
|
475
|
-
lang === "en" ? `${
|
|
600
|
+
lang === "en" ? `${drawn} of ${total} records shown. Export to Excel for the full set.` : `${drawn} fiches sur ${total}. Passez par l'export Excel pour la totalit\xE9.`
|
|
476
601
|
),
|
|
477
602
|
margin,
|
|
478
603
|
y + 2
|
|
@@ -53,6 +53,13 @@ export interface RmExportFilter {
|
|
|
53
53
|
label: string;
|
|
54
54
|
value: string;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Role d'une ligne de fiche, calque sur ce que `RmResultCard` projette :
|
|
58
|
+
* `meta` l'entete de ligne en petites capitales, `ref` la reference monospace
|
|
59
|
+
* sur son fond gris, `lead` le chapo en gras, `text` le corps, `desc` le
|
|
60
|
+
* commentaire en retrait.
|
|
61
|
+
*/
|
|
62
|
+
export type RmExportCardLine = 'meta' | 'ref' | 'lead' | 'text' | 'desc';
|
|
56
63
|
/**
|
|
57
64
|
* Une fiche : ce que la page montre sous forme de carte, rendu tel quel dans
|
|
58
65
|
* le rapport PDF. Une page qui n'en declare pas en obtient quand meme, les
|
|
@@ -63,9 +70,31 @@ export interface RmExportCard {
|
|
|
63
70
|
subtitle?: string;
|
|
64
71
|
/** Etiquettes courtes posees sous le titre (SMR, statut, pays...). */
|
|
65
72
|
badges?: string[];
|
|
73
|
+
/**
|
|
74
|
+
* Compteur de la fiche, celui que `RmResultCard` pose en gros a gauche
|
|
75
|
+
* (« 16 avis depuis 2018 »). C'est la premiere chose qu'on compare d'une
|
|
76
|
+
* fiche a l'autre : le rapport lui garde sa place et sa taille.
|
|
77
|
+
*/
|
|
78
|
+
count?: {
|
|
79
|
+
value: string;
|
|
80
|
+
label?: string;
|
|
81
|
+
extra?: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Le corps de la fiche. Un champ sans intitule est une ligne libre (le
|
|
85
|
+
* resume, l'indication) : elle prend toute la largeur, comme a l'ecran.
|
|
86
|
+
* Un champ intitule est une rangee (SMR, statut) : intitule a gauche,
|
|
87
|
+
* valeur a droite.
|
|
88
|
+
*
|
|
89
|
+
* `kind` reprend le role que la carte donne a la ligne, et le rapport lui
|
|
90
|
+
* rend sa graisse et sa taille : sans lui, l'entete de date, le chapo et le
|
|
91
|
+
* texte reglementaire sortaient tous en 9 points gris, et la fiche imprimee
|
|
92
|
+
* n'avait plus la hierarchie de la fiche a l'ecran.
|
|
93
|
+
*/
|
|
66
94
|
fields: {
|
|
67
95
|
label: string;
|
|
68
96
|
value: string;
|
|
97
|
+
kind?: RmExportCardLine;
|
|
69
98
|
}[];
|
|
70
99
|
}
|
|
71
100
|
/**
|