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,389 @@
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 { MedplumClient } from '@medplum/core';
5
+ import type { Binary, Media, Task } from '@medplum/fhirtypes';
6
+ import {
7
+ uploadFileToMedplum,
8
+ attachFileToTask,
9
+ getTaskAttachments,
10
+ getBinaryFromMedia,
11
+ } from './file-upload';
12
+
13
+ describe('File Upload Utilities', () => {
14
+ let medplum: MockClient;
15
+
16
+ beforeEach(() => {
17
+ medplum = new MockClient();
18
+ });
19
+
20
+ // Note: MockClient is compatible at runtime
21
+ const asMedplum = (): MedplumClient => medplum as unknown as MedplumClient;
22
+
23
+ describe('uploadFileToMedplum', () => {
24
+ it('should upload file and create Binary/Media resources', async () => {
25
+ const fileContent = Buffer.from('test file content');
26
+ const filename = 'test.pdf';
27
+ const contentType = 'application/pdf';
28
+
29
+ const result = await uploadFileToMedplum(medplum as unknown as MedplumClient, fileContent, filename, contentType);
30
+
31
+ expect(result).toBeDefined();
32
+ expect(result.binary).toBeDefined();
33
+ expect(result.media).toBeDefined();
34
+ expect(result.media.resourceType).toBe('Media');
35
+ expect(result.media.status).toBe('completed');
36
+ expect(result.media.content?.contentType).toBe(contentType);
37
+ expect(result.media.content?.title).toBe(filename);
38
+ expect(result.media.content?.url).toContain('Binary/');
39
+ });
40
+
41
+ it('should handle different file types', async () => {
42
+ const testCases = [
43
+ { filename: 'image.jpg', contentType: 'image/jpeg' },
44
+ { filename: 'document.docx', contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
45
+ { filename: 'spreadsheet.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
46
+ ];
47
+
48
+ for (const { filename, contentType } of testCases) {
49
+ const fileContent = Buffer.from(`content of ${filename}`);
50
+ const result = await uploadFileToMedplum(medplum as unknown as MedplumClient, fileContent, filename, contentType);
51
+
52
+ expect(result.media.content?.contentType).toBe(contentType);
53
+ expect(result.media.content?.title).toBe(filename);
54
+ }
55
+ });
56
+ });
57
+
58
+ describe('attachFileToTask', () => {
59
+ it('should attach media to task', async () => {
60
+ // Create a test task
61
+ const task = await medplum.createResource<Task>({
62
+ resourceType: 'Task',
63
+ status: 'requested',
64
+ intent: 'order',
65
+ description: 'Test task',
66
+ });
67
+
68
+ // Create a test media resource
69
+ const media = await medplum.createResource<Media>({
70
+ resourceType: 'Media',
71
+ status: 'completed',
72
+ content: {
73
+ contentType: 'application/pdf',
74
+ url: 'Binary/test-123',
75
+ title: 'test.pdf',
76
+ },
77
+ });
78
+
79
+ const mediaReference = { reference: `Media/${media.id}` };
80
+ const updatedTask = await attachFileToTask(medplum as unknown as MedplumClient, task, mediaReference);
81
+
82
+ expect(updatedTask.input).toBeDefined();
83
+ expect(updatedTask.input?.length).toBe(1);
84
+ expect(updatedTask.input?.[0].type?.coding?.[0]?.code).toBe('attachment');
85
+ expect(updatedTask.input?.[0].valueReference?.reference).toBe(mediaReference.reference);
86
+ });
87
+
88
+ it('should preserve existing task inputs when adding attachment', async () => {
89
+ // Create a task with existing input
90
+ const task = await medplum.createResource<Task>({
91
+ resourceType: 'Task',
92
+ status: 'requested',
93
+ intent: 'order',
94
+ description: 'Test task',
95
+ input: [
96
+ {
97
+ type: { text: 'Other Input' },
98
+ valueString: 'existing value',
99
+ },
100
+ ],
101
+ });
102
+
103
+ const media = await medplum.createResource<Media>({
104
+ resourceType: 'Media',
105
+ status: 'completed',
106
+ content: {
107
+ contentType: 'application/pdf',
108
+ url: 'Binary/test-456',
109
+ title: 'test2.pdf',
110
+ },
111
+ });
112
+
113
+ const mediaReference = { reference: `Media/${media.id}` };
114
+ const updatedTask = await attachFileToTask(medplum as unknown as MedplumClient, task, mediaReference);
115
+
116
+ expect(updatedTask.input?.length).toBe(2);
117
+ expect(updatedTask.input?.[0].valueString).toBe('existing value');
118
+ expect(updatedTask.input?.[1].valueReference?.reference).toBe(mediaReference.reference);
119
+ });
120
+
121
+ it('should handle task without existing inputs', async () => {
122
+ const task = await medplum.createResource<Task>({
123
+ resourceType: 'Task',
124
+ status: 'requested',
125
+ intent: 'order',
126
+ description: 'Test task without inputs',
127
+ });
128
+
129
+ const media = await medplum.createResource<Media>({
130
+ resourceType: 'Media',
131
+ status: 'completed',
132
+ content: {
133
+ contentType: 'image/jpeg',
134
+ url: 'Binary/test-789',
135
+ title: 'image.jpg',
136
+ },
137
+ });
138
+
139
+ const mediaReference = { reference: `Media/${media.id}` };
140
+ const updatedTask = await attachFileToTask(medplum as unknown as MedplumClient, task, mediaReference);
141
+
142
+ expect(updatedTask.input).toBeDefined();
143
+ expect(updatedTask.input?.length).toBe(1);
144
+ });
145
+ });
146
+
147
+ describe('getTaskAttachments', () => {
148
+ it('should retrieve all Media resources attached to a task', async () => {
149
+ // Create multiple media resources
150
+ const media1 = await medplum.createResource<Media>({
151
+ resourceType: 'Media',
152
+ status: 'completed',
153
+ content: {
154
+ contentType: 'application/pdf',
155
+ url: 'Binary/doc1',
156
+ title: 'document1.pdf',
157
+ },
158
+ });
159
+
160
+ const media2 = await medplum.createResource<Media>({
161
+ resourceType: 'Media',
162
+ status: 'completed',
163
+ content: {
164
+ contentType: 'image/jpeg',
165
+ url: 'Binary/img1',
166
+ title: 'image1.jpg',
167
+ },
168
+ });
169
+
170
+ // Create task with attachments
171
+ const task = await medplum.createResource<Task>({
172
+ resourceType: 'Task',
173
+ status: 'requested',
174
+ intent: 'order',
175
+ description: 'Test task with attachments',
176
+ input: [
177
+ {
178
+ type: {
179
+ coding: [
180
+ {
181
+ system: 'http://your-app-url.com/task-input-types',
182
+ code: 'attachment',
183
+ display: 'File Attachment',
184
+ },
185
+ ],
186
+ },
187
+ valueReference: { reference: `Media/${media1.id}` },
188
+ },
189
+ {
190
+ type: {
191
+ coding: [
192
+ {
193
+ system: 'http://your-app-url.com/task-input-types',
194
+ code: 'attachment',
195
+ display: 'File Attachment',
196
+ },
197
+ ],
198
+ },
199
+ valueReference: { reference: `Media/${media2.id}` },
200
+ },
201
+ ],
202
+ });
203
+
204
+ const attachments = await getTaskAttachments(medplum as unknown as MedplumClient, task);
205
+
206
+ expect(attachments).toBeDefined();
207
+ expect(attachments.length).toBe(2);
208
+ expect(attachments[0].id).toBe(media1.id);
209
+ expect(attachments[1].id).toBe(media2.id);
210
+ });
211
+
212
+ it('should return empty array for task with no inputs', async () => {
213
+ const task = await medplum.createResource<Task>({
214
+ resourceType: 'Task',
215
+ status: 'requested',
216
+ intent: 'order',
217
+ description: 'Task without inputs',
218
+ });
219
+
220
+ const attachments = await getTaskAttachments(medplum as unknown as MedplumClient, task);
221
+
222
+ expect(attachments).toBeDefined();
223
+ expect(attachments.length).toBe(0);
224
+ });
225
+
226
+ it('should filter out non-attachment inputs', async () => {
227
+ const media = await medplum.createResource<Media>({
228
+ resourceType: 'Media',
229
+ status: 'completed',
230
+ content: {
231
+ contentType: 'application/pdf',
232
+ url: 'Binary/doc2',
233
+ title: 'document2.pdf',
234
+ },
235
+ });
236
+
237
+ const task = await medplum.createResource<Task>({
238
+ resourceType: 'Task',
239
+ status: 'requested',
240
+ intent: 'order',
241
+ description: 'Test task with mixed inputs',
242
+ input: [
243
+ {
244
+ type: { text: 'Other Input' },
245
+ valueString: 'not an attachment',
246
+ },
247
+ {
248
+ type: {
249
+ coding: [
250
+ {
251
+ system: 'http://your-app-url.com/task-input-types',
252
+ code: 'attachment',
253
+ display: 'File Attachment',
254
+ },
255
+ ],
256
+ },
257
+ valueReference: { reference: `Media/${media.id}` },
258
+ },
259
+ ],
260
+ });
261
+
262
+ const attachments = await getTaskAttachments(medplum as unknown as MedplumClient, task);
263
+
264
+ expect(attachments.length).toBe(1);
265
+ expect(attachments[0].id).toBe(media.id);
266
+ });
267
+
268
+ it('should handle invalid media references gracefully', async () => {
269
+ const task = await medplum.createResource<Task>({
270
+ resourceType: 'Task',
271
+ status: 'requested',
272
+ intent: 'order',
273
+ description: 'Task with invalid reference',
274
+ input: [
275
+ {
276
+ type: {
277
+ coding: [
278
+ {
279
+ system: 'http://your-app-url.com/task-input-types',
280
+ code: 'attachment',
281
+ display: 'File Attachment',
282
+ },
283
+ ],
284
+ },
285
+ valueReference: { reference: 'Media/nonexistent-id' },
286
+ },
287
+ ],
288
+ });
289
+
290
+ // Mock console.error to prevent test output pollution
291
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
292
+
293
+ const attachments = await getTaskAttachments(medplum as unknown as MedplumClient, task);
294
+
295
+ // Should filter out failed fetches
296
+ expect(attachments.length).toBe(0);
297
+ expect(consoleErrorSpy).toHaveBeenCalled();
298
+
299
+ consoleErrorSpy.mockRestore();
300
+ });
301
+ });
302
+
303
+ describe('getBinaryFromMedia', () => {
304
+ it('should fetch Binary resource from Media reference', async () => {
305
+ const binary = await medplum.createResource<Binary>({
306
+ resourceType: 'Binary',
307
+ contentType: 'application/pdf',
308
+ data: Buffer.from('test binary content').toString('base64'),
309
+ });
310
+
311
+ const media = await medplum.createResource<Media>({
312
+ resourceType: 'Media',
313
+ status: 'completed',
314
+ content: {
315
+ contentType: 'application/pdf',
316
+ url: `Binary/${binary.id}`,
317
+ title: 'test.pdf',
318
+ },
319
+ });
320
+
321
+ const result = await getBinaryFromMedia(medplum as unknown as MedplumClient, media);
322
+
323
+ expect(result).toBeDefined();
324
+ expect(result?.id).toBe(binary.id);
325
+ expect(result?.contentType).toBe('application/pdf');
326
+ });
327
+
328
+ it('should return null for media without content URL', async () => {
329
+ const media = await medplum.createResource<Media>({
330
+ resourceType: 'Media',
331
+ status: 'completed',
332
+ content: {} as any,
333
+ });
334
+
335
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
336
+
337
+ const result = await getBinaryFromMedia(medplum as unknown as MedplumClient, media);
338
+
339
+ expect(result).toBeNull();
340
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
341
+ '[getBinaryFromMedia] Media resource has no content URL'
342
+ );
343
+
344
+ consoleErrorSpy.mockRestore();
345
+ });
346
+
347
+ it('should handle invalid Binary reference format', async () => {
348
+ const media = await medplum.createResource<Media>({
349
+ resourceType: 'Media',
350
+ status: 'completed',
351
+ content: {
352
+ contentType: 'application/pdf',
353
+ url: 'InvalidFormat',
354
+ title: 'test.pdf',
355
+ },
356
+ });
357
+
358
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
359
+
360
+ const result = await getBinaryFromMedia(medplum as unknown as MedplumClient, media);
361
+
362
+ expect(result).toBeNull();
363
+ expect(consoleErrorSpy).toHaveBeenCalled();
364
+
365
+ consoleErrorSpy.mockRestore();
366
+ });
367
+
368
+ it('should handle non-existent Binary resource', async () => {
369
+ const media = await medplum.createResource<Media>({
370
+ resourceType: 'Media',
371
+ status: 'completed',
372
+ content: {
373
+ contentType: 'application/pdf',
374
+ url: 'Binary/nonexistent-binary-id',
375
+ title: 'test.pdf',
376
+ },
377
+ });
378
+
379
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
380
+
381
+ const result = await getBinaryFromMedia(medplum as unknown as MedplumClient, media);
382
+
383
+ expect(result).toBeNull();
384
+ expect(consoleErrorSpy).toHaveBeenCalled();
385
+
386
+ consoleErrorSpy.mockRestore();
387
+ });
388
+ });
389
+ });
@@ -0,0 +1,222 @@
1
+ import { MedplumClient } from '@medplum/core';
2
+ import { Binary, Media, Reference, Task, TaskInput } from '@medplum/fhirtypes';
3
+
4
+ export interface FileUploadResult {
5
+ binary: Binary;
6
+ media: Media;
7
+ }
8
+
9
+ /**
10
+ * Uploads a file to Medplum and creates both Binary and Media resources.
11
+ *
12
+ * The Binary resource stores the actual file content, while the Media resource
13
+ * provides metadata and a reference to the Binary resource.
14
+ *
15
+ * @param medplum - The Medplum client instance
16
+ * @param file - The file to upload (File from browser or Buffer from Node.js)
17
+ * @param filename - The name of the file
18
+ * @param contentType - The MIME type of the file (e.g., 'application/pdf', 'image/jpeg')
19
+ * @returns An object containing both the Binary and Media resources
20
+ *
21
+ * @example
22
+ * const result = await uploadFileToMedplum(
23
+ * medplum,
24
+ * fileFromInput,
25
+ * 'document.pdf',
26
+ * 'application/pdf'
27
+ * );
28
+ */
29
+ export async function uploadFileToMedplum(
30
+ medplum: MedplumClient,
31
+ file: File | Buffer,
32
+ filename: string,
33
+ contentType: string
34
+ ): Promise<FileUploadResult> {
35
+ // Create Binary resource to store the file content
36
+ const binary = await medplum.createBinary(file, filename, contentType);
37
+
38
+ // Create Media resource as metadata wrapper
39
+ const media = await medplum.createResource<Media>({
40
+ resourceType: 'Media',
41
+ status: 'completed',
42
+ content: {
43
+ contentType,
44
+ url: `Binary/${binary.id}`,
45
+ title: filename,
46
+ },
47
+ });
48
+
49
+ return { binary, media };
50
+ }
51
+
52
+ /**
53
+ * Attaches a Media resource to a Task by adding it to the task's input array.
54
+ *
55
+ * This function follows the FHIR Task resource specification, where attachments
56
+ * are stored in the task.input array with a specific type code.
57
+ *
58
+ * @param medplum - The Medplum client instance
59
+ * @param task - The Task resource to attach the file to
60
+ * @param mediaReference - A reference to the Media resource (e.g., { reference: 'Media/123' })
61
+ * @returns The updated Task resource with the attachment added
62
+ *
63
+ * @example
64
+ * const updatedTask = await attachFileToTask(
65
+ * medplum,
66
+ * task,
67
+ * { reference: 'Media/abc-123' }
68
+ * );
69
+ */
70
+ export async function attachFileToTask(
71
+ medplum: MedplumClient,
72
+ task: Task,
73
+ mediaReference: Reference<Media>
74
+ ): Promise<Task> {
75
+ // Initialize input array if it doesn't exist
76
+ const currentInputs = task.input || [];
77
+
78
+ // Create new input entry for the attachment
79
+ const attachmentInput: TaskInput = {
80
+ type: {
81
+ coding: [
82
+ {
83
+ system: 'http://your-app-url.com/task-input-types',
84
+ code: 'attachment',
85
+ display: 'File Attachment',
86
+ },
87
+ ],
88
+ },
89
+ valueReference: mediaReference,
90
+ };
91
+
92
+ // Update the task with the new attachment
93
+ const updatedTask = await medplum.updateResource<Task>({
94
+ ...task,
95
+ input: [...currentInputs, attachmentInput],
96
+ });
97
+
98
+ return updatedTask;
99
+ }
100
+
101
+ /**
102
+ * Retrieves all Media resources attached to a Task.
103
+ *
104
+ * This function filters the task's input array for entries with the 'attachment' type
105
+ * and fetches the corresponding Media resources.
106
+ *
107
+ * @param medplum - The Medplum client instance
108
+ * @param task - The Task resource to get attachments from
109
+ * @returns An array of Media resources attached to the task
110
+ *
111
+ * @example
112
+ * const attachments = await getTaskAttachments(medplum, task);
113
+ * console.log(`Task has ${attachments.length} attachments`);
114
+ */
115
+ export async function getTaskAttachments(
116
+ medplum: MedplumClient,
117
+ task: Task
118
+ ): Promise<Media[]> {
119
+ // Return empty array if task has no inputs
120
+ if (!task.input || task.input.length === 0) {
121
+ return [];
122
+ }
123
+
124
+ // Filter inputs for attachment types
125
+ const attachmentInputs = task.input.filter((input) => {
126
+ const coding = input.type?.coding?.[0];
127
+ return coding?.code === 'attachment' && input.valueReference?.reference;
128
+ });
129
+
130
+ // Fetch all Media resources
131
+ const mediaPromises = attachmentInputs.map(async (input): Promise<Media | null> => {
132
+ const reference = input.valueReference?.reference;
133
+ if (!reference) {
134
+ return null;
135
+ }
136
+
137
+ try {
138
+ // Parse reference string (e.g., "Media/123")
139
+ const [resourceType, id] = reference.split('/');
140
+ if (resourceType !== 'Media' || !id) {
141
+ console.error(`[getTaskAttachments] Invalid reference format: ${reference}`);
142
+ return null;
143
+ }
144
+
145
+ return await medplum.readResource('Media', id);
146
+ } catch (error) {
147
+ console.error(`[getTaskAttachments] Failed to fetch Media resource: ${reference}`, error);
148
+ return null;
149
+ }
150
+ });
151
+
152
+ const mediaResources = await Promise.all(mediaPromises);
153
+
154
+ // Filter out null values (failed fetches)
155
+ return mediaResources.filter((media): media is Media => media !== null);
156
+ }
157
+
158
+ /**
159
+ * Gets the Binary content from a Media resource.
160
+ *
161
+ * This utility function extracts the file data from the Binary resource
162
+ * referenced by a Media resource.
163
+ *
164
+ * @param medplum - The Medplum client instance
165
+ * @param media - The Media resource containing the reference to the Binary
166
+ * @returns The Binary resource with file content
167
+ *
168
+ * @example
169
+ * const binary = await getBinaryFromMedia(medplum, media);
170
+ * console.log(`File size: ${binary.data?.length} bytes`);
171
+ */
172
+ export async function getBinaryFromMedia(
173
+ medplum: MedplumClient,
174
+ media: Media
175
+ ): Promise<Binary | null> {
176
+ const binaryUrl = media.content?.url;
177
+ if (!binaryUrl) {
178
+ console.error('[getBinaryFromMedia] Media resource has no content URL');
179
+ return null;
180
+ }
181
+
182
+ try {
183
+ // Handle signed URLs (external storage) vs FHIR references
184
+ if (binaryUrl.startsWith('http://') || binaryUrl.startsWith('https://')) {
185
+ // For signed URLs, fetch content directly from the URL
186
+ const response = await fetch(binaryUrl);
187
+ if (!response.ok) {
188
+ console.error(`[getBinaryFromMedia] Failed to fetch from signed URL: ${response.status} ${response.statusText}`);
189
+ return null;
190
+ }
191
+
192
+ // Convert response to base64-encoded data
193
+ const arrayBuffer = await response.arrayBuffer();
194
+ const buffer = Buffer.from(arrayBuffer);
195
+ const base64Data = buffer.toString('base64');
196
+
197
+ // Extract Binary ID from URL for the resource ID
198
+ const urlParts = binaryUrl.split('?')[0].split('/');
199
+ const binaryId = urlParts[urlParts.length - 1];
200
+
201
+ // Return a Binary-like object with the fetched data
202
+ return {
203
+ resourceType: 'Binary',
204
+ id: binaryId,
205
+ contentType: media.content?.contentType || 'application/octet-stream',
206
+ data: base64Data,
207
+ } as Binary;
208
+ } else {
209
+ // Parse FHIR reference (e.g., "Binary/123")
210
+ const [resourceType, id] = binaryUrl.split('/');
211
+ if (resourceType !== 'Binary' || !id) {
212
+ console.error(`[getBinaryFromMedia] Invalid Binary reference: ${binaryUrl}`);
213
+ return null;
214
+ }
215
+
216
+ return await medplum.readResource('Binary', id);
217
+ }
218
+ } catch (error) {
219
+ console.error(`[getBinaryFromMedia] Failed to fetch Binary resource: ${binaryUrl}`, error);
220
+ return null;
221
+ }
222
+ }
@@ -0,0 +1,18 @@
1
+ import { MedplumClient } from '@medplum/core';
2
+
3
+ export class MedplumSingleton {
4
+ private static instance: MedplumClient;
5
+
6
+ private constructor() {}
7
+
8
+ public static getInstance(): MedplumClient {
9
+ if (!MedplumSingleton.instance) {
10
+ throw new Error('MedplumClient instance not set. Please set it before using.');
11
+ }
12
+ return MedplumSingleton.instance;
13
+ }
14
+
15
+ public static setInstance(medplum: MedplumClient): void {
16
+ MedplumSingleton.instance = medplum;
17
+ }
18
+ }