vintasend 0.4.0 → 0.4.7

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 (284) hide show
  1. package/README.md +5 -0
  2. package/dist/examples/vintasend-medplum-example/.env.example +11 -0
  3. package/dist/examples/vintasend-medplum-example/IMPLEMENTATION_PLAN_FILE_ATTACHMENTS.md +597 -0
  4. package/dist/examples/vintasend-medplum-example/README.md +190 -0
  5. package/dist/examples/vintasend-medplum-example/TUTORIAL_EMAIL_NOTIFICATIONS.md +2596 -0
  6. package/dist/examples/vintasend-medplum-example/bots/handlers/send-pending-notifications-bot.ts +39 -0
  7. package/dist/examples/vintasend-medplum-example/bots/handlers/send-task-assignment-email.ts +41 -0
  8. package/dist/examples/vintasend-medplum-example/bots/handlers/task-due-soon-notification-bot.ts +86 -0
  9. package/dist/examples/vintasend-medplum-example/bots/index.ts +53 -0
  10. package/dist/examples/vintasend-medplum-example/bots/services/emails/schedule-task-due-soon-email.ts +84 -0
  11. package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.test.ts +388 -0
  12. package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.ts +113 -0
  13. package/dist/examples/vintasend-medplum-example/bots/shared/task-due-soon-helpers.ts +115 -0
  14. package/dist/examples/vintasend-medplum-example/bots/task-assignment-bot.ts +41 -0
  15. package/dist/examples/vintasend-medplum-example/compiled-notification-templates.json +6 -0
  16. package/dist/examples/vintasend-medplum-example/esbuild-script.mjs +71 -0
  17. package/dist/examples/vintasend-medplum-example/index.html +14 -0
  18. package/dist/examples/vintasend-medplum-example/lib/constants.ts +32 -0
  19. package/dist/examples/vintasend-medplum-example/lib/extensions.ts +1 -0
  20. package/dist/examples/vintasend-medplum-example/lib/file-upload.test.ts +389 -0
  21. package/dist/examples/vintasend-medplum-example/lib/file-upload.ts +222 -0
  22. package/dist/examples/vintasend-medplum-example/lib/medplum-singleton.ts +18 -0
  23. package/dist/examples/vintasend-medplum-example/lib/notification-service.test.ts +293 -0
  24. package/dist/examples/vintasend-medplum-example/lib/notification-service.ts +284 -0
  25. package/dist/examples/vintasend-medplum-example/lib/patients.ts +20 -0
  26. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/body.html.pug +37 -0
  27. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/subject.txt.pug +4 -0
  28. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/body.html.pug +34 -0
  29. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/subject.txt.pug +4 -0
  30. package/dist/examples/vintasend-medplum-example/package.json +75 -0
  31. package/dist/examples/vintasend-medplum-example/plugins/gql-plugin.mjs +31 -0
  32. package/dist/examples/vintasend-medplum-example/postcss.config.mjs +21 -0
  33. package/dist/examples/vintasend-medplum-example/public/favicon.ico +0 -0
  34. package/dist/examples/vintasend-medplum-example/public/img/integrations/acuity.png +0 -0
  35. package/dist/examples/vintasend-medplum-example/public/img/integrations/auth0.png +0 -0
  36. package/dist/examples/vintasend-medplum-example/public/img/integrations/azure.png +0 -0
  37. package/dist/examples/vintasend-medplum-example/public/img/integrations/calcom.png +0 -0
  38. package/dist/examples/vintasend-medplum-example/public/img/integrations/candid.png +0 -0
  39. package/dist/examples/vintasend-medplum-example/public/img/integrations/claude.png +0 -0
  40. package/dist/examples/vintasend-medplum-example/public/img/integrations/datadog.png +0 -0
  41. package/dist/examples/vintasend-medplum-example/public/img/integrations/deepseek.png +0 -0
  42. package/dist/examples/vintasend-medplum-example/public/img/integrations/entra.png +0 -0
  43. package/dist/examples/vintasend-medplum-example/public/img/integrations/epic.png +0 -0
  44. package/dist/examples/vintasend-medplum-example/public/img/integrations/google.png +0 -0
  45. package/dist/examples/vintasend-medplum-example/public/img/integrations/healthgorilla.png +0 -0
  46. package/dist/examples/vintasend-medplum-example/public/img/integrations/healthie.png +0 -0
  47. package/dist/examples/vintasend-medplum-example/public/img/integrations/labcorp.png +0 -0
  48. package/dist/examples/vintasend-medplum-example/public/img/integrations/okta.png +0 -0
  49. package/dist/examples/vintasend-medplum-example/public/img/integrations/openai.png +0 -0
  50. package/dist/examples/vintasend-medplum-example/public/img/integrations/particle.png +0 -0
  51. package/dist/examples/vintasend-medplum-example/public/img/integrations/quest.png +0 -0
  52. package/dist/examples/vintasend-medplum-example/public/img/integrations/recaptcha.png +0 -0
  53. package/dist/examples/vintasend-medplum-example/public/img/integrations/snowflake.png +0 -0
  54. package/dist/examples/vintasend-medplum-example/public/img/integrations/stedi.png +0 -0
  55. package/dist/examples/vintasend-medplum-example/public/img/integrations/stripe.png +0 -0
  56. package/dist/examples/vintasend-medplum-example/public/img/integrations/sumo.png +0 -0
  57. package/dist/examples/vintasend-medplum-example/scripts/README.md +162 -0
  58. package/dist/examples/vintasend-medplum-example/scripts/client.ts +18 -0
  59. package/dist/examples/vintasend-medplum-example/scripts/deploy-bots.ts +171 -0
  60. package/dist/examples/vintasend-medplum-example/src/App.tsx +185 -0
  61. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.test.tsx +350 -0
  62. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.tsx +241 -0
  63. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.test.tsx +616 -0
  64. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.tsx +138 -0
  65. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.test.tsx +92 -0
  66. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.tsx +47 -0
  67. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.test.tsx +464 -0
  68. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.tsx +186 -0
  69. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.test.tsx +80 -0
  70. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.tsx +82 -0
  71. package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.test.tsx +100 -0
  72. package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.tsx +20 -0
  73. package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.module.css +3 -0
  74. package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.tsx +62 -0
  75. package/dist/examples/vintasend-medplum-example/src/components/MessageWithLinks.tsx +47 -0
  76. package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.test.tsx +299 -0
  77. package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.tsx +52 -0
  78. package/dist/examples/vintasend-medplum-example/src/components/ResourceFormWithRequiredProfile.tsx +82 -0
  79. package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.test.tsx +1016 -0
  80. package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.tsx +298 -0
  81. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.test.tsx +732 -0
  82. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.tsx +282 -0
  83. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.test.tsx +268 -0
  84. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.tsx +224 -0
  85. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.test.tsx +255 -0
  86. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.tsx +212 -0
  87. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.test.tsx +120 -0
  88. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.tsx +57 -0
  89. package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.test.tsx +224 -0
  90. package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.tsx +100 -0
  91. package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.test.tsx +431 -0
  92. package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.tsx +130 -0
  93. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.module.css +31 -0
  94. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.test.tsx +234 -0
  95. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.tsx +143 -0
  96. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.module.css +11 -0
  97. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.test.tsx +875 -0
  98. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.tsx +943 -0
  99. package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.test.tsx +413 -0
  100. package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.tsx +203 -0
  101. package/dist/examples/vintasend-medplum-example/src/components/labs/LabSelectEmpty.tsx +22 -0
  102. package/dist/examples/vintasend-medplum-example/src/components/labs/README.md +104 -0
  103. package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.test.tsx +318 -0
  104. package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.tsx +87 -0
  105. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.test.tsx +126 -0
  106. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.tsx +38 -0
  107. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.module.css +23 -0
  108. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.test.tsx +167 -0
  109. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.tsx +53 -0
  110. package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.test.tsx +94 -0
  111. package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.tsx +165 -0
  112. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.module.css +8 -0
  113. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.test.tsx +523 -0
  114. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.tsx +230 -0
  115. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.module.css +23 -0
  116. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.test.tsx +567 -0
  117. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.tsx +358 -0
  118. package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.module.css +40 -0
  119. package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.tsx +257 -0
  120. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.module.css +7 -0
  121. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.test.tsx +279 -0
  122. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.tsx +156 -0
  123. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.module.css +45 -0
  124. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.test.tsx +90 -0
  125. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.tsx +84 -0
  126. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.module.css +26 -0
  127. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.tsx +90 -0
  128. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.test.tsx +305 -0
  129. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.tsx +46 -0
  130. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.module.css +262 -0
  131. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.test.tsx +622 -0
  132. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.tsx +286 -0
  133. package/dist/examples/vintasend-medplum-example/src/components/tasks/NewTaskModal.tsx +275 -0
  134. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskAttachmentList.tsx +132 -0
  135. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.module.css +45 -0
  136. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.test.tsx +749 -0
  137. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.tsx +416 -0
  138. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.test.tsx +278 -0
  139. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.tsx +133 -0
  140. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.module.css +16 -0
  141. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.test.tsx +255 -0
  142. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.tsx +203 -0
  143. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFileUpload.tsx +129 -0
  144. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.test.tsx +156 -0
  145. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.tsx +142 -0
  146. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.utils.ts +28 -0
  147. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.test.tsx +134 -0
  148. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.tsx +250 -0
  149. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.module.css +23 -0
  150. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.test.tsx +149 -0
  151. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.tsx +53 -0
  152. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.test.tsx +68 -0
  153. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.tsx +46 -0
  154. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.test.tsx +555 -0
  155. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.tsx +170 -0
  156. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.test.tsx +32 -0
  157. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.tsx +34 -0
  158. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.test.tsx +47 -0
  159. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.tsx +29 -0
  160. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.test.tsx +285 -0
  161. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.tsx +129 -0
  162. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.test.tsx +455 -0
  163. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.tsx +167 -0
  164. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.test.tsx +435 -0
  165. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.tsx +116 -0
  166. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.module.css +38 -0
  167. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.test.tsx +200 -0
  168. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.tsx +84 -0
  169. package/dist/examples/vintasend-medplum-example/src/components/utils.test.ts +176 -0
  170. package/dist/examples/vintasend-medplum-example/src/components/utils.ts +17 -0
  171. package/dist/examples/vintasend-medplum-example/src/config/constants.ts +3 -0
  172. package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.test.tsx +166 -0
  173. package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.ts +28 -0
  174. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.test.tsx +94 -0
  175. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.ts +11 -0
  176. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.test.tsx +477 -0
  177. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.ts +191 -0
  178. package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.test.tsx +100 -0
  179. package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.ts +18 -0
  180. package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.test.tsx +379 -0
  181. package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.ts +194 -0
  182. package/dist/examples/vintasend-medplum-example/src/index.css +8 -0
  183. package/dist/examples/vintasend-medplum-example/src/main.tsx +57 -0
  184. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.module.css +6 -0
  185. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.test.tsx +295 -0
  186. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.tsx +124 -0
  187. package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.test.tsx +77 -0
  188. package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.tsx +22 -0
  189. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.test.tsx +87 -0
  190. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.tsx +27 -0
  191. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.module.css +16 -0
  192. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.test.tsx +287 -0
  193. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.tsx +151 -0
  194. package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.test.tsx +519 -0
  195. package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.tsx +179 -0
  196. package/dist/examples/vintasend-medplum-example/src/pages/integrations/FavoriteMedicationsTable.tsx +76 -0
  197. package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.test.tsx +234 -0
  198. package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.tsx +222 -0
  199. package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.test.tsx +356 -0
  200. package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.tsx +275 -0
  201. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.module.css +8 -0
  202. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.test.tsx +103 -0
  203. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.tsx +78 -0
  204. package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.test.tsx +84 -0
  205. package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.tsx +82 -0
  206. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.test.tsx +364 -0
  207. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.tsx +149 -0
  208. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.test.tsx +159 -0
  209. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.tsx +37 -0
  210. package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.test.tsx +140 -0
  211. package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.tsx +72 -0
  212. package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.test.tsx +57 -0
  213. package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.tsx +14 -0
  214. package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.test.tsx +241 -0
  215. package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.tsx +710 -0
  216. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.module.css +37 -0
  217. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.test.tsx +428 -0
  218. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.tsx +334 -0
  219. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.module.css +24 -0
  220. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.test.tsx +154 -0
  221. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.tsx +115 -0
  222. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.test.ts +223 -0
  223. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.ts +89 -0
  224. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.test.tsx +147 -0
  225. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.tsx +79 -0
  226. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientTabsNavigation.tsx +35 -0
  227. package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.test.tsx +185 -0
  228. package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.tsx +115 -0
  229. package/dist/examples/vintasend-medplum-example/src/pages/patient/TimelineTab.tsx +14 -0
  230. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.test.tsx +170 -0
  231. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.tsx +117 -0
  232. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceDetailPage.tsx +28 -0
  233. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.test.tsx +131 -0
  234. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.tsx +65 -0
  235. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.test.tsx +108 -0
  236. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.tsx +16 -0
  237. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.module.css +7 -0
  238. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.test.tsx +37 -0
  239. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.tsx +44 -0
  240. package/dist/examples/vintasend-medplum-example/src/pages/resource/useResourceType.ts +44 -0
  241. package/dist/examples/vintasend-medplum-example/src/pages/resource/utils.ts +9 -0
  242. package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.test.tsx +302 -0
  243. package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.tsx +416 -0
  244. package/dist/examples/vintasend-medplum-example/src/pages/spaces/ChatInput.tsx +91 -0
  245. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.module.css +6 -0
  246. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.test.tsx +102 -0
  247. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.tsx +44 -0
  248. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.module.css +7 -0
  249. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.test.tsx +133 -0
  250. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.tsx +91 -0
  251. package/dist/examples/vintasend-medplum-example/src/test-utils/render.tsx +20 -0
  252. package/dist/examples/vintasend-medplum-example/src/test.setup.ts +49 -0
  253. package/dist/examples/vintasend-medplum-example/src/types/encounter.ts +8 -0
  254. package/dist/examples/vintasend-medplum-example/src/types/spaces.ts +10 -0
  255. package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.test.ts +141 -0
  256. package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.ts +59 -0
  257. package/dist/examples/vintasend-medplum-example/src/utils/claims.test.ts +153 -0
  258. package/dist/examples/vintasend-medplum-example/src/utils/claims.ts +65 -0
  259. package/dist/examples/vintasend-medplum-example/src/utils/communication-search.ts +47 -0
  260. package/dist/examples/vintasend-medplum-example/src/utils/coverage.test.ts +48 -0
  261. package/dist/examples/vintasend-medplum-example/src/utils/coverage.ts +33 -0
  262. package/dist/examples/vintasend-medplum-example/src/utils/documentReference.test.ts +102 -0
  263. package/dist/examples/vintasend-medplum-example/src/utils/documentReference.ts +55 -0
  264. package/dist/examples/vintasend-medplum-example/src/utils/encounter.test.ts +169 -0
  265. package/dist/examples/vintasend-medplum-example/src/utils/encounter.ts +261 -0
  266. package/dist/examples/vintasend-medplum-example/src/utils/intake-form.test.ts +154 -0
  267. package/dist/examples/vintasend-medplum-example/src/utils/intake-form.ts +272 -0
  268. package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.test.ts +1137 -0
  269. package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.ts +827 -0
  270. package/dist/examples/vintasend-medplum-example/src/utils/notifications.test.ts +27 -0
  271. package/dist/examples/vintasend-medplum-example/src/utils/notifications.ts +15 -0
  272. package/dist/examples/vintasend-medplum-example/src/utils/spaceMessaging.ts +249 -0
  273. package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.test.ts +450 -0
  274. package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.ts +147 -0
  275. package/dist/examples/vintasend-medplum-example/src/utils/task-search.ts +63 -0
  276. package/dist/examples/vintasend-medplum-example/src/vite-env.d.ts +3 -0
  277. package/dist/examples/vintasend-medplum-example/tsconfig.bots.json +4 -0
  278. package/dist/examples/vintasend-medplum-example/tsconfig.json +19 -0
  279. package/dist/examples/vintasend-medplum-example/vercel.json +3 -0
  280. package/dist/examples/vintasend-medplum-example/vite.config.ts +44 -0
  281. package/dist/services/notification-backends/base-notification-backend.d.ts +5 -0
  282. package/dist/services/notification-service.js +11 -1
  283. package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +5 -0
  284. package/package.json +1 -1
@@ -0,0 +1,293 @@
1
+ // @ts-nocheck - MockClient type compatibility with MedplumClient
2
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
+ import { MockClient } from '@medplum/mock';
4
+ import type { Binary, Media } from '@medplum/fhirtypes';
5
+ import { convertMediaToAttachment } from './notification-service';
6
+
7
+ describe('convertMediaToAttachment', () => {
8
+ let medplum: MockClient;
9
+
10
+ beforeEach(() => {
11
+ medplum = new MockClient();
12
+ });
13
+
14
+ it('should convert Media resource to VintaSend attachment format', async () => {
15
+ // Create a binary resource with base64 data
16
+ const binaryContent = Buffer.from('test file content');
17
+ const binary = await medplum.createResource<Binary>({
18
+ resourceType: 'Binary',
19
+ contentType: 'application/pdf',
20
+ data: binaryContent.toString('base64'),
21
+ });
22
+
23
+ // Create a media resource
24
+ const media = await medplum.createResource<Media>({
25
+ resourceType: 'Media',
26
+ status: 'completed',
27
+ content: {
28
+ contentType: 'application/pdf',
29
+ url: `Binary/${binary.id}`,
30
+ title: 'test-document.pdf',
31
+ },
32
+ });
33
+
34
+ const result = await convertMediaToAttachment(medplum, media);
35
+
36
+ expect(result).toBeDefined();
37
+ expect(result).not.toBeNull();
38
+ expect(result?.filename).toBe('test-document.pdf');
39
+ expect(result?.contentType).toBe('application/pdf');
40
+ expect(result?.file).toBeInstanceOf(Buffer);
41
+ expect(result?.file.toString()).toBe('test file content');
42
+ });
43
+
44
+ it('should handle different file types correctly', async () => {
45
+ const testCases = [
46
+ {
47
+ contentType: 'image/jpeg',
48
+ filename: 'photo.jpg',
49
+ content: 'jpeg image data',
50
+ },
51
+ {
52
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
53
+ filename: 'document.docx',
54
+ content: 'word document data',
55
+ },
56
+ {
57
+ contentType: 'text/plain',
58
+ filename: 'notes.txt',
59
+ content: 'plain text content',
60
+ },
61
+ ];
62
+
63
+ for (const testCase of testCases) {
64
+ const binary = await medplum.createResource<Binary>({
65
+ resourceType: 'Binary',
66
+ contentType: testCase.contentType,
67
+ data: Buffer.from(testCase.content).toString('base64'),
68
+ });
69
+
70
+ const media = await medplum.createResource<Media>({
71
+ resourceType: 'Media',
72
+ status: 'completed',
73
+ content: {
74
+ contentType: testCase.contentType,
75
+ url: `Binary/${binary.id}`,
76
+ title: testCase.filename,
77
+ },
78
+ });
79
+
80
+ const result = await convertMediaToAttachment(medplum, media);
81
+
82
+ expect(result?.filename).toBe(testCase.filename);
83
+ expect(result?.contentType).toBe(testCase.contentType);
84
+ expect(result?.file.toString()).toBe(testCase.content);
85
+ }
86
+ });
87
+
88
+ it('should use default filename when Media has no title', async () => {
89
+ const binary = await medplum.createResource<Binary>({
90
+ resourceType: 'Binary',
91
+ contentType: 'application/pdf',
92
+ data: Buffer.from('content').toString('base64'),
93
+ });
94
+
95
+ const media = await medplum.createResource<Media>({
96
+ resourceType: 'Media',
97
+ status: 'completed',
98
+ content: {
99
+ contentType: 'application/pdf',
100
+ url: `Binary/${binary.id}`,
101
+ // No title provided
102
+ },
103
+ });
104
+
105
+ const result = await convertMediaToAttachment(medplum, media);
106
+
107
+ expect(result?.filename).toBe('attachment');
108
+ });
109
+
110
+ it('should use default contentType when Media has no contentType', async () => {
111
+ const binary = await medplum.createResource<Binary>({
112
+ resourceType: 'Binary',
113
+ contentType: 'application/octet-stream',
114
+ data: Buffer.from('content').toString('base64'),
115
+ });
116
+
117
+ const media = await medplum.createResource<Media>({
118
+ resourceType: 'Media',
119
+ status: 'completed',
120
+ content: {
121
+ url: `Binary/${binary.id}`,
122
+ title: 'file.bin',
123
+ // No contentType provided
124
+ },
125
+ });
126
+
127
+ const result = await convertMediaToAttachment(medplum, media);
128
+
129
+ expect(result?.contentType).toBe('application/octet-stream');
130
+ });
131
+
132
+ it('should return null when Binary resource cannot be fetched', async () => {
133
+ const media = await medplum.createResource<Media>({
134
+ resourceType: 'Media',
135
+ status: 'completed',
136
+ content: {
137
+ contentType: 'application/pdf',
138
+ url: 'Binary/nonexistent-binary-id',
139
+ title: 'test.pdf',
140
+ },
141
+ });
142
+
143
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
144
+
145
+ const result = await convertMediaToAttachment(medplum, media);
146
+
147
+ expect(result).toBeNull();
148
+ expect(consoleErrorSpy).toHaveBeenCalled();
149
+
150
+ consoleErrorSpy.mockRestore();
151
+ });
152
+
153
+ it('should return null when Media has no content URL', async () => {
154
+ const media = await medplum.createResource<Media>({
155
+ resourceType: 'Media',
156
+ status: 'completed',
157
+ // No content object
158
+ });
159
+
160
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
161
+
162
+ const result = await convertMediaToAttachment(medplum, media);
163
+
164
+ expect(result).toBeNull();
165
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
166
+ '[getBinaryFromMedia] Media resource has no content URL'
167
+ );
168
+
169
+ consoleErrorSpy.mockRestore();
170
+ });
171
+
172
+ it('should return null when Binary has no data field', async () => {
173
+ const binary = await medplum.createResource<Binary>({
174
+ resourceType: 'Binary',
175
+ contentType: 'application/pdf',
176
+ // No data field
177
+ });
178
+
179
+ const media = await medplum.createResource<Media>({
180
+ resourceType: 'Media',
181
+ status: 'completed',
182
+ content: {
183
+ contentType: 'application/pdf',
184
+ url: `Binary/${binary.id}`,
185
+ title: 'test.pdf',
186
+ },
187
+ });
188
+
189
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
190
+
191
+ const result = await convertMediaToAttachment(medplum, media);
192
+
193
+ expect(result).toBeNull();
194
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
195
+ '[convertMediaToAttachment] Binary resource has no data:',
196
+ binary.id
197
+ );
198
+
199
+ consoleErrorSpy.mockRestore();
200
+ });
201
+
202
+ it('should handle base64 decoding correctly', async () => {
203
+ const originalContent = 'Hello, World! This is a test file with special characters: @#$%^&*()';
204
+ const binary = await medplum.createResource<Binary>({
205
+ resourceType: 'Binary',
206
+ contentType: 'text/plain',
207
+ data: Buffer.from(originalContent).toString('base64'),
208
+ });
209
+
210
+ const media = await medplum.createResource<Media>({
211
+ resourceType: 'Media',
212
+ status: 'completed',
213
+ content: {
214
+ contentType: 'text/plain',
215
+ url: `Binary/${binary.id}`,
216
+ title: 'test.txt',
217
+ },
218
+ });
219
+
220
+ const result = await convertMediaToAttachment(medplum, media);
221
+
222
+ expect(result?.file.toString()).toBe(originalContent);
223
+ });
224
+
225
+ it('should handle large files', async () => {
226
+ // Create a 1MB file
227
+ const largeContent = Buffer.alloc(1024 * 1024, 'a');
228
+ const binary = await medplum.createResource<Binary>({
229
+ resourceType: 'Binary',
230
+ contentType: 'application/octet-stream',
231
+ data: largeContent.toString('base64'),
232
+ });
233
+
234
+ const media = await medplum.createResource<Media>({
235
+ resourceType: 'Media',
236
+ status: 'completed',
237
+ content: {
238
+ contentType: 'application/octet-stream',
239
+ url: `Binary/${binary.id}`,
240
+ title: 'large-file.bin',
241
+ },
242
+ });
243
+
244
+ const result = await convertMediaToAttachment(medplum, media);
245
+
246
+ expect(result).toBeDefined();
247
+ expect(result?.file.length).toBe(1024 * 1024);
248
+ });
249
+
250
+ it('should handle binary content with special characters in filename', async () => {
251
+ const binary = await medplum.createResource<Binary>({
252
+ resourceType: 'Binary',
253
+ contentType: 'application/pdf',
254
+ data: Buffer.from('content').toString('base64'),
255
+ });
256
+
257
+ const media = await medplum.createResource<Media>({
258
+ resourceType: 'Media',
259
+ status: 'completed',
260
+ content: {
261
+ contentType: 'application/pdf',
262
+ url: `Binary/${binary.id}`,
263
+ title: 'test document (v2) [final].pdf',
264
+ },
265
+ });
266
+
267
+ const result = await convertMediaToAttachment(medplum, media);
268
+
269
+ expect(result?.filename).toBe('test document (v2) [final].pdf');
270
+ });
271
+
272
+ it('should handle conversion errors gracefully', async () => {
273
+ // Create media with invalid Binary URL format
274
+ const media = await medplum.createResource<Media>({
275
+ resourceType: 'Media',
276
+ status: 'completed',
277
+ content: {
278
+ contentType: 'application/pdf',
279
+ url: 'InvalidFormat/NoSlash',
280
+ title: 'test.pdf',
281
+ },
282
+ });
283
+
284
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
285
+
286
+ const result = await convertMediaToAttachment(medplum, media);
287
+
288
+ expect(result).toBeNull();
289
+ expect(consoleErrorSpy).toHaveBeenCalled();
290
+
291
+ consoleErrorSpy.mockRestore();
292
+ });
293
+ });
@@ -0,0 +1,284 @@
1
+ import { MedplumClient } from '@medplum/core';
2
+ import type { BotEvent } from '@medplum/core';
3
+ import type { ContextGenerator } from 'vintasend';
4
+ import { VintaSendFactory } from 'vintasend';
5
+ import { MedplumSingleton } from './medplum-singleton';
6
+ import { formatPatientNameWithPreferredName } from './patients';
7
+ import * as compiledTemplates from '../compiled-notification-templates.json';
8
+ import {
9
+ MedplumNotificationBackend,
10
+ MedplumAttachmentManager,
11
+ PugInlineEmailTemplateRendererFactory,
12
+ MedplumLogger,
13
+ } from 'vintasend-medplum';
14
+ import { SendgridNotificationAdapterFactory } from 'vintasend-sendgrid';
15
+ import type { Media } from '@medplum/fhirtypes';
16
+ import { getBinaryFromMedia } from './file-upload';
17
+
18
+ async function getUserById(medplum: MedplumClient, referenceString: string) {
19
+ if (!referenceString) {
20
+ // eslint-disable-next-line no-console
21
+ console.error('[getUserById] referenceString is null/undefined/empty!');
22
+ throw new Error('The "id" parameter cannot be null, undefined, or an empty string.');
23
+ }
24
+
25
+ const [resourceType, id] = referenceString.split('/');
26
+
27
+ if (!id) {
28
+ // eslint-disable-next-line no-console
29
+ console.error('[getUserById] ID extracted from referenceString is empty!');
30
+ throw new Error('The "id" parameter cannot be null, undefined, or an empty string.');
31
+ }
32
+
33
+ return medplum.readResource(resourceType as 'Patient' | 'Practitioner', id);
34
+ }
35
+
36
+ /**
37
+ * Converts a Media resource to VintaSend attachment format.
38
+ *
39
+ * Fetches the Binary resource referenced by the Media and extracts the file data,
40
+ * then returns it in the format expected by VintaSend for email attachments.
41
+ *
42
+ * @param medplum - The Medplum client instance
43
+ * @param media - The Media resource containing the file metadata
44
+ * @returns A NotificationAttachmentUpload object with file, filename, and contentType
45
+ *
46
+ * @example
47
+ * const attachment = await convertMediaToAttachment(medplum, media);
48
+ * // { file: Buffer, filename: 'document.pdf', contentType: 'application/pdf' }
49
+ */
50
+ export async function convertMediaToAttachment(
51
+ medplum: MedplumClient,
52
+ media: Media
53
+ ): Promise<{
54
+ file: Buffer;
55
+ filename: string;
56
+ contentType: string;
57
+ } | null> {
58
+ try {
59
+ // Fetch Binary resource from media.content.url
60
+ const binary = await getBinaryFromMedia(medplum, media);
61
+
62
+ if (!binary) {
63
+ console.error('[convertMediaToAttachment] Failed to fetch Binary resource for Media:', media.id);
64
+ return null;
65
+ }
66
+
67
+ // Extract file data - Binary.data is base64-encoded
68
+ let file: Buffer;
69
+ if (binary.data) {
70
+ // If data is embedded in the Binary resource as base64
71
+ file = Buffer.from(binary.data, 'base64');
72
+ } else {
73
+ // If Binary is stored externally, we need to fetch it via URL
74
+ // This is handled by getBinaryFromMedia
75
+ console.error('[convertMediaToAttachment] Binary resource has no data:', binary.id);
76
+ return null;
77
+ }
78
+
79
+ // Return in VintaSend NotificationAttachmentUpload format
80
+ return {
81
+ file,
82
+ filename: media.content?.title || 'attachment',
83
+ contentType: media.content?.contentType || 'application/octet-stream',
84
+ };
85
+ } catch (error) {
86
+ console.error('[convertMediaToAttachment] Error converting Media to attachment:', error);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ class TaskAssignmentContextGenerator implements ContextGenerator {
92
+ async generate({
93
+ userId,
94
+ taskTitle,
95
+ taskDescription,
96
+ taskIsUrgent,
97
+ taskLink,
98
+ requesterName,
99
+ attachmentCount,
100
+ }: {
101
+ userId: string;
102
+ taskTitle: string;
103
+ taskDescription: string;
104
+ taskIsUrgent: boolean;
105
+ taskLink: string;
106
+ requesterName: string;
107
+ attachmentCount?: number;
108
+ }): Promise<{
109
+ firstName: string;
110
+ taskTitle: string;
111
+ taskDescription: string;
112
+ taskIsUrgent: boolean;
113
+ taskLink: string;
114
+ requesterName: string;
115
+ attachmentCount: number;
116
+ }> {
117
+ const medplum = MedplumSingleton.getInstance();
118
+ const user = await getUserById(medplum, userId);
119
+ const firstName = formatPatientNameWithPreferredName(user.name?.[0]) ?? 'Practitioner';
120
+
121
+ return {
122
+ firstName,
123
+ taskTitle,
124
+ taskDescription,
125
+ taskIsUrgent,
126
+ taskLink,
127
+ requesterName,
128
+ attachmentCount: attachmentCount || 0,
129
+ };
130
+ }
131
+ }
132
+
133
+ class TaskDueSoonContextGenerator implements ContextGenerator {
134
+ async generate({
135
+ userId,
136
+ taskTitle,
137
+ taskDescription,
138
+ taskIsUrgent,
139
+ taskLink,
140
+ dueDate,
141
+ }: {
142
+ userId: string;
143
+ taskTitle: string;
144
+ taskDescription: string;
145
+ taskIsUrgent: boolean;
146
+ taskLink: string;
147
+ dueDate: string;
148
+ }): Promise<{
149
+ firstName: string;
150
+ taskTitle: string;
151
+ taskDescription: string;
152
+ taskIsUrgent: boolean;
153
+ taskLink: string;
154
+ dueDate: string;
155
+ }> {
156
+ const medplum = MedplumSingleton.getInstance();
157
+ const user = await getUserById(medplum, userId);
158
+ const firstName = formatPatientNameWithPreferredName(user.name?.[0]) ?? 'Practitioner';
159
+
160
+ return {
161
+ firstName,
162
+ taskTitle,
163
+ taskDescription,
164
+ taskIsUrgent,
165
+ taskLink,
166
+ dueDate,
167
+ };
168
+ }
169
+ }
170
+
171
+ class InboxMessageContextGenerator implements ContextGenerator {
172
+ async generate({
173
+ userId,
174
+ sender,
175
+ messageContent,
176
+ messageTopic,
177
+ }: {
178
+ userId: string;
179
+ sender: string;
180
+ messageContent: string;
181
+ messageTopic: string;
182
+ }): Promise<{
183
+ firstName: string;
184
+ senderName: string;
185
+ senderFirstName: string;
186
+ messageContent: string;
187
+ messageTopic: string;
188
+ }> {
189
+ const medplum = MedplumSingleton.getInstance();
190
+ const user = await getUserById(medplum, userId);
191
+ const firstName = formatPatientNameWithPreferredName(user.name?.[0]) ?? 'Practitioner';
192
+
193
+ // Extract sender name from reference
194
+ let senderName = 'Unknown';
195
+ let senderFirstName = 'Unknown';
196
+ try {
197
+ const [senderResourceType, senderId] = sender.split('/');
198
+ if (senderId) {
199
+ const senderResource = await medplum.readResource(senderResourceType as 'Patient' | 'Practitioner', senderId);
200
+ senderName = formatPatientNameWithPreferredName(senderResource.name?.[0]) ?? 'Unknown';
201
+ senderFirstName = senderResource.name?.[0]?.given?.[0] || 'Unknown';
202
+ }
203
+ } catch (error) {
204
+ // eslint-disable-next-line no-console
205
+ console.error('[InboxMessageContextGenerator] Error fetching sender:', error);
206
+ }
207
+
208
+ return {
209
+ firstName,
210
+ senderName,
211
+ senderFirstName,
212
+ messageContent,
213
+ messageTopic,
214
+ };
215
+ }
216
+ }
217
+
218
+ // context map for generating the context of each notification
219
+ export const contextGeneratorsMap = {
220
+ taskAssignment: new TaskAssignmentContextGenerator(),
221
+ taskDueSoon: new TaskDueSoonContextGenerator(),
222
+ inboxMessage: new InboxMessageContextGenerator(),
223
+ } as const;
224
+
225
+ export type NotificationTypeConfig = {
226
+ ContextMap: typeof contextGeneratorsMap;
227
+ NotificationIdType: string;
228
+ UserIdType: string;
229
+ };
230
+
231
+ export type SendGridConfig = {
232
+ SENDGRID_API_KEY: string;
233
+ SENDGRID_FROM_EMAIL: string;
234
+ SENDGRID_FROM_NAME: string;
235
+ };
236
+
237
+ /**
238
+ * Helper function to build SendGridConfig from bot event secrets
239
+ * Reduces duplication across bot handlers
240
+ * Throws if required secrets (API key or from email) are missing
241
+ */
242
+ export function buildSendGridConfig(event: BotEvent): SendGridConfig {
243
+ const apiKey = event.secrets.SENDGRID_API_KEY?.valueString;
244
+ const fromEmail = event.secrets.SENDGRID_FROM_EMAIL?.valueString;
245
+ const fromName = event.secrets.SENDGRID_FROM_NAME?.valueString || 'Medplum Notifications';
246
+
247
+ if (!apiKey) {
248
+ // eslint-disable-next-line no-console
249
+ console.error('[buildSendGridConfig] SENDGRID_API_KEY secret is missing or empty');
250
+ throw new Error('SENDGRID_API_KEY must be configured in bot secrets');
251
+ }
252
+
253
+ if (!fromEmail) {
254
+ // eslint-disable-next-line no-console
255
+ console.error('[buildSendGridConfig] SENDGRID_FROM_EMAIL secret is missing or empty');
256
+ throw new Error('SENDGRID_FROM_EMAIL must be configured in bot secrets');
257
+ }
258
+
259
+ return {
260
+ SENDGRID_API_KEY: apiKey,
261
+ SENDGRID_FROM_EMAIL: fromEmail,
262
+ SENDGRID_FROM_NAME: fromName,
263
+ };
264
+ }
265
+
266
+ export function getNotificationService(medplum: MedplumClient, sendgridConfig: SendGridConfig) {
267
+ const backend = new MedplumNotificationBackend<NotificationTypeConfig>(medplum);
268
+ const templateRenderer = new PugInlineEmailTemplateRendererFactory<NotificationTypeConfig>().create(
269
+ compiledTemplates
270
+ );
271
+ const adapter = new SendgridNotificationAdapterFactory<NotificationTypeConfig>().create(templateRenderer, false, {
272
+ apiKey: sendgridConfig.SENDGRID_API_KEY || '',
273
+ fromEmail: sendgridConfig.SENDGRID_FROM_EMAIL || '',
274
+ fromName: sendgridConfig.SENDGRID_FROM_NAME,
275
+ });
276
+ return new VintaSendFactory<NotificationTypeConfig>().create(
277
+ [adapter],
278
+ backend,
279
+ new MedplumLogger(),
280
+ contextGeneratorsMap,
281
+ undefined,
282
+ new MedplumAttachmentManager(medplum)
283
+ );
284
+ }
@@ -0,0 +1,20 @@
1
+ import { formatHumanName } from '@medplum/core';
2
+ import { HumanName } from '@medplum/fhirtypes';
3
+ import { PREFERRED_NAME_EXTENSION_URL } from './extensions';
4
+
5
+ export function getPatientPreferredName(patientName: HumanName | undefined): string | undefined {
6
+ if (!patientName) return;
7
+ const preferredName = patientName?.extension?.find(
8
+ (extension) => extension.url === PREFERRED_NAME_EXTENSION_URL
9
+ )?.valueString;
10
+ return preferredName;
11
+ }
12
+
13
+ export function formatPatientNameWithPreferredName(patientName: HumanName | undefined) {
14
+ const preferredName = getPatientPreferredName(patientName);
15
+ if (!preferredName) return formatHumanName(patientName);
16
+ const given = patientName?.given?.join(' ');
17
+ const familyName = patientName?.family;
18
+
19
+ return `${given} (${preferredName}) ${familyName}`;
20
+ }
@@ -0,0 +1,37 @@
1
+ doctype html
2
+ html
3
+ head
4
+ meta(charset='utf-8')
5
+ style.
6
+ body {
7
+ white-space: pre-line;
8
+ }
9
+ body
10
+ h1 Task Assigned
11
+
12
+ p Hello #{firstName},
13
+
14
+ p You have been assigned a new task by #{requesterName}.
15
+
16
+ p
17
+ strong Task:
18
+ | #{taskTitle}
19
+ if taskDescription
20
+ p
21
+ strong Description:
22
+ | #{taskDescription}
23
+
24
+ if attachmentCount > 0
25
+ p
26
+ strong Attachments:
27
+ | #{attachmentCount} file(s) attached
28
+ p Files are attached to this email for your reference.
29
+
30
+ if taskIsUrgent
31
+ p
32
+ strong URGENT
33
+
34
+ p
35
+ a(href=taskLink) View Task
36
+
37
+ p Please review the task details and take appropriate action.
@@ -0,0 +1,4 @@
1
+ if taskIsUrgent
2
+ | [URGENT] Task assigned to you: #{taskTitle}
3
+ else
4
+ | Task assigned to you: #{taskTitle}
@@ -0,0 +1,34 @@
1
+ doctype html
2
+ html
3
+ head
4
+ meta(charset='utf-8')
5
+ style.
6
+ body {
7
+ white-space: pre-line;
8
+ }
9
+ body
10
+ h1 Task Due Reminder
11
+
12
+ p Hello #{firstName},
13
+
14
+ p This is a reminder that you have a task that is due in approximately 24 hours.
15
+
16
+ p
17
+ strong Task:
18
+ | #{taskTitle}
19
+ if taskDescription
20
+ p
21
+ strong Description:
22
+ | #{taskDescription}
23
+ p
24
+ strong Due Date:
25
+ | #{dueDate}
26
+ if taskIsUrgent
27
+ p
28
+ strong URGENT
29
+
30
+ p
31
+ a(href=taskLink) View Task
32
+
33
+ p Please make sure to complete this task before the due date.
34
+
@@ -0,0 +1,4 @@
1
+ if taskIsUrgent
2
+ | [URGENT] Task due soon: #{taskTitle}
3
+ else
4
+ | Reminder: Task due soon - #{taskTitle}