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,293 @@
|
|
|
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 { Binary, Media } from '@medplum/fhirtypes';
|
|
5
|
+
import { convertMediaToAttachment } from './notification-service';
|
|
6
|
+
|
|
7
|
+
describe('convertMediaToAttachment', () => {
|
|
8
|
+
let medplum: MockClient;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
medplum = new MockClient();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should convert Media resource to VintaSend attachment format', async () => {
|
|
15
|
+
// Create a binary resource with base64 data
|
|
16
|
+
const binaryContent = Buffer.from('test file content');
|
|
17
|
+
const binary = await medplum.createResource<Binary>({
|
|
18
|
+
resourceType: 'Binary',
|
|
19
|
+
contentType: 'application/pdf',
|
|
20
|
+
data: binaryContent.toString('base64'),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Create a media resource
|
|
24
|
+
const media = await medplum.createResource<Media>({
|
|
25
|
+
resourceType: 'Media',
|
|
26
|
+
status: 'completed',
|
|
27
|
+
content: {
|
|
28
|
+
contentType: 'application/pdf',
|
|
29
|
+
url: `Binary/${binary.id}`,
|
|
30
|
+
title: 'test-document.pdf',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
35
|
+
|
|
36
|
+
expect(result).toBeDefined();
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
expect(result?.filename).toBe('test-document.pdf');
|
|
39
|
+
expect(result?.contentType).toBe('application/pdf');
|
|
40
|
+
expect(result?.file).toBeInstanceOf(Buffer);
|
|
41
|
+
expect(result?.file.toString()).toBe('test file content');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle different file types correctly', async () => {
|
|
45
|
+
const testCases = [
|
|
46
|
+
{
|
|
47
|
+
contentType: 'image/jpeg',
|
|
48
|
+
filename: 'photo.jpg',
|
|
49
|
+
content: 'jpeg image data',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
53
|
+
filename: 'document.docx',
|
|
54
|
+
content: 'word document data',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
contentType: 'text/plain',
|
|
58
|
+
filename: 'notes.txt',
|
|
59
|
+
content: 'plain text content',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const testCase of testCases) {
|
|
64
|
+
const binary = await medplum.createResource<Binary>({
|
|
65
|
+
resourceType: 'Binary',
|
|
66
|
+
contentType: testCase.contentType,
|
|
67
|
+
data: Buffer.from(testCase.content).toString('base64'),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const media = await medplum.createResource<Media>({
|
|
71
|
+
resourceType: 'Media',
|
|
72
|
+
status: 'completed',
|
|
73
|
+
content: {
|
|
74
|
+
contentType: testCase.contentType,
|
|
75
|
+
url: `Binary/${binary.id}`,
|
|
76
|
+
title: testCase.filename,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
81
|
+
|
|
82
|
+
expect(result?.filename).toBe(testCase.filename);
|
|
83
|
+
expect(result?.contentType).toBe(testCase.contentType);
|
|
84
|
+
expect(result?.file.toString()).toBe(testCase.content);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should use default filename when Media has no title', async () => {
|
|
89
|
+
const binary = await medplum.createResource<Binary>({
|
|
90
|
+
resourceType: 'Binary',
|
|
91
|
+
contentType: 'application/pdf',
|
|
92
|
+
data: Buffer.from('content').toString('base64'),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const media = await medplum.createResource<Media>({
|
|
96
|
+
resourceType: 'Media',
|
|
97
|
+
status: 'completed',
|
|
98
|
+
content: {
|
|
99
|
+
contentType: 'application/pdf',
|
|
100
|
+
url: `Binary/${binary.id}`,
|
|
101
|
+
// No title provided
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
106
|
+
|
|
107
|
+
expect(result?.filename).toBe('attachment');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should use default contentType when Media has no contentType', async () => {
|
|
111
|
+
const binary = await medplum.createResource<Binary>({
|
|
112
|
+
resourceType: 'Binary',
|
|
113
|
+
contentType: 'application/octet-stream',
|
|
114
|
+
data: Buffer.from('content').toString('base64'),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const media = await medplum.createResource<Media>({
|
|
118
|
+
resourceType: 'Media',
|
|
119
|
+
status: 'completed',
|
|
120
|
+
content: {
|
|
121
|
+
url: `Binary/${binary.id}`,
|
|
122
|
+
title: 'file.bin',
|
|
123
|
+
// No contentType provided
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
128
|
+
|
|
129
|
+
expect(result?.contentType).toBe('application/octet-stream');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return null when Binary resource cannot be fetched', async () => {
|
|
133
|
+
const media = await medplum.createResource<Media>({
|
|
134
|
+
resourceType: 'Media',
|
|
135
|
+
status: 'completed',
|
|
136
|
+
content: {
|
|
137
|
+
contentType: 'application/pdf',
|
|
138
|
+
url: 'Binary/nonexistent-binary-id',
|
|
139
|
+
title: 'test.pdf',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
144
|
+
|
|
145
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
146
|
+
|
|
147
|
+
expect(result).toBeNull();
|
|
148
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
149
|
+
|
|
150
|
+
consoleErrorSpy.mockRestore();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should return null when Media has no content URL', async () => {
|
|
154
|
+
const media = await medplum.createResource<Media>({
|
|
155
|
+
resourceType: 'Media',
|
|
156
|
+
status: 'completed',
|
|
157
|
+
// No content object
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
161
|
+
|
|
162
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
163
|
+
|
|
164
|
+
expect(result).toBeNull();
|
|
165
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
166
|
+
'[getBinaryFromMedia] Media resource has no content URL'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
consoleErrorSpy.mockRestore();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return null when Binary has no data field', async () => {
|
|
173
|
+
const binary = await medplum.createResource<Binary>({
|
|
174
|
+
resourceType: 'Binary',
|
|
175
|
+
contentType: 'application/pdf',
|
|
176
|
+
// No data field
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const media = await medplum.createResource<Media>({
|
|
180
|
+
resourceType: 'Media',
|
|
181
|
+
status: 'completed',
|
|
182
|
+
content: {
|
|
183
|
+
contentType: 'application/pdf',
|
|
184
|
+
url: `Binary/${binary.id}`,
|
|
185
|
+
title: 'test.pdf',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
190
|
+
|
|
191
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
192
|
+
|
|
193
|
+
expect(result).toBeNull();
|
|
194
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
195
|
+
'[convertMediaToAttachment] Binary resource has no data:',
|
|
196
|
+
binary.id
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
consoleErrorSpy.mockRestore();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle base64 decoding correctly', async () => {
|
|
203
|
+
const originalContent = 'Hello, World! This is a test file with special characters: @#$%^&*()';
|
|
204
|
+
const binary = await medplum.createResource<Binary>({
|
|
205
|
+
resourceType: 'Binary',
|
|
206
|
+
contentType: 'text/plain',
|
|
207
|
+
data: Buffer.from(originalContent).toString('base64'),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const media = await medplum.createResource<Media>({
|
|
211
|
+
resourceType: 'Media',
|
|
212
|
+
status: 'completed',
|
|
213
|
+
content: {
|
|
214
|
+
contentType: 'text/plain',
|
|
215
|
+
url: `Binary/${binary.id}`,
|
|
216
|
+
title: 'test.txt',
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
221
|
+
|
|
222
|
+
expect(result?.file.toString()).toBe(originalContent);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle large files', async () => {
|
|
226
|
+
// Create a 1MB file
|
|
227
|
+
const largeContent = Buffer.alloc(1024 * 1024, 'a');
|
|
228
|
+
const binary = await medplum.createResource<Binary>({
|
|
229
|
+
resourceType: 'Binary',
|
|
230
|
+
contentType: 'application/octet-stream',
|
|
231
|
+
data: largeContent.toString('base64'),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const media = await medplum.createResource<Media>({
|
|
235
|
+
resourceType: 'Media',
|
|
236
|
+
status: 'completed',
|
|
237
|
+
content: {
|
|
238
|
+
contentType: 'application/octet-stream',
|
|
239
|
+
url: `Binary/${binary.id}`,
|
|
240
|
+
title: 'large-file.bin',
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
245
|
+
|
|
246
|
+
expect(result).toBeDefined();
|
|
247
|
+
expect(result?.file.length).toBe(1024 * 1024);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should handle binary content with special characters in filename', async () => {
|
|
251
|
+
const binary = await medplum.createResource<Binary>({
|
|
252
|
+
resourceType: 'Binary',
|
|
253
|
+
contentType: 'application/pdf',
|
|
254
|
+
data: Buffer.from('content').toString('base64'),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const media = await medplum.createResource<Media>({
|
|
258
|
+
resourceType: 'Media',
|
|
259
|
+
status: 'completed',
|
|
260
|
+
content: {
|
|
261
|
+
contentType: 'application/pdf',
|
|
262
|
+
url: `Binary/${binary.id}`,
|
|
263
|
+
title: 'test document (v2) [final].pdf',
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
268
|
+
|
|
269
|
+
expect(result?.filename).toBe('test document (v2) [final].pdf');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should handle conversion errors gracefully', async () => {
|
|
273
|
+
// Create media with invalid Binary URL format
|
|
274
|
+
const media = await medplum.createResource<Media>({
|
|
275
|
+
resourceType: 'Media',
|
|
276
|
+
status: 'completed',
|
|
277
|
+
content: {
|
|
278
|
+
contentType: 'application/pdf',
|
|
279
|
+
url: 'InvalidFormat/NoSlash',
|
|
280
|
+
title: 'test.pdf',
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
285
|
+
|
|
286
|
+
const result = await convertMediaToAttachment(medplum, media);
|
|
287
|
+
|
|
288
|
+
expect(result).toBeNull();
|
|
289
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
290
|
+
|
|
291
|
+
consoleErrorSpy.mockRestore();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { MedplumClient } from '@medplum/core';
|
|
2
|
+
import type { BotEvent } from '@medplum/core';
|
|
3
|
+
import type { ContextGenerator } from 'vintasend';
|
|
4
|
+
import { VintaSendFactory } from 'vintasend';
|
|
5
|
+
import { MedplumSingleton } from './medplum-singleton';
|
|
6
|
+
import { formatPatientNameWithPreferredName } from './patients';
|
|
7
|
+
import * as compiledTemplates from '../compiled-notification-templates.json';
|
|
8
|
+
import {
|
|
9
|
+
MedplumNotificationBackend,
|
|
10
|
+
MedplumAttachmentManager,
|
|
11
|
+
PugInlineEmailTemplateRendererFactory,
|
|
12
|
+
MedplumLogger,
|
|
13
|
+
} from 'vintasend-medplum';
|
|
14
|
+
import { SendgridNotificationAdapterFactory } from 'vintasend-sendgrid';
|
|
15
|
+
import type { Media } from '@medplum/fhirtypes';
|
|
16
|
+
import { getBinaryFromMedia } from './file-upload';
|
|
17
|
+
|
|
18
|
+
async function getUserById(medplum: MedplumClient, referenceString: string) {
|
|
19
|
+
if (!referenceString) {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.error('[getUserById] referenceString is null/undefined/empty!');
|
|
22
|
+
throw new Error('The "id" parameter cannot be null, undefined, or an empty string.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const [resourceType, id] = referenceString.split('/');
|
|
26
|
+
|
|
27
|
+
if (!id) {
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.error('[getUserById] ID extracted from referenceString is empty!');
|
|
30
|
+
throw new Error('The "id" parameter cannot be null, undefined, or an empty string.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return medplum.readResource(resourceType as 'Patient' | 'Practitioner', id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts a Media resource to VintaSend attachment format.
|
|
38
|
+
*
|
|
39
|
+
* Fetches the Binary resource referenced by the Media and extracts the file data,
|
|
40
|
+
* then returns it in the format expected by VintaSend for email attachments.
|
|
41
|
+
*
|
|
42
|
+
* @param medplum - The Medplum client instance
|
|
43
|
+
* @param media - The Media resource containing the file metadata
|
|
44
|
+
* @returns A NotificationAttachmentUpload object with file, filename, and contentType
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const attachment = await convertMediaToAttachment(medplum, media);
|
|
48
|
+
* // { file: Buffer, filename: 'document.pdf', contentType: 'application/pdf' }
|
|
49
|
+
*/
|
|
50
|
+
export async function convertMediaToAttachment(
|
|
51
|
+
medplum: MedplumClient,
|
|
52
|
+
media: Media
|
|
53
|
+
): Promise<{
|
|
54
|
+
file: Buffer;
|
|
55
|
+
filename: string;
|
|
56
|
+
contentType: string;
|
|
57
|
+
} | null> {
|
|
58
|
+
try {
|
|
59
|
+
// Fetch Binary resource from media.content.url
|
|
60
|
+
const binary = await getBinaryFromMedia(medplum, media);
|
|
61
|
+
|
|
62
|
+
if (!binary) {
|
|
63
|
+
console.error('[convertMediaToAttachment] Failed to fetch Binary resource for Media:', media.id);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extract file data - Binary.data is base64-encoded
|
|
68
|
+
let file: Buffer;
|
|
69
|
+
if (binary.data) {
|
|
70
|
+
// If data is embedded in the Binary resource as base64
|
|
71
|
+
file = Buffer.from(binary.data, 'base64');
|
|
72
|
+
} else {
|
|
73
|
+
// If Binary is stored externally, we need to fetch it via URL
|
|
74
|
+
// This is handled by getBinaryFromMedia
|
|
75
|
+
console.error('[convertMediaToAttachment] Binary resource has no data:', binary.id);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Return in VintaSend NotificationAttachmentUpload format
|
|
80
|
+
return {
|
|
81
|
+
file,
|
|
82
|
+
filename: media.content?.title || 'attachment',
|
|
83
|
+
contentType: media.content?.contentType || 'application/octet-stream',
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('[convertMediaToAttachment] Error converting Media to attachment:', error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class TaskAssignmentContextGenerator implements ContextGenerator {
|
|
92
|
+
async generate({
|
|
93
|
+
userId,
|
|
94
|
+
taskTitle,
|
|
95
|
+
taskDescription,
|
|
96
|
+
taskIsUrgent,
|
|
97
|
+
taskLink,
|
|
98
|
+
requesterName,
|
|
99
|
+
attachmentCount,
|
|
100
|
+
}: {
|
|
101
|
+
userId: string;
|
|
102
|
+
taskTitle: string;
|
|
103
|
+
taskDescription: string;
|
|
104
|
+
taskIsUrgent: boolean;
|
|
105
|
+
taskLink: string;
|
|
106
|
+
requesterName: string;
|
|
107
|
+
attachmentCount?: number;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
firstName: string;
|
|
110
|
+
taskTitle: string;
|
|
111
|
+
taskDescription: string;
|
|
112
|
+
taskIsUrgent: boolean;
|
|
113
|
+
taskLink: string;
|
|
114
|
+
requesterName: string;
|
|
115
|
+
attachmentCount: number;
|
|
116
|
+
}> {
|
|
117
|
+
const medplum = MedplumSingleton.getInstance();
|
|
118
|
+
const user = await getUserById(medplum, userId);
|
|
119
|
+
const firstName = formatPatientNameWithPreferredName(user.name?.[0]) ?? 'Practitioner';
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
firstName,
|
|
123
|
+
taskTitle,
|
|
124
|
+
taskDescription,
|
|
125
|
+
taskIsUrgent,
|
|
126
|
+
taskLink,
|
|
127
|
+
requesterName,
|
|
128
|
+
attachmentCount: attachmentCount || 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class TaskDueSoonContextGenerator implements ContextGenerator {
|
|
134
|
+
async generate({
|
|
135
|
+
userId,
|
|
136
|
+
taskTitle,
|
|
137
|
+
taskDescription,
|
|
138
|
+
taskIsUrgent,
|
|
139
|
+
taskLink,
|
|
140
|
+
dueDate,
|
|
141
|
+
}: {
|
|
142
|
+
userId: string;
|
|
143
|
+
taskTitle: string;
|
|
144
|
+
taskDescription: string;
|
|
145
|
+
taskIsUrgent: boolean;
|
|
146
|
+
taskLink: string;
|
|
147
|
+
dueDate: string;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
firstName: string;
|
|
150
|
+
taskTitle: string;
|
|
151
|
+
taskDescription: string;
|
|
152
|
+
taskIsUrgent: boolean;
|
|
153
|
+
taskLink: string;
|
|
154
|
+
dueDate: string;
|
|
155
|
+
}> {
|
|
156
|
+
const medplum = MedplumSingleton.getInstance();
|
|
157
|
+
const user = await getUserById(medplum, userId);
|
|
158
|
+
const firstName = formatPatientNameWithPreferredName(user.name?.[0]) ?? 'Practitioner';
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
firstName,
|
|
162
|
+
taskTitle,
|
|
163
|
+
taskDescription,
|
|
164
|
+
taskIsUrgent,
|
|
165
|
+
taskLink,
|
|
166
|
+
dueDate,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class InboxMessageContextGenerator implements ContextGenerator {
|
|
172
|
+
async generate({
|
|
173
|
+
userId,
|
|
174
|
+
sender,
|
|
175
|
+
messageContent,
|
|
176
|
+
messageTopic,
|
|
177
|
+
}: {
|
|
178
|
+
userId: string;
|
|
179
|
+
sender: string;
|
|
180
|
+
messageContent: string;
|
|
181
|
+
messageTopic: string;
|
|
182
|
+
}): Promise<{
|
|
183
|
+
firstName: string;
|
|
184
|
+
senderName: string;
|
|
185
|
+
senderFirstName: string;
|
|
186
|
+
messageContent: string;
|
|
187
|
+
messageTopic: string;
|
|
188
|
+
}> {
|
|
189
|
+
const medplum = MedplumSingleton.getInstance();
|
|
190
|
+
const user = await getUserById(medplum, userId);
|
|
191
|
+
const firstName = formatPatientNameWithPreferredName(user.name?.[0]) ?? 'Practitioner';
|
|
192
|
+
|
|
193
|
+
// Extract sender name from reference
|
|
194
|
+
let senderName = 'Unknown';
|
|
195
|
+
let senderFirstName = 'Unknown';
|
|
196
|
+
try {
|
|
197
|
+
const [senderResourceType, senderId] = sender.split('/');
|
|
198
|
+
if (senderId) {
|
|
199
|
+
const senderResource = await medplum.readResource(senderResourceType as 'Patient' | 'Practitioner', senderId);
|
|
200
|
+
senderName = formatPatientNameWithPreferredName(senderResource.name?.[0]) ?? 'Unknown';
|
|
201
|
+
senderFirstName = senderResource.name?.[0]?.given?.[0] || 'Unknown';
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// eslint-disable-next-line no-console
|
|
205
|
+
console.error('[InboxMessageContextGenerator] Error fetching sender:', error);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
firstName,
|
|
210
|
+
senderName,
|
|
211
|
+
senderFirstName,
|
|
212
|
+
messageContent,
|
|
213
|
+
messageTopic,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// context map for generating the context of each notification
|
|
219
|
+
export const contextGeneratorsMap = {
|
|
220
|
+
taskAssignment: new TaskAssignmentContextGenerator(),
|
|
221
|
+
taskDueSoon: new TaskDueSoonContextGenerator(),
|
|
222
|
+
inboxMessage: new InboxMessageContextGenerator(),
|
|
223
|
+
} as const;
|
|
224
|
+
|
|
225
|
+
export type NotificationTypeConfig = {
|
|
226
|
+
ContextMap: typeof contextGeneratorsMap;
|
|
227
|
+
NotificationIdType: string;
|
|
228
|
+
UserIdType: string;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export type SendGridConfig = {
|
|
232
|
+
SENDGRID_API_KEY: string;
|
|
233
|
+
SENDGRID_FROM_EMAIL: string;
|
|
234
|
+
SENDGRID_FROM_NAME: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Helper function to build SendGridConfig from bot event secrets
|
|
239
|
+
* Reduces duplication across bot handlers
|
|
240
|
+
* Throws if required secrets (API key or from email) are missing
|
|
241
|
+
*/
|
|
242
|
+
export function buildSendGridConfig(event: BotEvent): SendGridConfig {
|
|
243
|
+
const apiKey = event.secrets.SENDGRID_API_KEY?.valueString;
|
|
244
|
+
const fromEmail = event.secrets.SENDGRID_FROM_EMAIL?.valueString;
|
|
245
|
+
const fromName = event.secrets.SENDGRID_FROM_NAME?.valueString || 'Medplum Notifications';
|
|
246
|
+
|
|
247
|
+
if (!apiKey) {
|
|
248
|
+
// eslint-disable-next-line no-console
|
|
249
|
+
console.error('[buildSendGridConfig] SENDGRID_API_KEY secret is missing or empty');
|
|
250
|
+
throw new Error('SENDGRID_API_KEY must be configured in bot secrets');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!fromEmail) {
|
|
254
|
+
// eslint-disable-next-line no-console
|
|
255
|
+
console.error('[buildSendGridConfig] SENDGRID_FROM_EMAIL secret is missing or empty');
|
|
256
|
+
throw new Error('SENDGRID_FROM_EMAIL must be configured in bot secrets');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
SENDGRID_API_KEY: apiKey,
|
|
261
|
+
SENDGRID_FROM_EMAIL: fromEmail,
|
|
262
|
+
SENDGRID_FROM_NAME: fromName,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function getNotificationService(medplum: MedplumClient, sendgridConfig: SendGridConfig) {
|
|
267
|
+
const backend = new MedplumNotificationBackend<NotificationTypeConfig>(medplum);
|
|
268
|
+
const templateRenderer = new PugInlineEmailTemplateRendererFactory<NotificationTypeConfig>().create(
|
|
269
|
+
compiledTemplates
|
|
270
|
+
);
|
|
271
|
+
const adapter = new SendgridNotificationAdapterFactory<NotificationTypeConfig>().create(templateRenderer, false, {
|
|
272
|
+
apiKey: sendgridConfig.SENDGRID_API_KEY || '',
|
|
273
|
+
fromEmail: sendgridConfig.SENDGRID_FROM_EMAIL || '',
|
|
274
|
+
fromName: sendgridConfig.SENDGRID_FROM_NAME,
|
|
275
|
+
});
|
|
276
|
+
return new VintaSendFactory<NotificationTypeConfig>().create(
|
|
277
|
+
[adapter],
|
|
278
|
+
backend,
|
|
279
|
+
new MedplumLogger(),
|
|
280
|
+
contextGeneratorsMap,
|
|
281
|
+
undefined,
|
|
282
|
+
new MedplumAttachmentManager(medplum)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { formatHumanName } from '@medplum/core';
|
|
2
|
+
import { HumanName } from '@medplum/fhirtypes';
|
|
3
|
+
import { PREFERRED_NAME_EXTENSION_URL } from './extensions';
|
|
4
|
+
|
|
5
|
+
export function getPatientPreferredName(patientName: HumanName | undefined): string | undefined {
|
|
6
|
+
if (!patientName) return;
|
|
7
|
+
const preferredName = patientName?.extension?.find(
|
|
8
|
+
(extension) => extension.url === PREFERRED_NAME_EXTENSION_URL
|
|
9
|
+
)?.valueString;
|
|
10
|
+
return preferredName;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatPatientNameWithPreferredName(patientName: HumanName | undefined) {
|
|
14
|
+
const preferredName = getPatientPreferredName(patientName);
|
|
15
|
+
if (!preferredName) return formatHumanName(patientName);
|
|
16
|
+
const given = patientName?.given?.join(' ');
|
|
17
|
+
const familyName = patientName?.family;
|
|
18
|
+
|
|
19
|
+
return `${given} (${preferredName}) ${familyName}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
doctype html
|
|
2
|
+
html
|
|
3
|
+
head
|
|
4
|
+
meta(charset='utf-8')
|
|
5
|
+
style.
|
|
6
|
+
body {
|
|
7
|
+
white-space: pre-line;
|
|
8
|
+
}
|
|
9
|
+
body
|
|
10
|
+
h1 Task Assigned
|
|
11
|
+
|
|
12
|
+
p Hello #{firstName},
|
|
13
|
+
|
|
14
|
+
p You have been assigned a new task by #{requesterName}.
|
|
15
|
+
|
|
16
|
+
p
|
|
17
|
+
strong Task:
|
|
18
|
+
| #{taskTitle}
|
|
19
|
+
if taskDescription
|
|
20
|
+
p
|
|
21
|
+
strong Description:
|
|
22
|
+
| #{taskDescription}
|
|
23
|
+
|
|
24
|
+
if attachmentCount > 0
|
|
25
|
+
p
|
|
26
|
+
strong Attachments:
|
|
27
|
+
| #{attachmentCount} file(s) attached
|
|
28
|
+
p Files are attached to this email for your reference.
|
|
29
|
+
|
|
30
|
+
if taskIsUrgent
|
|
31
|
+
p
|
|
32
|
+
strong URGENT
|
|
33
|
+
|
|
34
|
+
p
|
|
35
|
+
a(href=taskLink) View Task
|
|
36
|
+
|
|
37
|
+
p Please review the task details and take appropriate action.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
doctype html
|
|
2
|
+
html
|
|
3
|
+
head
|
|
4
|
+
meta(charset='utf-8')
|
|
5
|
+
style.
|
|
6
|
+
body {
|
|
7
|
+
white-space: pre-line;
|
|
8
|
+
}
|
|
9
|
+
body
|
|
10
|
+
h1 Task Due Reminder
|
|
11
|
+
|
|
12
|
+
p Hello #{firstName},
|
|
13
|
+
|
|
14
|
+
p This is a reminder that you have a task that is due in approximately 24 hours.
|
|
15
|
+
|
|
16
|
+
p
|
|
17
|
+
strong Task:
|
|
18
|
+
| #{taskTitle}
|
|
19
|
+
if taskDescription
|
|
20
|
+
p
|
|
21
|
+
strong Description:
|
|
22
|
+
| #{taskDescription}
|
|
23
|
+
p
|
|
24
|
+
strong Due Date:
|
|
25
|
+
| #{dueDate}
|
|
26
|
+
if taskIsUrgent
|
|
27
|
+
p
|
|
28
|
+
strong URGENT
|
|
29
|
+
|
|
30
|
+
p
|
|
31
|
+
a(href=taskLink) View Task
|
|
32
|
+
|
|
33
|
+
p Please make sure to complete this task before the due date.
|
|
34
|
+
|