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,732 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { act, render, screen, waitFor } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+ import { MedplumProvider } from '@medplum/react';
7
+ import type { ClinicalImpression, Encounter, Practitioner, Provenance, Task } from '@medplum/fhirtypes';
8
+ import { HomerSimpson, MockClient } from '@medplum/mock';
9
+ import { MemoryRouter } from 'react-router';
10
+ import { describe, expect, test, beforeEach, vi } from 'vitest';
11
+ import { EncounterChart } from './EncounterChart';
12
+ import { createReference } from '@medplum/core';
13
+
14
+ const mockPractitioner: Practitioner = {
15
+ resourceType: 'Practitioner',
16
+ id: 'practitioner-123',
17
+ name: [{ given: ['Dr.'], family: 'Test' }],
18
+ };
19
+
20
+ const mockEncounter: Encounter = {
21
+ resourceType: 'Encounter',
22
+ id: 'encounter-123',
23
+ status: 'in-progress',
24
+ class: {
25
+ system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
26
+ code: 'AMB',
27
+ },
28
+ subject: { reference: `Patient/${HomerSimpson.id}` },
29
+ participant: [
30
+ {
31
+ individual: createReference(mockPractitioner),
32
+ },
33
+ ],
34
+ };
35
+
36
+ const mockClinicalImpression: ClinicalImpression = {
37
+ resourceType: 'ClinicalImpression',
38
+ id: 'clinical-123',
39
+ status: 'in-progress',
40
+ subject: createReference(HomerSimpson),
41
+ encounter: createReference(mockEncounter),
42
+ note: [{ text: 'Test clinical note' }],
43
+ };
44
+
45
+ const mockTask: Task = {
46
+ resourceType: 'Task',
47
+ id: 'task-123',
48
+ status: 'in-progress',
49
+ intent: 'order',
50
+ encounter: createReference(mockEncounter),
51
+ authoredOn: '2024-01-01T10:00:00Z',
52
+ };
53
+
54
+ describe('EncounterChart', () => {
55
+ let medplum: MockClient;
56
+
57
+ beforeEach(async () => {
58
+ medplum = new MockClient();
59
+ await medplum.createResource(mockPractitioner);
60
+ await medplum.createResource(mockEncounter);
61
+ await medplum.createResource(mockClinicalImpression);
62
+ vi.clearAllMocks();
63
+ });
64
+
65
+ const setup = (props: Partial<Parameters<typeof EncounterChart>[0]> = {}): ReturnType<typeof render> => {
66
+ return render(
67
+ <MemoryRouter>
68
+ <MedplumProvider medplum={medplum}>
69
+ <MantineProvider>
70
+ <EncounterChart encounter={mockEncounter} {...props} />
71
+ </MantineProvider>
72
+ </MedplumProvider>
73
+ </MemoryRouter>
74
+ );
75
+ };
76
+
77
+ test('renders loading spinner initially', () => {
78
+ setup();
79
+ // The Loading component renders a spinner, not text
80
+ const loader = document.querySelector('.mantine-Loader-root');
81
+ expect(loader).toBeInTheDocument();
82
+ });
83
+
84
+ test('renders encounter header after loading', async () => {
85
+ setup();
86
+
87
+ await waitFor(() => {
88
+ expect(screen.getByText('Visit')).toBeInTheDocument();
89
+ });
90
+ });
91
+
92
+ test('renders chart note textarea', async () => {
93
+ setup();
94
+
95
+ await waitFor(() => {
96
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
97
+ });
98
+
99
+ const textarea = screen.getByRole('textbox');
100
+ expect(textarea).toBeInTheDocument();
101
+ expect(textarea).toHaveValue('Test clinical note');
102
+ });
103
+
104
+ test('updates chart note on change', async () => {
105
+ const user = userEvent.setup();
106
+ vi.spyOn(medplum, 'updateResource').mockResolvedValue(mockClinicalImpression as any);
107
+
108
+ setup();
109
+
110
+ await waitFor(() => {
111
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
112
+ });
113
+
114
+ const textarea = screen.getByRole('textbox');
115
+ await user.clear(textarea);
116
+ await user.type(textarea, 'Updated note');
117
+
118
+ await waitFor(
119
+ () => {
120
+ expect(medplum.updateResource).toHaveBeenCalled();
121
+ },
122
+ { timeout: 3000 }
123
+ );
124
+ });
125
+
126
+ test('displays tasks when available', async () => {
127
+ await medplum.createResource(mockTask);
128
+
129
+ setup();
130
+
131
+ await waitFor(() => {
132
+ expect(screen.getByText('Visit')).toBeInTheDocument();
133
+ });
134
+ });
135
+
136
+ test('renders notes tab by default', async () => {
137
+ setup();
138
+
139
+ await waitFor(() => {
140
+ expect(screen.getByText('Visit')).toBeInTheDocument();
141
+ });
142
+
143
+ // The notes tab should be active by default
144
+ await waitFor(() => {
145
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
146
+ });
147
+ });
148
+
149
+ test('switches to details tab when clicked', async () => {
150
+ const user = userEvent.setup();
151
+ setup();
152
+
153
+ await waitFor(() => {
154
+ expect(screen.getByText('Visit')).toBeInTheDocument();
155
+ });
156
+
157
+ await waitFor(() => {
158
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
159
+ });
160
+
161
+ const detailsTab = screen.getByText('Details & Billing');
162
+ await user.click(detailsTab);
163
+
164
+ await waitFor(() => {
165
+ expect(screen.queryByText('Fill chart note')).not.toBeInTheDocument();
166
+ });
167
+ });
168
+
169
+ test('switches back to notes tab when clicked', async () => {
170
+ const user = userEvent.setup();
171
+ setup();
172
+
173
+ await waitFor(() => {
174
+ expect(screen.getByText('Visit')).toBeInTheDocument();
175
+ });
176
+
177
+ const detailsTab = screen.getByText('Details & Billing');
178
+ await user.click(detailsTab);
179
+
180
+ await waitFor(() => {
181
+ expect(screen.queryByText('Fill chart note')).not.toBeInTheDocument();
182
+ });
183
+
184
+ const notesTab = screen.getByText('Note & Tasks');
185
+ await user.click(notesTab);
186
+
187
+ await waitFor(() => {
188
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
189
+ });
190
+ });
191
+
192
+ test('displays billing tab content when details tab is active', async () => {
193
+ const user = userEvent.setup();
194
+ setup();
195
+
196
+ await waitFor(() => {
197
+ expect(screen.getByText('Visit')).toBeInTheDocument();
198
+ });
199
+
200
+ const detailsTab = screen.getByText('Details & Billing');
201
+ await user.click(detailsTab);
202
+
203
+ await waitFor(() => {
204
+ expect(screen.queryByText('Fill chart note')).not.toBeInTheDocument();
205
+ });
206
+ });
207
+
208
+ test('fetches provenances on mount', async () => {
209
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([] as any);
210
+
211
+ setup();
212
+
213
+ await waitFor(() => {
214
+ expect(medplum.searchResources).toHaveBeenCalledWith(
215
+ 'Provenance',
216
+ expect.stringContaining('target=Encounter/encounter-123')
217
+ );
218
+ });
219
+ });
220
+
221
+ test('chart note is enabled when not signed', async () => {
222
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
223
+ if (resourceType === 'Provenance') {
224
+ return [] as any;
225
+ }
226
+ if (resourceType === 'ClinicalImpression') {
227
+ return [mockClinicalImpression];
228
+ }
229
+ if (resourceType === 'Task') {
230
+ return [];
231
+ }
232
+ return [];
233
+ });
234
+
235
+ setup();
236
+
237
+ await waitFor(() => {
238
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
239
+ });
240
+
241
+ const textarea = screen.getByRole('textbox');
242
+ expect(textarea).not.toBeDisabled();
243
+ });
244
+
245
+ test('handles encounter status change', async () => {
246
+ const user = userEvent.setup();
247
+ vi.spyOn(medplum, 'updateResource').mockResolvedValue({ ...mockEncounter, status: 'finished' } as any);
248
+
249
+ setup();
250
+
251
+ await waitFor(() => {
252
+ expect(screen.getByText('In Progress')).toBeInTheDocument();
253
+ });
254
+
255
+ const statusButton = screen.getByText('In Progress');
256
+ await user.click(statusButton);
257
+
258
+ await waitFor(() => {
259
+ expect(screen.getByText('Finished')).toBeInTheDocument();
260
+ });
261
+ });
262
+
263
+ test('renders with encounter reference', async () => {
264
+ const encounterRef = { reference: 'Encounter/encounter-123' };
265
+
266
+ await act(async () => {
267
+ setup({ encounter: encounterRef });
268
+ });
269
+
270
+ await waitFor(() => {
271
+ expect(screen.getByText('Visit')).toBeInTheDocument();
272
+ });
273
+ });
274
+
275
+ test('fetches tasks for encounter', async () => {
276
+ await medplum.createResource(mockTask);
277
+
278
+ vi.spyOn(medplum, 'searchResources');
279
+
280
+ setup();
281
+
282
+ await waitFor(() => {
283
+ expect(medplum.searchResources).toHaveBeenCalledWith(
284
+ 'Task',
285
+ expect.stringContaining('encounter=Encounter/encounter-123'),
286
+ expect.any(Object)
287
+ );
288
+ });
289
+ });
290
+
291
+ test('fetches clinical impressions for encounter', async () => {
292
+ vi.spyOn(medplum, 'searchResources');
293
+
294
+ setup();
295
+
296
+ await waitFor(() => {
297
+ expect(medplum.searchResources).toHaveBeenCalledWith(
298
+ 'ClinicalImpression',
299
+ expect.stringContaining('encounter=Encounter/encounter-123')
300
+ );
301
+ });
302
+ });
303
+
304
+ describe('signing functionality', () => {
305
+ const finishedEncounter: Encounter = {
306
+ ...mockEncounter,
307
+ status: 'finished',
308
+ };
309
+
310
+ const getSignButton = (): HTMLElement | null => {
311
+ const buttons = screen.getAllByRole('button');
312
+ return buttons.find((btn) => btn.querySelector('svg')) || null;
313
+ };
314
+
315
+ test('signs without locking - textarea remains enabled', async () => {
316
+ const user = userEvent.setup();
317
+ const mockProvenance: Provenance = {
318
+ resourceType: 'Provenance',
319
+ id: 'provenance-1',
320
+ target: [createReference(finishedEncounter)],
321
+ recorded: new Date().toISOString(),
322
+ agent: [
323
+ {
324
+ who: createReference(mockPractitioner),
325
+ },
326
+ ],
327
+ };
328
+
329
+ // Mock searchResources to return empty initially, then return provenance after signing
330
+ let provenanceReturned = false;
331
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
332
+ if (resourceType === 'Provenance') {
333
+ return Promise.resolve(provenanceReturned ? [mockProvenance] : []) as any;
334
+ }
335
+ if (resourceType === 'ClinicalImpression') {
336
+ return Promise.resolve([mockClinicalImpression]) as any;
337
+ }
338
+ if (resourceType === 'Task') {
339
+ return Promise.resolve([]) as any;
340
+ }
341
+ return Promise.resolve([]) as any;
342
+ });
343
+
344
+ vi.spyOn(medplum, 'createResource').mockImplementation(async (resource: any) => {
345
+ if (resource.resourceType === 'Provenance') {
346
+ provenanceReturned = true;
347
+ return mockProvenance as any;
348
+ }
349
+ return resource;
350
+ });
351
+
352
+ await medplum.createResource(finishedEncounter);
353
+ setup({ encounter: finishedEncounter });
354
+
355
+ await waitFor(() => {
356
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
357
+ });
358
+
359
+ await waitFor(() => {
360
+ const signButton = getSignButton();
361
+ expect(signButton).toBeInTheDocument();
362
+ });
363
+
364
+ const signButton = getSignButton();
365
+ if (signButton) {
366
+ await user.click(signButton);
367
+ }
368
+
369
+ await waitFor(() => {
370
+ expect(screen.getByText('Just Sign')).toBeInTheDocument();
371
+ });
372
+
373
+ await user.click(screen.getByText('Just Sign'));
374
+
375
+ await waitFor(() => {
376
+ expect(medplum.createResource).toHaveBeenCalledWith(
377
+ expect.objectContaining({
378
+ resourceType: 'Provenance',
379
+ target: [createReference(finishedEncounter)],
380
+ })
381
+ );
382
+ });
383
+
384
+ // Wait for modal to close and component to update
385
+ // Textarea should still be enabled after signing without locking
386
+ await waitFor(
387
+ () => {
388
+ const chartNoteCard = screen.getByText('Fill chart note').closest('.mantine-Card-root');
389
+ const textarea = chartNoteCard?.querySelector('textarea');
390
+ expect(textarea).not.toBeDisabled();
391
+ },
392
+ { timeout: 3000 }
393
+ );
394
+ });
395
+
396
+ test('signs with locking - textarea becomes disabled', async () => {
397
+ const user = userEvent.setup();
398
+ const completedClinicalImpression: ClinicalImpression = {
399
+ ...mockClinicalImpression,
400
+ status: 'completed',
401
+ };
402
+ const mockProvenance: Provenance = {
403
+ resourceType: 'Provenance',
404
+ id: 'provenance-1',
405
+ target: [createReference(finishedEncounter)],
406
+ recorded: new Date().toISOString(),
407
+ agent: [
408
+ {
409
+ who: createReference(mockPractitioner),
410
+ },
411
+ ],
412
+ };
413
+
414
+ // Mock searchResources to return empty initially, then return provenance after signing
415
+ let provenanceReturned = false;
416
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
417
+ if (resourceType === 'Provenance') {
418
+ return Promise.resolve(provenanceReturned ? [mockProvenance] : []) as any;
419
+ }
420
+ if (resourceType === 'ClinicalImpression') {
421
+ return Promise.resolve([completedClinicalImpression]) as any;
422
+ }
423
+ if (resourceType === 'Task') {
424
+ return Promise.resolve([]) as any;
425
+ }
426
+ return Promise.resolve([]) as any;
427
+ });
428
+
429
+ vi.spyOn(medplum, 'createResource').mockImplementation(async (resource: any) => {
430
+ if (resource.resourceType === 'Provenance') {
431
+ provenanceReturned = true;
432
+ return mockProvenance as any;
433
+ }
434
+ return resource;
435
+ });
436
+
437
+ await medplum.createResource(finishedEncounter);
438
+ setup({ encounter: finishedEncounter });
439
+
440
+ await waitFor(() => {
441
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
442
+ });
443
+
444
+ await waitFor(() => {
445
+ const signButton = getSignButton();
446
+ expect(signButton).toBeInTheDocument();
447
+ });
448
+
449
+ const signButton = getSignButton();
450
+ if (signButton) {
451
+ await user.click(signButton);
452
+ }
453
+
454
+ await waitFor(() => {
455
+ expect(screen.getByText('Sign & Lock Note')).toBeInTheDocument();
456
+ });
457
+
458
+ await user.click(screen.getByText('Sign & Lock Note'));
459
+
460
+ await waitFor(() => {
461
+ expect(medplum.createResource).toHaveBeenCalledWith(
462
+ expect.objectContaining({
463
+ resourceType: 'Provenance',
464
+ target: [createReference(finishedEncounter)],
465
+ })
466
+ );
467
+ });
468
+
469
+ // Wait for modal to close and component to update
470
+ // Textarea should be disabled after signing with locking
471
+ await waitFor(
472
+ () => {
473
+ const chartNoteCard = screen.getByText('Fill chart note').closest('.mantine-Card-root');
474
+ const textarea = chartNoteCard?.querySelector('textarea');
475
+ expect(textarea).toBeDisabled();
476
+ },
477
+ { timeout: 3000 }
478
+ );
479
+ });
480
+
481
+ test('signs with locking - completes incomplete tasks', async () => {
482
+ const user = userEvent.setup();
483
+ const incompleteTask: Task = {
484
+ ...mockTask,
485
+ id: 'task-incomplete',
486
+ status: 'in-progress',
487
+ };
488
+ const completedTask: Task = {
489
+ ...incompleteTask,
490
+ status: 'completed',
491
+ };
492
+
493
+ const completedClinicalImpression: ClinicalImpression = {
494
+ ...mockClinicalImpression,
495
+ status: 'completed',
496
+ };
497
+ const mockProvenance: Provenance = {
498
+ resourceType: 'Provenance',
499
+ id: 'provenance-1',
500
+ target: [createReference(finishedEncounter)],
501
+ recorded: new Date().toISOString(),
502
+ agent: [
503
+ {
504
+ who: createReference(mockPractitioner),
505
+ },
506
+ ],
507
+ };
508
+
509
+ await medplum.createResource(incompleteTask);
510
+ let provenanceReturned = false;
511
+ vi.spyOn(medplum, 'updateResource').mockResolvedValue(completedTask as any);
512
+ vi.spyOn(medplum, 'createResource').mockImplementation(async (resource: any) => {
513
+ if (resource.resourceType === 'Provenance') {
514
+ provenanceReturned = true;
515
+ return mockProvenance as any;
516
+ }
517
+ return resource;
518
+ });
519
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
520
+ if (resourceType === 'Provenance') {
521
+ return Promise.resolve(provenanceReturned ? [mockProvenance] : []) as any;
522
+ }
523
+ if (resourceType === 'ClinicalImpression') {
524
+ return Promise.resolve([completedClinicalImpression]) as any;
525
+ }
526
+ if (resourceType === 'Task') {
527
+ return Promise.resolve([incompleteTask]) as any;
528
+ }
529
+ return Promise.resolve([]) as any;
530
+ });
531
+
532
+ await medplum.createResource(finishedEncounter);
533
+ setup({ encounter: finishedEncounter });
534
+
535
+ await waitFor(() => {
536
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
537
+ });
538
+
539
+ await waitFor(() => {
540
+ const signButton = getSignButton();
541
+ expect(signButton).toBeInTheDocument();
542
+ });
543
+
544
+ const signButton = getSignButton();
545
+ if (signButton) {
546
+ await user.click(signButton);
547
+ }
548
+
549
+ await waitFor(() => {
550
+ expect(screen.getByText('Sign & Lock Note')).toBeInTheDocument();
551
+ });
552
+
553
+ await user.click(screen.getByText('Sign & Lock Note'));
554
+
555
+ // Verify that incomplete tasks are updated to completed
556
+ await waitFor(
557
+ () => {
558
+ expect(medplum.updateResource).toHaveBeenCalledWith(
559
+ expect.objectContaining({
560
+ resourceType: 'Task',
561
+ id: 'task-incomplete',
562
+ status: 'completed',
563
+ })
564
+ );
565
+ },
566
+ { timeout: 3000 }
567
+ );
568
+ });
569
+
570
+ test('signs with locking - does not update already completed tasks', async () => {
571
+ const user = userEvent.setup();
572
+ const completedTask: Task = {
573
+ ...mockTask,
574
+ id: 'task-completed',
575
+ status: 'completed',
576
+ };
577
+
578
+ const completedClinicalImpression: ClinicalImpression = {
579
+ ...mockClinicalImpression,
580
+ status: 'completed',
581
+ };
582
+ const mockProvenance: Provenance = {
583
+ resourceType: 'Provenance',
584
+ id: 'provenance-1',
585
+ target: [createReference(finishedEncounter)],
586
+ recorded: new Date().toISOString(),
587
+ agent: [
588
+ {
589
+ who: createReference(mockPractitioner),
590
+ },
591
+ ],
592
+ };
593
+
594
+ await medplum.createResource(completedTask);
595
+ let provenanceReturned = false;
596
+ vi.spyOn(medplum, 'updateResource');
597
+ vi.spyOn(medplum, 'createResource').mockImplementation(async (resource: any) => {
598
+ if (resource.resourceType === 'Provenance') {
599
+ provenanceReturned = true;
600
+ return mockProvenance as any;
601
+ }
602
+ return resource;
603
+ });
604
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
605
+ if (resourceType === 'Provenance') {
606
+ return Promise.resolve(provenanceReturned ? [mockProvenance] : []) as any;
607
+ }
608
+ if (resourceType === 'ClinicalImpression') {
609
+ return Promise.resolve([completedClinicalImpression]) as any;
610
+ }
611
+ if (resourceType === 'Task') {
612
+ return Promise.resolve([completedTask]) as any;
613
+ }
614
+ return Promise.resolve([]) as any;
615
+ });
616
+
617
+ await medplum.createResource(finishedEncounter);
618
+ setup({ encounter: finishedEncounter });
619
+
620
+ await waitFor(() => {
621
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
622
+ });
623
+
624
+ await waitFor(() => {
625
+ const signButton = getSignButton();
626
+ expect(signButton).toBeInTheDocument();
627
+ });
628
+
629
+ const signButton = getSignButton();
630
+ if (signButton) {
631
+ await user.click(signButton);
632
+ }
633
+
634
+ await waitFor(() => {
635
+ expect(screen.getByText('Sign & Lock Note')).toBeInTheDocument();
636
+ });
637
+
638
+ await user.click(screen.getByText('Sign & Lock Note'));
639
+
640
+ await waitFor(
641
+ () => {
642
+ expect(medplum.createResource).toHaveBeenCalled();
643
+ },
644
+ { timeout: 3000 }
645
+ );
646
+
647
+ // Verify that completed tasks are not updated
648
+ const updateCalls = vi.mocked(medplum.updateResource).mock.calls;
649
+ const taskUpdateCalls = updateCalls.filter((call) => call[0]?.resourceType === 'Task');
650
+ expect(taskUpdateCalls).toHaveLength(0);
651
+ });
652
+
653
+ test('chart note is disabled when signed and locked', async () => {
654
+ const mockProvenance: Provenance = {
655
+ resourceType: 'Provenance',
656
+ id: 'provenance-1',
657
+ target: [createReference(finishedEncounter)],
658
+ recorded: new Date().toISOString(),
659
+ agent: [
660
+ {
661
+ who: createReference(mockPractitioner),
662
+ },
663
+ ],
664
+ };
665
+
666
+ const completedClinicalImpression: ClinicalImpression = {
667
+ ...mockClinicalImpression,
668
+ status: 'completed',
669
+ };
670
+
671
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
672
+ if (resourceType === 'Provenance') {
673
+ return [mockProvenance] as any;
674
+ }
675
+ if (resourceType === 'ClinicalImpression') {
676
+ return [completedClinicalImpression] as any;
677
+ }
678
+ return [] as any;
679
+ });
680
+
681
+ setup({ encounter: finishedEncounter });
682
+
683
+ await waitFor(() => {
684
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
685
+ });
686
+
687
+ // Textarea should be disabled when signed and locked
688
+ await waitFor(() => {
689
+ const chartNoteCard = screen.getByText('Fill chart note').closest('.mantine-Card-root');
690
+ const textarea = chartNoteCard?.querySelector('textarea');
691
+ expect(textarea).toBeDisabled();
692
+ });
693
+ });
694
+
695
+ test('chart note is enabled when signed but not locked', async () => {
696
+ const mockProvenance: Provenance = {
697
+ resourceType: 'Provenance',
698
+ id: 'provenance-1',
699
+ target: [createReference(finishedEncounter)],
700
+ recorded: new Date().toISOString(),
701
+ agent: [
702
+ {
703
+ who: createReference(mockPractitioner),
704
+ },
705
+ ],
706
+ };
707
+
708
+ vi.spyOn(medplum, 'searchResources').mockImplementation((resourceType: string) => {
709
+ if (resourceType === 'Provenance') {
710
+ return [mockProvenance] as any;
711
+ }
712
+ if (resourceType === 'ClinicalImpression') {
713
+ return [mockClinicalImpression] as any;
714
+ }
715
+ return [] as any;
716
+ });
717
+
718
+ setup({ encounter: finishedEncounter });
719
+
720
+ await waitFor(() => {
721
+ expect(screen.getByText('Fill chart note')).toBeInTheDocument();
722
+ });
723
+
724
+ // Textarea should be enabled when signed but not locked
725
+ await waitFor(() => {
726
+ const chartNoteCard = screen.getByText('Fill chart note').closest('.mantine-Card-root');
727
+ const textarea = chartNoteCard?.querySelector('textarea');
728
+ expect(textarea).not.toBeDisabled();
729
+ });
730
+ });
731
+ });
732
+ });