Skip to content

Commit bf03bc2

Browse files
committed
Refactor event and repetition validation logic.
Simplify and centralize event scheduling and repetition validation by consolidating error handling and duration calculations. Introduced reusable utility functions and constants, improved error messaging, and streamlined runtime behavior to improve maintainability and clarity.
1 parent 0a4a835 commit bf03bc2

8 files changed

Lines changed: 206 additions & 238 deletions

File tree

src/components/dialog/StudyDialog.vue

Lines changed: 27 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Prevention -- A research institute of the Ludwig Boltzmann Gesellschaft,
44
Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung).
55
Licensed under the Elastic License 2.0. */
66
<script setup lang="ts">
7-
import { computed, inject, reactive, ref, Ref, watch } from 'vue';
7+
import { inject, reactive, ref, Ref, watch } from 'vue';
88
import InputText from 'primevue/inputtext';
99
import Calendar from 'primevue/calendar';
1010
import Textarea from 'primevue/textarea';
@@ -15,13 +15,12 @@ Licensed under the Elastic License 2.0. */
1515
DurationUnitEnum,
1616
Study,
1717
} from '../../generated-sources/openapi';
18-
import { dateToDateString } from '../../utils/dateUtils';
18+
import { createLuxonDateTime, dateToDateString } from '../../utils/dateUtils';
1919
import { useI18n } from 'vue-i18n';
20-
import { MoreTableChoice } from '../../models/MoreTableModel';
2120
import { useGlobalStore } from '../../stores/globalStore';
22-
import { DateTime, DurationLikeObject } from 'luxon';
2321
import ErrorLabel from '../forms/ErrorLabel.vue';
24-
import { roundAndCeil } from '../../utils/dataUtils';
22+
import { useErrorQueue } from '../../composable/useErrorHandling';
23+
import { calcStudyDuration } from '../../utils/studyUtils';
2524
2625
const dateFormat = useGlobalStore().getDateFormat;
2726
@@ -57,40 +56,6 @@ Licensed under the Elastic License 2.0. */
5756
5857
const maxStudyDuration = ref<number>(0);
5958
60-
const calcMaxStudyDuration = (
61-
duration: Duration,
62-
start: Date,
63-
end: Date,
64-
): number => {
65-
if (duration.unit) {
66-
const startDateTime = DateTime.fromJSDate(start).set({
67-
hour: 0,
68-
minute: 0,
69-
});
70-
const endDateTime = DateTime.fromJSDate(end).set({
71-
hour: 23,
72-
minute: 59,
73-
});
74-
if (
75-
!startDateTime.isValid ||
76-
!endDateTime.isValid ||
77-
startDateTime > endDateTime
78-
) {
79-
return 0;
80-
}
81-
return roundAndCeil(
82-
startDateTime
83-
.diff(endDateTime)
84-
.as(
85-
duration.unit
86-
?.toString()
87-
?.toLowerCase() as keyof DurationLikeObject,
88-
),
89-
);
90-
}
91-
return -1;
92-
};
93-
9459
const contactInstitute: Ref<string> = ref(study.contact?.institute ?? '');
9560
const contactPerson: Ref<string> = ref(
9661
study.contact?.person && study.contact?.person !== 'pending'
@@ -121,90 +86,67 @@ Licensed under the Elastic License 2.0. */
12186
}
12287
}
12388
124-
const errors = ref<MoreTableChoice[]>([]);
89+
const { errors, clearError, getError, addError } = useErrorQueue();
12590
12691
function checkRequiredFields(): void {
12792
errors.value = [];
12893
if (!returnStudy.title) {
129-
errors.value.push({ label: 'title', value: t('study.error.addTitle') });
94+
addError({ label: 'title', value: t('study.error.addTitle') });
13095
}
13196
if (!returnStudy.consentInfo) {
132-
errors.value.push({
97+
addError({
13398
label: 'consentInfo',
13499
value: t('study.error.addConsentInfo'),
135100
});
136101
}
137102
if (!returnStudy.participantInfo) {
138-
errors.value.push({
103+
addError({
139104
label: 'participantInfo',
140105
value: t('study.error.addParticipantInfo'),
141106
});
142107
}
143108
if (!contactPerson.value && !contactEmail.value) {
144-
errors.value.push({
109+
addError({
145110
label: 'contactInfo',
146111
value: t('study.error.addContactInfo'),
147112
});
148113
} else if (!contactPerson.value) {
149-
errors.value.push({
114+
addError({
150115
label: 'contactPerson',
151116
value: t('study.error.addContactPerson'),
152117
});
153118
} else if (!contactEmail.value) {
154-
errors.value.push({
119+
addError({
155120
label: 'contactEmail',
156121
value: t('study.error.addContactEmail'),
157122
});
158123
}
159124
}
160125
161-
const getError = computed(
162-
() =>
163-
(label: string | string[]): string | null | undefined => {
164-
if (Array.isArray(label)) {
165-
for (const lbl of label) {
166-
const error = errors.value.find((el) => el.label === lbl)?.value;
167-
if (error !== null && error !== undefined) {
168-
return error;
169-
}
170-
}
171-
} else {
172-
return errors.value.find((el) => el.label === label)?.value;
173-
}
174-
return null;
175-
},
176-
);
177-
178-
const clearError = (label: string | string[]): void => {
179-
if (Array.isArray(label)) {
180-
errors.value = errors.value.filter((el) => !label.includes(el.label));
181-
} else {
182-
errors.value = errors.value.filter((el) => el.label !== label);
183-
}
184-
};
185-
186-
watch([start, end], ([newStart, newEnd]) => {
187-
if (newEnd < newStart) {
188-
end.value = newStart;
189-
}
190-
});
191-
192126
watch(
193127
[studyDuration, start, end],
194128
([newDuration, newStart, newEnd]) => {
195-
const maxDuration = calcMaxStudyDuration(newDuration, newStart, newEnd);
196-
if (maxDuration < 0) {
129+
if (newEnd < newStart) {
130+
end.value = newStart;
131+
}
132+
const maxDuration = calcStudyDuration(
133+
createLuxonDateTime(newStart),
134+
createLuxonDateTime(newEnd),
135+
newDuration,
136+
);
137+
if (!maxDuration?.value) {
197138
return;
198139
} else {
199-
maxStudyDuration.value = maxDuration;
140+
maxStudyDuration.value = maxDuration.value;
200141
}
201142
202143
if (studyDuration.value && studyDuration.value > maxStudyDuration.value) {
203-
studyDuration.value = maxStudyDuration.value;
204-
errors.value = errors.value.filter((err) => err.label !== 'duration');
205-
errors.value.push({
144+
clearError('duration');
145+
addError({
206146
label: 'duration',
207-
value: t('study.error.durationSmallerThanStudySpan'),
147+
value: t('study.error.durationSmallerThanStudySpan', {
148+
maxDuration: maxStudyDuration.value,
149+
}),
208150
});
209151
}
210152
},
@@ -455,6 +397,7 @@ Licensed under the Elastic License 2.0. */
455397
class="!ml-2"
456398
type="submit"
457399
:label="$t('global.labels.save')"
400+
:disabled="errors.length > 0"
458401
@click="checkRequiredFields"
459402
/>
460403
</div>

src/components/shared/RelativeScheduler.vue

Lines changed: 36 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
correctEvent,
2121
correctEventRepetition,
2222
} from '../../utils/relativeScheduleUtils';
23-
import { studyDuration } from '../../utils/studyUtils';
23+
import { calcStudyDurationFromStudy } from '../../utils/studyUtils';
2424
import ErrorLabel from '../forms/ErrorLabel.vue';
2525
import { useErrorQueue } from '../../composable/useErrorHandling';
2626
import { valueToMinutes } from '../../utils/durationUtils';
@@ -30,7 +30,7 @@
3030
const studyStore = useStudyStore();
3131
const { study } = storeToRefs(studyStore);
3232
const maxDuration = computed((): Duration | undefined =>
33-
studyDuration(study.value),
33+
calcStudyDurationFromStudy(study.value),
3434
);
3535
3636
const schedule: RelativeEvent = dialogRef.value.data.scheduler;
@@ -292,33 +292,17 @@
292292
newFrequency,
293293
newEndRep,
294294
]) => {
295-
const correctedEvent = correctEvent(
296-
newStartOffset,
297-
newEndOffset,
298-
newStartTime,
299-
newEndTime,
300-
maxDuration.value,
301-
);
302-
if (correctedEvent.offsetCorrected) {
303-
addError({
304-
label: 'scheduleTooLong',
305-
value: t('scheduler.dialog.relativeSchedule.error.scheduleTooLong'),
306-
});
307-
}
308-
if (
309-
startTime.value !== (correctedEvent.correctStart ?? startTime.value) ||
310-
endTime.value !== (correctedEvent.correctEnd ?? endTime.value)
311-
) {
312-
startTime.value = correctedEvent.correctStart ?? startTime.value;
313-
endTime.value = correctedEvent.correctEnd ?? endTime.value;
314-
addError({
315-
label: 'startTimeBeforeEnd',
316-
value: t(
317-
'scheduler.dialog.relativeSchedule.error.startTimeBeforeEnd',
318-
),
319-
});
320-
}
321295
if (maxDuration.value) {
296+
const errorInEvent = correctEvent(
297+
newStartOffset,
298+
newEndOffset,
299+
newStartTime,
300+
newEndTime,
301+
maxDuration.value,
302+
);
303+
if (errorInEvent) {
304+
addError(errorInEvent);
305+
}
322306
const correctedRepetition = correctEventRepetition(
323307
startOffset.value,
324308
startTime.value,
@@ -327,30 +311,20 @@
327311
newFrequency,
328312
newEndRep,
329313
maxDuration.value,
314+
!repeatChecked.value,
330315
);
331316
repetitionEnabled.value = correctedRepetition.repetitionEnabled;
332317
frequencyXTimes.value = correctedRepetition.numberOfRepetitions;
333318
if (!correctedRepetition.repetitionEnabled) {
334319
repeatChecked.value = false;
335-
} else {
336-
calcRepetition();
337320
}
321+
calcRepetition();
338322
if (repeatChecked.value) {
339-
if (correctedRepetition.frequencyCorrected) {
340-
addError({
341-
label: 'repetitionTooLong',
342-
value: t(
343-
'scheduler.dialog.relativeSchedule.error.rrrule.repetitionTooLong',
344-
),
345-
});
323+
if (correctedRepetition.frequencyError) {
324+
addError(correctedRepetition.frequencyError);
346325
}
347-
if (correctedRepetition.frequencyEndCorrected) {
348-
addError({
349-
label: 'repetitionEndTooLong',
350-
value: t(
351-
'scheduler.dialog.relativeSchedule.error.rrrule.repetitionEndTooLong',
352-
),
353-
});
326+
if (correctedRepetition.frequencyEndError) {
327+
addError(correctedRepetition.frequencyEndError);
354328
}
355329
}
356330
}
@@ -410,7 +384,7 @@
410384
(newVal) => {
411385
const dateVal = Array.isArray(newVal) ? newVal[0] : newVal;
412386
startTime = createLuxonDateTime(dateVal) || startTime;
413-
clearError(['startTimeBeforeEnd']);
387+
clearError(['offsetCorrection']);
414388
}
415389
"
416390
/>
@@ -434,7 +408,7 @@
434408
:min="1"
435409
@blur="calcRepetition()"
436410
@input="
437-
clearError(['dtend', 'scheduleTooLong', 'startTimeBeforeEnd'])
411+
clearError(['dtend', 'startTimeBeforeEnd', 'offsetCorrection'])
438412
"
439413
/>
440414
</div>
@@ -450,14 +424,14 @@
450424
(newVal) => {
451425
const dateVal = Array.isArray(newVal) ? newVal[0] : newVal;
452426
endTime = createLuxonDateTime(dateVal) || endTime;
453-
clearError(['startTimeBeforeEnd']);
427+
clearError(['startTimeBeforeEnd', 'offsetCorrection']);
454428
}
455429
"
456430
/>
457431
</div>
458432
</div>
459433
<ErrorLabel
460-
:error="getError(['dtend', 'scheduleTooLong', 'startTimeBeforeEnd'])"
434+
:error="getError(['dtend', 'offsetCorrection'])"
461435
class="col-span-5 col-start-2 border-l-2 pl-3"
462436
/>
463437
</div>
@@ -501,28 +475,26 @@
501475
$t('scheduler.dialog.relativeSchedule.placeholder.enterNumber')
502476
"
503477
:min="1"
504-
@input="clearError(['rrruleFreq', 'repetitionTooLong'])"
478+
@input="clearError(['rrruleFreq', 'frequencyError'])"
505479
/>
506480
<Dropdown
507481
v-model="frequency.unit"
508482
:options="repetitionUnit"
509483
:option-label="'label'"
510484
:option-value="'value'"
511485
class="col-span-3 ml-4"
512-
@change="clearError(['rrruleFreq', 'repetitionTooLong'])"
486+
@change="clearError(['rrruleFreq', 'frequencyError'])"
513487
/>
514488
</div>
515489
<div v-if="frequencyXTimes !== undefined" class="col-span-2">
516490
{{
517-
`${$t(
518-
'scheduler.dialog.relativeSchedule.rrrule.repeated',
519-
)}: ${frequencyXTimes} ${$t(
520-
'scheduler.dialog.relativeSchedule.rrrule.times',
521-
)}`
491+
$t('scheduler.dialog.relativeSchedule.rrrule.runTime', {
492+
repetitionNum: frequencyXTimes,
493+
})
522494
}}
523495
</div>
524496
<ErrorLabel
525-
:error="getError(['rrruleFreq', 'repetitionTooLong'])"
497+
:error="getError(['rrruleFreq', 'frequencyError'])"
526498
class="col-span-5 col-start-2 border-l-2 pl-3"
527499
/>
528500

@@ -537,15 +509,15 @@
537509
"
538510
class="z-10"
539511
:min="1"
540-
@input="clearError(['rrruleEndAfter', 'repetitionEndTooLong'])"
512+
@input="clearError(['rrruleEndAfter', 'frequencyEndError'])"
541513
/>
542514
<Dropdown
543515
v-model="endRep.unit"
544516
:options="repetitionUnit"
545517
option-label="label"
546518
option-value="value"
547519
class="z-10 col-span-3 ml-4"
548-
@change="clearError(['rrruleEndAfter', 'repetitionEndTooLong'])"
520+
@change="clearError(['rrruleEndAfter', 'frequencyEndError'])"
549521
/>
550522
</div>
551523
<div v-if="totalDays && totalDays > 0" class="col-span-2">
@@ -554,7 +526,7 @@
554526
}}
555527
</div>
556528
<ErrorLabel
557-
:error="getError(['rrruleEndAfter', 'repetitionEndTooLong'])"
529+
:error="getError(['rrruleEndAfter', 'frequencyEndError'])"
558530
class="col-span-5 col-start-2 border-l-2 pl-3"
559531
/>
560532
</div>
@@ -571,7 +543,11 @@
571543
:label="$t('global.labels.cancel')"
572544
@click="cancel()"
573545
/>
574-
<Button :label="$t('global.labels.save')" @click="save()" />
546+
<Button
547+
:disabled="errors.length > 0"
548+
:label="$t('global.labels.save')"
549+
@click="save()"
550+
/>
575551
</div>
576552
</div>
577553
</div>

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const minutesInDay = 1440;
2+
export const minutesInHour = 60;

0 commit comments

Comments
 (0)