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
package/dist/examples/vintasend-medplum-example/bots/handlers/send-pending-notifications-bot.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BotEvent, MedplumClient } from '@medplum/core';
|
|
2
|
+
import { MedplumSingleton } from '../../lib/medplum-singleton';
|
|
3
|
+
import { buildSendGridConfig, getNotificationService } from '../../lib/notification-service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Medplum Bot: Send Pending Notifications
|
|
7
|
+
*
|
|
8
|
+
* This bot runs periodically (every 5 minutes) to process and send
|
|
9
|
+
* all pending scheduled notifications that are due to be sent.
|
|
10
|
+
*
|
|
11
|
+
* It uses VintaSend's notification service to check for notifications
|
|
12
|
+
* where sendAfter <= current time and triggers their delivery.
|
|
13
|
+
*
|
|
14
|
+
* Cron: every 5 minutes
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export async function handler(medplum: MedplumClient, event: BotEvent): Promise<any> {
|
|
18
|
+
console.log('[SendPendingNotificationsBot] Starting to process pending notifications');
|
|
19
|
+
const sendgridConfig = buildSendGridConfig(event);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
MedplumSingleton.setInstance(medplum);
|
|
23
|
+
const vintasend = getNotificationService(medplum, sendgridConfig);
|
|
24
|
+
|
|
25
|
+
// Send all pending notifications that are ready to be sent
|
|
26
|
+
const result = await vintasend.sendPendingNotifications();
|
|
27
|
+
|
|
28
|
+
console.log('[SendPendingNotificationsBot] Completed processing pending notifications');
|
|
29
|
+
console.log('[SendPendingNotificationsBot] Result:', JSON.stringify(result, null, 2));
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
message: 'Pending notifications processed',
|
|
33
|
+
result,
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[SendPendingNotificationsBot] Error processing pending notifications:', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BotEvent, MedplumClient } from '@medplum/core';
|
|
2
|
+
import { Task } from '@medplum/fhirtypes';
|
|
3
|
+
import { sendTaskAssignmentEmail } from '../services/emails/send-task-assignment-email';
|
|
4
|
+
import { buildSendGridConfig } from '../../lib/notification-service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Medplum Bot: Task Assignment Email Notification
|
|
8
|
+
*
|
|
9
|
+
* This bot is triggered by a subscription when a Task is created or updated
|
|
10
|
+
* with an owner. It sends an email notification to the assigned practitioner.
|
|
11
|
+
*
|
|
12
|
+
* Subscription Criteria: Task?owner:exists=true
|
|
13
|
+
* Triggers: create, update
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export async function handler(medplum: MedplumClient, event: BotEvent): Promise<Task> {
|
|
17
|
+
const task = event.input as Task;
|
|
18
|
+
const sendGridVariables = buildSendGridConfig(event);
|
|
19
|
+
|
|
20
|
+
console.log(`[TaskAssignmentBot] Processing task: ${task.id}`);
|
|
21
|
+
console.log(`[TaskAssignmentBot] Owner: ${task.owner?.reference}`);
|
|
22
|
+
|
|
23
|
+
// Only send email if task has an owner
|
|
24
|
+
if (task.owner?.reference) {
|
|
25
|
+
const appBaseUrl = process.env.APP_BASE_URL || 'https://your-app-url.com';
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await sendTaskAssignmentEmail(medplum, task, appBaseUrl, sendGridVariables);
|
|
29
|
+
console.log(`[TaskAssignmentBot] Email notification sent successfully for task: ${task.id}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`[TaskAssignmentBot] Failed to send email for task: ${task.id}`, error);
|
|
32
|
+
// Don't throw - we don't want the subscription to fail
|
|
33
|
+
// The notification will be logged in Medplum as failed
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
console.log(`[TaskAssignmentBot] Task ${task.id} has no owner, skipping email notification`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Return the task unchanged
|
|
40
|
+
return task;
|
|
41
|
+
}
|
package/dist/examples/vintasend-medplum-example/bots/handlers/task-due-soon-notification-bot.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BotEvent, MedplumClient } from '@medplum/core';
|
|
2
|
+
import { Task } from '@medplum/fhirtypes';
|
|
3
|
+
import { scheduleTaskDueSoonEmail } from '../services/emails/schedule-task-due-soon-email';
|
|
4
|
+
import { buildSendGridConfig } from '../../lib/notification-service';
|
|
5
|
+
import { getTaskDueSoonSchedulingReason } from '../shared/task-due-soon-helpers';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Medplum Bot: Task Due Soon Notification
|
|
9
|
+
*
|
|
10
|
+
* This bot triggers on Task creation/update and schedules email notifications
|
|
11
|
+
* to be sent 24 hours before the task due date.
|
|
12
|
+
*
|
|
13
|
+
* The bot uses VintaSend's scheduled messages (sendAfter) to ensure
|
|
14
|
+
* notifications are sent at the appropriate time. The actual sending is
|
|
15
|
+
* handled by the send-pending-notifications-bot.
|
|
16
|
+
*
|
|
17
|
+
* Subscription: Task (create/update)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export async function handler(medplum: MedplumClient, event: BotEvent): Promise<any> {
|
|
21
|
+
const task = event.input as Task;
|
|
22
|
+
const result = getTaskDueSoonSchedulingReason(task);
|
|
23
|
+
|
|
24
|
+
switch (result.kind) {
|
|
25
|
+
case 'invalidResource':
|
|
26
|
+
console.warn('[TaskDueSoonNotificationBot] Invalid task resource received');
|
|
27
|
+
return { message: 'Invalid task resource' };
|
|
28
|
+
case 'noDueDate':
|
|
29
|
+
console.log(`[TaskDueSoonNotificationBot] Task ${task?.id} has no due date, skipping`);
|
|
30
|
+
return { message: 'No due date set', taskId: task?.id };
|
|
31
|
+
case 'invalidDueDate':
|
|
32
|
+
console.warn(
|
|
33
|
+
`[TaskDueSoonNotificationBot] Task ${task?.id} has invalid due date: ${result.dueDate}, skipping`
|
|
34
|
+
);
|
|
35
|
+
return { message: 'Invalid due date', taskId: task?.id, dueDate: result.dueDate };
|
|
36
|
+
case 'finalState':
|
|
37
|
+
console.log(
|
|
38
|
+
`[TaskDueSoonNotificationBot] Task ${task?.id} is in final state (${result.status}), skipping`
|
|
39
|
+
);
|
|
40
|
+
return { message: `Task in final state: ${result.status}`, taskId: task?.id };
|
|
41
|
+
case 'noOwner':
|
|
42
|
+
console.log(`[TaskDueSoonNotificationBot] Task ${task?.id} has no owner, skipping`);
|
|
43
|
+
return { message: 'No owner assigned', taskId: task?.id };
|
|
44
|
+
case 'tooSoon':
|
|
45
|
+
console.log(
|
|
46
|
+
`[TaskDueSoonNotificationBot] Task ${task?.id} is due in ${result.hoursUntilDue.toFixed(
|
|
47
|
+
2
|
|
48
|
+
)} hours (less than 24), skipping`
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
message: 'Due date is less than 24 hours away',
|
|
52
|
+
taskId: task?.id,
|
|
53
|
+
hoursUntilDue: result.hoursUntilDue,
|
|
54
|
+
};
|
|
55
|
+
case 'ok':
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const appBaseUrl = process.env.APP_BASE_URL;
|
|
60
|
+
if (!appBaseUrl) {
|
|
61
|
+
console.error('[TaskDueSoonNotificationBot] APP_BASE_URL environment variable is not set');
|
|
62
|
+
throw new Error('APP_BASE_URL must be configured');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sendgridConfig = buildSendGridConfig(event);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
console.log(
|
|
69
|
+
`[TaskDueSoonNotificationBot] Scheduling notification for task ${task.id}, due in ${result.hoursUntilDue.toFixed(
|
|
70
|
+
2
|
|
71
|
+
)} hours`
|
|
72
|
+
);
|
|
73
|
+
await scheduleTaskDueSoonEmail(medplum, task, appBaseUrl, sendgridConfig);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
message: 'Notification scheduled successfully',
|
|
77
|
+
taskId: task.id,
|
|
78
|
+
dueDate: task.restriction?.period?.end,
|
|
79
|
+
hoursUntilDue: result.hoursUntilDue.toFixed(2),
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`[TaskDueSoonNotificationBot] Error scheduling notification for task ${task.id}:`, error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Extension } from '@medplum/fhirtypes';
|
|
2
|
+
|
|
3
|
+
export interface BotDescription {
|
|
4
|
+
name: string;
|
|
5
|
+
criteria?: string;
|
|
6
|
+
extension?: Extension[];
|
|
7
|
+
needsAdminMembership?: boolean;
|
|
8
|
+
runAsUser?: boolean;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
cronString?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const BOTS: BotDescription[] = [
|
|
14
|
+
{
|
|
15
|
+
name: 'send-task-assignment-email',
|
|
16
|
+
needsAdminMembership: true,
|
|
17
|
+
runAsUser: true,
|
|
18
|
+
criteria: 'Task?owner:missing=false',
|
|
19
|
+
extension: [
|
|
20
|
+
{
|
|
21
|
+
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
|
|
22
|
+
valueCode: 'create',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
|
|
26
|
+
valueCode: 'update',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'task-due-soon-notification-bot',
|
|
32
|
+
needsAdminMembership: true,
|
|
33
|
+
runAsUser: true,
|
|
34
|
+
criteria: 'Task?owner:missing=false',
|
|
35
|
+
extension: [
|
|
36
|
+
{
|
|
37
|
+
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
|
|
38
|
+
valueCode: 'create',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
|
|
42
|
+
valueCode: 'update',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'send-pending-notifications-bot',
|
|
48
|
+
needsAdminMembership: true,
|
|
49
|
+
runAsUser: true,
|
|
50
|
+
cronString: '*/5 * * * *', // Run every 5 minutes
|
|
51
|
+
timeout: 300, // 5 minutes timeout
|
|
52
|
+
},
|
|
53
|
+
];
|
package/dist/examples/vintasend-medplum-example/bots/services/emails/schedule-task-due-soon-email.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { MedplumClient } from '@medplum/core';
|
|
2
|
+
import { Task } from '@medplum/fhirtypes';
|
|
3
|
+
import { MedplumSingleton } from '../../../lib/medplum-singleton';
|
|
4
|
+
import { getNotificationService, SendGridConfig } from '../../../lib/notification-service';
|
|
5
|
+
import {
|
|
6
|
+
assertTaskOwnerReference,
|
|
7
|
+
getValidTaskDueDate,
|
|
8
|
+
parseOwnerReference,
|
|
9
|
+
computeReminderTime,
|
|
10
|
+
} from '../../shared/task-due-soon-helpers';
|
|
11
|
+
|
|
12
|
+
export async function scheduleTaskDueSoonEmail(
|
|
13
|
+
medplum: MedplumClient,
|
|
14
|
+
task: Task,
|
|
15
|
+
taskLinkBaseUrl: string,
|
|
16
|
+
sendgridConfig: SendGridConfig
|
|
17
|
+
) {
|
|
18
|
+
/* sends a task due soon reminder email to a practitioner 24 hours before the task is due */
|
|
19
|
+
|
|
20
|
+
const ownerRef = assertTaskOwnerReference(task);
|
|
21
|
+
const parsedOwner = parseOwnerReference(ownerRef);
|
|
22
|
+
if (!parsedOwner) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const dueDate = getValidTaskDueDate(task);
|
|
27
|
+
|
|
28
|
+
if (!task.id) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.error('[scheduleTaskDueSoonEmail] Task has no id');
|
|
31
|
+
throw new Error('Task must have an id to send task due soon email');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sendAfter = computeReminderTime(dueDate, 24);
|
|
35
|
+
if (!sendAfter) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
MedplumSingleton.setInstance(medplum);
|
|
40
|
+
const vintasend = getNotificationService(medplum, sendgridConfig);
|
|
41
|
+
|
|
42
|
+
const taskTitle = task.code?.text || task.description || 'Task';
|
|
43
|
+
const taskLink = `${taskLinkBaseUrl}/Task/${task.id}`;
|
|
44
|
+
const taskIsUrgent = task.priority === 'urgent';
|
|
45
|
+
const formattedDueDate = dueDate.toLocaleString('en-US', {
|
|
46
|
+
weekday: 'long',
|
|
47
|
+
year: 'numeric',
|
|
48
|
+
month: 'long',
|
|
49
|
+
day: 'numeric',
|
|
50
|
+
hour: '2-digit',
|
|
51
|
+
minute: '2-digit',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await vintasend.createNotification({
|
|
56
|
+
userId: ownerRef,
|
|
57
|
+
notificationType: 'EMAIL' as const,
|
|
58
|
+
title: 'Task Due Soon Reminder',
|
|
59
|
+
contextName: 'taskDueSoon' as const,
|
|
60
|
+
contextParameters: {
|
|
61
|
+
userId: ownerRef,
|
|
62
|
+
taskTitle,
|
|
63
|
+
taskDescription: task.description || '',
|
|
64
|
+
taskIsUrgent,
|
|
65
|
+
taskLink,
|
|
66
|
+
dueDate: formattedDueDate,
|
|
67
|
+
},
|
|
68
|
+
sendAfter,
|
|
69
|
+
bodyTemplate: 'emails/task-due-soon/body.html.pug',
|
|
70
|
+
subjectTemplate: 'emails/task-due-soon/subject.txt.pug',
|
|
71
|
+
extraParams: {},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.log(
|
|
76
|
+
`[scheduleTaskDueSoonEmail] Email scheduled for ${sendAfter.toISOString()} to: ${ownerRef} for task due on ${dueDate.toISOString()}`
|
|
77
|
+
);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
80
|
+
console.error('[scheduleTaskDueSoonEmail] Error creating/sending notification:', error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// @ts-nocheck - MockClient type compatibility with MedplumClient
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
|
+
import { MockClient } from '@medplum/mock';
|
|
4
|
+
import type { Task, Media, Binary, Practitioner } from '@medplum/fhirtypes';
|
|
5
|
+
import { sendTaskAssignmentEmail } from './send-task-assignment-email';
|
|
6
|
+
import type { SendGridConfig } from '../../../lib/notification-service';
|
|
7
|
+
|
|
8
|
+
// Mock the notification service
|
|
9
|
+
vi.mock('../../../lib/notification-service', async () => {
|
|
10
|
+
const actual = await vi.importActual('../../../lib/notification-service');
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
getNotificationService: vi.fn(() => ({
|
|
14
|
+
createNotification: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
})),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Mock the MedplumSingleton
|
|
20
|
+
vi.mock('../../../lib/medplum-singleton', () => ({
|
|
21
|
+
MedplumSingleton: {
|
|
22
|
+
setInstance: vi.fn(),
|
|
23
|
+
getInstance: vi.fn(),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('sendTaskAssignmentEmail with Attachments', () => {
|
|
28
|
+
let medplum: MockClient;
|
|
29
|
+
let sendgridConfig: SendGridConfig;
|
|
30
|
+
const taskLinkBaseUrl = 'https://example.com';
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
medplum = new MockClient();
|
|
34
|
+
sendgridConfig = {
|
|
35
|
+
SENDGRID_API_KEY: 'test-api-key',
|
|
36
|
+
SENDGRID_FROM_EMAIL: 'noreply@example.com',
|
|
37
|
+
SENDGRID_FROM_NAME: 'Test App',
|
|
38
|
+
};
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should send email with single attachment', async () => {
|
|
43
|
+
// Create a practitioner
|
|
44
|
+
const practitioner = await medplum.createResource<Practitioner>({
|
|
45
|
+
resourceType: 'Practitioner',
|
|
46
|
+
name: [{ given: ['John'], family: 'Doe' }],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create a binary resource
|
|
50
|
+
const binary = await medplum.createResource<Binary>({
|
|
51
|
+
resourceType: 'Binary',
|
|
52
|
+
contentType: 'application/pdf',
|
|
53
|
+
data: Buffer.from('test pdf content').toString('base64'),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Create a media resource
|
|
57
|
+
const media = await medplum.createResource<Media>({
|
|
58
|
+
resourceType: 'Media',
|
|
59
|
+
status: 'completed',
|
|
60
|
+
content: {
|
|
61
|
+
contentType: 'application/pdf',
|
|
62
|
+
url: `Binary/${binary.id}`,
|
|
63
|
+
title: 'test-document.pdf',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Create a task with attachment
|
|
68
|
+
const task = await medplum.createResource<Task>({
|
|
69
|
+
resourceType: 'Task',
|
|
70
|
+
status: 'requested',
|
|
71
|
+
intent: 'order',
|
|
72
|
+
description: 'Test task with attachment',
|
|
73
|
+
code: { text: 'Review Document' },
|
|
74
|
+
owner: { reference: `Practitioner/${practitioner.id}` },
|
|
75
|
+
priority: 'routine',
|
|
76
|
+
input: [
|
|
77
|
+
{
|
|
78
|
+
type: {
|
|
79
|
+
coding: [
|
|
80
|
+
{
|
|
81
|
+
system: 'http://your-app-url.com/task-input-types',
|
|
82
|
+
code: 'attachment',
|
|
83
|
+
display: 'File Attachment',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
valueReference: { reference: `Media/${media.id}` },
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const { getNotificationService } = await import('../../../lib/notification-service');
|
|
93
|
+
const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
|
|
94
|
+
(getNotificationService as any).mockReturnValue({
|
|
95
|
+
createNotification: mockCreateNotification,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
|
|
99
|
+
|
|
100
|
+
expect(mockCreateNotification).toHaveBeenCalledTimes(1);
|
|
101
|
+
const callArgs = mockCreateNotification.mock.calls[0][0];
|
|
102
|
+
|
|
103
|
+
expect(callArgs.contextParameters.attachmentCount).toBe(1);
|
|
104
|
+
expect(callArgs.attachments).toBeDefined();
|
|
105
|
+
expect(callArgs.attachments.length).toBe(1);
|
|
106
|
+
expect(callArgs.attachments[0].fileId).toBe(media.id);
|
|
107
|
+
expect(callArgs.attachments[0].description).toBe('test-document.pdf');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should send email with multiple attachments', async () => {
|
|
111
|
+
const practitioner = await medplum.createResource<Practitioner>({
|
|
112
|
+
resourceType: 'Practitioner',
|
|
113
|
+
name: [{ given: ['Jane'], family: 'Smith' }],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Create multiple binary resources
|
|
117
|
+
const binary1 = await medplum.createResource<Binary>({
|
|
118
|
+
resourceType: 'Binary',
|
|
119
|
+
contentType: 'application/pdf',
|
|
120
|
+
data: Buffer.from('pdf content').toString('base64'),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const binary2 = await medplum.createResource<Binary>({
|
|
124
|
+
resourceType: 'Binary',
|
|
125
|
+
contentType: 'image/jpeg',
|
|
126
|
+
data: Buffer.from('jpeg content').toString('base64'),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Create multiple media resources
|
|
130
|
+
const media1 = await medplum.createResource<Media>({
|
|
131
|
+
resourceType: 'Media',
|
|
132
|
+
status: 'completed',
|
|
133
|
+
content: {
|
|
134
|
+
contentType: 'application/pdf',
|
|
135
|
+
url: `Binary/${binary1.id}`,
|
|
136
|
+
title: 'document.pdf',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const media2 = await medplum.createResource<Media>({
|
|
141
|
+
resourceType: 'Media',
|
|
142
|
+
status: 'completed',
|
|
143
|
+
content: {
|
|
144
|
+
contentType: 'image/jpeg',
|
|
145
|
+
url: `Binary/${binary2.id}`,
|
|
146
|
+
title: 'image.jpg',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Create task with multiple attachments
|
|
151
|
+
const task = await medplum.createResource<Task>({
|
|
152
|
+
resourceType: 'Task',
|
|
153
|
+
status: 'requested',
|
|
154
|
+
intent: 'order',
|
|
155
|
+
description: 'Task with multiple attachments',
|
|
156
|
+
code: { text: 'Review Files' },
|
|
157
|
+
owner: { reference: `Practitioner/${practitioner.id}` },
|
|
158
|
+
input: [
|
|
159
|
+
{
|
|
160
|
+
type: {
|
|
161
|
+
coding: [
|
|
162
|
+
{
|
|
163
|
+
system: 'http://your-app-url.com/task-input-types',
|
|
164
|
+
code: 'attachment',
|
|
165
|
+
display: 'File Attachment',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
valueReference: { reference: `Media/${media1.id}` },
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: {
|
|
173
|
+
coding: [
|
|
174
|
+
{
|
|
175
|
+
system: 'http://your-app-url.com/task-input-types',
|
|
176
|
+
code: 'attachment',
|
|
177
|
+
display: 'File Attachment',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
valueReference: { reference: `Media/${media2.id}` },
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const { getNotificationService } = await import('../../../lib/notification-service');
|
|
187
|
+
const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
|
|
188
|
+
(getNotificationService as any).mockReturnValue({
|
|
189
|
+
createNotification: mockCreateNotification,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
|
|
193
|
+
|
|
194
|
+
expect(mockCreateNotification).toHaveBeenCalledTimes(1);
|
|
195
|
+
const callArgs = mockCreateNotification.mock.calls[0][0];
|
|
196
|
+
|
|
197
|
+
expect(callArgs.contextParameters.attachmentCount).toBe(2);
|
|
198
|
+
expect(callArgs.attachments).toBeDefined();
|
|
199
|
+
expect(callArgs.attachments.length).toBe(2);
|
|
200
|
+
expect(callArgs.attachments[0].fileId).toBe(media1.id);
|
|
201
|
+
expect(callArgs.attachments[0].description).toBe('document.pdf');
|
|
202
|
+
expect(callArgs.attachments[1].fileId).toBe(media2.id);
|
|
203
|
+
expect(callArgs.attachments[1].description).toBe('image.jpg');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should send email without attachments for task with no files', async () => {
|
|
207
|
+
const practitioner = await medplum.createResource<Practitioner>({
|
|
208
|
+
resourceType: 'Practitioner',
|
|
209
|
+
name: [{ given: ['Bob'], family: 'Johnson' }],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const task = await medplum.createResource<Task>({
|
|
213
|
+
resourceType: 'Task',
|
|
214
|
+
status: 'requested',
|
|
215
|
+
intent: 'order',
|
|
216
|
+
description: 'Task without attachments',
|
|
217
|
+
code: { text: 'Simple Task' },
|
|
218
|
+
owner: { reference: `Practitioner/${practitioner.id}` },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const { getNotificationService } = await import('../../../lib/notification-service');
|
|
222
|
+
const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
|
|
223
|
+
(getNotificationService as any).mockReturnValue({
|
|
224
|
+
createNotification: mockCreateNotification,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
|
|
228
|
+
|
|
229
|
+
expect(mockCreateNotification).toHaveBeenCalledTimes(1);
|
|
230
|
+
const callArgs = mockCreateNotification.mock.calls[0][0];
|
|
231
|
+
|
|
232
|
+
expect(callArgs.contextParameters.attachmentCount).toBe(0);
|
|
233
|
+
expect(callArgs.attachments).toBeDefined();
|
|
234
|
+
expect(callArgs.attachments.length).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle attachment conversion failures gracefully', async () => {
|
|
238
|
+
const practitioner = await medplum.createResource<Practitioner>({
|
|
239
|
+
resourceType: 'Practitioner',
|
|
240
|
+
name: [{ given: ['Alice'], family: 'Williams' }],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Create media resource with invalid binary reference
|
|
244
|
+
const media = await medplum.createResource<Media>({
|
|
245
|
+
resourceType: 'Media',
|
|
246
|
+
status: 'completed',
|
|
247
|
+
content: {
|
|
248
|
+
contentType: 'application/pdf',
|
|
249
|
+
url: 'Binary/nonexistent-binary-id',
|
|
250
|
+
title: 'invalid.pdf',
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const task = await medplum.createResource<Task>({
|
|
255
|
+
resourceType: 'Task',
|
|
256
|
+
status: 'requested',
|
|
257
|
+
intent: 'order',
|
|
258
|
+
description: 'Task with invalid attachment',
|
|
259
|
+
code: { text: 'Test Task' },
|
|
260
|
+
owner: { reference: `Practitioner/${practitioner.id}` },
|
|
261
|
+
input: [
|
|
262
|
+
{
|
|
263
|
+
type: {
|
|
264
|
+
coding: [
|
|
265
|
+
{
|
|
266
|
+
system: 'http://your-app-url.com/task-input-types',
|
|
267
|
+
code: 'attachment',
|
|
268
|
+
display: 'File Attachment',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
valueReference: { reference: `Media/${media.id}` },
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const { getNotificationService } = await import('../../../lib/notification-service');
|
|
278
|
+
const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
|
|
279
|
+
(getNotificationService as any).mockReturnValue({
|
|
280
|
+
createNotification: mockCreateNotification,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
284
|
+
|
|
285
|
+
await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
|
|
286
|
+
|
|
287
|
+
// Email should still be sent with the attachment reference
|
|
288
|
+
// The actual file retrieval happens later when VintaSend processes the notification
|
|
289
|
+
expect(mockCreateNotification).toHaveBeenCalledTimes(1);
|
|
290
|
+
const callArgs = mockCreateNotification.mock.calls[0][0];
|
|
291
|
+
|
|
292
|
+
expect(callArgs.contextParameters.attachmentCount).toBe(1);
|
|
293
|
+
expect(callArgs.attachments.length).toBe(1);
|
|
294
|
+
expect(callArgs.attachments[0].fileId).toBe(media.id);
|
|
295
|
+
|
|
296
|
+
consoleErrorSpy.mockRestore();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should skip email for tasks assigned to groups', async () => {
|
|
300
|
+
const task = await medplum.createResource<Task>({
|
|
301
|
+
resourceType: 'Task',
|
|
302
|
+
status: 'requested',
|
|
303
|
+
intent: 'order',
|
|
304
|
+
description: 'Task assigned to group',
|
|
305
|
+
code: { text: 'Group Task' },
|
|
306
|
+
owner: { reference: 'Group/test-group-123' },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const { getNotificationService } = await import('../../../lib/notification-service');
|
|
310
|
+
const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
|
|
311
|
+
(getNotificationService as any).mockReturnValue({
|
|
312
|
+
createNotification: mockCreateNotification,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
316
|
+
|
|
317
|
+
await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
|
|
318
|
+
|
|
319
|
+
expect(mockCreateNotification).not.toHaveBeenCalled();
|
|
320
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
321
|
+
'[sendTaskAssignmentEmail] Task assigned to Group, skipping email notification'
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
consoleLogSpy.mockRestore();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should handle urgent tasks with attachments', async () => {
|
|
328
|
+
const practitioner = await medplum.createResource<Practitioner>({
|
|
329
|
+
resourceType: 'Practitioner',
|
|
330
|
+
name: [{ given: ['Charlie'], family: 'Brown' }],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const binary = await medplum.createResource<Binary>({
|
|
334
|
+
resourceType: 'Binary',
|
|
335
|
+
contentType: 'application/pdf',
|
|
336
|
+
data: Buffer.from('urgent document').toString('base64'),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const media = await medplum.createResource<Media>({
|
|
340
|
+
resourceType: 'Media',
|
|
341
|
+
status: 'completed',
|
|
342
|
+
content: {
|
|
343
|
+
contentType: 'application/pdf',
|
|
344
|
+
url: `Binary/${binary.id}`,
|
|
345
|
+
title: 'urgent.pdf',
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const task = await medplum.createResource<Task>({
|
|
350
|
+
resourceType: 'Task',
|
|
351
|
+
status: 'requested',
|
|
352
|
+
intent: 'order',
|
|
353
|
+
description: 'Urgent task with attachment',
|
|
354
|
+
code: { text: 'Urgent Review' },
|
|
355
|
+
owner: { reference: `Practitioner/${practitioner.id}` },
|
|
356
|
+
priority: 'urgent',
|
|
357
|
+
input: [
|
|
358
|
+
{
|
|
359
|
+
type: {
|
|
360
|
+
coding: [
|
|
361
|
+
{
|
|
362
|
+
system: 'http://your-app-url.com/task-input-types',
|
|
363
|
+
code: 'attachment',
|
|
364
|
+
display: 'File Attachment',
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
valueReference: { reference: `Media/${media.id}` },
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const { getNotificationService } = await import('../../../lib/notification-service');
|
|
374
|
+
const mockCreateNotification = vi.fn().mockResolvedValue(undefined);
|
|
375
|
+
(getNotificationService as any).mockReturnValue({
|
|
376
|
+
createNotification: mockCreateNotification,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await sendTaskAssignmentEmail(medplum, task, taskLinkBaseUrl, sendgridConfig);
|
|
380
|
+
|
|
381
|
+
expect(mockCreateNotification).toHaveBeenCalledTimes(1);
|
|
382
|
+
const callArgs = mockCreateNotification.mock.calls[0][0];
|
|
383
|
+
|
|
384
|
+
expect(callArgs.contextParameters.taskIsUrgent).toBe(true);
|
|
385
|
+
expect(callArgs.contextParameters.attachmentCount).toBe(1);
|
|
386
|
+
expect(callArgs.attachments.length).toBe(1);
|
|
387
|
+
});
|
|
388
|
+
});
|