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={[
+ ,
+ ]}
+ />
+ )}
+
+
+
+
+ );
+
+ 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={[
- ,
- ]}
- />
- )}
-
-
-
-
- );
-
- 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 [];
}
}