@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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.131",
7
+ "version": "0.1.133",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -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 ?? 100);
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 ?? 100).map((row) => ({
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
- cards.forEach((card) => {
413
- const titleLines = doc.splitTextToSize(printable(card.title), inner - 8);
414
- const subtitleLines = card.subtitle ? doc.splitTextToSize(printable(card.subtitle), inner - 8) : [];
415
- const fields = card.fields.map((field) => ({
416
- label: doc.splitTextToSize(printable(field.label), labelWidth),
417
- value: doc.splitTextToSize(printable(field.value), valueWidth)
418
- }));
419
- 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;
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(252, 252, 254);
426
- doc.roundedRect(margin, y, inner, height, 2, 2, "FD");
427
- let cursor = y + 6;
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(11);
431
- doc.text(titleLines, margin + 4, cursor);
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 + 4, cursor + 1);
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 + 4;
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(7);
459
- doc.text(field.label, margin + 4, cursor + 1);
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, margin + 4 + labelWidth, cursor + 1);
464
- cursor += lines * 4.2;
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
- const total = payload.sheets[0]?.data.length ?? 0;
469
- if (total > cards.length) {
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" ? `${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.`
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.132",
3
+ "version": "0.1.134",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",