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,519 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import type { CodeableConcept, MedicationKnowledge } from '@medplum/fhirtypes';
4
+ import { MockClient } from '@medplum/mock';
5
+ import { MedplumProvider } from '@medplum/react';
6
+ import { showNotification } from '@mantine/notifications';
7
+ import { describe, expect, test, beforeEach, vi } from 'vitest';
8
+ import { DoseSpotFavoritesPage } from './DoseSpotFavoritesPage';
9
+ import { act, fireEvent, render, screen, waitFor } from '../../test-utils/render';
10
+ import { useDoseSpotClinicFormulary } from '@medplum/dosespot-react';
11
+ import * as notifications from '../../utils/notifications';
12
+
13
+ // Mock the hooks and dependencies
14
+ vi.mock('@medplum/dosespot-react', async () => {
15
+ const actual = await vi.importActual('@medplum/dosespot-react');
16
+ return {
17
+ ...actual,
18
+ useDoseSpotClinicFormulary: vi.fn(),
19
+ };
20
+ });
21
+
22
+ vi.mock('@mantine/notifications', () => ({
23
+ showNotification: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../../utils/notifications', () => ({
27
+ showErrorNotification: vi.fn(),
28
+ }));
29
+
30
+ describe('DoseSpotFavoritesPage', () => {
31
+ let medplum: MockClient;
32
+ let mockFormularyReturn: ReturnType<typeof useDoseSpotClinicFormulary>;
33
+
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+
37
+ medplum = new MockClient();
38
+
39
+ mockFormularyReturn = {
40
+ state: {
41
+ selectedMedication: undefined,
42
+ directions: undefined,
43
+ },
44
+ searchMedications: vi.fn().mockResolvedValue([]),
45
+ setSelectedMedication: vi.fn(),
46
+ setSelectedMedicationDirections: vi.fn(),
47
+ saveFavoriteMedication: vi.fn(),
48
+ clear: vi.fn(),
49
+ };
50
+
51
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
52
+ vi.spyOn(medplum, 'search').mockResolvedValue({
53
+ resourceType: 'Bundle',
54
+ type: 'searchset',
55
+ entry: [],
56
+ total: 0,
57
+ });
58
+ });
59
+
60
+ async function setup(): Promise<void> {
61
+ return act(async () => {
62
+ render(
63
+ <MedplumProvider medplum={medplum}>
64
+ <DoseSpotFavoritesPage />
65
+ </MedplumProvider>
66
+ );
67
+ });
68
+ }
69
+
70
+ test('Renders page title and add button', async () => {
71
+ await setup();
72
+
73
+ expect(screen.getByText('DoseSpot Medication Favorites')).toBeInTheDocument();
74
+ expect(screen.getByRole('button', { name: 'Add Favorite Medication' })).toBeInTheDocument();
75
+ });
76
+
77
+ test('Loads favorite medications on mount', async () => {
78
+ const mockMedicationKnowledge: MedicationKnowledge = {
79
+ resourceType: 'MedicationKnowledge',
80
+ id: 'med-1',
81
+ code: {
82
+ text: 'Aspirin 325mg',
83
+ coding: [
84
+ {
85
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
86
+ code: '1191',
87
+ display: 'Aspirin 325 MG Oral Tablet',
88
+ },
89
+ ],
90
+ },
91
+ };
92
+
93
+ vi.spyOn(medplum, 'search').mockResolvedValue({
94
+ resourceType: 'Bundle',
95
+ type: 'searchset',
96
+ entry: [
97
+ {
98
+ resource: { ...mockMedicationKnowledge, id: 'med-1' },
99
+ },
100
+ ],
101
+ total: 1,
102
+ });
103
+
104
+ await setup();
105
+
106
+ await waitFor(() => {
107
+ expect(medplum.search).toHaveBeenCalledWith('MedicationKnowledge', expect.stringContaining('code='));
108
+ });
109
+ });
110
+
111
+ test('Shows error notification when loading favorites fails', async () => {
112
+ const error = new Error('Failed to load favorites');
113
+ vi.spyOn(medplum, 'search').mockRejectedValue(error);
114
+
115
+ await setup();
116
+
117
+ await waitFor(() => {
118
+ expect(notifications.showErrorNotification).toHaveBeenCalledWith(error);
119
+ });
120
+ });
121
+
122
+ test('Opens modal when Add Favorite Medication button is clicked', async () => {
123
+ await setup();
124
+
125
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
126
+
127
+ await act(async () => {
128
+ fireEvent.click(addButton);
129
+ });
130
+
131
+ await waitFor(() => {
132
+ expect(screen.getByText('Medication')).toBeInTheDocument();
133
+ });
134
+
135
+ expect(screen.getByPlaceholderText('Search medications...')).toBeInTheDocument();
136
+ });
137
+
138
+ test('Closes modal when close button is clicked', async () => {
139
+ await setup();
140
+
141
+ // Wait for initial load to complete
142
+ await waitFor(() => {
143
+ expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
144
+ });
145
+
146
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
147
+
148
+ await act(async () => {
149
+ fireEvent.click(addButton);
150
+ });
151
+
152
+ await waitFor(() => {
153
+ expect(screen.getByText('Medication')).toBeInTheDocument();
154
+ });
155
+
156
+ // Find close button - it might be an X icon button or a button with aria-label
157
+ const closeButtons = screen.getAllByRole('button');
158
+ const closeButton = closeButtons.find(
159
+ (btn) =>
160
+ btn.getAttribute('aria-label')?.toLowerCase().includes('close') ||
161
+ btn.className.includes('mantine-Modal-close') ||
162
+ btn.querySelector('[class*="mantine-Modal-close"]')
163
+ );
164
+
165
+ expect(closeButton).toBeDefined();
166
+
167
+ if (closeButton) {
168
+ await act(async () => {
169
+ fireEvent.click(closeButton);
170
+ });
171
+
172
+ await waitFor(() => {
173
+ expect(screen.queryByText('Medication')).not.toBeInTheDocument();
174
+ });
175
+
176
+ expect(mockFormularyReturn.clear).toHaveBeenCalled();
177
+ }
178
+ });
179
+
180
+ test('Shows directions input when medication is selected', async () => {
181
+ const mockMedication: CodeableConcept = {
182
+ text: 'Aspirin 325mg',
183
+ coding: [
184
+ {
185
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
186
+ code: '1191',
187
+ display: 'Aspirin 325 MG Oral Tablet',
188
+ },
189
+ ],
190
+ };
191
+
192
+ // Create a new mock return with medication selected
193
+ const formularyReturnWithMedication: ReturnType<typeof useDoseSpotClinicFormulary> = {
194
+ ...mockFormularyReturn,
195
+ state: {
196
+ selectedMedication: mockMedication,
197
+ directions: '',
198
+ },
199
+ };
200
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(formularyReturnWithMedication);
201
+
202
+ await setup();
203
+
204
+ // Wait for initial load to complete
205
+ await waitFor(() => {
206
+ expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
207
+ });
208
+
209
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
210
+
211
+ await act(async () => {
212
+ fireEvent.click(addButton);
213
+ });
214
+
215
+ await waitFor(() => {
216
+ expect(screen.getByText('Medication')).toBeInTheDocument();
217
+ });
218
+
219
+ // The directions input should appear when a medication is selected
220
+ // Since we're mocking the hook, we verify the component structure supports it
221
+ expect(screen.getByPlaceholderText('Search medications...')).toBeInTheDocument();
222
+ });
223
+
224
+ test('Updates directions when text input changes', async () => {
225
+ const mockMedication: CodeableConcept = {
226
+ text: 'Aspirin 325mg',
227
+ coding: [
228
+ {
229
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
230
+ code: '1191',
231
+ display: 'Aspirin 325 MG Oral Tablet',
232
+ },
233
+ ],
234
+ };
235
+
236
+ // Create a new mock return with medication selected
237
+ const formularyReturnWithMedication: ReturnType<typeof useDoseSpotClinicFormulary> = {
238
+ ...mockFormularyReturn,
239
+ state: {
240
+ selectedMedication: mockMedication,
241
+ directions: 'Take 1 tablet daily',
242
+ },
243
+ };
244
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(formularyReturnWithMedication);
245
+
246
+ await setup();
247
+
248
+ // Wait for initial load to complete
249
+ await waitFor(() => {
250
+ expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
251
+ });
252
+
253
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
254
+
255
+ await act(async () => {
256
+ fireEvent.click(addButton);
257
+ });
258
+
259
+ await waitFor(() => {
260
+ expect(screen.getByText('Medication')).toBeInTheDocument();
261
+ });
262
+
263
+ // Verify the directions input exists and can be interacted with
264
+ // Since we're mocking, we verify the component structure supports direction updates
265
+ const directionsInputs = screen.queryAllByLabelText('Directions');
266
+ if (directionsInputs.length > 0) {
267
+ const directionsInput = directionsInputs[0];
268
+ await act(async () => {
269
+ fireEvent.change(directionsInput, { target: { value: 'Take 2 tablets daily' } });
270
+ });
271
+ expect(mockFormularyReturn.setSelectedMedicationDirections).toHaveBeenCalled();
272
+ }
273
+ });
274
+
275
+ test('Add Favorite button is disabled when medication or directions are missing', async () => {
276
+ await setup();
277
+
278
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
279
+
280
+ await act(async () => {
281
+ fireEvent.click(addButton);
282
+ });
283
+
284
+ await waitFor(() => {
285
+ expect(screen.getByRole('button', { name: 'Add Favorite' })).toBeDisabled();
286
+ });
287
+ });
288
+
289
+ test('Add Favorite button is enabled when medication and directions are provided', async () => {
290
+ const mockMedication: CodeableConcept = {
291
+ text: 'Aspirin 325mg',
292
+ coding: [
293
+ {
294
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
295
+ code: '1191',
296
+ display: 'Aspirin 325 MG Oral Tablet',
297
+ },
298
+ ],
299
+ };
300
+
301
+ mockFormularyReturn.state.selectedMedication = mockMedication;
302
+ mockFormularyReturn.state.directions = 'Take 1 tablet daily';
303
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
304
+
305
+ await setup();
306
+
307
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
308
+
309
+ await act(async () => {
310
+ fireEvent.click(addButton);
311
+ });
312
+
313
+ await waitFor(() => {
314
+ const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
315
+ expect(addFavoriteButton).not.toBeDisabled();
316
+ });
317
+ });
318
+
319
+ test('Successfully adds favorite medication', async () => {
320
+ const mockMedication: CodeableConcept = {
321
+ text: 'Aspirin 325mg',
322
+ coding: [
323
+ {
324
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
325
+ code: '1191',
326
+ display: 'Aspirin 325 MG Oral Tablet',
327
+ },
328
+ ],
329
+ };
330
+
331
+ const mockCreatedMedication: MedicationKnowledge = {
332
+ resourceType: 'MedicationKnowledge',
333
+ id: 'med-new',
334
+ code: mockMedication,
335
+ };
336
+
337
+ mockFormularyReturn.state.selectedMedication = mockMedication;
338
+ mockFormularyReturn.state.directions = 'Take 1 tablet daily';
339
+ (mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockResolvedValue(mockCreatedMedication);
340
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
341
+
342
+ await setup();
343
+
344
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
345
+
346
+ await act(async () => {
347
+ fireEvent.click(addButton);
348
+ });
349
+
350
+ await waitFor(() => {
351
+ expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled();
352
+ });
353
+
354
+ const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
355
+
356
+ await act(async () => {
357
+ fireEvent.click(addFavoriteButton);
358
+ });
359
+
360
+ await waitFor(() => {
361
+ expect(mockFormularyReturn.saveFavoriteMedication).toHaveBeenCalled();
362
+ });
363
+
364
+ await waitFor(() => {
365
+ expect(showNotification).toHaveBeenCalledWith({
366
+ title: 'Medication added to favorites',
367
+ message: 'The medication has been added to your favorites',
368
+ color: 'green',
369
+ });
370
+ });
371
+
372
+ expect(mockFormularyReturn.clear).toHaveBeenCalled();
373
+ });
374
+
375
+ test('Shows error notification when adding favorite fails', async () => {
376
+ const mockMedication: CodeableConcept = {
377
+ text: 'Aspirin 325mg',
378
+ coding: [
379
+ {
380
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
381
+ code: '1191',
382
+ display: 'Aspirin 325 MG Oral Tablet',
383
+ },
384
+ ],
385
+ };
386
+
387
+ const error = new Error('Failed to save medication');
388
+ mockFormularyReturn.state.selectedMedication = mockMedication;
389
+ mockFormularyReturn.state.directions = 'Take 1 tablet daily';
390
+ (mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockRejectedValue(error);
391
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
392
+
393
+ await setup();
394
+
395
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
396
+
397
+ await act(async () => {
398
+ fireEvent.click(addButton);
399
+ });
400
+
401
+ await waitFor(() => {
402
+ expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled();
403
+ });
404
+
405
+ const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
406
+
407
+ await act(async () => {
408
+ fireEvent.click(addFavoriteButton);
409
+ });
410
+
411
+ await waitFor(() => {
412
+ expect(notifications.showErrorNotification).toHaveBeenCalledWith(
413
+ expect.objectContaining({
414
+ title: 'Error adding medication to favorites',
415
+ })
416
+ );
417
+ });
418
+
419
+ expect(mockFormularyReturn.clear).toHaveBeenCalled();
420
+ });
421
+
422
+ test('Shows loading state while adding favorite', async () => {
423
+ const mockMedication: CodeableConcept = {
424
+ text: 'Aspirin 325mg',
425
+ coding: [
426
+ {
427
+ system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
428
+ code: '1191',
429
+ display: 'Aspirin 325 MG Oral Tablet',
430
+ },
431
+ ],
432
+ };
433
+
434
+ const mockCreatedMedication: MedicationKnowledge = {
435
+ resourceType: 'MedicationKnowledge',
436
+ id: 'med-new',
437
+ code: mockMedication,
438
+ };
439
+
440
+ let resolveSave: ((value: MedicationKnowledge) => void) | undefined;
441
+ const savePromise = new Promise<MedicationKnowledge>((resolve) => {
442
+ resolveSave = () => resolve(mockCreatedMedication);
443
+ });
444
+
445
+ mockFormularyReturn.state.selectedMedication = mockMedication;
446
+ mockFormularyReturn.state.directions = 'Take 1 tablet daily';
447
+ (mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockReturnValue(savePromise);
448
+ vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
449
+
450
+ await setup();
451
+
452
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
453
+
454
+ await act(async () => {
455
+ fireEvent.click(addButton);
456
+ });
457
+
458
+ await waitFor(() => {
459
+ expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled();
460
+ });
461
+
462
+ const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
463
+
464
+ await act(async () => {
465
+ fireEvent.click(addFavoriteButton);
466
+ });
467
+
468
+ // Button should be disabled while loading
469
+ await waitFor(() => {
470
+ expect(addFavoriteButton).toBeDisabled();
471
+ });
472
+
473
+ // Resolve the promise
474
+ await act(async () => {
475
+ resolveSave?.(mockCreatedMedication);
476
+ });
477
+ await savePromise;
478
+
479
+ await waitFor(() => {
480
+ expect(addFavoriteButton).not.toBeDisabled();
481
+ });
482
+ });
483
+
484
+ test('Clears form when modal is closed', async () => {
485
+ await setup();
486
+
487
+ // Wait for initial load to complete
488
+ await waitFor(() => {
489
+ expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
490
+ });
491
+
492
+ const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
493
+
494
+ await act(async () => {
495
+ fireEvent.click(addButton);
496
+ });
497
+
498
+ await waitFor(() => {
499
+ expect(screen.getByText('Medication')).toBeInTheDocument();
500
+ });
501
+
502
+ // Find close button - it might be an X icon button or a button with aria-label
503
+ const closeButtons = screen.getAllByRole('button');
504
+ const closeButton = closeButtons.find(
505
+ (btn) =>
506
+ btn.getAttribute('aria-label')?.toLowerCase().includes('close') || btn.className.includes('mantine-Modal-close')
507
+ );
508
+
509
+ expect(closeButton).toBeDefined();
510
+
511
+ if (closeButton) {
512
+ await act(async () => {
513
+ fireEvent.click(closeButton);
514
+ });
515
+
516
+ expect(mockFormularyReturn.clear).toHaveBeenCalled();
517
+ }
518
+ });
519
+ });
@@ -0,0 +1,179 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import React, { useEffect, useState } from 'react';
4
+ import {
5
+ Container,
6
+ Title,
7
+ Paper,
8
+ Button,
9
+ Modal,
10
+ Group,
11
+ Box,
12
+ TextInput,
13
+ Stack,
14
+ Divider,
15
+ Group as MantineGroup,
16
+ } from '@mantine/core';
17
+ import { showNotification } from '@mantine/notifications';
18
+ import { DOSESPOT_CLINIC_FAVORITE_ID_SYSTEM, useDoseSpotClinicFormulary } from '@medplum/dosespot-react';
19
+ import type { CodeableConcept, MedicationKnowledge } from '@medplum/fhirtypes';
20
+ import { IconPlus } from '@tabler/icons-react';
21
+ import { formatSearchQuery, isCodeableConcept, normalizeErrorString } from '@medplum/core';
22
+ import { AsyncAutocomplete, useMedplum } from '@medplum/react';
23
+ import { FavoriteMedicationsTable } from './FavoriteMedicationsTable';
24
+ import { showErrorNotification } from '../../utils/notifications';
25
+ import { v4 as uuidv4 } from 'uuid';
26
+
27
+ /**
28
+ * This is a demo component for how you could display your favorite Medications
29
+ * from DoseSpot.
30
+ *
31
+ * @returns A React component that displays the favorite medications and allows you to add new favorites.
32
+ */
33
+ export function DoseSpotFavoritesPage(): React.JSX.Element {
34
+ const [modalOpened, setModalOpened] = useState(false);
35
+ const [loading, setLoading] = useState(false);
36
+ const [loadingFavorites, setLoadingFavorites] = useState(true);
37
+ const [clinicFavoriteMedications, setClinicFavoriteMedications] = useState<MedicationKnowledge[] | undefined>();
38
+
39
+ const {
40
+ state,
41
+ saveFavoriteMedication,
42
+ searchMedications,
43
+ setSelectedMedicationDirections,
44
+ setSelectedMedication,
45
+ clear,
46
+ } = useDoseSpotClinicFormulary();
47
+ const medplum = useMedplum();
48
+
49
+ // Load favorite MedicationKnowledge resources that have a DoseSpot favorite id system
50
+ useEffect(() => {
51
+ const loadFavorites = async (): Promise<void> => {
52
+ try {
53
+ setLoadingFavorites(true);
54
+ const searchRequest = {
55
+ resourceType: 'MedicationKnowledge' as const,
56
+ filters: [
57
+ {
58
+ code: 'code',
59
+ operator: 'eq' as const,
60
+ value: `${DOSESPOT_CLINIC_FAVORITE_ID_SYSTEM}|`,
61
+ },
62
+ ],
63
+ };
64
+ const queryString = formatSearchQuery(searchRequest);
65
+ const result = await medplum.search('MedicationKnowledge', queryString);
66
+ setClinicFavoriteMedications(result.entry?.map((e) => e.resource as MedicationKnowledge) || []);
67
+ } catch (err) {
68
+ showErrorNotification(err);
69
+ } finally {
70
+ setLoadingFavorites(false);
71
+ }
72
+ };
73
+ loadFavorites().catch(showErrorNotification);
74
+ }, [medplum]);
75
+
76
+ const handleAddFavoriteMedication = async (): Promise<void> => {
77
+ try {
78
+ setLoading(true);
79
+ const created = await saveFavoriteMedication();
80
+ // Optimistically update local favorites list
81
+ setClinicFavoriteMedications((prev) => [created, ...(prev || [])]);
82
+ setModalOpened(false);
83
+ showNotification({
84
+ title: 'Medication added to favorites',
85
+ message: 'The medication has been added to your favorites',
86
+ color: 'green',
87
+ });
88
+ } catch (error) {
89
+ showErrorNotification({
90
+ title: 'Error adding medication to favorites',
91
+ message: normalizeErrorString(error as Error),
92
+ color: 'red',
93
+ });
94
+ } finally {
95
+ clear();
96
+ setLoading(false);
97
+ }
98
+ };
99
+
100
+ const toOption = (medication: CodeableConcept): { value: string; label: string; resource: CodeableConcept } => ({
101
+ value: uuidv4(),
102
+ label: medication.text || 'Unknown Medication',
103
+ resource: medication,
104
+ });
105
+
106
+ return (
107
+ <Container size="xl" py="xl">
108
+ <Paper mb="lg" p="md" withBorder shadow="sm">
109
+ <Group justify="space-between" mb="md">
110
+ <Title order={2}>DoseSpot Medication Favorites</Title>
111
+ <Button leftSection={<IconPlus size={16} />} onClick={() => setModalOpened(true)}>
112
+ Add Favorite Medication
113
+ </Button>
114
+ </Group>
115
+
116
+ {/* Example of a table to display favorite medications (MedicationKnowledge resources) */}
117
+ <FavoriteMedicationsTable
118
+ clinicFavoriteMedications={clinicFavoriteMedications}
119
+ loadingFavorites={loadingFavorites}
120
+ />
121
+ </Paper>
122
+
123
+ <Modal
124
+ opened={modalOpened}
125
+ onClose={() => {
126
+ setModalOpened(false);
127
+ clear();
128
+ }}
129
+ title="Medication"
130
+ size="lg"
131
+ withCloseButton
132
+ >
133
+ <Box>
134
+ <AsyncAutocomplete<CodeableConcept>
135
+ placeholder="Search medications..."
136
+ loadOptions={searchMedications}
137
+ toOption={toOption}
138
+ onChange={(medications) => {
139
+ if (medications.length > 0) {
140
+ setSelectedMedication(medications[0]);
141
+ } else {
142
+ setSelectedMedication(undefined);
143
+ }
144
+ }}
145
+ minInputLength={3} //DoseSpot requires at least 3 characters to search
146
+ clearable
147
+ maxValues={1} // Only allow single selection
148
+ />
149
+
150
+ {/* After selecting a medication, show the medication info with followup input items */}
151
+ {state.selectedMedication && isCodeableConcept(state.selectedMedication) && (
152
+ <Stack gap="md" mt="lg">
153
+ <Divider />
154
+ {/* Prescription Form */}
155
+ <TextInput
156
+ label="Directions"
157
+ placeholder="e.g., Take 1 tablet by mouth daily"
158
+ value={state.directions}
159
+ onChange={(e) => setSelectedMedicationDirections(e.target.value)}
160
+ required
161
+ />
162
+ </Stack>
163
+ )}
164
+
165
+ {/* Action Buttons */}
166
+ <MantineGroup justify="flex-end" gap="md" mt="md">
167
+ <Button
168
+ onClick={() => handleAddFavoriteMedication()}
169
+ disabled={!state.directions || !state.selectedMedication || loading}
170
+ loading={loading}
171
+ >
172
+ Add Favorite
173
+ </Button>
174
+ </MantineGroup>
175
+ </Box>
176
+ </Modal>
177
+ </Container>
178
+ );
179
+ }