@tekkare/romulus 0.1.241 → 0.1.243

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.
Files changed (64) hide show
  1. package/dist/module.json +1 -1
  2. package/layer/app/app.config.ts +90 -0
  3. package/layer/app/assets/tekkare_logo.svg +20 -0
  4. package/layer/app/components/AppUpdateToast.vue +157 -0
  5. package/layer/app/components/OipDirectusSources.vue +192 -0
  6. package/layer/app/components/OipDocumentPreview.vue +158 -0
  7. package/layer/app/components/OipMetadataEditDialog.vue +350 -0
  8. package/layer/app/components/OipNoteEditDialog.vue +382 -0
  9. package/layer/app/components/OipShell.vue +713 -0
  10. package/layer/app/components/OipSourceEditDialog.vue +419 -0
  11. package/layer/app/components/OipSourceGroups.vue +107 -0
  12. package/layer/app/components/auth/SplitLayout.vue +261 -0
  13. package/layer/app/components/layout/firstVisitVideo.vue +165 -0
  14. package/layer/app/components/panels/OipPanelDocuments.vue +220 -0
  15. package/layer/app/components/panels/OipPanelMetadata.vue +388 -0
  16. package/layer/app/components/panels/OipPanelNotes.vue +99 -0
  17. package/layer/app/components/panels/OipPanelProjects.vue +94 -0
  18. package/layer/app/components/panels/OipPanelSourceTables.vue +8 -0
  19. package/layer/app/components/panels/OipPanelSources.vue +361 -0
  20. package/layer/app/components/panels/OipPanelTopics.vue +339 -0
  21. package/layer/app/components/panels/OipPanelTutorials.vue +122 -0
  22. package/layer/app/composables/useOipCrumbLabel.ts +11 -0
  23. package/layer/app/composables/useOipEsIndices.ts +57 -0
  24. package/layer/app/composables/useOipFavorites.ts +69 -0
  25. package/layer/app/composables/useOipNotes.ts +90 -0
  26. package/layer/app/composables/useOipNotify.ts +21 -0
  27. package/layer/app/composables/useOipSourceTableGroups.ts +54 -0
  28. package/layer/app/composables/useViewEditing.ts +11 -0
  29. package/layer/app/layouts/anonymous.vue +6 -0
  30. package/layer/app/layouts/error.vue +38 -0
  31. package/layer/app/notes/categories.ts +46 -0
  32. package/layer/app/notes/types.ts +100 -0
  33. package/layer/app/plugins/oip-es-indices.client.ts +18 -0
  34. package/layer/app/types/metadata.ts +38 -0
  35. package/layer/app/types/oip.ts +60 -0
  36. package/layer/app/types/tableGroups.ts +42 -0
  37. package/layer/app/utils/countries.ts +30 -0
  38. package/layer/app/utils/exportPage.ts +418 -0
  39. package/layer/app/utils/headerTabs.ts +15 -0
  40. package/layer/app/utils/hybridRows.ts +22 -0
  41. package/layer/app/utils/oipRail.ts +82 -0
  42. package/layer/app/utils/postLoginRedirect.ts +58 -0
  43. package/layer/app/utils/valueTable.ts +126 -0
  44. package/layer/nuxt.config.ts +32 -0
  45. package/layer/server/api/oip/asset/[id].get.ts +89 -0
  46. package/layer/server/api/oip/documents.get.ts +92 -0
  47. package/layer/server/api/oip/es-indices.get.ts +21 -0
  48. package/layer/server/api/oip/favorites.get.ts +107 -0
  49. package/layer/server/api/oip/metadata.get.ts +109 -0
  50. package/layer/server/api/oip/metadata.patch.ts +163 -0
  51. package/layer/server/api/oip/note-options.get.ts +89 -0
  52. package/layer/server/api/oip/note.get.ts +95 -0
  53. package/layer/server/api/oip/notes.get.ts +173 -0
  54. package/layer/server/api/oip/notes.patch.ts +198 -0
  55. package/layer/server/api/oip/sources.patch.ts +145 -0
  56. package/layer/server/api/oip/sources.post.ts +88 -0
  57. package/layer/server/api/oip/table-groups.get.ts +145 -0
  58. package/layer/server/api/oip/tutorial-video.get.ts +44 -0
  59. package/layer/server/api/romulus/version.get.ts +4 -0
  60. package/layer/server/utils/oipAuth.ts +69 -0
  61. package/layer/server/utils/oipEsIndices.ts +99 -0
  62. package/layer/server/utils/oipGuards.ts +90 -0
  63. package/layer/server/utils/oipNoteMarkup.ts +197 -0
  64. package/package.json +6 -3
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.240",
7
+ "version": "0.1.242",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,90 @@
1
+ import type { OipExternalCrumb, OipLocaleOption, OipPart } from "./types/oip";
2
+
3
+ // Reglages du cadre OIP 7. Chaque app surcharge ce qui lui est propre dans son
4
+ // propre app.config.ts (Nuxt fusionne les deux, l'app gagne) :
5
+ //
6
+ // export default defineAppConfig({
7
+ // oip: {
8
+ // home: { label: "France • Drugs", flag: "fr", href: "https://.../france" },
9
+ // parts: [{ key: "drugs", label: "Médicaments", labelEn: "Drugs", to: "/medicam/market" }],
10
+ // },
11
+ // })
12
+ //
13
+ // Piege : Nuxt CONCATENE les tableaux d'un layer a l'autre. Pour remplacer un
14
+ // tableau du layer (locales), l'app le donne en fonction :
15
+ // `locales: () => [...]`.
16
+ export default defineAppConfig({
17
+ oip: {
18
+ /** Premier maillon du fil : la plateforme, sur un autre domaine. */
19
+ platform: {
20
+ label: "💠 OIP Platform®",
21
+ href: "https://openinnovationprogram.com/healthcare",
22
+ } as OipExternalCrumb,
23
+ /** Deuxieme maillon : le pays ou l'app sur la plateforme. Absent : sautee. */
24
+ home: null as OipExternalCrumb | null,
25
+ /** Parties d'un hub. Vide pour une app sans parties. */
26
+ parts: [] as OipPart[],
27
+ /** Langues proposees en bas du rail. */
28
+ locales: [
29
+ { code: "en", flag: "🇬🇧", label: "English" },
30
+ { code: "fr", flag: "🇫🇷", label: "Français" },
31
+ ] as OipLocaleOption[],
32
+ /**
33
+ * Largeur commune des panneaux du rail. Une seule pour toutes les tuiles :
34
+ * sinon le contenu de la page se reflue a chaque ouverture.
35
+ */
36
+ panelWidth: "640px",
37
+ /**
38
+ * Options des panneaux du rail servis par le layer, par tuile. `endpoint`
39
+ * pointe la route qui nourrit le panneau : une app peut la detourner vers
40
+ * la sienne (ou une autre API) sans toucher au panneau.
41
+ */
42
+ /**
43
+ * Registre des index Elasticsearch (collection Directus ElasticIndices).
44
+ * `preload` : charge les noms au demarrage, pour les apps qui designent
45
+ * leurs index par id (cf. useOipEsIndices).
46
+ */
47
+ esIndices: {
48
+ endpoint: "/api/oip/es-indices",
49
+ preload: false,
50
+ },
51
+ panels: {
52
+ Topics: {
53
+ endpoint: "/api/oip/favorites",
54
+ },
55
+ Metadata: {
56
+ endpoint: "/api/oip/metadata",
57
+ },
58
+ Sources: {
59
+ /** Fiches Directus des sources (POST lecture, PATCH correction). */
60
+ endpoint: "/api/oip/sources",
61
+ /** Tablegroups des sujets de l'app (tuile Tables). */
62
+ tablesEndpoint: "/api/oip/table-groups",
63
+ },
64
+ Notes: {
65
+ endpoint: "/api/oip/notes",
66
+ /** Une note entiere, pour la corriger (superadmin). */
67
+ noteEndpoint: "/api/oip/note",
68
+ /** Listes de l'editeur : categories, ecrans, jeux de donnees. */
69
+ optionsEndpoint: "/api/oip/note-options",
70
+ },
71
+ Documents: {
72
+ endpoint: "/api/oip/documents",
73
+ /**
74
+ * Documents Directus (oipDocuments) retenus par l'app, dans l'ordre
75
+ * d'affichage : `{ id, category, apps? }`. Vide : le panneau est vide.
76
+ */
77
+ catalog: [] as { id: number; category: string; apps?: string[] }[],
78
+ /** Libelles des categories, par cle. L'app peut en ajouter. */
79
+ categories: {
80
+ license: { fr: "Licences et réutilisation", en: "Licences and reuse" },
81
+ price: { fr: "Prix et remboursement", en: "Pricing and reimbursement" },
82
+ general: { fr: "Système de santé français", en: "French healthcare system" },
83
+ hospital: { fr: "Hôpital et nomenclatures", en: "Hospital and coding" },
84
+ trials: { fr: "Essais cliniques", en: "Clinical trials" },
85
+ establishments: { fr: "Établissements", en: "Establishments" },
86
+ } as Record<string, { fr: string; en: string }>,
87
+ },
88
+ },
89
+ },
90
+ });
@@ -0,0 +1,20 @@
1
+ <svg width="213" height="28" viewBox="0 0 213 28" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_5642_694)">
3
+ <path d="M125.201 24.8351L123.589 27.7971C123.521 27.9214 123.39 27.9995 123.244 27.9995H118.066C117.768 27.9995 117.58 27.6799 117.722 27.4206L130.596 4.01982C130.745 3.7499 131.132 3.7499 131.281 4.01982L144.127 27.4206C144.269 27.6799 144.081 27.9995 143.783 27.9995H138.395C138.249 27.9995 138.118 27.9178 138.05 27.79L136.495 24.8387C136.427 24.7108 136.292 24.6292 136.15 24.6292H125.546C125.404 24.6292 125.272 24.7073 125.201 24.8316V24.8351ZM133.636 19.3339L131.288 14.9016C131.143 14.6246 130.745 14.6246 130.596 14.9016L128.234 19.3339C128.096 19.5931 128.284 19.9092 128.579 19.9092H133.291C133.586 19.9092 133.774 19.5967 133.636 19.3339Z" fill="#2100AD"/>
4
+ <path d="M22.3567 9.47516H14.483C14.2664 9.47516 14.0923 9.64918 14.0923 9.86582V27.4457C14.0923 27.6624 13.9183 27.8364 13.7017 27.8364H9.05277C8.83612 27.8364 8.6621 27.6624 8.6621 27.4457V9.86582C8.6621 9.64918 8.48808 9.47516 8.27144 9.47516H0.390665C0.174023 9.47516 0 9.30113 0 9.08449V4.47465C0 4.25801 0.174023 4.08398 0.390665 4.08398H22.3567C22.5733 4.08398 22.7473 4.25801 22.7473 4.47465V9.08449C22.7473 9.30113 22.5733 9.47516 22.3567 9.47516Z" fill="#2100AD"/>
5
+ <path d="M171.591 27.9998H166.551C166.416 27.9998 166.288 27.9287 166.217 27.8115L162.438 21.6568C162.289 21.4117 162.016 21.2732 161.732 21.2874L155.094 21.2981C154.668 21.2981 154.32 21.671 154.32 22.1007V27.5239C154.32 27.7405 154.146 27.9145 153.929 27.9145H149.316C149.099 27.9145 148.925 27.7405 148.925 27.5239V4.47465C148.925 4.25801 149.099 4.08398 149.316 4.08398H160.705C162.698 4.08398 164.53 4.46755 166.207 5.23822C167.883 6.0089 169.211 7.04948 170.195 8.35999C171.179 9.67404 171.672 11.1088 171.672 12.6715C171.672 14.1454 171.243 15.5092 170.383 16.7664C169.595 17.9135 168.551 18.8724 167.251 19.6324C167.055 19.7461 166.992 19.9982 167.116 20.1865L171.921 27.396C172.095 27.6553 171.907 28.0033 171.594 28.0033L171.591 27.9998ZM161.416 15.843C162.158 15.843 162.833 15.6974 163.451 15.4097C164.069 15.122 164.559 14.7349 164.921 14.2519C165.283 13.7654 165.464 13.2362 165.464 12.6609C165.464 12.0855 165.283 11.5776 164.921 11.0946C164.559 10.6081 164.069 10.2245 163.451 9.93685C162.833 9.64918 162.154 9.50357 161.416 9.50357H154.71C154.494 9.50357 154.32 9.67759 154.32 9.89423V15.4523C154.32 15.669 154.494 15.843 154.71 15.843H161.416Z" fill="#2100AD"/>
6
+ <path d="M51.266 9.45754H34.3715C34.1549 9.45754 33.9809 9.63156 33.9809 9.8482V13.0978C33.9809 13.3145 34.1549 13.4885 34.3715 13.4885H42.7211C42.9377 13.4885 43.1118 13.6625 43.1118 13.8792V18.4996C43.1118 18.7163 42.9377 18.8903 42.7211 18.8903H34.3715C34.1549 18.8903 33.9809 19.0643 33.9809 19.281V22.1257C33.9809 22.3424 34.1549 22.5164 34.3715 22.5164H51.266C51.4827 22.5164 51.6567 22.6904 51.6567 22.9071V27.5133C51.6567 27.73 51.4827 27.904 51.266 27.904H28.8738C28.6572 27.904 28.4832 27.73 28.4832 27.5133V4.43217C28.4832 4.21553 28.6572 4.0415 28.8738 4.0415H51.266C51.4827 4.0415 51.6567 4.21553 51.6567 4.43217V9.06687C51.6567 9.28351 51.4827 9.45754 51.266 9.45754Z" fill="#2100AD"/>
7
+ <path d="M200.031 9.54982H183.136C182.92 9.54982 182.746 9.72385 182.746 9.94049V13.1901C182.746 13.4067 182.92 13.5808 183.136 13.5808H191.486C191.703 13.5808 191.877 13.7548 191.877 13.9714V18.5919C191.877 18.8086 191.703 18.9826 191.486 18.9826H183.136C182.92 18.9826 182.746 19.1566 182.746 19.3733V22.218C182.746 22.4347 182.92 22.6087 183.136 22.6087H200.031C200.248 22.6087 200.422 22.7827 200.422 22.9993V27.6056C200.422 27.8223 200.248 27.9963 200.031 27.9963H177.639C177.422 27.9963 177.248 27.8223 177.248 27.6056V4.52445C177.248 4.30781 177.422 4.13379 177.639 4.13379H200.031C200.248 4.13379 200.422 4.30781 200.422 4.52445V9.15916C200.422 9.3758 200.248 9.54982 200.031 9.54982Z" fill="#2100AD"/>
8
+ <path d="M205.923 3.27448C205.923 1.4277 207.404 0 209.325 0C211.247 0 212.774 1.43125 212.774 3.27448C212.774 5.11771 211.261 6.55606 209.325 6.55606C207.39 6.55606 205.923 5.12126 205.923 3.27448ZM206.498 3.27448C206.498 4.81228 207.745 6.01979 209.325 6.01979C210.906 6.01979 212.198 4.81228 212.198 3.27448C212.198 1.73668 210.927 0.511416 209.325 0.511416C207.723 0.511416 206.498 1.72603 206.498 3.27448ZM208.114 1.64079H209.492C210.174 1.64079 210.607 2.02435 210.607 2.63166C210.607 3.07915 210.366 3.40944 209.982 3.52664L210.767 4.87265H210.135L209.432 3.61542H208.654V4.87265H208.111V1.64079H208.114ZM209.481 3.12887C209.858 3.12887 210.05 2.92998 210.05 2.63166C210.05 2.33333 209.858 2.12735 209.481 2.12735H208.658V3.12887H209.481Z" fill="#2100AD"/>
9
+ <path d="M65.0387 14.4476V14.7886C65.0387 18.9332 65.0387 23.0813 65.0387 27.2259C65.0387 27.3005 65.0387 27.3786 65.0387 27.4532C65.0245 27.8013 64.8824 27.9398 64.5344 27.9433C63.7886 27.9433 63.0428 27.9433 62.297 27.9433C61.6186 27.9433 60.9403 27.9433 60.2619 27.9433C59.644 27.9433 59.5872 27.8865 59.5872 27.2579C59.5872 19.7358 59.5872 12.2102 59.5872 4.68813C59.5872 4.65262 59.5872 4.62065 59.5872 4.58514C59.5872 4.17672 59.7008 4.06307 60.1199 4.06307C60.7805 4.06307 61.441 4.06307 62.1016 4.06307C62.8972 4.06307 63.6962 4.06307 64.4918 4.06307C64.9002 4.06307 65.0387 4.19802 65.0387 4.61355C65.0387 6.05901 65.0387 7.50092 65.0387 8.94638C65.0387 9.94435 65.0387 10.9388 65.0387 11.9367V12.2386C65.1879 11.3046 65.4081 10.3918 65.7703 9.51817C67.0311 6.48164 69.336 4.67748 72.5182 3.98138C75.8175 3.26043 78.6338 4.22644 81.0346 6.53491C81.1732 6.66987 81.3117 6.80837 81.4395 6.95399C81.6562 7.19904 81.6491 7.37661 81.4147 7.61101C80.6617 8.36393 79.9053 9.11685 79.1524 9.86977C78.8185 10.2036 78.4811 10.541 78.1473 10.8748C77.8596 11.159 77.6927 11.1412 77.3908 10.8784C76.9646 10.509 76.5562 10.09 76.0697 9.82715C73.9565 8.68001 71.2396 10.0118 70.7353 12.3878C70.5045 13.471 70.6856 14.4832 71.1757 15.4527C71.7866 16.6567 72.6744 17.6582 73.6014 18.6242C75.0752 20.1549 76.716 21.5045 78.4563 22.7155C80.9601 24.4593 83.5065 26.1427 86.028 27.8581C86.1452 27.9398 86.2234 27.9362 86.3406 27.8581C88.3081 26.544 90.2898 25.2549 92.2396 23.9159C94.5268 22.3426 96.7678 20.7018 98.6891 18.6846C99.4314 17.9033 100.12 17.0615 100.76 16.1914C101.413 15.2965 101.782 14.2772 101.747 13.1407C101.669 10.6227 99.2893 8.84693 96.9205 9.54658C96.2528 9.74191 95.7059 10.129 95.2193 10.6156C95.1412 10.6937 95.0666 10.7718 94.9885 10.85C94.6901 11.1519 94.5197 11.1554 94.2213 10.8571C93.1595 9.79874 92.1011 8.74394 91.0428 7.68559C90.6947 7.33755 90.7018 7.19549 91.0357 6.84389C92.3177 5.50142 93.8165 4.51766 95.6242 4.02755C99.403 3.00117 105.377 4.68813 106.978 10.6404C107.124 11.1838 107.216 11.7414 107.344 12.3345C107.344 12.1853 107.344 12.0717 107.344 11.958C107.344 9.52527 107.344 7.09605 107.344 4.66327C107.344 4.18027 107.458 4.06662 107.941 4.06662C109.365 4.06662 110.789 4.06662 112.21 4.06662C112.678 4.06662 112.781 4.16961 112.781 4.62776C112.781 12.2102 112.781 19.7926 112.781 27.3751C112.781 27.8332 112.671 27.9433 112.217 27.9433C110.81 27.9433 109.404 27.9433 107.998 27.9433C107.444 27.9433 107.344 27.8474 107.344 27.3005C107.344 23.1204 107.344 18.9403 107.344 14.7602C107.344 14.6288 107.344 14.4974 107.344 14.366C106.993 17.2001 105.547 19.4766 103.722 21.5648C101.861 23.6886 99.6978 25.475 97.4319 27.1442C97.091 27.3964 96.75 27.645 96.402 27.883C96.3096 27.9433 96.1889 27.9575 96.0788 27.9931C96.0646 27.9966 96.0468 27.9931 96.0291 27.9931C89.4624 27.9931 82.8956 27.9931 76.3289 27.9931C76.1975 27.9931 76.0448 27.9433 75.9383 27.8688C73.7363 26.3132 71.6019 24.6795 69.7373 22.7262C68.8814 21.8312 68.0717 20.8794 67.3259 19.8885C66.1148 18.2797 65.3193 16.4755 65.0458 14.4583L65.0387 14.4476Z" fill="url(#paint0_linear_5642_694)"/>
10
+ </g>
11
+ <defs>
12
+ <linearGradient id="paint0_linear_5642_694" x1="59.5872" y1="15.8611" x2="112.781" y2="15.8611" gradientUnits="userSpaceOnUse">
13
+ <stop stop-color="#F95BFF"/>
14
+ <stop offset="1" stop-color="#5BC5FF"/>
15
+ </linearGradient>
16
+ <clipPath id="clip0_5642_694">
17
+ <rect width="212.774" height="28" fill="white"/>
18
+ </clipPath>
19
+ </defs>
20
+ </svg>
@@ -0,0 +1,157 @@
1
+ <!-- "A new version is available" prompt.
2
+ Nuxt's own check-outdated-build plugin polls /_nuxt/builds/latest.json and
3
+ fires `app:manifest:update` when the deployed build id stops matching the
4
+ one this tab booted with. Nuxt then hard-reloads on the next route change —
5
+ silently, and possibly mid-task. This toast surfaces that instead, so the
6
+ user reloads when it suits them; the built-in reload stays as the fallback. -->
7
+ <template>
8
+ <Transition name="update-toast">
9
+ <div v-if="visible" class="update-toast" role="status" aria-live="polite">
10
+ <span class="update-toast-mark" aria-hidden="true">
11
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
12
+ <path d="M21 12a9 9 0 1 1-2.64-6.36" />
13
+ <path d="M21 3v6h-6" />
14
+ </svg>
15
+ </span>
16
+ <div class="update-toast-body">
17
+ <p class="update-toast-title">
18
+ {{ locale === "en" ? "A new version is available" : "Une nouvelle version est disponible" }}
19
+ </p>
20
+ <p class="update-toast-text">
21
+ {{ locale === "en" ? "Reload to get the latest updates." : "Rechargez la page pour profiter des dernières mises à jour." }}
22
+ </p>
23
+ <div class="update-toast-actions">
24
+ <button type="button" class="update-toast-reload" @click="reload">
25
+ {{ locale === "en" ? "Reload" : "Recharger" }}
26
+ </button>
27
+ <button type="button" class="update-toast-later" @click="visible = false">
28
+ {{ locale === "en" ? "Later" : "Plus tard" }}
29
+ </button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </Transition>
34
+ </template>
35
+
36
+ <script setup>
37
+ const nuxtApp = useNuxtApp();
38
+ // Textes bilingues par ternaire, comme les autres panneaux du cadre.
39
+ const { locale } = useI18n();
40
+
41
+ const visible = ref(false);
42
+
43
+ // Only ever fires client-side, so the server render and the hydrating render
44
+ // both agree on `false`.
45
+ const unhook = nuxtApp.hook('app:manifest:update', () => {
46
+ visible.value = true;
47
+ });
48
+ onUnmounted(() => unhook());
49
+
50
+ // force: true — reloadNuxtApp otherwise no-ops when the same path was already
51
+ // reloaded in the last 10s (its sessionStorage guard), which would leave the
52
+ // button dead. The path carries the query so filters survive the reload, and
53
+ // persistState stays false: the point is to boot the new build clean.
54
+ const reload = () =>
55
+ reloadNuxtApp({
56
+ path: window.location.pathname + window.location.search,
57
+ persistState: false,
58
+ force: true,
59
+ });
60
+ </script>
61
+
62
+ <style scoped>
63
+ .update-toast {
64
+ position: fixed;
65
+ right: 20px;
66
+ bottom: 20px;
67
+ z-index: 70;
68
+ display: flex;
69
+ gap: 11px;
70
+ max-width: 340px;
71
+ padding: 14px 16px;
72
+ border-radius: 12px;
73
+ border: 1px solid #dbdef0;
74
+ background: var(--bg-surface);
75
+ box-shadow: 0 10px 30px rgba(30, 41, 59, 0.13);
76
+ }
77
+
78
+ .update-toast-body {
79
+ min-width: 0;
80
+ }
81
+
82
+ .update-toast-title {
83
+ font-weight: 600;
84
+ font-size: 14px;
85
+ color: #1e293b;
86
+ }
87
+
88
+ .update-toast-text {
89
+ margin-top: 2px;
90
+ font-size: 12px;
91
+ color: #878ca6;
92
+ }
93
+
94
+ .update-toast-actions {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ margin-top: 10px;
99
+ }
100
+
101
+ .update-toast-mark {
102
+ flex-shrink: 0;
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: 28px;
107
+ height: 28px;
108
+ border-radius: 9999px;
109
+ color: var(--primary, #2017ad);
110
+ background: color-mix(in srgb, var(--primary, #2017ad) 9%, transparent);
111
+ }
112
+
113
+ .update-toast-reload,
114
+ .update-toast-later {
115
+ font-size: 13px;
116
+ font-weight: 600;
117
+ border-radius: 7px;
118
+ padding: 6px 12px;
119
+ transition: background-color 0.15s ease, color 0.15s ease;
120
+ }
121
+
122
+ .update-toast-reload {
123
+ color: #ffffff;
124
+ background: var(--primary, #2017ad);
125
+ }
126
+
127
+ .update-toast-reload:hover {
128
+ filter: brightness(0.9);
129
+ }
130
+
131
+ .update-toast-later {
132
+ color: #878ca6;
133
+ }
134
+
135
+ .update-toast-later:hover {
136
+ color: #475569;
137
+ background: var(--bg-surface-3);
138
+ }
139
+
140
+ .update-toast-enter-active,
141
+ .update-toast-leave-active {
142
+ transition: opacity 0.22s ease, transform 0.22s ease;
143
+ }
144
+
145
+ .update-toast-enter-from,
146
+ .update-toast-leave-to {
147
+ opacity: 0;
148
+ transform: translateY(10px);
149
+ }
150
+
151
+ @media (prefers-reduced-motion: reduce) {
152
+ .update-toast-enter-active,
153
+ .update-toast-leave-active {
154
+ transition: none;
155
+ }
156
+ }
157
+ </style>
@@ -0,0 +1,192 @@
1
+ <template>
2
+ <div v-if="!isLoading && sources.length" class="oip-src-list">
3
+ <template v-for="(uuid, index) in reorderedUuids" :key="uuid">
4
+ <RmSourceCard
5
+ v-if="findSource(uuid)"
6
+ :model-value="uuid === sourceToOpen"
7
+ :title="constructTitle(uuid)"
8
+ :country="showCountry ? getCountry(uuid).label : undefined"
9
+ :flag="showCountry ? getCountry(uuid).iso : undefined"
10
+ :sector="findSource(uuid)?.provider?.status"
11
+ :about-provider="getDescriptions(uuid).provider"
12
+ :about-source="getDescriptions(uuid).source"
13
+ :link="findSource(uuid)?.source_link"
14
+ :last-update="getFreshness(uuid).lastUpdate"
15
+ :frequency="getFreshness(uuid).frequency"
16
+ :tags="getTags(uuid)"
17
+ :lang="cardLang"
18
+ :highlight="highlight && index === 0"
19
+ :editable="canEdit"
20
+ @copy="notifyCopy"
21
+ @edit="openEditor(uuid)"
22
+ />
23
+ </template>
24
+ </div>
25
+ <div v-else-if="isLoading" class="oip-src-list">
26
+ <RmSkeleton v-for="i in 3" :key="i" width="100%" :height="70" radius="8px" />
27
+ </div>
28
+
29
+ <OipSourceEditDialog
30
+ v-if="editing"
31
+ v-model="isEditorOpen"
32
+ :source="editing"
33
+ :lang="lang"
34
+ @saved="onSaved"
35
+ />
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ /**
40
+ * Sources d'un panneau (layer OIP), resolues via /api/oip/sources (qui proxie
41
+ * Directus) pour que l'URL et le jeton ne partent jamais dans le navigateur.
42
+ *
43
+ * Le rendu est celui de RmSourceCard : les champs Directus y vont bruts
44
+ * (`provider.status` pour le secteur, `geo_zones.country_iso` pour le drapeau,
45
+ * `update_frequency` pour la frequence), le composant portant les libelles.
46
+ */
47
+
48
+ const props = withDefaults(
49
+ defineProps<{
50
+ uuids: string[];
51
+ showCountry?: boolean;
52
+ sourceToOpen?: string | null;
53
+ lang?: string;
54
+ /** Cadre la premiere fiche en couleur d'accent : la source principale. */
55
+ highlight?: boolean;
56
+ /** Crayon de correction (superadmin). La route revalide le droit. */
57
+ editable?: boolean;
58
+ }>(),
59
+ { showCountry: true, sourceToOpen: null, lang: "en", highlight: false, editable: false }
60
+ );
61
+
62
+ const emit = defineEmits<{ updateSources: [any[]] }>();
63
+
64
+ const sources = ref<any[]>([]);
65
+ const isLoading = ref(false);
66
+
67
+ // RmSourceCard ne porte que le francais et l'anglais ; les autres locales
68
+ // retombent sur l'anglais plutot que d'afficher des libelles vides.
69
+ const cardLang = computed<"fr" | "en">(() => (props.lang === "fr" ? "fr" : "en"));
70
+
71
+ const reorderedUuids = computed(() => {
72
+ const uuids = Array.isArray(props.uuids) ? [...new Set(props.uuids)] : [];
73
+ if (props.sourceToOpen && uuids.includes(props.sourceToOpen)) {
74
+ return [props.sourceToOpen, ...uuids.filter((u) => u !== props.sourceToOpen)];
75
+ }
76
+ return uuids;
77
+ });
78
+
79
+ function findSource(uuid: string) {
80
+ return sources.value.find((s) => s?.source_uuid === uuid);
81
+ }
82
+
83
+ function getTranslated(source: any) {
84
+ const provider = source?.provider;
85
+ const pick = (arr: any[]) =>
86
+ arr?.find((t: any) =>
87
+ (t?.languages_code?.code || t?.languages_code || "").startsWith(props.lang)
88
+ ) || arr?.[0];
89
+ const st = pick(source?.translations || []);
90
+ const pt = pick(provider?.translations || []);
91
+ return {
92
+ providerName: pt?.provider_name || "",
93
+ providerDescription: pt?.provider_description || "",
94
+ sourceName: st?.source_name || source?.Nickname || "",
95
+ sourceDescription: st?.source_description || "",
96
+ };
97
+ }
98
+
99
+ function constructTitle(uuid: string) {
100
+ const t = getTranslated(findSource(uuid));
101
+ return [t.providerName, t.sourceName].filter(Boolean).join(" - ");
102
+ }
103
+
104
+ function getDescriptions(uuid: string) {
105
+ const t = getTranslated(findSource(uuid));
106
+ return { provider: t.providerDescription, source: t.sourceDescription };
107
+ }
108
+
109
+ function getCountry(uuid: string) {
110
+ const provider = findSource(uuid)?.provider;
111
+ return {
112
+ label: provider?.geo_zones?.country_label || provider?.provider_country || "",
113
+ // Sans code ISO (source mondiale, zone non renseignee), la fiche pose un
114
+ // globe : un drapeau devine serait faux.
115
+ iso: provider?.geo_zones?.country_iso || undefined,
116
+ };
117
+ }
118
+
119
+ function getFreshness(uuid: string) {
120
+ const source = findSource(uuid);
121
+ return {
122
+ lastUpdate: source?.latest_download_date || source?.source_last_updated || null,
123
+ frequency: source?.update_frequency || null,
124
+ };
125
+ }
126
+
127
+ function getTags(uuid: string) {
128
+ const tags = findSource(uuid)?.tags_terms;
129
+ return Array.isArray(tags) ? tags : [];
130
+ }
131
+
132
+ /**
133
+ * Crayon de correction : le droit est celui de la console d'OIP 7, passe par
134
+ * le cadre (`editable`). La route serveur le revalide : cacher le bouton ne
135
+ * protege rien.
136
+ */
137
+ const canEdit = computed(() => props.editable);
138
+
139
+ const isEditorOpen = ref(false);
140
+ const editingUuid = ref<string | null>(null);
141
+ const editing = computed(() => (editingUuid.value ? findSource(editingUuid.value) : null));
142
+
143
+ function openEditor(uuid: string) {
144
+ editingUuid.value = uuid;
145
+ isEditorOpen.value = true;
146
+ }
147
+
148
+ function onSaved() {
149
+ fetchSources();
150
+ }
151
+
152
+ const notify = useOipNotify();
153
+ const sourcesEndpoint = useAppConfig().oip.panels.Sources.endpoint;
154
+
155
+ function notifyCopy() {
156
+ notify(cardLang.value === "fr" ? "Titre de la source copié" : "Source title copied");
157
+ }
158
+
159
+ async function fetchSources() {
160
+ const uuids = (Array.isArray(props.uuids) ? props.uuids : []).filter(Boolean);
161
+ if (!uuids.length) {
162
+ sources.value = [];
163
+ return;
164
+ }
165
+ isLoading.value = true;
166
+ try {
167
+ const res = await $fetch<{ sources: any[] }>(sourcesEndpoint, {
168
+ method: "POST",
169
+ body: { uuids: [...new Set(uuids)] },
170
+ });
171
+ sources.value = res.sources ?? [];
172
+ emit("updateSources", sources.value);
173
+ } catch (err: any) {
174
+ console.warn("Failed to load sources:", err?.message || err);
175
+ sources.value = [];
176
+ } finally {
177
+ isLoading.value = false;
178
+ }
179
+ }
180
+
181
+ onMounted(fetchSources);
182
+ watch(() => props.uuids, fetchSources, { deep: true });
183
+ </script>
184
+
185
+ <style scoped>
186
+ .oip-src-list {
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 12px;
190
+ width: 100%;
191
+ }
192
+ </style>
@@ -0,0 +1,158 @@
1
+ <!--
2
+ Apercu d'un PDF dans un tiroir (layer OIP).
3
+
4
+ Le dessin du panneau « Documents » du rail, sorti la pour que les autres
5
+ ecrans de l'app ouvrent leurs PDF de la meme facon : comptes rendus de comite,
6
+ ordres du jour, transcriptions de debat. C'est fait pour jeter un oeil ; les
7
+ deux sorties de l'entete mènent au document lui-meme.
8
+
9
+ `RmDrawer` accepte une largeur libre en plus de ses tailles nommees. `xl`
10
+ (860px) coupait les pages A4 en deux colonnes illisibles ; la borne en `vw`
11
+ garde le tiroir sous la largeur de l'ecran.
12
+
13
+ Le document passe par `RmDocumentViewer` pour qu'on puisse chercher dedans
14
+ (PDF ou page HTML) : le lecteur du navigateur, dans une iframe, ne laissait
15
+ que Ctrl+F. Un fichier qui ne se lit pas (hote qui refuse CORS, format
16
+ inconnu) retombe sur l'iframe.
17
+ -->
18
+ <template>
19
+ <RmDrawer
20
+ v-model="open"
21
+ size="min(1180px, 94vw)"
22
+ :z-index="zIndex"
23
+ :label="title"
24
+ :close-label="locale === 'en' ? 'Close the preview' : 'Fermer l’aperçu'"
25
+ >
26
+ <template #header>
27
+ <div class="preview_head">
28
+ <p class="preview_title">{{ title }}</p>
29
+ <div class="preview_actions">
30
+ <a
31
+ v-if="openHref"
32
+ class="preview_action"
33
+ :href="openHref"
34
+ target="_blank"
35
+ rel="noopener noreferrer"
36
+ >
37
+ <i class="ph ph-arrow-square-out" />
38
+ {{ locale === "en" ? "New tab" : "Nouvel onglet" }}
39
+ </a>
40
+ <a v-if="downloadSrc" class="preview_action" :href="downloadSrc ?? undefined">
41
+ <i class="ph ph-download-simple" />
42
+ {{ locale === "en" ? "Download" : "Télécharger" }}
43
+ </a>
44
+ </div>
45
+ </div>
46
+ </template>
47
+
48
+ <template v-if="src">
49
+ <iframe v-if="fallback" class="preview_frame" :src="fallbackSrc || src" :title="title" />
50
+ <RmDocumentViewer
51
+ v-else
52
+ :src="src"
53
+ :query="query"
54
+ :anchor="anchor"
55
+ :base="fallbackSrc"
56
+ :lang="locale === 'en' ? 'en' : 'fr'"
57
+ @error="fallback = true"
58
+ />
59
+ </template>
60
+ </RmDrawer>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ const props = withDefaults(
65
+ defineProps<{
66
+ /** Ce que le cadre affiche. Un PDF, et servi comme tel. */
67
+ src: string | null;
68
+ title?: string;
69
+ /** Lien de telechargement. Absent, la sortie n'est pas proposee. */
70
+ downloadSrc?: string | null;
71
+ /** Le document chez son producteur, quand `src` passe par nous. */
72
+ externalSrc?: string | null;
73
+ /**
74
+ * Plan de superposition, pour un ecran qui pose son propre voile au-dessus
75
+ * du cadre : sans lui l'apercu s'ouvre derriere.
76
+ */
77
+ zIndex?: number | string;
78
+ /** Terme(s) a chercher dans le document des l'ouverture. */
79
+ query?: string | readonly string[] | null;
80
+ /** Passage a viser dans le document : la lecture s'ouvre a cet endroit. */
81
+ anchor?: string | null;
82
+ /**
83
+ * Le document chez nous sans proxy, quand `src` passe par `/api/pdf` :
84
+ * l'iframe de repli l'affiche (le proxy sert tout en PDF, une page HTML
85
+ * n'y survivrait pas) et les liens relatifs d'une page HTML s'y
86
+ * resolvent.
87
+ */
88
+ fallbackSrc?: string | null;
89
+ }>(),
90
+ {
91
+ title: "",
92
+ downloadSrc: null,
93
+ externalSrc: null,
94
+ zIndex: undefined,
95
+ query: null,
96
+ anchor: null,
97
+ fallbackSrc: null,
98
+ },
99
+ );
100
+
101
+ const open = defineModel<boolean>({ default: false });
102
+
103
+ /** Le document chez son producteur quand on le connait, sinon ce qu'on encadre. */
104
+ const openHref = computed(() => props.externalSrc || props.src || undefined);
105
+
106
+ const { locale } = useI18n();
107
+
108
+ /** Le lecteur n'a pas su lire `src` : on le confie au navigateur. */
109
+ const fallback = ref(false);
110
+ watch(
111
+ () => props.src,
112
+ () => {
113
+ fallback.value = false;
114
+ },
115
+ );
116
+ </script>
117
+
118
+ <style scoped>
119
+ .preview_head {
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 6px;
123
+ min-width: 0;
124
+ }
125
+
126
+ .preview_title {
127
+ margin: 0;
128
+ color: var(--text-primary);
129
+ font-size: var(--font-14);
130
+ font-weight: var(--weight-semibold);
131
+ }
132
+
133
+ .preview_actions {
134
+ display: flex;
135
+ gap: 12px;
136
+ }
137
+
138
+ .preview_action {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: 4px;
142
+ color: var(--color-secondary);
143
+ font-size: var(--font-12);
144
+ font-weight: var(--weight-medium);
145
+ text-decoration: none;
146
+ }
147
+
148
+ .preview_action:hover {
149
+ text-decoration: underline;
150
+ }
151
+
152
+ .preview_frame {
153
+ width: 100%;
154
+ height: 100%;
155
+ min-height: 70vh;
156
+ border: none;
157
+ }
158
+ </style>