@webitel/api-services 26.8.70 → 26.8.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/scripts/convertTimeUnits/__tests__/convertTimeUnits.spec.ts +19 -0
- package/src/scripts/convertTimeUnits/convertTimeUnits.ts +3 -0
- package/src/scripts/index.ts +3 -0
- package/src/validations/_shared/timeRange.validations.ts +103 -0
- package/src/validations/calendar/__tests__/calendar.validations.spec.ts +9 -9
- package/src/validations/calendar/calendar.validations.ts +10 -131
- package/src/validations/index.ts +3 -0
- package/src/validations/resourceGroup/__tests__/resourceGroup.validations.spec.ts +191 -0
- package/src/validations/resourceGroup/resourceGroup.validations.ts +36 -0
- package/src/validations/resourceInGroup/__tests__/resourceInGroup.validations.spec.ts +38 -0
- package/src/validations/resourceInGroup/resourceInGroup.validations.ts +16 -0
- package/types/.tsbuildinfo +1 -1
- package/types/scripts/convertTimeUnits/convertTimeUnits.d.ts +2 -0
- package/types/scripts/index.d.ts +2 -1
- package/types/validations/_shared/timeRange.validations.d.ts +18 -0
- package/types/validations/calendar/calendar.validations.d.ts +1 -7
- package/types/validations/index.d.ts +3 -0
- package/types/validations/resourceGroup/resourceGroup.validations.d.ts +17 -0
- package/types/validations/resourceInGroup/resourceInGroup.validations.d.ts +8 -0
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { minToSec, secToMin } from '../convertTimeUnits';
|
|
4
|
+
|
|
5
|
+
describe('convertTimeUnits', () => {
|
|
6
|
+
it('converts minutes to seconds', () => {
|
|
7
|
+
expect(minToSec(0)).toBe(0);
|
|
8
|
+
expect(minToSec(540)).toBe(32400);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('converts seconds to minutes', () => {
|
|
12
|
+
expect(secToMin(0)).toBe(0);
|
|
13
|
+
expect(secToMin(32400)).toBe(540);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('round-trips a time of day', () => {
|
|
17
|
+
expect(secToMin(minToSec(1439))).toBe(1439);
|
|
18
|
+
});
|
|
19
|
+
});
|
package/src/scripts/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import convertDuration from './convertDuration/convertDuration';
|
|
2
|
+
import { minToSec, secToMin } from './convertTimeUnits/convertTimeUnits';
|
|
2
3
|
import downloadFile from './downloadFile/downloadFile';
|
|
3
4
|
import { FileFormat } from './downloadFile/types/fileFormat.types';
|
|
4
5
|
import {
|
|
@@ -18,6 +19,8 @@ export {
|
|
|
18
19
|
downloadFile,
|
|
19
20
|
FileFormat,
|
|
20
21
|
isRelativeDatetimeValue,
|
|
22
|
+
minToSec,
|
|
21
23
|
normalizeDatetimeRange,
|
|
22
24
|
normalizeToTimestamp,
|
|
25
|
+
secToMin,
|
|
23
26
|
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { i18nIssue } from './i18nIssue';
|
|
4
|
+
|
|
5
|
+
export const dayMinuteSchema = z
|
|
6
|
+
.number()
|
|
7
|
+
.int()
|
|
8
|
+
.refine((value) => value >= 0 && value < 24 * 60, i18nIssue('hourRange'));
|
|
9
|
+
|
|
10
|
+
export type TimeRange = {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type TimeRangeError = {
|
|
16
|
+
index: number;
|
|
17
|
+
prop: string;
|
|
18
|
+
key: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const RANGE_PROPS = [
|
|
22
|
+
'start',
|
|
23
|
+
'end',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
const addRangeIssue = (
|
|
27
|
+
ctx: z.RefinementCtx,
|
|
28
|
+
key: string,
|
|
29
|
+
...path: number[]
|
|
30
|
+
) => {
|
|
31
|
+
RANGE_PROPS.forEach((prop) => {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: 'custom',
|
|
34
|
+
path: [
|
|
35
|
+
...path,
|
|
36
|
+
prop,
|
|
37
|
+
],
|
|
38
|
+
...i18nIssue(key),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const refineTimeRangeStartLessThanEnd = (
|
|
44
|
+
item: TimeRange,
|
|
45
|
+
ctx: z.RefinementCtx,
|
|
46
|
+
) => {
|
|
47
|
+
if (item.start >= item.end) addRangeIssue(ctx, 'timerangeStartLessThanEnd');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const timeRangeSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
start: dayMinuteSchema,
|
|
53
|
+
end: dayMinuteSchema,
|
|
54
|
+
})
|
|
55
|
+
.superRefine(refineTimeRangeStartLessThanEnd);
|
|
56
|
+
|
|
57
|
+
const isIntersecting = (a: TimeRange, b: TimeRange) =>
|
|
58
|
+
a.start <= b.end && b.start <= a.end;
|
|
59
|
+
|
|
60
|
+
export const refineTimeRangesNotIntersect =
|
|
61
|
+
<T extends TimeRange>(groupBy?: (item: T) => unknown) =>
|
|
62
|
+
(items: T[], ctx: z.RefinementCtx) => {
|
|
63
|
+
items.forEach((item, index) => {
|
|
64
|
+
const hasIntersection = items.some(
|
|
65
|
+
(other, otherIndex) =>
|
|
66
|
+
otherIndex !== index &&
|
|
67
|
+
groupBy?.(item) === groupBy?.(other) &&
|
|
68
|
+
isIntersecting(item, other),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (hasIntersection) addRangeIssue(ctx, 'timerangeNotIntersect', index);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const getTimeRangeErrors = (
|
|
76
|
+
schema: z.ZodType,
|
|
77
|
+
items: unknown,
|
|
78
|
+
): TimeRangeError[] => {
|
|
79
|
+
const result = schema.safeParse(items);
|
|
80
|
+
|
|
81
|
+
if (result.success) return [];
|
|
82
|
+
|
|
83
|
+
return result.error.issues.flatMap((issue) => {
|
|
84
|
+
const [index, prop] = issue.path;
|
|
85
|
+
const key = issue.code === 'custom' ? issue.params?.i18nKey : undefined;
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
typeof index !== 'number' ||
|
|
89
|
+
typeof prop !== 'string' ||
|
|
90
|
+
typeof key !== 'string'
|
|
91
|
+
) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
index,
|
|
98
|
+
prop,
|
|
99
|
+
key,
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
});
|
|
103
|
+
};
|
|
@@ -4,7 +4,7 @@ import type { z } from 'zod';
|
|
|
4
4
|
import * as validationsIndex from '../../index';
|
|
5
5
|
import {
|
|
6
6
|
calendarSchema,
|
|
7
|
-
|
|
7
|
+
getCalendarDayRangeErrors,
|
|
8
8
|
} from '../calendar.validations';
|
|
9
9
|
|
|
10
10
|
/** custom rules carry their message key in params, see `i18nIssue` */
|
|
@@ -668,7 +668,7 @@ describe('validations barrel export', () => {
|
|
|
668
668
|
});
|
|
669
669
|
});
|
|
670
670
|
|
|
671
|
-
describe('
|
|
671
|
+
describe('getCalendarDayRangeErrors', () => {
|
|
672
672
|
const row = (day: number, start: number, end: number) => ({
|
|
673
673
|
day,
|
|
674
674
|
disabled: false,
|
|
@@ -678,7 +678,7 @@ describe('getCalendarDayRangeIssues', () => {
|
|
|
678
678
|
|
|
679
679
|
it('reports nothing for valid rows', () => {
|
|
680
680
|
expect(
|
|
681
|
-
|
|
681
|
+
getCalendarDayRangeErrors([
|
|
682
682
|
row(0, 540, 541),
|
|
683
683
|
row(0, 600, 660),
|
|
684
684
|
row(1, 540, 1200),
|
|
@@ -688,7 +688,7 @@ describe('getCalendarDayRangeIssues', () => {
|
|
|
688
688
|
|
|
689
689
|
it('reports both ends of a row whose start is not before its end', () => {
|
|
690
690
|
expect(
|
|
691
|
-
|
|
691
|
+
getCalendarDayRangeErrors([
|
|
692
692
|
row(0, 600, 540),
|
|
693
693
|
]),
|
|
694
694
|
).toEqual([
|
|
@@ -706,15 +706,15 @@ describe('getCalendarDayRangeIssues', () => {
|
|
|
706
706
|
});
|
|
707
707
|
|
|
708
708
|
it('reports every row of an overlap on the same day', () => {
|
|
709
|
-
const
|
|
709
|
+
const errors = getCalendarDayRangeErrors([
|
|
710
710
|
row(0, 540, 720),
|
|
711
711
|
row(0, 600, 780),
|
|
712
712
|
]);
|
|
713
713
|
|
|
714
|
-
expect(
|
|
714
|
+
expect(errors.every(({ key }) => key === 'timerangeNotIntersect')).toBe(
|
|
715
715
|
true,
|
|
716
716
|
);
|
|
717
|
-
expect(
|
|
717
|
+
expect(errors.map(({ index, prop }) => `${index}.${prop}`).sort()).toEqual([
|
|
718
718
|
'0.end',
|
|
719
719
|
'0.start',
|
|
720
720
|
'1.end',
|
|
@@ -724,7 +724,7 @@ describe('getCalendarDayRangeIssues', () => {
|
|
|
724
724
|
|
|
725
725
|
it('does not treat the same hours on different days as an overlap', () => {
|
|
726
726
|
expect(
|
|
727
|
-
|
|
727
|
+
getCalendarDayRangeErrors([
|
|
728
728
|
row(0, 540, 720),
|
|
729
729
|
row(2, 540, 720),
|
|
730
730
|
]),
|
|
@@ -733,7 +733,7 @@ describe('getCalendarDayRangeIssues', () => {
|
|
|
733
733
|
|
|
734
734
|
it('reports a minute outside the day', () => {
|
|
735
735
|
expect(
|
|
736
|
-
|
|
736
|
+
getCalendarDayRangeErrors([
|
|
737
737
|
row(0, 0, 24 * 60),
|
|
738
738
|
]),
|
|
739
739
|
).toEqual([
|
|
@@ -1,67 +1,14 @@
|
|
|
1
1
|
import type { EngineCalendar } from '@webitel/api-services/gen/models';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { i18nIssue } from '../_shared/i18nIssue';
|
|
4
3
|
import { requiredLookupSchema } from '../_shared/lookup.validations';
|
|
4
|
+
import {
|
|
5
|
+
dayMinuteSchema,
|
|
6
|
+
getTimeRangeErrors,
|
|
7
|
+
refineTimeRangeStartLessThanEnd,
|
|
8
|
+
refineTimeRangesNotIntersect,
|
|
9
|
+
} from '../_shared/timeRange.validations';
|
|
5
10
|
import type { ZodShape } from '../types';
|
|
6
11
|
|
|
7
|
-
const HOUR_RANGE_KEY = 'hourRange';
|
|
8
|
-
const TIMERANGE_START_LESS_THAN_END_KEY = 'timerangeStartLessThanEnd';
|
|
9
|
-
const TIMERANGE_NOT_INTERSECT_KEY = 'timerangeNotIntersect';
|
|
10
|
-
|
|
11
|
-
/** Legacy hourRange: value >= 0 && value < 1440 */
|
|
12
|
-
const dayMinuteSchema = z
|
|
13
|
-
.number()
|
|
14
|
-
.int()
|
|
15
|
-
.refine((value) => value >= 0 && value < 24 * 60, i18nIssue(HOUR_RANGE_KEY));
|
|
16
|
-
|
|
17
|
-
type AcceptOfDayUi = {
|
|
18
|
-
day: number;
|
|
19
|
-
start: number;
|
|
20
|
-
end: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const getIntersectingIndices = (items: AcceptOfDayUi[]) => {
|
|
24
|
-
const indices = new Set<number>();
|
|
25
|
-
const indicesByDay = new Map<number, number[]>();
|
|
26
|
-
|
|
27
|
-
items.forEach((item, index) => {
|
|
28
|
-
const dayIndices = indicesByDay.get(item.day) ?? [];
|
|
29
|
-
dayIndices.push(index);
|
|
30
|
-
indicesByDay.set(item.day, dayIndices);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
indicesByDay.forEach((dayIndices) => {
|
|
34
|
-
const ranges: Array<{
|
|
35
|
-
start: number;
|
|
36
|
-
end: number;
|
|
37
|
-
index: number;
|
|
38
|
-
}> = [];
|
|
39
|
-
|
|
40
|
-
dayIndices.forEach((index) => {
|
|
41
|
-
const current = items[index];
|
|
42
|
-
|
|
43
|
-
ranges.forEach((range) => {
|
|
44
|
-
if (
|
|
45
|
-
(current.start >= range.start && current.end <= range.end) ||
|
|
46
|
-
(current.start <= range.start && current.end >= range.start) ||
|
|
47
|
-
(current.start <= range.end && current.end >= range.end)
|
|
48
|
-
) {
|
|
49
|
-
indices.add(index);
|
|
50
|
-
indices.add(range.index);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
ranges.push({
|
|
55
|
-
start: current.start,
|
|
56
|
-
end: current.end,
|
|
57
|
-
index,
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return indices;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
12
|
/** UI shape: minutes as `start`/`end` (API maps to startTimeOfDay/endTimeOfDay). */
|
|
66
13
|
const acceptOfDayUiSchema = z
|
|
67
14
|
.object({
|
|
@@ -70,54 +17,11 @@ const acceptOfDayUiSchema = z
|
|
|
70
17
|
start: dayMinuteSchema,
|
|
71
18
|
end: dayMinuteSchema,
|
|
72
19
|
})
|
|
73
|
-
.superRefine(
|
|
74
|
-
if (item.start >= item.end) {
|
|
75
|
-
ctx.addIssue({
|
|
76
|
-
code: 'custom',
|
|
77
|
-
path: [
|
|
78
|
-
'start',
|
|
79
|
-
],
|
|
80
|
-
...i18nIssue(TIMERANGE_START_LESS_THAN_END_KEY),
|
|
81
|
-
});
|
|
82
|
-
ctx.addIssue({
|
|
83
|
-
code: 'custom',
|
|
84
|
-
path: [
|
|
85
|
-
'end',
|
|
86
|
-
],
|
|
87
|
-
...i18nIssue(TIMERANGE_START_LESS_THAN_END_KEY),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
});
|
|
20
|
+
.superRefine(refineTimeRangeStartLessThanEnd);
|
|
91
21
|
|
|
92
22
|
const acceptsOfDayUiArraySchema = z
|
|
93
23
|
.array(acceptOfDayUiSchema)
|
|
94
|
-
.superRefine((
|
|
95
|
-
getIntersectingIndices(items).forEach((index) => {
|
|
96
|
-
ctx.addIssue({
|
|
97
|
-
code: 'custom',
|
|
98
|
-
path: [
|
|
99
|
-
index,
|
|
100
|
-
'start',
|
|
101
|
-
],
|
|
102
|
-
...i18nIssue(TIMERANGE_NOT_INTERSECT_KEY),
|
|
103
|
-
});
|
|
104
|
-
ctx.addIssue({
|
|
105
|
-
code: 'custom',
|
|
106
|
-
path: [
|
|
107
|
-
index,
|
|
108
|
-
'end',
|
|
109
|
-
],
|
|
110
|
-
...i18nIssue(TIMERANGE_NOT_INTERSECT_KEY),
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
export type CalendarDayRangeIssue = {
|
|
116
|
-
index: number;
|
|
117
|
-
prop: string;
|
|
118
|
-
/** message key, resolve as `validation.<key>` */
|
|
119
|
-
key: string;
|
|
120
|
-
};
|
|
24
|
+
.superRefine(refineTimeRangesNotIntersect((item) => item.day));
|
|
121
25
|
|
|
122
26
|
/**
|
|
123
27
|
* The same rules as the card schema, reported per row so a form can mark the
|
|
@@ -127,33 +31,8 @@ export type CalendarDayRangeIssue = {
|
|
|
127
31
|
* verdicts (overlapping ranges) are only re-derived by a full `$validate()`,
|
|
128
32
|
* so a row the user has not touched keeps a message that is no longer true.
|
|
129
33
|
*/
|
|
130
|
-
export const
|
|
131
|
-
items
|
|
132
|
-
): CalendarDayRangeIssue[] => {
|
|
133
|
-
const result = acceptsOfDayUiArraySchema.safeParse(items);
|
|
134
|
-
|
|
135
|
-
if (result.success) return [];
|
|
136
|
-
|
|
137
|
-
return result.error.issues.flatMap((issue) => {
|
|
138
|
-
const [index, prop] = issue.path;
|
|
139
|
-
const key =
|
|
140
|
-
issue.code === 'custom' && typeof issue.params?.i18nKey === 'string'
|
|
141
|
-
? issue.params.i18nKey
|
|
142
|
-
: undefined;
|
|
143
|
-
|
|
144
|
-
if (typeof index !== 'number' || typeof prop !== 'string' || !key) {
|
|
145
|
-
return [];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return [
|
|
149
|
-
{
|
|
150
|
-
index,
|
|
151
|
-
prop,
|
|
152
|
-
key,
|
|
153
|
-
},
|
|
154
|
-
];
|
|
155
|
-
});
|
|
156
|
-
};
|
|
34
|
+
export const getCalendarDayRangeErrors = (items: unknown) =>
|
|
35
|
+
getTimeRangeErrors(acceptsOfDayUiArraySchema, items);
|
|
157
36
|
|
|
158
37
|
export const calendarExceptSchema = z.object({
|
|
159
38
|
name: z.string().min(1),
|
package/src/validations/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './_shared/duration.validations';
|
|
|
3
3
|
export * from './_shared/i18nIssue';
|
|
4
4
|
export * from './_shared/lookup.validations';
|
|
5
5
|
export * from './_shared/phoneNumber.validations';
|
|
6
|
+
export * from './_shared/timeRange.validations';
|
|
6
7
|
export * from './_shared/variablePair.validations';
|
|
7
8
|
export * from './adjunctType/adjunctType.validations';
|
|
8
9
|
export * from './agent/agent.validations';
|
|
@@ -44,6 +45,8 @@ export * from './queueMember/memberCommunication.validations';
|
|
|
44
45
|
export * from './queueMember/queueMember.validations';
|
|
45
46
|
export * from './queueResGroup/queueResGroup.validations';
|
|
46
47
|
export * from './queueSkill/queueSkill.validations';
|
|
48
|
+
export * from './resourceGroup/resourceGroup.validations';
|
|
49
|
+
export * from './resourceInGroup/resourceInGroup.validations';
|
|
47
50
|
export * from './sla/sla.validations';
|
|
48
51
|
export * from './slaCondition/slaCondition.validations';
|
|
49
52
|
export * from './team/team.validations';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { getDefaultsFromZodSchema } from '@webitel/api-services/gen/utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getResourceGroupTimeRangeErrors,
|
|
7
|
+
resourceGroupSchema,
|
|
8
|
+
} from '../resourceGroup.validations';
|
|
9
|
+
|
|
10
|
+
const issueKey = (issue: z.core.$ZodIssue) =>
|
|
11
|
+
issue.code === 'custom' ? issue.params?.i18nKey : undefined;
|
|
12
|
+
|
|
13
|
+
const communication = {
|
|
14
|
+
id: '1',
|
|
15
|
+
name: 'Phone',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const validInput = {
|
|
19
|
+
name: 'Group',
|
|
20
|
+
communication,
|
|
21
|
+
time: [
|
|
22
|
+
{
|
|
23
|
+
start: 540,
|
|
24
|
+
end: 1200,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('resourceGroupSchema', () => {
|
|
30
|
+
it('seeds a new group with one 9:00-20:00 range', () => {
|
|
31
|
+
expect(getDefaultsFromZodSchema(resourceGroupSchema, {})).toMatchObject({
|
|
32
|
+
description: '',
|
|
33
|
+
time: [
|
|
34
|
+
{
|
|
35
|
+
start: 540,
|
|
36
|
+
end: 1200,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts a filled group', () => {
|
|
43
|
+
expect(resourceGroupSchema.safeParse(validInput).success).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('requires name and communication', () => {
|
|
47
|
+
const result = resourceGroupSchema.safeParse({
|
|
48
|
+
...validInput,
|
|
49
|
+
name: '',
|
|
50
|
+
communication: {},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const paths = result.error?.issues.map((issue) => issue.path.join('.'));
|
|
54
|
+
expect(paths).toContain('name');
|
|
55
|
+
expect(paths).toContain('communication');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('requires at least one time range', () => {
|
|
59
|
+
const result = resourceGroupSchema.safeParse({
|
|
60
|
+
...validInput,
|
|
61
|
+
time: [],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects a range that starts after it ends', () => {
|
|
68
|
+
const result = resourceGroupSchema.safeParse({
|
|
69
|
+
...validInput,
|
|
70
|
+
time: [
|
|
71
|
+
{
|
|
72
|
+
start: 600,
|
|
73
|
+
end: 540,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.error?.issues.map(issueKey)).toContain(
|
|
79
|
+
'timerangeStartLessThanEnd',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects intersecting ranges', () => {
|
|
84
|
+
const result = resourceGroupSchema.safeParse({
|
|
85
|
+
...validInput,
|
|
86
|
+
time: [
|
|
87
|
+
{
|
|
88
|
+
start: 540,
|
|
89
|
+
end: 720,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
start: 600,
|
|
93
|
+
end: 800,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.error?.issues.map(issueKey)).toContain(
|
|
99
|
+
'timerangeNotIntersect',
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getResourceGroupTimeRangeErrors', () => {
|
|
105
|
+
it('returns nothing for valid ranges', () => {
|
|
106
|
+
expect(
|
|
107
|
+
getResourceGroupTimeRangeErrors([
|
|
108
|
+
{
|
|
109
|
+
start: 540,
|
|
110
|
+
end: 600,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
start: 700,
|
|
114
|
+
end: 800,
|
|
115
|
+
},
|
|
116
|
+
]),
|
|
117
|
+
).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('reports a reversed range on both its ends', () => {
|
|
121
|
+
expect(
|
|
122
|
+
getResourceGroupTimeRangeErrors([
|
|
123
|
+
{
|
|
124
|
+
start: 600,
|
|
125
|
+
end: 540,
|
|
126
|
+
},
|
|
127
|
+
]),
|
|
128
|
+
).toEqual([
|
|
129
|
+
{
|
|
130
|
+
index: 0,
|
|
131
|
+
prop: 'start',
|
|
132
|
+
key: 'timerangeStartLessThanEnd',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
index: 0,
|
|
136
|
+
prop: 'end',
|
|
137
|
+
key: 'timerangeStartLessThanEnd',
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('reports both ends of every intersecting range', () => {
|
|
143
|
+
const errors = getResourceGroupTimeRangeErrors([
|
|
144
|
+
{
|
|
145
|
+
start: 540,
|
|
146
|
+
end: 720,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
start: 1000,
|
|
150
|
+
end: 1100,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
start: 600,
|
|
154
|
+
end: 800,
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
expect(errors).toEqual(
|
|
159
|
+
expect.arrayContaining([
|
|
160
|
+
{
|
|
161
|
+
index: 0,
|
|
162
|
+
prop: 'start',
|
|
163
|
+
key: 'timerangeNotIntersect',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
index: 2,
|
|
167
|
+
prop: 'end',
|
|
168
|
+
key: 'timerangeNotIntersect',
|
|
169
|
+
},
|
|
170
|
+
]),
|
|
171
|
+
);
|
|
172
|
+
expect(errors.some(({ index }) => index === 1)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('reports a minute outside the day', () => {
|
|
176
|
+
expect(
|
|
177
|
+
getResourceGroupTimeRangeErrors([
|
|
178
|
+
{
|
|
179
|
+
start: 0,
|
|
180
|
+
end: 1440,
|
|
181
|
+
},
|
|
182
|
+
]),
|
|
183
|
+
).toEqual([
|
|
184
|
+
{
|
|
185
|
+
index: 0,
|
|
186
|
+
prop: 'end',
|
|
187
|
+
key: 'hourRange',
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EngineOutboundResourceGroup } from '@webitel/api-services/gen/models';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { filledLookupSchema } from '../_shared/lookup.validations';
|
|
4
|
+
import {
|
|
5
|
+
getTimeRangeErrors,
|
|
6
|
+
refineTimeRangesNotIntersect,
|
|
7
|
+
type TimeRange,
|
|
8
|
+
timeRangeSchema,
|
|
9
|
+
} from '../_shared/timeRange.validations';
|
|
10
|
+
import type { ZodShape } from '../types';
|
|
11
|
+
|
|
12
|
+
const timeRangesUiArraySchema = z
|
|
13
|
+
.array(timeRangeSchema)
|
|
14
|
+
.min(1)
|
|
15
|
+
.superRefine(refineTimeRangesNotIntersect());
|
|
16
|
+
|
|
17
|
+
export const getResourceGroupTimeRangeErrors = (items: unknown) =>
|
|
18
|
+
getTimeRangeErrors(timeRangesUiArraySchema, items);
|
|
19
|
+
|
|
20
|
+
export const getDefaultResourceGroupTimeRange = (): TimeRange => ({
|
|
21
|
+
start: 9 * 60,
|
|
22
|
+
end: 20 * 60,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const resourceGroupSchema = z.object<
|
|
26
|
+
ZodShape<EngineOutboundResourceGroup> & {
|
|
27
|
+
time?: z.ZodType;
|
|
28
|
+
}
|
|
29
|
+
>({
|
|
30
|
+
name: z.string().min(1),
|
|
31
|
+
communication: filledLookupSchema,
|
|
32
|
+
description: z.string().optional().default(''),
|
|
33
|
+
time: timeRangesUiArraySchema.default(() => [
|
|
34
|
+
getDefaultResourceGroupTimeRange(),
|
|
35
|
+
]),
|
|
36
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getDefaultsFromZodSchema } from '@webitel/api-services/gen/utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { resourceInGroupSchema } from '../resourceInGroup.validations';
|
|
5
|
+
|
|
6
|
+
const resource = {
|
|
7
|
+
id: '1',
|
|
8
|
+
name: 'Gateway',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('resourceInGroupSchema', () => {
|
|
12
|
+
it('seeds a new resource with priority 0', () => {
|
|
13
|
+
expect(getDefaultsFromZodSchema(resourceInGroupSchema, {})).toMatchObject({
|
|
14
|
+
priority: 0,
|
|
15
|
+
reserveResource: {},
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('requires a resource', () => {
|
|
20
|
+
const result = resourceInGroupSchema.safeParse({
|
|
21
|
+
resource: {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.error?.issues.map((issue) => issue.path.join('.'))).toContain(
|
|
25
|
+
'resource',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('lets priority and reserve resource be cleared', () => {
|
|
30
|
+
expect(
|
|
31
|
+
resourceInGroupSchema.safeParse({
|
|
32
|
+
resource,
|
|
33
|
+
priority: null,
|
|
34
|
+
reserveResource: null,
|
|
35
|
+
}).success,
|
|
36
|
+
).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EngineOutboundResourceInGroup } from '@webitel/api-services/gen/models';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { clearableNumberSchema } from '../_shared/clearableNumber.validations';
|
|
4
|
+
import {
|
|
5
|
+
filledLookupSchema,
|
|
6
|
+
flexibleLookupSchema,
|
|
7
|
+
} from '../_shared/lookup.validations';
|
|
8
|
+
import type { ZodShape } from '../types';
|
|
9
|
+
|
|
10
|
+
export const resourceInGroupSchema = z.object<
|
|
11
|
+
ZodShape<EngineOutboundResourceInGroup>
|
|
12
|
+
>({
|
|
13
|
+
resource: filledLookupSchema,
|
|
14
|
+
reserveResource: flexibleLookupSchema.nullish().default({}),
|
|
15
|
+
priority: clearableNumberSchema.default(0),
|
|
16
|
+
});
|