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,279 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { MantineProvider } from '@mantine/core';
4
+ import { Notifications } from '@mantine/notifications';
5
+ import { act, render, screen, waitFor } from '@testing-library/react';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { MedplumProvider } from '@medplum/react';
8
+ import { MockClient, HomerSimpson } from '@medplum/mock';
9
+ import { MemoryRouter } from 'react-router';
10
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
11
+ import type { Patient } from '@medplum/fhirtypes';
12
+ import type { SlotInfo } from 'react-big-calendar';
13
+ import { CreateVisit } from './CreateVisit';
14
+
15
+ describe('CreateVisit', () => {
16
+ let medplum: MockClient;
17
+ let mockPatient: Patient;
18
+ let mockSlotInfo: SlotInfo;
19
+
20
+ beforeEach(async () => {
21
+ medplum = new MockClient();
22
+ vi.clearAllMocks();
23
+
24
+ mockPatient = {
25
+ ...HomerSimpson,
26
+ id: 'patient-1',
27
+ name: [{ given: ['Homer'], family: 'Simpson' }],
28
+ };
29
+
30
+ const startDate = new Date('2024-01-15T10:00:00Z');
31
+ const endDate = new Date('2024-01-15T10:30:00Z');
32
+ mockSlotInfo = {
33
+ start: startDate,
34
+ end: endDate,
35
+ slots: [],
36
+ action: 'select',
37
+ } as SlotInfo;
38
+
39
+ await medplum.createResource(mockPatient);
40
+ medplum.getProfile = vi.fn().mockResolvedValue({
41
+ resourceType: 'Practitioner',
42
+ id: 'practitioner-1',
43
+ name: [{ given: ['Dr.'], family: 'Smith' }],
44
+ });
45
+ });
46
+
47
+ const setup = (appointmentSlot?: SlotInfo): ReturnType<typeof render> => {
48
+ return render(
49
+ <MemoryRouter>
50
+ <MedplumProvider medplum={medplum}>
51
+ <MantineProvider>
52
+ <Notifications />
53
+ <CreateVisit appointmentSlot={appointmentSlot} />
54
+ </MantineProvider>
55
+ </MedplumProvider>
56
+ </MemoryRouter>
57
+ );
58
+ };
59
+
60
+ describe('Rendering', () => {
61
+ test('renders form with all required fields', async () => {
62
+ await act(async () => {
63
+ setup(mockSlotInfo);
64
+ });
65
+
66
+ await waitFor(() => {
67
+ expect(screen.getByLabelText(/Patient/i)).toBeInTheDocument();
68
+ expect(screen.getByLabelText(/Start Time/i)).toBeInTheDocument();
69
+ expect(screen.getByLabelText(/End Time/i)).toBeInTheDocument();
70
+ expect(screen.getByLabelText(/Class/i)).toBeInTheDocument();
71
+ expect(screen.getByLabelText(/Care template/i)).toBeInTheDocument();
72
+ expect(screen.getByRole('button', { name: /Create Visit/i })).toBeInTheDocument();
73
+ });
74
+ });
75
+
76
+ test('renders without date/time when appointmentSlot is not provided', async () => {
77
+ await act(async () => {
78
+ setup(undefined);
79
+ });
80
+
81
+ await waitFor(() => {
82
+ expect(screen.getByRole('button', { name: /Create Visit/i })).toBeInTheDocument();
83
+ });
84
+ });
85
+ });
86
+
87
+ describe('Form Validation', () => {
88
+ test('shows error notification when submitting with missing required fields', async () => {
89
+ const user = userEvent.setup();
90
+ await act(async () => {
91
+ setup(mockSlotInfo);
92
+ });
93
+
94
+ await waitFor(() => {
95
+ expect(screen.getByRole('button', { name: /Create Visit/i })).toBeInTheDocument();
96
+ });
97
+
98
+ const submitButton = screen.getByRole('button', { name: /Create Visit/i });
99
+ await user.click(submitButton);
100
+
101
+ await waitFor(() => {
102
+ expect(screen.getByText(/Please fill out required fields/i)).toBeInTheDocument();
103
+ });
104
+ });
105
+
106
+ test('does not proceed with submission when required fields are missing', async () => {
107
+ const user = userEvent.setup();
108
+ await act(async () => {
109
+ setup(mockSlotInfo);
110
+ });
111
+
112
+ await waitFor(() => {
113
+ expect(screen.getByRole('button', { name: /Create Visit/i })).toBeInTheDocument();
114
+ });
115
+
116
+ const submitButton = screen.getByRole('button', { name: /Create Visit/i });
117
+ await user.click(submitButton);
118
+
119
+ // Verify error notification is shown instead of proceeding
120
+ // Use getAllByText since notifications may persist from previous tests
121
+ await waitFor(() => {
122
+ const notifications = screen.getAllByText(/Please fill out required fields/i);
123
+ expect(notifications.length).toBeGreaterThan(0);
124
+ });
125
+ });
126
+ });
127
+
128
+ describe('PlanDefinition Actions', () => {
129
+ test('does not display included tasks card initially', async () => {
130
+ await act(async () => {
131
+ setup(mockSlotInfo);
132
+ });
133
+
134
+ await waitFor(() => {
135
+ expect(screen.queryByText(/Included Tasks/i)).not.toBeInTheDocument();
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('Date/Time Formatting', () => {
141
+ test('updates formatted date/time when appointmentSlot changes', async () => {
142
+ const { rerender } = await act(async () => {
143
+ return setup(mockSlotInfo);
144
+ });
145
+
146
+ await waitFor(() => {
147
+ expect(screen.getByText(/Jan.*15.*2024/i)).toBeInTheDocument();
148
+ });
149
+
150
+ const newSlotInfo: SlotInfo = {
151
+ start: new Date('2024-02-20T14:00:00Z'),
152
+ end: new Date('2024-02-20T14:30:00Z'),
153
+ slots: [],
154
+ action: 'select',
155
+ } as SlotInfo;
156
+
157
+ await act(async () => {
158
+ rerender(
159
+ <MemoryRouter>
160
+ <MedplumProvider medplum={medplum}>
161
+ <MantineProvider>
162
+ <Notifications />
163
+ <CreateVisit appointmentSlot={newSlotInfo} />
164
+ </MantineProvider>
165
+ </MedplumProvider>
166
+ </MemoryRouter>
167
+ );
168
+ });
169
+
170
+ await waitFor(() => {
171
+ expect(screen.getByText(/Feb.*20.*2024/i)).toBeInTheDocument();
172
+ });
173
+ });
174
+ });
175
+
176
+ describe('Button State', () => {
177
+ test('submit button is initially enabled', async () => {
178
+ await act(async () => {
179
+ setup(mockSlotInfo);
180
+ });
181
+
182
+ await waitFor(() => {
183
+ const submitButton = screen.getByRole('button', { name: /Create Visit/i });
184
+ expect(submitButton).toBeInTheDocument();
185
+ expect(submitButton).not.toBeDisabled();
186
+ });
187
+ });
188
+ });
189
+
190
+ describe('Form Field Changes', () => {
191
+ test('updates patient when patient is selected', async () => {
192
+ await act(async () => {
193
+ setup(mockSlotInfo);
194
+ });
195
+
196
+ const patientInput = await screen.findByLabelText(/Patient/i);
197
+ expect(patientInput).toBeInTheDocument();
198
+ });
199
+
200
+ test('updates start time when changed', async () => {
201
+ const user = userEvent.setup();
202
+ await act(async () => {
203
+ setup(mockSlotInfo);
204
+ });
205
+
206
+ await waitFor(() => {
207
+ expect(screen.getByLabelText(/Start Time/i)).toBeInTheDocument();
208
+ });
209
+
210
+ const startInput = screen.getByLabelText(/Start Time/i);
211
+ await act(async () => {
212
+ await user.clear(startInput);
213
+ await user.type(startInput, '2024-01-15T11:00');
214
+ });
215
+
216
+ expect(startInput).toHaveValue('2024-01-15T11:00');
217
+ });
218
+
219
+ test('updates end time when changed', async () => {
220
+ const user = userEvent.setup();
221
+ await act(async () => {
222
+ setup(mockSlotInfo);
223
+ });
224
+
225
+ const endInput = await screen.findByLabelText(/End Time/i);
226
+ await act(async () => {
227
+ await user.clear(endInput);
228
+ await user.type(endInput, '2024-01-15T12:00');
229
+ });
230
+
231
+ expect(endInput).toHaveValue('2024-01-15T12:00');
232
+ });
233
+
234
+ test('updates class when class is selected', async () => {
235
+ const user = userEvent.setup();
236
+ await act(async () => {
237
+ setup(mockSlotInfo);
238
+ });
239
+
240
+ const classInput = await screen.findByLabelText(/Class/i);
241
+ await act(async () => {
242
+ await user.click(classInput);
243
+ });
244
+
245
+ expect(classInput).toBeInTheDocument();
246
+ });
247
+
248
+ test('updates care template when template is selected', async () => {
249
+ await act(async () => {
250
+ setup(mockSlotInfo);
251
+ });
252
+
253
+ const templateInput = await screen.findByLabelText(/Care template/i);
254
+ expect(templateInput).toBeInTheDocument();
255
+ });
256
+ });
257
+
258
+ describe('Plan Definition Actions', () => {
259
+ test('displays included tasks when plan definition has actions', async () => {
260
+ const planDefinition: any = {
261
+ resourceType: 'PlanDefinition',
262
+ id: 'plan-1',
263
+ status: 'active',
264
+ action: [
265
+ { id: 'action-1', title: 'Task 1' },
266
+ { id: 'action-2', title: 'Task 2' },
267
+ ],
268
+ };
269
+ await medplum.createResource(planDefinition);
270
+
271
+ await act(async () => {
272
+ setup(mockSlotInfo);
273
+ });
274
+
275
+ const templateInput = await screen.findByLabelText(/Care template/i);
276
+ expect(templateInput).toBeInTheDocument();
277
+ });
278
+ });
279
+ });
@@ -0,0 +1,156 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { CodingInput, DateTimeInput, Form, ResourceInput, useMedplum } from '@medplum/react';
4
+ import { useState, useEffect } from 'react';
5
+ import type { JSX } from 'react';
6
+ import type { SlotInfo } from 'react-big-calendar';
7
+ import { Button, Card, Flex, Stack, Text, Title } from '@mantine/core';
8
+ import type { Coding, Patient, PlanDefinition, PlanDefinitionAction } from '@medplum/fhirtypes';
9
+ import { IconAlertSquareRounded, IconCircleCheck, IconCirclePlus } from '@tabler/icons-react';
10
+ import classes from './CreateVisit.module.css';
11
+ import { createEncounter } from '../../utils/encounter';
12
+ import { showErrorNotification } from '../../utils/notifications';
13
+ import { useNavigate } from 'react-router';
14
+ import { showNotification } from '@mantine/notifications';
15
+
16
+ interface CreateVisitProps {
17
+ appointmentSlot: SlotInfo | undefined;
18
+ }
19
+
20
+ export function CreateVisit(props: CreateVisitProps): JSX.Element {
21
+ const { appointmentSlot } = props;
22
+ const [formattedDate, setFormattedDate] = useState<string>('');
23
+ const [formattedSlotTime, setFormattedSlotTime] = useState<string>('');
24
+ const [patient, setPatient] = useState<Patient | undefined>();
25
+ const [planDefinitionData, setPlanDefinitionData] = useState<PlanDefinition | undefined>();
26
+ const [encounterClass, setEncounterClass] = useState<Coding | undefined>();
27
+ const [start, setStart] = useState<Date | undefined>(appointmentSlot?.start);
28
+ const [end, setEnd] = useState<Date | undefined>(appointmentSlot?.end);
29
+ const [isLoading, setIsLoading] = useState(false);
30
+ const medplum = useMedplum();
31
+ const navigate = useNavigate();
32
+
33
+ useEffect(() => {
34
+ if (!appointmentSlot) {
35
+ return;
36
+ }
37
+
38
+ const startDate = new Date(appointmentSlot?.start);
39
+ const endDate = new Date(appointmentSlot?.end);
40
+
41
+ const options: Intl.DateTimeFormatOptions = {
42
+ weekday: 'short',
43
+ month: 'short',
44
+ day: 'numeric',
45
+ year: 'numeric',
46
+ };
47
+ const dateStr = startDate.toLocaleDateString('en-US', options);
48
+
49
+ const timeOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric', hour12: true };
50
+ const startTimeStr = startDate.toLocaleTimeString('en-US', timeOptions);
51
+ const endTimeStr = endDate.toLocaleTimeString('en-US', timeOptions);
52
+
53
+ const formattedTime = `${startTimeStr} – ${endTimeStr}`;
54
+ setFormattedDate(dateStr);
55
+ setFormattedSlotTime(formattedTime);
56
+ }, [appointmentSlot]);
57
+
58
+ async function handleSubmit(): Promise<void> {
59
+ if (!patient || !planDefinitionData || !encounterClass || !start || !end) {
60
+ showNotification({
61
+ color: 'yellow',
62
+ icon: <IconAlertSquareRounded />,
63
+ title: 'Error',
64
+ message: 'Please fill out required fields.',
65
+ });
66
+ return;
67
+ }
68
+ setIsLoading(true);
69
+ try {
70
+ const encounter = await createEncounter(medplum, start, end, encounterClass, patient, planDefinitionData);
71
+ showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Visit created' });
72
+ navigate(`/Patient/${patient.id}/Encounter/${encounter.id}`)?.catch(console.error);
73
+ } catch (err) {
74
+ showErrorNotification(err);
75
+ } finally {
76
+ setIsLoading(false);
77
+ }
78
+ }
79
+
80
+ return (
81
+ <Form onSubmit={handleSubmit}>
82
+ <Flex direction="column" gap="md" h="100%" justify="space-between">
83
+ <Stack gap="md" h="100%">
84
+ <Stack gap={0}>
85
+ <Title order={1} fw={500}>
86
+ {formattedDate}
87
+ </Title>
88
+ <Text size="lg">{formattedSlotTime}</Text>
89
+ </Stack>
90
+
91
+ <ResourceInput
92
+ label="Patient"
93
+ resourceType="Patient"
94
+ name="Patient-id"
95
+ required={true}
96
+ onChange={(value) => setPatient(value as Patient)}
97
+ />
98
+
99
+ <DateTimeInput
100
+ name="start"
101
+ label="Start Time"
102
+ defaultValue={appointmentSlot?.start?.toISOString()}
103
+ required={true}
104
+ onChange={(value) => {
105
+ setStart(new Date(value));
106
+ }}
107
+ />
108
+
109
+ <DateTimeInput
110
+ name="end"
111
+ label="End Time"
112
+ defaultValue={appointmentSlot?.end?.toISOString()}
113
+ required={true}
114
+ onChange={(value) => {
115
+ setEnd(new Date(value));
116
+ }}
117
+ />
118
+
119
+ <CodingInput
120
+ name="class"
121
+ label="Class"
122
+ binding="http://terminology.hl7.org/ValueSet/v3-ActEncounterCode"
123
+ required={true}
124
+ onChange={setEncounterClass}
125
+ path="Encounter.type"
126
+ />
127
+
128
+ <ResourceInput
129
+ name="plandefinition"
130
+ resourceType="PlanDefinition"
131
+ label="Care template"
132
+ onChange={(value) => {
133
+ setPlanDefinitionData(value as PlanDefinition);
134
+ }}
135
+ required={true}
136
+ />
137
+ </Stack>
138
+
139
+ {planDefinitionData?.action && planDefinitionData.action.length > 0 && (
140
+ <Card className={classes.planDefinition}>
141
+ <Stack gap={0}>
142
+ <Text fw={500}>Included Tasks</Text>
143
+ {planDefinitionData?.action?.map((action: PlanDefinitionAction) => (
144
+ <Text key={action.id}>- {action.title}</Text>
145
+ ))}
146
+ </Stack>
147
+ </Card>
148
+ )}
149
+
150
+ <Button fullWidth mt="xl" type="submit" loading={isLoading} disabled={isLoading}>
151
+ <IconCirclePlus /> <Text ml="xs">Create Visit</Text>
152
+ </Button>
153
+ </Flex>
154
+ </Form>
155
+ );
156
+ }
@@ -0,0 +1,45 @@
1
+ .conversationItem {
2
+ padding: var(--mantine-spacing-sm);
3
+ margin-bottom: 4px;
4
+ border-radius: var(--mantine-radius-md);
5
+ cursor: pointer;
6
+ transition: all 0.2s ease;
7
+ color: var(--mantine-color-gray-7);
8
+ border: 1px solid transparent;
9
+ }
10
+
11
+ .conversationItem:hover {
12
+ background-color: var(--mantine-color-gray-1);
13
+ color: var(--mantine-color-dark-9);
14
+ }
15
+
16
+ .conversationItemActive {
17
+ background-color: var(--mantine-color-blue-0);
18
+ color: var(--mantine-color-blue-9);
19
+ border: 1px solid var(--mantine-color-blue-2);
20
+ box-shadow: none;
21
+ }
22
+
23
+ .conversationItemActive:hover {
24
+ background-color: var(--mantine-color-blue-1);
25
+ }
26
+
27
+ /* Dark Mode Overrides */
28
+ [data-mantine-color-scheme="dark"] .conversationItem {
29
+ color: var(--mantine-color-gray-5);
30
+ }
31
+
32
+ [data-mantine-color-scheme="dark"] .conversationItem:hover {
33
+ background-color: rgba(255, 255, 255, 0.05);
34
+ color: var(--mantine-color-gray-3);
35
+ }
36
+
37
+ [data-mantine-color-scheme="dark"] .conversationItemActive {
38
+ background-color: var(--mantine-color-blue-9);
39
+ color: var(--mantine-color-white);
40
+ border: 1px solid var(--mantine-color-blue-7);
41
+ }
42
+
43
+ [data-mantine-color-scheme="dark"] .conversationItemActive:hover {
44
+ background-color: var(--mantine-color-blue-8);
45
+ }
@@ -0,0 +1,90 @@
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 } from '@testing-library/react';
5
+ import { MedplumProvider } from '@medplum/react';
6
+ import { MockClient } from '@medplum/mock';
7
+ import { MemoryRouter } from 'react-router';
8
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
9
+ import { HistoryList } from './HistoryList';
10
+ import * as spacePersistence from '../../utils/spacePersistence';
11
+
12
+ const mockTopics = [
13
+ {
14
+ resourceType: 'Communication',
15
+ id: 'topic-1',
16
+ meta: { lastUpdated: '2023-01-01T10:00:00Z' },
17
+ topic: { text: 'Topic 1' },
18
+ },
19
+ {
20
+ resourceType: 'Communication',
21
+ id: 'topic-2',
22
+ meta: { lastUpdated: '2023-01-02T10:00:00Z' },
23
+ topic: { text: 'Topic 2' },
24
+ },
25
+ ];
26
+
27
+ describe('HistoryList', () => {
28
+ let medplum: MockClient;
29
+
30
+ beforeEach(() => {
31
+ medplum = new MockClient();
32
+ vi.clearAllMocks();
33
+ vi.spyOn(spacePersistence, 'loadRecentTopics').mockResolvedValue(mockTopics as any);
34
+ });
35
+
36
+ const setup = (currentTopicId?: string): ReturnType<typeof render> => {
37
+ return render(
38
+ <MemoryRouter>
39
+ <MedplumProvider medplum={medplum}>
40
+ <MantineProvider>
41
+ <HistoryList
42
+ currentTopicId={currentTopicId}
43
+ onSelectTopic={vi.fn()}
44
+ onSelectedItem={(topic) => `/Spaces/Communication/${topic.id}`}
45
+ />
46
+ </MantineProvider>
47
+ </MedplumProvider>
48
+ </MemoryRouter>
49
+ );
50
+ };
51
+
52
+ test('renders loading state initially', async () => {
53
+ vi.spyOn(spacePersistence, 'loadRecentTopics').mockImplementation(() => new Promise(() => {}));
54
+
55
+ setup();
56
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
57
+ });
58
+
59
+ test('renders empty state when no topics', async () => {
60
+ vi.spyOn(spacePersistence, 'loadRecentTopics').mockResolvedValue([]);
61
+
62
+ await act(async () => {
63
+ setup();
64
+ });
65
+
66
+ expect(screen.getByText('No conversations yet')).toBeInTheDocument();
67
+ });
68
+
69
+ test('renders list of topics', async () => {
70
+ await act(async () => {
71
+ setup();
72
+ });
73
+
74
+ expect(screen.getByText('Recent Conversations')).toBeInTheDocument();
75
+ expect(screen.getByText('Topic 1')).toBeInTheDocument();
76
+ expect(screen.getByText('Topic 2')).toBeInTheDocument();
77
+ });
78
+
79
+ test('generates correct links', async () => {
80
+ await act(async () => {
81
+ setup();
82
+ });
83
+
84
+ const link1 = screen.getByText('Topic 1').closest('a');
85
+ expect(link1).toHaveAttribute('href', '/Spaces/Communication/topic-1');
86
+
87
+ const link2 = screen.getByText('Topic 2').closest('a');
88
+ expect(link2).toHaveAttribute('href', '/Spaces/Communication/topic-2');
89
+ });
90
+ });
@@ -0,0 +1,84 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Stack, Text, Group, ScrollArea, Divider, Paper, Flex } from '@mantine/core';
4
+ import type { JSX } from 'react';
5
+ import { useState, useEffect } from 'react';
6
+ import { useMedplum, MedplumLink } from '@medplum/react';
7
+ import type { Communication } from '@medplum/fhirtypes';
8
+ import { loadRecentTopics } from '../../utils/spacePersistence';
9
+ import { showErrorNotification } from '../../utils/notifications';
10
+ import { formatDate } from '@medplum/core';
11
+ import classes from './HistoryList.module.css';
12
+ import cx from 'clsx';
13
+
14
+ interface HistoryListProps {
15
+ currentTopicId?: string;
16
+ onSelectTopic: (topicId: string) => void;
17
+ onSelectedItem: (topic: Communication) => string;
18
+ }
19
+
20
+ export function HistoryList({ currentTopicId, onSelectedItem }: HistoryListProps): JSX.Element {
21
+ const medplum = useMedplum();
22
+ const [topics, setTopics] = useState<Communication[]>([]);
23
+ const [loading, setLoading] = useState(true);
24
+
25
+ useEffect(() => {
26
+ const loadTopics = async (): Promise<void> => {
27
+ setLoading(true);
28
+ try {
29
+ const recentTopics = await loadRecentTopics(medplum, 20);
30
+ setTopics(recentTopics);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+ loadTopics().catch(showErrorNotification);
36
+ }, [medplum]);
37
+
38
+ return (
39
+ <Paper h="100%" bg="transparent" p={0}>
40
+ <Flex direction="column" p={0} h="100%">
41
+ <ScrollArea style={{ flex: 1 }} p={0}>
42
+ <Stack gap="xs" px="md">
43
+ <Text fw={600} c="dimmed" mt="md">
44
+ Recent Conversations
45
+ </Text>
46
+ {loading && (
47
+ <Text size="sm" c="dimmed">
48
+ Loading...
49
+ </Text>
50
+ )}
51
+ {!loading && topics.length === 0 && (
52
+ <Text size="sm" c="dimmed">
53
+ No conversations yet
54
+ </Text>
55
+ )}
56
+ {!loading &&
57
+ topics.length > 0 &&
58
+ topics.map((topic, index) => (
59
+ <Stack gap={0} key={topic.id}>
60
+ <MedplumLink to={onSelectedItem(topic)} style={{ textDecoration: 'none' }}>
61
+ <div
62
+ className={cx(classes.conversationItem, {
63
+ [classes.conversationItemActive]: currentTopicId === topic.id,
64
+ })}
65
+ >
66
+ <Text size="sm" fw={500} lineClamp={1} c="inherit">
67
+ {topic.topic?.text || 'Untitled conversation'}
68
+ </Text>
69
+ <Group gap="xs" mt={4} justify="space-between">
70
+ <Text size="xs" c="dimmed" style={{ opacity: 0.7 }}>
71
+ {formatDate(topic.meta?.lastUpdated)}
72
+ </Text>
73
+ </Group>
74
+ </div>
75
+ </MedplumLink>
76
+ {index < topics.length - 1 && <Divider />}
77
+ </Stack>
78
+ ))}
79
+ </Stack>
80
+ </ScrollArea>
81
+ </Flex>
82
+ </Paper>
83
+ );
84
+ }
@@ -0,0 +1,26 @@
1
+ .resourceBox {
2
+ cursor: pointer;
3
+ transition: all 0.2s ease;
4
+ }
5
+
6
+ [data-mantine-color-scheme='light'] .resourceBox {
7
+ background-color: var(--mantine-color-gray-0);
8
+ }
9
+
10
+ [data-mantine-color-scheme='dark'] .resourceBox {
11
+ background-color: var(--mantine-color-dark-6);
12
+ }
13
+
14
+ [data-mantine-color-scheme='light'] .resourceBox:hover {
15
+ background-color: var(--mantine-color-gray-1);
16
+ }
17
+
18
+ [data-mantine-color-scheme='dark'] .resourceBox:hover {
19
+ background-color: var(--mantine-color-dark-5);
20
+ }
21
+
22
+ .content {
23
+ flex: 1;
24
+ min-width: 0;
25
+ }
26
+