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,358 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import {
5
+ Flex,
6
+ Paper,
7
+ ScrollArea,
8
+ Stack,
9
+ Text,
10
+ ActionIcon,
11
+ Divider,
12
+ Button,
13
+ Center,
14
+ ThemeIcon,
15
+ Menu,
16
+ Skeleton,
17
+ Box,
18
+ Pagination,
19
+ Group,
20
+ } from '@mantine/core';
21
+ import type { Communication, Patient, Practitioner, Reference } from '@medplum/fhirtypes';
22
+ import { PatientSummary, ThreadChat } from '@medplum/react';
23
+ import { useCallback, useEffect, useMemo } from 'react';
24
+ import type { JSX } from 'react';
25
+ import { IconMessageCircle, IconChevronDown, IconPlus } from '@tabler/icons-react';
26
+ import { getReferenceString, Operator, parseSearchRequest } from '@medplum/core';
27
+ import type { SearchRequest } from '@medplum/core';
28
+ import { ChatList } from './ChatList';
29
+ import { NewTopicDialog } from './NewTopicDialog';
30
+ import { ParticipantFilter } from './ParticipantFilter';
31
+ import { useThreadInbox } from '../../hooks/useThreadInbox';
32
+ import classes from './ThreadInbox.module.css';
33
+ import { useDisclosure } from '@mantine/hooks';
34
+ import { showErrorNotification } from '../../utils/notifications';
35
+ import cx from 'clsx';
36
+ import { Link } from 'react-router';
37
+
38
+ /**
39
+ * ThreadInbox is a component that displays a list of threads and allows the user to select a thread to view.
40
+ * @param query - The query to fetch all communications.
41
+ * @param threadId - The id of the thread to select.
42
+ * @param subject - The default subject when creating a new thread.
43
+ * @param showPatientSummary - Whether to show the patient summary.
44
+ * @param onNew - A function to handle a new thread.
45
+ * @param getThreadUri - A function to build thread URIs.
46
+ * @param onChange - A function to handle search changes.
47
+ * @param inProgressUri - The URI for in-progress threads.
48
+ * @param completedUri - The URI for completed threads.
49
+ */
50
+
51
+ interface ThreadInboxProps {
52
+ query: string;
53
+ threadId: string | undefined;
54
+ subject?: Reference<Patient> | Patient | undefined;
55
+ showPatientSummary?: boolean | undefined;
56
+ onNew: (message: Communication) => void;
57
+ getThreadUri: (topic: Communication) => string;
58
+ onChange: (search: SearchRequest) => void;
59
+ inProgressUri: string;
60
+ completedUri: string;
61
+ }
62
+
63
+ export function ThreadInbox(props: ThreadInboxProps): JSX.Element {
64
+ const {
65
+ query,
66
+ threadId,
67
+ subject,
68
+ showPatientSummary = false,
69
+ onNew,
70
+ getThreadUri,
71
+ onChange,
72
+ inProgressUri,
73
+ completedUri,
74
+ } = props;
75
+
76
+ const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
77
+
78
+ const currentSearch = useMemo(() => parseSearchRequest(`Communication?${query}`), [query]);
79
+
80
+ const searchParams = useMemo(() => new URLSearchParams(query), [query]);
81
+ const itemsPerPage = Number.parseInt(searchParams.get('_count') || '20', 10);
82
+ const currentOffset = Number.parseInt(searchParams.get('_offset') || '0', 10);
83
+ const currentPage = Math.floor(currentOffset / itemsPerPage) + 1;
84
+ const status = (searchParams.get('status') as Communication['status']) || 'in-progress';
85
+
86
+ // Extract participants from parsed search request filters (comma-separated)
87
+ const selectedParticipants = useMemo((): Reference<Patient | Practitioner>[] => {
88
+ const recipientFilters = currentSearch.filters?.filter((f) => f.code === 'recipient') ?? [];
89
+ // Split comma-separated values and flatten
90
+ return recipientFilters.flatMap((f) =>
91
+ f.value
92
+ .split(',')
93
+ .filter(Boolean)
94
+ .map((ref) => ({ reference: ref }) as Reference<Patient | Practitioner>)
95
+ );
96
+ }, [currentSearch]);
97
+
98
+ const {
99
+ loading,
100
+ error,
101
+ threadMessages,
102
+ selectedThread,
103
+ total,
104
+ handleThreadStatusChange,
105
+ addThreadMessage,
106
+ refreshThreadMessages,
107
+ } = useThreadInbox({
108
+ query,
109
+ threadId,
110
+ });
111
+
112
+ const handleParticipantsChange = useCallback(
113
+ (participants: Reference<Patient | Practitioner>[]) => {
114
+ // Remove existing recipient filters
115
+ const otherFilters = currentSearch.filters?.filter((f) => f.code !== 'recipient') ?? [];
116
+
117
+ // Add recipient filter with comma-separated values (OR logic in FHIR)
118
+ const participantRefs = participants.map((p) => p.reference).filter(Boolean) as string[];
119
+ const newFilters =
120
+ participantRefs.length > 0
121
+ ? [...otherFilters, { code: 'recipient', operator: Operator.EQUALS, value: participantRefs.join(',') }]
122
+ : otherFilters;
123
+
124
+ onChange({
125
+ ...currentSearch,
126
+ filters: newFilters,
127
+ offset: 0, // Reset to first page when filter changes
128
+ });
129
+ },
130
+ [currentSearch, onChange]
131
+ );
132
+
133
+ useEffect(() => {
134
+ if (error) {
135
+ showErrorNotification(error);
136
+ }
137
+ }, [error]);
138
+
139
+ const handleTopicStatusChangeWithErrorHandling = async (newStatus: Communication['status']): Promise<void> => {
140
+ try {
141
+ await handleThreadStatusChange(newStatus);
142
+ await refreshThreadMessages();
143
+ } catch (error) {
144
+ showErrorNotification(error);
145
+ }
146
+ };
147
+
148
+ const handleNewTopicCompletion = (message: Communication): void => {
149
+ addThreadMessage(message);
150
+ onNew(message);
151
+ };
152
+
153
+ const skeletonTitleWidths = [80, 72, 68, 64];
154
+ const skeletonSubtitleWidths = [85, 78, 70, 60];
155
+
156
+ return (
157
+ <>
158
+ <div className={classes.container}>
159
+ <Flex direction="row" h="100%" w="100%">
160
+ {/* Left sidebar - Messages list */}
161
+ <Flex direction="column" w={380} h="100%" className={classes.rightBorder}>
162
+ <Paper h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
163
+ <ScrollArea style={{ flex: 1 }} scrollbarSize={10} type="hover" scrollHideDelay={250}>
164
+ <Flex h={64} align="center" justify="space-between" p="md">
165
+ <Group gap="xs">
166
+ <Button
167
+ component={Link}
168
+ to={inProgressUri}
169
+ className={cx(classes.button, { [classes.selected]: status === 'in-progress' })}
170
+ h={32}
171
+ radius="xl"
172
+ >
173
+ In progress
174
+ </Button>
175
+ <Button
176
+ component={Link}
177
+ to={completedUri}
178
+ className={cx(classes.button, { [classes.selected]: status === 'completed' })}
179
+ h={32}
180
+ radius="xl"
181
+ >
182
+ Completed
183
+ </Button>
184
+ <ParticipantFilter
185
+ selectedParticipants={selectedParticipants}
186
+ onFilterChange={handleParticipantsChange}
187
+ />
188
+ </Group>
189
+ <ActionIcon radius="50%" variant="filled" color="blue" onClick={openModal}>
190
+ <IconPlus size={16} />
191
+ </ActionIcon>
192
+ </Flex>
193
+ <Divider />
194
+ {loading ? (
195
+ <Stack gap="md" p="md">
196
+ {Array.from({ length: 10 }).map((_, index) => {
197
+ const titleWidth = skeletonTitleWidths[index % skeletonTitleWidths.length];
198
+ const subtitleWidth = skeletonSubtitleWidths[index % skeletonSubtitleWidths.length];
199
+ return (
200
+ <Flex key={index} gap="sm" align="flex-start">
201
+ <Skeleton height={40} width={40} radius="50%" />
202
+ <Box style={{ flex: 1 }}>
203
+ <Flex direction="column" gap="xs">
204
+ <Skeleton height={16} width={`${titleWidth}%`} />
205
+ <Skeleton height={14} width={`${subtitleWidth}%`} />
206
+ </Flex>
207
+ </Box>
208
+ </Flex>
209
+ );
210
+ })}
211
+ </Stack>
212
+ ) : (
213
+ threadMessages.length > 0 && (
214
+ <ChatList
215
+ threads={threadMessages}
216
+ selectedCommunication={selectedThread}
217
+ getThreadUri={getThreadUri}
218
+ />
219
+ )
220
+ )}
221
+ {threadMessages.length === 0 && !loading && <EmptyMessagesState />}
222
+ </ScrollArea>
223
+ {!loading && total !== undefined && total > itemsPerPage && (
224
+ <Box p="md">
225
+ <Center>
226
+ <Pagination
227
+ value={currentPage}
228
+ total={Math.ceil(total / itemsPerPage)}
229
+ onChange={(page) => {
230
+ const offset = (page - 1) * itemsPerPage;
231
+ onChange({
232
+ ...currentSearch,
233
+ offset,
234
+ });
235
+ }}
236
+ size="sm"
237
+ siblings={1}
238
+ boundaries={1}
239
+ />
240
+ </Center>
241
+ </Box>
242
+ )}
243
+ </Paper>
244
+ </Flex>
245
+
246
+ {selectedThread ? (
247
+ <>
248
+ {/* Main chat area */}
249
+ <Flex direction="column" style={{ flex: 1 }} h="100%" className={classes.rightBorder}>
250
+ <Paper h="100%">
251
+ <Stack h="100%" gap={0}>
252
+ <Flex h={64} align="center" justify="space-between" p="md">
253
+ <Text fw={800} truncate fz="lg">
254
+ {selectedThread.topic?.text ?? 'Messages'}
255
+ </Text>
256
+
257
+ <Menu position="bottom-end" shadow="md">
258
+ <Menu.Target>
259
+ <Button
260
+ variant="light"
261
+ color={getStatusColor(selectedThread.status)}
262
+ rightSection={
263
+ selectedThread.status === 'completed' ? undefined : <IconChevronDown size={16} />
264
+ }
265
+ radius="xl"
266
+ size="sm"
267
+ >
268
+ {selectedThread.status
269
+ .split('-')
270
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
271
+ .join(' ')}
272
+ </Button>
273
+ </Menu.Target>
274
+
275
+ {selectedThread.status !== 'completed' && (
276
+ <>
277
+ <Menu.Dropdown>
278
+ <Menu.Item onClick={() => handleTopicStatusChangeWithErrorHandling('completed')}>
279
+ Completed
280
+ </Menu.Item>
281
+ </Menu.Dropdown>
282
+ </>
283
+ )}
284
+ </Menu>
285
+ </Flex>
286
+ <Divider />
287
+ <Flex direction="column" style={{ flex: 1 }} h="100%">
288
+ <ThreadChat
289
+ key={`${getReferenceString(selectedThread)}`}
290
+ title={'Messages'}
291
+ thread={selectedThread}
292
+ excludeHeader={true}
293
+ />
294
+ </Flex>
295
+ </Stack>
296
+ </Paper>
297
+ </Flex>
298
+
299
+ {/* Right sidebar - Patient summary */}
300
+ {selectedThread.subject && showPatientSummary && (
301
+ <Flex direction="column" w={300} h="100%">
302
+ <ScrollArea p={0} h="100%" scrollbarSize={10} type="hover" scrollHideDelay={250}>
303
+ <PatientSummary key={selectedThread.id} patient={selectedThread.subject as Reference<Patient>} />
304
+ </ScrollArea>
305
+ </Flex>
306
+ )}
307
+ </>
308
+ ) : (
309
+ <Flex direction="column" style={{ flex: 1 }} h="100%">
310
+ <NoMessages />
311
+ </Flex>
312
+ )}
313
+ </Flex>
314
+ </div>
315
+ <NewTopicDialog subject={subject} opened={modalOpened} onClose={closeModal} onSubmit={handleNewTopicCompletion} />
316
+ </>
317
+ );
318
+ }
319
+
320
+ function NoMessages(): JSX.Element {
321
+ return (
322
+ <Center h="100%" w="100%">
323
+ <Stack align="center" gap="md">
324
+ <ThemeIcon size={64} variant="light" color="gray">
325
+ <IconMessageCircle size={32} />
326
+ </ThemeIcon>
327
+ <Stack align="center" gap="xs">
328
+ <Text size="sm" c="dimmed" ta="center">
329
+ Select a message from the list to view details
330
+ </Text>
331
+ </Stack>
332
+ </Stack>
333
+ </Center>
334
+ );
335
+ }
336
+
337
+ function getStatusColor(status: Communication['status']): string {
338
+ if (status === 'completed') {
339
+ return 'green';
340
+ }
341
+ if (status === 'stopped') {
342
+ return 'red';
343
+ }
344
+ return 'blue';
345
+ }
346
+
347
+ function EmptyMessagesState(): JSX.Element {
348
+ return (
349
+ <Flex direction="column" h="100%" justify="center" align="center">
350
+ <Stack align="center" gap="md" pt="xl">
351
+ <IconMessageCircle size={64} color="var(--mantine-color-gray-4)" />
352
+ <Text size="lg" c="dimmed" fw={500}>
353
+ No messages found
354
+ </Text>
355
+ </Stack>
356
+ </Flex>
357
+ );
358
+ }
@@ -0,0 +1,40 @@
1
+ .planDefinition {
2
+ cursor: pointer;
3
+ }
4
+
5
+ .selected {
6
+ background-color: var(--mantine-color-blue-6) !important;
7
+ }
8
+
9
+ .footer {
10
+ display: flex;
11
+ justify-content: flex-end;
12
+ padding: var(--mantine-spacing-md);
13
+ height: 70px;
14
+ }
15
+
16
+ .preview {
17
+ height: 100%;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ .notFound {
23
+ display: flex;
24
+ background-color: var(--mantine-color-yellow-0);
25
+ color: var(--mantine-color-yellow-8);
26
+ align-items: center;
27
+ justify-content: center;
28
+ }
29
+
30
+ [data-mantine-color-scheme='dark'] .preview,
31
+ [data-mantine-color-scheme='dark'] .planDefinition,
32
+ [data-mantine-color-scheme='dark'] .footer {
33
+ background-color: var(--mantine-color-dark-8);
34
+ }
35
+
36
+ [data-mantine-color-scheme='light'] .preview,
37
+ [data-mantine-color-scheme='light'] .planDefinition,
38
+ [data-mantine-color-scheme='light'] .footer {
39
+ background-color: var(--mantine-color-gray-1);
40
+ }
@@ -0,0 +1,257 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import {
4
+ Box,
5
+ Button,
6
+ Card,
7
+ Grid,
8
+ Loader,
9
+ Modal,
10
+ Paper,
11
+ ScrollArea,
12
+ Stack,
13
+ Text,
14
+ TextInput,
15
+ Title,
16
+ } from '@mantine/core';
17
+ import { showNotification } from '@mantine/notifications';
18
+ import { normalizeErrorString } from '@medplum/core';
19
+ import type { PlanDefinition } from '@medplum/fhirtypes';
20
+ import { useMedplum } from '@medplum/react';
21
+ import { IconCircleCheck, IconCircleOff } from '@tabler/icons-react';
22
+ import cx from 'clsx';
23
+ import { useCallback, useEffect, useState } from 'react';
24
+ import type { JSX } from 'react';
25
+ import classes from './AddPlanDefinition.module.css';
26
+
27
+ interface AddPlanDefinitionProps {
28
+ encounterId: string;
29
+ patientId: string;
30
+ onApply: () => void;
31
+ }
32
+
33
+ export const AddPlanDefinition = ({ encounterId, patientId, onApply }: AddPlanDefinitionProps): JSX.Element => {
34
+ const [opened, setOpened] = useState(false);
35
+ const [planDefinitions, setPlanDefinitions] = useState<PlanDefinition[]>([]);
36
+ const [selectedPlanDefinition, setSelectedPlanDefinition] = useState<PlanDefinition | undefined>();
37
+ const [searchQuery, setSearchQuery] = useState<string>('');
38
+ const [isLoading, setIsLoading] = useState(false);
39
+ const medplum = useMedplum();
40
+
41
+ const handleApplyPlanDefinition = async (): Promise<void> => {
42
+ if (!selectedPlanDefinition) {
43
+ showNotification({
44
+ color: 'red',
45
+ icon: <IconCircleOff />,
46
+ title: 'Error',
47
+ message: 'No plan definition selected',
48
+ });
49
+ return;
50
+ }
51
+
52
+ try {
53
+ await medplum.post(medplum.fhirUrl('PlanDefinition', selectedPlanDefinition.id as string, '$apply'), {
54
+ resourceType: 'Parameters',
55
+ parameter: [
56
+ {
57
+ name: 'subject',
58
+ valueString: `Patient/${patientId}`,
59
+ },
60
+ {
61
+ name: 'encounter',
62
+ valueString: `Encounter/${encounterId}`,
63
+ },
64
+ ],
65
+ });
66
+
67
+ showNotification({
68
+ color: 'green',
69
+ icon: <IconCircleCheck />,
70
+ title: 'Success',
71
+ message: 'Plan definition applied to the encounter',
72
+ });
73
+
74
+ onApply();
75
+ handleClose();
76
+ } catch (error) {
77
+ showNotification({
78
+ color: 'red',
79
+ icon: <IconCircleOff />,
80
+ title: 'Error',
81
+ message: normalizeErrorString(error),
82
+ });
83
+ }
84
+ };
85
+
86
+ const fetchPlanDefinitions = useCallback(
87
+ (query?: string) => {
88
+ setIsLoading(true);
89
+ const searchParam = query ? `name=${query}` : undefined;
90
+ medplum
91
+ .searchResources('PlanDefinition', searchParam)
92
+ .then((result) => {
93
+ const filteredResult = result.filter((pd) => pd.name?.trim());
94
+ setPlanDefinitions(filteredResult);
95
+ })
96
+ .catch((err) => {
97
+ showNotification({
98
+ color: 'red',
99
+ icon: <IconCircleOff />,
100
+ title: 'Error',
101
+ message: normalizeErrorString(err),
102
+ });
103
+ })
104
+ .finally(() => {
105
+ setIsLoading(false);
106
+ });
107
+ },
108
+ [medplum]
109
+ );
110
+
111
+ useEffect(() => {
112
+ if (opened) {
113
+ fetchPlanDefinitions();
114
+ }
115
+ }, [opened, fetchPlanDefinitions]);
116
+
117
+ useEffect(() => {
118
+ const debounceTimer = setTimeout(() => {
119
+ if (opened) {
120
+ fetchPlanDefinitions(searchQuery);
121
+ }
122
+ }, 300);
123
+
124
+ return () => clearTimeout(debounceTimer);
125
+ }, [searchQuery, opened, fetchPlanDefinitions]);
126
+
127
+ const handleClose = (): void => {
128
+ setOpened(false);
129
+ setSelectedPlanDefinition(undefined);
130
+ setSearchQuery('');
131
+ };
132
+
133
+ return (
134
+ <>
135
+ <Stack gap="md">
136
+ <Button variant="outline" color="blue" fullWidth onClick={() => setOpened(true)}>
137
+ Add care template
138
+ </Button>
139
+ <Text size="sm">Task groups predefined by care planner</Text>
140
+ </Stack>
141
+
142
+ <Modal
143
+ opened={opened}
144
+ onClose={handleClose}
145
+ title="Add Care Template"
146
+ size="75%"
147
+ styles={{
148
+ title: {
149
+ fontSize: '1.2rem',
150
+ fontWeight: 600,
151
+ },
152
+ body: {
153
+ padding: '0',
154
+ height: '80vh',
155
+ },
156
+ }}
157
+ >
158
+ <Stack h="100%" justify="space-between" gap={0}>
159
+ <Box flex={1} miw={0}>
160
+ <Grid p="md">
161
+ <Grid.Col span={6} pr="md">
162
+ <Box>
163
+ <Title order={5} mb="xs">
164
+ Select care template
165
+ </Title>
166
+ <Text size="sm" c="dimmed" mb="md">
167
+ Care templates are predefined sets of actions that can be applied to encounters.
168
+ </Text>
169
+
170
+ <TextInput
171
+ placeholder="Search by name"
172
+ mb="md"
173
+ value={searchQuery}
174
+ onChange={(event) => setSearchQuery(event.currentTarget.value)}
175
+ rightSection={isLoading ? <Loader size={16} /> : null}
176
+ styles={{
177
+ input: {
178
+ '&:focus': {
179
+ borderColor: '#228be6',
180
+ },
181
+ },
182
+ }}
183
+ />
184
+
185
+ <ScrollArea style={{ height: 'calc(80vh - 250px)' }} type="scroll">
186
+ {planDefinitions.length > 0 &&
187
+ planDefinitions.map((plan) => (
188
+ <Card
189
+ key={plan.id}
190
+ p="md"
191
+ mb="xs"
192
+ className={cx(classes.planDefinition, {
193
+ [classes.selected]: selectedPlanDefinition?.id === plan.id,
194
+ })}
195
+ onClick={() => setSelectedPlanDefinition(plan)}
196
+ >
197
+ <Text fz="md" fw={500} c={selectedPlanDefinition?.id === plan.id ? 'white' : undefined}>
198
+ {plan.name}
199
+ </Text>
200
+ <Text fw={500} c={selectedPlanDefinition?.id === plan.id ? 'white' : 'dimmed'}>
201
+ {plan.subtitle}
202
+ </Text>
203
+ </Card>
204
+ ))}
205
+
206
+ {planDefinitions.length === 0 && !isLoading && (
207
+ <Paper className={classes.notFound} h={40}>
208
+ <Text>Nothing found! Try searching for another name of the care plan.</Text>
209
+ </Paper>
210
+ )}
211
+ </ScrollArea>
212
+ </Box>
213
+ </Grid.Col>
214
+
215
+ <Grid.Col span={6}>
216
+ <Paper withBorder className={classes.preview}>
217
+ <ScrollArea style={{ height: 'calc(80vh - 110px)' }} type="scroll">
218
+ <Stack gap="sm" px="md" pt="md">
219
+ <Title order={5}>Preview</Title>
220
+ {selectedPlanDefinition ? (
221
+ <>
222
+ <Stack gap={0} p={0}>
223
+ <Text fz="md" fw={500}>
224
+ {selectedPlanDefinition.name}
225
+ </Text>
226
+ <Text fw={500} c="dimmed">
227
+ {selectedPlanDefinition.subtitle}
228
+ </Text>
229
+ </Stack>
230
+
231
+ <Stack gap="xs" pb="md">
232
+ {selectedPlanDefinition.action?.map((action, index) => (
233
+ <Card key={`${action.id}-task-${index}`} withBorder shadow="sm">
234
+ <Text fw={500}>{action.title}</Text>
235
+ {action.description && <Text c="dimmed">{action.description}</Text>}
236
+ </Card>
237
+ ))}
238
+ </Stack>
239
+ </>
240
+ ) : (
241
+ <Text c="dimmed">Select a template to see preview.</Text>
242
+ )}
243
+ </Stack>
244
+ </ScrollArea>
245
+ </Paper>
246
+ </Grid.Col>
247
+ </Grid>
248
+ </Box>
249
+
250
+ <Box className={classes.footer} h={70} p="md">
251
+ <Button onClick={handleApplyPlanDefinition}>Add care template</Button>
252
+ </Box>
253
+ </Stack>
254
+ </Modal>
255
+ </>
256
+ );
257
+ };
@@ -0,0 +1,7 @@
1
+ [data-mantine-color-scheme='dark'] .planDefinition {
2
+ background-color: var(--mantine-color-dark-8);
3
+ }
4
+
5
+ [data-mantine-color-scheme='light'] .planDefinition {
6
+ background-color: var(--mantine-color-gray-1);
7
+ }