From d72d9bca04ca8618ec6b2a52212f2bd3be81a2a0 Mon Sep 17 00:00:00 2001 From: Daniel Jakab Date: Fri, 1 Nov 2024 14:56:37 +0100 Subject: [PATCH] Finish deployment cards --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/components/CarpAccordion/index.tsx | 2 +- src/components/CarpAccordion/styles.ts | 1 - src/locales/en/error.json | 3 +- src/pages/Deployment/BasicInfo/index.tsx | 127 ++++++++------ src/pages/Deployment/BasicInfo/styles.ts | 14 +- src/pages/Deployment/Devices/index.tsx | 156 ++++++++++++++++-- src/pages/Deployment/Devices/styles.ts | 60 ++++++- .../Deployment/InformedConsentCard/index.tsx | 2 +- src/pages/Deployment/Participants/index.tsx | 14 +- src/pages/Deployment/Participants/styles.ts | 1 + .../InputElements/PhoneNumberInput/index.tsx | 4 +- .../InputElements/utils.ts | 4 +- src/utils/queries/participants.ts | 70 ++++++++ 15 files changed, 378 insertions(+), 92 deletions(-) diff --git a/package.json b/package.json index f6e82d8..241ebca 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@carp-dk/authentication-react": "^1.0.1", - "@carp-dk/client": "1.3.0", + "@carp-dk/client": "1.4.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@js-joda/core": "^5.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31e6f8f..a87bfa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^1.0.1 version: 1.0.1(axios@1.7.7)(oidc-client-ts@3.1.0)(react-oidc-context@3.2.0(oidc-client-ts@3.1.0)(react@18.3.1))(react@18.3.1) '@carp-dk/client': - specifier: 1.3.0 - version: 1.3.0 + specifier: 1.4.0 + version: 1.4.0 '@emotion/react': specifier: ^11.11.4 version: 11.13.3(@types/react@18.3.12)(react@18.3.1) @@ -296,8 +296,8 @@ packages: react: '>=16.8.0' react-oidc-context: ^2.3.1 - '@carp-dk/client@1.3.0': - resolution: {integrity: sha512-kCUgF0H1jkxefuUIRqLv1Hs3s3CXoT/TCSZorOPB79gdxTPLdLvJqU4rQwHZaiwrYtkO3TliDrQVeSoT4Y1VZw==} + '@carp-dk/client@1.4.0': + resolution: {integrity: sha512-AUZnKyQJs0hhT4WE48AMFmO8QkADSwXh5LiK3QP6jIdInsmzkipX3f4jQopSuxsYTNKa9g+MoLKfAp8VVWpSdg==} '@carp-dk/eslint-config@1.1.0': resolution: {integrity: sha512-vIz9PJZBaSadbe3rO9nTJQoa01c8h9judJv1uVDYjlApienWU8W22Yi8aBJLHil3wI9dFXTPcDvx1yqMpkOo+w==} @@ -3564,7 +3564,7 @@ snapshots: react: 18.3.1 react-oidc-context: 3.2.0(oidc-client-ts@3.1.0)(react@18.3.1) - '@carp-dk/client@1.3.0': {} + '@carp-dk/client@1.4.0': {} ? '@carp-dk/eslint-config@1.1.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.1(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.2(eslint@8.57.1))(eslint@8.57.1))(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint-plugin-prefer-arrow@1.2.3(eslint@8.57.1))(eslint-plugin-prettier@5.0.0-alpha.2(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react-refresh@0.4.13(eslint@8.57.1))(eslint-plugin-react@7.37.2(eslint@8.57.1))(eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1)(prettier-plugin-organize-imports@3.2.4(prettier@3.3.3)(typescript@5.6.3))(prettier@3.3.3)' : dependencies: diff --git a/src/components/CarpAccordion/index.tsx b/src/components/CarpAccordion/index.tsx index 1488eaf..a472f4a 100644 --- a/src/components/CarpAccordion/index.tsx +++ b/src/components/CarpAccordion/index.tsx @@ -32,7 +32,7 @@ const CarpAccordion = ({ title, description, children }: Props) => { /> } > - + {title} {description && expanded && ( {description} diff --git a/src/components/CarpAccordion/styles.ts b/src/components/CarpAccordion/styles.ts index 4000b69..cf53515 100644 --- a/src/components/CarpAccordion/styles.ts +++ b/src/components/CarpAccordion/styles.ts @@ -30,5 +30,4 @@ export const Title = styled(Typography)(({ theme }) => ({ export const StyledTypography = styled(Typography)(({ theme }) => ({ color: theme.palette.text.secondary, - marginTop: 0, })); diff --git a/src/locales/en/error.json b/src/locales/en/error.json index 6feab14..39ce2b6 100644 --- a/src/locales/en/error.json +++ b/src/locales/en/error.json @@ -1,4 +1,5 @@ { "deployment_data": "An error occurred while loading deployment data", - "informed_consents": "An error occurred while loading informed consents" + "informed_consents": "An error occurred while loading informed consents", + "participants": "An error occurred while loading participants" } diff --git a/src/pages/Deployment/BasicInfo/index.tsx b/src/pages/Deployment/BasicInfo/index.tsx index acba03e..0d49620 100644 --- a/src/pages/Deployment/BasicInfo/index.tsx +++ b/src/pages/Deployment/BasicInfo/index.tsx @@ -5,12 +5,13 @@ import { useParticipantGroupsAccountsAndStatus } from "@Utils/queries/participan import { ParticipantGroup } from "@carp-dk/client"; import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { Stack } from "@mui/material"; +import { Box, Stack, Typography } from "@mui/material"; import { formatDateTime } from "@Utils/utility"; import { Stop } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import LoadingSkeleton from "../LoadingSkeleton"; import { + ExportButton, Left, Right, SecondaryText, @@ -20,6 +21,8 @@ import { StyledStatusDot, StyledStatusText, } from "./styles"; +import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; +import { useCreateSummary, useExports } from "@Utils/queries/studies"; const BasicInfo = () => { const { deploymentId, id: studyId } = useParams(); @@ -32,6 +35,8 @@ const BasicInfo = () => { } = useParticipantGroupsAccountsAndStatus(studyId); const [deployment, setDeployment] = useState(null); + const generateExport = useCreateSummary(); + useEffect(() => { if (!participantDataLoading && participantData && participantData.groups) { setDeployment( @@ -53,69 +58,81 @@ const BasicInfo = () => { ); return ( - - - - - - {deployment.deploymentStatus.__type.split(".").pop()} - - - {!deployment.deploymentStatus.__type.includes("Stopped") && ( - - - {t("common:stop_deployment")} - - )} - - - - - {`${t("common:created_on", { - date: formatDateTime(deployment.deploymentStatus.createdOn, { - year: "numeric", - month: "numeric", - day: "numeric", - }), - })}`} - - {deployment.deploymentStatus.startedOn && ( + <> + + + generateExport.mutate({ studyId, deploymentIds: [deploymentId] }) + } + > + + Export Data + + + + + + + + {deployment.deploymentStatus.__type.split(".").pop()} + + + {!deployment.deploymentStatus.__type.includes("Stopped") && ( + + + {t("common:stop_deployment")} + + )} + + + - {`${t("common:started_on", { - date: formatDateTime(deployment.deploymentStatus.startedOn, { + {`${t("common:created_on", { + date: formatDateTime(deployment.deploymentStatus.createdOn, { year: "numeric", month: "numeric", day: "numeric", }), })}`} - )} - {deployment.deploymentStatus.stoppedOn && ( + {deployment.deploymentStatus.startedOn && ( + + {`${t("common:started_on", { + date: formatDateTime(deployment.deploymentStatus.startedOn, { + year: "numeric", + month: "numeric", + day: "numeric", + }), + })}`} + + )} + {deployment.deploymentStatus.stoppedOn && ( + + {`${t("common:stopped_on", { + date: formatDateTime(deployment.deploymentStatus.stoppedOn, { + year: "numeric", + month: "numeric", + day: "numeric", + }), + })}`} + + )} + + + - {`${t("common:stopped_on", { - date: formatDateTime(deployment.deploymentStatus.stoppedOn, { - year: "numeric", - month: "numeric", - day: "numeric", - }), - })}`} + {t("common:deployment_id", { id: deploymentId })} - )} - - - - - {t("common:deployment_id", { id: deploymentId })} - - - - - + + + + + ); }; diff --git a/src/pages/Deployment/BasicInfo/styles.ts b/src/pages/Deployment/BasicInfo/styles.ts index 52561a2..490b9e5 100644 --- a/src/pages/Deployment/BasicInfo/styles.ts +++ b/src/pages/Deployment/BasicInfo/styles.ts @@ -5,7 +5,7 @@ import { getDeploymentStatusColor } from "@Utils/utility"; export const StyledCard = styled(Card)(({ theme }) => ({ display: "flex", justifyContent: "space-between", - padding: "10px 16px", + padding: "12px 16px", marginBottom: 32, borderRadius: 8, border: `1px solid ${theme.palette.grey[700]}`, @@ -95,11 +95,21 @@ export const StyledStatusText = styled(Typography, { textTransform: "uppercase", })); + +export const ExportButton = styled(Button)(({ theme }) => ({ + border: `1px solid ${theme.palette.grey[700]}`, + borderRadius: 16, + textTransform: "none", + padding: "8px 16px", + color: theme.palette.primary.main, + gap: 8, +})); + export const StyledButton = styled(Button)(({ theme }) => ({ border: `1px solid ${theme.palette.grey[700]}`, borderRadius: 16, textTransform: "none", - padding: "px 16px", + padding: "8px 16px", color: theme.palette.error.main, gap: 8, "&:disabled": { diff --git a/src/pages/Deployment/Devices/index.tsx b/src/pages/Deployment/Devices/index.tsx index 2bb7988..ee5a69f 100644 --- a/src/pages/Deployment/Devices/index.tsx +++ b/src/pages/Deployment/Devices/index.tsx @@ -1,14 +1,31 @@ import CarpErrorCardComponent from "@Components/CarpErrorCardComponent"; -import { useParticipantGroupsAccountsAndStatus } from "@Utils/queries/participants"; -import { Stack, Typography } from "@mui/material"; +import { + useDeviceDeployed, + useParticipantGroupsAccountsAndStatus, + useRegisterDevice, +} from "@Utils/queries/participants"; +import { Checkbox, Modal, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import CarpAccordion from "@Components/CarpAccordion"; import LoadingSkeleton from "../LoadingSkeleton"; -import { StyledStatusDot } from "./styles"; +import { + ActionButton, + Bottom, + CancelButton, + Description, + DescriptionContainer, + ModalBox, + StyledStatusDot, + Title, +} from "./styles"; import { t } from "i18next"; import { useStudyDetails } from "@Utils/queries/studies"; import { getDeviceIcon } from "@Utils/utility"; +import { title } from "process"; +import { CarpServiceError } from "@carp-dk/client"; +import { randomUUID } from "crypto"; +import { v4 } from "uuid"; const Devices = () => { const { id: studyId, deploymentId } = useParams(); @@ -24,6 +41,10 @@ const Devices = () => { isLoading: participantGroupsAndStatusesLoading, error: participantGroupsAndStatusesError, } = useParticipantGroupsAccountsAndStatus(studyId); + + const registerDevice = useRegisterDevice(studyId); + const deviceDeployed = useDeviceDeployed(studyId); + const [devices, setDevices] = useState< { primaryDevice: { name: string; type: string; status: string }; @@ -31,6 +52,38 @@ const Devices = () => { }[] >([]); + const [modalState, setModalState] = useState<{ + open: boolean; + roleName: string; + deviceId: string; + }>({ open: false, roleName: "", deviceId: "" }); + const [allowDeploy, setAllowDeploy] = useState(false); + + const onConfirm = async () => { + console.log(modalState.roleName); + await registerDevice + .mutateAsync({ + studyDeploymentId: deploymentId, + roleName: modalState.roleName, + deviceId: modalState.deviceId, + }) + .catch((err) => { + if ( + (err as CarpServiceError).httpResponseMessage !== + "The passed device is already registered." + ) { + throw err; + } + }); + console.log("deploying"); + await deviceDeployed.mutateAsync({ + studyDeploymentId: deploymentId, + roleName: modalState.roleName, + }); + + setModalState({ open: false, roleName: "", deviceId: "" }); + }; + useEffect(() => { if (study && participantGroupsAndStatuses) { const connectedDevices = study.protocolSnapshot.primaryDevices @@ -88,6 +141,49 @@ const Devices = () => { title={t("deployment:devices_card.title")} description={t("deployment:devices_card.description")} > + + + {"Deployment of a Master Device"} + + + { + "The device will be permanently deployed. You can not undo this action." + } + + + + + setAllowDeploy(!allowDeploy)} /> + + {"I'm sure I want to deploy it"} + + + + { + setModalState({ open: false, roleName: "", deviceId: "" }); + setAllowDeploy(false); + }} + > + Cancel + + onConfirm()} + disabled={!allowDeploy} + > + {"Deploy"} + + + + + + {devices && devices.map(({ primaryDevice, connections }) => ( @@ -103,13 +199,37 @@ const Devices = () => { > + setModalState({ + open: true, + roleName: primaryDevice.name, + deviceId: v4(), + }) + } + sx={{ + "&:hover": { + backgroundColor: "#ededed", + borderRadius: "100px", + cursor: "pointer", + }, + }} + justifyContent={"center"} > - {getDeviceIcon(primaryDevice.type)} - {primaryDevice.name} + + {getDeviceIcon(primaryDevice.type)} + + {primaryDevice.name} + + @@ -118,13 +238,23 @@ const Devices = () => { - {getDeviceIcon(connection.type)} - {connection.name} + + {getDeviceIcon(connection.type)} + + {connection.name} + + ); diff --git a/src/pages/Deployment/Devices/styles.ts b/src/pages/Deployment/Devices/styles.ts index bc215b5..1c92c4a 100644 --- a/src/pages/Deployment/Devices/styles.ts +++ b/src/pages/Deployment/Devices/styles.ts @@ -1,3 +1,4 @@ +import { Typography, Button } from "@mui/material"; import { styled } from "@Utils/theme"; import { getDeviceStatusColor } from "@Utils/utility"; @@ -8,7 +9,64 @@ export const StyledStatusDot = styled("div", { height: 8, borderRadius: "50%", backgroundColor: getDeviceStatusColor(status), - marginLeft: 6, flexShrink: 0, alignSelf: "center", })); + + +export const ModalBox = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "70%", + backgroundColor: theme.palette.common.white, + borderRadius: 16, + padding: 24, + maxWidth: 550, +})); + +export const Title = styled(Typography)(({ theme }) => ({ + color: theme.palette.error.main, + marginBottom: 24, +})); + +export const DescriptionContainer = styled("div")({ + marginBottom: 38, +}); + +export const Description = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.primary, + fontWeight: 400, + display: "inline", +})); + +export const BoldText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.primary, + display: "inline", + marginLeft: 4, +})); + +export const Bottom = styled("div")({ + display: "flex", + justifyContent: "space-between", +}); + +export const ButtonsContainer = styled("div")({ + display: "flex", + gap: 16, +}); + +export const CancelButton = styled(Button)({ + textTransform: "capitalize", + borderRadius: 16, + padding: "10px 24px", +}); + +export const ActionButton = styled(Button)({ + padding: "10px 24px", + textTransform: "capitalize", + borderRadius: 16, +}); diff --git a/src/pages/Deployment/InformedConsentCard/index.tsx b/src/pages/Deployment/InformedConsentCard/index.tsx index f35b7e0..f5c63e9 100644 --- a/src/pages/Deployment/InformedConsentCard/index.tsx +++ b/src/pages/Deployment/InformedConsentCard/index.tsx @@ -108,7 +108,7 @@ const InformedConsentCard = () => { if (participantDataError || statusesError) { return ( ); diff --git a/src/pages/Deployment/Participants/index.tsx b/src/pages/Deployment/Participants/index.tsx index 17abaf6..899ac79 100644 --- a/src/pages/Deployment/Participants/index.tsx +++ b/src/pages/Deployment/Participants/index.tsx @@ -36,7 +36,7 @@ const Participants = () => { if (error) { return ( ); @@ -52,24 +52,24 @@ const Participants = () => { gap={16} key={p.participantId} display={"grid"} - gridTemplateColumns={"1fr 1fr 1fr"} + gridTemplateColumns={"25% 20% 20%"} > - + - {p.firstName === "" || p.firstName === null + {!p.firstName ? p.role[0] : `${p.firstName[0]}${p.lastName[0]}`} - + {p.email ?? } - + {p.firstName && ( - + {p.firstName} {p.lastName} )} diff --git a/src/pages/Deployment/Participants/styles.ts b/src/pages/Deployment/Participants/styles.ts index a85ef4d..c6d7cc9 100644 --- a/src/pages/Deployment/Participants/styles.ts +++ b/src/pages/Deployment/Participants/styles.ts @@ -27,6 +27,7 @@ export const Right = styled("div")({ export const AccountIcon = styled("div")(({ theme }) => ({ width: 28, height: 28, + flexShrink: 0, backgroundColor: theme.palette.company.isotype, borderRadius: "50%", position: "relative", diff --git a/src/pages/Participant/ParticipantDataCard/InputElements/PhoneNumberInput/index.tsx b/src/pages/Participant/ParticipantDataCard/InputElements/PhoneNumberInput/index.tsx index b61b1e3..dd611ed 100644 --- a/src/pages/Participant/ParticipantDataCard/InputElements/PhoneNumberInput/index.tsx +++ b/src/pages/Participant/ParticipantDataCard/InputElements/PhoneNumberInput/index.tsx @@ -31,12 +31,12 @@ const PhoneNumberInput = ({ formik, editing }: Props) => {