diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 0e4c9005787..8964b0c8e57 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -13,9 +13,9 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-label: "stale" stale-pr-label: "stale" - stale-issue-message: "Hi, @gigincg, @nihal467, @khavinshankar, @mathew-alex, @aparnacoronasafe, This issue has been automatically marked as stale because it has not had any recent activity." + stale-issue-message: "Hi, @coronasafe/care-frontend-maintainers, This issue has been automatically marked as stale because it has not had any recent activity." stale-pr-message: "Hi, This pr has been automatically marked as stale because it has not had any recent activity. It will be automatically closed if no further activity occurs for 7 more days. Thank you for your contributions." - close-pr-message: "Hi, @gigincg, @nihal467, @khavinshankar, @mathew-alex, This pr has been automatically closed because it has not had any recent activity. Thank you for your contributions. Feel free to repopen the pr." + close-pr-message: "Hi, @coronasafe/care-frontend-maintainers, This PR has been automatically closed due to inactivity. Thank you for your contributions. Feel free to re-open the PR." exempt-issue-labels: "blocked,waiting for related PR,waiting for back end,help wanted,work-in-progress,In Progress,wishlist,EPIC" exempt-pr-labels: "tested,needs testing,need Review,waiting for related PR,waiting for back end,help wanted,blocked,work-in-progress,In Progress" days-before-issue-stale: 14 diff --git a/README.md b/README.md index 6e36f52b3f1..dbb306a1bd8 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,33 @@ Authenticate to staging API with any of the following credentials #### 🧪 Run cypress tests -Ensure that the development server is running and then run the cypress tests in either of the ways described below. +To run cypress tests locally, you'll need to setup the backend to run locally and load dummy data required for cypress to the database. See [docs](https://github.com/coronasafe/care#self-hosting). + +Once backend is running locally, you'll have to ensure your local front-end is connected to local backend, by setting the `CARE_API` env. + +```env +#.env +CARE_API=http://127.0.0.1:9000 +``` + +Once done, start the development server by running + +```sh +npm run dev +``` + +Once development server is running, then run the cypress tests in either of the ways described below. + +```sh +npm run cypress:run # To run all tests in headless mode. +``` + +```sh +npm run cypress:run:gui # To run all tests in headed mode. +``` ```sh -$ npm run cypress:run # To run all tests in headless mode. -$ npm run cypress:run:gui # To run all tests in headed mode. -$ npm run cypress:open # To debug and run tests individually. +npm run cypress:open # To debug and run tests individually. ``` - Failed test screenshots are saved in `cypress/screenshots` diff --git a/cypress/e2e/users_spec/user_manage.cy.ts b/cypress/e2e/users_spec/user_manage.cy.ts index af3b9149b2f..0ff66fba27a 100644 --- a/cypress/e2e/users_spec/user_manage.cy.ts +++ b/cypress/e2e/users_spec/user_manage.cy.ts @@ -33,7 +33,7 @@ describe("Manage User", () => { cy.awaitUrl("/users"); }); - it("linking skills for a users and verify its reflection in profile", () => { + it("linking skills for users and verify its reflection in profile", () => { // select the district user and select one skill link and verify its profile reflection userPage.typeInSearchInput(usernameforworkinghour); userPage.checkUsernameText(usernameforworkinghour); @@ -49,7 +49,7 @@ describe("Manage User", () => { manageUserPage.navigateToProfile(); userCreationPage.verifyElementContainsText( "username-profile-details", - usernameforworkinghour + usernameforworkinghour, ); manageUserPage.assertSkillInAlreadyLinkedSkills(linkedskill); }); diff --git a/src/Components/ExternalResult/ResultList.tsx b/src/Components/ExternalResult/ResultList.tsx index 9d34b397026..0759b47d73a 100644 --- a/src/Components/ExternalResult/ResultList.tsx +++ b/src/Components/ExternalResult/ResultList.tsx @@ -194,7 +194,7 @@ export default function ResultList() { }); } - if (loading) { + if (loading || !data) { manageResults = ( diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 6a33d1d1bdf..e0c79823434 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -64,6 +64,7 @@ import ConfirmDialog from "../Common/ConfirmDialog.js"; import request from "../../Utils/request/request.js"; import routes from "../../Redux/api.js"; import useQuery from "../../Utils/request/useQuery.js"; +import { t } from "i18next"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); @@ -1477,7 +1478,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { > i.text == "Expired")?.id && - !preDischargeForm.discharge_notes.trim() + DISCHARGE_REASONS.find((i) => i.text == "Expired")?.id ) { - setErrors({ - ...errors, - discharge_notes: "Please enter the cause of death", - }); - setIsSendingDischargeApi(false); - return; + const newErrors: Record = {}; + + if (!preDischargeForm.discharge_notes.trim()) { + newErrors["discharge_notes"] = "Please enter the cause of death"; + } + if (!preDischargeForm.death_confirmed_doctor?.trim()) { + newErrors["death_confirmed_doctor"] = "Field is required"; + } + + if (Object.entries(newErrors).length) { + setErrors({ ...errors, ...newErrors }); + setIsSendingDischargeApi(false); + return; + } } const dischargeDetails = { @@ -310,6 +318,7 @@ const DischargeModal = ({ { setPreDischargeForm((form) => { diff --git a/src/Components/Facility/DoctorVideoSlideover.tsx b/src/Components/Facility/DoctorVideoSlideover.tsx index 5fde98364da..d2591bf3600 100644 --- a/src/Components/Facility/DoctorVideoSlideover.tsx +++ b/src/Components/Facility/DoctorVideoSlideover.tsx @@ -1,79 +1,57 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import SlideOver from "../../CAREUI/interactive/SlideOver"; import { UserAssignedModel } from "../Users/models"; import { SkillObjectModel } from "../Users/models"; import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon"; -import { relativeTime } from "../../Utils/utils"; +import { classNames, relativeTime } from "../../Utils/utils"; import useAuthUser from "../../Common/hooks/useAuthUser"; import { triggerGoal } from "../../Integrations/Plausible"; import { Warn } from "../../Utils/Notifications"; import Switch from "../../CAREUI/interactive/Switch"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; +import Loading from "../Common/Loading"; -enum FilterTypes { - ALL = "All", - DOCTOR = "Doctor", - NURSE = "Nurse", - TELEICU = "TeleICU Hub", -} +const UserGroups = { + ALL: "All", + DOCTOR: "Doctor", + NURSE: "Nurse", + TELEICU: "TeleICU Doctor", +}; -interface DoctorDetails { - users: UserAssignedModel[]; - type: FilterTypes.DOCTOR | FilterTypes.NURSE | FilterTypes.TELEICU; -} +type UserGroup = keyof typeof UserGroups; + +type UserAnnotatedWithGroup = UserAssignedModel & { group?: UserGroup }; const isHomeUser = (user: UserAssignedModel, facilityId: string) => user.home_facility_object?.id === facilityId; - export default function DoctorVideoSlideover(props: { show: boolean; facilityId: string; setShow: (show: boolean) => void; }) { const { show, facilityId, setShow } = props; - const [filteredDoctors, setFilteredDoctors] = useState([]); - const [filter, setFilter] = useState(FilterTypes.ALL); + const [filter, setFilter] = useState("ALL"); - const { data: users, loading } = useQuery(routes.getFacilityUsers, { + const { data } = useQuery(routes.getFacilityUsers, { prefetch: show, pathParams: { facility_id: facilityId }, query: { limit: 50 }, }); - useEffect(() => { - const filterDoctors = (users: UserAssignedModel[]) => { - const filteredusers = users - .filter((user: UserAssignedModel) => { - return user.alt_phone_number || user.video_connect_link; - }) - .sort((a: UserAssignedModel, b: UserAssignedModel) => { - const aIsHomeUser = isHomeUser(a, facilityId); - const bIsHomeUser = isHomeUser(b, facilityId); - return aIsHomeUser === bIsHomeUser ? 0 : aIsHomeUser ? -1 : 1; - }); - const Doctors = filteredusers.filter((user: UserAssignedModel) => { - return user.user_type === "Doctor" && isHomeUser(user, facilityId); - }); - const Nurses = filteredusers.filter((user: UserAssignedModel) => { - return user.user_type === "Nurse" && isHomeUser(user, facilityId); - }); - const TeleICU = filteredusers.filter((user: UserAssignedModel) => { - return ( - (user.user_type === "Doctor" || user.user_type === "Nurse") && - !isHomeUser(user, facilityId) - ); - }); - setFilteredDoctors([ - { users: Doctors, type: FilterTypes.DOCTOR }, - { users: Nurses, type: FilterTypes.NURSE }, - { users: TeleICU, type: FilterTypes.TELEICU }, - ]); - }; - if (users?.results && !loading) { - filterDoctors(users?.results); + const getUserGroup = (user: UserAssignedModel) => { + if (isHomeUser(user, facilityId)) { + if (user.user_type === "Doctor") return "DOCTOR"; + if (user.user_type === "Nurse") return "NURSE"; } - }, [facilityId, filter, loading, users?.results]); + + if (user.user_type === "Doctor") return "TELEICU"; + }; + + const annotatedUsers: UserAnnotatedWithGroup[] | undefined = data?.results + .filter((user) => user.alt_phone_number || user.video_connect_link) + .map((user) => ({ ...user, group: getUserGroup(user) })) + .filter((user) => !!user.group) as UserAnnotatedWithGroup[]; return (
({ ...acc, [type]: type }), - {}, - ) as Record - } + tabs={UserGroups} selected={filter} onChange={(tab) => setFilter(tab)} size="md" />
- {filteredDoctors.map((doctor) => { - return ( - (filter === FilterTypes.ALL || filter === doctor.type) && ( -
-

{doctor.type}

- {doctor.users.map((user, i) => { - return ( -
-
    - -
-
- ); - })} -
- ) - ); - })} + + {!annotatedUsers ? ( + + ) : filter === "ALL" ? ( +
+ + + + + +
+ ) : ( +
+ +
+ )}
); } +const UserGroupList = (props: { + users: UserAnnotatedWithGroup[]; + group: UserGroup; + showGroupHeading?: boolean; +}) => { + const users = props.users.filter((user) => user.group === props.group); + + return ( +
+ {props.showGroupHeading && ( +
+ + {UserGroups[props.group]} + +
+
+ )} + + {!users.length && ( + + No users in this category + + )} + + {!!users.length && ( +
    + {users.map((user) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +}; + type MSLaunchURI = ( uri: string, successCB?: null | (() => void), noHandlerCB?: null | (() => void), ) => void; -function UserListItem(props: { user: UserAssignedModel; facilityId: string }) { - const user = props.user; - const facilityId = props.facilityId; +function UserListItem({ user }: { user: UserAnnotatedWithGroup }) { const icon: IconName = user.user_type === "Doctor" ? "l-user-md" : "l-user-nurse"; @@ -207,17 +217,13 @@ function UserListItem(props: { user: UserAssignedModel; facilityId: string }) { } return ( -
  • @@ -232,16 +238,11 @@ function UserListItem(props: { user: UserAssignedModel; facilityId: string }) { }
  • +
    ); } diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index fa712e0df59..09c5c1889dc 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -411,7 +411,9 @@ export const FacilityHome = (props: any) => { Central Nursing Station - + {CameraFeedPermittedUserTypes.includes(authUser.user_type) && ( + + )} { const LiveMonitoringButton = () => { const facilityId = useSlug("facility"); const [location, setLocation] = useState(); - const authUser = useAuthUser(); return ( - {CameraFeedPermittedUserTypes.includes(authUser.user_type) && ( - - - - Live Monitoring - - - )} + + + + Live Monitoring + + key} /> {field("dosage_type").value === "TITRATED" ? ( -
    +
    key} /> {field("dosage_type").value === "TITRATED" ? ( -
    +
    {t("edit_caution_note")}
    } - className="w-full max-w-3xl lg:min-w-[600px]" + className="w-full max-w-4xl lg:min-w-[768px]" > {t("modification_caution_note")}
    } - className="w-full max-w-3xl lg:min-w-[600px]" + className="w-full max-w-4xl lg:min-w-[768px]" > targetDosage) - ) - return "Dosage should be between start and target dosage"; + if (baseDosage && targetDosage) { + const [min, max] = [baseDosage, targetDosage].sort((a, b) => a - b); + + if (!(min <= valueDosage && valueDosage <= max)) { + return "Dosage should be between start and target dosage"; + } + } }; }; diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 51baa4d1a28..35a5bbfea02 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -105,7 +105,6 @@ export const PatientManager = () => { const [diagnoses, setDiagnoses] = useState([]); const [showDialog, setShowDialog] = useState<"create" | "list-discharged">(); const [showDoctors, setShowDoctors] = useState(false); - const [showDoctorConnect, setShowDoctorConnect] = useState(false); const [phone_number, setPhoneNumber] = useState(""); const [phoneNumberError, setPhoneNumberError] = useState(""); const [emergency_phone_number, setEmergencyPhoneNumber] = useState(""); @@ -248,12 +247,6 @@ export const PatientManager = () => { qParams.diagnoses_differential, ]); - useEffect(() => { - if (params.facility) { - setShowDoctorConnect(true); - } - }, [qParams.facility]); - const date_range_fields = [ [params.created_date_before, params.created_date_after], [params.modified_date_before, params.modified_date_after], @@ -785,7 +778,7 @@ export const PatientManager = () => { }} isTab2Active={!!tabValue} /> - {showDoctorConnect && ( + {!!params.facility && ( { diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index cb73739b3ba..e746184ee2a 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -451,7 +451,7 @@ export default function PatientInfoCard(props: { consultation?.deprecated_verified_by) && (
    - Treating Physician:{" "} + {t("treating_doctor")}:{" "} {consultation?.treating_physician_object ? `${consultation?.treating_physician_object.first_name} ${consultation?.treating_physician_object.last_name}` diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index b4fb9e8b10a..76940fefc79 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -465,7 +465,7 @@ export default function ManageUsers() { open={expandFacilityList} setOpen={setExpandFacilityList} slideFrom="right" - title={t("facilities")} + title={t("linked_facilities")} dialogClass="md:w-[400px]" > @@ -728,6 +728,7 @@ export function UserFacilities(props: { user: any }) { handleOk={handleUnlinkFacilitySubmit} /> )} +
    +
    + {isLoading || userFacilitiesLoading ? (
    @@ -757,13 +760,23 @@ export function UserFacilities(props: { user: any }) {
    {/* Home Facility section */} {user?.home_facility_object && ( -
    -
    - {t("home_facility")} -
    +
    -
    - {user?.home_facility_object?.name} +
    +
    + {user?.home_facility_object?.name}{" "} + + + Home Facility + +
    -
    )} {/* Linked Facilities section */} {!!userFacilities?.results.length && ( -
    -
    - {t("linked_facilities")} -
    +
    {userFacilities.results.map( (facility: FacilityModel, i: number) => { diff --git a/src/Components/Users/UserFilter.tsx b/src/Components/Users/UserFilter.tsx index 4a070ee7cde..d5c16d22571 100644 --- a/src/Components/Users/UserFilter.tsx +++ b/src/Components/Users/UserFilter.tsx @@ -148,7 +148,7 @@ export default function UserFilter(props: any) { {...field("district")} state={filterState.state} /> -
    +