diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6e760362cd6..dbeff965240 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,11 +4,13 @@ on: push: branches: - develop - - master + - staging + tags: + - v* pull_request: branches: - develop - - master + - staging workflow_dispatch: concurrency: @@ -29,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-${{ hashFiles('package-lock.json', 'Dockerfile') }} + key: ${{ runner.os }}-buildx-test-${{ hashFiles('package-lock.json', 'Dockerfile') }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ 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 @@ -60,132 +63,89 @@ 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' + 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: - - 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- + ${{ runner.os }}-buildx-build- - - 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 }} - - - 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 +191,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 +237,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 +283,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 +329,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 +375,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 +421,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: diff --git a/Dockerfile b/Dockerfile index 1aed8202330..d65fe6e29c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ #build-stage -FROM node:18-buster-slim as build-stage +FROM --platform=$BUILDPLATFORM node:18-buster-slim as build-stage 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 diff --git a/cypress/e2e/users_spec/user_homepage.cy.ts b/cypress/e2e/users_spec/user_homepage.cy.ts index 3a633bd65a4..1a0bec587a7 100644 --- a/cypress/e2e/users_spec/user_homepage.cy.ts +++ b/cypress/e2e/users_spec/user_homepage.cy.ts @@ -28,6 +28,7 @@ describe("User Homepage", () => { userPage.typeInFirstName("Dev"); userPage.typeInLastName("Doctor"); userPage.selectRole("Doctor"); + userPage.selectState("Kerala"); userPage.selectDistrict("Ernakulam"); userPage.typeInPhoneNumber(phone_number); userPage.typeInAltPhoneNumber(alt_phone_number); diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 12ac64c51b3..6f83718e9b8 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -29,7 +29,7 @@ export class UserPage { .and("include", "phone_number=%2B919876543219") .and("include", "alt_phone_number=%2B919876543219") .and("include", "user_type=Doctor") - .and("include", "district_id=7"); + .and("include", "district=7"); } checkUsernameText(username: string) { @@ -62,9 +62,12 @@ export class UserPage { cy.get("[role='option']").contains(role).click(); } + selectState(state: string) { + cy.searchAndSelectOption("#state input", state); + } + selectDistrict(district: string) { - cy.get("input[name='district']").click().type(district); - cy.get("[role='option']").contains(district).click(); + cy.searchAndSelectOption("#district input", district); } typeInPhoneNumber(phone: string) { diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts index 898da28f3ad..23f9e7b70c7 100644 --- a/src/Common/hooks/useMSEplayer.ts +++ b/src/Common/hooks/useMSEplayer.ts @@ -1,5 +1,4 @@ import { useEffect, useRef } from "react"; -import axios from "axios"; export interface IAsset { middlewareHostname: string; @@ -45,9 +44,18 @@ const stopStream = (payload: { id: string }, options: IOptions) => { const { id } = payload; ws?.close(); - axios - .post(`https://${middlewareHostname}/stop`, { - id, + fetch(`https://${middlewareHostname}/stop`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }) + .then((res) => { + if (!res.ok) { + throw new Error("network response was not ok"); + } + return res.json(); }) .then((res) => options?.onSuccess && options.onSuccess(res)) .catch((err) => options.onError && options.onError(err)); diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 86b7199cdac..ba023788496 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { AssetData, ResolvedMiddleware } from "../AssetTypes"; import * as Notification from "../../../Utils/Notifications.js"; import { BedModel } from "../../Facility/models"; -import axios from "axios"; import { getCameraConfig } from "../../../Utils/transformUtils"; import CameraConfigure from "../configure/CameraConfigure"; import Loading from "../../Common/Loading"; @@ -100,13 +99,18 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { }; try { setLoadingAddPreset(true); - const presetData = await axios.get( + + const response = await fetch( `https://${resolvedMiddleware?.hostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}` ); + if (!response.ok) { + throw new Error("Network error"); + } + const presetData = await response.json(); const { res } = await request(routes.createAssetBed, { body: { - meta: { ...data, ...presetData.data }, + meta: { ...data, ...presetData }, asset: assetId, bed: bed?.id as string, }, diff --git a/src/Components/Common/DateInputV2.tsx b/src/Components/Common/DateInputV2.tsx index da974fbe0e9..bce5f24b11c 100644 --- a/src/Components/Common/DateInputV2.tsx +++ b/src/Components/Common/DateInputV2.tsx @@ -159,6 +159,22 @@ const DateInputV2: React.FC = ({ return true; }; + const isDateWithinLimits = (parsedDate: dayjs.Dayjs): boolean => { + if (parsedDate?.isValid()) { + if ( + (max && parsedDate.toDate() > max) || + (min && parsedDate.toDate() < min) + ) { + Notification.Error({ + msg: outOfLimitsErrorMessage ?? "Cannot select date out of range", + }); + return false; + } + return true; + } + return false; + }; + const isSelectedMonth = (month: number) => month === datePickerHeaderDate.getMonth(); @@ -261,7 +277,7 @@ const DateInputV2: React.FC = ({ onChange={(e) => { setDisplayValue(e.target.value.replaceAll("/", "")); const value = dayjs(e.target.value, "DD/MM/YYYY", true); - if (value.isValid()) { + if (isDateWithinLimits(value)) { onChange(value.toDate()); close(); setIsOpen?.(false); diff --git a/src/Components/Facility/ConsultationCard.tsx b/src/Components/Facility/ConsultationCard.tsx index 10aa4c295af..96728c6a2e3 100644 --- a/src/Components/Facility/ConsultationCard.tsx +++ b/src/Components/Facility/ConsultationCard.tsx @@ -6,172 +6,211 @@ import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import RelativeDateUserMention from "../Common/RelativeDateUserMention"; import useConfig from "../../Common/hooks/useConfig"; import Chip from "../../CAREUI/display/Chip"; +import * as Notification from "../../Utils/Notifications.js"; +import { useState } from "react"; +import DialogModal from "../Common/Dialog.js"; +import Beds from "./Consultations/Beds"; interface ConsultationProps { itemData: ConsultationModel; isLastConsultation?: boolean; + refetch: () => void; } export const ConsultationCard = (props: ConsultationProps) => { - const { itemData, isLastConsultation } = props; + const { itemData, isLastConsultation, refetch } = props; const { kasp_string } = useConfig(); + const [open, setOpen] = useState(false); + const bedDialogTitle = itemData.discharge_date + ? "Bed History" + : !itemData.current_bed + ? "Assign Bed" + : "Switch Bed"; return ( -
- {itemData.is_kasp && ( -
- {kasp_string} -
- )} + <> + setOpen(false)} + className="md:max-w-3xl" + > + {itemData.facility && itemData.patient && itemData.id ? ( + + ) : ( +
Invalid Patient Data
+ )} +
+
+ {itemData.is_kasp && ( +
+ {kasp_string} +
+ )} -
-
+
-
- Facility -
-
- {itemData.facility_name}{" "} - {itemData.is_telemedicine && ( - (Telemedicine) - )} -
-
-
-
-
- Suggestion{" "} + Facility
- {itemData.suggestion_text?.toLocaleLowerCase()} + {itemData.facility_name}{" "} + {itemData.is_telemedicine && ( + (Telemedicine) + )}
-
- {itemData.kasp_enabled_date && (
-
-
- {kasp_string} Enabled date{" "} -
-
- {itemData.kasp_enabled_date - ? formatDateTime(itemData.kasp_enabled_date) - : "-"} +
+
+
+ Suggestion{" "} +
+
+ {itemData.suggestion_text?.toLocaleLowerCase()} +
- )} - {itemData.admitted && itemData.encounter_date && ( -
+ {itemData.kasp_enabled_date && (
-
- Admitted on -
-
- {formatDateTime(itemData.encounter_date)} - {itemData.is_readmission && ( - - )} +
+
+ {kasp_string} Enabled date{" "} +
+
+ {itemData.kasp_enabled_date + ? formatDateTime(itemData.kasp_enabled_date) + : "-"} +
-
- )} - {!itemData.admitted && ( -
+ )} + {itemData.admitted && itemData.encounter_date && (
-
- Admitted{" "} +
+
+ Admitted on +
+
+ {formatDateTime(itemData.encounter_date)} + {itemData.is_readmission && ( + + )} +
-
- No +
+ )} + {!itemData.admitted && ( +
+
+
+ Admitted{" "} +
+
+ No +
-
- )} - {itemData.discharge_date && ( -
+ )} + {itemData.discharge_date && (
-
- Discharged on{" "} +
+
+ Discharged on{" "} +
+
+ {formatDateTime(itemData.discharge_date)} +
-
- {formatDateTime(itemData.discharge_date)} +
+ )} +
+
+ { +
+
Created :
+
+
-
- )} -
-
- { + }
-
Created :
+
Last Modified :
- } -
-
Last Modified :
-
- -
-
-
- - navigate( - `/facility/${itemData.facility}/patient/${itemData.patient}/consultation/${itemData.id}` - ) - } - > - View Consultation / Consultation Updates - - - navigate( - `/facility/${itemData.facility}/patient/${itemData.patient}/consultation/${itemData.id}/files/` - ) - } - > - View / Upload Consultation Files - - {isLastConsultation && ( +
navigate( - `/facility/${itemData.facility}/patient/${itemData.patient}/consultation/${itemData.id}/daily-rounds` + `/facility/${itemData.facility}/patient/${itemData.patient}/consultation/${itemData.id}` ) } - disabled={ - (itemData.discharge_date as string | undefined) != undefined + > + View Consultation / Consultation Updates + + + navigate( + `/facility/${itemData.facility}/patient/${itemData.patient}/consultation/${itemData.id}/files/` + ) } - authorizeFor={NonReadOnlyUsers} > - Add Consultation Updates + View / Upload Consultation Files - )} + {isLastConsultation && ( + { + if (itemData.admitted && !itemData.current_bed) { + Notification.Error({ + msg: "Please assign a bed to the patient", + }); + setOpen(true); + } else { + navigate( + `/facility/${itemData.facility}/patient/${itemData.patient}/consultation/${itemData.id}/daily-rounds` + ); + } + }} + disabled={itemData.discharge_date} + authorizeFor={NonReadOnlyUsers} + > + Add Consultation Updates + + )} +
-
+ ); }; diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index a063ce91a8d..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) ); @@ -1457,7 +1456,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { className="col-span-6" {...field("is_telemedicine")} value={JSON.parse(state.form.is_telemedicine)} - label="Is Telemedicine required for the patient?" + label="Would you like to refer the patient for remote monitoring to an external doctor?" /> {JSON.parse(state.form.is_telemedicine) && ( diff --git a/src/Components/Facility/CoverImageEditModal.tsx b/src/Components/Facility/CoverImageEditModal.tsx index 8de463ad9b5..5e6e505617f 100644 --- a/src/Components/Facility/CoverImageEditModal.tsx +++ b/src/Components/Facility/CoverImageEditModal.tsx @@ -1,4 +1,3 @@ -import axios from "axios"; import { ChangeEventHandler, useCallback, @@ -20,6 +19,7 @@ import { LocalStorageKeys } from "../../Common/constants"; import DialogModal from "../Common/Dialog"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; +import uploadFile from "../../Utils/request/uploadFile"; interface Props { open: boolean; onClose: (() => void) | undefined; @@ -105,34 +105,35 @@ const CoverImageEditModal = ({ const formData = new FormData(); formData.append("cover_image", selectedFile); - + const url = `/api/v1/facility/${facility.id}/cover_image/`; setIsUploading(true); - try { - const response = await axios.post( - `/api/v1/facility/${facility.id}/cover_image/`, - formData, - { - headers: { - "Content-Type": "multipart/form-data", - Authorization: - "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), - }, + + uploadFile( + url, + formData, + "POST", + { + Authorization: + "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), + }, + (xhr: XMLHttpRequest) => { + if (xhr.status === 200) { + Success({ msg: "Cover image updated." }); + } else { + Notification.Error({ + msg: "Something went wrong!", + }); + setIsUploading(false); } - ); - if (response.status === 200) { - Success({ msg: "Cover image updated." }); - } else { + }, + null, + () => { Notification.Error({ - msg: "Something went wrong!", + msg: "Network Failure. Please check your internet connectivity.", }); setIsUploading(false); } - } catch (e) { - Notification.Error({ - msg: "Network Failure. Please check your internet connectivity.", - }); - setIsUploading(false); - } + ); await sleep(1000); setIsUploading(false); diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index f8d6583f24c..1b1ee8f67dd 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -1,4 +1,3 @@ -import axios from "axios"; import CircularProgress from "../Common/components/CircularProgress"; import { useCallback, @@ -33,6 +32,7 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import FilePreviewDialog from "../Common/FilePreviewDialog"; +import uploadFile from "../../Utils/request/uploadFile"; const Loading = lazy(() => import("../Common/Loading")); @@ -936,42 +936,41 @@ export const FileUpload = (props: FileUploadProps) => { const f = file; if (!f) return; const newFile = new File([f], `${internal_name}`); - - const config = { - headers: { - "Content-type": file?.type, - "Content-disposition": "inline", - }, - onUploadProgress: (progressEvent: any) => { - const percentCompleted = Math.round( - (progressEvent.loaded * 100) / progressEvent.total - ); - setUploadPercent(percentCompleted); - }, - }; - + console.log("filetype: ", newFile.type); return new Promise((resolve, reject) => { - axios - .put(url, newFile, config) - .then(() => { - setUploadStarted(false); - // setUploadSuccess(true); - setFile(null); - setUploadFileName(""); - fetchData(); - Notification.Success({ - msg: "File Uploaded Successfully", - }); - setUploadFileError(""); - resolve(); - }) - .catch((e) => { + uploadFile( + url, + newFile, + "PUT", + { "Content-Type": file?.type }, + (xhr: XMLHttpRequest) => { + if (xhr.status >= 200 && xhr.status < 300) { + setUploadStarted(false); + setFile(null); + setUploadFileName(""); + fetchData(); + Notification.Success({ + msg: "File Uploaded Successfully", + }); + setUploadFileError(""); + resolve(); + } else { + Notification.Error({ + msg: "Error Uploading File: " + xhr.statusText, + }); + setUploadStarted(false); + reject(); + } + }, + setUploadPercent, + () => { Notification.Error({ - msg: "Error Uploading File: " + e.message, + msg: "Error Uploading File: Network Error", }); setUploadStarted(false); reject(); - }); + } + ); }); }; @@ -1049,33 +1048,30 @@ export const FileUpload = (props: FileUploadProps) => { const f = audioBlob; if (f === undefined) return; const newFile = new File([f], `${internal_name}`, { type: f.type }); - const config = { - headers: { - "Content-type": newFile?.type, - "Content-disposition": "inline", - }, - onUploadProgress: (progressEvent: any) => { - const percentCompleted = Math.round( - (progressEvent.loaded * 100) / progressEvent.total - ); - setUploadPercent(percentCompleted); - }, - }; - axios - .put(url, newFile, config) - .then(() => { - setAudioUploadStarted(false); - // setUploadSuccess(true); - setAudioName(""); - fetchData(); - Notification.Success({ - msg: "File Uploaded Successfully", - }); - }) - .catch(() => { + uploadFile( + url, + newFile, + "PUT", + { "Content-Type": newFile?.type }, + (xhr: XMLHttpRequest) => { + if (xhr.status >= 200 && xhr.status < 300) { + setAudioUploadStarted(false); + // setUploadSuccess(true); + setAudioName(""); + fetchData(); + Notification.Success({ + msg: "File Uploaded Successfully", + }); + } else { + setAudioUploadStarted(false); + } + }, + setUploadPercent, + () => { setAudioUploadStarted(false); - }); + } + ); }; const validateAudioUpload = () => { diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index b438c21a76a..841a8d31ddf 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -1375,6 +1375,7 @@ export const PatientHome = (props: any) => { isLastConsultation={ item.id == patientData.last_consultation?.id } + refetch={refetch} /> )} diff --git a/src/Components/Patient/UpdateStatusDialog.tsx b/src/Components/Patient/UpdateStatusDialog.tsx index c302aa1b957..064e19ff2ee 100644 --- a/src/Components/Patient/UpdateStatusDialog.tsx +++ b/src/Components/Patient/UpdateStatusDialog.tsx @@ -1,5 +1,4 @@ import { useEffect, useState, useReducer } from "react"; -import axios from "axios"; import { SAMPLE_TEST_STATUS, SAMPLE_TEST_RESULT, @@ -18,6 +17,7 @@ import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import { useTranslation } from "react-i18next"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; +import uploadFile from "../../Utils/request/uploadFile"; interface Props { sample: SampleTestModel; @@ -104,36 +104,36 @@ const UpdateStatusDialog = (props: Props) => { if (f === undefined) return; const newFile = new File([f], `${internal_name}`); - const config = { - headers: { - "Content-type": contentType, + uploadFile( + url, + newFile, + "PUT", + { + "Content-Type": contentType, "Content-disposition": "inline", }, - onUploadProgress: (progressEvent: any) => { - const percentCompleted = Math.round( - (progressEvent.loaded * 100) / progressEvent.total - ); - setUploadPercent(percentCompleted); + (xhr: XMLHttpRequest) => { + if (xhr.status >= 200 && xhr.status < 300) { + setUploadStarted(false); + setUploadDone(true); + request(routes.editUpload, { + pathParams: { + id: data.id, + fileType: "SAMPLE_MANAGEMENT", + associatingId: sample.id?.toString() ?? "", + }, + body: { upload_completed: true }, + }); + Notification.Success({ msg: "File Uploaded Successfully" }); + } else { + setUploadStarted(false); + } }, - }; - - axios - .put(url, newFile, config) - .then(() => { + setUploadPercent, + () => { setUploadStarted(false); - setUploadDone(true); - request(routes.editUpload, { - pathParams: { - id: data.id, - fileType: "SAMPLE_MANAGEMENT", - associatingId: sample.id?.toString() ?? "", - }, - body: { upload_completed: true }, - }); - - Notification.Success({ msg: "File Uploaded Successfully" }); - }) - .catch(() => setUploadStarted(false)); + } + ); }; const onFileChange = (e: React.ChangeEvent) => { diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index cb0caaec950..dbfca3a815a 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { navigate } from "raviger"; -import { lazy, useState } from "react"; +import { lazy, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import CountBlock from "../../CAREUI/display/Count"; import CareIcon from "../../CAREUI/icons/CareIcon"; @@ -73,8 +73,7 @@ export default function ManageUsers() { const [weeklyHoursError, setWeeklyHoursError] = useState(""); const extremeSmallScreenBreakpoint = 320; - const isExtremeSmallScreen = - width <= extremeSmallScreenBreakpoint ? true : false; + const isExtremeSmallScreen = width <= extremeSmallScreenBreakpoint; const { data: userListData, @@ -92,15 +91,24 @@ export default function ManageUsers() { phone_number: qParams.phone_number, alt_phone_number: qParams.alt_phone_number, user_type: qParams.user_type, - district_id: qParams.district_id, + district_id: qParams.district, }, }); + useEffect(() => { + if (!qParams.state && qParams.district) { + advancedFilter.removeFilters(["district"]); + } + if (!qParams.district && qParams.state) { + advancedFilter.removeFilters(["state"]); + } + }, [advancedFilter, qParams]); + const { data: districtData, loading: districtDataLoading } = useQuery( routes.getDistrict, { - prefetch: !!qParams.district_id, - pathParams: { id: qParams.district_id }, + prefetch: !!qParams.district, + pathParams: { id: qParams.district }, } ); @@ -535,8 +543,8 @@ export default function ManageUsers() { badge("Role", "user_type"), value( "District", - "district_id", - qParams.district_id ? districtData?.name || "" : "" + "district", + qParams.district ? districtData?.name || "" : "" ), ]} /> diff --git a/src/Components/Users/UserFilter.tsx b/src/Components/Users/UserFilter.tsx index f2ce3be914c..d89a79c03ee 100644 --- a/src/Components/Users/UserFilter.tsx +++ b/src/Components/Users/UserFilter.tsx @@ -1,4 +1,3 @@ -import DistrictSelect from "../Facility/FacilityFilter/DistrictSelect"; import { parsePhoneNumber } from "../../Utils/utils"; import TextFormField from "../Form/FormFields/TextFormField"; import SelectMenuV2 from "../Form/SelectMenuV2"; @@ -7,8 +6,10 @@ import { USER_TYPE_OPTIONS } from "../../Common/constants"; import useMergeState from "../../Common/hooks/useMergeState"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; -import useQuery from "../../Utils/request/useQuery"; -import routes from "../../Redux/api"; +import DistrictAutocompleteFormField from "../Common/DistrictAutocompleteFormField"; +import StateAutocompleteFormField from "../Common/StateAutocompleteFormField"; +import { useTranslation } from "react-i18next"; +import * as Notify from "../../Utils/Notifications"; const parsePhoneNumberForFilterParam = (phoneNumber: string) => { if (!phoneNumber) return ""; @@ -18,6 +19,7 @@ const parsePhoneNumberForFilterParam = (phoneNumber: string) => { }; export default function UserFilter(props: any) { + const { t } = useTranslation(); const { filter, onChange, closeFilter, removeFilters } = props; const [filterState, setFilterState] = useMergeState({ first_name: filter.first_name || "", @@ -25,17 +27,10 @@ export default function UserFilter(props: any) { phone_number: filter.phone_number || "+91", alt_phone_number: filter.alt_phone_number || "+91", user_type: filter.user_type || "", - district_id: filter.district_id || "", - district_ref: null, + district: filter.district || "", + state: filter.state || "", }); - const setDistrict = (selected: any) => { - const filterData: any = { ...filterState }; - filterData["district_ref"] = selected; - filterData["district_id"] = (selected || {}).id; - setFilterState(filterData); - }; - const applyFilter = () => { const { first_name, @@ -43,7 +38,8 @@ export default function UserFilter(props: any) { phone_number, alt_phone_number, user_type, - district_id, + district, + state, } = filterState; const data = { first_name: first_name || "", @@ -51,22 +47,30 @@ export default function UserFilter(props: any) { phone_number: parsePhoneNumberForFilterParam(phone_number), alt_phone_number: parsePhoneNumberForFilterParam(alt_phone_number), user_type: user_type || "", - district_id: district_id || "", + district: district || "", + state: district ? state || "" : "", }; + if (state && !district) { + Notify.Warn({ + msg: "District is required when state is selected", + }); + return; + } onChange(data); }; - useQuery(routes.getDistrict, { - prefetch: !!filter.district_id, - pathParams: { id: filter.district_id }, - onResponse: (result) => { - if (!result || !result.data || !result.res) return; - setFilterState({ district_ref: result.data }); - }, - }); + const handleChange = ({ name, value }: any) => { + if (name === "state" && !value) + setFilterState({ ...filterState, state: value, district: undefined }); + else setFilterState({ ...filterState, [name]: value }); + }; - const handleChange = ({ name, value }: any) => - setFilterState({ ...filterState, [name]: value }); + const field = (name: string) => ({ + name, + label: t(name), + value: filterState[name], + onChange: handleChange, + }); return (
-
- District - -
-
+ + +
void, + setUploadPercent: Dispatch> | null, + onError: () => void +) => { + const xhr = new XMLHttpRequest(); + xhr.open(reqMethod, url); + + Object.entries(headers).forEach(([key, value]) => { + xhr.setRequestHeader(key, value); + }); + + xhr.onload = () => { + onLoad(xhr); + }; + + if (setUploadPercent != null) { + xhr.upload.onprogress = (event: ProgressEvent) => { + handleUploadPercentage(event, setUploadPercent); + }; + } + + xhr.onerror = () => { + onError(); + }; + xhr.send(file); +}; + +export default uploadFile; diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index 9b98e1ae3bf..bd715f818c3 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -1,3 +1,4 @@ +import { Dispatch, SetStateAction } from "react"; import { LocalStorageKeys } from "../../Common/constants"; import * as Notification from "../Notifications"; import { QueryParams, RequestOptions } from "./types"; @@ -96,3 +97,13 @@ export function mergeRequestOptions( silent: overrides.silent ?? options.silent, }; } + +export function handleUploadPercentage( + event: ProgressEvent, + setUploadPercent: Dispatch> +) { + if (event.lengthComputable) { + const percentComplete = Math.round((event.loaded / event.total) * 100); + setUploadPercent(percentComplete); + } +}