diff --git a/client/src/pages/InstructorResultCreatorPage/EditorPanel.tsx b/client/src/pages/InstructorResultCreatorPage/EditorPanel.tsx new file mode 100644 index 000000000..628d4d657 --- /dev/null +++ b/client/src/pages/InstructorResultCreatorPage/EditorPanel.tsx @@ -0,0 +1,183 @@ +import { AssessmentValues, ResultCreatorReturnProps } from '@app/pages/InstructorResultCreatorPage/useResultCreator'; +import { useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; +import { countCurrentPoints } from '@app/pages/InstructorResultCreatorPage/countPoints'; +import { Box, createStyles, Divider, Grid, makeStyles, MenuItem, Theme } from '@material-ui/core'; +import { ChildHeader } from '@app/pages/InstructorResultCreatorPage/MeasurementEditor/ChildHeader'; +import { + MeasurementEditor, + MeasurementValues, +} from '@app/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementEditor'; +import { ButtonSecondary } from '@app/components/Button'; +import { ActionMenuButtonSecondary } from '@app/components/Button/ActionMenuButtonSecondary'; +import { AssessmentParam } from '@app/graphql/types'; + +interface EditorPanelProps { + isLoading: boolean; + measurement: string; + onClick: (type: string, value: string | AssessmentValues) => void; + resultCreator: ResultCreatorReturnProps; +} + +export function EditorPanel({ resultCreator, measurement, onClick, isLoading }: EditorPanelProps) { + const classes = useStyles(); + const { t } = useTranslation(); + + const { selectedChild: child } = resultCreator; + + const [localResult, setLocalResult] = useState(resultCreator.values); + const [localNote, setLocalNote] = useState(getCurrentNote()); + + useEffect(() => { + setLocalResult(resultCreator.values); + }, [resultCreator.values]); + + useEffect(() => { + setLocalNote(getCurrentNote() || ''); + }, [resultCreator.values, getCurrentNote()]); + + const pointSum = Object.values(countCurrentPoints(localResult, child.currentParams)).reduce((acc, v) => { + if (Number.isNaN(v)) return acc; + + return acc + v; + }, 0); + + const isLastChild = () => { + if (resultCreator.selectedKindergarten.children?.length) { + const lastChildIndex = resultCreator.selectedKindergarten.children?.length - 1; + + return resultCreator.selectedChild._id === resultCreator.selectedKindergarten.children[lastChildIndex]._id; + } + + return false; + }; + + const handleChange = (value: MeasurementValues) => { + setLocalResult((prev) => { + return { + ...prev, + ...value, + }; + }); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + onClick('back-to-table', '')} variant="text"> + {t('add-result-page.back-to-table')} + + + + + + {isLastChild() ? ( + onClick('save-and-back-to-table', { ...localResult, note: localNote })} + > + {t('add-result-page.save-and-back-to-table')} + + ) : ( + onClick('save-and-next', { ...localResult, note: localNote })} + options={[ + + onClick('save-and-back-to-table', { ...localResult, note: localNote }) + } + key="add-result-page.save-and-back-to-table" + > + {t('add-result-page.save-and-back-to-table')} + , + ]} + /> + )} + + + + + ); + + function countMaxPoints(): number { + return Object.values(resultCreator.selectedChild.currentParams!).reduce((acc: number, v: AssessmentParam) => { + if (!v || !v.lowerLimitPoints || !v.upperLimitPoints) return acc; + + if (v.lowerLimitPoints > v.upperLimitPoints) { + return acc + v.lowerLimitPoints; + } + + return acc + v.upperLimitPoints; + }, 0) as number; + } + + function getCurrentNote() { + const currentResult = getCurrentResult(); + + if (!currentResult) return ''; + + if (measurement === 'first') { + return currentResult.firstMeasurementNote; + } + + return currentResult.lastMeasurementNote; + } + + function getCurrentResult() { + return resultCreator.kindergartenResults.find((r) => r.childId === resultCreator.selectedChild._id); + } +} + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + editor: { + flex: '1 1 auto', + height: 0, + overflowY: 'auto', + overflowX: 'hidden', + }, + editorContainer: { + height: '100%', + }, + footerContainer: { + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1, 2), + }, + }), +); diff --git a/client/src/pages/InstructorResultCreatorPage/InstructorResultCreatorPage.tsx b/client/src/pages/InstructorResultCreatorPage/InstructorResultCreatorPage.tsx index 21516d5fd..717d56a99 100644 --- a/client/src/pages/InstructorResultCreatorPage/InstructorResultCreatorPage.tsx +++ b/client/src/pages/InstructorResultCreatorPage/InstructorResultCreatorPage.tsx @@ -2,19 +2,19 @@ import { useEffect } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { activePage } from '../../apollo_client'; -import { PageContainer } from '../../components/PageContainer'; -import { openSnackbar } from '../../components/Snackbar/openSnackbar'; +import { activePage } from '@app/apollo_client'; +import { PageContainer } from '@app/components/PageContainer'; +import { openSnackbar } from '@app/components/Snackbar/openSnackbar'; -import { useIsDevice } from '../../queries/useBreakpoints'; +import { useIsDevice } from '@app/queries/useBreakpoints'; import { useUpdateAssessmentResult, UpdatedAssessmentInput, -} from '../../operations/mutations/Results/updateAssessmentResult'; +} from '@app/operations/mutations/Results/updateAssessmentResult'; import { CreatedAssessmentInput, useCreateAssessmentResult, -} from '../../operations/mutations/Results/createAssessmentResult'; +} from '@app/operations/mutations/Results/createAssessmentResult'; import { ResultCreatorErrorReturnProps, ResultCreatorReturnProps, diff --git a/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementEditor.tsx b/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementEditor.tsx index 028105593..fe3e8b688 100644 --- a/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementEditor.tsx +++ b/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementEditor.tsx @@ -3,19 +3,19 @@ import { useTranslation } from 'react-i18next'; import { AssessmentParam } from '@app/graphql/types'; import { theme } from '@app/theme/theme'; import { OutlinedTextField } from '@app/components/OutlinedTextField'; -import { MeasurementPoint } from './MeasurementPoint'; -import dayjs from '../../../localizedMoment'; +import dayjs from '@app/localizedMoment'; +import { MeasurementPoint, Origin } from './MeasurementPoint'; import { countPoints, countInvertedPoints } from '../countPoints'; import { ResultCreatorReturnProps, AssessmentValues } from '../useResultCreator'; -interface MeasurementValues { +export interface MeasurementValues { run: number; pendelumRun: number; throw: number; jump: number; } -interface Props { +interface MeasurementEditorProps { resultCreator: ResultCreatorReturnProps; measurement: string; value: AssessmentValues; @@ -25,18 +25,25 @@ interface Props { onEditClick: (name: string) => void; } -export function MeasurementEditor(props: Props) { +export function MeasurementEditor({ + resultCreator, + value, + onChange, + onEditClick, + note, + onNoteChange, + measurement, +}: MeasurementEditorProps) { const classes = useStyles(); const { t } = useTranslation(); + const LENGTH_LIMIT = 500; - const { selectedChild: child } = props.resultCreator; + const { selectedChild: child } = resultCreator; const { run, pendelumRun, jump, throw: _throw } = child.currentParams!; - const result = props.resultCreator.kindergartenResults.find( - (r) => r.childId === props.resultCreator.selectedChild._id, - ); + const result = resultCreator.kindergartenResults.find((r) => r.childId === resultCreator.selectedChild._id); if (!child.currentParams || !run || !pendelumRun || !jump || !_throw) { return {"The child doesn't suit to the test"}; @@ -46,89 +53,93 @@ export function MeasurementEditor(props: Props) { handleChange(inputValue, 'pendelumRun', origin)} + onClick={() => onEditClick('pendelumRun')} param={pendelumRun} - points={countPoints(props.value.pendelumRun, pendelumRun)} - color={getInvertedColor(props.value.pendelumRun, pendelumRun)} - value={props.value.pendelumRun} - changeDate={getPendelumRunMeasurementDate()} + points={countPoints(value.pendelumRun, pendelumRun)} + step={0.1} unit="s" - label={t('add-result-page.dexterity')} - disabled={props.resultCreator.edited === 'pendelumRun'} - onChange={(value) => props.onChange({ ...props.value, pendelumRun: value })} - onClick={() => props.onEditClick('pendelumRun')} + value={value.pendelumRun} /> + handleChange(inputValue, 'jump', origin)} + onClick={() => onEditClick('jump')} param={jump} - points={countInvertedPoints(props.value.jump, jump)} - color={getColor(props.value.jump, jump)} - value={props.value.jump} - changeDate={getJumpMeasurementDate()} + points={countInvertedPoints(value.jump, jump)} + step={1} unit="cm" - label={t('add-result-page.power')} - disabled={props.resultCreator.edited === 'jump'} - onChange={(value) => props.onChange({ ...props.value, jump: value })} - onClick={() => props.onEditClick('jump')} + value={value.jump} /> + handleChange(inputValue, 'throw', origin)} + onClick={() => onEditClick('throw')} param={_throw} - points={countInvertedPoints(props.value.throw, _throw)} - color={getColor(props.value.throw, _throw)} - value={props.value.throw} - changeDate={getThrowMeasurementDate()} + points={countInvertedPoints(value.throw, _throw)} + step={10} unit="cm" - label={t('add-result-page.strength')} - disabled={props.resultCreator.edited === 'throw'} - onChange={(value) => props.onChange({ ...props.value, throw: value })} - onClick={() => props.onEditClick('throw')} + value={value.throw} /> + handleChange(inputValue, 'run', origin)} + onClick={() => onEditClick('run')} param={run} - points={countPoints(props.value.run, run)} - color={getInvertedColor(props.value.run, run)} - value={props.value.run} - changeDate={getRunMeasurementDate()} + points={countPoints(value.run, run)} + step={0.1} unit="s" - label={t('add-result-page.velocity')} - disabled={props.resultCreator.edited === 'run'} - onChange={(value) => props.onChange({ ...props.value, run: value })} - onClick={() => props.onEditClick('run')} + value={value.run} /> + {t('add-result-page.note')} + inputValue.length <= LENGTH_LIMIT && onNoteChange(inputValue)} options={{ multiline: true, minRows: 7 }} - onChange={(value) => value.length <= LENGTH_LIMIT && props.onNoteChange(value)} + value={note || ''} /> + {t('add-results-page.add-note-modal.text-limit', { - noteLength: props.note?.length || 0, + noteLength: note?.length || 0, noteLimit: LENGTH_LIMIT, })} @@ -139,10 +150,18 @@ export function MeasurementEditor(props: Props) { ); + function handleChange(inputValue: string, name: string, origin: Origin) { + onChange({ ...value, [name]: inputValue }); + + if (origin === Origin.CHECKBOX && Number(inputValue) === 0) { + resultCreator.add(); + } + } + function getPendelumRunMeasurementDate() { let date: Date | undefined; - if (props.measurement === 'first') { + if (measurement === 'first') { date = result?.firstMeasurementPendelumRunDate; } else { date = result?.lastMeasurementPendelumRunDate; @@ -156,7 +175,7 @@ export function MeasurementEditor(props: Props) { function getRunMeasurementDate() { let date: Date | undefined; - if (props.measurement === 'first') { + if (measurement === 'first') { date = result?.firstMeasurementRunDate; } else { date = result?.lastMeasurementRunDate; @@ -170,7 +189,7 @@ export function MeasurementEditor(props: Props) { function getThrowMeasurementDate() { let date: Date | undefined; - if (props.measurement === 'first') { + if (measurement === 'first') { date = result?.firstMeasurementThrowDate; } else { date = result?.lastMeasurementThrowDate; @@ -184,7 +203,7 @@ export function MeasurementEditor(props: Props) { function getJumpMeasurementDate() { let date: Date | undefined; - if (props.measurement === 'first') { + if (measurement === 'first') { date = result?.firstMeasurementJumpDate; } else { date = result?.lastMeasurementJumpDate; @@ -195,25 +214,25 @@ export function MeasurementEditor(props: Props) { return dayjs(date).fromNow(); } - function getColor(value: number, param: AssessmentParam) { - if (value <= param.weakStageLimit) return (theme.palette?.error as SimplePaletteColorOptions).main || 'red'; + function getColor(limit: number, param: AssessmentParam) { + if (limit <= param.weakStageLimit) return (theme.palette?.error as SimplePaletteColorOptions).main || 'red'; - if (value <= param.middleStageLimit) + if (limit <= param.middleStageLimit) return (theme.palette?.warning as SimplePaletteColorOptions).main || 'yellow'; - if (value <= param.goodStageLimit) + if (limit <= param.goodStageLimit) return (theme.palette?.success as SimplePaletteColorOptions).light || 'green'; return (theme.palette?.success as SimplePaletteColorOptions).main || 'green'; } - function getInvertedColor(value: number, param: AssessmentParam) { - if (value >= param.weakStageLimit) return (theme.palette?.error as SimplePaletteColorOptions).main || 'red'; + function getInvertedColor(limit: number, param: AssessmentParam) { + if (limit >= param.weakStageLimit) return (theme.palette?.error as SimplePaletteColorOptions).main || 'red'; - if (value >= param.middleStageLimit) + if (limit >= param.middleStageLimit) return (theme.palette?.warning as SimplePaletteColorOptions).main || 'yellow'; - if (value >= param.goodStageLimit) + if (limit >= param.goodStageLimit) return (theme.palette?.warning as SimplePaletteColorOptions).light || 'green'; return (theme.palette?.success as SimplePaletteColorOptions).main || 'green'; diff --git a/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementPoint.tsx b/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementPoint.tsx index 6702d66ae..5c2a42d10 100644 --- a/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementPoint.tsx +++ b/client/src/pages/InstructorResultCreatorPage/MeasurementEditor/MeasurementPoint.tsx @@ -12,25 +12,32 @@ import { } from '@material-ui/core'; import { Edit, AddCircle as Add } from '@material-ui/icons'; import { useTranslation } from 'react-i18next'; -import { CircleChart } from '../../../components/CircleChart'; -import { ButtonSecondary } from '../../../components/Button/ButtonSecondary'; -import { useIsDevice } from '../../../queries/useBreakpoints'; -import { AssessmentParam } from '../../../graphql/types'; +import { CircleChart } from '@app/components/CircleChart'; +import { ButtonSecondary } from '@app/components/Button'; +import { useIsDevice } from '@app/queries/useBreakpoints'; +import { AssessmentParam } from '@app/graphql/types'; + +// eslint-disable-next-line no-shadow +export enum Origin { + INPUT = 'input', + CHECKBOX = 'checkbox', + SLIDER = 'slider', +} interface Props { - label: string; - value: number; - unit: string; - step: number; - maxValue: number; - points: number; + changeDate?: string; color: string; - isEmpty: boolean; disabled: boolean; - param: AssessmentParam; - changeDate?: string; - onChange: (value: number) => void; + isEmpty: boolean; + label: string; + maxValue: number; + onChange: (value: string, origin: Origin) => void; onClick: () => void; + param: AssessmentParam; + points: number; + step: number; + unit: string; + value: number; } export const MeasurementPoint = memo((props: Props) => { @@ -48,22 +55,16 @@ export const MeasurementPoint = memo((props: Props) => { {props.label}  {props.changeDate && ({props.changeDate})} + {} + props.onChange(v as number)} - marks={getMarks()} classes={{ mark: classes.mark, track: classes.sliderRoot, @@ -72,32 +73,43 @@ export const MeasurementPoint = memo((props: Props) => { valueLabel: classes.valueLabel, disabled: classes.sliderDisabled, }} + disabled={!props.disabled} + marks={getMarks()} + max={Math.floor(props.param.upperLimit + 0.25 * props.param.upperLimit)} + min={Math.floor(props.param.lowerLimit - 0.25 * props.param.lowerLimit)} + onChange={handleSliderChange} + step={props.step} + value={props.value} + valueLabelDisplay="auto" /> + props.onChange(parseFloat(v))} inputProps={{ - step: props.step, - min: 0, - max: props.maxValue, - type: 'number', 'aria-labelledby': 'input-slider', + max: props.maxValue, + min: 0, + step: props.step, + type: 'text', }} - classes={{ input: classes.input }} + margin="dense" + onChange={handleInputChange} + value={props.value} /> + {props.unit} + {!device.isSmallMobile && ( @@ -110,6 +122,7 @@ export const MeasurementPoint = memo((props: Props) => { /> )} + {t('add-result-page.received-points')}{' '} @@ -123,40 +136,59 @@ export const MeasurementPoint = memo((props: Props) => { + } label={{t('add-result-page.no-result')}} labelPlacement="end" - onChange={() => props.onChange(0)} + onChange={() => props.onChange('0', Origin.CHECKBOX)} /> ); + function handleInputChange(event: { target: { value: string } }) { + const v = event.target.value; + const vv = v + .replace(/[^\d.]/g, '') + .replace(/,/g, '.') + .replace(/\.\./g, '.') + .replace(/(\.\d{1})\d*/g, '$1'); + + return props.onChange(vv, Origin.INPUT); + } + + function handleSliderChange(_: React.ChangeEvent<{}>, v: number | number[]) { + const value = (v as number).toFixed(1); + + return props.onChange(value, Origin.SLIDER); + } + function getMarks() { - const marks = [ + const { lowerLimit, upperLimit } = props.param; + const margin = 0.25; + + return [ { - value: Math.floor(props.param.lowerLimit - 0.25 * props.param.lowerLimit), - label: Math.floor(props.param.lowerLimit - 0.25 * props.param.lowerLimit), + value: Math.floor((1 - margin) * lowerLimit), + label: Math.floor((1 - margin) * lowerLimit), }, { - value: props.param.lowerLimit, - label: props.param.lowerLimit, + value: lowerLimit, + label: lowerLimit, }, { - value: props.param.upperLimit, - label: props.param.upperLimit, + value: upperLimit, + label: upperLimit, }, { - value: Math.floor(props.param.upperLimit + 0.25 * props.param.upperLimit), - label: Math.floor(props.param.upperLimit + 0.25 * props.param.upperLimit), + value: Math.floor((1 + margin) * upperLimit), + label: Math.floor((1 + margin) * upperLimit), }, ]; - - return marks; } }); diff --git a/client/src/pages/InstructorResultCreatorPage/MobileResultCreator.tsx b/client/src/pages/InstructorResultCreatorPage/MobileResultCreator.tsx index 8b3f622ab..4c436e6cf 100644 --- a/client/src/pages/InstructorResultCreatorPage/MobileResultCreator.tsx +++ b/client/src/pages/InstructorResultCreatorPage/MobileResultCreator.tsx @@ -2,8 +2,9 @@ import { useState, useEffect } from 'react'; import { createStyles, Divider, Grid, Paper, makeStyles, MenuItem, Typography } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; -import { ButtonSecondary } from '../../components/Button'; -import { ActionMenuButtonSecondary } from '../../components/Button/ActionMenuButtonSecondary'; +import { AssessmentParam } from '@app/graphql/types'; +import { ButtonSecondary } from '@app/components/Button'; +import { ActionMenuButtonSecondary } from '@app/components/Button/ActionMenuButtonSecondary'; import { countCurrentPoints } from './countPoints'; import { ResultCreatorReturnProps, AssessmentValues } from './useResultCreator'; import { ChildPickerDrawer } from './ChildPicker/ChildPickerDrawer'; @@ -54,6 +55,7 @@ export function MobileResultCreator({ resultCreator, measurement, onClick }: Pro onClick={onClick} /> + + + + + @@ -105,6 +111,7 @@ export function MobileResultCreator({ resultCreator, measurement, onClick }: Pro ]} /> + { - if (!v || !v.lowerLimitPoints || !v.upperLimitPoints) return acc; - - if (v.lowerLimitPoints > v.upperLimitPoints) { - return acc + v.lowerLimitPoints; - } - - return acc + v.upperLimitPoints; - }, 0); + return Object.values(resultCreator.selectedChild.currentParams!).reduce( + (acc: number, v: AssessmentParam): number => { + if (!v || !v.lowerLimitPoints || !v.upperLimitPoints) return acc; + + if (v.lowerLimitPoints > v.upperLimitPoints) { + return acc + v.lowerLimitPoints; + } + + return acc + v.upperLimitPoints; + }, + 0, + ) as number; } function getCurrentNote() { diff --git a/client/src/pages/InstructorResultCreatorPage/ResultCreator.tsx b/client/src/pages/InstructorResultCreatorPage/ResultCreator.tsx index dec4eb804..a83bb0654 100644 --- a/client/src/pages/InstructorResultCreatorPage/ResultCreator.tsx +++ b/client/src/pages/InstructorResultCreatorPage/ResultCreator.tsx @@ -1,15 +1,10 @@ -import { useState, useEffect } from 'react'; -import { Box, createStyles, Divider, Grid, makeStyles, MenuItem, Paper, Theme, Typography } from '@material-ui/core'; +import { createStyles, Grid, makeStyles, Paper, Theme, Typography } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; -import { ButtonSecondary } from '../../components/Button'; -import { ActionMenuButtonSecondary } from '../../components/Button/ActionMenuButtonSecondary'; import { Kindergarten } from '@app/graphql/types'; import { ChildPicker } from './ChildPicker/ChildPicker'; -import { ChildHeader } from './MeasurementEditor/ChildHeader'; -import { MeasurementEditor } from './MeasurementEditor/MeasurementEditor'; import { ResultCreatorReturnProps, AssessmentValues } from './useResultCreator'; -import { countCurrentPoints } from './countPoints'; +import { EditorPanel } from './EditorPanel'; interface Props { resultCreator: ResultCreatorReturnProps; @@ -22,11 +17,13 @@ export function ResultCreator({ resultCreator, measurement, onClick, isLoading } const classes = useStyles(); const { t } = useTranslation(); - const kindergartens = resultCreator.selectedAssessment.kindergartens.map((k) => k.kindergarten) || []; - const childList = resultCreator.selectedKindergarten.children ?? []; - const selectedKindergarten = resultCreator.selectedKindergarten._id; - const selectedChild = resultCreator.selectedChild._id; - const { selectedGroup } = resultCreator; + const { selectedGroup, selectedAssessment, kindergartenResults, selectedKindergarten, selectedChild } = + resultCreator; + + const kindergartens = selectedAssessment.kindergartens.map((k) => k.kindergarten) || []; + const childList = selectedKindergarten.children ?? []; + const selectedKindergartenId = selectedKindergarten._id; + const selectedChildId = selectedChild._id; return ( @@ -34,19 +31,20 @@ export function ResultCreator({ resultCreator, measurement, onClick, isLoading } {t('add-result-page.kindergarten')}} - selectedKindergarten={selectedKindergarten} - selectedGroup={selectedGroup} kindergartens={kindergartens.filter((k) => !!k) as Kindergarten[]} - selected={selectedChild} measurement={measurement} - results={resultCreator.kindergartenResults} - childList={childList} - assessment={resultCreator.selectedAssessment} onClick={onClick} + results={kindergartenResults} + selected={selectedChildId} + selectedGroup={selectedGroup} + selectedKindergarten={selectedKindergartenId} /> + @@ -55,162 +53,8 @@ export function ResultCreator({ resultCreator, measurement, onClick, isLoading } ); } -interface EditorPanelProps { - measurement: string; - resultCreator: ResultCreatorReturnProps; - isLoading: boolean; - onClick: (type: string, value: string | AssessmentValues) => void; -} - -function EditorPanel(props: EditorPanelProps) { - const classes = useStyles(); - const { t } = useTranslation(); - - const { selectedChild: child } = props.resultCreator; - - const [localResult, setLocalResult] = useState(props.resultCreator.values); - const [localNote, setLocalNote] = useState(getCurrentNote()); - - useEffect(() => { - setLocalResult(props.resultCreator.values); - }, [props.resultCreator.values]); - - useEffect(() => { - setLocalNote(getCurrentNote() || ''); - }, [props.resultCreator.values, getCurrentNote()]); - - const pointSum = Object.values(countCurrentPoints(localResult, child.currentParams)).reduce((acc, v) => { - if (Number.isNaN(v)) return acc; - - return acc + v; - }, 0); - - const isLastChild = () => { - if (props.resultCreator.selectedKindergarten.children?.length) { - const lastChildIndex = props.resultCreator.selectedKindergarten.children?.length - 1; - - if ( - props.resultCreator.selectedChild._id === - props.resultCreator.selectedKindergarten.children[lastChildIndex]._id - ) - return true; - } - - return false; - }; - - return ( - - - - - - - - - { - setLocalResult((prev) => ({ - ...prev, - ...value, - })); - }} - onNoteChange={setLocalNote} - onEditClick={props.resultCreator.edit} - /> - - - - - - - - - props.onClick('back-to-table', '')} variant="text"> - {t('add-result-page.back-to-table')} - - - - - {isLastChild() ? ( - - props.onClick('save-and-back-to-table', { ...localResult, note: localNote }) - } - > - {t('add-result-page.save-and-back-to-table')} - - ) : ( - props.onClick('save-and-next', { ...localResult, note: localNote })} - options={[ - - props.onClick('save-and-back-to-table', { ...localResult, note: localNote }) - } - key="add-result-page.save-and-back-to-table" - > - {t('add-result-page.save-and-back-to-table')} - , - ]} - /> - )} - - - - - ); - - function countMaxPoints() { - return Object.values(props.resultCreator.selectedChild!.currentParams!).reduce((acc, v) => { - if (!v || !v.lowerLimitPoints || !v.upperLimitPoints) return acc; - - if (v.lowerLimitPoints > v.upperLimitPoints) { - return acc + v.lowerLimitPoints; - } - - return acc + v.upperLimitPoints; - }, 0); - } - - function getCurrentNote() { - const currentResult = getCurrentResult(); - - if (!currentResult) return ''; - - if (props.measurement === 'first') { - return currentResult.firstMeasurementNote; - } - - return currentResult.lastMeasurementNote; - } - - function getCurrentResult() { - return props.resultCreator.kindergartenResults.find((r) => r.childId === props.resultCreator.selectedChild._id); - } -} - const useStyles = makeStyles((theme: Theme) => createStyles({ - editor: { - flex: '1 1 auto', - height: 0, - overflowY: 'auto', - overflowX: 'hidden', - }, container: { maxHeight: '85vh', height: '85vh', @@ -226,13 +70,5 @@ const useStyles = makeStyles((theme: Theme) => overflowY: 'auto', overflowX: 'hidden', }, - editorContainer: { - height: '100%', - }, - footerContainer: { - display: 'flex', - alignItems: 'center', - padding: theme.spacing(1, 2), - }, }), ); diff --git a/client/src/pages/InstructorResultCreatorPage/useResultCreator.ts b/client/src/pages/InstructorResultCreatorPage/useResultCreator.ts index 4f1a906b0..1d3208a4a 100644 --- a/client/src/pages/InstructorResultCreatorPage/useResultCreator.ts +++ b/client/src/pages/InstructorResultCreatorPage/useResultCreator.ts @@ -29,6 +29,7 @@ export interface ResultCreatorReturnProps { edited: string; kindergartenResults: AssessmentResult[]; edit: (name: string) => void; + add: () => void; } export interface ResultCreatorErrorReturnProps { @@ -66,20 +67,28 @@ export function useResultCreator({ }; } + const edit = (name: string) => { + setEdited(name); + localStorage.setItem('edited', name); + }; + + const add = () => { + setEdited(''); + localStorage.removeItem('edited'); + }; + return { assessments, - values: getResultValue(), + edit, + add, + edited, + error: null, + kindergartenResults, selectedAssessment, - selectedKindergarten, selectedChild, selectedGroup: groupId, - error: null, - edited, - kindergartenResults, - edit: (name: string) => { - setEdited(name); - localStorage.setItem('edited', name); - }, + selectedKindergarten, + values: getResultValue(), }; function getResultValue() { diff --git a/server/src/newsletters/domain/events/handlers/newsletter_created_handler.ts b/server/src/newsletters/domain/events/handlers/newsletter_created_handler.ts index 794020986..d3e9f1c18 100644 --- a/server/src/newsletters/domain/events/handlers/newsletter_created_handler.ts +++ b/server/src/newsletters/domain/events/handlers/newsletter_created_handler.ts @@ -20,20 +20,21 @@ export class NewsletterCreatedHandler private childRepository: ChildRepository, ) {} - async handle({ newsletter }: NewsletterCreatedEvent): Promise { + async handle({ newsletter }: NewsletterCreatedEvent): Promise { + let errors: string[] = []; await this.repository.create(newsletter); if ( newsletter.recipients.length === 1 && newsletter.recipients[0] === 'fundacja@mali-wspaniali.pl' ) { - await this.sendMailWith(newsletter, newsletter.recipients); + errors = await this.sendMailWith(newsletter, newsletter.recipients); } if (newsletter.type.includes('ALL')) { const users = await this.userRepository.getAll({ role: 'parent' }); - await this.sendMailWith( + errors = await this.sendMailWith( newsletter, users.map(u => u.mail), ); @@ -48,15 +49,24 @@ export class NewsletterCreatedHandler children.map(c => c.id), ); - await this.sendMailWith( + errors = await this.sendMailWith( newsletter, parents.map(u => u.mail), ); } + + if (errors?.length) { + console.error('send mail ERRORS:', errors); + } + + return errors; } - sendMailWith(newsletter: NewsletterCore, bcc: string[]) { - return this.sendMail.send({ + async sendMailWith( + newsletter: NewsletterCore, + bcc: string[], + ): Promise { + return await this.sendMail.send({ from: process.env.SENDER, bcc, subject: newsletter.title, diff --git a/server/src/shared/services/send_mail/freshmail_service.ts b/server/src/shared/services/send_mail/freshmail_service.ts index 2ffa8a01a..ce43df5f6 100644 --- a/server/src/shared/services/send_mail/freshmail_service.ts +++ b/server/src/shared/services/send_mail/freshmail_service.ts @@ -4,39 +4,23 @@ import { Sendable } from '../../../shared/services/send_mail/send_mail'; @Injectable() export class FreshmailProvider implements Sendable { - send(options: { + async send(options: { from: string; bcc: string[]; subject: string; text: string; html: string; - }) { - axios.post( - `${this.getRootUrl()}/messaging/emails/`, - { - recipients: options.bcc.map(mail => ({ - email: mail, - name: mail, - })), - from: { - name: options.from, - email: options.from, - }, - subject: options.subject, + }): Promise { + const errors: string[] = []; + const chunkSize = 100; - contents: [ - { - type: 'text/html', - body: options.html, - }, - { - type: 'text/plain', - body: options.text, - }, - ], - }, - { headers: this.createHeader() }, - ); + for (let i = 0; i < options.bcc.length; i += chunkSize) { + const chunk = options.bcc.slice(i, i + chunkSize); + const error = await this.sendChunk({ ...options, bcc: chunk }); + errors.push(...error); + } + + return errors; } private createHeader() { @@ -46,4 +30,50 @@ export class FreshmailProvider implements Sendable { private getRootUrl() { return 'https://api.freshmail.com/v3'; } + + private async sendChunk(options: { + from: string; + bcc: string[]; + subject: string; + text: string; + html: string; + }): Promise { + const errors: string[] = []; + + await axios + .post( + `${this.getRootUrl()}/messaging/emails/`, + { + recipients: options.bcc.map(mail => ({ + email: mail, + name: mail, + })), + from: { + name: options.from, + email: options.from, + }, + subject: options.subject, + + contents: [ + { + type: 'text/html', + body: options.html, + }, + { + type: 'text/plain', + body: options.text, + }, + ], + }, + { headers: this.createHeader() }, + ) + .then(() => { + console.log('Emails processed: ', options.bcc.length); + }) + .catch(error => { + errors.push(...error.response.data.errors); + }); + + return errors; + } } diff --git a/server/src/shared/services/send_mail/send_mail.ts b/server/src/shared/services/send_mail/send_mail.ts index 35bbe8770..f6c66661e 100644 --- a/server/src/shared/services/send_mail/send_mail.ts +++ b/server/src/shared/services/send_mail/send_mail.ts @@ -22,13 +22,14 @@ export class SendMail { private readonly freshmailService: FreshmailProvider, ) {} - send(options: MailOptions): void { + async send(options: MailOptions): Promise { if (process.env.IS_PRODUCTION_MAIL === 'true') { - this.freshmailService.send(options); - - return; + return await this.freshmailService.send(options); } - this.sandboxProvider.send(options); + await this.sandboxProvider.send(options); + console.log('Emails processed:', options.bcc.length); + + return []; } }