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,57 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider, createTheme } from '@mantine/core';
4
+ import '@mantine/core/styles.css';
5
+ import { Notifications } from '@mantine/notifications';
6
+ import '@mantine/notifications/styles.css';
7
+ import '@mantine/spotlight/styles.css';
8
+ import { MedplumClient } from '@medplum/core';
9
+ import { MedplumProvider } from '@medplum/react';
10
+ import '@medplum/react/styles.css';
11
+ import { StrictMode } from 'react';
12
+ import { createRoot } from 'react-dom/client';
13
+ import { RouterProvider, createBrowserRouter } from 'react-router';
14
+ import { App } from './App';
15
+
16
+ const medplum = new MedplumClient({
17
+ onUnauthenticated: () => (window.location.href = '/'),
18
+ // baseUrl: 'http://localhost:8103/', // Uncomment this to run against the server on your localhost
19
+ cacheTime: 60000,
20
+ autoBatchTime: 100,
21
+ });
22
+
23
+ const theme = createTheme({
24
+ headings: {
25
+ sizes: {
26
+ h1: {
27
+ fontSize: '1.125rem',
28
+ fontWeight: '500',
29
+ lineHeight: '2.0',
30
+ },
31
+ },
32
+ },
33
+ fontSizes: {
34
+ xs: '0.6875rem',
35
+ sm: '0.875rem',
36
+ md: '0.875rem',
37
+ lg: '1.0rem',
38
+ xl: '1.125rem',
39
+ },
40
+ });
41
+
42
+ const router = createBrowserRouter([{ path: '*', element: <App /> }]);
43
+
44
+ const navigate = (path: string): Promise<void> => router.navigate(path);
45
+
46
+ const container = document.getElementById('root') as HTMLDivElement;
47
+ const root = createRoot(container);
48
+ root.render(
49
+ <StrictMode>
50
+ <MedplumProvider medplum={medplum} navigate={navigate}>
51
+ <MantineProvider theme={theme}>
52
+ <Notifications position="bottom-right" />
53
+ <RouterProvider router={router} />
54
+ </MantineProvider>
55
+ </MedplumProvider>
56
+ </StrictMode>
57
+ );
@@ -0,0 +1,6 @@
1
+ .paper {
2
+ @mixin smaller-than $mantine-breakpoint-sm {
3
+ margin: 2px;
4
+ padding: 4px;
5
+ }
6
+ }
@@ -0,0 +1,295 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { Notifications } from '@mantine/notifications';
5
+ import { MockClient } from '@medplum/mock';
6
+ import type { Communication, Patient, Task, UserConfiguration } from '@medplum/fhirtypes';
7
+ import { MedplumProvider } from '@medplum/react';
8
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
9
+ import { MemoryRouter, Routes, Route } from 'react-router';
10
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
11
+ import { SearchPage } from './SearchPage';
12
+ import type { WithId } from '@medplum/core';
13
+
14
+ describe('SearchPage', () => {
15
+ let medplum: MockClient;
16
+
17
+ beforeEach(() => {
18
+ medplum = new MockClient();
19
+ localStorage.clear();
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ afterEach(() => {
24
+ localStorage.clear();
25
+ });
26
+
27
+ async function setup(url: string, client = medplum, userConfig?: UserConfiguration): Promise<void> {
28
+ if (userConfig) {
29
+ vi.spyOn(client, 'getUserConfiguration').mockReturnValue(userConfig as unknown as WithId<UserConfiguration>);
30
+ }
31
+ await act(async () => {
32
+ render(
33
+ <MedplumProvider medplum={client}>
34
+ <MemoryRouter initialEntries={[url]} initialIndex={0}>
35
+ <MantineProvider>
36
+ <Notifications />
37
+ <Routes>
38
+ <Route path="/:resourceType" element={<SearchPage />} />
39
+ <Route path="/:resourceType/new" element={<div>New Resource</div>} />
40
+ <Route path="/Patient/:patientId/:resourceType/:id" element={<div>Patient Resource</div>} />
41
+ <Route path="/:resourceType/:id" element={<div>Resource Detail</div>} />
42
+ </Routes>
43
+ </MantineProvider>
44
+ </MemoryRouter>
45
+ </MedplumProvider>
46
+ );
47
+ });
48
+ }
49
+
50
+ test('Renders SearchControl for Task resource type', async () => {
51
+ await setup('/Task');
52
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
53
+ });
54
+
55
+ test('Renders SearchControl for Patient resource type', async () => {
56
+ await setup('/Patient');
57
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
58
+ });
59
+
60
+ test('Renders with search query parameters', async () => {
61
+ await setup('/Task?_fields=id,_lastUpdated,code,description');
62
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
63
+ });
64
+
65
+ test('Uses default resource type from localStorage', async () => {
66
+ localStorage.setItem('defaultResourceType', 'Practitioner');
67
+ await setup('/Practitioner');
68
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
69
+ });
70
+
71
+ test('Uses default resource type from UserConfiguration', async () => {
72
+ const userConfig: UserConfiguration = {
73
+ resourceType: 'UserConfiguration',
74
+ option: [{ id: 'defaultResourceType', valueString: 'ServiceRequest' }],
75
+ };
76
+ await setup('/ServiceRequest', medplum, userConfig);
77
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
78
+ });
79
+
80
+ test('Saves and retrieves last search from localStorage', async () => {
81
+ await setup('/Task?_fields=id,code&name:contains=test');
82
+
83
+ await waitFor(() => {
84
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
85
+ });
86
+
87
+ // Verify default resource type was saved
88
+ expect(localStorage.getItem('defaultResourceType')).toBe('Task');
89
+
90
+ // Verify search was saved
91
+ const savedSearch = localStorage.getItem('Task-defaultSearch');
92
+ expect(savedSearch).toBeTruthy();
93
+ const parsed = JSON.parse(savedSearch as string);
94
+ expect(parsed.resourceType).toBe('Task');
95
+ });
96
+
97
+ test('Retrieves filters from last search', async () => {
98
+ // Set up a previous search in localStorage
99
+ const previousSearch = {
100
+ resourceType: 'Task',
101
+ filters: [{ code: 'status', operator: 'eq', value: 'in-progress' }],
102
+ };
103
+ localStorage.setItem('Task-defaultSearch', JSON.stringify(previousSearch));
104
+
105
+ await setup('/Task');
106
+
107
+ await waitFor(() => {
108
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
109
+ });
110
+ });
111
+
112
+ test('Retrieves sort rules from last search', async () => {
113
+ // Set up a previous search in localStorage
114
+ const previousSearch = {
115
+ resourceType: 'Task',
116
+ sortRules: [{ code: 'priority', descending: false }],
117
+ };
118
+ localStorage.setItem('Task-defaultSearch', JSON.stringify(previousSearch));
119
+
120
+ await setup('/Task');
121
+
122
+ await waitFor(() => {
123
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
124
+ });
125
+ });
126
+
127
+ test('Defaults to _lastUpdated descending sort when no last search', async () => {
128
+ await setup('/Observation');
129
+
130
+ await waitFor(() => {
131
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
132
+ });
133
+ });
134
+
135
+ test('Handles search with offset and count', async () => {
136
+ await setup('/Task?_offset=10&_count=50');
137
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
138
+ });
139
+
140
+ test('Navigates to resource with patient reference on click', async () => {
141
+ // Create a task with a patient reference
142
+ const patient: Patient = {
143
+ resourceType: 'Patient',
144
+ id: 'patient-123',
145
+ name: [{ family: 'Test', given: ['Patient'] }],
146
+ };
147
+ await medplum.createResource(patient);
148
+
149
+ const task: Task = {
150
+ resourceType: 'Task',
151
+ id: 'task-456',
152
+ status: 'in-progress',
153
+ intent: 'order',
154
+ for: { reference: 'Patient/patient-123', display: 'Test Patient' },
155
+ };
156
+ await medplum.createResource(task);
157
+
158
+ await setup('/Task');
159
+
160
+ await waitFor(() => {
161
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
162
+ });
163
+ });
164
+
165
+ test('Navigates to resource without patient reference on click', async () => {
166
+ const task: Task = {
167
+ resourceType: 'Task',
168
+ id: 'task-789',
169
+ status: 'draft',
170
+ intent: 'order',
171
+ };
172
+ await medplum.createResource(task);
173
+
174
+ await setup('/Task');
175
+
176
+ await waitFor(() => {
177
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
178
+ });
179
+ });
180
+
181
+ test('Handles resource with subject reference to Patient', async () => {
182
+ const task: Task = {
183
+ resourceType: 'Task',
184
+ id: 'task-subject',
185
+ status: 'in-progress',
186
+ intent: 'order',
187
+ };
188
+ await medplum.createResource(task);
189
+
190
+ await setup('/Task');
191
+ await waitFor(() => {
192
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
193
+ });
194
+ });
195
+
196
+ test('Handles Communication with sender reference to Patient', async () => {
197
+ const communication: Communication = {
198
+ resourceType: 'Communication',
199
+ id: 'comm-123',
200
+ status: 'completed',
201
+ sender: { reference: 'Patient/patient-456' },
202
+ };
203
+ await medplum.createResource(communication);
204
+
205
+ await setup('/Communication');
206
+ await waitFor(() => {
207
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
208
+ });
209
+ });
210
+
211
+ test('New button navigates to new resource page', async () => {
212
+ await setup('/Task');
213
+ expect(await screen.findByText('New...')).toBeInTheDocument();
214
+
215
+ await act(async () => {
216
+ fireEvent.click(screen.getByText('New...'));
217
+ });
218
+
219
+ await waitFor(() => {
220
+ expect(screen.getByText('New Resource')).toBeInTheDocument();
221
+ });
222
+ });
223
+
224
+ test('Handles resource click navigation', async () => {
225
+ window.open = vi.fn();
226
+
227
+ await setup('/Task');
228
+ await waitFor(() => {
229
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
230
+ });
231
+
232
+ // Verify SearchControl is rendered and functional
233
+ const searchControl = screen.getByTestId('search-control');
234
+ expect(searchControl).toBeInTheDocument();
235
+ });
236
+
237
+ test('SearchControl supports checkbox selection', async () => {
238
+ await setup('/Task');
239
+ const searchControl = await screen.findByTestId('search-control');
240
+ expect(searchControl).toBeInTheDocument();
241
+ });
242
+
243
+ test('Handles onChange for search definition updates', async () => {
244
+ await setup('/Task');
245
+
246
+ await waitFor(() => {
247
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
248
+ });
249
+
250
+ // Test pagination which triggers onChange
251
+ const nextPageButton = screen.queryByLabelText('Next page');
252
+ if (nextPageButton) {
253
+ await act(async () => {
254
+ fireEvent.click(nextPageButton);
255
+ });
256
+ }
257
+ });
258
+
259
+ test('Renders with default fields when not specified', async () => {
260
+ await setup('/Practitioner');
261
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
262
+ });
263
+
264
+ test('Shows loading when search is not ready', async () => {
265
+ // Create a mock that simulates loading state
266
+ const loadingClient = new MockClient();
267
+ vi.spyOn(loadingClient, 'isLoading').mockReturnValue(true);
268
+
269
+ await setup('/Task', loadingClient);
270
+
271
+ // The component should show loading or SearchControl once ready
272
+ await waitFor(
273
+ () => {
274
+ const searchControl = screen.queryByTestId('search-control');
275
+ const loader = document.querySelector('.mantine-Loader-root');
276
+ expect(searchControl || loader).toBeTruthy();
277
+ },
278
+ { timeout: 3000 }
279
+ );
280
+ });
281
+
282
+ test('Handles auxClick to open resource in new tab', async () => {
283
+ window.open = vi.fn();
284
+
285
+ await setup('/Task');
286
+
287
+ await waitFor(() => {
288
+ expect(screen.getByTestId('search-control')).toBeInTheDocument();
289
+ });
290
+
291
+ // The onAuxClick handler should be available on resource rows
292
+ const searchControl = screen.getByTestId('search-control');
293
+ expect(searchControl).toBeInTheDocument();
294
+ });
295
+ });
@@ -0,0 +1,124 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Paper } from '@mantine/core';
4
+ import { DEFAULT_SEARCH_COUNT, formatSearchQuery, isReference, parseSearchRequest } from '@medplum/core';
5
+ import type { Filter, SearchRequest, SortRule } from '@medplum/core';
6
+ import type { Patient, Reference, Resource, UserConfiguration } from '@medplum/fhirtypes';
7
+ import { Loading, SearchControl, useMedplum } from '@medplum/react';
8
+ import { useEffect, useState } from 'react';
9
+ import type { JSX } from 'react';
10
+ import { useLocation, useNavigate } from 'react-router';
11
+ import { useResourceType } from './resource/useResourceType';
12
+ import classes from './SearchPage.module.css';
13
+
14
+ export function SearchPage(): JSX.Element {
15
+ const medplum = useMedplum();
16
+ const navigate = useNavigate();
17
+ const location = useLocation();
18
+ const [search, setSearch] = useState<SearchRequest>();
19
+
20
+ useEffect(() => {
21
+ const parsedSearch = parseSearchRequest(location.pathname + location.search);
22
+
23
+ const populatedSearch = addSearchValues(parsedSearch, medplum.getUserConfiguration());
24
+
25
+ if (
26
+ location.pathname === `/${populatedSearch.resourceType}` &&
27
+ location.search === formatSearchQuery(populatedSearch)
28
+ ) {
29
+ saveLastSearch(populatedSearch);
30
+ setSearch(populatedSearch);
31
+ } else {
32
+ navigate(`/${populatedSearch.resourceType}${formatSearchQuery(populatedSearch)}`)?.catch(console.error);
33
+ }
34
+ }, [medplum, navigate, location]);
35
+
36
+ useResourceType(search?.resourceType, { onInvalidResourceType: () => navigate('..')?.catch(console.error) });
37
+
38
+ if (!search?.resourceType || !search.fields || search.fields.length === 0) {
39
+ return <Loading />;
40
+ }
41
+
42
+ return (
43
+ <Paper shadow="xs" m="xs" p="xs" className={classes.paper}>
44
+ <SearchControl
45
+ checkboxesEnabled={true}
46
+ search={search}
47
+ onClick={(e) => navigate(getResourceUrl(e.resource))?.catch(console.error)}
48
+ onAuxClick={(e) => window.open(getResourceUrl(e.resource), '_blank')}
49
+ onNew={() => {
50
+ navigate(`/${search.resourceType}/new`)?.catch(console.error);
51
+ }}
52
+ onChange={(e) => {
53
+ navigate(`/${search.resourceType}${formatSearchQuery(e.definition)}`)?.catch(console.error);
54
+ }}
55
+ />
56
+ </Paper>
57
+ );
58
+ }
59
+
60
+ function addSearchValues(search: SearchRequest, config: UserConfiguration | undefined): SearchRequest {
61
+ const resourceType = search.resourceType || getDefaultResourceType(config);
62
+ const fields = search.fields ?? ['_id', '_lastUpdated'];
63
+ const filters = search.filters ?? (!search.resourceType ? getDefaultFilters(resourceType) : undefined);
64
+ const sortRules = search.sortRules ?? getDefaultSortRules(resourceType);
65
+ const offset = search.offset ?? 0;
66
+ const count = search.count ?? DEFAULT_SEARCH_COUNT;
67
+
68
+ return {
69
+ ...search,
70
+ resourceType,
71
+ fields,
72
+ filters,
73
+ sortRules,
74
+ offset,
75
+ count,
76
+ };
77
+ }
78
+
79
+ function getDefaultResourceType(config: UserConfiguration | undefined): string {
80
+ return (
81
+ localStorage.getItem('defaultResourceType') ??
82
+ config?.option?.find((o) => o.id === 'defaultResourceType')?.valueString ??
83
+ 'Task'
84
+ );
85
+ }
86
+
87
+ function getDefaultFilters(resourceType: string): Filter[] | undefined {
88
+ return getLastSearch(resourceType)?.filters;
89
+ }
90
+
91
+ function getDefaultSortRules(resourceType: string): SortRule[] {
92
+ const lastSearch = getLastSearch(resourceType);
93
+ if (lastSearch?.sortRules) {
94
+ return lastSearch.sortRules;
95
+ }
96
+ return [{ code: '_lastUpdated', descending: true }];
97
+ }
98
+
99
+ function getLastSearch(resourceType: string): SearchRequest | undefined {
100
+ const value = localStorage.getItem(resourceType + '-defaultSearch');
101
+ return value ? (JSON.parse(value) as SearchRequest) : undefined;
102
+ }
103
+
104
+ function saveLastSearch(search: SearchRequest): void {
105
+ localStorage.setItem('defaultResourceType', search.resourceType);
106
+ localStorage.setItem(search.resourceType + '-defaultSearch', JSON.stringify(search));
107
+ }
108
+
109
+ function getResourceUrl<T extends Resource>(resource: T): string {
110
+ const patientFields = ['patient', 'subject', 'sender'] as (keyof T)[];
111
+ for (const key of patientFields) {
112
+ if (key in resource) {
113
+ const value = resource[key];
114
+ if (isPatientReference(value)) {
115
+ return `/${value.reference}/${resource.resourceType}/${resource.id}`;
116
+ }
117
+ }
118
+ }
119
+ return `/${resource.resourceType}/${resource.id}`;
120
+ }
121
+
122
+ function isPatientReference(input: unknown): input is Reference<Patient> & { reference: string } {
123
+ return isReference(input) && input.reference.startsWith('Patient/');
124
+ }
@@ -0,0 +1,77 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import type { MedplumClient } from '@medplum/core';
4
+ import { DrAliceSmith, MockClient } from '@medplum/mock';
5
+ import { MedplumProvider } from '@medplum/react';
6
+ import crypto from 'crypto';
7
+ import { MemoryRouter } from 'react-router';
8
+ import { TextEncoder } from 'util';
9
+ import { describe, expect, test, beforeAll, vi } from 'vitest';
10
+ import { App } from '../App';
11
+ import { act, fireEvent, render, screen } from '../test-utils/render';
12
+
13
+ describe('SignInPage', () => {
14
+ function setup(url = '/signin', medplumClient?: MedplumClient): MedplumClient {
15
+ const client = medplumClient ?? new MockClient({ profile: null, clientId: 'my-client-id' });
16
+ render(
17
+ <MemoryRouter initialEntries={[url]} initialIndex={0}>
18
+ <MedplumProvider medplum={client}>
19
+ <App />
20
+ </MedplumProvider>
21
+ </MemoryRouter>
22
+ );
23
+ return client;
24
+ }
25
+
26
+ beforeAll(() => {
27
+ Object.defineProperty(global, 'TextEncoder', {
28
+ value: TextEncoder,
29
+ });
30
+
31
+ Object.defineProperty(global.self, 'crypto', {
32
+ value: crypto.webcrypto,
33
+ });
34
+ });
35
+
36
+ function expectSigninPageRendered(): void {
37
+ expect(screen.getByText('Sign in to Provider')).toBeInTheDocument();
38
+ expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument();
39
+ }
40
+
41
+ test('Renders', async () => {
42
+ setup();
43
+
44
+ expectSigninPageRendered();
45
+ });
46
+
47
+ test('Success', async () => {
48
+ const client = setup();
49
+
50
+ vi.spyOn(client, 'processCode').mockImplementation(async () => {
51
+ (client as MockClient).setProfile(DrAliceSmith);
52
+ return DrAliceSmith;
53
+ });
54
+
55
+ await act(async () => {
56
+ fireEvent.change(screen.getByLabelText('Email *'), { target: { value: 'admin@example.com' } });
57
+ });
58
+
59
+ await act(async () => {
60
+ fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
61
+ });
62
+
63
+ // Wait for password field to appear
64
+ await screen.findByLabelText('Password *');
65
+
66
+ await act(async () => {
67
+ fireEvent.change(screen.getByLabelText('Password *'), { target: { value: 'password' } });
68
+ });
69
+
70
+ const submitButton = screen.getByRole('button', { name: /Continue|Sign In/i });
71
+ await act(async () => {
72
+ fireEvent.click(submitButton);
73
+ });
74
+
75
+ expect(await screen.findByTestId('search-control')).toBeInTheDocument();
76
+ });
77
+ });
@@ -0,0 +1,22 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Title } from '@mantine/core';
4
+ import { Logo, SignInForm } from '@medplum/react';
5
+ import type { JSX } from 'react';
6
+ import { useNavigate } from 'react-router';
7
+
8
+ export function SignInPage(): JSX.Element {
9
+ const navigate = useNavigate();
10
+ return (
11
+ <SignInForm
12
+ // Configure according to your settings
13
+ googleClientId="921088377005-3j1sa10vr6hj86jgmdfh2l53v3mp7lfi.apps.googleusercontent.com"
14
+ onSuccess={() => navigate('/')?.catch(console.error)}
15
+ >
16
+ <Logo size={32} />
17
+ <Title order={3} py="lg">
18
+ Sign in to Provider
19
+ </Title>
20
+ </SignInForm>
21
+ );
22
+ }
@@ -0,0 +1,87 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { Notifications } from '@mantine/notifications';
5
+ import { MockClient } from '@medplum/mock';
6
+ import { MedplumProvider } from '@medplum/react';
7
+ import { render, screen, waitFor } from '@testing-library/react';
8
+ import { MemoryRouter, Routes, Route } from 'react-router';
9
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
10
+ import { EncounterChartPage } from './EncounterChartPage';
11
+ import * as notificationsModule from '../../utils/notifications';
12
+
13
+ describe('EncounterChartPage', () => {
14
+ let medplum: MockClient;
15
+ let showErrorNotificationSpy: ReturnType<typeof vi.spyOn>;
16
+
17
+ beforeEach(() => {
18
+ medplum = new MockClient();
19
+ vi.clearAllMocks();
20
+ showErrorNotificationSpy = vi.spyOn(notificationsModule, 'showErrorNotification');
21
+ });
22
+
23
+ function setup(initialPath: string, routePattern: string = '/Encounter/:encounterId/*'): ReturnType<typeof render> {
24
+ return render(
25
+ <MemoryRouter initialEntries={[initialPath]}>
26
+ <MedplumProvider medplum={medplum}>
27
+ <MantineProvider>
28
+ <Notifications />
29
+ <Routes>
30
+ <Route path={routePattern} element={<EncounterChartPage />}>
31
+ <Route path="*" element={<div data-testid="outlet">Outlet Content</div>} />
32
+ </Route>
33
+ </Routes>
34
+ </MantineProvider>
35
+ </MedplumProvider>
36
+ </MemoryRouter>
37
+ );
38
+ }
39
+
40
+ test('Renders EncounterChart when encounterId is present', async () => {
41
+ setup('/Encounter/encounter-123');
42
+
43
+ await waitFor(() => {
44
+ const loader = document.querySelector('.mantine-Loader-root');
45
+ expect(loader).toBeInTheDocument();
46
+ });
47
+ });
48
+
49
+ test('Renders Outlet when encounterId is present', async () => {
50
+ setup('/Encounter/encounter-123');
51
+
52
+ await waitFor(() => {
53
+ expect(screen.getByTestId('outlet')).toBeInTheDocument();
54
+ });
55
+
56
+ expect(screen.getByText('Outlet Content')).toBeInTheDocument();
57
+ });
58
+
59
+ test('Shows error notification and returns null when encounterId is missing', () => {
60
+ setup('/Encounter', '/Encounter/:encounterId?/*');
61
+
62
+ expect(showErrorNotificationSpy).toHaveBeenCalledWith('Encounter ID not found');
63
+ const loader = document.querySelector('.mantine-Loader-root');
64
+ expect(loader).not.toBeInTheDocument();
65
+ expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
66
+ });
67
+
68
+ test('Creates correct encounter reference from encounterId', async () => {
69
+ setup('/Encounter/encounter-456');
70
+
71
+ await waitFor(() => {
72
+ const loader = document.querySelector('.mantine-Loader-root');
73
+ expect(loader).toBeInTheDocument();
74
+ });
75
+ });
76
+
77
+ test('Handles nested routes correctly', async () => {
78
+ setup('/Encounter/encounter-123/nested/path');
79
+
80
+ await waitFor(() => {
81
+ const loader = document.querySelector('.mantine-Loader-root');
82
+ expect(loader).toBeInTheDocument();
83
+ });
84
+
85
+ expect(screen.getByTestId('outlet')).toBeInTheDocument();
86
+ });
87
+ });