@webitel/api-services 0.1.61 → 0.1.63
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/api/clients/calendars/__tests__/calendars.spec.ts +795 -0
- package/src/api/clients/calendars/calendars.ts +89 -62
- package/src/validations/_shared/i18nIssue.ts +18 -0
- package/src/validations/calendar/__tests__/calendar.validations.spec.ts +612 -0
- package/src/validations/calendar/calendar.validations.ts +214 -0
- package/src/validations/index.ts +1 -0
- package/types/.tsbuildinfo +1 -1
- package/types/api/clients/calendars/calendars.d.ts +7 -0
- package/types/validations/_shared/i18nIssue.d.ts +18 -0
- package/types/validations/calendar/calendar.validations.d.ts +33 -0
- package/types/validations/index.d.ts +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { EngineCalendar } from '@webitel/api-services/gen/models';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { i18nIssue } from '../_shared/i18nIssue';
|
|
4
|
+
import { requiredLookupSchema } from '../_shared/lookup.validations';
|
|
5
|
+
import type { ZodShape } from '../types';
|
|
6
|
+
|
|
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
|
+
/** UI shape: minutes as `start`/`end` (API maps to startTimeOfDay/endTimeOfDay). */
|
|
66
|
+
const acceptOfDayUiSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
day: z.number().int().min(0).max(6),
|
|
69
|
+
disabled: z.boolean().default(false),
|
|
70
|
+
start: dayMinuteSchema,
|
|
71
|
+
end: dayMinuteSchema,
|
|
72
|
+
})
|
|
73
|
+
.superRefine((item, ctx) => {
|
|
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
|
+
});
|
|
91
|
+
|
|
92
|
+
const acceptsOfDayUiArraySchema = z
|
|
93
|
+
.array(acceptOfDayUiSchema)
|
|
94
|
+
.superRefine((items, ctx) => {
|
|
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
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The same rules as the card schema, reported per row so a form can mark the
|
|
124
|
+
* offending inputs while the user types.
|
|
125
|
+
*
|
|
126
|
+
* A regle field status cannot serve that: with a standard schema, cross-row
|
|
127
|
+
* verdicts (overlapping ranges) are only re-derived by a full `$validate()`,
|
|
128
|
+
* so a row the user has not touched keeps a message that is no longer true.
|
|
129
|
+
*/
|
|
130
|
+
export const getCalendarDayRangeIssues = (
|
|
131
|
+
items: unknown,
|
|
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
|
+
};
|
|
157
|
+
|
|
158
|
+
const exceptUiSchema = z.object({
|
|
159
|
+
name: z.string().optional(),
|
|
160
|
+
date: z
|
|
161
|
+
.union([
|
|
162
|
+
z.number(),
|
|
163
|
+
z.string(),
|
|
164
|
+
])
|
|
165
|
+
.optional(),
|
|
166
|
+
repeat: z.boolean().optional(),
|
|
167
|
+
working: z.boolean().optional(),
|
|
168
|
+
workStart: dayMinuteSchema.nullish(),
|
|
169
|
+
workStop: dayMinuteSchema.nullish(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const defaultAccepts = () =>
|
|
173
|
+
Array.from(
|
|
174
|
+
{
|
|
175
|
+
length: 7,
|
|
176
|
+
},
|
|
177
|
+
(_, day) => ({
|
|
178
|
+
day,
|
|
179
|
+
disabled: false,
|
|
180
|
+
start: 9 * 60,
|
|
181
|
+
end: 20 * 60,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const defaultSpecials = () =>
|
|
186
|
+
Array.from(
|
|
187
|
+
{
|
|
188
|
+
length: 7,
|
|
189
|
+
},
|
|
190
|
+
(_, day) => ({
|
|
191
|
+
day,
|
|
192
|
+
disabled: true,
|
|
193
|
+
start: 9 * 60,
|
|
194
|
+
end: 20 * 60,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
export const calendarSchema = z.object<
|
|
199
|
+
ZodShape<EngineCalendar> & {
|
|
200
|
+
expires?: z.ZodType;
|
|
201
|
+
accepts?: z.ZodType;
|
|
202
|
+
specials?: z.ZodType;
|
|
203
|
+
}
|
|
204
|
+
>({
|
|
205
|
+
name: z.string().min(1),
|
|
206
|
+
description: z.string().optional().default(''),
|
|
207
|
+
timezone: requiredLookupSchema,
|
|
208
|
+
startAt: z.any().optional(),
|
|
209
|
+
endAt: z.any().optional(),
|
|
210
|
+
expires: z.boolean().optional().default(false),
|
|
211
|
+
accepts: acceptsOfDayUiArraySchema.default(defaultAccepts),
|
|
212
|
+
specials: z.array(acceptOfDayUiSchema).default(defaultSpecials),
|
|
213
|
+
excepts: z.array(exceptUiSchema).optional().default([]),
|
|
214
|
+
});
|
package/src/validations/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './auditForm/auditForm.validations';
|
|
2
2
|
export * from './bucket/bucket.validations';
|
|
3
|
+
export * from './calendar/calendar.validations';
|
|
3
4
|
export * from './casePriority/casePriority.validations';
|
|
4
5
|
export * from './caseSource/caseSource.validations';
|
|
5
6
|
export * from './caseStatus/caseStatus.validations';
|