Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • opentalk/web-frontend
1 result
Show changes
Commits on Source (2)
Showing
with 548 additions and 26 deletions
...@@ -949,7 +949,7 @@ dashboard-meeting-recurrence-daily = Täglich ...@@ -949,7 +949,7 @@ dashboard-meeting-recurrence-daily = Täglich
dashboard-meeting-recurrence-weekly = Wöchentlich dashboard-meeting-recurrence-weekly = Wöchentlich
dashboard-meeting-recurrence-bi-weekly = 14-Tägig dashboard-meeting-recurrence-bi-weekly = 14-Tägig
dashboard-meeting-recurrence-monthly = Monatlich dashboard-meeting-recurrence-monthly = Monatlich
dashboard-meeting-recurrence-custom = Benutzerdefiniert ... dashboard-meeting-recurrence-custom = Benutzerdefiniert
dashboard-recurrence-dialog-title = Benutzerdefinierte Meeting-Wiederholung dashboard-recurrence-dialog-title = Benutzerdefinierte Meeting-Wiederholung
dashboard-recurrence-dialog-frequency-label = Wiederholen alle dashboard-recurrence-dialog-frequency-label = Wiederholen alle
...@@ -978,6 +978,20 @@ dashboard-recurrence-dialog-end-option-on = Am ...@@ -978,6 +978,20 @@ dashboard-recurrence-dialog-end-option-on = Am
dashboard-recurrence-dialog-save-button = Speichern dashboard-recurrence-dialog-save-button = Speichern
dashboard-recurrence-dialog-close-button = Abbrechen dashboard-recurrence-dialog-close-button = Abbrechen
dashboard-meeting-training-participation-report-switch = Teilnahmebericht für Schulungen
dashboard-meeting-training-participation-report-option-every-thirty-min = Alle 30 Min. nachfragen
dashboard-meeting-training-participation-report-option-every-sixty-min = Alle 60 Min. nachfragen
dashboard-meeting-training-participation-report-option-thirty-to-sixty-min = Variabel alle 30 - 60 Min. nachfragen
dashboard-meeting-training-participation-report-option-ninety-to-hundred-twenty-min = Variabel alle 90 - 120 Min. nachfragen
dashboard-meeting-training-participation-report-option-custom = Benutzerdefiniert
dashboard-custom-training-participation-report-dialog-title = Benutzerdefinierte Abfragezeiten
dashboard-custom-training-participation-report-dialog-initial-timeout = Erste Bestätigungsabfrage variabel nach
dashboard-custom-training-participation-report-dialog-interval-duration = Nachfolgende Abfragen variabel nach
dashboard-custom-training-participation-report-dialog-to = bis
dashboard-custom-training-participation-report-dialog-minutes = Minuten
dashboard-custom-training-participation-report-dialog-error = Der Wert muss größer als {$min} sein
dashboard-payment-status-downgraded = Achtung: Für Ihren Account ist derzeit keine gültige Zahlmethode hinterlegt.<br /> Die Nutzung ist derzeit auf den {$tariffName}-Tarif eingeschränkt. dashboard-payment-status-downgraded = Achtung: Für Ihren Account ist derzeit keine gültige Zahlmethode hinterlegt.<br /> Die Nutzung ist derzeit auf den {$tariffName}-Tarif eingeschränkt.
dashboard-add-payment-button = Jetzt auswählen dashboard-add-payment-button = Jetzt auswählen
...@@ -1216,8 +1230,8 @@ timer-not-done-icon-title = Nicht fertig ...@@ -1216,8 +1230,8 @@ timer-not-done-icon-title = Nicht fertig
participation-confirmation-dialog-title = Bestätigung der Schulungs-Teilnahme participation-confirmation-dialog-title = Bestätigung der Schulungs-Teilnahme
participation-confirmation-dialog-description = Sie nehmen an einer Schulung oder einem Kurs teil. Für den Teilnahmebericht wird die Anwesenheit wiederholt abgefragt. participation-confirmation-dialog-description = Sie nehmen an einer Schulung oder einem Kurs teil. Für den Teilnahmebericht wird die Anwesenheit wiederholt abgefragt.
participation-confirmation-dialog-confirm-button = Anwesenheit bestätigen participation-confirmation-dialog-confirm-button = Anwesenheit bestätigen
training-participation-logging-disable-button = Schulungs-Teilnahmeprotokollierung aktivieren training-participation-logging-disable-button = Schulungs-Teilnahmeprotokollierung deaktivieren
training-participation-logging-enable-button = Schulungs-Teilnahmeprotokollierung deaktivieren training-participation-logging-enable-button = Schulungs-Teilnahmeprotokollierung aktivieren
training-participation-report-pdf-asset-notification = <messageContainer>Der Teilnahmebericht wurde exportiert. Die entsprechende Datei befindet sich im Dashboard unter den <messageLink>Meeting Details</messageLink>.</messageContainer> training-participation-report-pdf-asset-notification = <messageContainer>Der Teilnahmebericht wurde exportiert. Die entsprechende Datei befindet sich im Dashboard unter den <messageLink>Meeting Details</messageLink>.</messageContainer>
presence-logging-enabled-notification = Schulungs-Teilnahmeprotokollierung aktiviert presence-logging-enabled-notification = Schulungs-Teilnahmeprotokollierung aktiviert
presence-logging-disabled-notification = Schulungs-Teilnahmeprotokollierung deaktiviert presence-logging-disabled-notification = Schulungs-Teilnahmeprotokollierung deaktiviert
......
...@@ -949,7 +949,7 @@ dashboard-meeting-recurrence-daily = Daily ...@@ -949,7 +949,7 @@ dashboard-meeting-recurrence-daily = Daily
dashboard-meeting-recurrence-weekly = Weekly dashboard-meeting-recurrence-weekly = Weekly
dashboard-meeting-recurrence-bi-weekly = Bi-Weekly dashboard-meeting-recurrence-bi-weekly = Bi-Weekly
dashboard-meeting-recurrence-monthly = Monthly dashboard-meeting-recurrence-monthly = Monthly
dashboard-meeting-recurrence-custom = Custom ... dashboard-meeting-recurrence-custom = Custom
dashboard-recurrence-dialog-title = Custom meeting repetition dashboard-recurrence-dialog-title = Custom meeting repetition
dashboard-recurrence-dialog-frequency-label = Repeat every dashboard-recurrence-dialog-frequency-label = Repeat every
...@@ -978,6 +978,20 @@ dashboard-recurrence-dialog-end-option-on = On ...@@ -978,6 +978,20 @@ dashboard-recurrence-dialog-end-option-on = On
dashboard-recurrence-dialog-save-button = Save dashboard-recurrence-dialog-save-button = Save
dashboard-recurrence-dialog-close-button = Cancel dashboard-recurrence-dialog-close-button = Cancel
dashboard-meeting-training-participation-report-switch = Training participation report
dashboard-meeting-training-participation-report-option-every-thirty-min = Ask every 30 min
dashboard-meeting-training-participation-report-option-every-sixty-min = Ask every 60 min
dashboard-meeting-training-participation-report-option-thirty-to-sixty-min = Variable ask every 30 - 60 min
dashboard-meeting-training-participation-report-option-ninety-to-hundred-twenty-min = Variable ask every 90 - 120 min
dashboard-meeting-training-participation-report-option-custom = Custom
dashboard-custom-training-participation-report-dialog-title = Custom query times
dashboard-custom-training-participation-report-dialog-initial-timeout = First confirmation query variable after
dashboard-custom-training-participation-report-dialog-interval-duration = Subsequent queries variable after
dashboard-custom-training-participation-report-dialog-to = to
dashboard-custom-training-participation-report-dialog-minutes = minutes
dashboard-custom-training-participation-report-dialog-error = The value must be greater than {$min}
dashboard-payment-status-downgraded = Attention: There is currently no valid payment method stored for your account.<br /> Currently you are restricted to the {$tariffName} plan. dashboard-payment-status-downgraded = Attention: There is currently no valid payment method stored for your account.<br /> Currently you are restricted to the {$tariffName} plan.
dashboard-add-payment-button = Add payment dashboard-add-payment-button = Add payment
......
// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu> // SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
// //
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
import { TrainingParticipationReportParameterSet } from '@opentalk/rest-api-rtk-query/src/types/event';
import type { RootState } from '../../../store'; import type { RootState } from '../../../store';
import { createModule, Namespaced } from '../../../types'; import { createModule, Namespaced } from '../../../types';
import { createSignalingApiCall } from '../../createSignalingApiCall'; import { createSignalingApiCall } from '../../createSignalingApiCall';
...@@ -20,32 +22,12 @@ export enum ParticipationLoggingState { ...@@ -20,32 +22,12 @@ export enum ParticipationLoggingState {
*/ */
WaitingForConfirmation = 'waiting_for_confirmation', WaitingForConfirmation = 'waiting_for_confirmation',
} }
export type ParticipationLogging = { export type ParticipationLogging = {
state: ParticipationLoggingState; state: ParticipationLoggingState;
}; };
export interface TimeRange { export interface EnablePresenceLogging extends Partial<TrainingParticipationReportParameterSet> {
/**
* The earliest number of seconds after which the checkpoint can be created. Must be a strictly positive number.
*/
after: number;
/**
* The number of seconds within which the checkpoint can be created after the after value. Must be 0 or greater.
*/
within: number;
}
export interface EnablePresenceLogging {
action: 'enable_presence_logging'; action: 'enable_presence_logging';
/**
* default: { "after": 600, "within": 1200 }
*/
initialCheckpointDelay?: TimeRange;
/**
* default: { "after": 6300, "within": 1800 }
*/
checkpointInterval?: TimeRange;
} }
export interface DisablePresenceLogging { export interface DisablePresenceLogging {
......
...@@ -18,7 +18,7 @@ import { ...@@ -18,7 +18,7 @@ import {
import { Interval, addMinutes, areIntervalsOverlapping, formatRFC3339 } from 'date-fns'; import { Interval, addMinutes, areIntervalsOverlapping, formatRFC3339 } from 'date-fns';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { FormikValues } from 'formik/dist/types'; import { FormikValues } from 'formik/dist/types';
import { isEmpty } from 'lodash'; import { isEmpty, isEqual } from 'lodash';
import { useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
...@@ -48,6 +48,7 @@ import { CreateOrUpdateMeetingFormikValues, DashboardDateTimePicker } from './fr ...@@ -48,6 +48,7 @@ import { CreateOrUpdateMeetingFormikValues, DashboardDateTimePicker } from './fr
import EventConflictDialog from './fragments/EventConflictDialog'; import EventConflictDialog from './fragments/EventConflictDialog';
import MeetingFormSwitch from './fragments/MeetingFormSwitch'; import MeetingFormSwitch from './fragments/MeetingFormSwitch';
import StreamingOptions from './fragments/StreamingOptions'; import StreamingOptions from './fragments/StreamingOptions';
import { TrainingParticipationReportSelect } from './fragments/TrainingParticipationReportSelect/TrainingParticipationReportSelect';
interface CreateOrUpdateMeetingFormProps { interface CreateOrUpdateMeetingFormProps {
existingEvent?: Event; existingEvent?: Event;
...@@ -86,6 +87,7 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -86,6 +87,7 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
const { data: tariff } = useGetMeTariffQuery(); const { data: tariff } = useGetMeTariffQuery();
const isStreamingEnabled = tariff && isFeatureEnabledPredicate('stream', tariff.modules); const isStreamingEnabled = tariff && isFeatureEnabledPredicate('stream', tariff.modules);
const isTrainingParticipationReportEnabled = tariff?.modules.trainingParticipationReport;
const navigate = useNavigate(); const navigate = useNavigate();
...@@ -173,6 +175,25 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -173,6 +175,25 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
}), }),
}), }),
e2eEncryption: yup.boolean().optional(), e2eEncryption: yup.boolean().optional(),
trainingParticipationReport: yup.object().shape({
enabled: yup.boolean().required(),
parameter: yup.object().when('enabled', ([enabled]) => {
if (!enabled) {
return yup.object().optional();
}
return yup.object({
initialCheckpointDelay: yup.object().shape({
after: yup.number().min(0).required(),
within: yup.number().min(0).required(),
}),
checkpointInterval: yup.object().shape({
after: yup.number().min(60).required(),
within: yup.number().min(0).required(),
}),
});
}),
}),
}); });
const recurrenceFrequencyOptions: Array<FrequencyOption> = [ const recurrenceFrequencyOptions: Array<FrequencyOption> = [
...@@ -241,6 +262,27 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -241,6 +262,27 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
}; };
}; };
// const getTPRInitialValue = () => {
// console.debug('value in get initial: ', existingEvent?.trainingParticipationReport);
// return existingEvent?.trainingParticipationReport
// ? {
// enabled: true,
// parameter: existingEvent.trainingParticipationReport,
// }
// : {
// enabled: false,
// };
// };
const TPRValue = existingEvent?.trainingParticipationReport
? {
enabled: true,
parameter: existingEvent.trainingParticipationReport,
}
: {
enabled: false,
};
// We need to pass an empty array, if we want to disable streaming while updating an event // We need to pass an empty array, if we want to disable streaming while updating an event
const getStreamingPayload = (values: FormikValues) => { const getStreamingPayload = (values: FormikValues) => {
return values.streaming.enabled ? [values.streaming.platform] : []; return values.streaming.enabled ? [values.streaming.platform] : [];
...@@ -272,6 +314,8 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -272,6 +314,8 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
showMeetingDetails: getShowMeetingDetailsInitialValue(), showMeetingDetails: getShowMeetingDetailsInitialValue(),
streaming: getStreamingInitialValue(), streaming: getStreamingInitialValue(),
e2eEncryption: existingEvent?.room.e2EEncryption || false, e2eEncryption: existingEvent?.room.e2EEncryption || false,
// trainingParticipationReport: getTPRInitialValue(),
trainingParticipationReport: TPRValue,
}, },
validationSchema, validationSchema,
validateOnChange: false, validateOnChange: false,
...@@ -286,6 +330,25 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -286,6 +330,25 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
}, },
}); });
const createTrainingParticipationReportPayload = () => {
const localValue = formik.values.trainingParticipationReport;
//null will explicitly force the backend to remove the previous value
//undefined will ignore the field in case we do not need any update
if (!localValue.enabled) {
return existingEvent?.trainingParticipationReport ? null : undefined;
}
if (
existingEvent?.trainingParticipationReport &&
isEqual(existingEvent.trainingParticipationReport, localValue.parameter)
) {
return undefined;
}
//sending the value will create/update accordingly
return localValue.parameter;
};
const onChangeStartDate = async (date: Date | null) => { const onChangeStartDate = async (date: Date | null) => {
if (!date) { if (!date) {
await formik.setFieldValue('startDate', ''); await formik.setFieldValue('startDate', '');
...@@ -338,6 +401,7 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -338,6 +401,7 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
hasSharedFolder: values.sharedFolder || false, hasSharedFolder: values.sharedFolder || false,
streamingTargets: getStreamingPayload(values), streamingTargets: getStreamingPayload(values),
e2eEncryption: values.e2eEncryption || false, e2eEncryption: values.e2eEncryption || false,
trainingParticipationReport: createTrainingParticipationReportPayload(),
}; };
if (values.recurrencePattern) { if (values.recurrencePattern) {
...@@ -690,6 +754,8 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea ...@@ -690,6 +754,8 @@ const CreateOrUpdateMeetingForm = ({ existingEvent, onForwardButtonClick }: Crea
{isStreamingEnabled && <StreamingOptions formik={formik} />} {isStreamingEnabled && <StreamingOptions formik={formik} />}
{isTrainingParticipationReportEnabled && <TrainingParticipationReportSelect formik={formik} />}
{features.e2eEncryption && ( {features.e2eEncryption && (
<MeetingFormSwitch <MeetingFormSwitch
checked={formik.values.e2eEncryption} checked={formik.values.e2eEncryption}
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
import { Stack } from '@mui/material'; import { Stack } from '@mui/material';
import { RecurrencePattern, StreamingPlatform } from '@opentalk/rest-api-rtk-query'; import { RecurrencePattern, StreamingPlatform } from '@opentalk/rest-api-rtk-query';
import { TrainingParticipationReportParameterSet } from '@opentalk/rest-api-rtk-query/src/types/event';
import { FormikProps } from 'formik'; import { FormikProps } from 'formik';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
...@@ -15,6 +16,10 @@ interface Streaming { ...@@ -15,6 +16,10 @@ interface Streaming {
enabled: boolean; enabled: boolean;
streamingTarget?: StreamingPlatform; streamingTarget?: StreamingPlatform;
} }
interface TrainingParticipationReport {
enabled: boolean;
parameter?: TrainingParticipationReportParameterSet;
}
export interface CreateOrUpdateMeetingFormikValues { export interface CreateOrUpdateMeetingFormikValues {
title?: string; title?: string;
description?: string; description?: string;
...@@ -27,6 +32,7 @@ export interface CreateOrUpdateMeetingFormikValues { ...@@ -27,6 +32,7 @@ export interface CreateOrUpdateMeetingFormikValues {
isAdhoc?: boolean; isAdhoc?: boolean;
sharedFolder: boolean; sharedFolder: boolean;
streaming: Streaming; streaming: Streaming;
trainingParticipationReport: TrainingParticipationReport;
showMeetingDetails: boolean; showMeetingDetails: boolean;
e2eEncryption: boolean; e2eEncryption: boolean;
} }
......
// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
//
// SPDX-License-Identifier: EUPL-1.2
import { render, screen } from '@testing-library/react';
import {
CustomTrainingParticipationReportDialog,
CustomTrainingParticipationReportDialogProps,
} from './CustomTrainingParticipationReportDialog';
const mockDialogProps: CustomTrainingParticipationReportDialogProps = {
closeDialog: jest.fn(),
saveOption: jest.fn(),
previousOption: {
initialCheckpointDelay: { after: 60, within: 60 },
checkpointInterval: { after: 120, within: 120 },
},
};
describe('Custom Recurrence Dialog', () => {
test('Dialog renders correctly', () => {
render(<CustomTrainingParticipationReportDialog {...mockDialogProps} />);
expect(screen.getByTestId('custom-training-participation-report-dialog')).toBeInTheDocument();
});
});
// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
//
// SPDX-License-Identifier: EUPL-1.2
import {
Button,
Dialog,
DialogActions,
DialogContent as MuiDialogContent,
DialogProps,
DialogTitle,
Stack,
styled,
Typography,
} from '@mui/material';
import { TrainingParticipationReportParameterSet } from '@opentalk/rest-api-rtk-query/src/types/event';
import { minutesToSeconds, secondsToMinutes } from 'date-fns';
import { useFormik } from 'formik';
import { useTranslation } from 'react-i18next';
import { CommonTextField, ErrorFormMessage } from '../../../../commonComponents';
import { formikNumberFieldProps } from '../../../../utils/formikUtils';
import yup from '../../../../utils/yupUtils';
export interface CustomTrainingParticipationReportDialogProps extends Omit<DialogProps, 'open'> {
closeDialog: () => void;
previousOption: TrainingParticipationReportParameterSet;
saveOption: (option: TrainingParticipationReportParameterSet) => void;
}
const CUSTOM_TRAINING_PARTICIPATION_REPORT_DIALOG_LABEL_ID = 'custom-training-participation-report-dialog-title';
const NumberInput = styled(CommonTextField)({
maxWidth: '4rem',
'& input': {
paddingRight: 0,
textAlign: 'center',
},
});
const DialogContent = styled(MuiDialogContent)(({ theme }) => ({
[theme.breakpoints.up('md')]: {
minWidth: theme.typography.pxToRem(500),
},
}));
export const CustomTrainingParticipationReportDialog = ({
previousOption,
closeDialog,
saveOption,
...props
}: CustomTrainingParticipationReportDialogProps) => {
const { t } = useTranslation();
const validationSchema = yup.object({
initialCheckpointDelay: yup.object().shape({
from: yup.number().min(0).required(),
to: yup.number().when('from', ([from]) => {
return yup.number().min(from, t('dashboard-custom-training-participation-report-dialog-error', { min: from }));
}),
}),
checkpointInterval: yup.object().shape({
from: yup.number().min(1).required(),
to: yup.number().when('from', ([from]) => {
return yup.number().min(from, t('dashboard-custom-training-participation-report-dialog-error', { min: from }));
}),
}),
});
const convertedPreviousOption = {
initialCheckpointDelay: {
from: secondsToMinutes(previousOption.initialCheckpointDelay.after),
to: secondsToMinutes(previousOption.initialCheckpointDelay.after + previousOption.initialCheckpointDelay.within),
},
checkpointInterval: {
from: secondsToMinutes(previousOption.checkpointInterval.after),
to: secondsToMinutes(previousOption.checkpointInterval.after + previousOption.checkpointInterval.within),
},
};
const formik = useFormik({
initialValues: convertedPreviousOption,
validationSchema,
validateOnChange: true,
validateOnBlur: false,
onSubmit: ({ initialCheckpointDelay, checkpointInterval }) => {
const initialCheckpointDelayAfter = minutesToSeconds(initialCheckpointDelay.from);
const initialCheckpointDelayWithin = minutesToSeconds(initialCheckpointDelay.to - initialCheckpointDelay.from);
const checkpointIntervalAfter = minutesToSeconds(checkpointInterval.from);
const checkpointIntervalWithin = minutesToSeconds(checkpointInterval.to - checkpointInterval.from);
saveOption({
initialCheckpointDelay: { after: initialCheckpointDelayAfter, within: initialCheckpointDelayWithin },
checkpointInterval: { after: checkpointIntervalAfter, within: checkpointIntervalWithin },
});
closeDialog();
},
});
const closeWithReset = () => {
closeDialog();
formik.resetForm();
};
return (
<Dialog
{...props}
open
onClose={closeWithReset}
aria-labelledby={CUSTOM_TRAINING_PARTICIPATION_REPORT_DIALOG_LABEL_ID}
data-testid="custom-training-participation-report-dialog"
>
<DialogTitle id={CUSTOM_TRAINING_PARTICIPATION_REPORT_DIALOG_LABEL_ID}>
{t('dashboard-custom-training-participation-report-dialog-title')}
</DialogTitle>
<DialogContent>
<Stack marginBottom={2}>
<Typography>{t('dashboard-custom-training-participation-report-dialog-initial-timeout')}</Typography>
<Stack direction="row" spacing={1} alignItems="center">
<NumberInput
{...formikNumberFieldProps('initialCheckpointDelay.from', formik)}
type="number"
slotProps={{ htmlInput: { min: 0 } }}
/>
<Typography>{t('dashboard-custom-training-participation-report-dialog-to')}</Typography>
<NumberInput
{...formikNumberFieldProps('initialCheckpointDelay.to', formik)}
type="number"
slotProps={{ htmlInput: { min: formik.values.initialCheckpointDelay.from } }}
/>
<Typography>{t('dashboard-custom-training-participation-report-dialog-minutes')}</Typography>
</Stack>
{formik.errors.initialCheckpointDelay?.to && (
<ErrorFormMessage helperText={formik.errors.initialCheckpointDelay.to} />
)}
</Stack>
<Stack>
<Typography>{t('dashboard-custom-training-participation-report-dialog-interval-duration')}</Typography>
<Stack direction="row" spacing={1} alignItems="center">
<NumberInput
{...formikNumberFieldProps('checkpointInterval.from', formik)}
type="number"
slotProps={{ htmlInput: { min: 1 } }}
/>
<Typography>{t('dashboard-custom-training-participation-report-dialog-to')}</Typography>
<NumberInput
{...formikNumberFieldProps('checkpointInterval.to', formik)}
type="number"
slotProps={{ htmlInput: { min: formik.values.checkpointInterval.from } }}
/>
<Typography>{t('dashboard-custom-training-participation-report-dialog-minutes')}</Typography>
</Stack>
{formik.errors.checkpointInterval?.to && (
<ErrorFormMessage helperText={formik.errors.checkpointInterval.to} />
)}
</Stack>
</DialogContent>
<DialogActions>
<Button variant="contained" color="secondary" onClick={closeWithReset}>
{t('global-close')}
</Button>
<Button variant="contained" onClick={() => formik.handleSubmit()}>
{t('global-save')}
</Button>
</DialogActions>
</Dialog>
);
};
// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
//
// SPDX-License-Identifier: EUPL-1.2
import { RecurrencePattern } from '@opentalk/rest-api-rtk-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { Formik } from 'formik';
import { TrainingParticipationReportSelect } from './TrainingParticipationReportSelect';
const initialValues = {
trainingParticipationReport: {
enabled: true,
},
waitingRoom: false,
isTimeDependent: false,
startDate: '',
endDate: '',
recurrencePattern: '' as RecurrencePattern,
sharedFolder: false,
streaming: {
enabled: false,
},
showMeetingDetails: false,
e2eEncryption: false,
};
describe('Training participation report select', () => {
test('Select is not rendered when not enabled', async () => {
render(
<Formik initialValues={{ ...initialValues, trainingParticipationReport: { enabled: false } }} onSubmit={() => {}}>
{(formikProps) => <TrainingParticipationReportSelect formik={formikProps} />}
</Formik>
);
const selectButton = screen.queryByRole('combobox');
expect(selectButton).not.toBeInTheDocument();
});
test('Select is rendered and usable when enabled', () => {
render(
<Formik initialValues={initialValues} onSubmit={() => {}}>
{(formikProps) => <TrainingParticipationReportSelect formik={formikProps} />}
</Formik>
);
const selectButton = screen.getByRole('combobox');
expect(selectButton).toBeInTheDocument();
selectButton && fireEvent.mouseDown(selectButton);
const listbox = screen.getByRole('listbox');
expect(listbox).toBeInTheDocument();
const sixtyMinOption = screen.getByRole('option', {
name: 'dashboard-meeting-training-participation-report-option-every-sixty-min',
});
fireEvent.click(sixtyMinOption);
const select = screen.getByTestId('parameter-select').textContent?.replace(/\u200B/g, '');
expect(select).toBe('dashboard-meeting-training-participation-report-option-every-sixty-min');
});
});
// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
//
// SPDX-License-Identifier: EUPL-1.2
import { Collapse, MenuItem, Stack } from '@mui/material';
import { TrainingParticipationReportParameterSet } from '@opentalk/rest-api-rtk-query/src/types/event';
import { FormikProps } from 'formik';
import { isEqual } from 'lodash';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CommonTextField } from '../../../../commonComponents';
import { formikMinimalProps } from '../../../../utils/formikUtils';
import { CreateOrUpdateMeetingFormikValues } from '../DashboardDateTimePicker';
import MeetingFormSwitch from '../MeetingFormSwitch';
import { CustomTrainingParticipationReportDialog } from './CustomTrainingParticipationReportDialog';
interface TrainingParticipationReportSelectProps {
formik: FormikProps<CreateOrUpdateMeetingFormikValues>;
}
enum TrainingParticipationReportConfigOptions {
EveryThirtyMin = 'every-thirty-min',
EverySixtyMin = 'every-sixty-min',
ThirtyToSixtyMin = 'thirty-to-sixty-min',
NinetyToOneHundredTwentyMin = 'ninety-to-hundred-twenty-min',
Custom = 'custom',
}
export const TrainingParticipationReportSelect = ({ formik }: TrainingParticipationReportSelectProps) => {
const { t } = useTranslation();
const { enabled, parameter } = formik.values.trainingParticipationReport;
const [isCustomDialogOpen, setIsCustomDialogOpen] = useState(false);
const [customOption, setCustomOption] = useState({
initialCheckpointDelay: { after: 600, within: 1200 },
checkpointInterval: { after: 2700, within: 900 },
});
const options: Record<TrainingParticipationReportConfigOptions, TrainingParticipationReportParameterSet> = {
[TrainingParticipationReportConfigOptions.EveryThirtyMin]: {
initialCheckpointDelay: { after: 1800, within: 0 },
checkpointInterval: { after: 1800, within: 0 },
},
[TrainingParticipationReportConfigOptions.EverySixtyMin]: {
initialCheckpointDelay: { after: 3600, within: 0 },
checkpointInterval: { after: 3600, within: 0 },
},
[TrainingParticipationReportConfigOptions.ThirtyToSixtyMin]: {
initialCheckpointDelay: { after: 1800, within: 1800 },
checkpointInterval: { after: 1800, within: 1800 },
},
[TrainingParticipationReportConfigOptions.NinetyToOneHundredTwentyMin]: {
initialCheckpointDelay: { after: 5400, within: 1800 },
checkpointInterval: { after: 5400, within: 1800 },
},
[TrainingParticipationReportConfigOptions.Custom]: customOption,
};
const [selectedOption, setSelectedOption] = useState(TrainingParticipationReportConfigOptions.EveryThirtyMin);
useEffect(() => {
if (!enabled) {
return;
}
if (parameter) {
const existingOptionKey = (Object.keys(options) as Array<TrainingParticipationReportConfigOptions>).find(
(option) => isEqual(parameter, options[option])
);
if (existingOptionKey) {
setSelectedOption(existingOptionKey);
} else {
setSelectedOption(TrainingParticipationReportConfigOptions.Custom);
setCustomOption(parameter);
}
return;
}
formik.setFieldValue(
'trainingParticipationReport.parameter',
options[TrainingParticipationReportConfigOptions.EveryThirtyMin]
);
}, [enabled]);
const handleSelect = (option: TrainingParticipationReportConfigOptions) => {
if (option === TrainingParticipationReportConfigOptions.Custom) {
setIsCustomDialogOpen(true);
}
setSelectedOption(option);
formik.setFieldValue('trainingParticipationReport.parameter', options[option]);
};
const handleSave = (value: TrainingParticipationReportParameterSet) => {
setCustomOption(value);
formik.setFieldValue('trainingParticipationReport.parameter', value);
setIsCustomDialogOpen(false);
};
return (
<Stack spacing={2}>
<MeetingFormSwitch
switchProps={formikMinimalProps('trainingParticipationReport.enabled', formik)}
checked={enabled}
switchValueLabel={t('dashboard-meeting-training-participation-report-switch')}
/>
<Collapse orientation="vertical" in={enabled} unmountOnExit mountOnEnter>
<CommonTextField data-testid="parameter-select" select value={selectedOption}>
{Object.keys(options).map((option) => (
<MenuItem
key={option}
value={option}
onClick={() => handleSelect(option as TrainingParticipationReportConfigOptions)}
>
{t(`dashboard-meeting-training-participation-report-option-${option}`)}
</MenuItem>
))}
</CommonTextField>
</Collapse>
{isCustomDialogOpen && (
<CustomTrainingParticipationReportDialog
closeDialog={() => setIsCustomDialogOpen(false)}
previousOption={customOption}
saveOption={handleSave}
/>
)}
</Stack>
);
};
...@@ -171,3 +171,33 @@ export function formikDurationFieldProps<Values>( ...@@ -171,3 +171,33 @@ export function formikDurationFieldProps<Values>(
helperText: (hasError && (errorMessage as string)) || undefined, helperText: (hasError && (errorMessage as string)) || undefined,
}; };
} }
export function formikNumberFieldProps<Values>(
fieldName: string,
formik: FormikProps<Values>,
/**
* Duration value in minutes
*
* Default: 1
*/
defaultValue?: number
) {
const { values, handleBlur, handleChange } = formik;
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const nextValue = parseInt(event.target.value);
if (Number.isNaN(nextValue)) {
handleChange({ ...event, target: { ...event.target, value: defaultValue } });
return;
}
handleChange(event);
};
return {
name: fieldName,
onChange: onChange,
onBlur: handleBlur,
value: get(values, fieldName, defaultValue ?? 1)?.toString(),
};
}
...@@ -34,6 +34,27 @@ export interface SharedFolderData { ...@@ -34,6 +34,27 @@ export interface SharedFolderData {
readWrite?: SharedFolderCredentials; readWrite?: SharedFolderCredentials;
} }
export interface TimeRange {
/**
* The earliest number of seconds after which the checkpoint can be created. Must be 0 or greater.
*/
after: number;
/**
* The number of seconds within which the checkpoint can be created after the after value. Must be 0 or greater.
*/
within: number;
}
export interface TrainingParticipationReportParameterSet {
/**
* default: { "after": 600, "within": 1200 }
*/
initialCheckpointDelay: TimeRange;
/**
* default: { "after": 6300, "within": 1800 }
*/
checkpointInterval: TimeRange;
}
/** /**
* EventRoomInfo in an Event object * EventRoomInfo in an Event object
*/ */
...@@ -83,6 +104,7 @@ export interface CreateBaseEventPayload { ...@@ -83,6 +104,7 @@ export interface CreateBaseEventPayload {
showMeetingDetails?: boolean; showMeetingDetails?: boolean;
hasSharedFolder?: boolean; hasSharedFolder?: boolean;
streamingTargets?: Array<StreamingPlatform>; streamingTargets?: Array<StreamingPlatform>;
trainingParticipationReport?: TrainingParticipationReportParameterSet;
} }
/** /**
...@@ -156,6 +178,7 @@ export interface UpdateEventPayload { ...@@ -156,6 +178,7 @@ export interface UpdateEventPayload {
showMeetingDetails?: boolean; showMeetingDetails?: boolean;
hasSharedFolder?: boolean; hasSharedFolder?: boolean;
streamingTargets?: Array<StreamingPlatform>; streamingTargets?: Array<StreamingPlatform>;
trainingParticipationReport?: TrainingParticipationReportParameterSet | null;
} }
/** /**
...@@ -228,6 +251,7 @@ interface AbstractEvent extends BaseEvent { ...@@ -228,6 +251,7 @@ interface AbstractEvent extends BaseEvent {
sharedFolder?: SharedFolderData; sharedFolder?: SharedFolderData;
showMeetingDetails?: boolean; showMeetingDetails?: boolean;
streamingTargets?: Array<StreamingPlatform>; streamingTargets?: Array<StreamingPlatform>;
trainingParticipationReport?: TrainingParticipationReportParameterSet;
} }
/** /**
......

Consent

On this website, we use the web analytics service Matomo to analyze and review the use of our website. Through the collected statistics, we can improve our offerings and make them more appealing for you. Here, you can decide whether to allow us to process your data and set corresponding cookies for these purposes, in addition to technically necessary cookies. Further information on data protection—especially regarding "cookies" and "Matomo"—can be found in our privacy policy. You can withdraw your consent at any time.