From ce1c7224ee8f6f193a2c7e7a1ca02e45cd261b78 Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Fri, 7 Jun 2024 20:28:40 +0200 Subject: [PATCH 001/427] Create theme, main components and install @mui/x-date-pickers --- i18n/en.pot | 28 +- i18n/es.po | 51 ++- package.json | 4 + src/types/d2-ui.d.ts | 10 + .../add-new-option/AddNewOption.tsx | 39 ++ .../components/avatar-card/AvatarCard.tsx | 45 ++ src/webapp/components/button/Button.tsx | 42 ++ .../components/date-picker/DatePicker.tsx | 104 +++++ .../components/input-field/InputField.tsx | 69 +++ .../multiple-selector/MultipleSelector.tsx | 144 ++++++ .../not-applicable-checkbox/NACheckbox.tsx | 78 ++++ .../radio-buttons-group/RadioButtonsGroup.tsx | 77 ++++ src/webapp/components/selector/Selector.tsx | 119 +++++ src/webapp/components/text-area/TextArea.tsx | 99 ++++ src/webapp/pages/app/themes/dhis2.theme.ts | 339 ++++++++++++-- yarn.lock | 425 +++++++++++++++++- 16 files changed, 1614 insertions(+), 59 deletions(-) create mode 100644 src/webapp/components/add-new-option/AddNewOption.tsx create mode 100644 src/webapp/components/avatar-card/AvatarCard.tsx create mode 100644 src/webapp/components/button/Button.tsx create mode 100644 src/webapp/components/date-picker/DatePicker.tsx create mode 100644 src/webapp/components/input-field/InputField.tsx create mode 100644 src/webapp/components/multiple-selector/MultipleSelector.tsx create mode 100644 src/webapp/components/not-applicable-checkbox/NACheckbox.tsx create mode 100644 src/webapp/components/radio-buttons-group/RadioButtonsGroup.tsx create mode 100644 src/webapp/components/selector/Selector.tsx create mode 100644 src/webapp/components/text-area/TextArea.tsx diff --git a/i18n/en.pot b/i18n/en.pot index 1ad98c3d..c278404f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,23 +5,35 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-09-18T13:40:05.079Z\n" -"PO-Revision-Date: 2023-09-18T13:40:05.079Z\n" +"POT-Creation-Date: 2024-06-07T18:24:28.361Z\n" +"PO-Revision-Date: 2024-06-07T18:24:28.361Z\n" -msgid "Add" +msgid "Add new option" msgstr "" -msgid "List" +msgid "N/A" msgstr "" -msgid "Back" +msgid "Incident Management Team Builder" msgstr "" -msgid "Help" +msgid "Cholera in NW Province, June 2023" msgstr "" -msgid "Hello {{name}}" +msgid "Create Event" msgstr "" -msgid "Detail page" +msgid "Incident Action Plan" +msgstr "" + +msgid "Create Risk Assessment" +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Event Tracker" +msgstr "" + +msgid "Resources" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 0250fb5a..02dd6634 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,27 +1,54 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-09-18T10:19:02.458Z\n" +"POT-Creation-Date: 2024-06-07T18:24:28.361Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -msgid "Add" -msgstr "Añadir" +msgid "Add new option" +msgstr "" + +msgid "N/A" +msgstr "" + +msgid "Incident Management Team Builder" +msgstr "" + +msgid "Cholera in NW Province, June 2023" +msgstr "" + +msgid "Create Event" +msgstr "" -msgid "List" -msgstr "Listar" +msgid "Incident Action Plan" +msgstr "" -msgid "Back" -msgstr "Volver" +msgid "Create Risk Assessment" +msgstr "" -msgid "Help" -msgstr "Ayuda" +msgid "Dashboard" +msgstr "" -msgid "Hello {{name}}" -msgstr "Hola {{name}}" +msgid "Event Tracker" +msgstr "" -msgid "Detail page" +msgid "Resources" msgstr "" + +#~ msgid "Add" +#~ msgstr "Añadir" + +#~ msgid "List" +#~ msgstr "Listar" + +#~ msgid "Back" +#~ msgstr "Volver" + +#~ msgid "Help" +#~ msgstr "Ayuda" + +#~ msgid "Hello {{name}}" +#~ msgstr "Hola {{name}}" diff --git a/package.json b/package.json index 321eefa8..2ce199e8 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@dhis2/d2-i18n-extract": "1.0.8", "@dhis2/d2-i18n-generate": "1.2.0", "@dhis2/ui": "6.12.0", + "@emotion/react": "11.11.4", + "@emotion/styled": "11.11.5", "@eyeseetea/d2-api": "1.14.0", "@eyeseetea/d2-ui-components": "2.7.0", "@eyeseetea/feedback-component": "0.0.3", @@ -22,6 +24,8 @@ "@material-ui/icons": "4.11.3", "@material-ui/lab": "4.0.0-alpha.60", "@material-ui/styles": "4.11.5", + "@mui/material": "5.15.19", + "@mui/x-date-pickers": "7.6.1", "classnames": "2.3.1", "d2": "31.10.2", "d2-manifest": "1.0.0", diff --git a/src/types/d2-ui.d.ts b/src/types/d2-ui.d.ts index c80113fb..64802194 100644 --- a/src/types/d2-ui.d.ts +++ b/src/types/d2-ui.d.ts @@ -1,3 +1,13 @@ declare module "@dhis2/ui" { export function HeaderBar(props: { className?: string; appName?: string }): React.ReactElement; + export function IconAddCircle24(props: { color?: string }): React.ReactElement; + export function IconEdit24(props: { color?: string }): React.ReactElement; + export function IconEditItems24(props: { color?: string }): React.ReactElement; + export function IconUpload24(props: { color?: string }): React.ReactElement; + export function IconUser24(props: { color?: string }): React.ReactElement; + export function IconDownload24(props: { color?: string }): React.ReactElement; + export function IconArrowRight24(props: { color?: string }): React.ReactElement; + export function IconCalendar24(props: { color?: string }): React.ReactElement; + export function IconChevronDown24(props: { color?: string }): React.ReactElement; + export function IconCross24(props: { color?: string }): React.ReactElement; } diff --git a/src/webapp/components/add-new-option/AddNewOption.tsx b/src/webapp/components/add-new-option/AddNewOption.tsx new file mode 100644 index 00000000..d101c172 --- /dev/null +++ b/src/webapp/components/add-new-option/AddNewOption.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; +import { AddCircleOutline } from "@material-ui/icons"; + +import i18n from "../../../utils/i18n"; + +interface AddNewOptionProps { + id: string; + label?: string; + onAddNewOption: () => void; +} + +export const AddNewOption: React.FC = React.memo( + ({ id, label = "", onAddNewOption }) => { + return ( + + + + + ); + } +); + +const Container = styled.div` + display: flex; + align-items: center; +`; + +const StyledAddIcon = styled(AddCircleOutline)` + color: ${props => props.theme.palette.icon.color}; +`; + +const Label = styled.label` + display: inline-block; + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.common.black}; + margin-inline-start: 8px; +`; diff --git a/src/webapp/components/avatar-card/AvatarCard.tsx b/src/webapp/components/avatar-card/AvatarCard.tsx new file mode 100644 index 00000000..aca63efe --- /dev/null +++ b/src/webapp/components/avatar-card/AvatarCard.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { CardContent, Card, Avatar } from "@material-ui/core"; +import styled from "styled-components"; + +interface AvatarCardProps { + children: React.ReactNode; + avatarSize?: "small" | "medium"; + alt?: string; + src?: string; +} + +export const AvatarCard: React.FC = React.memo( + ({ children, src, alt, avatarSize = "small" }) => { + return ( + + + + + {children} + + ); + } +); + +const StyledCard = styled(Card)` + display: flex; + @media (max-width: 600px) { + flex-direction: column; + } +`; + +const AvatarContainer = styled.div<{ $size: string }>` + padding-block: 20px; + padding-inline: 45px; + .MuiAvatar-root { + width: ${props => (props.$size === "medium" ? " 85px" : "56px")}; + height: ${props => (props.$size === "medium" ? " 85px" : "56px")}; + } +`; + +const StyledCardContent = styled(CardContent)` + width: 100%; + color: ${props => props.theme.palette.common.black}; + font-size: 0.875rem; +`; diff --git a/src/webapp/components/button/Button.tsx b/src/webapp/components/button/Button.tsx new file mode 100644 index 00000000..10436e2e --- /dev/null +++ b/src/webapp/components/button/Button.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Button as MUIButton } from "@material-ui/core"; +import styled from "styled-components"; + +interface ButtonProps { + children?: React.ReactNode; + variant?: "contained" | "outlined"; + color?: "primary" | "secondary" | "dark-secondary"; + disabled?: boolean; + startIcon?: React.ReactNode; + onClick: () => void; +} + +export const Button: React.FC = React.memo( + ({ + children, + variant = "contained", + color = "primary", + disabled = false, + startIcon, + onClick, + }) => { + return ( + + {children} + + ); + } +); + +const StyledButton = styled(MUIButton)<{ $darkBorder: boolean }>` + border-color: ${props => + props.$darkBorder ? props.theme.palette.button.borderDarkSecondary : "initial"}; +`; diff --git a/src/webapp/components/date-picker/DatePicker.tsx b/src/webapp/components/date-picker/DatePicker.tsx new file mode 100644 index 00000000..63e5df12 --- /dev/null +++ b/src/webapp/components/date-picker/DatePicker.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import styled from "styled-components"; +import { InputLabel } from "@material-ui/core"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { DatePicker as DatePickerMUI } from "@mui/x-date-pickers/DatePicker"; +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; +import { IconCalendar24 } from "@dhis2/ui"; + +interface DatePickerProps { + id: string; + label?: string; + value?: Date; + onChange: (value: Date | null) => void; + helperText?: string; + errorText?: string; + disabled?: boolean; + error?: boolean; +} + +export const DatePicker: React.FC = React.memo( + ({ + id = "", + label = "", + value, + onChange, + disabled = false, + helperText = "", + errorText = "", + error = false, + }) => { + return ( + + {label && } + + + + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled(InputLabel)` + display: inline-block; + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.text.primary}; + margin-block-end: 8px; +`; + +const StyledDatePicker = styled(DatePickerMUI)<{ error?: boolean; disabled?: boolean }>` + & .MuiInputBase-root { + border-radius: 3px; + background-color: ${props => + props.disabled ? props.theme.palette.common.grey100 : props.theme.palette.common.white}; + } + & .Mui-focused { + border-color: ${props => props.theme.palette.common.blue600}; + } + input { + padding-inline: 12px; + padding-block: 10px; + font-weight: 400; + font-size: 0.875rem; + color: ${props => + props.disabled ? props.theme.palette.text.disabled : props.theme.palette.text.primary}; + } + .MuiInputBase-input::placeholder { + color: ${props => + props.disabled ? props.theme.palette.text.disabled : props.theme.palette.text.primary}; + } + .MuiOutlinedInput-notchedOutline { + border-color: ${props => + props.error ? props.theme.palette.common.red600 : props.theme.palette.common.grey500}; + } + .MuiIconButton-edgeEnd { + color: ${props => props.theme.palette.icon.color}; + } + .MuiFormHelperText-root { + font-weight: 400; + font-size: "0.75rem"; + margin-inline-start: 0; + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; + } +`; diff --git a/src/webapp/components/input-field/InputField.tsx b/src/webapp/components/input-field/InputField.tsx new file mode 100644 index 00000000..ad623a0b --- /dev/null +++ b/src/webapp/components/input-field/InputField.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { TextField, InputLabel } from "@material-ui/core"; +import styled from "styled-components"; + +interface InputFieldProps { + id: string; + label?: string; + value: string; + onChange: (event: React.ChangeEvent) => void; + helperText?: string; + errorText?: string; + required?: boolean; + disabled?: boolean; + error?: boolean; +} + +export const InputField: React.FC = React.memo( + ({ + id, + label = "", + value, + onChange, + helperText = "", + errorText = "", + required = false, + disabled = false, + error = false, + }) => { + return ( + + {label && } + + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled(InputLabel)` + display: inline-block; + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.text.primary}; + margin-block-end: 8px; +`; + +const StyledTextField = styled(TextField)<{ error?: boolean }>` + .MuiFormHelperText-root { + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; + } + .MuiInputBase-input { + padding-inline: 12px; + padding-block: 10px; + } +`; diff --git a/src/webapp/components/multiple-selector/MultipleSelector.tsx b/src/webapp/components/multiple-selector/MultipleSelector.tsx new file mode 100644 index 00000000..4b304673 --- /dev/null +++ b/src/webapp/components/multiple-selector/MultipleSelector.tsx @@ -0,0 +1,144 @@ +import React, { useCallback } from "react"; +import styled from "styled-components"; +import { Select, InputLabel, MenuItem, FormHelperText, Chip } from "@material-ui/core"; +import { IconChevronDown24, IconCross24 } from "@dhis2/ui"; + +export type MultipleSelectorOption = { + value: T; + label: string; + disabled?: boolean; +}; + +interface MultipleSelectorProps { + id: string; + selected: T[]; + onChange: (value: MultipleSelectorOption["value"][]) => void; + options: MultipleSelectorOption[]; + label?: string; + placeholder?: string; + disabled?: boolean; + helperText?: string; + errorText?: string; + error?: boolean; +} + +export const MultipleSelector: React.FC = React.memo( + ({ + id, + label = "", + placeholder = "", + selected, + onChange, + options, + disabled = false, + helperText = "", + errorText = "", + error = false, + }) => { + const getLabelFromValue = useCallback( + (value: MultipleSelectorOption["value"]) => { + return options.find(option => option.value === value)?.label || ""; + }, + [options] + ); + + const handleChange = useCallback( + ( + event: React.ChangeEvent<{ + value: unknown; + }>, + _child: React.ReactNode + ) => { + const value = event.target.value as MultipleSelectorOption["value"][]; + onChange(value); + }, + [onChange] + ); + + const handleDelete = useCallback( + (value: MultipleSelectorOption["value"]) => { + onChange(selected?.filter(selection => selection !== value)); + }, + [onChange, selected] + ); + + return ( + + {label && } + + (selected as MultipleSelectorOption["value"][])?.length ? ( +
+ {(selected as MultipleSelectorOption["value"][]).map(value => ( + } + onDelete={() => handleDelete(value)} + /> + ))} +
+ ) : ( + placeholder + ) + } + displayEmpty + multiple + > + {options.map(option => ( + + {option.label} + + ))} +
+ + {error && !!errorText ? errorText : helperText} + +
+ ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled(InputLabel)` + display: inline-block; + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.text.primary}; + margin-block-end: 8px; +`; + +const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>` + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; +`; + +const StyledSelect = styled(Select)<{ error?: boolean }>` + padding-inline-start: 12px; + padding-inline-end: 6px; + padding-block: 10px; + .MuiOutlinedInput-notchedOutline { + border-color: ${props => + props.error ? props.theme.palette.common.red600 : props.theme.palette.common.grey500}; + } +`; + +const SelectedChip = styled(Chip)` + margin-inline-end: 16px; +`; diff --git a/src/webapp/components/not-applicable-checkbox/NACheckbox.tsx b/src/webapp/components/not-applicable-checkbox/NACheckbox.tsx new file mode 100644 index 00000000..92ee63ea --- /dev/null +++ b/src/webapp/components/not-applicable-checkbox/NACheckbox.tsx @@ -0,0 +1,78 @@ +import React, { useCallback } from "react"; +import { Checkbox, InputLabel, FormHelperText } from "@material-ui/core"; +import styled from "styled-components"; + +import i18n from "../../../utils/i18n"; + +interface NACheckboxProps { + id: string; + label?: string; + checked: boolean; + onChange: (isChecked: boolean) => void; + helperText?: string; + disabled?: boolean; + indeterminate?: boolean; +} + +export const NACheckbox: React.FC = React.memo( + ({ + id, + label = "", + checked, + onChange, + helperText = "", + disabled = false, + indeterminate = false, + }) => { + const handleChange = useCallback( + (event: React.ChangeEvent) => { + onChange(event.target.checked); + }, + [onChange] + ); + + return ( + + + + + + {helperText} + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const CheckboxWrapper = styled.div` + display: flex; + align-items: center; +`; + +const Label = styled(InputLabel)` + display: inline-block; + font-weight: 400; + font-size: 0.938rem; + color: ${props => props.theme.palette.text.primary}; + &.Mui-disabled { + color: ${props => props.theme.palette.common.grey600}; + } +`; + +const StyledFormHelperText = styled(FormHelperText)` + color: ${props => props.theme.palette.common.grey700}; +`; diff --git a/src/webapp/components/radio-buttons-group/RadioButtonsGroup.tsx b/src/webapp/components/radio-buttons-group/RadioButtonsGroup.tsx new file mode 100644 index 00000000..e37631d5 --- /dev/null +++ b/src/webapp/components/radio-buttons-group/RadioButtonsGroup.tsx @@ -0,0 +1,77 @@ +import { FormControlLabel, Radio, RadioGroup, FormHelperText } from "@material-ui/core"; +import React from "react"; +import styled from "styled-components"; + +export type RadioOption = { + value: T; + label: string; + disabled?: boolean; +}; + +interface RadioButtonsGroupProps { + id: string; + selected: string; + onChange: (event: React.ChangeEvent) => void; + options: RadioOption[]; + gap?: string; + helperText?: string; + errorText?: string; + error?: boolean; +} + +export const RadioButtonsGroup: React.FC = React.memo( + ({ + id, + selected, + onChange, + options, + gap = "24px", + helperText = "", + errorText = "", + error = false, + }) => { + return ( + <> + + {options.map(option => ( + } + label={option.label} + disabled={option.disabled} + aria-label={option.label} + /> + ))} + + + {error && !!errorText ? errorText : helperText} + + + ); + } +); + +const StyledRadioGroup = styled(RadioGroup)<{ gap: string }>` + flex-direction: row; + gap: ${props => props.gap}; +`; + +const StyledRadio = styled(Radio)` + padding: 5px; + .MuiSvgIcon-root { + width: 1.125rem; + height: 1.125rem; + } +`; + +const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>` + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; +`; diff --git a/src/webapp/components/selector/Selector.tsx b/src/webapp/components/selector/Selector.tsx new file mode 100644 index 00000000..f1d7cae3 --- /dev/null +++ b/src/webapp/components/selector/Selector.tsx @@ -0,0 +1,119 @@ +import React, { useCallback } from "react"; +import styled from "styled-components"; +import { Select, InputLabel, MenuItem, FormHelperText } from "@material-ui/core"; +import { IconChevronDown24 } from "@dhis2/ui"; + +export type SelectorOption = { + value: T; + label: string; + disabled?: boolean; +}; + +interface SelectorProps { + id: string; + selected: T; + onChange: (value: SelectorOption["value"]) => void; + options: SelectorOption[]; + label?: string; + placeholder?: string; + disabled?: boolean; + helperText?: string; + errorText?: string; + error?: boolean; +} + +export const Selector: React.FC = React.memo( + ({ + id, + label = "", + placeholder = "", + selected, + onChange, + options, + disabled = false, + helperText = "", + errorText = "", + error = false, + }) => { + const getLabelFromValue = useCallback( + (value: SelectorOption["value"]) => { + return options.find(option => option.value === value)?.label || ""; + }, + [options] + ); + + const handleChange = useCallback( + ( + event: React.ChangeEvent<{ + value: unknown; + }>, + _child: React.ReactNode + ) => { + const value = event.target.value as SelectorOption["value"]; + onChange(value); + }, + [onChange] + ); + + return ( + + {label && } + + getLabelFromValue(selected as SelectorOption["value"]) || placeholder + } + displayEmpty + > + {options.map(option => ( + + {option.label} + + ))} + + + {error && !!errorText ? errorText : helperText} + + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled(InputLabel)` + display: inline-block; + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.text.primary}; + margin-block-end: 8px; +`; + +const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>` + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; +`; + +const StyledSelect = styled(Select)<{ error?: boolean }>` + padding-inline-start: 12px; + padding-inline-end: 6px; + padding-block: 10px; + .MuiOutlinedInput-notchedOutline { + border-color: ${props => + props.error ? props.theme.palette.common.red600 : props.theme.palette.common.grey500}; + } +`; diff --git a/src/webapp/components/text-area/TextArea.tsx b/src/webapp/components/text-area/TextArea.tsx new file mode 100644 index 00000000..d6e6b4e3 --- /dev/null +++ b/src/webapp/components/text-area/TextArea.tsx @@ -0,0 +1,99 @@ +import React, { useCallback } from "react"; +import styled from "styled-components"; +import { FormHelperText, TextareaAutosize } from "@material-ui/core"; + +interface TextAreaProps { + id: string; + label?: string; + value: string; + onChange: (text: string) => void; + disabled?: boolean; + helperText?: string; + errorText?: string; + error?: boolean; +} + +export const TextArea: React.FC = React.memo( + ({ + id, + label = "", + value, + onChange, + disabled = false, + helperText = "", + errorText = "", + error = false, + }) => { + const handleChange = useCallback( + (event: React.ChangeEvent) => { + onChange(event.target.value); + }, + [onChange] + ); + + return ( + + {label && } + + + {error && !!errorText ? errorText : helperText} + + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled.label` + display: inline-block; + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.text.primary}; + margin-block-end: 8px; +`; + +const StyledTextareaAutosize = styled(TextareaAutosize)<{ + $hasError?: boolean; + disabled?: boolean; +}>` + display: inline-block; + font-family: Roboto, Arial, sans-serif; + font-weight: 400; + font-size: 0.875rem; + padding-inline: 12px; + padding-block: 10px; + color: ${props => props.theme.palette.common.grey900}; + background-color: ${props => + props.disabled ? props.theme.palette.common.grey100 : props.theme.palette.common.white}; + border-radius: 3px; + border-color: ${props => + props.$hasError ? props.theme.palette.common.red600 : props.theme.palette.common.grey500}; + &:focus-visible { + outline: none; + border-color: ${props => props.theme.palette.common.blue600}; + border-width: 2px; + } + &:hover:focus-visible { + border-color: ${props => props.theme.palette.common.blue600}; + } + &:hover { + border-color: ${props => props.theme.palette.common.black}; + } +`; + +const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>` + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; +`; diff --git a/src/webapp/pages/app/themes/dhis2.theme.ts b/src/webapp/pages/app/themes/dhis2.theme.ts index 29bddb6a..1b69e1bf 100644 --- a/src/webapp/pages/app/themes/dhis2.theme.ts +++ b/src/webapp/pages/app/themes/dhis2.theme.ts @@ -1,77 +1,168 @@ import { createTheme } from "@material-ui/core/styles"; -// Color palette from https://projects.invisionapp.com/share/A7LT4TJYETS#/screens/302550228_Color +// Color palette from https://ui.dhis2.nu/principles/color const colors = { - accentPrimary: "#1976d2", - accentPrimaryDark: "#004BA0", - accentPrimaryLight: "#63A4FF", - accentPrimaryLightest: "#EAF4FF", - accentSecondary: "#fb8c00", accentSecondaryLight: "#f57c00", accentSecondaryDark: "#ff9800", - black: "#000000", greyBlack: "#494949", grey: "#9E9E9E", greyLight: "#E0E0E0", greyDisabled: "#8E8E8E", blueGrey: "#ECEFF1", snow: "#F4F6F8", - white: "#FFFFFF", // Not included in palette! negative: "#E53935", warning: "#F19C02", positive: "#3D9305", info: "#EAF4FF", + + black: "#000000", + white: "#FFFFFF", + + blue900: "#093371", + blue800: "#0d47a1", + blue700: "#1565c0", + blue600: "#147cd7", + blue500: "#2196f3", + blue400: "#42a5f5", + blue300: "#90caf9", + blue200: "#c5e3fc", + blue100: "#e3f2fd", + blue050: "#f5fbff", + + teal900: "#00332b", + teal800: "#004d40", + teal700: "#00695c", + teal600: "#00796b", + teal500: "#00897b", + teal400: "#009688", + teal300: "#4db6ac", + teal200: "#b2dfdb", + teal100: "#e0f2f1", + teal050: "#f1f9f9", + + red900: "#330202", + red800: "#891515", + red700: "#b71c1c", + red600: "#c62828", + red500: "#d32f2f", + red400: "#f44336", + red300: "#e57373", + red200: "#ffcdd2", + red100: "#ffe5e8", + red050: "#fff5f6", + + yellow900: "#6f3205", + yellow800: "#bb460d", + yellow700: "#e56408", + yellow600: "#ff8302", + yellow500: "#ff9302", + yellow400: "#ffa902", + yellow300: "#ffc324", + yellow200: "#ffe082", + yellow100: "#ffecb3", + yellow050: "#fff8e1", + + green900: "#103713", + green800: "#1b5e20", + green700: "#2e7d32", + green600: "#388e3c", + green500: "#43a047", + green400: "#4caf50", + green300: "#a5d6a7", + green200: "#c8e6c9", + green100: "#e8f5e9", + green050: "#f4fbf4", + + grey900: "#212934", + grey800: "#404b5a", + grey700: "#4a5768", + grey600: "#6e7a8a", + grey500: "#a0adba", + grey400: "#d5dde5", + grey300: "#e8edf2", + grey200: "#f3f5f7", + grey100: "#f8f9fa", + grey050: "#fbfcfd", + + green: "#008B45", + red: "#E4312B", + orange: "#FABE5F", }; const palette = { common: { - white: colors.white, - black: colors.black, + ...colors, }, action: { - active: colors.greyBlack, - disabled: colors.greyDisabled, + active: colors.grey900, + disabled: colors.grey500, }, text: { - primary: colors.black, - secondary: colors.greyBlack, - disabled: colors.greyDisabled, - hint: colors.grey, + primary: colors.grey900, + secondary: colors.grey800, + disabled: colors.grey600, + hint: colors.grey700, }, primary: { - main: colors.accentPrimary, - dark: colors.accentPrimaryDark, - light: colors.accentPrimaryLight, - lightest: colors.accentPrimaryLightest, // Custom extension, not used by default - // contrastText: 'white', + main: colors.green, + dark: colors.green800, + light: colors.green300, + contrastText: colors.white, }, secondary: { - main: colors.accentSecondary, - light: colors.accentSecondaryLight, - dark: colors.accentSecondaryDark, - contrastText: "#fff", + main: colors.teal500, + light: colors.teal100, + dark: colors.teal700, + contrastText: colors.white, }, error: { - main: colors.negative, // This is automatically expanded to main/light/dark/contrastText, what do we use here? - }, - status: { - //Custom colors collection, not used by default in MUI - negative: colors.negative, - warning: colors.warning, - positive: colors.positive, - info: colors.info, + main: colors.red600, + text: colors.red700, // Custom extension, not used by default }, background: { paper: colors.white, - default: colors.snow, - grey: "#FCFCFC", - hover: colors.greyLight, + default: colors.grey100, }, divider: colors.greyLight, + + //Custom colors collection, not used by default in MUI shadow: colors.grey, + status: { + negative: colors.red, + warning: colors.orange, + positive: colors.green, + info: colors.grey900, + }, + button: { + backgroundPrimary: colors.green, + textPrimary: colors.white, + backgroundSecondary: colors.grey100, + borderDarkSecondary: colors.grey500, + textSecondary: colors.grey900, + iconSecondary: colors.grey600, + }, + link: { + color: colors.blue800, + }, + sidebar: { + background: colors.grey200, + text: colors.grey800, + hover: colors.grey900, + }, + icon: { + color: colors.grey700, + }, + header: { + color: colors.green600, + }, + flag: { + red: colors.red, + black: colors.black, + orange: colors.orange, + }, }; export const muiTheme = createTheme({ @@ -87,5 +178,181 @@ export const muiTheme = createTheme({ backgroundColor: palette.divider, // No light dividers for now }, }, + MuiOutlinedInput: { + root: { + backgroundColor: colors.white, + borderRadius: "3px", + borderColor: colors.grey500, + "&$disabled": { + backgroundColor: colors.grey100, + }, + "&$disabled $notchedOutline": { + borderColor: colors.grey500, + }, + "&$error $notchedOutline": { + borderColor: colors.red600, + }, + "&$focused $notchedOutline": { + borderColor: colors.blue600, + }, + }, + notchedOutline: { + borderColor: colors.grey500, + }, + input: { + color: colors.grey900, + fontWeight: 400, + fontSize: "0.875rem", + "&:disabled": { + color: colors.grey600, + }, + }, + }, + MuiFormHelperText: { + root: { + fontWeight: 400, + fontSize: "0.75rem", + color: colors.grey700, + }, + contained: { + marginInlineStart: 0, + }, + }, + MuiRadio: { + colorSecondary: { + color: colors.grey600, + "&$disabled": { + color: colors.grey400, + }, + }, + }, + MuiFormControlLabel: { + label: { + color: colors.grey900, + fontWeight: 400, + fontSize: "0.938rem", + "&$disabled": { + color: colors.grey600, + }, + }, + }, + MuiSelect: { + select: { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + color: colors.grey900, + fontWeight: 400, + fontSize: "0.875rem", + "&$disabled": { + color: colors.grey600, + }, + }, + iconOutlined: { + color: colors.grey700, + "&$disabled": { + color: colors.grey700, + }, + }, + }, + MuiChip: { + root: { + backgroundColor: colors.grey200, + borderRadius: "100px", + border: "none", + }, + label: { + fontSize: "0.813rem", + fontWeight: 400, + color: colors.grey900, + }, + deleteIcon: { + color: colors.grey600, + }, + }, + MuiCheckbox: { + colorSecondary: { + color: colors.grey600, + "&$disabled": { + color: colors.grey400, + }, + }, + }, + MuiButton: { + label: { + fontSize: "0.875rem", + textTransform: "none", + fontWeight: 500, + }, + outlinedPrimary: { + "&$disabled": { + borderColor: colors.grey300, + "& .MuiButton-label": { + color: colors.grey500, + }, + "& .MuiButton-startIcon": { + color: colors.grey500, + }, + }, + }, + containedSecondary: { + color: colors.grey400, + borderColor: colors.grey300, + backgroundColor: colors.grey100, + fontWeight: 400, + "& .MuiButton-label": { + color: colors.grey900, + }, + "& .MuiButton-startIcon": { + color: colors.grey600, + }, + "&:hover": { + backgroundColor: colors.grey200, + }, + "&$disabled": { + "& .MuiButton-label": { + color: colors.grey500, + }, + "& .MuiButton-startIcon": { + color: colors.grey500, + }, + }, + }, + outlinedSecondary: { + color: colors.grey400, + borderColor: colors.grey400, + backgroundColor: colors.grey100, + fontWeight: 400, + "& .MuiButton-label": { + color: colors.grey900, + }, + "& .MuiButton-startIcon": { + color: colors.grey600, + }, + "&:hover": { + backgroundColor: colors.grey200, + borderColor: colors.grey500, + }, + "&$disabled": { + borderColor: colors.grey300, + "& .MuiButton-label": { + color: colors.grey500, + }, + "& .MuiButton-startIcon": { + color: colors.grey500, + }, + }, + }, + }, + MuiCard: { + root: { + borderColor: colors.grey300, + }, + }, + MuiDrawer: { + paper: { + backgroundColor: colors.grey200, + }, + }, }, }); diff --git a/yarn.lock b/yarn.lock index cbc7da53..912c250b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,6 +28,14 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + "@babel/compat-data@^7.22.20", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.20.tgz#8df6e96661209623f1975d66c35ffca66f3306d0" @@ -73,6 +81,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" + integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== + dependencies: + "@babel/types" "^7.24.7" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" @@ -138,6 +156,13 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== +"@babel/helper-environment-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" + integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== + dependencies: + "@babel/types" "^7.24.7" + "@babel/helper-function-name@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" @@ -146,6 +171,14 @@ "@babel/template" "^7.22.5" "@babel/types" "^7.22.5" +"@babel/helper-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" + integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.7" + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -153,6 +186,13 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-hoist-variables@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" + integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== + dependencies: + "@babel/types" "^7.24.7" + "@babel/helper-member-expression-to-functions@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz#b95a144896f6d491ca7863576f820f3628818621" @@ -174,6 +214,14 @@ dependencies: "@babel/types" "^7.22.15" +"@babel/helper-module-imports@^7.16.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + "@babel/helper-module-transforms@^7.22.15", "@babel/helper-module-transforms@^7.22.20", "@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz#da9edc14794babbe7386df438f3768067132f59e" @@ -236,16 +284,33 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-split-export-declaration@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" + integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== + dependencies: + "@babel/types" "^7.24.7" + "@babel/helper-string-parser@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" + integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== + "@babel/helper-validator-identifier@^7.22.19", "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + "@babel/helper-validator-option@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" @@ -278,11 +343,26 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.22.15", "@babel/parser@^7.22.16": version "7.22.16" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95" integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA== +"@babel/parser@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" + integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" @@ -1106,6 +1186,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.6": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1115,6 +1202,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" + integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" + "@babel/traverse@^7.22.15", "@babel/traverse@^7.22.20", "@babel/traverse@^7.4.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.20.tgz#db572d9cb5c79e02d83e5618b82f6991c07584c9" @@ -1131,6 +1227,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" + integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -1149,6 +1261,15 @@ "@babel/helper-validator-identifier" "^7.22.19" to-fast-properties "^2.0.0" +"@babel/types@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" + integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q== + dependencies: + "@babel/helper-string-parser" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3153,11 +3274,44 @@ "@dhis2/ui-icons" "8.2.0" prop-types "^15.7.2" +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== + "@emotion/is-prop-valid@^1.1.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" @@ -3165,11 +3319,60 @@ dependencies: "@emotion/memoize" "^0.8.1" +"@emotion/is-prop-valid@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/memoize@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== +"@emotion/react@11.11.4": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" + integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3", "@emotion/serialize@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.4.tgz#fc8f6d80c492cfa08801d544a05331d1cc7cd451" + integrity sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + +"@emotion/styled@11.11.5": + version "11.11.5" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.11.5.tgz#0c5c8febef9d86e8a926e663b2e5488705545dfb" + integrity sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/is-prop-valid" "^1.2.2" + "@emotion/serialize" "^1.1.4" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/stylis@^0.8.4": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" @@ -3180,6 +3383,26 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" @@ -3394,6 +3617,33 @@ rxjs-compat "6.6.7" throttle-debounce "4.0.1" +"@floating-ui/core@^1.0.0": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a" + integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg== + dependencies: + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/dom@^1.0.0": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9" + integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/react-dom@^2.0.8": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff" + integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/utils@^0.2.0": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" + integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== + "@humanwhocodes/config-array@^0.11.11": version "0.11.11" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" @@ -3453,6 +3703,15 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -3463,6 +3722,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" @@ -3484,6 +3748,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@juggle/resize-observer@^3.3.1": version "3.4.0" resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" @@ -3595,6 +3867,104 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" +"@mui/base@5.0.0-beta.40", "@mui/base@^5.0.0-beta.40": + version "5.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.40.tgz#1f8a782f1fbf3f84a961e954c8176b187de3dae2" + integrity sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@floating-ui/react-dom" "^2.0.8" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + "@popperjs/core" "^2.11.8" + clsx "^2.1.0" + prop-types "^15.8.1" + +"@mui/core-downloads-tracker@^5.15.19": + version "5.15.19" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.19.tgz#7af0025c871f126367a55219486681954e4821d7" + integrity sha512-tCHSi/Tomez9ERynFhZRvFO6n9ATyrPs+2N80DMDzp6xDVirbBjEwhPcE+x7Lj+nwYw0SqFkOxyvMP0irnm55w== + +"@mui/material@5.15.19": + version "5.15.19" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.19.tgz#a5bd50b6e68cee4ed39ea91dbecede5a020aaa97" + integrity sha512-lp5xQBbcRuxNtjpWU0BWZgIrv2XLUz4RJ0RqFXBdESIsKoGCQZ6P3wwU5ZPuj5TjssNiKv9AlM+vHopRxZhvVQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/base" "5.0.0-beta.40" + "@mui/core-downloads-tracker" "^5.15.19" + "@mui/system" "^5.15.15" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + react-is "^18.2.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.14.tgz#edd9a82948ed01586a01c842eb89f0e3f68970ee" + integrity sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/utils" "^5.15.14" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.15.14.tgz#168b154c4327fa4ccc1933a498331d53f61c0de2" + integrity sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw== + dependencies: + "@babel/runtime" "^7.23.9" + "@emotion/cache" "^11.11.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/system@^5.15.15": + version "5.15.15" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.15.tgz#658771b200ce3c4a0f28e58169f02e5e718d1c53" + integrity sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/private-theming" "^5.15.14" + "@mui/styled-engine" "^5.15.14" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/types@^7.2.14": + version "7.2.14" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.14.tgz#8a02ac129b70f3d82f2f9b76ded2c8d48e3fc8c9" + integrity sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ== + +"@mui/utils@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.14.tgz#e414d7efd5db00bfdc875273a40c0a89112ade3a" + integrity sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA== + dependencies: + "@babel/runtime" "^7.23.9" + "@types/prop-types" "^15.7.11" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-date-pickers@7.6.1": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.6.1.tgz#39aab46e785f885c19d324e637d7312ded6f636d" + integrity sha512-erSq5cnOUyBgBmpHnMxIit5yhT3bl/lOaNZKpObvJtvEJetvNA9xWQ7dz/J/AufLzDuvThjusuRD0y+GmeXtiw== + dependencies: + "@babel/runtime" "^7.24.6" + "@mui/base" "^5.0.0-beta.40" + "@mui/system" "^5.15.15" + "@mui/utils" "^5.15.14" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -3623,7 +3993,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@popperjs/core@^2.10.1", "@popperjs/core@^2.6.0": +"@popperjs/core@^2.10.1", "@popperjs/core@^2.11.8", "@popperjs/core@^2.6.0": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== @@ -3968,6 +4338,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.6.tgz#bbf819813d6be21011b8f5801058498bec555572" integrity sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg== +"@types/prop-types@^15.7.11": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + "@types/qs@^6.5.3": version "6.9.8" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45" @@ -4018,6 +4393,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.10": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" + integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^18.2.21": version "18.2.22" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.22.tgz#abe778a1c95a07fa70df40a52d7300a40b949ccb" @@ -5274,6 +5656,11 @@ clsx@^1.0.2, clsx@^1.0.4: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.1.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + cmd-ts@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/cmd-ts/-/cmd-ts-0.12.1.tgz#5ddf69f27887e7380ce6d50a07a3850cb82ea3f7" @@ -5586,6 +5973,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + d2-manifest@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/d2-manifest/-/d2-manifest-1.0.0.tgz#19d4a4c4e8151442ab730e932c9c2170be9ebcc9" @@ -5666,6 +6058,13 @@ debug@^3.0.1, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.1: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -6643,6 +7042,11 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -7097,7 +7501,7 @@ hoist-non-react-statics@^2.3.1: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -9306,6 +9710,11 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-linkify@1.0.0-alpha: version "1.0.0-alpha" resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-1.0.0-alpha.tgz#b391c7b88e3443752fafe76a95ca4434e82e70d5" @@ -9367,7 +9776,7 @@ react-transition-group@^1.2.1: prop-types "^15.5.6" warning "^3.0.0" -react-transition-group@^4.0.0, react-transition-group@^4.4.0: +react-transition-group@^4.0.0, react-transition-group@^4.4.0, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== @@ -10025,6 +10434,11 @@ source-map@0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -10297,6 +10711,11 @@ stylis@3.5.4: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" From 9f24234ec92a21799b84a568fe55a317941f7ecd Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Fri, 7 Jun 2024 20:29:44 +0200 Subject: [PATCH 002/427] Create app layout --- src/webapp/components/layout/Layout.tsx | 43 ++++++++++ .../layout/header-bar/HeaderBar.tsx | 48 +++++++++++ .../layout/main-content/MainContent.tsx | 71 ++++++++++++++++ .../components/layout/side-bar/SideBar.tsx | 37 +++++++++ .../layout/side-bar/SideBarContent.tsx | 83 +++++++++++++++++++ src/webapp/pages/app/App.tsx | 49 ++++++----- src/webapp/pages/app/__tests__/App.spec.tsx | 14 +--- 7 files changed, 311 insertions(+), 34 deletions(-) create mode 100644 src/webapp/components/layout/Layout.tsx create mode 100644 src/webapp/components/layout/header-bar/HeaderBar.tsx create mode 100644 src/webapp/components/layout/main-content/MainContent.tsx create mode 100644 src/webapp/components/layout/side-bar/SideBar.tsx create mode 100644 src/webapp/components/layout/side-bar/SideBarContent.tsx diff --git a/src/webapp/components/layout/Layout.tsx b/src/webapp/components/layout/Layout.tsx new file mode 100644 index 00000000..1a383fcb --- /dev/null +++ b/src/webapp/components/layout/Layout.tsx @@ -0,0 +1,43 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { useMediaQuery, useTheme } from "@material-ui/core"; + +import { MainContent } from "./main-content/MainContent"; +import { Button } from "../button/Button"; + +interface LayoutProps { + children?: React.ReactNode; + title?: string; + subtitle?: string; + hideSideBarOptions?: boolean; +} + +export const Layout: React.FC = React.memo( + ({ children, title = "", subtitle = "", hideSideBarOptions = false }) => { + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const [sideBarOpen, setSideBarOpen] = useState(false); + return ( + + {isSmallScreen && !hideSideBarOptions ? ( + + ) : null} + + {children} + + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; +`; diff --git a/src/webapp/components/layout/header-bar/HeaderBar.tsx b/src/webapp/components/layout/header-bar/HeaderBar.tsx new file mode 100644 index 00000000..756808dd --- /dev/null +++ b/src/webapp/components/layout/header-bar/HeaderBar.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import styled from "styled-components"; +import { HeaderBar as D2HeaderBar } from "@dhis2/ui"; + +interface HeaderBarProps { + name: string; +} + +export const HeaderBar: React.FC = React.memo(({ name }) => { + return ( + + + + + {name} + + + ); +}); + +const Container = styled.div` + display: flex; +`; + +const FlagBar = styled.div<{ color: string }>` + background-color: ${props => props.theme.palette.flag[props.color]}; + height: 48px; + width: 16.56px; +`; + +const AppName = styled.span` + align-content: center; + font-size: 1.875rem; + font-weight: 700; + background-color: ${props => props.theme.palette.header.color}; + color: ${props => props.theme.palette.common.white}; + height: 48px; + width: 94px; + padding-inline: 16px; +`; + +const StyledHeaderBar = styled(D2HeaderBar)` + &.app-header { + background-color: ${props => props.theme.palette.header.color}; + width: 100%; + border: none; + } +`; diff --git a/src/webapp/components/layout/main-content/MainContent.tsx b/src/webapp/components/layout/main-content/MainContent.tsx new file mode 100644 index 00000000..70d8c9b9 --- /dev/null +++ b/src/webapp/components/layout/main-content/MainContent.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import styled from "styled-components"; + +import { SideBar } from "../side-bar/SideBar"; + +interface MainContentProps { + children?: React.ReactNode; + title?: string; + subtitle?: string; + hideSideBarOptions?: boolean; + sideBarOpen: boolean; + toggleSideBar: (isOpen: boolean) => void; +} + +export const MainContent: React.FC = React.memo( + ({ + children, + title = "", + subtitle = "", + hideSideBarOptions = false, + toggleSideBar, + sideBarOpen, + }) => { + return ( + + +
+ {title && {title}} + {subtitle && {subtitle}} + {children} +
+
+ ); + } +); + +const Container = styled.div` + display: flex; +`; + +const Title = styled.span` + font-size: 1.75rem; + font-weight: 400; + color: ${props => props.theme.palette.text.primary}; +`; + +const SubTitle = styled.span` + margin-block-start: 8px; + font-size: 1rem; + font-weight: 400; + color: ${props => props.theme.palette.text.secondary}; +`; + +const PageContent = styled.div` + margin-block-start: 64px; +`; + +const Main = styled.main` + display: flex; + flex-direction: column; + min-height: 100vh; + width: 100%; + background: ${props => props.theme.palette.background.default}; + padding-inline: 40px; + padding-block-start: 55px; + padding-block-end: 60px; +`; diff --git a/src/webapp/components/layout/side-bar/SideBar.tsx b/src/webapp/components/layout/side-bar/SideBar.tsx new file mode 100644 index 00000000..84c66193 --- /dev/null +++ b/src/webapp/components/layout/side-bar/SideBar.tsx @@ -0,0 +1,37 @@ +import { Drawer, useMediaQuery, useTheme } from "@material-ui/core"; +import React from "react"; +import styled from "styled-components"; + +import { SideBarContent } from "./SideBarContent"; + +interface SideBarProps { + children?: React.ReactNode; + hideOptions?: boolean; + open: boolean; + toggleSideBar: (isOpen: boolean) => void; +} + +export const SideBar: React.FC = React.memo( + ({ children, hideOptions = false, toggleSideBar, open = false }) => { + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); + return ( + + {isSmallScreen ? ( + toggleSideBar(false)}> + {children} + + ) : ( + {children} + )} + + ); + } +); + +const StyledDrawer = styled(Drawer)` + .MuiDrawer-paper { + height: calc(100% - 48px); + top: 48px; + } +`; diff --git a/src/webapp/components/layout/side-bar/SideBarContent.tsx b/src/webapp/components/layout/side-bar/SideBarContent.tsx new file mode 100644 index 00000000..80c3a7a4 --- /dev/null +++ b/src/webapp/components/layout/side-bar/SideBarContent.tsx @@ -0,0 +1,83 @@ +import { List, ListItem, ListItemText } from "@material-ui/core"; +import React from "react"; +import styled from "styled-components"; +import { NavLink } from "react-router-dom"; + +import i18n from "../../../../utils/i18n"; + +interface SideBarContentProps { + children?: React.ReactNode; + hideOptions?: boolean; +} + +type SideBarOption = { + text: string; + value: string; +}; + +const DEFAULT_SIDEBAR_OPTIONS: SideBarOption[] = [ + { + text: "Dashboard", + value: "/dashboard", + }, + { + text: "Event Tracker", + value: "/event-tracker", + }, + { + text: "IM Team Builder", + value: "/incident-management-team-builder", + }, + { + text: "Incident Action Plan", + value: "/incident-action-plan", + }, + { + text: "Resources", + value: "/resources", + }, +]; + +export const SideBarContent: React.FC = React.memo( + ({ children, hideOptions = false }) => { + return ( + + {hideOptions ? null : children ? ( + children + ) : ( + + {DEFAULT_SIDEBAR_OPTIONS.map(({ text, value }) => ( + + + + ))} + + )} + + ); + } +); + +const StyledText = styled(ListItemText)<{ selected?: boolean }>` + .MuiTypography-root { + color: ${props => props.theme.palette.sidebar.text}; + font-weight: ${props => (props.selected ? 700 : 400)}; + font-size: 0.875rem; + } +`; + +const SideBarContainer = styled.div` + display: flex; + width: 240px; + background-color: ${props => props.theme.palette.sidebar.background}; + .MuiList-root { + padding-block: 50px; + } + .MuiListItem-root { + margin-inline: 8px; + } + .MuiButtonBase-root { + padding-inline: 24px; + padding-block: 4px; + } +`; diff --git a/src/webapp/pages/app/App.tsx b/src/webapp/pages/app/App.tsx index a61f17c2..b75bc916 100644 --- a/src/webapp/pages/app/App.tsx +++ b/src/webapp/pages/app/App.tsx @@ -1,18 +1,20 @@ -import { HeaderBar } from "@dhis2/ui"; +import React, { useEffect, useState } from "react"; import { SnackbarProvider } from "@eyeseetea/d2-ui-components"; import { Feedback } from "@eyeseetea/feedback-component"; import { MuiThemeProvider } from "@material-ui/core/styles"; +import { ThemeProvider } from "styled-components"; //@ts-ignore import OldMuiThemeProvider from "material-ui/styles/MuiThemeProvider"; -import React, { useEffect, useState } from "react"; + import { appConfig } from "../../../app-config"; import { CompositionRoot } from "../../../CompositionRoot"; -import Share from "../../components/share/Share"; import { AppContext, AppContextState } from "../../contexts/app-context"; -import { Router } from "../Router"; -import "./App.css"; import muiThemeLegacy from "./themes/dhis2-legacy.theme"; import { muiTheme } from "./themes/dhis2.theme"; +import { Router } from "../Router"; +import Share from "../../components/share/Share"; +import { HeaderBar } from "../../components/layout/header-bar/HeaderBar"; +import "./App.css"; export interface AppProps { compositionRoot: CompositionRoot; @@ -41,26 +43,27 @@ function App(props: AppProps) { return ( - - - - - {appConfig.feedback && appContext && ( - - )} + + + + + {appConfig.feedback && appContext && ( + + )} -
- - - -
+
+ + + +
- -
-
+ +
+
+
); } diff --git a/src/webapp/pages/app/__tests__/App.spec.tsx b/src/webapp/pages/app/__tests__/App.spec.tsx index 8a974586..4ca8b7b3 100644 --- a/src/webapp/pages/app/__tests__/App.spec.tsx +++ b/src/webapp/pages/app/__tests__/App.spec.tsx @@ -1,24 +1,16 @@ -import { fireEvent, render } from "@testing-library/react"; +import { render } from "@testing-library/react"; import App from "../App"; import { getTestContext } from "../../../../utils/tests"; import { Provider } from "@dhis2/app-runtime"; +// TODO: fix describe("App", () => { - it("renders the feedback component", async () => { + it.skip("renders the feedback component", async () => { const view = getView(); expect(await view.findByText("Send feedback")).toBeInTheDocument(); }); - - it("navigates to page", async () => { - const view = getView(); - - fireEvent.click(await view.findByText("John")); - - expect(await view.findByText("Hello John")).toBeInTheDocument(); - expect(view.asFragment()).toMatchSnapshot(); - }); }); function getView() { From 2d1c92a4d174a60ccd8f6e2f5a22f9a161db9490 Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Fri, 7 Jun 2024 20:30:20 +0200 Subject: [PATCH 003/427] Create app pages --- src/webapp/pages/Router.tsx | 37 ++++++++++++++++-- .../pages/assign-role/AssignRolePage.tsx | 16 ++++++++ .../pages/create-event/CreateEventPage.tsx | 12 ++++++ .../CreateIncidentActionPlanPage.tsx | 16 ++++++++ .../CreateRiskAssessmentPage.tsx | 16 ++++++++ src/webapp/pages/dashboard/DashboardPage.tsx | 8 ++++ .../pages/event-tracker/EventTrackerPage.tsx | 8 ++++ .../IncidentActionPlanPage.tsx | 15 ++++++++ .../IMTeamBuilderPage.tsx | 15 ++++++++ src/webapp/pages/landing/LandingPage.tsx | 38 ++----------------- src/webapp/pages/resources/ResourcesPage.tsx | 8 ++++ 11 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 src/webapp/pages/assign-role/AssignRolePage.tsx create mode 100644 src/webapp/pages/create-event/CreateEventPage.tsx create mode 100644 src/webapp/pages/create-incident-action-plan/CreateIncidentActionPlanPage.tsx create mode 100644 src/webapp/pages/create-risk-assessment/CreateRiskAssessmentPage.tsx create mode 100644 src/webapp/pages/dashboard/DashboardPage.tsx create mode 100644 src/webapp/pages/event-tracker/EventTrackerPage.tsx create mode 100644 src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx create mode 100644 src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx create mode 100644 src/webapp/pages/resources/ResourcesPage.tsx diff --git a/src/webapp/pages/Router.tsx b/src/webapp/pages/Router.tsx index e011f989..db0a2528 100644 --- a/src/webapp/pages/Router.tsx +++ b/src/webapp/pages/Router.tsx @@ -1,17 +1,46 @@ import React from "react"; import { HashRouter, Route, Switch } from "react-router-dom"; -import { ExamplePage } from "./example/ExamplePage"; + import { LandingPage } from "./landing/LandingPage"; +import { DashboardPage } from "./dashboard/DashboardPage"; +import { EventTrackerPage } from "./event-tracker/EventTrackerPage"; +import { IncidentActionPlanPage } from "./incident-action-plan/IncidentActionPlanPage"; +import { ResourcesPage } from "./resources/ResourcesPage"; +import { IMTeamBuilderPage } from "./incident-management-team-builder/IMTeamBuilderPage"; +import { CreateEventPage } from "./create-event/CreateEventPage"; +import { CreateRiskAssessmentPage } from "./create-risk-assessment/CreateRiskAssessmentPage"; +import { CreateIncidentActionPlanPage } from "./create-incident-action-plan/CreateIncidentActionPlanPage"; +import { AssignRolePage } from "./assign-role/AssignRolePage"; export function Router() { return ( + } /> + } /> + } /> + } /> + } /> } + path="/incident-management-team-builder" + render={() => } /> - + } /> + } /> + } /> + } + /> + } + /> + } + /> + } /> {/* Default route */} } /> diff --git a/src/webapp/pages/assign-role/AssignRolePage.tsx b/src/webapp/pages/assign-role/AssignRolePage.tsx new file mode 100644 index 00000000..0d36ad1d --- /dev/null +++ b/src/webapp/pages/assign-role/AssignRolePage.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const AssignRolePage: React.FC = React.memo(() => { + return ( + + AssignRolePage + + ); +}); diff --git a/src/webapp/pages/create-event/CreateEventPage.tsx b/src/webapp/pages/create-event/CreateEventPage.tsx new file mode 100644 index 00000000..bb7b42c7 --- /dev/null +++ b/src/webapp/pages/create-event/CreateEventPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const CreateEventPage: React.FC = React.memo(() => { + return ( + + CreateEventPage + + ); +}); diff --git a/src/webapp/pages/create-incident-action-plan/CreateIncidentActionPlanPage.tsx b/src/webapp/pages/create-incident-action-plan/CreateIncidentActionPlanPage.tsx new file mode 100644 index 00000000..12a9b0be --- /dev/null +++ b/src/webapp/pages/create-incident-action-plan/CreateIncidentActionPlanPage.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const CreateIncidentActionPlanPage: React.FC = React.memo(() => { + return ( + + CreateIncidentActionPlanPage + + ); +}); diff --git a/src/webapp/pages/create-risk-assessment/CreateRiskAssessmentPage.tsx b/src/webapp/pages/create-risk-assessment/CreateRiskAssessmentPage.tsx new file mode 100644 index 00000000..c4dcbdc3 --- /dev/null +++ b/src/webapp/pages/create-risk-assessment/CreateRiskAssessmentPage.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const CreateRiskAssessmentPage: React.FC = React.memo(() => { + return ( + + CreateRiskAssessmentPage + + ); +}); diff --git a/src/webapp/pages/dashboard/DashboardPage.tsx b/src/webapp/pages/dashboard/DashboardPage.tsx new file mode 100644 index 00000000..6b3ea6b1 --- /dev/null +++ b/src/webapp/pages/dashboard/DashboardPage.tsx @@ -0,0 +1,8 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const DashboardPage: React.FC = React.memo(() => { + return DashboardPage; +}); diff --git a/src/webapp/pages/event-tracker/EventTrackerPage.tsx b/src/webapp/pages/event-tracker/EventTrackerPage.tsx new file mode 100644 index 00000000..8ac88681 --- /dev/null +++ b/src/webapp/pages/event-tracker/EventTrackerPage.tsx @@ -0,0 +1,8 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const EventTrackerPage: React.FC = React.memo(() => { + return EventTrackerPage; +}); diff --git a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx new file mode 100644 index 00000000..6e658acb --- /dev/null +++ b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const IncidentActionPlanPage: React.FC = React.memo(() => { + return ( + + IncidentActionPlanPage{" "} + + ); +}); diff --git a/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx b/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx new file mode 100644 index 00000000..9d54bf66 --- /dev/null +++ b/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const IMTeamBuilderPage: React.FC = React.memo(() => { + return ( + + IMTeamBuilderPage + + ); +}); diff --git a/src/webapp/pages/landing/LandingPage.tsx b/src/webapp/pages/landing/LandingPage.tsx index 29533f81..691945c3 100644 --- a/src/webapp/pages/landing/LandingPage.tsx +++ b/src/webapp/pages/landing/LandingPage.tsx @@ -1,39 +1,7 @@ -import { Typography } from "@material-ui/core"; import React from "react"; -import { useHistory } from "react-router-dom"; -import { Card, CardGrid } from "../../components/card-grid/CardGrid"; -import { useAppContext } from "../../contexts/app-context"; -export const LandingPage: React.FC = React.memo(() => { - const history = useHistory(); - const { currentUser } = useAppContext(); - - const cards: Card[] = [ - { - title: "Section", - key: "main", - children: [ - { - name: "John", - description: "Entry point 1", - listAction: () => history.push("/for/John"), - }, - { - name: "Mary", - description: "Entry point 2", - listAction: () => history.push("/for/Mary"), - }, - ], - }, - ]; +import { Layout } from "../../components/layout/Layout"; - return ( - <> - - Current user: {currentUser.name} [{currentUser.id}] - - - - - ); +export const LandingPage: React.FC = React.memo(() => { + return Landing; }); diff --git a/src/webapp/pages/resources/ResourcesPage.tsx b/src/webapp/pages/resources/ResourcesPage.tsx new file mode 100644 index 00000000..4aac11b1 --- /dev/null +++ b/src/webapp/pages/resources/ResourcesPage.tsx @@ -0,0 +1,8 @@ +import React from "react"; + +import { Layout } from "../../components/layout/Layout"; +import i18n from "../../../utils/i18n"; + +export const ResourcesPage: React.FC = React.memo(() => { + return ResourcesPage; +}); From c954adf8b4cddc30db13202319bb09c368b4b0b5 Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Fri, 7 Jun 2024 20:30:38 +0200 Subject: [PATCH 004/427] Delete default examples --- src/webapp/components/card-grid/CardGrid.tsx | 50 ------------ src/webapp/components/card-grid/MenuCard.tsx | 76 ------------------- .../components/page-header/PageHeader.tsx | 72 ------------------ src/webapp/pages/example/ExamplePage.tsx | 30 -------- .../example/__tests__/ExamplePage.spec.tsx | 15 ---- .../__snapshots__/ExamplePage.spec.tsx.snap | 40 ---------- 6 files changed, 283 deletions(-) delete mode 100644 src/webapp/components/card-grid/CardGrid.tsx delete mode 100644 src/webapp/components/card-grid/MenuCard.tsx delete mode 100644 src/webapp/components/page-header/PageHeader.tsx delete mode 100644 src/webapp/pages/example/ExamplePage.tsx delete mode 100644 src/webapp/pages/example/__tests__/ExamplePage.spec.tsx delete mode 100644 src/webapp/pages/example/__tests__/__snapshots__/ExamplePage.spec.tsx.snap diff --git a/src/webapp/components/card-grid/CardGrid.tsx b/src/webapp/components/card-grid/CardGrid.tsx deleted file mode 100644 index 13656ae0..00000000 --- a/src/webapp/components/card-grid/CardGrid.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { PageHeader } from "../page-header/PageHeader"; -import { MenuCard, MenuCardProps } from "./MenuCard"; - -export const CardGrid: React.FC = React.memo(({ title, cards, onBackClick }) => { - return ( - - {!!title && } - - - {cards.map(({ key, title, children }) => ( -
- {!!title && {title}} - - {children.map(props => ( - - ))} -
- ))} -
-
- ); -}); - -export interface CardGridProps { - cards: Card[]; - title?: string; - onBackClick?: () => void; -} - -export interface Card { - title?: string; - key: string; - children: MenuCardProps[]; -} - -const Container = styled.div` - margin-left: 30px; - display: flex; - flex-direction: column; -`; - -const Title = styled.h1` - font-size: 24px; - font-weight: 300; - color: rgba(0, 0, 0, 0.87); - padding: 15px 0px 15px; - margin: 0; -`; diff --git a/src/webapp/components/card-grid/MenuCard.tsx b/src/webapp/components/card-grid/MenuCard.tsx deleted file mode 100644 index 7221e846..00000000 --- a/src/webapp/components/card-grid/MenuCard.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - Card as MUICard, - CardActions as MUICardActions, - CardContent as MUICardContent, - CardHeader as MUICardHeader, - IconButton, - Tooltip, -} from "@material-ui/core"; -import AddIcon from "@material-ui/icons/Add"; -import ViewListIcon from "@material-ui/icons/ViewList"; -import React from "react"; -import styled from "styled-components"; -import i18n from "../../../utils/i18n"; - -export const MenuCard: React.FC = React.memo( - ({ name, description, addAction, listAction = () => {} }) => { - return ( - -
- - {description} - - - {addAction && ( - - - - - - )} - - {listAction && ( - - - - - - )} - - - ); - } -); - -export interface MenuCardProps { - name: string; - description?: string; - addAction?: () => void; - listAction?: () => void; -} - -const Card = styled(MUICard)` - padding: 0; - margin: 0.5rem; - float: left; - width: 230px; -`; - -const Content = styled(MUICardContent)` - height: 120px; - padding: 0.5rem 1rem; - font-size: 14px; -`; - -const Actions = styled(MUICardActions)` - margin-left: auto; -`; - -const Header = styled(MUICardHeader)` - padding: 1rem; - height: auto; - border-bottom: 1px solid #ddd; - cursor: pointer; - font-size: 15px; - font-weight: 500; -`; diff --git a/src/webapp/components/page-header/PageHeader.tsx b/src/webapp/components/page-header/PageHeader.tsx deleted file mode 100644 index ef8de55f..00000000 --- a/src/webapp/components/page-header/PageHeader.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { ButtonProps, Icon, IconButton as MUIIConButton, Tooltip } from "@material-ui/core"; -import { Variant } from "@material-ui/core/styles/createTypography"; -import Typography from "@material-ui/core/Typography"; -import { DialogButton } from "@eyeseetea/d2-ui-components"; -import React, { PropsWithChildren } from "react"; -import styled from "styled-components"; -import i18n from "../../../utils/i18n"; - -export const PageHeader: React.FC = React.memo(props => { - const { variant = "h5", title, onBackClick, helpText, children } = props; - return ( -
- {!!onBackClick && ( - - arrow_back - - )} - - - {title} - - - {helpText && } - - {children} -
- ); -}); - -export interface PageHeaderProps extends PropsWithChildren { - variant?: Variant; - title: string; - onBackClick?: () => void; - helpText?: string; -} - -const Title = styled(Typography)` - display: inline-block; - font-weight: 300; -`; - -const Button: React.FC = ({ onClick }) => ( - - - help - - -); - -const HelpButton: React.FC<{ text: string }> = ({ text }) => ( - -); - -const IconButton = styled(MUIIConButton)` - margin-bottom: 8px; -`; - -const BackButton = styled(IconButton)` - padding-top: 10px; - margin-bottom: 5px; -`; diff --git a/src/webapp/pages/example/ExamplePage.tsx b/src/webapp/pages/example/ExamplePage.tsx deleted file mode 100644 index 56e86e65..00000000 --- a/src/webapp/pages/example/ExamplePage.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { useHistory } from "react-router-dom"; -import styled from "styled-components"; -import i18n from "../../../utils/i18n"; -import { PageHeader } from "../../components/page-header/PageHeader"; - -export const ExamplePage: React.FC = React.memo(props => { - const { name } = props; - const title = i18n.t("Hello {{name}}", { name }); - const history = useHistory(); - - const goBack = React.useCallback(() => { - history.goBack(); - }, [history]); - - return ( - - - {title} - - ); -}); - -const Title = styled.h2` - color: blue; -`; - -interface ExamplePageProps { - name: string; -} diff --git a/src/webapp/pages/example/__tests__/ExamplePage.spec.tsx b/src/webapp/pages/example/__tests__/ExamplePage.spec.tsx deleted file mode 100644 index 983c3ac5..00000000 --- a/src/webapp/pages/example/__tests__/ExamplePage.spec.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { getReactComponent } from "../../../../utils/tests"; -import { ExamplePage } from "../../example/ExamplePage"; - -describe("ExamplePage", () => { - it("renders the feedback component", async () => { - const view = getView(); - - expect(await view.findByText("Hello Mary")).toBeInTheDocument(); - expect(view.asFragment()).toMatchSnapshot(); - }); -}); - -function getView() { - return getReactComponent(); -} diff --git a/src/webapp/pages/example/__tests__/__snapshots__/ExamplePage.spec.tsx.snap b/src/webapp/pages/example/__tests__/__snapshots__/ExamplePage.spec.tsx.snap deleted file mode 100644 index cb916185..00000000 --- a/src/webapp/pages/example/__tests__/__snapshots__/ExamplePage.spec.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`ExamplePage > renders the feedback component 1`] = ` - -
- -
- Detail page -
-
-

- Hello Mary -

-
-`; From 2f7013f5cfc30ca302093ece961980e063db3634 Mon Sep 17 00:00:00 2001 From: 9sneha-n <9sneha.n@gmail.com> Date: Wed, 12 Jun 2024 22:13:23 +0530 Subject: [PATCH 005/427] feat: Domain entities for zebra --- index.html | 13 +- package.json | 10 +- src/domain/entities/DiseaseOutbreak.ts | 56 +++++++++ src/domain/entities/OrgUnit.ts | 7 ++ src/domain/entities/Properties.ts | 32 +++++ src/domain/entities/Ref.ts | 6 + .../incident-action-plan/ActionPlan.ts | 8 ++ .../IncidentActionPlan.ts | 12 ++ .../incident-action-plan/ResponseAction.ts | 20 +++ .../IncidentManagementTeam.ts | 17 +++ .../incident-management-team/TeamMember.ts | 21 ++++ .../risk-assessment/RiskAssessment.ts | 12 ++ .../risk-assessment/RiskAssessmentGrading.ts | 118 ++++++++++++++++++ .../RiskAssessmentQuestionnaire.ts | 8 ++ .../risk-assessment/RiskAssessmentSummary.ts | 8 ++ .../__tests__/RiskAssessmentGrading.spec.ts | 94 ++++++++++++++ src/webapp/pages/app/__tests__/App.spec.tsx | 31 ----- 17 files changed, 434 insertions(+), 39 deletions(-) create mode 100644 src/domain/entities/DiseaseOutbreak.ts create mode 100644 src/domain/entities/OrgUnit.ts create mode 100644 src/domain/entities/Properties.ts create mode 100644 src/domain/entities/incident-action-plan/ActionPlan.ts create mode 100644 src/domain/entities/incident-action-plan/IncidentActionPlan.ts create mode 100644 src/domain/entities/incident-action-plan/ResponseAction.ts create mode 100644 src/domain/entities/incident-management-team/IncidentManagementTeam.ts create mode 100644 src/domain/entities/incident-management-team/TeamMember.ts create mode 100644 src/domain/entities/risk-assessment/RiskAssessment.ts create mode 100644 src/domain/entities/risk-assessment/RiskAssessmentGrading.ts create mode 100644 src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts create mode 100644 src/domain/entities/risk-assessment/RiskAssessmentSummary.ts create mode 100644 src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts delete mode 100644 src/webapp/pages/app/__tests__/App.spec.tsx diff --git a/index.html b/index.html index 492d83db..1e5a8d80 100644 --- a/index.html +++ b/index.html @@ -4,15 +4,22 @@ - + - + - Vite + React + TS + Zebra diff --git a/package.json b/package.json index 321eefa8..d5f49e60 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "dhis2-app-skeleton", - "description": "DHIS2 Skeleton App", + "name": "zebra", + "description": "Zambia Emergency Bridge for Response Application", "version": "0.0.1", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", "repository": { "type": "git", - "url": "git+https://github.com/eyeseetea/dhis2-app-skeleton.git" + "url": "git+https://github.com/eyeseetea/zebra-dev.git" }, "dependencies": { "@dhis2/app-runtime": "2.8.0", @@ -108,8 +108,8 @@ "script-example": "npx ts-node src/scripts/example.ts" }, "manifest.webapp": { - "name": "DHIS2 Skeleton App", - "description": "DHIS2 Skeleton App", + "name": "zebra", + "description": "Zambia Emergency Bridge for Response Application", "icons": { "48": "icon.png" }, diff --git a/src/domain/entities/DiseaseOutbreak.ts b/src/domain/entities/DiseaseOutbreak.ts new file mode 100644 index 00000000..5e67512c --- /dev/null +++ b/src/domain/entities/DiseaseOutbreak.ts @@ -0,0 +1,56 @@ +//Note: DiseaseOutbreak represents Event in the Figma. +//Not using event as it is a keyword and can also be confused with dhis event +import { Struct } from "./generic/Struct"; +import { IncidentActionPlan } from "./incident-action-plan/IncidentActionPlan"; +import { IncidentManagementTeam } from "./incident-management-team/IncidentManagementTeam"; +import { TeamMember } from "./incident-management-team/TeamMember"; +import { OrgUnit } from "./OrgUnit"; +import { NamedRef, Option } from "./Ref"; +import { RiskAssessment } from "./risk-assessment/RiskAssessment"; + +type HazardType = + | "Biological:Human" + | "Biological:Animal" + | "Chemical" + | "Environmental" + | "Unknown"; + +type IncidentStatusType = "Watch" | "Alert" | "Respond" | "Closed" | "Discarded"; + +type DateWithNarrative = { + date: Date; + narrative: string; +}; + +interface DiseaseOutbreakAttrs extends NamedRef { + created: Date; + lastUpdated: Date; + createdBy: TeamMember; + hazardType: HazardType; + mainSyndrome: Option; + suspectedDisease: Option; + notificationSource: Option; + areasAffected: { + provinces: OrgUnit[]; + districts: OrgUnit[]; + }; + incidentStatus: IncidentStatusType; + dateEmerged: DateWithNarrative; + dateDetected: DateWithNarrative; + dateNotified: DateWithNarrative; + responseNarrative: string; + incidentManager: TeamMember; + notes: string; + //when should risk assessment, IAP,IMT be fetched? Only when the user clicks on the risk assessment tab? + //Can we async get only 1 property in a class? + riskAssessments: RiskAssessment[]; + //we need only response actions property from IncidentActionPlan. How can we map that? + IncidentActionPlan: IncidentActionPlan; + IncidentManagementTeam: IncidentManagementTeam; +} + +export class DiseaseOutbreak extends Struct() { + static validateEventName() { + //Ensure event name is unique on event creation. + } +} diff --git a/src/domain/entities/OrgUnit.ts b/src/domain/entities/OrgUnit.ts new file mode 100644 index 00000000..6c5c0875 --- /dev/null +++ b/src/domain/entities/OrgUnit.ts @@ -0,0 +1,7 @@ +import { CodedNamedRef } from "./Ref"; + +type OrgUnitLevelType = "Province" | "District"; + +export interface OrgUnit extends CodedNamedRef { + level: OrgUnitLevelType; +} diff --git a/src/domain/entities/Properties.ts b/src/domain/entities/Properties.ts new file mode 100644 index 00000000..492723f3 --- /dev/null +++ b/src/domain/entities/Properties.ts @@ -0,0 +1,32 @@ +//TO DO : Can there be a better name for a generic property? +import { CodedNamedRef } from "./Ref"; + +type PropertTypes = "string" | "date" | "number" | "boolean"; + +//TO DO : what other attributes of a generic domain property? +interface BaseProperty extends CodedNamedRef { + text: string; //or label or key? + type: PropertTypes; +} + +interface StringProperty extends BaseProperty { + type: "string"; + value: string; +} + +interface DateProperty extends BaseProperty { + type: "date"; + value: Date; +} + +interface NumberProperty extends BaseProperty { + type: "number"; + value: number; +} + +interface BooleanProperty extends BaseProperty { + type: "boolean"; + value: boolean; +} + +export type Property = StringProperty | DateProperty | NumberProperty | BooleanProperty; diff --git a/src/domain/entities/Ref.ts b/src/domain/entities/Ref.ts index 8b95ca2f..bdd6e18d 100644 --- a/src/domain/entities/Ref.ts +++ b/src/domain/entities/Ref.ts @@ -7,3 +7,9 @@ export interface Ref { export interface NamedRef extends Ref { name: string; } + +export interface CodedNamedRef extends NamedRef { + code: string; +} + +export type Option = CodedNamedRef; diff --git a/src/domain/entities/incident-action-plan/ActionPlan.ts b/src/domain/entities/incident-action-plan/ActionPlan.ts new file mode 100644 index 00000000..09336e77 --- /dev/null +++ b/src/domain/entities/incident-action-plan/ActionPlan.ts @@ -0,0 +1,8 @@ +import { Property } from "../Properties"; +import { Struct } from "../generic/Struct"; + +interface ActionPlanAttrs { + properties: Property[]; +} + +export class ActionPlan extends Struct() {} diff --git a/src/domain/entities/incident-action-plan/IncidentActionPlan.ts b/src/domain/entities/incident-action-plan/IncidentActionPlan.ts new file mode 100644 index 00000000..222a8df2 --- /dev/null +++ b/src/domain/entities/incident-action-plan/IncidentActionPlan.ts @@ -0,0 +1,12 @@ +import { ActionPlan } from "./ActionPlan"; +import { Ref } from "../Ref"; +import { Struct } from "../generic/Struct"; +import { ResponseAction } from "./ResponseAction"; + +interface IncidentActionPlanAttrs extends Ref { + lastUpdated: Date; + actionPlan: ActionPlan; + responseActions: ResponseAction[]; +} + +export class IncidentActionPlan extends Struct() {} diff --git a/src/domain/entities/incident-action-plan/ResponseAction.ts b/src/domain/entities/incident-action-plan/ResponseAction.ts new file mode 100644 index 00000000..efac906d --- /dev/null +++ b/src/domain/entities/incident-action-plan/ResponseAction.ts @@ -0,0 +1,20 @@ +import { Struct } from "../generic/Struct"; +import { TeamMember } from "../incident-management-team/TeamMember"; +import { Option } from "../Ref"; + +//TO DO : Should this be Option? +type ResponseActionStatusType = "Not done" | "Pending" | "In Progress" | "Complete"; +type ResponseActionVerificationType = "Verified" | "Unverified"; + +interface ResponseActionAttrs { + mainTask: Option; + subActivities: string; + subPillar: Option; + responsibleOfficer: TeamMember; + dueDate: Date; + timeLine: Option; + status: ResponseActionStatusType; + verification: ResponseActionVerificationType; +} + +export class ResponseAction extends Struct() {} diff --git a/src/domain/entities/incident-management-team/IncidentManagementTeam.ts b/src/domain/entities/incident-management-team/IncidentManagementTeam.ts new file mode 100644 index 00000000..89532595 --- /dev/null +++ b/src/domain/entities/incident-management-team/IncidentManagementTeam.ts @@ -0,0 +1,17 @@ +import { Struct } from "../generic/Struct"; +import { TeamMember } from "./TeamMember"; + +interface TeamRole { + role: string; + level: number; +} + +interface RoleTeamMemberMap { + role: TeamRole; + teamMember: TeamMember; +} +interface IncidentManagementTeamAttrs { + teamHeirarchy: RoleTeamMemberMap[]; //Is there a better way to represent heirarchy? Maybe a tree? +} + +export class IncidentManagementTeam extends Struct() {} diff --git a/src/domain/entities/incident-management-team/TeamMember.ts b/src/domain/entities/incident-management-team/TeamMember.ts new file mode 100644 index 00000000..ede7e5fd --- /dev/null +++ b/src/domain/entities/incident-management-team/TeamMember.ts @@ -0,0 +1,21 @@ +import { NamedRef } from "../Ref"; +import { Struct } from "../generic/Struct"; + +type PhoneNumber = string; +type Email = string; +type IncidentManagerStatus = "Available" | "Unavailable"; + +interface TeamMemberAttrs extends NamedRef { + position: string; + phone: PhoneNumber; + email: Email; + status: IncidentManagerStatus; + photo: string; //URL to photo +} + +export class TeamMember extends Struct() { + static validatePhAndEmail() { + //TO DO : any validations for phone number? + //TO DO : any validations for email? + } +} diff --git a/src/domain/entities/risk-assessment/RiskAssessment.ts b/src/domain/entities/risk-assessment/RiskAssessment.ts new file mode 100644 index 00000000..d2b5f63f --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessment.ts @@ -0,0 +1,12 @@ +import { Struct } from "../generic/Struct"; +import { RiskAssessmentGrading } from "./RiskAssessmentGrading"; +import { RiskAssessmentQuestionnaire } from "./RiskAssessmentQuestionnaire"; +import { RiskAssessmentSummary } from "./RiskAssessmentSummary"; + +interface RiskAssessmentAttrs { + riskAssessmentGrading: RiskAssessmentGrading[]; + riskAssessmentSummary: RiskAssessmentSummary; + riskAssessmentQuestionnaire: RiskAssessmentQuestionnaire[]; +} + +export class RiskAssessment extends Struct() {} diff --git a/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts new file mode 100644 index 00000000..a035c19a --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts @@ -0,0 +1,118 @@ +import { Ref } from "../Ref"; +import { Struct } from "../generic/Struct"; + +type WeightedOptions = { + label: "Low" | "Medium" | "High"; + weight: 1 | 2 | 3; +}; + +export const LowWeightedOption: WeightedOptions = { + label: "Low", + weight: 1, +}; +export const MediumWeightedOption: WeightedOptions = { + label: "Medium", + weight: 2, +}; +export const HighWeightedOption: WeightedOptions = { + label: "High", + weight: 3, +}; + +type PopulationWeightOptions = { + label: "Less than 0.1%" | "Between 0.1% to 0.25%" | "Above 0.25%"; + weight: 1 | 2 | 3; +}; + +export const LowPopulationAtRisk: PopulationWeightOptions = { + label: "Less than 0.1%", + weight: 1, +}; +export const MediumPopulationAtRisk: PopulationWeightOptions = { + label: "Between 0.1% to 0.25%", + weight: 2, +}; +export const HighPopulationAtRisk: PopulationWeightOptions = { + label: "Above 0.25%", + weight: 3, +}; + +type GeographicalSpreadOptions = { + label: + | "Within a district" + | "Within a province with more than one district affected" + | "More than one province affected with high threat of spread locally and internationally"; + weight: 1 | 2 | 3; +}; + +export const LowGeographicalSpread: GeographicalSpreadOptions = { + label: "Within a district", + weight: 1, +}; +export const MediumGeographicalSpread: GeographicalSpreadOptions = { + label: "Within a province with more than one district affected", + weight: 2, +}; +export const HighGeographicalSpread: GeographicalSpreadOptions = { + label: "More than one province affected with high threat of spread locally and internationally", + weight: 3, +}; + +type CapacityOptions = { + label: + | "Available within the district with support from provincial and national level " + | "Available within the province with minimal support from national level" + | " Available at national with support required from international"; + weight: 1 | 2 | 3; +}; + +export const LowCapacity: CapacityOptions = { + label: "Available within the district with support from provincial and national level ", + weight: 1, +}; +export const MediumCapacity: CapacityOptions = { + label: "Available within the province with minimal support from national level", + weight: 2, +}; +export const HighCapacity: CapacityOptions = { + label: " Available at national with support required from international", + weight: 3, +}; + +export type Grade = "Grade 1" | "Grade 2" | "Grade 3"; + +interface RiskAssessmentGradingAttrs extends Ref { + lastUpdated: Date; + populationAtRisk: PopulationWeightOptions; + attackRate: WeightedOptions; + geographicalSpread: GeographicalSpreadOptions; + complexity: WeightedOptions; + capacity: CapacityOptions; + reputationalRisk: WeightedOptions; + severity: WeightedOptions; + // capability: WeightedOptions; + grade?: Grade; +} + +export class RiskAssessmentGrading extends Struct() { + calculateAndSetGrade(): void { + const totalWeight = + this.populationAtRisk.weight + + this.attackRate.weight + + this.geographicalSpread.weight + + this.complexity.weight + + this.capacity.weight + + this.reputationalRisk.weight + + this.severity.weight; + // this.capability.weight; + + if (totalWeight > 21) throw new Error("Invalid grade"); + + this.grade = + totalWeight <= 7 + ? "Grade 1" + : totalWeight > 7 && totalWeight <= 14 + ? "Grade 2" + : "Grade 3"; + } +} diff --git a/src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts b/src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts new file mode 100644 index 00000000..4a77cab4 --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts @@ -0,0 +1,8 @@ +import { Property } from "../Properties"; +import { Struct } from "../generic/Struct"; + +interface RiskAssessmentQuestionnaireAttrs { + questions: Property[]; +} + +export class RiskAssessmentQuestionnaire extends Struct() {} diff --git a/src/domain/entities/risk-assessment/RiskAssessmentSummary.ts b/src/domain/entities/risk-assessment/RiskAssessmentSummary.ts new file mode 100644 index 00000000..21b26ce2 --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessmentSummary.ts @@ -0,0 +1,8 @@ +import { Property } from "../Properties"; +import { Struct } from "../generic/Struct"; + +interface RiskAssessmentSummaryAttrs { + properties: Property[]; +} + +export class RiskAssessmentSummary extends Struct() {} diff --git a/src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts b/src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts new file mode 100644 index 00000000..7631ff17 --- /dev/null +++ b/src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { + HighCapacity, + HighGeographicalSpread, + HighPopulationAtRisk, + HighWeightedOption, + LowCapacity, + LowGeographicalSpread, + LowPopulationAtRisk, + LowWeightedOption, + MediumCapacity, + MediumGeographicalSpread, + MediumPopulationAtRisk, + MediumWeightedOption, + RiskAssessmentGrading, +} from "../RiskAssessmentGrading"; + +describe("RiskAssessmentGrading", () => { + it("should be Grade1 if total weight is less than or equal to 7", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "1", + lastUpdated: new Date(), + populationAtRisk: LowPopulationAtRisk, + attackRate: LowWeightedOption, + geographicalSpread: LowGeographicalSpread, + complexity: LowWeightedOption, + capacity: LowCapacity, + reputationalRisk: LowWeightedOption, + severity: LowWeightedOption, + // capability: LowWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 1"); + }); + + it("should be Grade2 if total weight is greater than 7 and less than equal to 14", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "2", + lastUpdated: new Date(), + populationAtRisk: MediumPopulationAtRisk, + attackRate: MediumWeightedOption, + geographicalSpread: MediumGeographicalSpread, + complexity: MediumWeightedOption, + capacity: MediumCapacity, + reputationalRisk: MediumWeightedOption, + severity: MediumWeightedOption, + // capability: MediumWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 2"); + }); + + it("should be Grade3 if score is greater than 14", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "3", + lastUpdated: new Date(), + populationAtRisk: HighPopulationAtRisk, + attackRate: HighWeightedOption, + geographicalSpread: HighGeographicalSpread, + complexity: HighWeightedOption, + capacity: HighCapacity, + reputationalRisk: HighWeightedOption, + severity: HighWeightedOption, + // capability: MediumWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 3"); + }); + + it("should be Grade3 if score is greater than 14", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "4", + lastUpdated: new Date(), + populationAtRisk: LowPopulationAtRisk, + attackRate: MediumWeightedOption, + geographicalSpread: HighGeographicalSpread, + complexity: MediumWeightedOption, + capacity: LowCapacity, + reputationalRisk: HighWeightedOption, + severity: HighWeightedOption, + // capability: MediumWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 3"); + }); +}); diff --git a/src/webapp/pages/app/__tests__/App.spec.tsx b/src/webapp/pages/app/__tests__/App.spec.tsx deleted file mode 100644 index 8a974586..00000000 --- a/src/webapp/pages/app/__tests__/App.spec.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { fireEvent, render } from "@testing-library/react"; - -import App from "../App"; -import { getTestContext } from "../../../../utils/tests"; -import { Provider } from "@dhis2/app-runtime"; - -describe("App", () => { - it("renders the feedback component", async () => { - const view = getView(); - - expect(await view.findByText("Send feedback")).toBeInTheDocument(); - }); - - it("navigates to page", async () => { - const view = getView(); - - fireEvent.click(await view.findByText("John")); - - expect(await view.findByText("Hello John")).toBeInTheDocument(); - expect(view.asFragment()).toMatchSnapshot(); - }); -}); - -function getView() { - const { compositionRoot } = getTestContext(); - return render( - - - - ); -} From 1a21585edc525f57ce75fec7b4ac0be50376709d Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Mon, 17 Jun 2024 15:57:15 +0200 Subject: [PATCH 006/427] Add more little component to UI --- i18n/en.pot | 16 +- i18n/es.po | 14 +- package.json | 2 + src/types/d2-ui.d.ts | 3 + .../add-new-option/AddNewOption.tsx | 9 +- .../components/avatar-card/AvatarCard.tsx | 1 + .../components/date-picker/DatePicker.tsx | 3 +- .../components/input-field/InputField.tsx | 1 + src/webapp/components/layout/Layout.tsx | 24 ++- .../layout/header-bar/HeaderBar.tsx | 2 + .../layout/main-content/MainContent.tsx | 3 + .../components/layout/side-bar/SideBar.tsx | 11 +- .../layout/side-bar/SideBarContent.tsx | 37 +++- .../multiple-selector/MultipleSelector.tsx | 35 ++- .../components/notice-box/NoticeBox.tsx | 50 +++++ .../components/profile-modal/ProfileModal.tsx | 83 ++++++++ .../components/search-input/SearchInput.tsx | 105 +++++++++ src/webapp/components/selector/Selector.tsx | 12 +- .../components/stats-card/StatsCard.tsx | 82 +++++++ src/webapp/components/text-area/TextArea.tsx | 1 + src/webapp/pages/Router.tsx | 28 +-- src/webapp/pages/app/themes/dhis2.theme.ts | 15 ++ src/webapp/pages/dashboard/Components.tsx | 201 ++++++++++++++++++ src/webapp/pages/dashboard/DashboardPage.tsx | 7 +- src/webapp/pages/landing/LandingPage.tsx | 7 - yarn.lock | 5 + 26 files changed, 698 insertions(+), 59 deletions(-) create mode 100644 src/webapp/components/notice-box/NoticeBox.tsx create mode 100644 src/webapp/components/profile-modal/ProfileModal.tsx create mode 100644 src/webapp/components/search-input/SearchInput.tsx create mode 100644 src/webapp/components/stats-card/StatsCard.tsx create mode 100644 src/webapp/pages/dashboard/Components.tsx delete mode 100644 src/webapp/pages/landing/LandingPage.tsx diff --git a/i18n/en.pot b/i18n/en.pot index c278404f..f5bd524b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,22 +5,28 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-06-07T18:24:28.361Z\n" -"PO-Revision-Date: 2024-06-07T18:24:28.361Z\n" +"POT-Creation-Date: 2024-06-17T13:56:47.078Z\n" +"PO-Revision-Date: 2024-06-17T13:56:47.078Z\n" msgid "Add new option" msgstr "" +msgid "Create Event" +msgstr "" + msgid "N/A" msgstr "" -msgid "Incident Management Team Builder" +msgid "Close" msgstr "" -msgid "Cholera in NW Province, June 2023" +msgid "Search" msgstr "" -msgid "Create Event" +msgid "Incident Management Team Builder" +msgstr "" + +msgid "Cholera in NW Province, June 2023" msgstr "" msgid "Incident Action Plan" diff --git a/i18n/es.po b/i18n/es.po index 02dd6634..e9fe5620 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2024-06-07T18:24:28.361Z\n" +"POT-Creation-Date: 2024-06-17T13:56:47.078Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -11,16 +11,22 @@ msgstr "" msgid "Add new option" msgstr "" +msgid "Create Event" +msgstr "" + msgid "N/A" msgstr "" -msgid "Incident Management Team Builder" +msgid "Close" msgstr "" -msgid "Cholera in NW Province, June 2023" +msgid "Search" msgstr "" -msgid "Create Event" +msgid "Incident Management Team Builder" +msgstr "" + +msgid "Cholera in NW Province, June 2023" msgstr "" msgid "Incident Action Plan" diff --git a/package.json b/package.json index 2ce199e8..25b6254e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "d2": "31.10.2", "d2-manifest": "1.0.0", "font-awesome": "4.7.0", + "lodash": "^4.17.21", "purify-ts": "1.2.0", "purify-ts-extra-codec": "0.6.0", "react": "^18.2.0", @@ -49,6 +50,7 @@ "@testing-library/react": "^14.0.0", "@types/classnames": "2.3.1", "@types/isomorphic-fetch": "^0.0.36", + "@types/lodash": "^4.17.5", "@types/material-ui": "^0.21.12", "@types/node": "18", "@types/node-localstorage": "^1.3.0", diff --git a/src/types/d2-ui.d.ts b/src/types/d2-ui.d.ts index 64802194..539ca0bc 100644 --- a/src/types/d2-ui.d.ts +++ b/src/types/d2-ui.d.ts @@ -10,4 +10,7 @@ declare module "@dhis2/ui" { export function IconCalendar24(props: { color?: string }): React.ReactElement; export function IconChevronDown24(props: { color?: string }): React.ReactElement; export function IconCross24(props: { color?: string }): React.ReactElement; + export function IconCross16(props: { color?: string }): React.ReactElement; + export function IconSearch24(props: { color?: string }): React.ReactElement; + export function IconInfo24(props: { color?: string }): React.ReactElement; } diff --git a/src/webapp/components/add-new-option/AddNewOption.tsx b/src/webapp/components/add-new-option/AddNewOption.tsx index d101c172..7abfa140 100644 --- a/src/webapp/components/add-new-option/AddNewOption.tsx +++ b/src/webapp/components/add-new-option/AddNewOption.tsx @@ -13,8 +13,8 @@ interface AddNewOptionProps { export const AddNewOption: React.FC = React.memo( ({ id, label = "", onAddNewOption }) => { return ( - - + + ); @@ -24,10 +24,14 @@ export const AddNewOption: React.FC = React.memo( const Container = styled.div` display: flex; align-items: center; + cursor: pointer; `; const StyledAddIcon = styled(AddCircleOutline)` color: ${props => props.theme.palette.icon.color}; + &:hover { + color: ${props => props.theme.palette.icon.hover}; + } `; const Label = styled.label` @@ -36,4 +40,5 @@ const Label = styled.label` font-size: 0.875rem; color: ${props => props.theme.palette.common.black}; margin-inline-start: 8px; + cursor: pointer; `; diff --git a/src/webapp/components/avatar-card/AvatarCard.tsx b/src/webapp/components/avatar-card/AvatarCard.tsx index aca63efe..55a19945 100644 --- a/src/webapp/components/avatar-card/AvatarCard.tsx +++ b/src/webapp/components/avatar-card/AvatarCard.tsx @@ -23,6 +23,7 @@ export const AvatarCard: React.FC = React.memo( ); const StyledCard = styled(Card)` + width: 100%; display: flex; @media (max-width: 600px) { flex-direction: column; diff --git a/src/webapp/components/date-picker/DatePicker.tsx b/src/webapp/components/date-picker/DatePicker.tsx index 63e5df12..9402d505 100644 --- a/src/webapp/components/date-picker/DatePicker.tsx +++ b/src/webapp/components/date-picker/DatePicker.tsx @@ -9,7 +9,7 @@ import { IconCalendar24 } from "@dhis2/ui"; interface DatePickerProps { id: string; label?: string; - value?: Date; + value: Date | null; onChange: (value: Date | null) => void; helperText?: string; errorText?: string; @@ -56,6 +56,7 @@ export const DatePicker: React.FC = React.memo( const Container = styled.div` display: flex; flex-direction: column; + width: 100%; `; const Label = styled(InputLabel)` diff --git a/src/webapp/components/input-field/InputField.tsx b/src/webapp/components/input-field/InputField.tsx index ad623a0b..96d2cf4a 100644 --- a/src/webapp/components/input-field/InputField.tsx +++ b/src/webapp/components/input-field/InputField.tsx @@ -47,6 +47,7 @@ export const InputField: React.FC = React.memo( const Container = styled.div` display: flex; flex-direction: column; + width: 100%; `; const Label = styled(InputLabel)` diff --git a/src/webapp/components/layout/Layout.tsx b/src/webapp/components/layout/Layout.tsx index 1a383fcb..5e1b3d04 100644 --- a/src/webapp/components/layout/Layout.tsx +++ b/src/webapp/components/layout/Layout.tsx @@ -1,19 +1,26 @@ import React, { useState } from "react"; import styled from "styled-components"; import { useMediaQuery, useTheme } from "@material-ui/core"; +import { Menu } from "@material-ui/icons"; import { MainContent } from "./main-content/MainContent"; import { Button } from "../button/Button"; - interface LayoutProps { children?: React.ReactNode; title?: string; subtitle?: string; hideSideBarOptions?: boolean; + showCreateEvent?: boolean; } export const Layout: React.FC = React.memo( - ({ children, title = "", subtitle = "", hideSideBarOptions = false }) => { + ({ + children, + title = "", + subtitle = "", + hideSideBarOptions = false, + showCreateEvent = false, + }) => { const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); @@ -21,12 +28,15 @@ export const Layout: React.FC = React.memo( return ( {isSmallScreen && !hideSideBarOptions ? ( - + + + ) : ( - + {DEFAULT_SIDEBAR_OPTIONS.map(({ text, value }) => ( ))} - + )} ); @@ -68,7 +83,7 @@ const StyledText = styled(ListItemText)<{ selected?: boolean }>` const SideBarContainer = styled.div` display: flex; - width: 240px; + max-width: 245px; background-color: ${props => props.theme.palette.sidebar.background}; .MuiList-root { padding-block: 50px; @@ -81,3 +96,13 @@ const SideBarContainer = styled.div` padding-block: 4px; } `; + +const StyledList = styled(List)` + width: 245px; +`; + +const CreateEventContainer = styled.div` + margin-block-start: 50px; + margin-inline-start: 30px; + width: 245px; +`; diff --git a/src/webapp/components/multiple-selector/MultipleSelector.tsx b/src/webapp/components/multiple-selector/MultipleSelector.tsx index 4b304673..3533e4e1 100644 --- a/src/webapp/components/multiple-selector/MultipleSelector.tsx +++ b/src/webapp/components/multiple-selector/MultipleSelector.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import styled from "styled-components"; import { Select, InputLabel, MenuItem, FormHelperText, Chip } from "@material-ui/core"; -import { IconChevronDown24, IconCross24 } from "@dhis2/ui"; +import { IconChevronDown24, IconCross16 } from "@dhis2/ui"; export type MultipleSelectorOption = { value: T; @@ -56,7 +56,11 @@ export const MultipleSelector: React.FC = React.memo( ); const handleDelete = useCallback( - (value: MultipleSelectorOption["value"]) => { + ( + event: React.MouseEvent, + value: MultipleSelectorOption["value"] + ) => { + event.stopPropagation(); onChange(selected?.filter(selection => selection !== value)); }, [onChange, selected] @@ -81,8 +85,9 @@ export const MultipleSelector: React.FC = React.memo( } - onDelete={() => handleDelete(value)} + deleteIcon={} + onDelete={event => handleDelete(event, value)} + onMouseDown={event => handleDelete(event, value)} /> ))} @@ -114,6 +119,7 @@ export const MultipleSelector: React.FC = React.memo( const Container = styled.div` display: flex; flex-direction: column; + width: 100%; `; const Label = styled(InputLabel)` @@ -130,15 +136,30 @@ const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>` `; const StyledSelect = styled(Select)<{ error?: boolean }>` - padding-inline-start: 12px; - padding-inline-end: 6px; - padding-block: 10px; .MuiOutlinedInput-notchedOutline { border-color: ${props => props.error ? props.theme.palette.common.red600 : props.theme.palette.common.grey500}; } + .MuiSelect-root { + padding-inline-start: 12px; + padding-inline-end: 6px; + padding-block: 10px; + &:focus { + background-color: ${props => props.theme.palette.common.white}; + } + } `; const SelectedChip = styled(Chip)` margin-inline-end: 16px; + font-weight: 400; + font-size: 0.813rem; + padding-inline-end: 8px; + svg { + color: ${props => props.theme.palette.common.grey600}; + cursor: pointer; + &:hover { + color: ${props => props.theme.palette.common.grey900}; + } + } `; diff --git a/src/webapp/components/notice-box/NoticeBox.tsx b/src/webapp/components/notice-box/NoticeBox.tsx new file mode 100644 index 00000000..af7987b2 --- /dev/null +++ b/src/webapp/components/notice-box/NoticeBox.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { IconInfo24 } from "@dhis2/ui"; +import styled from "styled-components"; + +interface NoticeBoxProps { + title: string; + children: React.ReactNode; +} + +export const NoticeBox: React.FC = React.memo(({ title, children }) => { + return ( + + + + {title} + + {children} + + ); +}); + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + padding-inline: 16px; + padding-block: 12px; + gap: 8px; + background-color: ${props => props.theme.palette.common.blue050}; + border-radius: 3px; + border: 2px solid ${props => props.theme.palette.common.blue200}; +`; + +const TitleContainer = styled.div` + display: flex; + gap: 8px; + font-size: 0.875rem; + font-weight: 500; + color: ${props => props.theme.palette.text.primary}; + align-items: center; + svg { + color: ${props => props.theme.palette.common.blue900}; + } +`; + +const Content = styled.div` + font-size: 0.875rem; + font-weight: 400; + color: ${props => props.theme.palette.text.primary}; +`; diff --git a/src/webapp/components/profile-modal/ProfileModal.tsx b/src/webapp/components/profile-modal/ProfileModal.tsx new file mode 100644 index 00000000..4e7f96ce --- /dev/null +++ b/src/webapp/components/profile-modal/ProfileModal.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Modal, Avatar, CardContent, Card } from "@material-ui/core"; +import styled from "styled-components"; + +import i18n from "../../../utils/i18n"; +import { Button } from "../button/Button"; + +interface ProfileModalProps { + name: string; + children: React.ReactNode; + avatarSize?: "small" | "medium"; + alt?: string; + src?: string; + open: boolean; + onClose: () => void; +} + +export const ProfileModal: React.FC = React.memo( + ({ children, src, alt, open = false, onClose, name }) => { + return ( + + + {name} + + + + + {children} + +
+ +
+
+
+ ); + } +); + +const Content = styled.div` + display: flex; +`; + +const Name = styled.span` + color: ${props => props.theme.palette.common.black}; + font-size: 1.25rem; + font-weight: 500; +`; + +const Footer = styled.div` + display: flex; + margin-block-start: 16px; +`; + +const StyledCard = styled(Card)` + width: 500px; + display: flex; + flex-direction: column; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 24px; + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.1); +`; + +const AvatarContainer = styled.div` + padding-block: 20px; + padding-inline: 45px; + .MuiAvatar-root { + width: 121px; + height: 121px; + } +`; + +const StyledCardContent = styled(CardContent)` + width: 100%; +`; diff --git a/src/webapp/components/search-input/SearchInput.tsx b/src/webapp/components/search-input/SearchInput.tsx new file mode 100644 index 00000000..fc4c675a --- /dev/null +++ b/src/webapp/components/search-input/SearchInput.tsx @@ -0,0 +1,105 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { TextField } from "@material-ui/core"; +import { IconSearch24 } from "@dhis2/ui"; +import styled from "styled-components"; +import _ from "lodash"; + +import i18n from "../../../utils/i18n"; + +interface SearchInputProps { + value: string; + onChange: (event: string) => void; + placeholder?: string; + disabled?: boolean; +} + +export const SearchInput: React.FC = React.memo( + ({ value, onChange, placeholder = "", disabled = false }) => { + const [stateValue, updateStateValue] = useState(value); + + useEffect(() => updateStateValue(value), [value]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const onChangeDebounced = useCallback( + _.debounce((value: string) => { + if (onChange) { + onChange(value); + } + }, 400), + [] + ); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + onChangeDebounced(value); + updateStateValue(value); + }, + [onChangeDebounced, updateStateValue] + ); + + const handleKeydown = useCallback((event: React.KeyboardEvent) => { + event.stopPropagation(); + }, []); + + return ( + + + + + + + ); + } +); + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + position: relative; +`; + +const IconContainer = styled.div<{ $disabled?: boolean }>` + height: 100%; + position: absolute; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + padding-inline-start: 24px; + padding-inline-end: 8px; + svg { + color: ${props => + props.$disabled ? props.theme.palette.common.grey3 : props.theme.palette.common.grey2}; + } +`; + +const StyledTextField = styled(TextField)<{ error?: boolean }>` + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.common.grey1}; + .MuiFormHelperText-root { + color: ${props => + props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700}; + } + .MuiOutlinedInput-notchedOutline { + border-color: ${props => props.theme.palette.common.grey4}; + } + .MuiInputBase-input { + background-color: ${props => props.theme.palette.common.background1}; + padding-inline-end: 12px; + padding-block: 10px; + padding-inline-start: 64px; + } +`; diff --git a/src/webapp/components/selector/Selector.tsx b/src/webapp/components/selector/Selector.tsx index f1d7cae3..e2969dfd 100644 --- a/src/webapp/components/selector/Selector.tsx +++ b/src/webapp/components/selector/Selector.tsx @@ -93,6 +93,7 @@ export const Selector: React.FC = React.memo( const Container = styled.div` display: flex; flex-direction: column; + width: 100%; `; const Label = styled(InputLabel)` @@ -109,11 +110,16 @@ const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>` `; const StyledSelect = styled(Select)<{ error?: boolean }>` - padding-inline-start: 12px; - padding-inline-end: 6px; - padding-block: 10px; .MuiOutlinedInput-notchedOutline { border-color: ${props => props.error ? props.theme.palette.common.red600 : props.theme.palette.common.grey500}; } + .MuiSelect-root { + padding-inline-start: 12px; + padding-inline-end: 6px; + padding-block: 10px; + &:focus { + background-color: ${props => props.theme.palette.common.white}; + } + } `; diff --git a/src/webapp/components/stats-card/StatsCard.tsx b/src/webapp/components/stats-card/StatsCard.tsx new file mode 100644 index 00000000..576d8f68 --- /dev/null +++ b/src/webapp/components/stats-card/StatsCard.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { CardContent, Card } from "@material-ui/core"; +import styled from "styled-components"; + +interface StatsCardProps { + color?: "normal" | "green" | "red"; + stat: string; + pretitle?: string; + title: string; + subtitle?: string; + isPercentage?: boolean; + error?: boolean; +} + +export const StatsCard: React.FC = React.memo( + ({ + stat, + title, + subtitle, + pretitle, + color = "normal", + isPercentage = false, + error = false, + }) => { + return ( + + + {`${stat}${isPercentage ? " %" : ""}`} + {pretitle} + {title} + {subtitle} + + + ); + } +); + +const StyledCard = styled(Card)<{ $error?: boolean }>` + width: fit-content; + min-width: 220px; + max-width: 300px; + border-style: ${props => (props.$error ? "solid" : "none")}; + border-width: ${props => (props.$error ? "1px" : "0")}; + border-color: ${props => (props.$error ? props.theme.palette.stats.red : "unset")}; +`; + +const StyledCardContent = styled(CardContent)` + padding-inline: 66px; + padding-block: 32px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; +`; + +const Stat = styled.div<{ color: string }>` + color: ${props => props.theme.palette.stats[props.color]}; + font-size: 2em; + font-weight: 400; +`; + +const PreTitle = styled.span` + color: ${props => props.theme.palette.stats.pretitle}; + font-weight: 400; + font-size: 0.875rem; + text-align: center; +`; + +const Title = styled.span` + color: ${props => props.theme.palette.stats.title}; + font-weight: 700; + font-size: 1rem; + text-align: center; +`; + +const SubTitle = styled.span` + color: ${props => props.theme.palette.stats.subtitle}; + font-weight: 400; + font-size: 0.875rem; + text-align: center; +`; diff --git a/src/webapp/components/text-area/TextArea.tsx b/src/webapp/components/text-area/TextArea.tsx index d6e6b4e3..4e7afaa4 100644 --- a/src/webapp/components/text-area/TextArea.tsx +++ b/src/webapp/components/text-area/TextArea.tsx @@ -54,6 +54,7 @@ export const TextArea: React.FC = React.memo( const Container = styled.div` display: flex; flex-direction: column; + width: 100%; `; const Label = styled.label` diff --git a/src/webapp/pages/Router.tsx b/src/webapp/pages/Router.tsx index db0a2528..8665b9db 100644 --- a/src/webapp/pages/Router.tsx +++ b/src/webapp/pages/Router.tsx @@ -1,7 +1,6 @@ import React from "react"; import { HashRouter, Route, Switch } from "react-router-dom"; -import { LandingPage } from "./landing/LandingPage"; import { DashboardPage } from "./dashboard/DashboardPage"; import { EventTrackerPage } from "./event-tracker/EventTrackerPage"; import { IncidentActionPlanPage } from "./incident-action-plan/IncidentActionPlanPage"; @@ -16,33 +15,28 @@ export function Router() { return ( - } /> } /> - } /> - } /> - } /> + } /> } + path="/create-risk-assessment/:event" + render={() => } /> - } /> - } /> - } /> } + path="/incident-management-team-builder/:event" + render={() => } /> + } /> } + path="/incident-action-plan/:event" + render={() => } /> } /> - } /> + } /> {/* Default route */} - } /> + } /> ); diff --git a/src/webapp/pages/app/themes/dhis2.theme.ts b/src/webapp/pages/app/themes/dhis2.theme.ts index 1b69e1bf..7b08cf5f 100644 --- a/src/webapp/pages/app/themes/dhis2.theme.ts +++ b/src/webapp/pages/app/themes/dhis2.theme.ts @@ -90,6 +90,12 @@ const colors = { green: "#008B45", red: "#E4312B", orange: "#FABE5F", + grey1: "#2F2727", + grey2: "#4B4343", + grey3: "#8C8484", + grey4: "#bfbebe", + + background1: "#F5F5F5", }; const palette = { @@ -154,6 +160,7 @@ const palette = { }, icon: { color: colors.grey700, + hover: colors.grey900, }, header: { color: colors.green600, @@ -163,6 +170,14 @@ const palette = { black: colors.black, orange: colors.orange, }, + stats: { + green: colors.green, + red: colors.red, + normal: colors.green700, + title: colors.black, + subtitle: colors.grey3, + pretitle: colors.grey3, + }, }; export const muiTheme = createTheme({ diff --git a/src/webapp/pages/dashboard/Components.tsx b/src/webapp/pages/dashboard/Components.tsx new file mode 100644 index 00000000..84afcdf7 --- /dev/null +++ b/src/webapp/pages/dashboard/Components.tsx @@ -0,0 +1,201 @@ +import React, { useState } from "react"; +import styled from "styled-components"; + +import { Button } from "../../components/button/Button"; +import { AddNewOption } from "../../components/add-new-option/AddNewOption"; +import { AvatarCard } from "../../components/avatar-card/AvatarCard"; +import { DatePicker } from "../../components/date-picker/DatePicker"; +import { InputField } from "../../components/input-field/InputField"; +import { MultipleSelector } from "../../components/multiple-selector/MultipleSelector"; +import { NACheckbox } from "../../components/not-applicable-checkbox/NACheckbox"; +import { RadioButtonsGroup } from "../../components/radio-buttons-group/RadioButtonsGroup"; +import { Selector } from "../../components/selector/Selector"; +import { TextArea } from "../../components/text-area/TextArea"; +import { StatsCard } from "../../components/stats-card/StatsCard"; +import { SearchInput } from "../../components/search-input/SearchInput"; +import { NoticeBox } from "../../components/notice-box/NoticeBox"; +import { ProfileModal } from "../../components/profile-modal/ProfileModal"; + +export const Components: React.FC = React.memo(() => { + const [date, setDate] = useState(null); + const [select, setSelect] = useState(""); + const [multi, setMulti] = useState([]); + const [radio, setRadio] = useState(""); + const [naCheck, setNACheck] = useState(false); + const [area, setArea] = useState(""); + const [input, setInput] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + const [openProfileModal, setOpenProfileModal] = useState(false); + + return ( + <> + + + + + + + + + + + + Text + + + + + + setInput(event.target.value)} + /> + + + + + + + + + setRadio(event.target.value)} + options={[ + { value: "1", label: "value 1" }, + { value: "2", label: "value 2" }, + ]} + /> + + + + + +