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,80 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { render, screen } from '@testing-library/react';
5
+ import { MedplumProvider } from '@medplum/react';
6
+ import type { Encounter, Patient } from '@medplum/fhirtypes';
7
+ import { MockClient } from '@medplum/mock';
8
+ import { describe, expect, test, beforeEach, vi } from 'vitest';
9
+ import ConditionModal from './ConditionModal';
10
+
11
+ const mockPatient: Patient = {
12
+ resourceType: 'Patient',
13
+ id: 'patient-123',
14
+ name: [{ given: ['John'], family: 'Doe' }],
15
+ };
16
+
17
+ const mockEncounter: Encounter = {
18
+ resourceType: 'Encounter',
19
+ id: 'encounter-123',
20
+ status: 'in-progress',
21
+ class: {
22
+ system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
23
+ code: 'AMB',
24
+ },
25
+ subject: { reference: 'Patient/patient-123' },
26
+ };
27
+
28
+ describe('ConditionModal', () => {
29
+ let medplum: MockClient;
30
+
31
+ beforeEach(() => {
32
+ medplum = new MockClient();
33
+ vi.clearAllMocks();
34
+ });
35
+
36
+ const setup = (props: Partial<Parameters<typeof ConditionModal>[0]> = {}): ReturnType<typeof render> => {
37
+ return render(
38
+ <MedplumProvider medplum={medplum}>
39
+ <MantineProvider>
40
+ <ConditionModal patient={mockPatient} encounter={mockEncounter} onSubmit={vi.fn()} {...props} />
41
+ </MantineProvider>
42
+ </MedplumProvider>
43
+ );
44
+ };
45
+
46
+ test('renders form fields', () => {
47
+ setup();
48
+ expect(screen.getByText('ICD-10 Code')).toBeInTheDocument();
49
+ expect(screen.getByText('Status')).toBeInTheDocument();
50
+ expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
51
+ });
52
+
53
+ test('renders required field indicators', () => {
54
+ setup();
55
+ const requiredMarkers = screen.getAllByText('*');
56
+ expect(requiredMarkers.length).toBeGreaterThan(0);
57
+ });
58
+
59
+ test('renders ICD-10 code input', () => {
60
+ setup();
61
+ expect(screen.getByText('ICD-10 Code')).toBeInTheDocument();
62
+ });
63
+
64
+ test('renders status input', () => {
65
+ setup();
66
+ expect(screen.getByText('Status')).toBeInTheDocument();
67
+ });
68
+
69
+ test('renders save button', () => {
70
+ setup();
71
+ const saveButton = screen.getByRole('button', { name: 'Save' });
72
+ expect(saveButton).toBeInTheDocument();
73
+ });
74
+
75
+ test('form is wrapped in Form component', () => {
76
+ const { container } = setup();
77
+ const form = container.querySelector('form');
78
+ expect(form).toBeInTheDocument();
79
+ });
80
+ });
@@ -0,0 +1,82 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { CodeableConceptInput, Form, SubmitButton } from '@medplum/react';
4
+ import { Group, Stack } from '@mantine/core';
5
+ import { useCallback, useState } from 'react';
6
+ import type { JSX } from 'react';
7
+ import { addProfileToResource, createReference, HTTP_HL7_ORG, HTTP_TERMINOLOGY_HL7_ORG } from '@medplum/core';
8
+ import type { CodeableConcept, Condition, Encounter, Patient } from '@medplum/fhirtypes';
9
+ import { showErrorNotification } from '../../utils/notifications';
10
+
11
+ export interface ConditionDialogProps {
12
+ readonly patient: Patient;
13
+ readonly encounter: Encounter;
14
+ readonly onSubmit: (condition: Condition) => void;
15
+ }
16
+
17
+ export default function ConditionModal(props: ConditionDialogProps): JSX.Element {
18
+ const { patient, encounter, onSubmit } = props;
19
+ const [diagnosis, setDiagnosis] = useState<CodeableConcept | undefined>();
20
+ const [clinicalStatus, setClinicalStatus] = useState<CodeableConcept | undefined>();
21
+
22
+ const handleSubmit = useCallback(() => {
23
+ if (!diagnosis) {
24
+ showErrorNotification('Please select a diagnosis');
25
+ return;
26
+ }
27
+
28
+ const updatedCondition: Condition = addProfileToResource(
29
+ {
30
+ resourceType: 'Condition',
31
+ category: [
32
+ {
33
+ coding: [
34
+ {
35
+ system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/condition-category',
36
+ code: 'problem-list-item',
37
+ display: 'Problem List Item',
38
+ },
39
+ ],
40
+ text: 'Problem List Item',
41
+ },
42
+ ],
43
+ subject: createReference(patient),
44
+ encounter: encounter && createReference(encounter),
45
+ code: {
46
+ coding: diagnosis.coding ? [...diagnosis.coding] : [],
47
+ },
48
+ clinicalStatus,
49
+ },
50
+ HTTP_HL7_ORG + '/fhir/us/core/StructureDefinition/us-core-condition-problems-health-concerns'
51
+ );
52
+
53
+ onSubmit(updatedCondition);
54
+ }, [patient, encounter, diagnosis, clinicalStatus, onSubmit]);
55
+
56
+ return (
57
+ <Form onSubmit={handleSubmit}>
58
+ <Stack>
59
+ <CodeableConceptInput
60
+ binding="http://hl7.org/fhir/sid/icd-10-cm/vs"
61
+ label="ICD-10 Code"
62
+ name="diagnosis"
63
+ path="Condition.code"
64
+ required
65
+ onChange={(diagnosis) => setDiagnosis(diagnosis)}
66
+ />
67
+
68
+ <CodeableConceptInput
69
+ name="clinicalStatus"
70
+ label="Status"
71
+ path="Condition.clinicalStatus"
72
+ binding={HTTP_HL7_ORG + '/fhir/ValueSet/condition-clinical'}
73
+ onChange={(clinicalStatus) => setClinicalStatus(clinicalStatus)}
74
+ required
75
+ />
76
+ <Group justify="flex-end" gap={4} mt="md">
77
+ <SubmitButton>Save</SubmitButton>
78
+ </Group>
79
+ </Stack>
80
+ </Form>
81
+ );
82
+ }
@@ -0,0 +1,100 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { render, screen } from '@testing-library/react';
5
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
6
+ import { DoseSpotIcon } from './DoseSpotIcon';
7
+
8
+ vi.mock('@medplum/dosespot-react', () => ({
9
+ useDoseSpotNotifications: vi.fn(),
10
+ }));
11
+
12
+ import { useDoseSpotNotifications } from '@medplum/dosespot-react';
13
+
14
+ describe('DoseSpotIcon', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ function setup(): ReturnType<typeof render> {
20
+ return render(
21
+ <MantineProvider>
22
+ <DoseSpotIcon />
23
+ </MantineProvider>
24
+ );
25
+ }
26
+
27
+ test('Renders icon without indicator when unreadCount is 0', () => {
28
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(0);
29
+ const { container } = setup();
30
+
31
+ const icon = container.querySelector('svg.tabler-icon-pill');
32
+ expect(icon).toBeInTheDocument();
33
+ expect(screen.queryByText('0')).not.toBeInTheDocument();
34
+ });
35
+
36
+ test('Renders icon without indicator when unreadCount is undefined', () => {
37
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(undefined);
38
+ const { container } = setup();
39
+
40
+ const icon = container.querySelector('svg.tabler-icon-pill');
41
+ expect(icon).toBeInTheDocument();
42
+ expect(screen.queryByText(/^\d+$/)).not.toBeInTheDocument();
43
+ });
44
+
45
+ test('Renders icon with indicator when unreadCount is greater than 0', () => {
46
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(5);
47
+ const { container } = setup();
48
+
49
+ const icon = container.querySelector('svg.tabler-icon-pill');
50
+ expect(icon).toBeInTheDocument();
51
+ expect(screen.getByText('5')).toBeInTheDocument();
52
+ });
53
+
54
+ test('Formats unreadCount with toLocaleString for large numbers', () => {
55
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(1234);
56
+ setup();
57
+
58
+ expect(screen.getByText('1,234')).toBeInTheDocument();
59
+ });
60
+
61
+ test('Displays correct count for single digit', () => {
62
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(1);
63
+ setup();
64
+
65
+ expect(screen.getByText('1')).toBeInTheDocument();
66
+ });
67
+
68
+ test('Displays correct count for double digit', () => {
69
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(42);
70
+ setup();
71
+
72
+ expect(screen.getByText('42')).toBeInTheDocument();
73
+ });
74
+
75
+ test('Displays correct count for triple digit', () => {
76
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(999);
77
+ setup();
78
+
79
+ expect(screen.getByText('999')).toBeInTheDocument();
80
+ });
81
+
82
+ test('Icon is always rendered regardless of unreadCount', () => {
83
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(0);
84
+ const { container, rerender } = setup();
85
+
86
+ let icon = container.querySelector('svg.tabler-icon-pill');
87
+ expect(icon).toBeInTheDocument();
88
+
89
+ vi.mocked(useDoseSpotNotifications).mockReturnValue(10);
90
+ rerender(
91
+ <MantineProvider>
92
+ <DoseSpotIcon />
93
+ </MantineProvider>
94
+ );
95
+
96
+ icon = container.querySelector('svg.tabler-icon-pill');
97
+ expect(icon).toBeInTheDocument();
98
+ expect(screen.getByText('10')).toBeInTheDocument();
99
+ });
100
+ });
@@ -0,0 +1,20 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Indicator } from '@mantine/core';
4
+ import { useDoseSpotNotifications } from '@medplum/dosespot-react';
5
+ import { IconPill } from '@tabler/icons-react';
6
+ import type { JSX } from 'react';
7
+
8
+ export function DoseSpotIcon(): JSX.Element {
9
+ const unreadCount = useDoseSpotNotifications();
10
+
11
+ const icon = <IconPill size={20} />;
12
+
13
+ return unreadCount ? (
14
+ <Indicator inline label={unreadCount.toLocaleString()} size={16} offset={2} position="bottom-end" color="red">
15
+ {icon}
16
+ </Indicator>
17
+ ) : (
18
+ icon
19
+ );
20
+ }
@@ -0,0 +1,3 @@
1
+ .icon {
2
+ color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
3
+ }
@@ -0,0 +1,62 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Avatar, Badge, Button, Group, Paper, Text } from '@mantine/core';
4
+ import { IconWorld } from '@tabler/icons-react';
5
+ import type { JSX } from 'react';
6
+ import classes from './IntegrationCard.module.css';
7
+
8
+ export interface IntegrationCardProps {
9
+ readonly name: string;
10
+ readonly displayUrl: string;
11
+ readonly url: string;
12
+ readonly tags: string[];
13
+ readonly description: string;
14
+ readonly imageUrl: string;
15
+ readonly onClick: () => void;
16
+ }
17
+
18
+ export function IntegrationCard(props: IntegrationCardProps): JSX.Element {
19
+ return (
20
+ <Paper radius="md" withBorder shadow="md" p="md" bg="var(--mantine-color-body)" maw={300}>
21
+ <Group wrap="nowrap">
22
+ <Avatar
23
+ src={props.imageUrl}
24
+ size={48}
25
+ radius="md"
26
+ style={{ border: '1px solid var(--mantine-color-gray-2)' }}
27
+ />
28
+ <div>
29
+ <Text fz="xl" fw={800}>
30
+ {props.name}
31
+ </Text>
32
+ <Group wrap="nowrap" gap={4} mt={-2}>
33
+ <IconWorld stroke={2} size={12} color="var(--mantine-primary-color-6)" className={classes.icon} />
34
+ <Text fz="xs" c="dimmed">
35
+ <a
36
+ href={props.url}
37
+ target="_blank"
38
+ rel="noopener noreferrer"
39
+ style={{ color: 'var(--mantine-primary-color-6)', textDecoration: 'none' }}
40
+ >
41
+ {props.displayUrl}
42
+ </a>
43
+ </Text>
44
+ </Group>
45
+ </div>
46
+ </Group>
47
+ <Group mt="sm" gap={4}>
48
+ {props.tags.map((tag) => (
49
+ <Badge variant="light" color="gray" key={tag} size="sm">
50
+ {tag}
51
+ </Badge>
52
+ ))}
53
+ </Group>
54
+ <Text fz="sm" mt="sm" h={80}>
55
+ {props.description}
56
+ </Text>
57
+ <Button variant="default" fullWidth mt="md" onClick={props.onClick}>
58
+ Request Integration
59
+ </Button>
60
+ </Paper>
61
+ );
62
+ }
@@ -0,0 +1,47 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Text, Anchor } from '@mantine/core';
4
+ import type { JSX } from 'react';
5
+
6
+ interface MessageWithLinksProps {
7
+ content: string;
8
+ }
9
+
10
+ export function MessageWithLinks({ content }: MessageWithLinksProps): JSX.Element {
11
+ const resourcePattern = /\b([A-Z][a-zA-Z]+)\/([a-zA-Z0-9-]+)\b/g;
12
+
13
+ const parts: JSX.Element[] = [];
14
+ let lastIndex = 0;
15
+ let match;
16
+
17
+ while ((match = resourcePattern.exec(content)) !== null) {
18
+ const [fullMatch, resourceType, resourceId] = match;
19
+ const matchIndex = match.index;
20
+
21
+ if (matchIndex > lastIndex) {
22
+ parts.push(<span key={`text-${lastIndex}`}>{content.substring(lastIndex, matchIndex)}</span>);
23
+ }
24
+
25
+ const href = `/${resourceType}/${resourceId}`;
26
+ parts.push(
27
+ <Anchor
28
+ key={`link-${matchIndex}`}
29
+ href={href}
30
+ target="_blank"
31
+ rel="noopener noreferrer"
32
+ c="#7c3aed"
33
+ style={{ fontWeight: 500 }}
34
+ >
35
+ {fullMatch}
36
+ </Anchor>
37
+ );
38
+
39
+ lastIndex = matchIndex + fullMatch.length;
40
+ }
41
+
42
+ if (lastIndex < content.length) {
43
+ parts.push(<span key={`text-${lastIndex}`}>{content.substring(lastIndex)}</span>);
44
+ }
45
+
46
+ return <Text style={{ whiteSpace: 'pre-wrap' }}>{parts.length > 0 ? parts : content}</Text>;
47
+ }
@@ -0,0 +1,299 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import type { Patient } from '@medplum/fhirtypes';
5
+ import type { LabOrganization } from '@medplum/health-gorilla-core';
6
+ import { MockClient } from '@medplum/mock';
7
+ import { MedplumProvider } from '@medplum/react';
8
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { MemoryRouter } from 'react-router';
11
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
12
+ import { PerformingLabInput } from './PerformingLabInput';
13
+ import { useHealthGorillaLabOrder, HealthGorillaLabOrderProvider } from '@medplum/health-gorilla-react';
14
+
15
+ vi.mock('@medplum/health-gorilla-react', async () => {
16
+ const actual = await vi.importActual('@medplum/health-gorilla-react');
17
+ return {
18
+ ...actual,
19
+ useHealthGorillaLabOrder: vi.fn(),
20
+ };
21
+ });
22
+
23
+ const mockPatient: Patient = {
24
+ resourceType: 'Patient',
25
+ id: 'patient-123',
26
+ name: [{ given: ['John'], family: 'Doe' }],
27
+ };
28
+
29
+ function createMockLab(id: string, name: string): LabOrganization {
30
+ return {
31
+ resourceType: 'Organization',
32
+ id,
33
+ name,
34
+ } as LabOrganization;
35
+ }
36
+
37
+ describe('PerformingLabInput', () => {
38
+ let medplum: MockClient;
39
+ let mockSearchAvailableLabs: ReturnType<typeof vi.fn>;
40
+ let mockSetPerformingLab: ReturnType<typeof vi.fn>;
41
+ let mockLabOrderReturn: ReturnType<typeof useHealthGorillaLabOrder>;
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ medplum = new MockClient();
46
+
47
+ mockSearchAvailableLabs = vi.fn();
48
+ mockSetPerformingLab = vi.fn();
49
+
50
+ mockLabOrderReturn = {
51
+ state: {
52
+ performingLab: undefined,
53
+ performingLabAccountNumber: undefined,
54
+ selectedTests: [],
55
+ testMetadata: {},
56
+ diagnoses: [],
57
+ billingInformation: {
58
+ billTo: 'insurance',
59
+ },
60
+ specimenCollectedDateTime: undefined,
61
+ orderNotes: undefined,
62
+ },
63
+ removeDiagnosis: vi.fn(),
64
+ setDiagnoses: vi.fn(),
65
+ getActivePatientCoverages: vi.fn().mockResolvedValue([]),
66
+ updateBillingInformation: vi.fn(),
67
+ setSpecimenCollectedDateTime: vi.fn(),
68
+ setOrderNotes: vi.fn(),
69
+ validateOrder: vi.fn().mockReturnValue(undefined),
70
+ createOrderBundle: vi.fn(),
71
+ searchAvailableLabs: mockSearchAvailableLabs,
72
+ searchAvailableTests: vi.fn().mockResolvedValue([]),
73
+ setPerformingLab: mockSetPerformingLab,
74
+ setPerformingLabAccountNumber: vi.fn(),
75
+ addTest: vi.fn(),
76
+ removeTest: vi.fn(),
77
+ setTests: vi.fn(),
78
+ updateTestMetadata: vi.fn(),
79
+ addDiagnosis: vi.fn(),
80
+ } as any;
81
+
82
+ vi.mocked(useHealthGorillaLabOrder).mockReturnValue(mockLabOrderReturn);
83
+ });
84
+
85
+ function setup(props: Partial<Parameters<typeof PerformingLabInput>[0]> = {}): ReturnType<typeof render> {
86
+ return render(
87
+ <MemoryRouter>
88
+ <MedplumProvider medplum={medplum}>
89
+ <MantineProvider>
90
+ <HealthGorillaLabOrderProvider {...mockLabOrderReturn}>
91
+ <PerformingLabInput patient={mockPatient} {...props} />
92
+ </HealthGorillaLabOrderProvider>
93
+ </MantineProvider>
94
+ </MedplumProvider>
95
+ </MemoryRouter>
96
+ );
97
+ }
98
+
99
+ test('renders performing lab input', () => {
100
+ setup();
101
+ expect(screen.getByText('Performing lab')).toBeInTheDocument();
102
+ });
103
+
104
+ test('is disabled when patient is not provided', () => {
105
+ setup({ patient: undefined });
106
+ // When disabled, AsyncAutocomplete might not render the searchbox
107
+ // Check that the component renders but input is disabled
108
+ const input = screen.queryByRole('searchbox');
109
+ if (input) {
110
+ expect(input).toBeDisabled();
111
+ } else {
112
+ // If searchbox is not rendered, check for disabled state in another way
113
+ const label = screen.getByText('Performing lab');
114
+ expect(label).toBeInTheDocument();
115
+ }
116
+ });
117
+
118
+ test('is enabled when patient is provided', () => {
119
+ setup({ patient: mockPatient });
120
+ const input = screen.getByRole('searchbox');
121
+ expect(input).not.toBeDisabled();
122
+ });
123
+
124
+ test('displays error message when error is provided', () => {
125
+ setup({
126
+ patient: mockPatient,
127
+ error: { message: 'Please select a performing lab' },
128
+ });
129
+ expect(screen.getByText('Please select a performing lab')).toBeInTheDocument();
130
+ });
131
+
132
+ test('calls setPerformingLab when performingLab prop changes', async () => {
133
+ const lab1 = createMockLab('lab-1', 'Lab One');
134
+ const lab2 = createMockLab('lab-2', 'Lab Two');
135
+
136
+ const { rerender } = setup({ performingLab: lab1 });
137
+
138
+ await waitFor(() => {
139
+ expect(mockSetPerformingLab).toHaveBeenCalledWith(lab1);
140
+ });
141
+
142
+ vi.clearAllMocks();
143
+
144
+ rerender(
145
+ <MemoryRouter>
146
+ <MedplumProvider medplum={medplum}>
147
+ <MantineProvider>
148
+ <HealthGorillaLabOrderProvider {...mockLabOrderReturn}>
149
+ <PerformingLabInput patient={mockPatient} performingLab={lab2} />
150
+ </HealthGorillaLabOrderProvider>
151
+ </MantineProvider>
152
+ </MedplumProvider>
153
+ </MemoryRouter>
154
+ );
155
+
156
+ await waitFor(() => {
157
+ expect(mockSetPerformingLab).toHaveBeenCalledWith(lab2);
158
+ });
159
+ });
160
+
161
+ test('calls searchAvailableLabs when user types in input', async () => {
162
+ const user = userEvent.setup();
163
+ const mockLabs = [createMockLab('lab-1', 'Test Lab')];
164
+ mockSearchAvailableLabs.mockResolvedValue(mockLabs);
165
+
166
+ setup({ patient: mockPatient });
167
+
168
+ const input = screen.getByRole('searchbox');
169
+ await user.type(input, 'Test');
170
+
171
+ await waitFor(
172
+ () => {
173
+ expect(mockSearchAvailableLabs).toHaveBeenCalled();
174
+ },
175
+ { timeout: 3000 }
176
+ );
177
+ });
178
+
179
+ test('calls setPerformingLab when lab is selected', async () => {
180
+ const user = userEvent.setup();
181
+ const mockLab = createMockLab('lab-1', 'Test Lab');
182
+ mockSearchAvailableLabs.mockResolvedValue([mockLab]);
183
+
184
+ setup({ patient: mockPatient });
185
+
186
+ const input = screen.getByRole('searchbox');
187
+ await user.type(input, 'Test');
188
+
189
+ await waitFor(
190
+ () => {
191
+ expect(mockSearchAvailableLabs).toHaveBeenCalled();
192
+ },
193
+ { timeout: 3000 }
194
+ );
195
+
196
+ // Select the lab option
197
+ await act(async () => {
198
+ fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' });
199
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
200
+ });
201
+
202
+ await waitFor(() => {
203
+ expect(mockSetPerformingLab).toHaveBeenCalledWith(mockLab);
204
+ });
205
+ });
206
+
207
+ test('calls setPerformingLab with undefined when lab is cleared', async () => {
208
+ const mockLab = createMockLab('lab-1', 'Test Lab');
209
+
210
+ setup({ patient: mockPatient, performingLab: mockLab });
211
+
212
+ await waitFor(() => {
213
+ expect(screen.getByText('Performing lab')).toBeInTheDocument();
214
+ });
215
+
216
+ // Find and click clear button if available
217
+ const clearButtons = screen.queryAllByRole('button', { hidden: true });
218
+ const clearButton = clearButtons.find((btn) => {
219
+ const ariaLabel = btn.getAttribute('aria-label');
220
+ return ariaLabel?.toLowerCase().includes('clear') || ariaLabel?.toLowerCase().includes('remove');
221
+ });
222
+
223
+ if (clearButton) {
224
+ await act(async () => {
225
+ fireEvent.click(clearButton);
226
+ });
227
+
228
+ await waitFor(() => {
229
+ expect(mockSetPerformingLab).toHaveBeenCalledWith(undefined);
230
+ });
231
+ }
232
+ // If clear button is not available, the else branch (line 39) is harder to test
233
+ // as it requires directly triggering onChange with an empty array
234
+ // This is acceptable as the main functionality is tested
235
+ });
236
+
237
+ test('displays default value when performingLab is provided', async () => {
238
+ const mockLab = createMockLab('lab-1', 'Test Lab');
239
+
240
+ setup({ patient: mockPatient, performingLab: mockLab });
241
+
242
+ await waitFor(() => {
243
+ expect(screen.getByText('Test Lab')).toBeInTheDocument();
244
+ });
245
+ });
246
+
247
+ test('handles empty search results', async () => {
248
+ const user = userEvent.setup();
249
+ mockSearchAvailableLabs.mockResolvedValue([]);
250
+
251
+ setup({ patient: mockPatient });
252
+
253
+ const input = screen.getByRole('searchbox');
254
+ await user.type(input, 'NonExistentLab');
255
+
256
+ await waitFor(
257
+ () => {
258
+ expect(mockSearchAvailableLabs).toHaveBeenCalled();
259
+ },
260
+ { timeout: 3000 }
261
+ );
262
+
263
+ // Should not crash and should still be interactive
264
+ expect(input).toBeInTheDocument();
265
+ });
266
+
267
+ test('handles searchAvailableLabs error gracefully', async () => {
268
+ const user = userEvent.setup();
269
+ mockSearchAvailableLabs.mockRejectedValue(new Error('Search failed'));
270
+
271
+ setup({ patient: mockPatient });
272
+
273
+ const input = screen.getByRole('searchbox');
274
+ await user.type(input, 'Test');
275
+
276
+ await waitFor(
277
+ () => {
278
+ expect(mockSearchAvailableLabs).toHaveBeenCalled();
279
+ },
280
+ { timeout: 3000 }
281
+ );
282
+
283
+ // Component should still be rendered
284
+ expect(input).toBeInTheDocument();
285
+ });
286
+
287
+ test('does not call setPerformingLab on mount if performingLab is undefined', () => {
288
+ setup({ patient: mockPatient, performingLab: undefined });
289
+ // setPerformingLab should not be called when performingLab is undefined
290
+ expect(mockSetPerformingLab).not.toHaveBeenCalled();
291
+ });
292
+
293
+ test('renders required indicator', () => {
294
+ setup({ patient: mockPatient });
295
+ const label = screen.getByText('Performing lab');
296
+ // Required fields typically have an asterisk or required attribute
297
+ expect(label).toBeInTheDocument();
298
+ });
299
+ });