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.
- package/README.md +5 -0
- package/dist/examples/vintasend-medplum-example/.env.example +11 -0
- package/dist/examples/vintasend-medplum-example/IMPLEMENTATION_PLAN_FILE_ATTACHMENTS.md +597 -0
- package/dist/examples/vintasend-medplum-example/README.md +190 -0
- package/dist/examples/vintasend-medplum-example/TUTORIAL_EMAIL_NOTIFICATIONS.md +2596 -0
- package/dist/examples/vintasend-medplum-example/bots/handlers/send-pending-notifications-bot.ts +39 -0
- package/dist/examples/vintasend-medplum-example/bots/handlers/send-task-assignment-email.ts +41 -0
- package/dist/examples/vintasend-medplum-example/bots/handlers/task-due-soon-notification-bot.ts +86 -0
- package/dist/examples/vintasend-medplum-example/bots/index.ts +53 -0
- package/dist/examples/vintasend-medplum-example/bots/services/emails/schedule-task-due-soon-email.ts +84 -0
- package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.test.ts +388 -0
- package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.ts +113 -0
- package/dist/examples/vintasend-medplum-example/bots/shared/task-due-soon-helpers.ts +115 -0
- package/dist/examples/vintasend-medplum-example/bots/task-assignment-bot.ts +41 -0
- package/dist/examples/vintasend-medplum-example/compiled-notification-templates.json +6 -0
- package/dist/examples/vintasend-medplum-example/esbuild-script.mjs +71 -0
- package/dist/examples/vintasend-medplum-example/index.html +14 -0
- package/dist/examples/vintasend-medplum-example/lib/constants.ts +32 -0
- package/dist/examples/vintasend-medplum-example/lib/extensions.ts +1 -0
- package/dist/examples/vintasend-medplum-example/lib/file-upload.test.ts +389 -0
- package/dist/examples/vintasend-medplum-example/lib/file-upload.ts +222 -0
- package/dist/examples/vintasend-medplum-example/lib/medplum-singleton.ts +18 -0
- package/dist/examples/vintasend-medplum-example/lib/notification-service.test.ts +293 -0
- package/dist/examples/vintasend-medplum-example/lib/notification-service.ts +284 -0
- package/dist/examples/vintasend-medplum-example/lib/patients.ts +20 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/body.html.pug +37 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/subject.txt.pug +4 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/body.html.pug +34 -0
- package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/subject.txt.pug +4 -0
- package/dist/examples/vintasend-medplum-example/package.json +75 -0
- package/dist/examples/vintasend-medplum-example/plugins/gql-plugin.mjs +31 -0
- package/dist/examples/vintasend-medplum-example/postcss.config.mjs +21 -0
- package/dist/examples/vintasend-medplum-example/public/favicon.ico +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/acuity.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/auth0.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/azure.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/calcom.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/candid.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/claude.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/datadog.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/deepseek.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/entra.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/epic.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/google.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/healthgorilla.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/healthie.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/labcorp.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/okta.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/openai.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/particle.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/quest.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/recaptcha.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/snowflake.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/stedi.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/stripe.png +0 -0
- package/dist/examples/vintasend-medplum-example/public/img/integrations/sumo.png +0 -0
- package/dist/examples/vintasend-medplum-example/scripts/README.md +162 -0
- package/dist/examples/vintasend-medplum-example/scripts/client.ts +18 -0
- package/dist/examples/vintasend-medplum-example/scripts/deploy-bots.ts +171 -0
- package/dist/examples/vintasend-medplum-example/src/App.tsx +185 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.test.tsx +350 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.tsx +241 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.test.tsx +616 -0
- package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.tsx +138 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.test.tsx +92 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.tsx +47 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.test.tsx +464 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.tsx +186 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.test.tsx +80 -0
- package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.tsx +82 -0
- package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.test.tsx +100 -0
- package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.tsx +20 -0
- package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.module.css +3 -0
- package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.tsx +62 -0
- package/dist/examples/vintasend-medplum-example/src/components/MessageWithLinks.tsx +47 -0
- package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.test.tsx +299 -0
- package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.tsx +52 -0
- package/dist/examples/vintasend-medplum-example/src/components/ResourceFormWithRequiredProfile.tsx +82 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.test.tsx +1016 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.tsx +298 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.test.tsx +732 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.tsx +282 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.test.tsx +268 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.tsx +224 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.test.tsx +255 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.tsx +212 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.test.tsx +120 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.tsx +57 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.test.tsx +224 -0
- package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.tsx +100 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.test.tsx +431 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.tsx +130 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.module.css +31 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.test.tsx +234 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.tsx +143 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.module.css +11 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.test.tsx +875 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.tsx +943 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.test.tsx +413 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.tsx +203 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/LabSelectEmpty.tsx +22 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/README.md +104 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.test.tsx +318 -0
- package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.tsx +87 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.test.tsx +126 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.tsx +38 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.module.css +23 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.test.tsx +167 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.tsx +53 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.test.tsx +94 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.tsx +165 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.module.css +8 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.test.tsx +523 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.tsx +230 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.module.css +23 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.test.tsx +567 -0
- package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.tsx +358 -0
- package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.module.css +40 -0
- package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.tsx +257 -0
- package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.module.css +7 -0
- package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.test.tsx +279 -0
- package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.tsx +156 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.module.css +45 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.test.tsx +90 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.tsx +84 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.module.css +26 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.tsx +90 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.test.tsx +305 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.tsx +46 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.module.css +262 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.test.tsx +622 -0
- package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.tsx +286 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/NewTaskModal.tsx +275 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskAttachmentList.tsx +132 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.module.css +45 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.test.tsx +749 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.tsx +416 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.test.tsx +278 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.tsx +133 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.module.css +16 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.test.tsx +255 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.tsx +203 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFileUpload.tsx +129 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.test.tsx +156 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.tsx +142 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.utils.ts +28 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.test.tsx +134 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.tsx +250 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.module.css +23 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.test.tsx +149 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.tsx +53 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.test.tsx +68 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.tsx +46 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.test.tsx +555 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.tsx +170 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.test.tsx +32 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.tsx +34 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.test.tsx +47 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.tsx +29 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.test.tsx +285 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.tsx +129 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.test.tsx +455 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.tsx +167 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.test.tsx +435 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.tsx +116 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.module.css +38 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.test.tsx +200 -0
- package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.tsx +84 -0
- package/dist/examples/vintasend-medplum-example/src/components/utils.test.ts +176 -0
- package/dist/examples/vintasend-medplum-example/src/components/utils.ts +17 -0
- package/dist/examples/vintasend-medplum-example/src/config/constants.ts +3 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.test.tsx +166 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.ts +28 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.test.tsx +94 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.ts +11 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.test.tsx +477 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.ts +191 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.test.tsx +100 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.ts +18 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.test.tsx +379 -0
- package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.ts +194 -0
- package/dist/examples/vintasend-medplum-example/src/index.css +8 -0
- package/dist/examples/vintasend-medplum-example/src/main.tsx +57 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.module.css +6 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.test.tsx +295 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.tsx +124 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.test.tsx +77 -0
- package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.tsx +22 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.test.tsx +87 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.tsx +27 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.module.css +16 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.test.tsx +287 -0
- package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.tsx +151 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.test.tsx +519 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.tsx +179 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/FavoriteMedicationsTable.tsx +76 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.test.tsx +234 -0
- package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.tsx +222 -0
- package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.test.tsx +356 -0
- package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.tsx +275 -0
- package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.module.css +8 -0
- package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.test.tsx +103 -0
- package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.tsx +78 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.test.tsx +84 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.tsx +82 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.test.tsx +364 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.tsx +149 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.test.tsx +159 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.tsx +37 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.test.tsx +140 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.tsx +72 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.test.tsx +57 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.tsx +14 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.test.tsx +241 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.tsx +710 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.module.css +37 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.test.tsx +428 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.tsx +334 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.module.css +24 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.test.tsx +154 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.tsx +115 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.test.ts +223 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.ts +89 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.test.tsx +147 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.tsx +79 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientTabsNavigation.tsx +35 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.test.tsx +185 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.tsx +115 -0
- package/dist/examples/vintasend-medplum-example/src/pages/patient/TimelineTab.tsx +14 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.test.tsx +170 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.tsx +117 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceDetailPage.tsx +28 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.test.tsx +131 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.tsx +65 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.test.tsx +108 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.tsx +16 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.module.css +7 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.test.tsx +37 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.tsx +44 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/useResourceType.ts +44 -0
- package/dist/examples/vintasend-medplum-example/src/pages/resource/utils.ts +9 -0
- package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.test.tsx +302 -0
- package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.tsx +416 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/ChatInput.tsx +91 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.module.css +6 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.test.tsx +102 -0
- package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.tsx +44 -0
- package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.module.css +7 -0
- package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.test.tsx +133 -0
- package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.tsx +91 -0
- package/dist/examples/vintasend-medplum-example/src/test-utils/render.tsx +20 -0
- package/dist/examples/vintasend-medplum-example/src/test.setup.ts +49 -0
- package/dist/examples/vintasend-medplum-example/src/types/encounter.ts +8 -0
- package/dist/examples/vintasend-medplum-example/src/types/spaces.ts +10 -0
- package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.test.ts +141 -0
- package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.ts +59 -0
- package/dist/examples/vintasend-medplum-example/src/utils/claims.test.ts +153 -0
- package/dist/examples/vintasend-medplum-example/src/utils/claims.ts +65 -0
- package/dist/examples/vintasend-medplum-example/src/utils/communication-search.ts +47 -0
- package/dist/examples/vintasend-medplum-example/src/utils/coverage.test.ts +48 -0
- package/dist/examples/vintasend-medplum-example/src/utils/coverage.ts +33 -0
- package/dist/examples/vintasend-medplum-example/src/utils/documentReference.test.ts +102 -0
- package/dist/examples/vintasend-medplum-example/src/utils/documentReference.ts +55 -0
- package/dist/examples/vintasend-medplum-example/src/utils/encounter.test.ts +169 -0
- package/dist/examples/vintasend-medplum-example/src/utils/encounter.ts +261 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-form.test.ts +154 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-form.ts +272 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.test.ts +1137 -0
- package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.ts +827 -0
- package/dist/examples/vintasend-medplum-example/src/utils/notifications.test.ts +27 -0
- package/dist/examples/vintasend-medplum-example/src/utils/notifications.ts +15 -0
- package/dist/examples/vintasend-medplum-example/src/utils/spaceMessaging.ts +249 -0
- package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.test.ts +450 -0
- package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.ts +147 -0
- package/dist/examples/vintasend-medplum-example/src/utils/task-search.ts +63 -0
- package/dist/examples/vintasend-medplum-example/src/vite-env.d.ts +3 -0
- package/dist/examples/vintasend-medplum-example/tsconfig.bots.json +4 -0
- package/dist/examples/vintasend-medplum-example/tsconfig.json +19 -0
- package/dist/examples/vintasend-medplum-example/vercel.json +3 -0
- package/dist/examples/vintasend-medplum-example/vite.config.ts +44 -0
- package/dist/services/notification-backends/base-notification-backend.d.ts +5 -0
- package/dist/services/notification-service.js +11 -1
- package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import type { CodeableConcept, MedicationKnowledge } from '@medplum/fhirtypes';
|
|
4
|
+
import { MockClient } from '@medplum/mock';
|
|
5
|
+
import { MedplumProvider } from '@medplum/react';
|
|
6
|
+
import { showNotification } from '@mantine/notifications';
|
|
7
|
+
import { describe, expect, test, beforeEach, vi } from 'vitest';
|
|
8
|
+
import { DoseSpotFavoritesPage } from './DoseSpotFavoritesPage';
|
|
9
|
+
import { act, fireEvent, render, screen, waitFor } from '../../test-utils/render';
|
|
10
|
+
import { useDoseSpotClinicFormulary } from '@medplum/dosespot-react';
|
|
11
|
+
import * as notifications from '../../utils/notifications';
|
|
12
|
+
|
|
13
|
+
// Mock the hooks and dependencies
|
|
14
|
+
vi.mock('@medplum/dosespot-react', async () => {
|
|
15
|
+
const actual = await vi.importActual('@medplum/dosespot-react');
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
useDoseSpotClinicFormulary: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
vi.mock('@mantine/notifications', () => ({
|
|
23
|
+
showNotification: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('../../utils/notifications', () => ({
|
|
27
|
+
showErrorNotification: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe('DoseSpotFavoritesPage', () => {
|
|
31
|
+
let medplum: MockClient;
|
|
32
|
+
let mockFormularyReturn: ReturnType<typeof useDoseSpotClinicFormulary>;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
|
|
37
|
+
medplum = new MockClient();
|
|
38
|
+
|
|
39
|
+
mockFormularyReturn = {
|
|
40
|
+
state: {
|
|
41
|
+
selectedMedication: undefined,
|
|
42
|
+
directions: undefined,
|
|
43
|
+
},
|
|
44
|
+
searchMedications: vi.fn().mockResolvedValue([]),
|
|
45
|
+
setSelectedMedication: vi.fn(),
|
|
46
|
+
setSelectedMedicationDirections: vi.fn(),
|
|
47
|
+
saveFavoriteMedication: vi.fn(),
|
|
48
|
+
clear: vi.fn(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
|
|
52
|
+
vi.spyOn(medplum, 'search').mockResolvedValue({
|
|
53
|
+
resourceType: 'Bundle',
|
|
54
|
+
type: 'searchset',
|
|
55
|
+
entry: [],
|
|
56
|
+
total: 0,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function setup(): Promise<void> {
|
|
61
|
+
return act(async () => {
|
|
62
|
+
render(
|
|
63
|
+
<MedplumProvider medplum={medplum}>
|
|
64
|
+
<DoseSpotFavoritesPage />
|
|
65
|
+
</MedplumProvider>
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test('Renders page title and add button', async () => {
|
|
71
|
+
await setup();
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText('DoseSpot Medication Favorites')).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByRole('button', { name: 'Add Favorite Medication' })).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('Loads favorite medications on mount', async () => {
|
|
78
|
+
const mockMedicationKnowledge: MedicationKnowledge = {
|
|
79
|
+
resourceType: 'MedicationKnowledge',
|
|
80
|
+
id: 'med-1',
|
|
81
|
+
code: {
|
|
82
|
+
text: 'Aspirin 325mg',
|
|
83
|
+
coding: [
|
|
84
|
+
{
|
|
85
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
86
|
+
code: '1191',
|
|
87
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
vi.spyOn(medplum, 'search').mockResolvedValue({
|
|
94
|
+
resourceType: 'Bundle',
|
|
95
|
+
type: 'searchset',
|
|
96
|
+
entry: [
|
|
97
|
+
{
|
|
98
|
+
resource: { ...mockMedicationKnowledge, id: 'med-1' },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
total: 1,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await setup();
|
|
105
|
+
|
|
106
|
+
await waitFor(() => {
|
|
107
|
+
expect(medplum.search).toHaveBeenCalledWith('MedicationKnowledge', expect.stringContaining('code='));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('Shows error notification when loading favorites fails', async () => {
|
|
112
|
+
const error = new Error('Failed to load favorites');
|
|
113
|
+
vi.spyOn(medplum, 'search').mockRejectedValue(error);
|
|
114
|
+
|
|
115
|
+
await setup();
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(notifications.showErrorNotification).toHaveBeenCalledWith(error);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('Opens modal when Add Favorite Medication button is clicked', async () => {
|
|
123
|
+
await setup();
|
|
124
|
+
|
|
125
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
126
|
+
|
|
127
|
+
await act(async () => {
|
|
128
|
+
fireEvent.click(addButton);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByText('Medication')).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(screen.getByPlaceholderText('Search medications...')).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('Closes modal when close button is clicked', async () => {
|
|
139
|
+
await setup();
|
|
140
|
+
|
|
141
|
+
// Wait for initial load to complete
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
147
|
+
|
|
148
|
+
await act(async () => {
|
|
149
|
+
fireEvent.click(addButton);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(screen.getByText('Medication')).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Find close button - it might be an X icon button or a button with aria-label
|
|
157
|
+
const closeButtons = screen.getAllByRole('button');
|
|
158
|
+
const closeButton = closeButtons.find(
|
|
159
|
+
(btn) =>
|
|
160
|
+
btn.getAttribute('aria-label')?.toLowerCase().includes('close') ||
|
|
161
|
+
btn.className.includes('mantine-Modal-close') ||
|
|
162
|
+
btn.querySelector('[class*="mantine-Modal-close"]')
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(closeButton).toBeDefined();
|
|
166
|
+
|
|
167
|
+
if (closeButton) {
|
|
168
|
+
await act(async () => {
|
|
169
|
+
fireEvent.click(closeButton);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(screen.queryByText('Medication')).not.toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(mockFormularyReturn.clear).toHaveBeenCalled();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('Shows directions input when medication is selected', async () => {
|
|
181
|
+
const mockMedication: CodeableConcept = {
|
|
182
|
+
text: 'Aspirin 325mg',
|
|
183
|
+
coding: [
|
|
184
|
+
{
|
|
185
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
186
|
+
code: '1191',
|
|
187
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Create a new mock return with medication selected
|
|
193
|
+
const formularyReturnWithMedication: ReturnType<typeof useDoseSpotClinicFormulary> = {
|
|
194
|
+
...mockFormularyReturn,
|
|
195
|
+
state: {
|
|
196
|
+
selectedMedication: mockMedication,
|
|
197
|
+
directions: '',
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(formularyReturnWithMedication);
|
|
201
|
+
|
|
202
|
+
await setup();
|
|
203
|
+
|
|
204
|
+
// Wait for initial load to complete
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
210
|
+
|
|
211
|
+
await act(async () => {
|
|
212
|
+
fireEvent.click(addButton);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
expect(screen.getByText('Medication')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// The directions input should appear when a medication is selected
|
|
220
|
+
// Since we're mocking the hook, we verify the component structure supports it
|
|
221
|
+
expect(screen.getByPlaceholderText('Search medications...')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('Updates directions when text input changes', async () => {
|
|
225
|
+
const mockMedication: CodeableConcept = {
|
|
226
|
+
text: 'Aspirin 325mg',
|
|
227
|
+
coding: [
|
|
228
|
+
{
|
|
229
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
230
|
+
code: '1191',
|
|
231
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Create a new mock return with medication selected
|
|
237
|
+
const formularyReturnWithMedication: ReturnType<typeof useDoseSpotClinicFormulary> = {
|
|
238
|
+
...mockFormularyReturn,
|
|
239
|
+
state: {
|
|
240
|
+
selectedMedication: mockMedication,
|
|
241
|
+
directions: 'Take 1 tablet daily',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(formularyReturnWithMedication);
|
|
245
|
+
|
|
246
|
+
await setup();
|
|
247
|
+
|
|
248
|
+
// Wait for initial load to complete
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
254
|
+
|
|
255
|
+
await act(async () => {
|
|
256
|
+
fireEvent.click(addButton);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(screen.getByText('Medication')).toBeInTheDocument();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Verify the directions input exists and can be interacted with
|
|
264
|
+
// Since we're mocking, we verify the component structure supports direction updates
|
|
265
|
+
const directionsInputs = screen.queryAllByLabelText('Directions');
|
|
266
|
+
if (directionsInputs.length > 0) {
|
|
267
|
+
const directionsInput = directionsInputs[0];
|
|
268
|
+
await act(async () => {
|
|
269
|
+
fireEvent.change(directionsInput, { target: { value: 'Take 2 tablets daily' } });
|
|
270
|
+
});
|
|
271
|
+
expect(mockFormularyReturn.setSelectedMedicationDirections).toHaveBeenCalled();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('Add Favorite button is disabled when medication or directions are missing', async () => {
|
|
276
|
+
await setup();
|
|
277
|
+
|
|
278
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
279
|
+
|
|
280
|
+
await act(async () => {
|
|
281
|
+
fireEvent.click(addButton);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(screen.getByRole('button', { name: 'Add Favorite' })).toBeDisabled();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('Add Favorite button is enabled when medication and directions are provided', async () => {
|
|
290
|
+
const mockMedication: CodeableConcept = {
|
|
291
|
+
text: 'Aspirin 325mg',
|
|
292
|
+
coding: [
|
|
293
|
+
{
|
|
294
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
295
|
+
code: '1191',
|
|
296
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
mockFormularyReturn.state.selectedMedication = mockMedication;
|
|
302
|
+
mockFormularyReturn.state.directions = 'Take 1 tablet daily';
|
|
303
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
|
|
304
|
+
|
|
305
|
+
await setup();
|
|
306
|
+
|
|
307
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
308
|
+
|
|
309
|
+
await act(async () => {
|
|
310
|
+
fireEvent.click(addButton);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await waitFor(() => {
|
|
314
|
+
const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
|
|
315
|
+
expect(addFavoriteButton).not.toBeDisabled();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('Successfully adds favorite medication', async () => {
|
|
320
|
+
const mockMedication: CodeableConcept = {
|
|
321
|
+
text: 'Aspirin 325mg',
|
|
322
|
+
coding: [
|
|
323
|
+
{
|
|
324
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
325
|
+
code: '1191',
|
|
326
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const mockCreatedMedication: MedicationKnowledge = {
|
|
332
|
+
resourceType: 'MedicationKnowledge',
|
|
333
|
+
id: 'med-new',
|
|
334
|
+
code: mockMedication,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
mockFormularyReturn.state.selectedMedication = mockMedication;
|
|
338
|
+
mockFormularyReturn.state.directions = 'Take 1 tablet daily';
|
|
339
|
+
(mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockResolvedValue(mockCreatedMedication);
|
|
340
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
|
|
341
|
+
|
|
342
|
+
await setup();
|
|
343
|
+
|
|
344
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
345
|
+
|
|
346
|
+
await act(async () => {
|
|
347
|
+
fireEvent.click(addButton);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await waitFor(() => {
|
|
351
|
+
expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
|
|
355
|
+
|
|
356
|
+
await act(async () => {
|
|
357
|
+
fireEvent.click(addFavoriteButton);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
expect(mockFormularyReturn.saveFavoriteMedication).toHaveBeenCalled();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await waitFor(() => {
|
|
365
|
+
expect(showNotification).toHaveBeenCalledWith({
|
|
366
|
+
title: 'Medication added to favorites',
|
|
367
|
+
message: 'The medication has been added to your favorites',
|
|
368
|
+
color: 'green',
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(mockFormularyReturn.clear).toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('Shows error notification when adding favorite fails', async () => {
|
|
376
|
+
const mockMedication: CodeableConcept = {
|
|
377
|
+
text: 'Aspirin 325mg',
|
|
378
|
+
coding: [
|
|
379
|
+
{
|
|
380
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
381
|
+
code: '1191',
|
|
382
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const error = new Error('Failed to save medication');
|
|
388
|
+
mockFormularyReturn.state.selectedMedication = mockMedication;
|
|
389
|
+
mockFormularyReturn.state.directions = 'Take 1 tablet daily';
|
|
390
|
+
(mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockRejectedValue(error);
|
|
391
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
|
|
392
|
+
|
|
393
|
+
await setup();
|
|
394
|
+
|
|
395
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
396
|
+
|
|
397
|
+
await act(async () => {
|
|
398
|
+
fireEvent.click(addButton);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
|
|
406
|
+
|
|
407
|
+
await act(async () => {
|
|
408
|
+
fireEvent.click(addFavoriteButton);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
await waitFor(() => {
|
|
412
|
+
expect(notifications.showErrorNotification).toHaveBeenCalledWith(
|
|
413
|
+
expect.objectContaining({
|
|
414
|
+
title: 'Error adding medication to favorites',
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(mockFormularyReturn.clear).toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('Shows loading state while adding favorite', async () => {
|
|
423
|
+
const mockMedication: CodeableConcept = {
|
|
424
|
+
text: 'Aspirin 325mg',
|
|
425
|
+
coding: [
|
|
426
|
+
{
|
|
427
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
428
|
+
code: '1191',
|
|
429
|
+
display: 'Aspirin 325 MG Oral Tablet',
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const mockCreatedMedication: MedicationKnowledge = {
|
|
435
|
+
resourceType: 'MedicationKnowledge',
|
|
436
|
+
id: 'med-new',
|
|
437
|
+
code: mockMedication,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
let resolveSave: ((value: MedicationKnowledge) => void) | undefined;
|
|
441
|
+
const savePromise = new Promise<MedicationKnowledge>((resolve) => {
|
|
442
|
+
resolveSave = () => resolve(mockCreatedMedication);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
mockFormularyReturn.state.selectedMedication = mockMedication;
|
|
446
|
+
mockFormularyReturn.state.directions = 'Take 1 tablet daily';
|
|
447
|
+
(mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockReturnValue(savePromise);
|
|
448
|
+
vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn);
|
|
449
|
+
|
|
450
|
+
await setup();
|
|
451
|
+
|
|
452
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
453
|
+
|
|
454
|
+
await act(async () => {
|
|
455
|
+
fireEvent.click(addButton);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
await waitFor(() => {
|
|
459
|
+
expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' });
|
|
463
|
+
|
|
464
|
+
await act(async () => {
|
|
465
|
+
fireEvent.click(addFavoriteButton);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Button should be disabled while loading
|
|
469
|
+
await waitFor(() => {
|
|
470
|
+
expect(addFavoriteButton).toBeDisabled();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Resolve the promise
|
|
474
|
+
await act(async () => {
|
|
475
|
+
resolveSave?.(mockCreatedMedication);
|
|
476
|
+
});
|
|
477
|
+
await savePromise;
|
|
478
|
+
|
|
479
|
+
await waitFor(() => {
|
|
480
|
+
expect(addFavoriteButton).not.toBeDisabled();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('Clears form when modal is closed', async () => {
|
|
485
|
+
await setup();
|
|
486
|
+
|
|
487
|
+
// Wait for initial load to complete
|
|
488
|
+
await waitFor(() => {
|
|
489
|
+
expect(screen.getByText('No favorite medications found')).toBeInTheDocument();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' });
|
|
493
|
+
|
|
494
|
+
await act(async () => {
|
|
495
|
+
fireEvent.click(addButton);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
await waitFor(() => {
|
|
499
|
+
expect(screen.getByText('Medication')).toBeInTheDocument();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Find close button - it might be an X icon button or a button with aria-label
|
|
503
|
+
const closeButtons = screen.getAllByRole('button');
|
|
504
|
+
const closeButton = closeButtons.find(
|
|
505
|
+
(btn) =>
|
|
506
|
+
btn.getAttribute('aria-label')?.toLowerCase().includes('close') || btn.className.includes('mantine-Modal-close')
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
expect(closeButton).toBeDefined();
|
|
510
|
+
|
|
511
|
+
if (closeButton) {
|
|
512
|
+
await act(async () => {
|
|
513
|
+
fireEvent.click(closeButton);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(mockFormularyReturn.clear).toHaveBeenCalled();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.tsx
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Container,
|
|
6
|
+
Title,
|
|
7
|
+
Paper,
|
|
8
|
+
Button,
|
|
9
|
+
Modal,
|
|
10
|
+
Group,
|
|
11
|
+
Box,
|
|
12
|
+
TextInput,
|
|
13
|
+
Stack,
|
|
14
|
+
Divider,
|
|
15
|
+
Group as MantineGroup,
|
|
16
|
+
} from '@mantine/core';
|
|
17
|
+
import { showNotification } from '@mantine/notifications';
|
|
18
|
+
import { DOSESPOT_CLINIC_FAVORITE_ID_SYSTEM, useDoseSpotClinicFormulary } from '@medplum/dosespot-react';
|
|
19
|
+
import type { CodeableConcept, MedicationKnowledge } from '@medplum/fhirtypes';
|
|
20
|
+
import { IconPlus } from '@tabler/icons-react';
|
|
21
|
+
import { formatSearchQuery, isCodeableConcept, normalizeErrorString } from '@medplum/core';
|
|
22
|
+
import { AsyncAutocomplete, useMedplum } from '@medplum/react';
|
|
23
|
+
import { FavoriteMedicationsTable } from './FavoriteMedicationsTable';
|
|
24
|
+
import { showErrorNotification } from '../../utils/notifications';
|
|
25
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This is a demo component for how you could display your favorite Medications
|
|
29
|
+
* from DoseSpot.
|
|
30
|
+
*
|
|
31
|
+
* @returns A React component that displays the favorite medications and allows you to add new favorites.
|
|
32
|
+
*/
|
|
33
|
+
export function DoseSpotFavoritesPage(): React.JSX.Element {
|
|
34
|
+
const [modalOpened, setModalOpened] = useState(false);
|
|
35
|
+
const [loading, setLoading] = useState(false);
|
|
36
|
+
const [loadingFavorites, setLoadingFavorites] = useState(true);
|
|
37
|
+
const [clinicFavoriteMedications, setClinicFavoriteMedications] = useState<MedicationKnowledge[] | undefined>();
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
state,
|
|
41
|
+
saveFavoriteMedication,
|
|
42
|
+
searchMedications,
|
|
43
|
+
setSelectedMedicationDirections,
|
|
44
|
+
setSelectedMedication,
|
|
45
|
+
clear,
|
|
46
|
+
} = useDoseSpotClinicFormulary();
|
|
47
|
+
const medplum = useMedplum();
|
|
48
|
+
|
|
49
|
+
// Load favorite MedicationKnowledge resources that have a DoseSpot favorite id system
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const loadFavorites = async (): Promise<void> => {
|
|
52
|
+
try {
|
|
53
|
+
setLoadingFavorites(true);
|
|
54
|
+
const searchRequest = {
|
|
55
|
+
resourceType: 'MedicationKnowledge' as const,
|
|
56
|
+
filters: [
|
|
57
|
+
{
|
|
58
|
+
code: 'code',
|
|
59
|
+
operator: 'eq' as const,
|
|
60
|
+
value: `${DOSESPOT_CLINIC_FAVORITE_ID_SYSTEM}|`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
const queryString = formatSearchQuery(searchRequest);
|
|
65
|
+
const result = await medplum.search('MedicationKnowledge', queryString);
|
|
66
|
+
setClinicFavoriteMedications(result.entry?.map((e) => e.resource as MedicationKnowledge) || []);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
showErrorNotification(err);
|
|
69
|
+
} finally {
|
|
70
|
+
setLoadingFavorites(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
loadFavorites().catch(showErrorNotification);
|
|
74
|
+
}, [medplum]);
|
|
75
|
+
|
|
76
|
+
const handleAddFavoriteMedication = async (): Promise<void> => {
|
|
77
|
+
try {
|
|
78
|
+
setLoading(true);
|
|
79
|
+
const created = await saveFavoriteMedication();
|
|
80
|
+
// Optimistically update local favorites list
|
|
81
|
+
setClinicFavoriteMedications((prev) => [created, ...(prev || [])]);
|
|
82
|
+
setModalOpened(false);
|
|
83
|
+
showNotification({
|
|
84
|
+
title: 'Medication added to favorites',
|
|
85
|
+
message: 'The medication has been added to your favorites',
|
|
86
|
+
color: 'green',
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
showErrorNotification({
|
|
90
|
+
title: 'Error adding medication to favorites',
|
|
91
|
+
message: normalizeErrorString(error as Error),
|
|
92
|
+
color: 'red',
|
|
93
|
+
});
|
|
94
|
+
} finally {
|
|
95
|
+
clear();
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const toOption = (medication: CodeableConcept): { value: string; label: string; resource: CodeableConcept } => ({
|
|
101
|
+
value: uuidv4(),
|
|
102
|
+
label: medication.text || 'Unknown Medication',
|
|
103
|
+
resource: medication,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Container size="xl" py="xl">
|
|
108
|
+
<Paper mb="lg" p="md" withBorder shadow="sm">
|
|
109
|
+
<Group justify="space-between" mb="md">
|
|
110
|
+
<Title order={2}>DoseSpot Medication Favorites</Title>
|
|
111
|
+
<Button leftSection={<IconPlus size={16} />} onClick={() => setModalOpened(true)}>
|
|
112
|
+
Add Favorite Medication
|
|
113
|
+
</Button>
|
|
114
|
+
</Group>
|
|
115
|
+
|
|
116
|
+
{/* Example of a table to display favorite medications (MedicationKnowledge resources) */}
|
|
117
|
+
<FavoriteMedicationsTable
|
|
118
|
+
clinicFavoriteMedications={clinicFavoriteMedications}
|
|
119
|
+
loadingFavorites={loadingFavorites}
|
|
120
|
+
/>
|
|
121
|
+
</Paper>
|
|
122
|
+
|
|
123
|
+
<Modal
|
|
124
|
+
opened={modalOpened}
|
|
125
|
+
onClose={() => {
|
|
126
|
+
setModalOpened(false);
|
|
127
|
+
clear();
|
|
128
|
+
}}
|
|
129
|
+
title="Medication"
|
|
130
|
+
size="lg"
|
|
131
|
+
withCloseButton
|
|
132
|
+
>
|
|
133
|
+
<Box>
|
|
134
|
+
<AsyncAutocomplete<CodeableConcept>
|
|
135
|
+
placeholder="Search medications..."
|
|
136
|
+
loadOptions={searchMedications}
|
|
137
|
+
toOption={toOption}
|
|
138
|
+
onChange={(medications) => {
|
|
139
|
+
if (medications.length > 0) {
|
|
140
|
+
setSelectedMedication(medications[0]);
|
|
141
|
+
} else {
|
|
142
|
+
setSelectedMedication(undefined);
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
minInputLength={3} //DoseSpot requires at least 3 characters to search
|
|
146
|
+
clearable
|
|
147
|
+
maxValues={1} // Only allow single selection
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
{/* After selecting a medication, show the medication info with followup input items */}
|
|
151
|
+
{state.selectedMedication && isCodeableConcept(state.selectedMedication) && (
|
|
152
|
+
<Stack gap="md" mt="lg">
|
|
153
|
+
<Divider />
|
|
154
|
+
{/* Prescription Form */}
|
|
155
|
+
<TextInput
|
|
156
|
+
label="Directions"
|
|
157
|
+
placeholder="e.g., Take 1 tablet by mouth daily"
|
|
158
|
+
value={state.directions}
|
|
159
|
+
onChange={(e) => setSelectedMedicationDirections(e.target.value)}
|
|
160
|
+
required
|
|
161
|
+
/>
|
|
162
|
+
</Stack>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* Action Buttons */}
|
|
166
|
+
<MantineGroup justify="flex-end" gap="md" mt="md">
|
|
167
|
+
<Button
|
|
168
|
+
onClick={() => handleAddFavoriteMedication()}
|
|
169
|
+
disabled={!state.directions || !state.selectedMedication || loading}
|
|
170
|
+
loading={loading}
|
|
171
|
+
>
|
|
172
|
+
Add Favorite
|
|
173
|
+
</Button>
|
|
174
|
+
</MantineGroup>
|
|
175
|
+
</Box>
|
|
176
|
+
</Modal>
|
|
177
|
+
</Container>
|
|
178
|
+
);
|
|
179
|
+
}
|