From 9df45fc00b97ac4ab2ea2514d11c8d1e99fb2569 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 13 Mar 2024 13:38:35 +0530 Subject: [PATCH 1/7] Add builds for staging branches --- .github/workflows/deploy.yaml | 140 ++++++++++++---------------------- 1 file changed, 49 insertions(+), 91 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6e760362cd6..863de2aaf8f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -5,6 +5,8 @@ on: branches: - develop - master + tags: + - v* pull_request: branches: - develop @@ -38,9 +40,9 @@ jobs: uses: actions/cache@v3 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ hashFiles('package-lock.json', 'Dockerfile') }} + key: ${{ runner.os }}-buildx-build-${{ hashFiles('package-lock.json', 'Dockerfile') }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-build- - name: Test build uses: docker/build-push-action@v3 @@ -60,132 +62,88 @@ jobs: rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache - build-staging: + build: needs: test - name: Build & Push Staging to container registries - if: github.ref == 'refs/heads/develop' + name: Build & Push to container registries runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Docker meta + - name: Generate docker tags id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | - ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - ${{ secrets.DOCKER_HUB_USERNAME }}/${{ env.IMAGE_NAME }} + ghcr.io/${{ github.repository }} + ${{ secrets.DOCKER_HUB_USERNAME }}/${{ github.event.repository.name }} tags: | + type=raw,value=production-latest,enable=${{ github.ref == 'refs/heads/v*' }} + type=raw,value=production-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}},enable=${{ github.ref == 'refs/heads/v*' }} + type=raw,value=staging-latest,enable=${{ github.ref == 'refs/heads/staging' }} + type=raw,value=staging-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}},enable=${{ github.ref == 'refs/heads/staging' }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/develop' }} - type=raw,value=latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}} + type=raw,value=latest-${{ github.run_number }},enable=${{ github.ref == 'refs/heads/develop' }} type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} flavor: | - latest=true + latest=false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ hashFiles('package-lock.json', 'Dockerfile') }} - restore-keys: | - ${{ runner.os }}-buildx- + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image - uses: docker/build-push-action@v3 - with: - context: . - file: Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new - - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache - - build-production: - needs: test - name: Build & Push Production to container registries - if: github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: | - ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - ${{ secrets.DOCKER_HUB_USERNAME }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=production-latest,enable=${{ github.ref == 'refs/heads/master' }} - type=raw,value=production-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ hashFiles('package-lock.json', 'Dockerfile') }} + key: ${{ runner.os }}-buildx-build-${{ hashFiles('package-lock.json', 'Dockerfile') }} restore-keys: | - ${{ runner.os }}-buildx- - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + ${{ runner.os }}-buildx-build- - - name: Build image - uses: docker/build-push-action@v3 + - name: Build and push image + uses: docker/build-push-action@v5 with: context: . file: Dockerfile push: true + provenance: false + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - name: Move cache run: | rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache + notify-release: + needs: build + if: github.ref == 'refs/tags/v*' + name: Notify release + runs-on: ubuntu-latest + steps: + - name: Notify release + run: | + echo "Release ${{ github.sha }} is ready to be deployed to production" + deploy-staging-gcp: - needs: build-staging + needs: build + if: github.ref == 'refs/heads/staging' name: Deploy to staging GCP cluster runs-on: ubuntu-latest environment: @@ -231,7 +189,7 @@ jobs: kubectl apply -f care-fe.yaml deploy-production-manipur: - needs: build-production + needs: notify-release name: Deploy to GKE Manipur runs-on: ubuntu-latest environment: @@ -277,7 +235,7 @@ jobs: kubectl apply -f care-fe.yaml deploy-production-karnataka: - needs: build-production + needs: notify-release name: Deploy to GKE Karnataka runs-on: ubuntu-latest environment: @@ -323,7 +281,7 @@ jobs: kubectl apply -f care-fe.yaml deploy-production-sikkim: - needs: build-production + needs: notify-release name: Deploy to GKE Sikkim runs-on: ubuntu-latest environment: @@ -369,7 +327,7 @@ jobs: kubectl apply -f care-fe.yaml deploy-production-assam: - needs: build-production + needs: notify-release name: Deploy to GKE Assam runs-on: ubuntu-latest environment: @@ -415,7 +373,7 @@ jobs: kubectl apply -f care-fe.yaml deploy-production-nagaland: - needs: build-production + needs: notify-release name: Deploy to GKE Nagaland runs-on: ubuntu-latest environment: @@ -461,7 +419,7 @@ jobs: kubectl apply -f care-fe.yaml deploy-production-meghalaya: - needs: build-production + needs: notify-release name: Deploy to GKE Meghalaya runs-on: ubuntu-latest environment: From 936d9c1f725e6f5d0528fad5c9c5714599c61d12 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 13 Mar 2024 13:43:14 +0530 Subject: [PATCH 2/7] update cache action --- .github/workflows/deploy.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 863de2aaf8f..ab06106547a 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -31,25 +31,26 @@ jobs: runs-on: ubuntu-latest name: Test steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-build-${{ hashFiles('package-lock.json', 'Dockerfile') }} + key: ${{ runner.os }}-buildx-test-${{ hashFiles('package-lock.json', 'Dockerfile') }} restore-keys: | - ${{ runner.os }}-buildx-build- + ${{ runner.os }}-buildx-test- - name: Test build - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile push: false + provenance: false cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new From e30fde26441dae0e751037ab172023575da78534 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 13 Mar 2024 13:44:27 +0530 Subject: [PATCH 3/7] build on branches only --- .github/workflows/deploy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index ab06106547a..8be67339b27 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -65,6 +65,7 @@ jobs: build: needs: test + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/staging' || github.ref == 'refs/tags/v*' name: Build & Push to container registries runs-on: ubuntu-latest steps: From ad8b5e8ea46785555b53c255fcb93af758df0498 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 15 Mar 2024 15:35:23 +0530 Subject: [PATCH 4/7] Fixes patient no. not being sent to backend if updated to empty string (#7418) * Fixes patient no. not being sent to backend if updated to empty string * minor fix --- src/Components/Facility/ConsultationForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 48b8a367e77..70e78ccb3c8 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -761,10 +761,9 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { weight: Number(state.form.weight), height: Number(state.form.height), bed: bed && bed instanceof Array ? bed[0]?.id : bed?.id, + patient_no: state.form.patient_no || null, }; - if (state.form.patient_no) data["patient_no"] = state.form.patient_no; - const res = await dispatchAction( id ? updateConsultation(id!, data) : createConsultation(data) ); From b998ffd886a20a1efa7e8d2d547d3fc7041d0374 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 15 Mar 2024 15:49:21 +0530 Subject: [PATCH 5/7] Fix arm builds and update build branches (#7409) * disable arm builds and fix build targets * add workaround for arm builds --- .github/workflows/deploy.yaml | 4 ++-- Dockerfile | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 8be67339b27..dbeff965240 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,13 +4,13 @@ on: push: branches: - develop - - master + - staging tags: - v* pull_request: branches: - develop - - master + - staging workflow_dispatch: concurrency: diff --git a/Dockerfile b/Dockerfile index 1aed8202330..b185aa96984 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ WORKDIR /app ENV NODE_OPTIONS="--max-old-space-size=4096" +RUN if [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; then apt-get update && apt-get install -y python3-dev make g++; fi + COPY package.json package-lock.json ./ RUN npm install --legacy-peer-deps From 921fb4c9b857518441d2915e6d5fb982d94a514e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 15 Mar 2024 17:18:25 +0530 Subject: [PATCH 6/7] Improve build speeds for multi platform builds (#7423) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b185aa96984..d65fe6e29c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ #build-stage -FROM node:18-buster-slim as build-stage +FROM --platform=$BUILDPLATFORM node:18-buster-slim as build-stage WORKDIR /app From df8c57b4f4abc29de89526fe31a76b00787df396 Mon Sep 17 00:00:00 2001 From: Gokulram A Date: Tue, 19 Mar 2024 12:17:11 +0530 Subject: [PATCH 7/7] Add titrated prescription dosage type (#6565) * feat: option to add titrated drug dose in prescription * Update prescription model and form to use dosage_type and base_dosage instead of is_prn and dosage * Update edit prescription form and fix dosage validation * Refactor administration dosage validation * Fix unit validation in PrescriptionFormValidator * Add dosage information to TimelineNode * Remove dosage from TimelineNode component * fix: renamed cypress dosage field selector to base_dosage * Merge branch 'develop' into fix-issue-6432 * update cypress to match new dosage field's id * fixed linting issue --------- Co-authored-by: Rithvik Nishad Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Co-authored-by: khavinshankar --- .../pageobject/Patient/PatientPrescription.ts | 2 +- .../Form/FormFields/DosageFormField.tsx | 14 +++ .../FormFields/NumericWithUnitsFormField.tsx | 2 + .../Medicine/AdministerMedicine.tsx | 52 ++++++++++- .../Medicine/CreatePrescriptionForm.tsx | 77 ++++++++++++---- .../Medicine/EditPrescriptionForm.tsx | 72 ++++++++++++--- .../Medicine/MedicineAdministration.tsx | 88 ++++++++++++++----- .../AdministrationTable.tsx | 6 +- .../AdministrationTableRow.tsx | 17 +++- .../MedicineAdministrationSheet/index.tsx | 6 +- .../Medicine/PrescriptionBuilder.tsx | 12 ++- .../Medicine/PrescriptionDetailCard.tsx | 71 ++++++++++++--- .../Medicine/PrescriptionsTable.tsx | 15 +++- .../Medicine/PrescrpitionTimeline.tsx | 5 +- src/Components/Medicine/models.ts | 12 ++- src/Components/Medicine/validators.ts | 48 ++++++++-- src/Locale/en/Medicine.json | 6 +- 17 files changed, 409 insertions(+), 96 deletions(-) create mode 100644 src/Components/Form/FormFields/DosageFormField.tsx diff --git a/cypress/pageobject/Patient/PatientPrescription.ts b/cypress/pageobject/Patient/PatientPrescription.ts index c20d32672d6..e5934fcb95a 100644 --- a/cypress/pageobject/Patient/PatientPrescription.ts +++ b/cypress/pageobject/Patient/PatientPrescription.ts @@ -27,7 +27,7 @@ export class PatientPrescription { } enterDosage(doseAmount: string) { - cy.get("#dosage").type(doseAmount, { force: true }); + cy.get("#base_dosage").type(doseAmount, { force: true }); } selectDosageFrequency(frequency: string) { diff --git a/src/Components/Form/FormFields/DosageFormField.tsx b/src/Components/Form/FormFields/DosageFormField.tsx new file mode 100644 index 00000000000..5f669701913 --- /dev/null +++ b/src/Components/Form/FormFields/DosageFormField.tsx @@ -0,0 +1,14 @@ +import { DOSAGE_UNITS } from "../../Medicine/models"; +import NumericWithUnitsFormField from "./NumericWithUnitsFormField"; +import { FormFieldBaseProps } from "./Utils"; + +type Props = FormFieldBaseProps & { + placeholder?: string; + autoComplete?: string; + min?: string | number; + max?: string | number; +}; + +export default function DosageFormField(props: Props) { + return ; +} diff --git a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx index d0dae7ddff5..754079bb527 100644 --- a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx +++ b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx @@ -35,6 +35,7 @@ export default function NumericWithUnitsFormField(props: Props) { autoComplete={props.autoComplete} required={field.required} value={numValue} + disabled={props.disabled} onChange={(e) => field.handleChange(Number(e.target.value) + " " + unitValue) } @@ -48,6 +49,7 @@ export default function NumericWithUnitsFormField(props: Props) { onChange={(e) => field.handleChange(numValue + " " + e.target.value) } + disabled={props.disabled} > {props.units.map((unit) => ( diff --git a/src/Components/Medicine/AdministerMedicine.tsx b/src/Components/Medicine/AdministerMedicine.tsx index 8798b476b06..12b7be3e0da 100644 --- a/src/Components/Medicine/AdministerMedicine.tsx +++ b/src/Components/Medicine/AdministerMedicine.tsx @@ -13,6 +13,8 @@ import dayjs from "../../Utils/dayjs"; import useSlug from "../../Common/hooks/useSlug"; import request from "../../Utils/request/request"; import MedicineRoutes from "./routes"; +import DosageFormField from "../Form/FormFields/DosageFormField"; +import { AdministrationDosageValidator } from "./validators"; interface Props { prescription: Prescription; @@ -24,6 +26,8 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { const consultation = useSlug("consultation"); const [isLoading, setIsLoading] = useState(false); const [notes, setNotes] = useState(""); + const [dosage, setDosage] = useState(); + const [error, setError] = useState(); const [isCustomTime, setIsCustomTime] = useState(false); const [customTime, setCustomTime] = useState( dayjs().format("YYYY-MM-DDTHH:mm") @@ -41,21 +45,45 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { description={
Last administered - - {prescription.last_administered_on - ? formatDateTime(prescription.last_administered_on) + + {" "} + {prescription.last_administration?.administered_date + ? formatDateTime( + prescription.last_administration.administered_date + ) : t("never")} + {prescription.dosage_type === "TITRATED" && ( + + {t("dosage")} + {":"} {prescription.last_administration?.dosage ?? "NA"} + + )} + + Administered by:{" "} + {prescription.last_administration?.administered_by?.username ?? + "NA"} +
} show onClose={() => props.onClose(false)} onConfirm={async () => { + if (prescription.dosage_type === "TITRATED") { + const error = AdministrationDosageValidator( + prescription.base_dosage, + prescription.target_dosage + )(dosage); + setError(error); + if (error) return; + } + setIsLoading(true); const { res } = await request(MedicineRoutes.administerPrescription, { pathParams: { consultation, external_id: prescription.id }, body: { notes, + dosage, administered_date: isCustomTime ? customTime : undefined, }, }); @@ -70,6 +98,24 @@ export default function AdministerMedicine({ prescription, ...props }: Props) {
+ {prescription.dosage_type === "TITRATED" && ( + setDosage(value)} + required + min={prescription.base_dosage} + max={prescription.target_dosage} + disabled={isLoading} + error={error} + errorClassName={error ? "block" : "hidden"} + /> + )} +
-
+ {props.prescription.dosage_type !== "PRN" && ( + { + if (e.value) { + field("dosage_type").onChange({ + name: "dosage_type", + value: "TITRATED", + }); + } else { + field("dosage_type").onChange({ + name: "dosage_type", + value: "REGULAR", + }); + } + }} + /> + )} +
t("PRESCRIPTION_ROUTE_" + key)} optionValue={(key) => key} /> - + {field("dosage_type").value === "TITRATED" ? ( +
+ + +
+ ) : ( + + )}
- {props.prescription.is_prn ? ( + {props.prescription.dosage_type === "PRN" ? ( <> - @@ -130,6 +164,13 @@ export default function CreatePrescriptionForm(props: {
)} + {field("dosage_type").value === "TITRATED" && ( + + )} + )} diff --git a/src/Components/Medicine/EditPrescriptionForm.tsx b/src/Components/Medicine/EditPrescriptionForm.tsx index 2bd3805af73..972b918eedf 100644 --- a/src/Components/Medicine/EditPrescriptionForm.tsx +++ b/src/Components/Medicine/EditPrescriptionForm.tsx @@ -1,13 +1,12 @@ import { useState } from "react"; import Form from "../Form/Form"; -import { DOSAGE_UNITS, Prescription } from "./models"; +import { Prescription } from "./models"; import request from "../../Utils/request/request"; import * as Notification from "../../Utils/Notifications"; import useSlug from "../../Common/hooks/useSlug"; import { RequiredFieldValidator } from "../Form/FieldValidators"; import { useTranslation } from "react-i18next"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import NumericWithUnitsFormField from "../Form/FormFields/NumericWithUnitsFormField"; import { PRESCRIPTION_FREQUENCIES, PRESCRIPTION_ROUTES, @@ -16,6 +15,8 @@ import TextFormField from "../Form/FormFields/TextFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import { EditPrescriptionFormValidator } from "./validators"; import MedicineRoutes from "./routes"; +import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; +import DosageFormField from "../Form/FormFields/DosageFormField"; interface Props { initial: Prescription; @@ -88,6 +89,27 @@ export default function EditPrescriptionForm(props: Props) { {...field("discontinued_reason")} /> + {props.initial.dosage_type !== "PRN" && ( + { + if (e.value) { + field("dosage_type").onChange({ + name: "dosage_type", + value: "TITRATED", + }); + } else { + field("dosage_type").onChange({ + name: "dosage_type", + value: "REGULAR", + }); + } + }} + /> + )} +
t("PRESCRIPTION_ROUTE_" + key)} optionValue={(key) => key} /> - + {field("dosage_type").value === "TITRATED" ? ( +
+ + +
+ ) : ( + + )}
- {props.initial.is_prn ? ( + {props.initial.dosage_type === "PRN" ? ( <> - @@ -154,6 +193,13 @@ export default function EditPrescriptionForm(props: Props) {
)} + {field("dosage_type").value === "TITRATED" && ( + + )} + )} diff --git a/src/Components/Medicine/MedicineAdministration.tsx b/src/Components/Medicine/MedicineAdministration.tsx index d899a3800fb..23aef4b4dd1 100644 --- a/src/Components/Medicine/MedicineAdministration.tsx +++ b/src/Components/Medicine/MedicineAdministration.tsx @@ -6,23 +6,31 @@ import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { Error, Success } from "../../Utils/Notifications"; -import { classNames, formatDateTime } from "../../Utils/utils"; +import { formatDateTime } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import dayjs from "../../Utils/dayjs"; import TextFormField from "../Form/FormFields/TextFormField"; import request from "../../Utils/request/request"; import MedicineRoutes from "./routes"; import useSlug from "../../Common/hooks/useSlug"; +import DosageFormField from "../Form/FormFields/DosageFormField"; +import { AdministrationDosageValidator } from "./validators"; interface Props { prescriptions: Prescription[]; onDone: () => void; } +type DosageField = { + dosage: MedicineAdministrationRecord["dosage"]; + error?: string; +}; + export default function MedicineAdministration(props: Props) { const { t } = useTranslation(); const consultation = useSlug("consultation"); const [shouldAdminister, setShouldAdminister] = useState([]); + const [dosages, setDosages] = useState([]); const [notes, setNotes] = useState( [] ); @@ -39,6 +47,7 @@ export default function MedicineAdministration(props: Props) { useEffect(() => { setShouldAdminister(Array(prescriptions.length).fill(false)); + setDosages(Array(prescriptions.length).fill({ dosage: undefined })); setNotes(Array(prescriptions.length).fill("")); setIsCustomTime(Array(prescriptions.length).fill(false)); setCustomTime( @@ -47,13 +56,31 @@ export default function MedicineAdministration(props: Props) { }, [props.prescriptions]); const handleSubmit = async () => { - const administrations = prescriptions - .map((prescription, i) => ({ - prescription, - notes: notes[i], - administered_date: isCustomTime[i] ? customTime[i] : undefined, - })) - .filter((_, i) => shouldAdminister[i]); + const administrations = []; + + for (let i = 0; i < prescriptions.length; i++) { + if (shouldAdminister[i]) { + if (prescriptions[i].dosage_type === "TITRATED") { + const error = AdministrationDosageValidator( + prescriptions[i].base_dosage, + prescriptions[i].target_dosage + )(dosages[i].dosage); + setDosages((dosages) => { + const newDosages = [...dosages]; + newDosages[i].error = error; + return newDosages; + }); + if (error) return; + } + const administration = { + prescription: prescriptions[i], + notes: notes[i], + dosage: dosages[i].dosage, + administered_date: isCustomTime[i] ? customTime[i] : undefined, + }; + administrations.push(administration); + } + } const ok = await Promise.all( administrations.map(({ prescription, ...body }) => @@ -74,7 +101,6 @@ export default function MedicineAdministration(props: Props) { }; const selectedCount = shouldAdminister.filter(Boolean).length; - const is_prn = prescriptions.some((obj) => obj.is_prn); return (
@@ -85,12 +111,7 @@ export default function MedicineAdministration(props: Props) { readonly selected={shouldAdminister[index]} > -
+
{" "} {t("last_administered")} - {obj.last_administered_on - ? formatDateTime(obj.last_administered_on) + {obj.last_administration?.administered_date + ? formatDateTime(obj.last_administration?.administered_date) : t("never")} + {obj.dosage_type === "TITRATED" && ( + + {t("dosage")} + {":"} {obj.last_administration?.dosage ?? "NA"} + + )}
-
+ {obj.dosage_type === "TITRATED" && ( + + setDosages((dosages) => { + const newDosages = [...dosages]; + newDosages[index].dosage = value; + return newDosages; + }) + } + required + min={obj.base_dosage} + max={obj.target_dosage} + disabled={!shouldAdminister[index]} + error={dosages[index]?.error} + errorClassName={dosages[index]?.error ? "block" : "hidden"} + /> )} - > {t("medicine")}

Dosage &

-

{!prescriptions[0]?.is_prn ? "Frequency" : "Indicator"}

+

+ {prescriptions[0]?.dosage_type !== "PRN" + ? "Frequency" + : "Indicator"} +

diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx index 40675640411..7a1542b43ae 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx @@ -49,7 +49,7 @@ export default function MedicineAdministrationTableRow({ ), archived: false, }, - key: `${prescription.last_administered_on}`, + key: `${prescription.last_administration?.administered_date}`, } ); @@ -141,7 +141,9 @@ export default function MedicineAdministrationTableRow({ onClose={() => setShowEdit(false)} show={showEdit} title={`${t("edit")} ${t( - prescription.is_prn ? "prn_prescription" : "prescription_medication" + prescription.dosage_type === "PRN" + ? "prn_prescription" + : "prescription_medication" )}: ${ prescription.medicine_object?.name ?? prescription.medicine_old }`} @@ -193,9 +195,16 @@ export default function MedicineAdministrationTableRow({
-

{prescription.dosage}

+ {prescription.dosage_type !== "TITRATED" ? ( +

{prescription.base_dosage}

+ ) : ( +

+ {prescription.base_dosage} - {prescription.target_dosage} +

+ )} +

- {!prescription.is_prn + {prescription.dosage_type !== "PRN" ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) : prescription.indicator}

diff --git a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx index dba0943db20..48c7e2aaff8 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx @@ -27,7 +27,11 @@ const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => { const [showDiscontinued, setShowDiscontinued] = useState(false); - const filters = { is_prn, prescription_type: "REGULAR", limit: 100 }; + const filters = { + dosage_type: is_prn ? "PRN" : "REGULAR,TITRATED", + prescription_type: "REGULAR", + limit: 100, + }; const { data, loading, refetch } = useQuery( MedicineRoutes.listPrescriptions, diff --git a/src/Components/Medicine/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx index 39bf9b2f506..998b96f3315 100644 --- a/src/Components/Medicine/PrescriptionBuilder.tsx +++ b/src/Components/Medicine/PrescriptionBuilder.tsx @@ -31,7 +31,11 @@ export default function PrescriptionBuilder({ const { data, refetch } = useQuery(MedicineRoutes.listPrescriptions, { pathParams: { consultation }, - query: { is_prn, prescription_type, limit: 100 }, + query: { + dosage_type: is_prn ? "PRN" : "REGULAR,TITRATED", + prescription_type, + limit: 100, + }, }); return ( @@ -118,5 +122,7 @@ export default function PrescriptionBuilder({ ); } -const DefaultPrescription: Partial = { is_prn: false }; -const DefaultPRNPrescription: Partial = { is_prn: true }; +const DefaultPrescription: Partial = { + dosage_type: "REGULAR", +}; +const DefaultPRNPrescription: Partial = { dosage_type: "PRN" }; diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index 0e1f28e9654..7112f75d598 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -40,7 +40,11 @@ export default function PrescriptionDetailCard({ > {prescription.prescription_type === "DISCHARGE" && `${t("discharge")} `} - {t(prescription.is_prn ? "prn_prescription" : "prescription")} + {t( + prescription.dosage_type === "PRN" + ? "prn_prescription" + : "prescription" + )} {` #${prescription.id?.slice(-5)}`} {prescription.discontinued && ( @@ -82,37 +86,64 @@ export default function PrescriptionDetailCard({
-
- +
+ {prescription.medicine_object?.name ?? prescription.medicine_old} {prescription.route && t("PRESCRIPTION_ROUTE_" + prescription.route)} - - {prescription.dosage} - + {prescription.dosage_type === "TITRATED" ? ( + <> + + {prescription.base_dosage} + + + {prescription.target_dosage} + + + ) : ( + + {prescription.base_dosage} + + )} - {prescription.is_prn ? ( + {prescription.dosage_type === "PRN" ? ( <> {prescription.indicator} {prescription.max_dosage} {prescription.min_hours_between_doses && @@ -128,21 +159,33 @@ export default function PrescriptionDetailCard({ prescription.frequency.toUpperCase() )} - + {prescription.days} )} + {prescription.instruction_on_titration && ( + + + + )} + {prescription.notes && ( - + )} {prescription.discontinued && ( {prescription.discontinued_reason} diff --git a/src/Components/Medicine/PrescriptionsTable.tsx b/src/Components/Medicine/PrescriptionsTable.tsx index a1b039e71dd..6cedc4ec29c 100644 --- a/src/Components/Medicine/PrescriptionsTable.tsx +++ b/src/Components/Medicine/PrescriptionsTable.tsx @@ -38,7 +38,11 @@ export default function PrescriptionsTable({ const { data } = useQuery(MedicineRoutes.listPrescriptions, { pathParams: { consultation }, - query: { is_prn, prescription_type, limit: 100 }, + query: { + dosage_type: is_prn ? "PRN" : "REGULAR,TITRATED", + prescription_type, + limit: 100, + }, }); const lastModified = data?.results[0]?.modified_date; @@ -193,8 +197,11 @@ export default function PrescriptionsTable({ min_hours_between_doses__pretty: obj.min_hours_between_doses && obj.min_hours_between_doses + " hour(s)", - last_administered__pretty: obj.last_administered_on ? ( - + last_administered__pretty: obj.last_administration + ?.administered_date ? ( + ) : ( "never" ), @@ -275,7 +282,7 @@ export default function PrescriptionsTable({ const COMMON_TKEYS = { medicine: "medicine", route: "route__pretty", - dosage: "dosage", + base_dosage: "base_dosage", }; const REGULAR_NORMAL_TKEYS = { diff --git a/src/Components/Medicine/PrescrpitionTimeline.tsx b/src/Components/Medicine/PrescrpitionTimeline.tsx index 03b9f746bba..27f997e2429 100644 --- a/src/Components/Medicine/PrescrpitionTimeline.tsx +++ b/src/Components/Medicine/PrescrpitionTimeline.tsx @@ -119,8 +119,9 @@ const MedicineAdministeredNode = ({ name="medicine" event={event} className={classNames(event.cancelled && "opacity-70")} - // TODO: to add administered dosage when Titrated Prescriptions are implemented - titleSuffix={`administered the medicine at ${formatTime( + titleSuffix={`administered ${ + event.administration.dosage + } dose of the medicine at ${formatTime( event.administration.administered_date )}.`} actions={ diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index b97e2c52252..1a5f6fe8a13 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -18,7 +18,10 @@ interface BasePrescription { medicine_object?: MedibaseMedicine; medicine_old?: string; route?: (typeof PRESCRIPTION_ROUTES)[number]; - dosage: DosageValue; + dosage_type?: "REGULAR" | "TITRATED" | "PRN"; + base_dosage?: DosageValue; + target_dosage?: DosageValue; + instruction_on_titration?: string; notes?: string; meta?: object; readonly prescription_type?: "DISCHARGE" | "REGULAR"; @@ -26,7 +29,7 @@ interface BasePrescription { discontinued_reason?: string; readonly prescribed_by: PerformedByModel; readonly discontinued_date: string; - readonly last_administered_on?: string; + readonly last_administration?: MedicineAdministrationRecord; readonly is_migrated: boolean; readonly created_date: string; readonly modified_date: string; @@ -44,7 +47,7 @@ export interface NormalPrescription extends BasePrescription { | "QOD" | "QWK"; days?: number; - is_prn: false; + dosage_type: "REGULAR" | "TITRATED"; indicator?: undefined; max_dosage?: undefined; min_hours_between_doses?: undefined; @@ -54,7 +57,7 @@ export interface PRNPrescription extends BasePrescription { indicator: string; max_dosage?: DosageValue; min_hours_between_doses?: number; - is_prn: true; + dosage_type: "PRN"; frequency?: undefined; days?: undefined; } @@ -65,6 +68,7 @@ export type MedicineAdministrationRecord = { readonly id: string; readonly prescription: Prescription; notes: string; + dosage?: string; administered_date?: string; readonly administered_by: PerformedByModel; readonly archived_by: PerformedByModel | undefined; diff --git a/src/Components/Medicine/validators.ts b/src/Components/Medicine/validators.ts index 40261646d05..5bc1335f2e1 100644 --- a/src/Components/Medicine/validators.ts +++ b/src/Components/Medicine/validators.ts @@ -6,10 +6,21 @@ export const PrescriptionFormValidator = () => { return (form: Prescription): FormErrors => { const errors: Partial> = {}; errors.medicine_object = RequiredFieldValidator()(form.medicine_object); - errors.dosage = RequiredFieldValidator()(form.dosage); - if (form.is_prn) + if (form.dosage_type === "TITRATED") { + errors.base_dosage = RequiredFieldValidator()(form.base_dosage); + errors.target_dosage = RequiredFieldValidator()(form.target_dosage); + if ( + form.base_dosage && + form.target_dosage && + form.base_dosage.split(" ")[1] !== form.target_dosage.split(" ")[1] + ) { + errors.base_dosage = "Unit must be same as target dosage's unit"; + errors.target_dosage = "Unit must be same as base dosage's unit"; + } + } else errors.base_dosage = RequiredFieldValidator()(form.base_dosage); + if (form.dosage_type === "PRN") errors.indicator = RequiredFieldValidator()(form.indicator); - if (!form.is_prn) + if (form.dosage_type !== "PRN") errors.frequency = RequiredFieldValidator()(form.frequency); return errors; }; @@ -31,10 +42,10 @@ const PRESCRIPTION_COMPARE_FIELDS: (keyof Prescription)[] = [ "medicine", "days", "discontinued", - "dosage", + "base_dosage", "frequency", "indicator", - "is_prn", + "dosage_type", "max_dosage", "min_hours_between_doses", "prescription_type", @@ -47,3 +58,30 @@ export const comparePrescriptions = (a: Prescription, b: Prescription) => { a.medicine_object?.id === b.medicine_object?.id ); }; + +export const AdministrationDosageValidator = ( + base_dosage: Prescription["base_dosage"], + target_dosage: Prescription["target_dosage"] +) => { + return (value: Prescription["base_dosage"]) => { + const getDosageValue = (dosage: string | undefined) => { + return dosage ? Number(dosage.split(" ")[0]) : undefined; + }; + + const valueDosage = getDosageValue(value); + const baseDosage = getDosageValue(base_dosage); + const targetDosage = getDosageValue(target_dosage); + + if (!valueDosage) return "This field is required"; + + if (value?.split(" ")[1] !== base_dosage?.split(" ")[1]) + return "Unit must be the same as start and target dosage's unit"; + + if ( + baseDosage && + targetDosage && + (valueDosage < baseDosage || valueDosage > targetDosage) + ) + return "Dosage should be between start and target dosage"; + }; +}; diff --git a/src/Locale/en/Medicine.json b/src/Locale/en/Medicine.json index f15bd7c802f..95ddda7c1fd 100644 --- a/src/Locale/en/Medicine.json +++ b/src/Locale/en/Medicine.json @@ -2,6 +2,10 @@ "medicine": "Medicine", "route": "Route", "dosage": "Dosage", + "start_dosage": "Start Dosage", + "target_dosage": "Target Dosage", + "instruction_on_titration": "Instruction on titration", + "titrate_dosage": "Titrate Dosage", "indicator": "Indicator", "inidcator_event": "Indicator Event", "max_dosage_24_hrs": "Max. dosage in 24 hrs.", @@ -54,4 +58,4 @@ "PRESCRIPTION_FREQUENCY_Q4H": "4th hourly", "PRESCRIPTION_FREQUENCY_QOD": "Alternate day", "PRESCRIPTION_FREQUENCY_QWK": "Once a week" -} \ No newline at end of file +}