@wasto/sdk 1.0.3
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/dist/index.d.mts +2562 -0
- package/dist/index.d.ts +2562 -0
- package/dist/index.js +2566 -0
- package/dist/index.mjs +2405 -0
- package/package.json +30 -0
- package/src/client.ts +692 -0
- package/src/dashboard-apps.ts +350 -0
- package/src/dto/cms-app-settings.ts +91 -0
- package/src/enums.ts +298 -0
- package/src/free-tier.ts +56 -0
- package/src/index.ts +35 -0
- package/src/opening-hours/constants.ts +48 -0
- package/src/opening-hours/convert.ts +276 -0
- package/src/opening-hours/format.ts +99 -0
- package/src/opening-hours/index.ts +9 -0
- package/src/opening-hours/is-open-now.ts +38 -0
- package/src/opening-hours/resolve.ts +71 -0
- package/src/opening-hours/time.ts +74 -0
- package/src/opening-hours/timezone.ts +34 -0
- package/src/opening-hours/types.ts +53 -0
- package/src/opening-hours/validate.ts +182 -0
- package/src/roi/calculations.ts +732 -0
- package/src/roi/index.ts +1 -0
- package/src/types/asset-bookings.types.ts +19 -0
- package/src/types/asset-interventions.types.ts +24 -0
- package/src/types/assets.types.ts +68 -0
- package/src/types/auth.types.ts +37 -0
- package/src/types/blog-posts.types.ts +19 -0
- package/src/types/calendar-pdf.types.ts +24 -0
- package/src/types/calendo-events.types.ts +64 -0
- package/src/types/challenges.types.ts +30 -0
- package/src/types/chatbot.types.ts +82 -0
- package/src/types/cities.types.ts +27 -0
- package/src/types/city-groups.types.ts +46 -0
- package/src/types/cms.types.ts +87 -0
- package/src/types/field-reports.types.ts +66 -0
- package/src/types/free-tier.types.ts +54 -0
- package/src/types/index.ts +32 -0
- package/src/types/iot.types.ts +31 -0
- package/src/types/maintenance.types.ts +44 -0
- package/src/types/mapso.types.ts +146 -0
- package/src/types/notifications.types.ts +68 -0
- package/src/types/onboarding.types.ts +117 -0
- package/src/types/operational-zones.types.ts +70 -0
- package/src/types/pagination.types.ts +33 -0
- package/src/types/providers.types.ts +29 -0
- package/src/types/quiz-questions.types.ts +59 -0
- package/src/types/recycling-points.types.ts +47 -0
- package/src/types/roi.types.ts +60 -0
- package/src/types/surveys.types.ts +103 -0
- package/src/types/tickets.types.ts +65 -0
- package/src/types/users.types.ts +30 -0
- package/src/types/waste-collection-places-query.types.ts +4 -0
- package/src/types/waste-collection-places.types.ts +33 -0
- package/src/types/waste-types.types.ts +21 -0
package/src/enums.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enums aligned with Prisma schema (no @prisma/client dependency).
|
|
3
|
+
* Use these in DTOs and types so the SDK can be used from browser/frontend.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum UserRole {
|
|
7
|
+
ADMIN = 'ADMIN',
|
|
8
|
+
GESTIONNAIRE_COLLECTIVITE = 'GESTIONNAIRE_COLLECTIVITE',
|
|
9
|
+
USER = 'USER',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum CityGroupType {
|
|
13
|
+
AGGLOMERATION = 'AGGLOMERATION',
|
|
14
|
+
COMMUNAUTE = 'COMMUNAUTE',
|
|
15
|
+
METROPOLE = 'METROPOLE',
|
|
16
|
+
SYNDICAT = 'SYNDICAT',
|
|
17
|
+
AUTRE = 'AUTRE',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export enum CityStatus {
|
|
21
|
+
NON_ACTIF = 'NON_ACTIF',
|
|
22
|
+
INCOMPLET = 'INCOMPLET',
|
|
23
|
+
ACTIF = 'ACTIF',
|
|
24
|
+
MAINTENANCE = 'MAINTENANCE',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export enum RecipientGroupType {
|
|
28
|
+
CITY = 'CITY',
|
|
29
|
+
CITY_GROUP = 'CITY_GROUP',
|
|
30
|
+
ZONE = 'ZONE',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export enum NotificationStatus {
|
|
34
|
+
DRAFT = 'DRAFT',
|
|
35
|
+
SCHEDULED = 'SCHEDULED',
|
|
36
|
+
SENT = 'SENT',
|
|
37
|
+
ARCHIVED = 'ARCHIVED',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export enum NotificationDeliveryStatus {
|
|
41
|
+
PENDING = 'PENDING',
|
|
42
|
+
SENT = 'SENT',
|
|
43
|
+
FAILED = 'FAILED',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export enum CmsSiteType {
|
|
47
|
+
SITE = 'SITE',
|
|
48
|
+
APP = 'APP',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export enum CmsMenuLinkType {
|
|
52
|
+
PAGE = 'PAGE',
|
|
53
|
+
EXTERNAL = 'EXTERNAL',
|
|
54
|
+
COMPONENT = 'COMPONENT',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export enum ChatMessageRole {
|
|
58
|
+
USER = 'USER',
|
|
59
|
+
ASSISTANT = 'ASSISTANT',
|
|
60
|
+
AGENT = 'AGENT',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export enum ChatStatus {
|
|
64
|
+
OPEN = 'OPEN',
|
|
65
|
+
WAITING_AGENT = 'WAITING_AGENT',
|
|
66
|
+
RESOLVED = 'RESOLVED',
|
|
67
|
+
CLOSED = 'CLOSED',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export enum PermissionAction {
|
|
71
|
+
READ = 'READ',
|
|
72
|
+
WRITE = 'WRITE',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export enum PermissionResource {
|
|
76
|
+
USERS = 'USERS',
|
|
77
|
+
CITIES = 'CITIES',
|
|
78
|
+
CITY_GROUPS = 'CITY_GROUPS',
|
|
79
|
+
BLOG_POSTS = 'BLOG_POSTS',
|
|
80
|
+
CHALLENGES = 'CHALLENGES',
|
|
81
|
+
SERIES = 'SERIES',
|
|
82
|
+
BADGES = 'BADGES',
|
|
83
|
+
NOTIFICATIONS = 'NOTIFICATIONS',
|
|
84
|
+
RECIPIENT_GROUPS = 'RECIPIENT_GROUPS',
|
|
85
|
+
WASTE_TYPES = 'WASTE_TYPES',
|
|
86
|
+
WASTE_COLLECTION_PLACES = 'WASTE_COLLECTION_PLACES',
|
|
87
|
+
RECYCLING_POINTS = 'RECYCLING_POINTS',
|
|
88
|
+
QUIZ_QUESTIONS = 'QUIZ_QUESTIONS',
|
|
89
|
+
CHAT_CONVERSATIONS = 'CHAT_CONVERSATIONS',
|
|
90
|
+
ANALYTICS = 'ANALYTICS',
|
|
91
|
+
ASSETS = 'ASSETS',
|
|
92
|
+
IOT_DEVICES = 'IOT_DEVICES',
|
|
93
|
+
SURVEYS = 'SURVEYS',
|
|
94
|
+
TICKETS = 'TICKETS',
|
|
95
|
+
TICKET_FORMS = 'TICKET_FORMS',
|
|
96
|
+
PROVIDERS = 'PROVIDERS',
|
|
97
|
+
CMS_SITES = 'CMS_SITES',
|
|
98
|
+
CMS_PAGES = 'CMS_PAGES',
|
|
99
|
+
CMS_MEDIA = 'CMS_MEDIA',
|
|
100
|
+
CALENDO_EVENTS = 'CALENDO_EVENTS',
|
|
101
|
+
MAPSO_EVENTS = 'MAPSO_EVENTS',
|
|
102
|
+
FIELD_REPORTS = 'FIELD_REPORTS',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export enum TerritoryActionType {
|
|
106
|
+
AWARENESS = 'AWARENESS',
|
|
107
|
+
CONTROLS = 'CONTROLS',
|
|
108
|
+
EQUIPMENT = 'EQUIPMENT',
|
|
109
|
+
WASTO_APP = 'WASTO_APP',
|
|
110
|
+
TRAINING = 'TRAINING',
|
|
111
|
+
OTHER = 'OTHER',
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export enum TerritoryActionStatus {
|
|
115
|
+
PLANNED = 'PLANNED',
|
|
116
|
+
IN_PROGRESS = 'IN_PROGRESS',
|
|
117
|
+
COMPLETED = 'COMPLETED',
|
|
118
|
+
CANCELLED = 'CANCELLED',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export enum AssetType {
|
|
122
|
+
BENNE = 'BENNE',
|
|
123
|
+
BORNE_VERRE = 'BORNE_VERRE',
|
|
124
|
+
BORNE_TEMPORAIRE = 'BORNE_TEMPORAIRE',
|
|
125
|
+
EPHEMERE = 'EPHEMERE',
|
|
126
|
+
LOCATION = 'LOCATION',
|
|
127
|
+
VEHICULE = 'VEHICULE',
|
|
128
|
+
CAMION = 'CAMION',
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export enum AssetStatus {
|
|
132
|
+
DISPONIBLE = 'DISPONIBLE',
|
|
133
|
+
EN_LOCATION = 'EN_LOCATION',
|
|
134
|
+
EN_MAINTENANCE = 'EN_MAINTENANCE',
|
|
135
|
+
HORS_SERVICE = 'HORS_SERVICE',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export enum BookingStatus {
|
|
139
|
+
PENDING = 'PENDING',
|
|
140
|
+
CONFIRMED = 'CONFIRMED',
|
|
141
|
+
CANCELLED = 'CANCELLED',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export enum SurveyStatus {
|
|
145
|
+
DRAFT = 'DRAFT',
|
|
146
|
+
ACTIVE = 'ACTIVE',
|
|
147
|
+
ARCHIVED = 'ARCHIVED',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export enum SurveyMode {
|
|
151
|
+
FIELD = 'FIELD',
|
|
152
|
+
ONLINE = 'ONLINE',
|
|
153
|
+
PHONE = 'PHONE',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export enum QuestionType {
|
|
157
|
+
TEXT = 'TEXT',
|
|
158
|
+
NUMBER = 'NUMBER',
|
|
159
|
+
CHOICE = 'CHOICE',
|
|
160
|
+
MULTIPLE_CHOICE = 'MULTIPLE_CHOICE',
|
|
161
|
+
RATING = 'RATING',
|
|
162
|
+
DATE = 'DATE',
|
|
163
|
+
LOCATION = 'LOCATION',
|
|
164
|
+
PHOTO = 'PHOTO',
|
|
165
|
+
YES_NO = 'YES_NO',
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export enum ResponseStatus {
|
|
169
|
+
DRAFT = 'DRAFT',
|
|
170
|
+
SUBMITTED = 'SUBMITTED',
|
|
171
|
+
SYNCED = 'SYNCED',
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export enum TicketFormStatus {
|
|
175
|
+
DRAFT = 'DRAFT',
|
|
176
|
+
ACTIVE = 'ACTIVE',
|
|
177
|
+
ARCHIVED = 'ARCHIVED',
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export enum TicketStatus {
|
|
181
|
+
OUVERT = 'OUVERT',
|
|
182
|
+
EN_COURS = 'EN_COURS',
|
|
183
|
+
RESOLU = 'RESOLU',
|
|
184
|
+
FERME = 'FERME',
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export enum FieldReportFormStatus {
|
|
188
|
+
DRAFT = 'DRAFT',
|
|
189
|
+
ACTIVE = 'ACTIVE',
|
|
190
|
+
ARCHIVED = 'ARCHIVED',
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export enum FieldReportStatus {
|
|
194
|
+
OUVERT = 'OUVERT',
|
|
195
|
+
EN_COURS = 'EN_COURS',
|
|
196
|
+
RESOLU = 'RESOLU',
|
|
197
|
+
FERME = 'FERME',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export enum ProviderType {
|
|
201
|
+
LOCATION_ASSETS = 'LOCATION_ASSETS',
|
|
202
|
+
ANIMATION_CHALLENGE = 'ANIMATION_CHALLENGE',
|
|
203
|
+
MAINTENANCE = 'MAINTENANCE',
|
|
204
|
+
AUTRE = 'AUTRE',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export enum InterventionType {
|
|
208
|
+
MAINTENANCE = 'MAINTENANCE',
|
|
209
|
+
INDISPO = 'INDISPO',
|
|
210
|
+
CONTROLE = 'CONTROLE',
|
|
211
|
+
OTHER = 'OTHER',
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export enum InterventionStatus {
|
|
215
|
+
PLANNED = 'PLANNED',
|
|
216
|
+
IN_PROGRESS = 'IN_PROGRESS',
|
|
217
|
+
DONE = 'DONE',
|
|
218
|
+
CANCELLED = 'CANCELLED',
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export enum AssetEventType {
|
|
222
|
+
LOCATION_CHANGE = 'LOCATION_CHANGE',
|
|
223
|
+
STATUS_CHANGE = 'STATUS_CHANGE',
|
|
224
|
+
MAINTENANCE = 'MAINTENANCE',
|
|
225
|
+
INSPECTION = 'INSPECTION',
|
|
226
|
+
OTHER = 'OTHER',
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export enum CalendoEventType {
|
|
230
|
+
CONCERT = 'CONCERT',
|
|
231
|
+
ANIMATION = 'ANIMATION',
|
|
232
|
+
EXPOSITION = 'EXPOSITION',
|
|
233
|
+
OTHER = 'OTHER',
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export enum CalendoEventStatus {
|
|
237
|
+
DRAFT = 'DRAFT',
|
|
238
|
+
PUBLISHED = 'PUBLISHED',
|
|
239
|
+
CANCELLED = 'CANCELLED',
|
|
240
|
+
SOLD_OUT = 'SOLD_OUT',
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export enum CalendoTicketSaleStatus {
|
|
244
|
+
CONFIRMED = 'CONFIRMED',
|
|
245
|
+
CANCELLED = 'CANCELLED',
|
|
246
|
+
REFUNDED = 'REFUNDED',
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export enum MapEventType {
|
|
250
|
+
FAIR = 'FAIR',
|
|
251
|
+
MARKET = 'MARKET',
|
|
252
|
+
FESTIVAL = 'FESTIVAL',
|
|
253
|
+
PERMANENT = 'PERMANENT',
|
|
254
|
+
OTHER = 'OTHER',
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export enum MapEventStatus {
|
|
258
|
+
DRAFT = 'DRAFT',
|
|
259
|
+
PUBLISHED = 'PUBLISHED',
|
|
260
|
+
ARCHIVED = 'ARCHIVED',
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export enum MapPoiCategory {
|
|
264
|
+
ATTRACTION_STAND = 'ATTRACTION_STAND',
|
|
265
|
+
TOILET = 'TOILET',
|
|
266
|
+
SECURITY = 'SECURITY',
|
|
267
|
+
FOOD = 'FOOD',
|
|
268
|
+
WATER_FOUNTAIN = 'WATER_FOUNTAIN',
|
|
269
|
+
WASTO_RECYCLING = 'WASTO_RECYCLING',
|
|
270
|
+
WASTO_BIOWASTE = 'WASTO_BIOWASTE',
|
|
271
|
+
WASTO_RETURN = 'WASTO_RETURN',
|
|
272
|
+
TRAVAUX = 'TRAVAUX',
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export enum MapZoneType {
|
|
276
|
+
PARKING_PROVISOIRE = 'PARKING_PROVISOIRE',
|
|
277
|
+
ZONE_INTERDITE = 'ZONE_INTERDITE',
|
|
278
|
+
AUTRE = 'AUTRE',
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export enum MapPoiStatus {
|
|
282
|
+
ACTIVE = 'ACTIVE',
|
|
283
|
+
MAINTENANCE = 'MAINTENANCE',
|
|
284
|
+
CLOSED = 'CLOSED',
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export enum OperationalZoneType {
|
|
288
|
+
TRAVAUX = 'TRAVAUX',
|
|
289
|
+
PARKING_PROVISOIRE = 'PARKING_PROVISOIRE',
|
|
290
|
+
ZONE_INTERDITE = 'ZONE_INTERDITE',
|
|
291
|
+
AUTRE = 'AUTRE',
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export enum OperationalZoneStatus {
|
|
295
|
+
DRAFT = 'DRAFT',
|
|
296
|
+
ACTIVE = 'ACTIVE',
|
|
297
|
+
ARCHIVED = 'ARCHIVED',
|
|
298
|
+
}
|
package/src/free-tier.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { FreeTierFeature, FreeTierResource } from './types/free-tier.types';
|
|
2
|
+
|
|
3
|
+
/** Quotas numériques offre gratuite — valeurs ajustables sans changer la logique. */
|
|
4
|
+
export const FREE_TIER_LIMITS = {
|
|
5
|
+
cmsApps: 1,
|
|
6
|
+
cmsPages: 5,
|
|
7
|
+
blogPosts: 10,
|
|
8
|
+
cmsMedia: 50,
|
|
9
|
+
cmsMediaBytesMb: 50,
|
|
10
|
+
wasteTypes: 15,
|
|
11
|
+
collectionPlaces: 10,
|
|
12
|
+
printCalendarTemplates: 3,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/** Feature gates offre gratuite — `false` = bloqué. */
|
|
16
|
+
export const FREE_TIER_FEATURES: Record<FreeTierFeature, boolean> = {
|
|
17
|
+
challenges: false,
|
|
18
|
+
roi: false,
|
|
19
|
+
notifications: false,
|
|
20
|
+
recyclingPoints: false,
|
|
21
|
+
assets: false,
|
|
22
|
+
nativeStorePublication: false,
|
|
23
|
+
inviteMembers: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const FREE_TIER_RESOURCES: readonly FreeTierResource[] = [
|
|
27
|
+
'cmsApps',
|
|
28
|
+
'cmsPages',
|
|
29
|
+
'blogPosts',
|
|
30
|
+
'cmsMedia',
|
|
31
|
+
'wasteTypes',
|
|
32
|
+
'collectionPlaces',
|
|
33
|
+
'printCalendarTemplates',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export const FREE_TIER_FEATURE_KEYS: readonly FreeTierFeature[] = [
|
|
37
|
+
'challenges',
|
|
38
|
+
'roi',
|
|
39
|
+
'notifications',
|
|
40
|
+
'recyclingPoints',
|
|
41
|
+
'assets',
|
|
42
|
+
'nativeStorePublication',
|
|
43
|
+
'inviteMembers',
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
export function getFreeTierLimit(resource: FreeTierResource): number {
|
|
47
|
+
return FREE_TIER_LIMITS[resource];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getFreeTierMediaBytesLimit(): number {
|
|
51
|
+
return FREE_TIER_LIMITS.cmsMediaBytesMb * 1024 * 1024;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isFreeTierFeatureEnabled(feature: FreeTierFeature): boolean {
|
|
55
|
+
return FREE_TIER_FEATURES[feature] === true;
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Enums
|
|
2
|
+
export * from './enums';
|
|
3
|
+
export * from './dashboard-apps';
|
|
4
|
+
export * from './free-tier';
|
|
5
|
+
|
|
6
|
+
// Types (interfaces Request/Response, pagination, etc.) — no decorators; DTOs with validation live in API
|
|
7
|
+
export * from './types';
|
|
8
|
+
|
|
9
|
+
// ROI déchets — formules GEC / CEN / ROI (runtime)
|
|
10
|
+
export * from './roi';
|
|
11
|
+
|
|
12
|
+
// Horaires d'ouverture structurés (UTC)
|
|
13
|
+
export * from './opening-hours';
|
|
14
|
+
|
|
15
|
+
// CMS app settings (interfaces only)
|
|
16
|
+
export type {
|
|
17
|
+
AppSplashSettings,
|
|
18
|
+
AppAdaptiveIconSettings,
|
|
19
|
+
AppIconSettings,
|
|
20
|
+
AppThemeGradient,
|
|
21
|
+
AppThemeSettings,
|
|
22
|
+
AppLayoutsConfig,
|
|
23
|
+
AppTabConfig,
|
|
24
|
+
AppStackScreenConfig,
|
|
25
|
+
AppSettings,
|
|
26
|
+
} from './dto/cms-app-settings';
|
|
27
|
+
|
|
28
|
+
// HTTP client
|
|
29
|
+
export {
|
|
30
|
+
createWastoClient,
|
|
31
|
+
WastoApiError,
|
|
32
|
+
type WastoClientConfig,
|
|
33
|
+
type WastoClient,
|
|
34
|
+
type WastoRequestErrorContext,
|
|
35
|
+
} from './client';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Weekday } from './types';
|
|
2
|
+
|
|
3
|
+
/** Dates de référence (semaine du 2024-01-01, lundi) pour conversions TZ */
|
|
4
|
+
export const WEEKDAY_REF_DATES: Record<Weekday, string> = {
|
|
5
|
+
monday: '2024-01-01',
|
|
6
|
+
tuesday: '2024-01-02',
|
|
7
|
+
wednesday: '2024-01-03',
|
|
8
|
+
thursday: '2024-01-04',
|
|
9
|
+
friday: '2024-01-05',
|
|
10
|
+
saturday: '2024-01-06',
|
|
11
|
+
sunday: '2024-01-07',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const WEEKDAY_LABELS_FR: Record<Weekday, string> = {
|
|
15
|
+
monday: 'Lundi',
|
|
16
|
+
tuesday: 'Mardi',
|
|
17
|
+
wednesday: 'Mercredi',
|
|
18
|
+
thursday: 'Jeudi',
|
|
19
|
+
friday: 'Vendredi',
|
|
20
|
+
saturday: 'Samedi',
|
|
21
|
+
sunday: 'Dimanche',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const WEEKDAY_LABELS_FR_SHORT: Record<Weekday, string> = {
|
|
25
|
+
monday: 'Lun',
|
|
26
|
+
tuesday: 'Mar',
|
|
27
|
+
wednesday: 'Mer',
|
|
28
|
+
thursday: 'Jeu',
|
|
29
|
+
friday: 'Ven',
|
|
30
|
+
saturday: 'Sam',
|
|
31
|
+
sunday: 'Dim',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const WEEKDAY_INDEX: Record<Weekday, number> = {
|
|
35
|
+
monday: 0,
|
|
36
|
+
tuesday: 1,
|
|
37
|
+
wednesday: 2,
|
|
38
|
+
thursday: 3,
|
|
39
|
+
friday: 4,
|
|
40
|
+
saturday: 5,
|
|
41
|
+
sunday: 6,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const PRESET_SLOTS = {
|
|
45
|
+
morning: { open: '08:00', close: '12:00' },
|
|
46
|
+
afternoon: { open: '14:00', close: '18:00' },
|
|
47
|
+
fullDay: { open: '08:00', close: '18:00' },
|
|
48
|
+
} as const;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OpeningHours,
|
|
3
|
+
OpeningHoursException,
|
|
4
|
+
OpeningHoursRule,
|
|
5
|
+
TimeSlot,
|
|
6
|
+
Weekday,
|
|
7
|
+
} from './types';
|
|
8
|
+
import { WEEKDAYS } from './types';
|
|
9
|
+
import {
|
|
10
|
+
addDaysToIsoDate,
|
|
11
|
+
nextWeekday,
|
|
12
|
+
parseTimeMinutes,
|
|
13
|
+
refDateForWeekday,
|
|
14
|
+
sortSlots,
|
|
15
|
+
utcHhmmFromDate,
|
|
16
|
+
weekdayFromUtcDate,
|
|
17
|
+
} from './time';
|
|
18
|
+
import { localDateTimeToUtc, utcDateIso } from './timezone';
|
|
19
|
+
|
|
20
|
+
type LocalOpeningHoursInput = {
|
|
21
|
+
rules: OpeningHoursRule[];
|
|
22
|
+
exceptions?: OpeningHoursException[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function addSlotToMap(
|
|
26
|
+
map: Map<Weekday, TimeSlot[]>,
|
|
27
|
+
day: Weekday,
|
|
28
|
+
slot: TimeSlot,
|
|
29
|
+
): void {
|
|
30
|
+
const existing = map.get(day) ?? [];
|
|
31
|
+
existing.push(slot);
|
|
32
|
+
map.set(day, existing);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mergeSlots(slots: TimeSlot[]): TimeSlot[] {
|
|
36
|
+
const sorted = sortSlots(slots);
|
|
37
|
+
const merged: TimeSlot[] = [];
|
|
38
|
+
for (const slot of sorted) {
|
|
39
|
+
const last = merged[merged.length - 1];
|
|
40
|
+
if (last && last.close === slot.open) {
|
|
41
|
+
merged[merged.length - 1] = { open: last.open, close: slot.close };
|
|
42
|
+
} else {
|
|
43
|
+
merged.push({ ...slot });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return merged.filter((s) => {
|
|
47
|
+
const o = parseTimeMinutes(s.open);
|
|
48
|
+
const c = parseTimeMinutes(s.close);
|
|
49
|
+
return o != null && c != null && c > o;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mapToRules(map: Map<Weekday, TimeSlot[]>): OpeningHoursRule[] {
|
|
54
|
+
const rules: OpeningHoursRule[] = [];
|
|
55
|
+
for (const day of WEEKDAYS) {
|
|
56
|
+
const slots = map.get(day);
|
|
57
|
+
if (!slots?.length) continue;
|
|
58
|
+
rules.push({ days: [day], slots: mergeSlots(slots) });
|
|
59
|
+
}
|
|
60
|
+
return coalesceDayRules(rules);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function coalesceDayRules(rules: OpeningHoursRule[]): OpeningHoursRule[] {
|
|
64
|
+
const bySignature = new Map<string, Weekday[]>();
|
|
65
|
+
for (const rule of rules) {
|
|
66
|
+
const sig = JSON.stringify(rule.slots);
|
|
67
|
+
const days = bySignature.get(sig) ?? [];
|
|
68
|
+
days.push(...rule.days);
|
|
69
|
+
bySignature.set(sig, days);
|
|
70
|
+
}
|
|
71
|
+
const out: OpeningHoursRule[] = [];
|
|
72
|
+
for (const [sig, days] of bySignature) {
|
|
73
|
+
out.push({
|
|
74
|
+
days: sortWeekdays([...new Set(days)]),
|
|
75
|
+
slots: JSON.parse(sig) as TimeSlot[],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sortWeekdays(days: Weekday[]): Weekday[] {
|
|
82
|
+
const order = new Map(WEEKDAYS.map((d, i) => [d, i]));
|
|
83
|
+
return [...days].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function convertLocalSlotToUtcFixed(
|
|
87
|
+
map: Map<Weekday, TimeSlot[]>,
|
|
88
|
+
localDay: Weekday,
|
|
89
|
+
slot: TimeSlot,
|
|
90
|
+
timeZone: string,
|
|
91
|
+
): void {
|
|
92
|
+
const openMin = parseTimeMinutes(slot.open);
|
|
93
|
+
const closeMin = parseTimeMinutes(slot.close);
|
|
94
|
+
if (openMin == null || closeMin == null) return;
|
|
95
|
+
|
|
96
|
+
const openDate = refDateForWeekday(localDay);
|
|
97
|
+
let closeDate = openDate;
|
|
98
|
+
const closeTime = slot.close;
|
|
99
|
+
|
|
100
|
+
if (closeMin <= openMin) {
|
|
101
|
+
closeDate = addDaysToIsoDate(openDate, 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const openUtc = localDateTimeToUtc(openDate, slot.open, timeZone);
|
|
105
|
+
const closeUtc = localDateTimeToUtc(closeDate, closeTime, timeZone);
|
|
106
|
+
|
|
107
|
+
const openDay = weekdayFromUtcDate(openUtc);
|
|
108
|
+
const closeDay = weekdayFromUtcDate(closeUtc);
|
|
109
|
+
const openStr = utcHhmmFromDate(openUtc);
|
|
110
|
+
const closeStr = utcHhmmFromDate(closeUtc);
|
|
111
|
+
|
|
112
|
+
if (openDay === closeDay) {
|
|
113
|
+
if (openStr !== closeStr) {
|
|
114
|
+
addSlotToMap(map, openDay, { open: openStr, close: closeStr });
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addSlotToMap(map, openDay, { open: openStr, close: '23:59' });
|
|
120
|
+
if (closeStr !== '00:00') {
|
|
121
|
+
addSlotToMap(map, closeDay, { open: '00:00', close: closeStr });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function convertUtcSlotToLocal(
|
|
126
|
+
map: Map<Weekday, TimeSlot[]>,
|
|
127
|
+
utcDay: Weekday,
|
|
128
|
+
slot: TimeSlot,
|
|
129
|
+
timeZone: string,
|
|
130
|
+
): void {
|
|
131
|
+
const openMin = parseTimeMinutes(slot.open);
|
|
132
|
+
const closeMin = parseTimeMinutes(slot.close);
|
|
133
|
+
if (openMin == null || closeMin == null) return;
|
|
134
|
+
|
|
135
|
+
const openDate = refDateForWeekday(utcDay);
|
|
136
|
+
let closeDate = openDate;
|
|
137
|
+
const closeTime = slot.close;
|
|
138
|
+
|
|
139
|
+
if (closeMin <= openMin) {
|
|
140
|
+
closeDate = addDaysToIsoDate(openDate, 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const openUtc = new Date(`${openDate}T${slot.open}:00.000Z`);
|
|
144
|
+
const closeUtc = new Date(`${closeDate}T${closeTime}:00.000Z`);
|
|
145
|
+
|
|
146
|
+
const openLocal = formatZoned(openUtc, timeZone);
|
|
147
|
+
const closeLocal = formatZoned(closeUtc, timeZone);
|
|
148
|
+
|
|
149
|
+
if (openLocal.day === closeLocal.day) {
|
|
150
|
+
if (openLocal.time !== closeLocal.time) {
|
|
151
|
+
addSlotToMap(map, openLocal.day, { open: openLocal.time, close: closeLocal.time });
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
addSlotToMap(map, openLocal.day, { open: openLocal.time, close: '23:59' });
|
|
157
|
+
if (closeLocal.time !== '00:00') {
|
|
158
|
+
addSlotToMap(map, closeLocal.day, { open: '00:00', close: closeLocal.time });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatZoned(utc: Date, timeZone: string): { day: Weekday; time: string } {
|
|
163
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
164
|
+
timeZone,
|
|
165
|
+
weekday: 'long',
|
|
166
|
+
hour: '2-digit',
|
|
167
|
+
minute: '2-digit',
|
|
168
|
+
hour12: false,
|
|
169
|
+
}).formatToParts(utc);
|
|
170
|
+
const weekdayRaw = parts.find((p) => p.type === 'weekday')?.value?.toLowerCase();
|
|
171
|
+
const hour = parts.find((p) => p.type === 'hour')?.value ?? '00';
|
|
172
|
+
const minute = parts.find((p) => p.type === 'minute')?.value ?? '00';
|
|
173
|
+
const dayMap: Record<string, Weekday> = {
|
|
174
|
+
monday: 'monday',
|
|
175
|
+
tuesday: 'tuesday',
|
|
176
|
+
wednesday: 'wednesday',
|
|
177
|
+
thursday: 'thursday',
|
|
178
|
+
friday: 'friday',
|
|
179
|
+
saturday: 'saturday',
|
|
180
|
+
sunday: 'sunday',
|
|
181
|
+
};
|
|
182
|
+
const day = dayMap[weekdayRaw ?? 'monday'] ?? 'monday';
|
|
183
|
+
return { day, time: `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function convertLocalToUtc(
|
|
187
|
+
local: LocalOpeningHoursInput,
|
|
188
|
+
timeZone: string,
|
|
189
|
+
): OpeningHours {
|
|
190
|
+
const map = new Map<Weekday, TimeSlot[]>();
|
|
191
|
+
|
|
192
|
+
for (const rule of local.rules) {
|
|
193
|
+
for (const day of rule.days) {
|
|
194
|
+
for (const slot of rule.slots) {
|
|
195
|
+
convertLocalSlotToUtcFixed(map, day, slot, timeZone);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const exceptions = local.exceptions?.map((exc) => {
|
|
201
|
+
if (exc.closed) return exc;
|
|
202
|
+
const excMap = new Map<Weekday, TimeSlot[]>();
|
|
203
|
+
for (const slot of exc.slots) {
|
|
204
|
+
convertLocalSlotToUtcFixed(excMap, 'monday', slot, timeZone);
|
|
205
|
+
}
|
|
206
|
+
const merged = [...excMap.values()].flat();
|
|
207
|
+
return { ...exc, slots: merged.length ? mergeSlots(merged) : exc.slots };
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
version: 1,
|
|
212
|
+
storage: 'utc',
|
|
213
|
+
rules: mapToRules(map),
|
|
214
|
+
exceptions,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function convertUtcToLocal(
|
|
219
|
+
utc: OpeningHours,
|
|
220
|
+
timeZone: string,
|
|
221
|
+
): LocalOpeningHoursInput {
|
|
222
|
+
const map = new Map<Weekday, TimeSlot[]>();
|
|
223
|
+
|
|
224
|
+
for (const rule of utc.rules) {
|
|
225
|
+
for (const day of rule.days) {
|
|
226
|
+
for (const slot of rule.slots) {
|
|
227
|
+
convertUtcSlotToLocal(map, day, slot, timeZone);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const exceptions = utc.exceptions?.map((exc) => {
|
|
233
|
+
if (exc.closed) return exc;
|
|
234
|
+
const excMap = new Map<Weekday, TimeSlot[]>();
|
|
235
|
+
for (const slot of exc.slots) {
|
|
236
|
+
convertUtcSlotToLocal(excMap, 'monday', slot, timeZone);
|
|
237
|
+
}
|
|
238
|
+
const merged = [...excMap.values()].flat();
|
|
239
|
+
return { ...exc, slots: merged.length ? mergeSlots(merged) : exc.slots };
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
rules: mapToRules(map),
|
|
244
|
+
exceptions,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function localRulesFromRange(
|
|
249
|
+
fromDay: Weekday,
|
|
250
|
+
toDay: Weekday,
|
|
251
|
+
slots: TimeSlot[],
|
|
252
|
+
exceptDays: Weekday[] = [],
|
|
253
|
+
): OpeningHoursRule {
|
|
254
|
+
const fromIdx = WEEKDAYS.indexOf(fromDay);
|
|
255
|
+
const toIdx = WEEKDAYS.indexOf(toDay);
|
|
256
|
+
const except = new Set(exceptDays);
|
|
257
|
+
const days: Weekday[] = [];
|
|
258
|
+
if (fromIdx <= toIdx) {
|
|
259
|
+
for (let i = fromIdx; i <= toIdx; i++) {
|
|
260
|
+
const d = WEEKDAYS[i];
|
|
261
|
+
if (!except.has(d)) days.push(d);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
for (let i = fromIdx; i < 7; i++) {
|
|
265
|
+
const d = WEEKDAYS[i];
|
|
266
|
+
if (!except.has(d)) days.push(d);
|
|
267
|
+
}
|
|
268
|
+
for (let i = 0; i <= toIdx; i++) {
|
|
269
|
+
const d = WEEKDAYS[i];
|
|
270
|
+
if (!except.has(d)) days.push(d);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { days, slots: [...slots] };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { nextWeekday };
|