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,943 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import {
4
+ Paper,
5
+ Stack,
6
+ Text,
7
+ Group,
8
+ Badge,
9
+ Divider,
10
+ Loader,
11
+ Button,
12
+ Timeline,
13
+ ThemeIcon,
14
+ ScrollArea,
15
+ } from '@mantine/core';
16
+ import { formatDate, formatHumanName } from '@medplum/core';
17
+ import type {
18
+ ServiceRequest,
19
+ HumanName,
20
+ DocumentReference,
21
+ DiagnosticReport,
22
+ QuestionnaireResponse,
23
+ MedicationRequest,
24
+ Reference,
25
+ CarePlan,
26
+ } from '@medplum/fhirtypes';
27
+ import type { JSX } from 'react';
28
+ import { useResource, useMedplum, AttachmentDisplay, ObservationTable } from '@medplum/react';
29
+ import { IconSend, IconCheck, IconFlask, IconClipboardCheck } from '@tabler/icons-react';
30
+ import { useState, useEffect, useMemo } from 'react';
31
+ import { fetchLabOrderRequisitionDocuments, getHealthGorillaRequisitionId } from '../../utils/documentReference';
32
+ import classes from './LabOrderDetails.module.css';
33
+ import cx from 'clsx';
34
+ import { showErrorNotification } from '../../utils/notifications';
35
+
36
+ interface LabOrderDetailsProps {
37
+ order: ServiceRequest;
38
+ }
39
+
40
+ interface ProgressStep {
41
+ id: string;
42
+ title: string;
43
+ description: string;
44
+ icon: JSX.Element;
45
+ status: 'completed' | 'current' | 'pending';
46
+ timestamp?: string;
47
+ }
48
+
49
+ export function LabOrderDetails(props: LabOrderDetailsProps): JSX.Element {
50
+ const { order } = props;
51
+ const medplum = useMedplum();
52
+ const patient = useResource(order.subject);
53
+ const requester = useResource(order.requester);
54
+ const [diagnosticReports, setDiagnosticReports] = useState<DiagnosticReport[]>([]);
55
+ const [labOrderRequisitionDocs, setLabOrderRequisitionDocs] = useState<DocumentReference[]>([]);
56
+ const [loadingDocs, setLoadingDocs] = useState<boolean>(false);
57
+ const [specimenLabelDocs, setSpecimenLabelDocs] = useState<DocumentReference[]>([]);
58
+ const [loadingSpecimenDocs, setLoadingSpecimenDocs] = useState<boolean>(false);
59
+ const [questionnaireResponse, setQuestionnaireResponse] = useState<QuestionnaireResponse | null>(null);
60
+ const [loadingQuestionnaire, setLoadingQuestionnaire] = useState<boolean>(false);
61
+ const [activeDetailTab, setActiveDetailTab] = useState<'report' | 'progress' | 'order'>(
62
+ order.status !== 'completed' ? 'progress' : 'report'
63
+ );
64
+
65
+ // Filter DiagnosticReports for this specific order
66
+ useEffect(() => {
67
+ const fetchPrimaryReport = async (): Promise<void> => {
68
+ const primaryReport = await medplum.searchResources('DiagnosticReport', {
69
+ 'based-on': `ServiceRequest/${order.id}`,
70
+ _sort: '-_lastUpdated',
71
+ _count: 1,
72
+ });
73
+ setDiagnosticReports(primaryReport);
74
+ };
75
+
76
+ fetchPrimaryReport().catch(showErrorNotification);
77
+ }, [medplum, order.id]);
78
+
79
+ // Get the primary diagnostic report for this order
80
+ const primaryReport = diagnosticReports.length > 0 ? diagnosticReports[0] : undefined;
81
+
82
+ // Progress tracker logic
83
+ const getProgressSteps = useMemo((): ProgressStep[] => {
84
+ const steps: ProgressStep[] = [
85
+ {
86
+ id: 'order-sent',
87
+ title: 'Order Sent',
88
+ description: 'Lab order has been submitted',
89
+ icon: <IconSend size={16} />,
90
+ status: 'completed',
91
+ timestamp:
92
+ order.authoredOn || order.meta?.lastUpdated
93
+ ? formatDate(order.authoredOn || order.meta?.lastUpdated)
94
+ : undefined,
95
+ },
96
+ {
97
+ id: 'lab-acknowledged',
98
+ title: 'Order Acknowledged',
99
+ description: 'Lab has received and acknowledged the order',
100
+ icon: <IconCheck size={16} />,
101
+ status: 'pending',
102
+ },
103
+ {
104
+ id: 'testing',
105
+ title: 'Testing',
106
+ description: 'Lab is processing the sample',
107
+ icon: <IconFlask size={16} />,
108
+ status: 'pending',
109
+ },
110
+ {
111
+ id: 'final',
112
+ title: 'Final',
113
+ description: 'Results are ready',
114
+ icon: <IconClipboardCheck size={16} />,
115
+ status: 'pending',
116
+ },
117
+ ];
118
+
119
+ // Determine current step based on available data
120
+ let currentStepIndex = 0;
121
+
122
+ // Step 1: Order Sent - always completed if order exists
123
+ if (order.authoredOn) {
124
+ currentStepIndex = 1;
125
+ }
126
+
127
+ // Step 2: Lab Acknowledged - presence of accession number or LabOrderRequisition
128
+ if (order.requisition?.value || labOrderRequisitionDocs.length > 0) {
129
+ steps[1].status = 'completed';
130
+ steps[1].timestamp = order.requisition?.value ? 'Acknowledged' : undefined;
131
+ currentStepIndex = 2;
132
+ }
133
+
134
+ // Step 3: Testing - DiagnosticReport.effectiveDateTime
135
+ if (primaryReport?.effectiveDateTime) {
136
+ steps[2].status = 'completed';
137
+ steps[2].timestamp = formatDate(primaryReport.effectiveDateTime);
138
+ currentStepIndex = 3;
139
+ }
140
+
141
+ // Step 4: Final - DiagnosticReport.issued timestamp
142
+ if (primaryReport?.issued && primaryReport.status === 'final') {
143
+ steps[3].status = 'completed';
144
+ steps[3].timestamp = formatDate(primaryReport.issued);
145
+ currentStepIndex = 4;
146
+ }
147
+
148
+ // Set current step
149
+ if (currentStepIndex < steps.length) {
150
+ steps[currentStepIndex].status = 'current';
151
+ }
152
+
153
+ return steps;
154
+ }, [order, primaryReport, labOrderRequisitionDocs]);
155
+
156
+ // Helper function to get step color
157
+ const getStepColor = (status: 'completed' | 'current' | 'pending'): string => {
158
+ if (status === 'completed') {
159
+ return 'green';
160
+ }
161
+ if (status === 'current') {
162
+ return 'blue';
163
+ }
164
+ return 'gray.2';
165
+ };
166
+
167
+ // Fetch Lab Order Requisition documents when order changes
168
+ useEffect(() => {
169
+ const fetchDocuments = async (): Promise<void> => {
170
+ if (!order.id) {
171
+ setLabOrderRequisitionDocs([]);
172
+ return;
173
+ }
174
+ setLoadingDocs(true);
175
+ setLabOrderRequisitionDocs([]); // Clear previous documents immediately
176
+
177
+ try {
178
+ const docs = await fetchLabOrderRequisitionDocuments(medplum, order);
179
+ setLabOrderRequisitionDocs(docs);
180
+ } catch (error) {
181
+ console.error('Error fetching lab order requisition documents:', error);
182
+ setLabOrderRequisitionDocs([]);
183
+ } finally {
184
+ setLoadingDocs(false);
185
+ }
186
+ };
187
+
188
+ fetchDocuments().catch(console.error);
189
+
190
+ // Cleanup function to clear documents when component unmounts or order changes
191
+ return () => {
192
+ setLabOrderRequisitionDocs([]);
193
+ setLoadingDocs(false);
194
+ };
195
+ }, [medplum, order]);
196
+
197
+ // Fetch Specimen Label documents when order changes
198
+ useEffect(() => {
199
+ const fetchSpecimenLabelDocuments = async (): Promise<void> => {
200
+ if (!order.id) {
201
+ setSpecimenLabelDocs([]);
202
+ return;
203
+ }
204
+ setLoadingSpecimenDocs(true);
205
+ setSpecimenLabelDocs([]); // Clear previous documents immediately
206
+
207
+ try {
208
+ // Extract Health Gorilla Requisition ID from ServiceRequest (same as requisition docs)
209
+ const healthGorillaRequisitionId = getHealthGorillaRequisitionId(order);
210
+
211
+ if (!healthGorillaRequisitionId) {
212
+ setSpecimenLabelDocs([]);
213
+ return;
214
+ }
215
+
216
+ // Search for DocumentReference with category "SpecimenLabel" using the same identifier pattern
217
+ const searchParams = new URLSearchParams({
218
+ category: 'SpecimenLabel',
219
+ identifier: `https://www.healthgorilla.com|${healthGorillaRequisitionId}`,
220
+ _sort: '-_lastUpdated',
221
+ });
222
+
223
+ const searchResult = await medplum.searchResources('DocumentReference', searchParams, { cache: 'no-cache' });
224
+ setSpecimenLabelDocs(searchResult);
225
+ } catch (error) {
226
+ console.error('Error fetching specimen label documents:', error);
227
+ setSpecimenLabelDocs([]);
228
+ } finally {
229
+ setLoadingSpecimenDocs(false);
230
+ }
231
+ };
232
+
233
+ fetchSpecimenLabelDocuments().catch(console.error);
234
+
235
+ // Cleanup function to clear documents when component unmounts or order changes
236
+ return () => {
237
+ setSpecimenLabelDocs([]);
238
+ setLoadingSpecimenDocs(false);
239
+ };
240
+ }, [medplum, order]);
241
+
242
+ // Fetch QuestionnaireResponse when order changes
243
+ useEffect(() => {
244
+ const fetchQuestionnaireResponse = async (): Promise<void> => {
245
+ setQuestionnaireResponse(null);
246
+
247
+ // First, check if current order has QuestionnaireResponse in supportingInfo
248
+ if (order.supportingInfo && order.supportingInfo.length > 0) {
249
+ const questionnaireRef = order.supportingInfo.find((ref) =>
250
+ ref.reference?.startsWith('QuestionnaireResponse/')
251
+ );
252
+
253
+ if (questionnaireRef?.reference) {
254
+ try {
255
+ setLoadingQuestionnaire(true);
256
+ const response = await medplum.readResource(
257
+ 'QuestionnaireResponse',
258
+ questionnaireRef.reference.split('/')[1]
259
+ );
260
+ setQuestionnaireResponse(response);
261
+ return;
262
+ } catch (error) {
263
+ console.error('Error fetching questionnaire response from current order:', error);
264
+ } finally {
265
+ setLoadingQuestionnaire(false);
266
+ }
267
+ }
268
+ }
269
+
270
+ // For HealthGorilla case: if no QuestionnaireResponse found in current order,
271
+ // search for the original ServiceRequest that has this ServiceRequest in its basedOn field
272
+ // This applies to both open and completed orders
273
+ if (order.id) {
274
+ try {
275
+ setLoadingQuestionnaire(true);
276
+
277
+ // Search for ServiceRequests for the same patient with the same code to narrow down results
278
+ const searchResult = await medplum.searchResources('ServiceRequest', {
279
+ subject: order.subject?.reference,
280
+ code: order.code?.coding?.[0]?.code, // Include the same code to narrow down results
281
+ _count: 50, // Get more results to filter through
282
+ });
283
+
284
+ if (searchResult && searchResult.length > 0) {
285
+ // Find the ServiceRequest that has this order in its basedOn field
286
+ // Prioritize those that also have QuestionnaireResponse in supportingInfo
287
+ const originalOrder = searchResult.find((sr: ServiceRequest) => {
288
+ if (sr.basedOn && sr.basedOn.length > 0) {
289
+ const hasBasedOnMatch = sr.basedOn.some(
290
+ (ref: Reference<CarePlan | ServiceRequest | MedicationRequest>) =>
291
+ ref.reference === `ServiceRequest/${order.id}`
292
+ );
293
+
294
+ if (hasBasedOnMatch) {
295
+ // Check if this ServiceRequest also has QuestionnaireResponse in supportingInfo
296
+ return sr.supportingInfo?.some((ref) => ref.reference?.startsWith('QuestionnaireResponse/')) ?? false;
297
+ }
298
+ return false;
299
+ }
300
+ return false;
301
+ });
302
+
303
+ if (originalOrder) {
304
+ // Check if original order has QuestionnaireResponse in supportingInfo
305
+ if (originalOrder.supportingInfo && originalOrder.supportingInfo.length > 0) {
306
+ const questionnaireRef = originalOrder.supportingInfo.find((ref) =>
307
+ ref.reference?.startsWith('QuestionnaireResponse/')
308
+ );
309
+
310
+ if (questionnaireRef?.reference) {
311
+ const response = await medplum.readResource(
312
+ 'QuestionnaireResponse',
313
+ questionnaireRef.reference.split('/')[1]
314
+ );
315
+ setQuestionnaireResponse(response);
316
+ }
317
+ }
318
+ } else {
319
+ // Alternative: Look for any ServiceRequest with QuestionnaireResponse in supportingInfo
320
+ const serviceRequestWithQuestionnaire = searchResult.find((sr: ServiceRequest) =>
321
+ sr.supportingInfo?.some((ref) => ref.reference?.startsWith('QuestionnaireResponse/'))
322
+ );
323
+
324
+ if (serviceRequestWithQuestionnaire) {
325
+ const questionnaireRef = serviceRequestWithQuestionnaire.supportingInfo?.find((ref) =>
326
+ ref.reference?.startsWith('QuestionnaireResponse/')
327
+ );
328
+
329
+ if (questionnaireRef?.reference) {
330
+ const response = await medplum.readResource(
331
+ 'QuestionnaireResponse',
332
+ questionnaireRef.reference.split('/')[1]
333
+ );
334
+ setQuestionnaireResponse(response);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ } catch (error) {
340
+ console.error('Error fetching questionnaire response from original order:', error);
341
+ } finally {
342
+ setLoadingQuestionnaire(false);
343
+ }
344
+ }
345
+ };
346
+
347
+ fetchQuestionnaireResponse().catch(console.error);
348
+
349
+ // Cleanup function
350
+ return () => {
351
+ setQuestionnaireResponse(null);
352
+ setLoadingQuestionnaire(false);
353
+ };
354
+ }, [medplum, order]);
355
+
356
+ return (
357
+ <ScrollArea h="100%">
358
+ <Paper h="100%">
359
+ <Stack gap="0">
360
+ <Stack gap="md" p="md">
361
+ <Stack gap="md">
362
+ <Stack gap="0">
363
+ <Text size="xl" fw={800}>
364
+ {(() => {
365
+ // If there are multiple codes (2 or more), show them separated by commas
366
+ if (order.code?.coding && order.code.coding.length >= 2) {
367
+ return order.code.coding.map((coding) => coding.display).join(', ');
368
+ }
369
+
370
+ // If there's a text field and only one code, use the text field
371
+ if (order.code?.text) {
372
+ return order.code.text;
373
+ }
374
+
375
+ // Otherwise, show the first code or fallback
376
+ return order.code?.coding?.[0]?.display || 'Lab Order';
377
+ })()}
378
+ </Text>
379
+ <Text size="sm" c="gray.7">
380
+ {order.status === 'completed' && order.meta?.lastUpdated
381
+ ? `Completed ${formatDate(order.meta.lastUpdated)} • Ordered ${formatDate(order.authoredOn || order.meta?.lastUpdated)}`
382
+ : `Ordered ${formatDate(order.authoredOn || order.meta?.lastUpdated)}`}
383
+ </Text>
384
+ </Stack>
385
+ <Divider />
386
+ <Group justify="space-between" align="center">
387
+ <Group gap="xs">
388
+ <Button
389
+ className={cx(classes.button, {
390
+ [classes.selected]: activeDetailTab === (order.status !== 'completed' ? 'progress' : 'report'),
391
+ })}
392
+ h={32}
393
+ radius="xl"
394
+ onClick={() => setActiveDetailTab(order.status !== 'completed' ? 'progress' : 'report')}
395
+ >
396
+ {order.status !== 'completed' ? 'Progress Tracker' : 'Report'}
397
+ </Button>
398
+ <Button
399
+ className={cx(classes.button, { [classes.selected]: activeDetailTab === 'order' })}
400
+ h={32}
401
+ radius="xl"
402
+ onClick={() => setActiveDetailTab('order')}
403
+ >
404
+ Order Details
405
+ </Button>
406
+ </Group>
407
+ <Badge size="lg" color={getStatusColor(order.status)} variant="light">
408
+ {getStatusDisplayText(order.status)}
409
+ </Badge>
410
+ </Group>
411
+ </Stack>
412
+ </Stack>
413
+
414
+ <Stack gap="xs" p="md">
415
+ {/* Order Details Tab Content */}
416
+ {activeDetailTab === 'order' && (
417
+ <Stack gap="md">
418
+ <Stack gap="sm" mb="xl">
419
+ <Group align="flex-start" gap="lg">
420
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
421
+ Order Date
422
+ </Text>
423
+ <Text size="sm">{formatDate(order.authoredOn || order.meta?.lastUpdated)}</Text>
424
+ </Group>
425
+
426
+ {order.code?.coding && (
427
+ <Group align="flex-start" gap="lg">
428
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
429
+ Test Code
430
+ </Text>
431
+ <Stack gap="xs">
432
+ {order.code.coding.map((coding, index) => (
433
+ <Text key={index} size="sm">
434
+ {coding.display} ({coding.code})
435
+ </Text>
436
+ ))}
437
+ </Stack>
438
+ </Group>
439
+ )}
440
+
441
+ {requester?.resourceType === 'Practitioner' && (
442
+ <Group align="flex-start" gap="lg">
443
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
444
+ Ordering provider
445
+ </Text>
446
+ <Text size="sm">{formatHumanName(requester.name?.[0] as HumanName)}</Text>
447
+ </Group>
448
+ )}
449
+
450
+ {order.performer?.[0]?.display && (
451
+ <Group align="flex-start" gap="lg">
452
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
453
+ Performing lab
454
+ </Text>
455
+ <Text size="sm">{order.performer[0].display}</Text>
456
+ </Group>
457
+ )}
458
+
459
+ {order.requisition?.value && (
460
+ <Group align="flex-start" gap="lg">
461
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
462
+ Requisition ID
463
+ </Text>
464
+ <Text size="sm">{order.requisition.value}</Text>
465
+ </Group>
466
+ )}
467
+
468
+ {patient?.resourceType === 'Patient' && (
469
+ <Group align="flex-start" gap="lg">
470
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
471
+ Patient
472
+ </Text>
473
+ <Text size="sm">{formatHumanName(patient.name?.[0] as HumanName)}</Text>
474
+ </Group>
475
+ )}
476
+
477
+ {order.priority && (
478
+ <Group align="flex-start" gap="lg">
479
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
480
+ Priority
481
+ </Text>
482
+ <Text size="sm">{order.priority}</Text>
483
+ </Group>
484
+ )}
485
+
486
+ {order.reasonCode && (
487
+ <Group align="flex-start" gap="lg">
488
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
489
+ Reason
490
+ </Text>
491
+ <Stack gap="xs">
492
+ {order.reasonCode.map((reason, index) => (
493
+ <Text key={index} size="sm">
494
+ {reason.text || reason.coding?.[0]?.display}
495
+ </Text>
496
+ ))}
497
+ </Stack>
498
+ </Group>
499
+ )}
500
+
501
+ {order.note && (
502
+ <Group align="flex-start" gap="lg">
503
+ <Text fw={500} size="sm" style={{ width: '150px' }} c="dimmed">
504
+ Notes
505
+ </Text>
506
+ <Stack gap="xs">
507
+ {order.note.map((note, index) => (
508
+ <Text key={index} size="sm">
509
+ {note.text}
510
+ </Text>
511
+ ))}
512
+ </Stack>
513
+ </Group>
514
+ )}
515
+
516
+ {order.orderDetail && (
517
+ <>
518
+ <Divider />
519
+ <Stack gap="sm">
520
+ <Text fw={800} size="lg">
521
+ Order Details
522
+ </Text>
523
+ {order.orderDetail.map((detail, index) => (
524
+ <Group key={index} align="flex-start">
525
+ <Text fw={500} size="sm">
526
+ Detail {index + 1}:
527
+ </Text>
528
+ <Text size="sm">{detail.text || detail.coding?.[0]?.display}</Text>
529
+ </Group>
530
+ ))}
531
+ </Stack>
532
+ </>
533
+ )}
534
+ </Stack>
535
+
536
+ {/* Lab Order Requisition Documents - show for both open and completed items */}
537
+ <Divider />
538
+ <Stack gap="lg" mb="xl">
539
+ <Text fw={800} size="md" pb="0">
540
+ Requisition Document
541
+ </Text>
542
+ {loadingDocs && (
543
+ <Group>
544
+ <Loader size="sm" />
545
+ <Text size="sm" c="dimmed">
546
+ Loading requisition documents...
547
+ </Text>
548
+ </Group>
549
+ )}
550
+
551
+ {!loadingDocs && labOrderRequisitionDocs.length > 0 && (
552
+ <Stack gap="md">
553
+ {labOrderRequisitionDocs.map((doc, index) => (
554
+ <Stack key={doc.id || index} gap="xs">
555
+ {doc.content && doc.content.length > 0 && (
556
+ <Stack gap="xs">
557
+ {doc.content.map((content, contentIndex) => (
558
+ <div
559
+ key={contentIndex}
560
+ style={{
561
+ height: '600px',
562
+ borderRadius: '4px',
563
+ overflow: 'hidden',
564
+ border: '1px solid #3C3C3C',
565
+ }}
566
+ >
567
+ <style>
568
+ {`
569
+ div[data-testid="attachment-iframe"] {
570
+ height: 600px !important;
571
+ }
572
+ div[data-testid="attachment-iframe"] iframe {
573
+ height: 600px !important;
574
+ }
575
+ `}
576
+ </style>
577
+ <AttachmentDisplay value={content.attachment} />
578
+ </div>
579
+ ))}
580
+ </Stack>
581
+ )}
582
+ </Stack>
583
+ ))}
584
+ </Stack>
585
+ )}
586
+
587
+ {!loadingDocs && labOrderRequisitionDocs.length === 0 && (
588
+ <Stack gap="xs">
589
+ <Text size="sm" c="dimmed">
590
+ No lab order requisition documents found.
591
+ </Text>
592
+ </Stack>
593
+ )}
594
+ </Stack>
595
+
596
+ {/* Order Entry Questions - show when QuestionnaireResponse is linked */}
597
+ {questionnaireResponse && (
598
+ <>
599
+ <Divider />
600
+ <Stack gap="md" mb="xl">
601
+ <Text fw={800} size="md">
602
+ Order Entry Questions
603
+ </Text>
604
+
605
+ {loadingQuestionnaire && (
606
+ <Group>
607
+ <Loader size="sm" />
608
+ <Text size="sm" c="dimmed">
609
+ Loading questionnaire response...
610
+ </Text>
611
+ </Group>
612
+ )}
613
+
614
+ {!loadingQuestionnaire && questionnaireResponse && (
615
+ <Stack gap="sm">
616
+ {questionnaireResponse.item && questionnaireResponse.item.length > 0 ? (
617
+ questionnaireResponse.item.map((item, index) => (
618
+ <Group key={index} align="flex-start" style={{ alignItems: 'flex-start' }} gap="lg">
619
+ <div style={{ width: '150px', flexShrink: 0 }}>
620
+ <Text fw={500} size="sm" c="dimmed">
621
+ {item.text || item.linkId || `Question ${index + 1}`}
622
+ </Text>
623
+ </div>
624
+ <div style={{ flex: 1 }}>
625
+ {item.answer && item.answer.length > 0 ? (
626
+ <Stack gap="xs">
627
+ {item.answer.map((answer, answerIndex) => {
628
+ // Extract the answer value based on FHIR QuestionnaireResponse answer types
629
+ let answerText = 'No answer provided';
630
+
631
+ if (answer.valueString) {
632
+ answerText = answer.valueString;
633
+ } else if (answer.valueCoding?.display) {
634
+ answerText = answer.valueCoding.display;
635
+ } else if (answer.valueCoding?.code) {
636
+ answerText = `${answer.valueCoding.code}${answer.valueCoding.display ? ` - ${answer.valueCoding.display}` : ''}`;
637
+ } else if (answer.valueBoolean !== undefined) {
638
+ answerText = answer.valueBoolean.toString();
639
+ } else if (answer.valueInteger !== undefined) {
640
+ answerText = answer.valueInteger.toString();
641
+ } else if (answer.valueDecimal !== undefined) {
642
+ answerText = answer.valueDecimal.toString();
643
+ } else if (answer.valueDate) {
644
+ answerText = answer.valueDate;
645
+ } else if (answer.valueDateTime) {
646
+ answerText = answer.valueDateTime;
647
+ } else if (answer.valueTime) {
648
+ answerText = answer.valueTime;
649
+ } else if (answer.valueUri) {
650
+ answerText = answer.valueUri;
651
+ } else if (answer.valueQuantity) {
652
+ answerText = `${answer.valueQuantity.value}${answer.valueQuantity.unit ? ` ${answer.valueQuantity.unit}` : ''}`;
653
+ } else if (answer.valueReference?.display) {
654
+ answerText = answer.valueReference.display;
655
+ } else if (answer.valueReference?.reference) {
656
+ answerText = answer.valueReference.reference;
657
+ } else if (answer.valueAttachment?.title) {
658
+ answerText = answer.valueAttachment.title;
659
+ } else if (answer.valueAttachment?.url) {
660
+ answerText = answer.valueAttachment.url;
661
+ }
662
+
663
+ return (
664
+ <Text key={answerIndex} size="sm">
665
+ {answerText}
666
+ </Text>
667
+ );
668
+ })}
669
+ </Stack>
670
+ ) : (
671
+ <Text size="sm" c="dimmed">
672
+ No answer provided
673
+ </Text>
674
+ )}
675
+ </div>
676
+ </Group>
677
+ ))
678
+ ) : (
679
+ <Text size="sm" c="dimmed">
680
+ No questionnaire items found.
681
+ </Text>
682
+ )}
683
+ </Stack>
684
+ )}
685
+ </Stack>
686
+ </>
687
+ )}
688
+
689
+ {/* Specimen Label Documents - show for both open and completed items */}
690
+ <Divider />
691
+ <Stack gap="lg" mb="xl">
692
+ <Text fw={800} size="md" pb="0">
693
+ Specimen Label
694
+ </Text>
695
+ {loadingSpecimenDocs && (
696
+ <Group>
697
+ <Loader size="sm" />
698
+ <Text size="sm" c="dimmed">
699
+ Loading specimen label documents...
700
+ </Text>
701
+ </Group>
702
+ )}
703
+
704
+ {!loadingSpecimenDocs && specimenLabelDocs.length > 0 && (
705
+ <Stack gap="md">
706
+ {specimenLabelDocs.map((doc, index) => (
707
+ <Stack key={doc.id || index} gap="xs">
708
+ {doc.content && doc.content.length > 0 && (
709
+ <Stack gap="xs">
710
+ {doc.content.map((content, contentIndex) => (
711
+ <div
712
+ key={contentIndex}
713
+ style={{
714
+ height: '600px',
715
+ borderRadius: '4px',
716
+ overflow: 'hidden',
717
+ border: '1px solid #3C3C3C',
718
+ }}
719
+ >
720
+ <style>
721
+ {`
722
+ div[data-testid="attachment-iframe"] {
723
+ height: 600px !important;
724
+ }
725
+ div[data-testid="attachment-iframe"] iframe {
726
+ height: 600px !important;
727
+ }
728
+ `}
729
+ </style>
730
+ <AttachmentDisplay value={content.attachment} />
731
+ </div>
732
+ ))}
733
+ </Stack>
734
+ )}
735
+ </Stack>
736
+ ))}
737
+ </Stack>
738
+ )}
739
+
740
+ {!loadingSpecimenDocs && specimenLabelDocs.length === 0 && (
741
+ <Text size="sm" c="dimmed">
742
+ No specimen label documents found.
743
+ </Text>
744
+ )}
745
+ </Stack>
746
+ </Stack>
747
+ )}
748
+
749
+ {/* Progress Tracker Tab Content - for open items */}
750
+ {activeDetailTab === 'progress' && (
751
+ <Stack gap="md">
752
+ <Stack p="xl" align="center">
753
+ <Timeline
754
+ active={getProgressSteps.findIndex((step) => step.status === 'current')}
755
+ bulletSize={24}
756
+ lineWidth={2}
757
+ color="green"
758
+ styles={{
759
+ root: {
760
+ maxWidth: '400px',
761
+ },
762
+ }}
763
+ >
764
+ {getProgressSteps.map((step, index) => {
765
+ const nextStep = getProgressSteps[index + 1];
766
+ const isCurrentToNext = step.status === 'current' && nextStep;
767
+ const isCompletedToCompleted = step.status === 'completed' && nextStep?.status === 'completed';
768
+ const isCompletedToCurrent = step.status === 'completed' && nextStep?.status === 'current';
769
+ const isPendingToPending = step.status === 'pending' && nextStep?.status === 'pending';
770
+
771
+ return (
772
+ <Timeline.Item
773
+ key={step.id}
774
+ lineVariant={isCurrentToNext || isPendingToPending ? 'dotted' : 'solid'}
775
+ bullet={
776
+ <ThemeIcon
777
+ size={32}
778
+ radius="xl"
779
+ color={getStepColor(step.status)}
780
+ variant="filled"
781
+ style={{
782
+ color: step.status === 'pending' ? 'var(--mantine-color-gray-8)' : undefined,
783
+ }}
784
+ data-completed-to-completed={isCompletedToCompleted ? 'true' : undefined}
785
+ data-completed-to-current={isCompletedToCurrent ? 'true' : undefined}
786
+ >
787
+ {step.icon}
788
+ </ThemeIcon>
789
+ }
790
+ title={
791
+ <Group gap="xs" align="center">
792
+ <Text fw={step.status === 'current' ? 600 : 500} size="sm">
793
+ {step.title}
794
+ </Text>
795
+ {step.timestamp && (
796
+ <Badge size="xs" variant="light" color="gray">
797
+ {step.timestamp}
798
+ </Badge>
799
+ )}
800
+ </Group>
801
+ }
802
+ >
803
+ <Text size="sm" c="dimmed">
804
+ {step.description}
805
+ </Text>
806
+ </Timeline.Item>
807
+ );
808
+ })}
809
+ </Timeline>
810
+ </Stack>
811
+ </Stack>
812
+ )}
813
+
814
+ {/* Report Tab Content - for completed items */}
815
+ {activeDetailTab === 'report' && primaryReport && (
816
+ <Stack gap="sm" mb="xl">
817
+ {primaryReport.result && primaryReport.result.length > 0 && (
818
+ <Stack pt="md">
819
+ <ObservationTable value={primaryReport.result} hideObservationNotes={false} />
820
+ </Stack>
821
+ )}
822
+
823
+ <Stack mt="md">
824
+ <Group align="flex-start">
825
+ <Text fw={500} size="sm" style={{ minWidth: '150px' }} c="dimmed">
826
+ Report Status
827
+ </Text>
828
+ <Text size="sm" style={{ textTransform: 'capitalize' }}>
829
+ {primaryReport.status}
830
+ </Text>
831
+ </Group>
832
+
833
+ {primaryReport.issued && (
834
+ <Group align="flex-start">
835
+ <Text fw={500} size="sm" style={{ minWidth: '150px' }} c="dimmed">
836
+ Issue Date
837
+ </Text>
838
+ <Text size="sm">{formatDate(primaryReport.issued)}</Text>
839
+ </Group>
840
+ )}
841
+
842
+ {primaryReport.conclusion && (
843
+ <Group align="flex-start">
844
+ <Text fw={500} size="sm" style={{ minWidth: '150px' }} c="dimmed">
845
+ Interpretation
846
+ </Text>
847
+ <Text size="sm">{primaryReport.conclusion}</Text>
848
+ </Group>
849
+ )}
850
+ </Stack>
851
+
852
+ {/* Results PDF */}
853
+ {primaryReport?.presentedForm && primaryReport.presentedForm.length > 0 && (
854
+ <>
855
+ <Divider mt="xl" />
856
+ <Stack gap="lg" mb="xl">
857
+ <Text fw={800} size="md" pb="0">
858
+ Lab Document
859
+ </Text>
860
+ <Stack gap="md">
861
+ {primaryReport.presentedForm.map((form, index) => (
862
+ <Stack key={index} gap="xs">
863
+ <div
864
+ style={{
865
+ height: '600px',
866
+ borderRadius: '4px',
867
+ overflow: 'hidden',
868
+ border: '1px solid #3C3C3C',
869
+ }}
870
+ >
871
+ <style>
872
+ {`
873
+ div[data-testid="attachment-iframe"] {
874
+ height: 600px !important;
875
+ }
876
+ div[data-testid="attachment-iframe"] iframe {
877
+ height: 600px !important;
878
+ }
879
+ `}
880
+ </style>
881
+ <AttachmentDisplay value={form} />
882
+ </div>
883
+ </Stack>
884
+ ))}
885
+ </Stack>
886
+ </Stack>
887
+ </>
888
+ )}
889
+ </Stack>
890
+ )}
891
+ </Stack>
892
+ </Stack>
893
+ </Paper>
894
+ </ScrollArea>
895
+ );
896
+ }
897
+
898
+ const getStatusColor = (status: string | undefined): string => {
899
+ switch (status) {
900
+ case 'active':
901
+ return 'blue';
902
+ case 'draft':
903
+ case 'requested':
904
+ return 'yellow';
905
+ case 'on-hold':
906
+ return 'orange';
907
+ case 'revoked':
908
+ case 'cancelled':
909
+ case 'entered-in-error':
910
+ return 'red';
911
+ case 'completed':
912
+ return 'green';
913
+ case 'unknown':
914
+ return 'gray';
915
+ default:
916
+ return 'gray';
917
+ }
918
+ };
919
+
920
+ const getStatusDisplayText = (status: string | undefined): string => {
921
+ switch (status) {
922
+ case 'active':
923
+ return 'Active';
924
+ case 'draft':
925
+ return 'Draft';
926
+ case 'requested':
927
+ return 'Requested';
928
+ case 'on-hold':
929
+ return 'On Hold';
930
+ case 'revoked':
931
+ return 'Revoked';
932
+ case 'cancelled':
933
+ return 'Cancelled';
934
+ case 'entered-in-error':
935
+ return 'Error';
936
+ case 'completed':
937
+ return 'Completed';
938
+ case 'unknown':
939
+ return 'Unknown';
940
+ default:
941
+ return status || 'Unknown';
942
+ }
943
+ };