@wasto/sdk 1.0.3 → 1.0.4

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.
Files changed (56) hide show
  1. package/dist/index.d.mts +17 -1
  2. package/dist/index.d.ts +17 -1
  3. package/dist/index.js +7 -0
  4. package/dist/index.mjs +7 -0
  5. package/package.json +1 -1
  6. package/src/client.ts +701 -692
  7. package/src/dashboard-apps.ts +350 -350
  8. package/src/dto/cms-app-settings.ts +91 -91
  9. package/src/enums.ts +298 -298
  10. package/src/free-tier.ts +56 -56
  11. package/src/index.ts +35 -35
  12. package/src/opening-hours/constants.ts +48 -48
  13. package/src/opening-hours/convert.ts +276 -276
  14. package/src/opening-hours/format.ts +99 -99
  15. package/src/opening-hours/index.ts +9 -9
  16. package/src/opening-hours/is-open-now.ts +38 -38
  17. package/src/opening-hours/resolve.ts +71 -71
  18. package/src/opening-hours/time.ts +74 -74
  19. package/src/opening-hours/timezone.ts +34 -34
  20. package/src/opening-hours/types.ts +53 -53
  21. package/src/opening-hours/validate.ts +182 -182
  22. package/src/roi/calculations.ts +732 -732
  23. package/src/roi/index.ts +1 -1
  24. package/src/types/app-preload.types.ts +10 -0
  25. package/src/types/asset-bookings.types.ts +19 -19
  26. package/src/types/asset-interventions.types.ts +24 -24
  27. package/src/types/assets.types.ts +68 -68
  28. package/src/types/auth.types.ts +37 -37
  29. package/src/types/blog-posts.types.ts +19 -19
  30. package/src/types/calendar-pdf.types.ts +24 -24
  31. package/src/types/calendo-events.types.ts +64 -64
  32. package/src/types/challenges.types.ts +30 -30
  33. package/src/types/chatbot.types.ts +82 -82
  34. package/src/types/cities.types.ts +27 -27
  35. package/src/types/city-groups.types.ts +46 -46
  36. package/src/types/cms.types.ts +87 -87
  37. package/src/types/field-reports.types.ts +66 -66
  38. package/src/types/free-tier.types.ts +54 -54
  39. package/src/types/index.ts +33 -32
  40. package/src/types/iot.types.ts +31 -31
  41. package/src/types/maintenance.types.ts +44 -44
  42. package/src/types/mapso.types.ts +146 -146
  43. package/src/types/notifications.types.ts +68 -68
  44. package/src/types/onboarding.types.ts +117 -117
  45. package/src/types/operational-zones.types.ts +70 -70
  46. package/src/types/pagination.types.ts +33 -33
  47. package/src/types/providers.types.ts +29 -29
  48. package/src/types/quiz-questions.types.ts +59 -59
  49. package/src/types/recycling-points.types.ts +47 -47
  50. package/src/types/roi.types.ts +60 -60
  51. package/src/types/surveys.types.ts +103 -103
  52. package/src/types/tickets.types.ts +65 -65
  53. package/src/types/users.types.ts +30 -30
  54. package/src/types/waste-collection-places-query.types.ts +4 -4
  55. package/src/types/waste-collection-places.types.ts +33 -33
  56. package/src/types/waste-types.types.ts +21 -21
package/src/client.ts CHANGED
@@ -1,692 +1,701 @@
1
- import type {
2
- PaginatedResponse,
3
- PaginationMeta,
4
- } from './types/pagination.types';
5
- import type {
6
- SearchStreetSectorsQuery,
7
- SearchStreetSectorsResponse,
8
- } from './types/calendar-pdf.types';
9
-
10
- const DEFAULT_PAGE = 1;
11
- const DEFAULT_PAGE_SIZE = 20;
12
-
13
- export interface WastoClientConfig {
14
- baseUrl: string;
15
- getToken?: () => string | null;
16
- /** Headers added to every request (e.g. X-Wasto-Client). */
17
- defaultHeaders?: Record<string, string>;
18
- /**
19
- * Optional hook when a request fails (HTTP error or network).
20
- * Called before the error is rethrown. Host apps (e.g. via cms-components)
21
- * wire Sentry here — wasto-sdk stays vendor-agnostic.
22
- */
23
- onRequestError?: (error: Error, context: WastoRequestErrorContext) => void;
24
- }
25
-
26
- export type WastoRequestErrorContext = {
27
- method: string;
28
- url: string;
29
- path: string;
30
- status?: number;
31
- };
32
-
33
- export class WastoApiError extends Error {
34
- constructor(
35
- message: string,
36
- public status: number,
37
- public body?: unknown,
38
- ) {
39
- super(message);
40
- this.name = 'WastoApiError';
41
- }
42
- }
43
-
44
- function getHeaders(config: WastoClientConfig, init?: HeadersInit): HeadersInit {
45
- const token = config.getToken?.();
46
- const headers: Record<string, string> = {
47
- ...(config.defaultHeaders ?? {}),
48
- ...(init as Record<string, string>),
49
- };
50
- if (token) {
51
- headers['Authorization'] = `Bearer ${token}`;
52
- }
53
- const body = (init as { body?: unknown })?.body;
54
- if (!headers['Content-Type'] && !(typeof FormData !== 'undefined' && body instanceof FormData)) {
55
- headers['Content-Type'] = 'application/json';
56
- }
57
- return headers;
58
- }
59
-
60
- async function handleResponse<T>(res: Response): Promise<T> {
61
- if (!res.ok) {
62
- let message = `HTTP ${res.status}`;
63
- try {
64
- const text = await res.text();
65
- if (text) {
66
- try {
67
- const json = JSON.parse(text);
68
- message = json.message ?? json.error ?? text;
69
- } catch {
70
- message = text;
71
- }
72
- }
73
- } catch {
74
- // ignore
75
- }
76
- throw new WastoApiError(message, res.status, undefined);
77
- }
78
- const contentType = res.headers.get('content-type');
79
- if (contentType?.includes('application/json')) {
80
- return res.json() as Promise<T>;
81
- }
82
- return res.text() as Promise<T>;
83
- }
84
-
85
- export function createWastoClient(config: WastoClientConfig) {
86
- const base = config.baseUrl.replace(/\/$/, '');
87
-
88
- const notifyRequestError = (
89
- err: unknown,
90
- context: Omit<WastoRequestErrorContext, 'status'> & { status?: number },
91
- ) => {
92
- if (!config.onRequestError) return;
93
- const error = err instanceof Error ? err : new Error(String(err));
94
- const status =
95
- context.status ??
96
- (error instanceof WastoApiError ? error.status : undefined);
97
- try {
98
- config.onRequestError(error, {
99
- method: context.method,
100
- url: context.url,
101
- path: context.path,
102
- status,
103
- });
104
- } catch {
105
- // Never let the reporter break the request path.
106
- }
107
- };
108
-
109
- const request = async <T>(
110
- path: string,
111
- options: RequestInit = {},
112
- ): Promise<T> => {
113
- const url = path.startsWith('http') ? path : `${base}${path.startsWith('/') ? path : `/${path}`}`;
114
- const method = (options.method ?? 'GET').toUpperCase();
115
- try {
116
- const res = await fetch(url, {
117
- ...options,
118
- headers: getHeaders(config, options.headers),
119
- });
120
- return await handleResponse<T>(res);
121
- } catch (err) {
122
- notifyRequestError(err, { method, url, path });
123
- throw err;
124
- }
125
- };
126
-
127
- /** Public API: no Authorization header (for /public/* routes) */
128
- const requestPublic = async <T>(
129
- path: string,
130
- options: RequestInit = {},
131
- ): Promise<T> => {
132
- const url = path.startsWith('http') ? path : `${base}${path.startsWith('/') ? path : `/${path}`}`;
133
- const method = (options.method ?? 'GET').toUpperCase();
134
- const headers: Record<string, string> = {
135
- ...(options.headers as Record<string, string>),
136
- };
137
- const body = (options as { body?: unknown })?.body;
138
- // Keep behavior aligned with `request`/`getHeaders`: if a JSON body is provided,
139
- // ensure the API receives `Content-Type: application/json` so body parsing works.
140
- if (!headers['Content-Type'] && !(typeof FormData !== 'undefined' && body instanceof FormData)) {
141
- headers['Content-Type'] = 'application/json';
142
- }
143
- try {
144
- const res = await fetch(url, { ...options, headers });
145
- return await handleResponse<T>(res);
146
- } catch (err) {
147
- notifyRequestError(err, { method, url, path });
148
- throw err;
149
- }
150
- };
151
-
152
- return {
153
- request,
154
-
155
- auth: {
156
- login: (email: string, password: string) =>
157
- request<
158
- | { access_token: string; user?: unknown }
159
- | { requiresTwoFactor: true; pendingToken: string }
160
- >('/auth/login', {
161
- method: 'POST',
162
- body: JSON.stringify({ email, password }),
163
- }),
164
- register: (data: { email: string; password: string; firstName?: string; lastName?: string }) =>
165
- request<{ access_token?: string; user?: unknown }>('/auth/register', {
166
- method: 'POST',
167
- body: JSON.stringify(data),
168
- }),
169
- forgotPassword: (email: string) =>
170
- request<{ ok: boolean }>('/auth/forgot-password', {
171
- method: 'POST',
172
- body: JSON.stringify({ email }),
173
- }),
174
- resetPassword: (token: string, password: string) =>
175
- request<{ ok: boolean }>('/auth/reset-password', {
176
- method: 'POST',
177
- body: JSON.stringify({ token, password }),
178
- }),
179
- acceptInvite: (data: {
180
- token: string;
181
- password: string;
182
- firstName?: string;
183
- lastName?: string;
184
- }) =>
185
- request<{ access_token: string; user?: unknown }>('/auth/accept-invite', {
186
- method: 'POST',
187
- body: JSON.stringify(data),
188
- }),
189
- verify2fa: (pendingToken: string, code: string) =>
190
- request<{ access_token: string; user?: unknown }>('/auth/verify-2fa', {
191
- method: 'POST',
192
- body: JSON.stringify({ pendingToken, code }),
193
- }),
194
- },
195
-
196
- users: {
197
- findAll: (params?: { page?: number; pageSize?: number }) => {
198
- const q = new URLSearchParams();
199
- if (params?.page != null) q.set('page', String(params.page));
200
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
201
- const query = q.toString();
202
- return request<PaginatedResponse<unknown>>(`/users${query ? `?${query}` : ''}`);
203
- },
204
- getById: (id: string) => request<unknown>(`/users/${id}`),
205
- getMe: () => request<unknown>('/users/me'),
206
- create: (data: unknown) =>
207
- request<unknown>('/users', { method: 'POST', body: JSON.stringify(data) }),
208
- invite: (data: unknown) =>
209
- request<unknown>('/users/invite', { method: 'POST', body: JSON.stringify(data) }),
210
- update: (id: string, data: unknown) =>
211
- request<unknown>(`/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
212
- updateMe: (data: unknown) =>
213
- request<unknown>('/users/me', { method: 'PATCH', body: JSON.stringify(data) }),
214
- remove: (id: string) =>
215
- request<void>(`/users/${id}`, { method: 'DELETE' }),
216
- },
217
-
218
- assets: {
219
- findAll: (params?: { page?: number; pageSize?: number; cityId?: string; cityGroupId?: string; type?: string; status?: string }) => {
220
- const q = new URLSearchParams();
221
- if (params?.page != null) q.set('page', String(params.page));
222
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
223
- if (params?.cityId) q.set('cityId', params.cityId);
224
- if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
225
- if (params?.type) q.set('type', params.type);
226
- if (params?.status) q.set('status', params.status);
227
- const query = q.toString();
228
- return request<PaginatedResponse<unknown>>(`/assets${query ? `?${query}` : ''}`);
229
- },
230
- getMapLocations: (params?: { page?: number; pageSize?: number; cityId?: string; cityGroupId?: string; type?: string; status?: string }) => {
231
- const q = new URLSearchParams();
232
- if (params?.page != null) q.set('page', String(params.page));
233
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
234
- if (params?.cityId) q.set('cityId', params.cityId);
235
- if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
236
- if (params?.type) q.set('type', params.type);
237
- if (params?.status) q.set('status', params.status);
238
- const query = q.toString();
239
- return request<PaginatedResponse<unknown>>(`/assets/map/locations${query ? `?${query}` : ''}`);
240
- },
241
- getById: (id: string) => request<unknown>(`/assets/${id}`),
242
- create: (data: unknown) =>
243
- request<unknown>('/assets', { method: 'POST', body: JSON.stringify(data) }),
244
- update: (id: string, data: unknown) =>
245
- request<unknown>(`/assets/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
246
- remove: (id: string) =>
247
- request<void>(`/assets/${id}`, { method: 'DELETE' }),
248
- },
249
-
250
- cities: {
251
- findAll: (params?: { page?: number; pageSize?: number }) => {
252
- const q = new URLSearchParams();
253
- if (params?.page != null) q.set('page', String(params.page));
254
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
255
- const query = q.toString();
256
- return request<PaginatedResponse<unknown>>(`/cities${query ? `?${query}` : ''}`);
257
- },
258
- getById: (id: string) => request<unknown>(`/cities/${id}`),
259
- create: (data: unknown) =>
260
- request<unknown>('/cities', { method: 'POST', body: JSON.stringify(data) }),
261
- update: (id: string, data: unknown) =>
262
- request<unknown>(`/cities/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
263
- remove: (id: string) =>
264
- request<void>(`/cities/${id}`, { method: 'DELETE' }),
265
-
266
- wasteTypes: (cityId: string) => ({
267
- /** @deprecated Préférez cityGroups.wasteTypes pour la gestion du catalogue. */
268
- list: (params?: { page?: number; pageSize?: number }) => {
269
- const q = new URLSearchParams();
270
- if (params?.page != null) q.set('page', String(params.page));
271
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
272
- const query = q.toString();
273
- return request<PaginatedResponse<unknown>>(`/cities/${cityId}/waste-types${query ? `?${query}` : ''}`);
274
- },
275
- /** @deprecated Préférez cityGroups.wasteTypes.create */
276
- create: (data: unknown) =>
277
- request<unknown>(`/cities/${cityId}/waste-types`, {
278
- method: 'POST',
279
- body: JSON.stringify(data),
280
- }),
281
- getById: (id: string) =>
282
- request<unknown>(`/cities/${cityId}/waste-types/${id}`),
283
- /** @deprecated Préférez cityGroups.wasteTypes.update */
284
- update: (id: string, data: unknown) =>
285
- request<unknown>(`/cities/${cityId}/waste-types/${id}`, {
286
- method: 'PATCH',
287
- body: JSON.stringify(data),
288
- }),
289
- /** @deprecated Délie la ville ; préférez cityGroups.wasteTypes.unlinkCity ou remove */
290
- remove: (id: string) =>
291
- request<void>(`/cities/${cityId}/waste-types/${id}`, {
292
- method: 'DELETE',
293
- }),
294
- }),
295
-
296
- wasteCollectionPlaces: (cityId: string) => ({
297
- list: (params?: { page?: number; pageSize?: number }) => {
298
- const q = new URLSearchParams();
299
- if (params?.page != null) q.set('page', String(params.page));
300
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
301
- const query = q.toString();
302
- return request<PaginatedResponse<unknown>>(`/cities/${cityId}/waste-collection-places${query ? `?${query}` : ''}`);
303
- },
304
- create: (data: unknown) =>
305
- request<unknown>(`/cities/${cityId}/waste-collection-places`, {
306
- method: 'POST',
307
- body: JSON.stringify(data),
308
- }),
309
- getById: (id: string) =>
310
- request<unknown>(`/cities/${cityId}/waste-collection-places/${id}`),
311
- update: (id: string, data: unknown) =>
312
- request<unknown>(`/cities/${cityId}/waste-collection-places/${id}`, {
313
- method: 'PATCH',
314
- body: JSON.stringify(data),
315
- }),
316
- remove: (id: string) =>
317
- request<void>(`/cities/${cityId}/waste-collection-places/${id}`, {
318
- method: 'DELETE',
319
- }),
320
- }),
321
-
322
- quizQuestions: (cityId: string) => ({
323
- list: (params?: { page?: number; pageSize?: number }) => {
324
- const q = new URLSearchParams();
325
- if (params?.page != null) q.set('page', String(params.page));
326
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
327
- const query = q.toString();
328
- return request<PaginatedResponse<unknown>>(`/cities/${cityId}/quiz-questions${query ? `?${query}` : ''}`);
329
- },
330
- create: (data: unknown) =>
331
- request<unknown>(`/cities/${cityId}/quiz-questions`, {
332
- method: 'POST',
333
- body: JSON.stringify(data),
334
- }),
335
- getById: (id: string) =>
336
- request<unknown>(`/cities/${cityId}/quiz-questions/${id}`),
337
- update: (id: string, data: unknown) =>
338
- request<unknown>(`/cities/${cityId}/quiz-questions/${id}`, {
339
- method: 'PATCH',
340
- body: JSON.stringify(data),
341
- }),
342
- remove: (id: string) =>
343
- request<void>(`/cities/${cityId}/quiz-questions/${id}`, {
344
- method: 'DELETE',
345
- }),
346
- }),
347
-
348
- recyclingPoints: (cityId: string) => ({
349
- list: (params?: { page?: number; pageSize?: number }) => {
350
- const q = new URLSearchParams();
351
- if (params?.page != null) q.set('page', String(params.page));
352
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
353
- const query = q.toString();
354
- return request<PaginatedResponse<unknown>>(`/cities/${cityId}/recycling-points${query ? `?${query}` : ''}`);
355
- },
356
- create: (data: unknown) =>
357
- request<unknown>(`/cities/${cityId}/recycling-points`, {
358
- method: 'POST',
359
- body: JSON.stringify(data),
360
- }),
361
- getById: (id: string) =>
362
- request<unknown>(`/cities/${cityId}/recycling-points/${id}`),
363
- update: (id: string, data: unknown) =>
364
- request<unknown>(`/cities/${cityId}/recycling-points/${id}`, {
365
- method: 'PATCH',
366
- body: JSON.stringify(data),
367
- }),
368
- remove: (id: string) =>
369
- request<void>(`/cities/${cityId}/recycling-points/${id}`, {
370
- method: 'DELETE',
371
- }),
372
- }),
373
- },
374
-
375
- wasteTypeDetails: (wasteTypeId: string) => ({
376
- list: (params?: { page?: number; pageSize?: number }) => {
377
- const q = new URLSearchParams();
378
- if (params?.page != null) q.set('page', String(params.page));
379
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
380
- const query = q.toString();
381
- return request<PaginatedResponse<unknown>>(`/waste-types/${wasteTypeId}/details${query ? `?${query}` : ''}`);
382
- },
383
- create: (data: unknown) =>
384
- request<unknown>(`/waste-types/${wasteTypeId}/details`, {
385
- method: 'POST',
386
- body: JSON.stringify(data),
387
- }),
388
- update: (id: string, data: unknown) =>
389
- request<unknown>(`/waste-types/${wasteTypeId}/details/${id}`, {
390
- method: 'PATCH',
391
- body: JSON.stringify(data),
392
- }),
393
- remove: (id: string) =>
394
- request<void>(`/waste-types/${wasteTypeId}/details/${id}`, {
395
- method: 'DELETE',
396
- }),
397
- }),
398
-
399
- cityGroups: {
400
- findAll: (params?: { page?: number; pageSize?: number }) => {
401
- const q = new URLSearchParams();
402
- if (params?.page != null) q.set('page', String(params.page));
403
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
404
- const query = q.toString();
405
- return request<PaginatedResponse<unknown>>(`/city-groups${query ? `?${query}` : ''}`);
406
- },
407
- getById: (id: string) => request<unknown>(`/city-groups/${id}`),
408
- create: (data: unknown) =>
409
- request<unknown>('/city-groups', { method: 'POST', body: JSON.stringify(data) }),
410
- update: (id: string, data: unknown) =>
411
- request<unknown>(`/city-groups/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
412
- remove: (id: string) =>
413
- request<void>(`/city-groups/${id}`, { method: 'DELETE' }),
414
-
415
- wasteTypes: (cityGroupId: string) => ({
416
- list: (params?: { page?: number; pageSize?: number }) => {
417
- const q = new URLSearchParams();
418
- if (params?.page != null) q.set('page', String(params.page));
419
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
420
- const query = q.toString();
421
- return request<PaginatedResponse<unknown>>(
422
- `/city-groups/${cityGroupId}/waste-types${query ? `?${query}` : ''}`,
423
- );
424
- },
425
- getById: (id: string) =>
426
- request<unknown>(`/city-groups/${cityGroupId}/waste-types/${id}`),
427
- create: (data: unknown) =>
428
- request<unknown>(`/city-groups/${cityGroupId}/waste-types`, {
429
- method: 'POST',
430
- body: JSON.stringify(data),
431
- }),
432
- update: (id: string, data: unknown) =>
433
- request<unknown>(`/city-groups/${cityGroupId}/waste-types/${id}`, {
434
- method: 'PATCH',
435
- body: JSON.stringify(data),
436
- }),
437
- remove: (id: string) =>
438
- request<void>(`/city-groups/${cityGroupId}/waste-types/${id}`, {
439
- method: 'DELETE',
440
- }),
441
- linkCity: (wasteTypeId: string, cityId: string) =>
442
- request<unknown>(
443
- `/city-groups/${cityGroupId}/waste-types/${wasteTypeId}/cities/${cityId}`,
444
- { method: 'POST' },
445
- ),
446
- unlinkCity: (wasteTypeId: string, cityId: string) =>
447
- request<void>(
448
- `/city-groups/${cityGroupId}/waste-types/${wasteTypeId}/cities/${cityId}`,
449
- { method: 'DELETE' },
450
- ),
451
- }),
452
- },
453
-
454
- public: {
455
- cities: {
456
- findAll: (params?: { page?: number; pageSize?: number }) => {
457
- const q = new URLSearchParams();
458
- if (params?.page != null) q.set('page', String(params.page));
459
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
460
- const query = q.toString();
461
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities${query ? `?${query}` : ''}`);
462
- },
463
- getById: (id: string) => requestPublic<unknown>(`/public/cities/${id}`),
464
- getWasteTypes: (cityId: string, params?: { page?: number; pageSize?: number }) => {
465
- const q = new URLSearchParams();
466
- if (params?.page != null) q.set('page', String(params.page));
467
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
468
- const query = q.toString();
469
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/waste-types${query ? `?${query}` : ''}`);
470
- },
471
- getWasteCollectionPlaces: (cityId: string, params?: { page?: number; pageSize?: number }) => {
472
- const q = new URLSearchParams();
473
- if (params?.page != null) q.set('page', String(params.page));
474
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
475
- const query = q.toString();
476
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/waste-collection-places${query ? `?${query}` : ''}`);
477
- },
478
- getRecyclingPoints: (cityId: string, params?: { page?: number; pageSize?: number }) => {
479
- const q = new URLSearchParams();
480
- if (params?.page != null) q.set('page', String(params.page));
481
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
482
- const query = q.toString();
483
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/recycling-points${query ? `?${query}` : ''}`);
484
- },
485
- getChallenges: (cityId: string, params?: { page?: number; pageSize?: number }) => {
486
- const q = new URLSearchParams();
487
- if (params?.page != null) q.set('page', String(params.page));
488
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
489
- const query = q.toString();
490
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/challenges${query ? `?${query}` : ''}`);
491
- },
492
- getBadges: (cityId: string, params?: { page?: number; pageSize?: number }) => {
493
- const q = new URLSearchParams();
494
- if (params?.page != null) q.set('page', String(params.page));
495
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
496
- const query = q.toString();
497
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/badges${query ? `?${query}` : ''}`);
498
- },
499
- getOperationalZones: (cityId: string, params?: { page?: number; pageSize?: number }) => {
500
- const q = new URLSearchParams();
501
- if (params?.page != null) q.set('page', String(params.page));
502
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
503
- const query = q.toString();
504
- return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/operational-zones${query ? `?${query}` : ''}`);
505
- },
506
- searchStreetSectors: (
507
- cityId: string,
508
- params: SearchStreetSectorsQuery,
509
- ) => {
510
- const q = new URLSearchParams();
511
- q.set('q', params.q);
512
- if (params.cityGroupId) q.set('cityGroupId', params.cityGroupId);
513
- return requestPublic<SearchStreetSectorsResponse>(
514
- `/public/cities/${encodeURIComponent(cityId)}/street-sectors?${q.toString()}`,
515
- );
516
- },
517
- getCalendarPdfUrl: (cityId: string, placeId: string, year?: number) => {
518
- const q = new URLSearchParams();
519
- if (year != null) q.set('year', String(year));
520
- const query = q.toString();
521
- return `${base}/public/cities/${encodeURIComponent(cityId)}/places/${encodeURIComponent(placeId)}/calendar-pdf${query ? `?${query}` : ''}`;
522
- },
523
- getCalendarPrintUrl: (
524
- cityId: string,
525
- placeId: string,
526
- year?: number,
527
- preview = false,
528
- ) => {
529
- const q = new URLSearchParams();
530
- if (year != null) q.set('year', String(year));
531
- if (preview) q.set('preview', '1');
532
- const query = q.toString();
533
- return `${base}/public/cities/${encodeURIComponent(cityId)}/places/${encodeURIComponent(placeId)}/calendar-print${query ? `?${query}` : ''}`;
534
- },
535
- },
536
- challenges: {
537
- list: (cityId?: string, params?: { page?: number; pageSize?: number }) => {
538
- const q = new URLSearchParams();
539
- if (cityId != null) q.set('cityId', cityId);
540
- if (params?.page != null) q.set('page', String(params.page));
541
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
542
- const query = q.toString();
543
- return requestPublic<PaginatedResponse<unknown>>(`/public/challenges${query ? `?${query}` : ''}`);
544
- },
545
- getById: (id: string) => requestPublic<unknown>(`/public/challenges/${id}`),
546
- getSeries: (id: string) =>
547
- requestPublic<unknown>(`/public/challenges/${id}/series`),
548
- complete: (id: string, body: { deviceId: string; score: number; seriesId: string }) =>
549
- requestPublic<unknown>(`/public/challenges/${id}/complete`, {
550
- method: 'POST',
551
- body: JSON.stringify(body),
552
- }),
553
- getProgress: (deviceId: string) =>
554
- requestPublic<unknown[]>(`/public/challenges/progress/${encodeURIComponent(deviceId)}`),
555
- },
556
- blogPosts: {
557
- list: (cityGroupId?: string, params?: { page?: number; pageSize?: number }) => {
558
- const q = new URLSearchParams();
559
- if (cityGroupId != null) q.set('cityGroupId', cityGroupId);
560
- if (params?.page != null) q.set('page', String(params.page));
561
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
562
- const query = q.toString();
563
- return requestPublic<PaginatedResponse<unknown>>(`/public/blog-posts${query ? `?${query}` : ''}`);
564
- },
565
- getById: (id: string) => requestPublic<unknown>(`/public/blog-posts/${id}`),
566
- },
567
- announcements: {
568
- listForCity: (cityId: string) =>
569
- requestPublic<
570
- Array<{
571
- id: string;
572
- title: string;
573
- body: string;
574
- imageUrl?: string | null;
575
- sentAt?: string | null;
576
- }>
577
- >(`/public/announcements?cityId=${encodeURIComponent(cityId)}`),
578
- },
579
- cityGroups: {
580
- searchStreetSectors: (cityGroupId: string, params: { q: string }) => {
581
- const q = new URLSearchParams();
582
- q.set('q', params.q);
583
- return requestPublic<SearchStreetSectorsResponse>(
584
- `/public/city-groups/${encodeURIComponent(cityGroupId)}/street-sectors?${q.toString()}`,
585
- );
586
- },
587
- },
588
- tickets: {
589
- getForms: (params?: { page?: number; pageSize?: number }) => {
590
- const q = new URLSearchParams();
591
- if (params?.page != null) q.set('page', String(params.page));
592
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
593
- const query = q.toString();
594
- return requestPublic<PaginatedResponse<unknown>>(`/public/tickets/forms${query ? `?${query}` : ''}`);
595
- },
596
- getForm: (id: string) => requestPublic<unknown>(`/public/tickets/forms/${id}`),
597
- create: (data: unknown) =>
598
- requestPublic<unknown>('/public/tickets', {
599
- method: 'POST',
600
- body: JSON.stringify(data),
601
- }),
602
- getByAccessKey: (accessKey: string) =>
603
- requestPublic<unknown>(`/public/tickets/${encodeURIComponent(accessKey)}`),
604
- },
605
- chat: {
606
- getAvailability: (params: string | { cityId?: string; cityGroupId?: string }) => {
607
- const q = new URLSearchParams();
608
- if (typeof params === 'string') {
609
- q.set('cityId', params);
610
- } else {
611
- if (params.cityGroupId) q.set('cityGroupId', params.cityGroupId);
612
- if (params.cityId) q.set('cityId', params.cityId);
613
- }
614
- return requestPublic<{
615
- enabled: boolean;
616
- cityGroupId: string | null;
617
- reason?: string;
618
- }>(`/public/chat/availability?${q.toString()}`);
619
- },
620
- createConversation: (body: {
621
- deviceId: string;
622
- cityId?: string;
623
- cityGroupId?: string;
624
- }) =>
625
- requestPublic<unknown>('/public/chat/conversations', {
626
- method: 'POST',
627
- body: JSON.stringify(body),
628
- }),
629
- setConversationLocation: (
630
- conversationId: string,
631
- body: { cityId: string; placeId?: string },
632
- ) =>
633
- requestPublic<unknown>(
634
- `/public/chat/conversations/${encodeURIComponent(conversationId)}/location`,
635
- {
636
- method: 'PATCH',
637
- body: JSON.stringify(body),
638
- },
639
- ),
640
- getConversations: (deviceId: string, params?: { page?: number; pageSize?: number }) => {
641
- const q = new URLSearchParams();
642
- if (params?.page != null) q.set('page', String(params.page));
643
- if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
644
- const query = q.toString();
645
- return requestPublic<PaginatedResponse<unknown>>(`/public/chat/conversations/${encodeURIComponent(deviceId)}${query ? `?${query}` : ''}`);
646
- },
647
- getMessages: (conversationId: string) =>
648
- requestPublic<unknown>(`/public/chat/conversations/${conversationId}/messages`),
649
- sendMessage: (
650
- conversationId: string,
651
- body: { content: string; actionOptionId?: string; placeId?: string },
652
- ) =>
653
- requestPublic<unknown>(`/public/chat/conversations/${conversationId}/messages`, {
654
- method: 'POST',
655
- body: JSON.stringify(body),
656
- }),
657
- },
658
- devices: {
659
- registerPushToken: (deviceId: string, body: { token: string }) =>
660
- requestPublic<void>(`/public/devices/${encodeURIComponent(deviceId)}/push-token`, {
661
- method: 'POST',
662
- body: JSON.stringify(body),
663
- }),
664
- },
665
- analytics: {
666
- track: (payload: unknown) =>
667
- requestPublic<void>('/public/analytics/track', {
668
- method: 'POST',
669
- body: JSON.stringify(payload),
670
- }),
671
- delete: (deviceId: string) =>
672
- requestPublic<void>(`/public/analytics/delete/${encodeURIComponent(deviceId)}`, {
673
- method: 'DELETE',
674
- }),
675
- },
676
- cms: {
677
- getPageById: (id: string) =>
678
- requestPublic<unknown>(`/public/cms/pages/${encodeURIComponent(id)}`),
679
- getSiteBySlug: (slug: string) =>
680
- requestPublic<unknown>(`/public/cms/sites/${encodeURIComponent(slug)}`),
681
- getAppBySlug: (slug: string) =>
682
- requestPublic<unknown>(`/public/cms/apps/${encodeURIComponent(slug)}`),
683
- getAppMenuById: (appSlug: string, menuId: string) =>
684
- requestPublic<unknown>(
685
- `/public/cms/apps/${encodeURIComponent(appSlug)}/menus/by-id/${encodeURIComponent(menuId)}`,
686
- ),
687
- },
688
- },
689
- };
690
- }
691
-
692
- export type WastoClient = ReturnType<typeof createWastoClient>;
1
+ import type {
2
+ PaginatedResponse,
3
+ PaginationMeta,
4
+ } from './types/pagination.types';
5
+ import type {
6
+ SearchStreetSectorsQuery,
7
+ SearchStreetSectorsResponse,
8
+ } from './types/calendar-pdf.types';
9
+ import type { CityAppPreload } from './types/app-preload.types';
10
+
11
+ const DEFAULT_PAGE = 1;
12
+ const DEFAULT_PAGE_SIZE = 20;
13
+
14
+ export interface WastoClientConfig {
15
+ baseUrl: string;
16
+ getToken?: () => string | null;
17
+ /** Headers added to every request (e.g. X-Wasto-Client). */
18
+ defaultHeaders?: Record<string, string>;
19
+ /**
20
+ * Optional hook when a request fails (HTTP error or network).
21
+ * Called before the error is rethrown. Host apps (e.g. via cms-components)
22
+ * wire Sentry here — wasto-sdk stays vendor-agnostic.
23
+ */
24
+ onRequestError?: (error: Error, context: WastoRequestErrorContext) => void;
25
+ }
26
+
27
+ export type WastoRequestErrorContext = {
28
+ method: string;
29
+ url: string;
30
+ path: string;
31
+ status?: number;
32
+ };
33
+
34
+ export class WastoApiError extends Error {
35
+ constructor(
36
+ message: string,
37
+ public status: number,
38
+ public body?: unknown,
39
+ ) {
40
+ super(message);
41
+ this.name = 'WastoApiError';
42
+ }
43
+ }
44
+
45
+ function getHeaders(config: WastoClientConfig, init?: HeadersInit): HeadersInit {
46
+ const token = config.getToken?.();
47
+ const headers: Record<string, string> = {
48
+ ...(config.defaultHeaders ?? {}),
49
+ ...(init as Record<string, string>),
50
+ };
51
+ if (token) {
52
+ headers['Authorization'] = `Bearer ${token}`;
53
+ }
54
+ const body = (init as { body?: unknown })?.body;
55
+ if (!headers['Content-Type'] && !(typeof FormData !== 'undefined' && body instanceof FormData)) {
56
+ headers['Content-Type'] = 'application/json';
57
+ }
58
+ return headers;
59
+ }
60
+
61
+ async function handleResponse<T>(res: Response): Promise<T> {
62
+ if (!res.ok) {
63
+ let message = `HTTP ${res.status}`;
64
+ try {
65
+ const text = await res.text();
66
+ if (text) {
67
+ try {
68
+ const json = JSON.parse(text);
69
+ message = json.message ?? json.error ?? text;
70
+ } catch {
71
+ message = text;
72
+ }
73
+ }
74
+ } catch {
75
+ // ignore
76
+ }
77
+ throw new WastoApiError(message, res.status, undefined);
78
+ }
79
+ const contentType = res.headers.get('content-type');
80
+ if (contentType?.includes('application/json')) {
81
+ return res.json() as Promise<T>;
82
+ }
83
+ return res.text() as Promise<T>;
84
+ }
85
+
86
+ export function createWastoClient(config: WastoClientConfig) {
87
+ const base = config.baseUrl.replace(/\/$/, '');
88
+
89
+ const notifyRequestError = (
90
+ err: unknown,
91
+ context: Omit<WastoRequestErrorContext, 'status'> & { status?: number },
92
+ ) => {
93
+ if (!config.onRequestError) return;
94
+ const error = err instanceof Error ? err : new Error(String(err));
95
+ const status =
96
+ context.status ??
97
+ (error instanceof WastoApiError ? error.status : undefined);
98
+ try {
99
+ config.onRequestError(error, {
100
+ method: context.method,
101
+ url: context.url,
102
+ path: context.path,
103
+ status,
104
+ });
105
+ } catch {
106
+ // Never let the reporter break the request path.
107
+ }
108
+ };
109
+
110
+ const request = async <T>(
111
+ path: string,
112
+ options: RequestInit = {},
113
+ ): Promise<T> => {
114
+ const url = path.startsWith('http') ? path : `${base}${path.startsWith('/') ? path : `/${path}`}`;
115
+ const method = (options.method ?? 'GET').toUpperCase();
116
+ try {
117
+ const res = await fetch(url, {
118
+ ...options,
119
+ headers: getHeaders(config, options.headers),
120
+ });
121
+ return await handleResponse<T>(res);
122
+ } catch (err) {
123
+ notifyRequestError(err, { method, url, path });
124
+ throw err;
125
+ }
126
+ };
127
+
128
+ /** Public API: no Authorization header (for /public/* routes) */
129
+ const requestPublic = async <T>(
130
+ path: string,
131
+ options: RequestInit = {},
132
+ ): Promise<T> => {
133
+ const url = path.startsWith('http') ? path : `${base}${path.startsWith('/') ? path : `/${path}`}`;
134
+ const method = (options.method ?? 'GET').toUpperCase();
135
+ const headers: Record<string, string> = {
136
+ ...(options.headers as Record<string, string>),
137
+ };
138
+ const body = (options as { body?: unknown })?.body;
139
+ // Keep behavior aligned with `request`/`getHeaders`: if a JSON body is provided,
140
+ // ensure the API receives `Content-Type: application/json` so body parsing works.
141
+ if (!headers['Content-Type'] && !(typeof FormData !== 'undefined' && body instanceof FormData)) {
142
+ headers['Content-Type'] = 'application/json';
143
+ }
144
+ try {
145
+ const res = await fetch(url, { ...options, headers });
146
+ return await handleResponse<T>(res);
147
+ } catch (err) {
148
+ notifyRequestError(err, { method, url, path });
149
+ throw err;
150
+ }
151
+ };
152
+
153
+ return {
154
+ request,
155
+
156
+ auth: {
157
+ login: (email: string, password: string) =>
158
+ request<
159
+ | { access_token: string; user?: unknown }
160
+ | { requiresTwoFactor: true; pendingToken: string }
161
+ >('/auth/login', {
162
+ method: 'POST',
163
+ body: JSON.stringify({ email, password }),
164
+ }),
165
+ register: (data: { email: string; password: string; firstName?: string; lastName?: string }) =>
166
+ request<{ access_token?: string; user?: unknown }>('/auth/register', {
167
+ method: 'POST',
168
+ body: JSON.stringify(data),
169
+ }),
170
+ forgotPassword: (email: string) =>
171
+ request<{ ok: boolean }>('/auth/forgot-password', {
172
+ method: 'POST',
173
+ body: JSON.stringify({ email }),
174
+ }),
175
+ resetPassword: (token: string, password: string) =>
176
+ request<{ ok: boolean }>('/auth/reset-password', {
177
+ method: 'POST',
178
+ body: JSON.stringify({ token, password }),
179
+ }),
180
+ acceptInvite: (data: {
181
+ token: string;
182
+ password: string;
183
+ firstName?: string;
184
+ lastName?: string;
185
+ }) =>
186
+ request<{ access_token: string; user?: unknown }>('/auth/accept-invite', {
187
+ method: 'POST',
188
+ body: JSON.stringify(data),
189
+ }),
190
+ verify2fa: (pendingToken: string, code: string) =>
191
+ request<{ access_token: string; user?: unknown }>('/auth/verify-2fa', {
192
+ method: 'POST',
193
+ body: JSON.stringify({ pendingToken, code }),
194
+ }),
195
+ },
196
+
197
+ users: {
198
+ findAll: (params?: { page?: number; pageSize?: number }) => {
199
+ const q = new URLSearchParams();
200
+ if (params?.page != null) q.set('page', String(params.page));
201
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
202
+ const query = q.toString();
203
+ return request<PaginatedResponse<unknown>>(`/users${query ? `?${query}` : ''}`);
204
+ },
205
+ getById: (id: string) => request<unknown>(`/users/${id}`),
206
+ getMe: () => request<unknown>('/users/me'),
207
+ create: (data: unknown) =>
208
+ request<unknown>('/users', { method: 'POST', body: JSON.stringify(data) }),
209
+ invite: (data: unknown) =>
210
+ request<unknown>('/users/invite', { method: 'POST', body: JSON.stringify(data) }),
211
+ update: (id: string, data: unknown) =>
212
+ request<unknown>(`/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
213
+ updateMe: (data: unknown) =>
214
+ request<unknown>('/users/me', { method: 'PATCH', body: JSON.stringify(data) }),
215
+ remove: (id: string) =>
216
+ request<void>(`/users/${id}`, { method: 'DELETE' }),
217
+ },
218
+
219
+ assets: {
220
+ findAll: (params?: { page?: number; pageSize?: number; cityId?: string; cityGroupId?: string; type?: string; status?: string }) => {
221
+ const q = new URLSearchParams();
222
+ if (params?.page != null) q.set('page', String(params.page));
223
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
224
+ if (params?.cityId) q.set('cityId', params.cityId);
225
+ if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
226
+ if (params?.type) q.set('type', params.type);
227
+ if (params?.status) q.set('status', params.status);
228
+ const query = q.toString();
229
+ return request<PaginatedResponse<unknown>>(`/assets${query ? `?${query}` : ''}`);
230
+ },
231
+ getMapLocations: (params?: { page?: number; pageSize?: number; cityId?: string; cityGroupId?: string; type?: string; status?: string }) => {
232
+ const q = new URLSearchParams();
233
+ if (params?.page != null) q.set('page', String(params.page));
234
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
235
+ if (params?.cityId) q.set('cityId', params.cityId);
236
+ if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
237
+ if (params?.type) q.set('type', params.type);
238
+ if (params?.status) q.set('status', params.status);
239
+ const query = q.toString();
240
+ return request<PaginatedResponse<unknown>>(`/assets/map/locations${query ? `?${query}` : ''}`);
241
+ },
242
+ getById: (id: string) => request<unknown>(`/assets/${id}`),
243
+ create: (data: unknown) =>
244
+ request<unknown>('/assets', { method: 'POST', body: JSON.stringify(data) }),
245
+ update: (id: string, data: unknown) =>
246
+ request<unknown>(`/assets/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
247
+ remove: (id: string) =>
248
+ request<void>(`/assets/${id}`, { method: 'DELETE' }),
249
+ },
250
+
251
+ cities: {
252
+ findAll: (params?: { page?: number; pageSize?: number }) => {
253
+ const q = new URLSearchParams();
254
+ if (params?.page != null) q.set('page', String(params.page));
255
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
256
+ const query = q.toString();
257
+ return request<PaginatedResponse<unknown>>(`/cities${query ? `?${query}` : ''}`);
258
+ },
259
+ getById: (id: string) => request<unknown>(`/cities/${id}`),
260
+ create: (data: unknown) =>
261
+ request<unknown>('/cities', { method: 'POST', body: JSON.stringify(data) }),
262
+ update: (id: string, data: unknown) =>
263
+ request<unknown>(`/cities/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
264
+ remove: (id: string) =>
265
+ request<void>(`/cities/${id}`, { method: 'DELETE' }),
266
+
267
+ wasteTypes: (cityId: string) => ({
268
+ /** @deprecated Préférez cityGroups.wasteTypes pour la gestion du catalogue. */
269
+ list: (params?: { page?: number; pageSize?: number }) => {
270
+ const q = new URLSearchParams();
271
+ if (params?.page != null) q.set('page', String(params.page));
272
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
273
+ const query = q.toString();
274
+ return request<PaginatedResponse<unknown>>(`/cities/${cityId}/waste-types${query ? `?${query}` : ''}`);
275
+ },
276
+ /** @deprecated Préférez cityGroups.wasteTypes.create */
277
+ create: (data: unknown) =>
278
+ request<unknown>(`/cities/${cityId}/waste-types`, {
279
+ method: 'POST',
280
+ body: JSON.stringify(data),
281
+ }),
282
+ getById: (id: string) =>
283
+ request<unknown>(`/cities/${cityId}/waste-types/${id}`),
284
+ /** @deprecated Préférez cityGroups.wasteTypes.update */
285
+ update: (id: string, data: unknown) =>
286
+ request<unknown>(`/cities/${cityId}/waste-types/${id}`, {
287
+ method: 'PATCH',
288
+ body: JSON.stringify(data),
289
+ }),
290
+ /** @deprecated Délie la ville ; préférez cityGroups.wasteTypes.unlinkCity ou remove */
291
+ remove: (id: string) =>
292
+ request<void>(`/cities/${cityId}/waste-types/${id}`, {
293
+ method: 'DELETE',
294
+ }),
295
+ }),
296
+
297
+ wasteCollectionPlaces: (cityId: string) => ({
298
+ list: (params?: { page?: number; pageSize?: number }) => {
299
+ const q = new URLSearchParams();
300
+ if (params?.page != null) q.set('page', String(params.page));
301
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
302
+ const query = q.toString();
303
+ return request<PaginatedResponse<unknown>>(`/cities/${cityId}/waste-collection-places${query ? `?${query}` : ''}`);
304
+ },
305
+ create: (data: unknown) =>
306
+ request<unknown>(`/cities/${cityId}/waste-collection-places`, {
307
+ method: 'POST',
308
+ body: JSON.stringify(data),
309
+ }),
310
+ getById: (id: string) =>
311
+ request<unknown>(`/cities/${cityId}/waste-collection-places/${id}`),
312
+ update: (id: string, data: unknown) =>
313
+ request<unknown>(`/cities/${cityId}/waste-collection-places/${id}`, {
314
+ method: 'PATCH',
315
+ body: JSON.stringify(data),
316
+ }),
317
+ remove: (id: string) =>
318
+ request<void>(`/cities/${cityId}/waste-collection-places/${id}`, {
319
+ method: 'DELETE',
320
+ }),
321
+ }),
322
+
323
+ quizQuestions: (cityId: string) => ({
324
+ list: (params?: { page?: number; pageSize?: number }) => {
325
+ const q = new URLSearchParams();
326
+ if (params?.page != null) q.set('page', String(params.page));
327
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
328
+ const query = q.toString();
329
+ return request<PaginatedResponse<unknown>>(`/cities/${cityId}/quiz-questions${query ? `?${query}` : ''}`);
330
+ },
331
+ create: (data: unknown) =>
332
+ request<unknown>(`/cities/${cityId}/quiz-questions`, {
333
+ method: 'POST',
334
+ body: JSON.stringify(data),
335
+ }),
336
+ getById: (id: string) =>
337
+ request<unknown>(`/cities/${cityId}/quiz-questions/${id}`),
338
+ update: (id: string, data: unknown) =>
339
+ request<unknown>(`/cities/${cityId}/quiz-questions/${id}`, {
340
+ method: 'PATCH',
341
+ body: JSON.stringify(data),
342
+ }),
343
+ remove: (id: string) =>
344
+ request<void>(`/cities/${cityId}/quiz-questions/${id}`, {
345
+ method: 'DELETE',
346
+ }),
347
+ }),
348
+
349
+ recyclingPoints: (cityId: string) => ({
350
+ list: (params?: { page?: number; pageSize?: number }) => {
351
+ const q = new URLSearchParams();
352
+ if (params?.page != null) q.set('page', String(params.page));
353
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
354
+ const query = q.toString();
355
+ return request<PaginatedResponse<unknown>>(`/cities/${cityId}/recycling-points${query ? `?${query}` : ''}`);
356
+ },
357
+ create: (data: unknown) =>
358
+ request<unknown>(`/cities/${cityId}/recycling-points`, {
359
+ method: 'POST',
360
+ body: JSON.stringify(data),
361
+ }),
362
+ getById: (id: string) =>
363
+ request<unknown>(`/cities/${cityId}/recycling-points/${id}`),
364
+ update: (id: string, data: unknown) =>
365
+ request<unknown>(`/cities/${cityId}/recycling-points/${id}`, {
366
+ method: 'PATCH',
367
+ body: JSON.stringify(data),
368
+ }),
369
+ remove: (id: string) =>
370
+ request<void>(`/cities/${cityId}/recycling-points/${id}`, {
371
+ method: 'DELETE',
372
+ }),
373
+ }),
374
+ },
375
+
376
+ wasteTypeDetails: (wasteTypeId: string) => ({
377
+ list: (params?: { page?: number; pageSize?: number }) => {
378
+ const q = new URLSearchParams();
379
+ if (params?.page != null) q.set('page', String(params.page));
380
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
381
+ const query = q.toString();
382
+ return request<PaginatedResponse<unknown>>(`/waste-types/${wasteTypeId}/details${query ? `?${query}` : ''}`);
383
+ },
384
+ create: (data: unknown) =>
385
+ request<unknown>(`/waste-types/${wasteTypeId}/details`, {
386
+ method: 'POST',
387
+ body: JSON.stringify(data),
388
+ }),
389
+ update: (id: string, data: unknown) =>
390
+ request<unknown>(`/waste-types/${wasteTypeId}/details/${id}`, {
391
+ method: 'PATCH',
392
+ body: JSON.stringify(data),
393
+ }),
394
+ remove: (id: string) =>
395
+ request<void>(`/waste-types/${wasteTypeId}/details/${id}`, {
396
+ method: 'DELETE',
397
+ }),
398
+ }),
399
+
400
+ cityGroups: {
401
+ findAll: (params?: { page?: number; pageSize?: number }) => {
402
+ const q = new URLSearchParams();
403
+ if (params?.page != null) q.set('page', String(params.page));
404
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
405
+ const query = q.toString();
406
+ return request<PaginatedResponse<unknown>>(`/city-groups${query ? `?${query}` : ''}`);
407
+ },
408
+ getById: (id: string) => request<unknown>(`/city-groups/${id}`),
409
+ create: (data: unknown) =>
410
+ request<unknown>('/city-groups', { method: 'POST', body: JSON.stringify(data) }),
411
+ update: (id: string, data: unknown) =>
412
+ request<unknown>(`/city-groups/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
413
+ remove: (id: string) =>
414
+ request<void>(`/city-groups/${id}`, { method: 'DELETE' }),
415
+
416
+ wasteTypes: (cityGroupId: string) => ({
417
+ list: (params?: { page?: number; pageSize?: number }) => {
418
+ const q = new URLSearchParams();
419
+ if (params?.page != null) q.set('page', String(params.page));
420
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
421
+ const query = q.toString();
422
+ return request<PaginatedResponse<unknown>>(
423
+ `/city-groups/${cityGroupId}/waste-types${query ? `?${query}` : ''}`,
424
+ );
425
+ },
426
+ getById: (id: string) =>
427
+ request<unknown>(`/city-groups/${cityGroupId}/waste-types/${id}`),
428
+ create: (data: unknown) =>
429
+ request<unknown>(`/city-groups/${cityGroupId}/waste-types`, {
430
+ method: 'POST',
431
+ body: JSON.stringify(data),
432
+ }),
433
+ update: (id: string, data: unknown) =>
434
+ request<unknown>(`/city-groups/${cityGroupId}/waste-types/${id}`, {
435
+ method: 'PATCH',
436
+ body: JSON.stringify(data),
437
+ }),
438
+ remove: (id: string) =>
439
+ request<void>(`/city-groups/${cityGroupId}/waste-types/${id}`, {
440
+ method: 'DELETE',
441
+ }),
442
+ linkCity: (wasteTypeId: string, cityId: string) =>
443
+ request<unknown>(
444
+ `/city-groups/${cityGroupId}/waste-types/${wasteTypeId}/cities/${cityId}`,
445
+ { method: 'POST' },
446
+ ),
447
+ unlinkCity: (wasteTypeId: string, cityId: string) =>
448
+ request<void>(
449
+ `/city-groups/${cityGroupId}/waste-types/${wasteTypeId}/cities/${cityId}`,
450
+ { method: 'DELETE' },
451
+ ),
452
+ }),
453
+ },
454
+
455
+ public: {
456
+ cities: {
457
+ findAll: (params?: { page?: number; pageSize?: number }) => {
458
+ const q = new URLSearchParams();
459
+ if (params?.page != null) q.set('page', String(params.page));
460
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
461
+ const query = q.toString();
462
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities${query ? `?${query}` : ''}`);
463
+ },
464
+ getById: (id: string) => requestPublic<unknown>(`/public/cities/${id}`),
465
+ /**
466
+ * Cold-start agrégé : ville + types de déchets + lieux de collecte + points carte.
467
+ * Remplace 4 appels paginés au lancement de l’app générée.
468
+ */
469
+ getAppPreload: (cityId: string) =>
470
+ requestPublic<CityAppPreload>(
471
+ `/public/cities/${encodeURIComponent(cityId)}/app-preload`,
472
+ ),
473
+ getWasteTypes: (cityId: string, params?: { page?: number; pageSize?: number }) => {
474
+ const q = new URLSearchParams();
475
+ if (params?.page != null) q.set('page', String(params.page));
476
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
477
+ const query = q.toString();
478
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/waste-types${query ? `?${query}` : ''}`);
479
+ },
480
+ getWasteCollectionPlaces: (cityId: string, params?: { page?: number; pageSize?: number }) => {
481
+ const q = new URLSearchParams();
482
+ if (params?.page != null) q.set('page', String(params.page));
483
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
484
+ const query = q.toString();
485
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/waste-collection-places${query ? `?${query}` : ''}`);
486
+ },
487
+ getRecyclingPoints: (cityId: string, params?: { page?: number; pageSize?: number }) => {
488
+ const q = new URLSearchParams();
489
+ if (params?.page != null) q.set('page', String(params.page));
490
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
491
+ const query = q.toString();
492
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/recycling-points${query ? `?${query}` : ''}`);
493
+ },
494
+ getChallenges: (cityId: string, params?: { page?: number; pageSize?: number }) => {
495
+ const q = new URLSearchParams();
496
+ if (params?.page != null) q.set('page', String(params.page));
497
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
498
+ const query = q.toString();
499
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/challenges${query ? `?${query}` : ''}`);
500
+ },
501
+ getBadges: (cityId: string, params?: { page?: number; pageSize?: number }) => {
502
+ const q = new URLSearchParams();
503
+ if (params?.page != null) q.set('page', String(params.page));
504
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
505
+ const query = q.toString();
506
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/badges${query ? `?${query}` : ''}`);
507
+ },
508
+ getOperationalZones: (cityId: string, params?: { page?: number; pageSize?: number }) => {
509
+ const q = new URLSearchParams();
510
+ if (params?.page != null) q.set('page', String(params.page));
511
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
512
+ const query = q.toString();
513
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/operational-zones${query ? `?${query}` : ''}`);
514
+ },
515
+ searchStreetSectors: (
516
+ cityId: string,
517
+ params: SearchStreetSectorsQuery,
518
+ ) => {
519
+ const q = new URLSearchParams();
520
+ q.set('q', params.q);
521
+ if (params.cityGroupId) q.set('cityGroupId', params.cityGroupId);
522
+ return requestPublic<SearchStreetSectorsResponse>(
523
+ `/public/cities/${encodeURIComponent(cityId)}/street-sectors?${q.toString()}`,
524
+ );
525
+ },
526
+ getCalendarPdfUrl: (cityId: string, placeId: string, year?: number) => {
527
+ const q = new URLSearchParams();
528
+ if (year != null) q.set('year', String(year));
529
+ const query = q.toString();
530
+ return `${base}/public/cities/${encodeURIComponent(cityId)}/places/${encodeURIComponent(placeId)}/calendar-pdf${query ? `?${query}` : ''}`;
531
+ },
532
+ getCalendarPrintUrl: (
533
+ cityId: string,
534
+ placeId: string,
535
+ year?: number,
536
+ preview = false,
537
+ ) => {
538
+ const q = new URLSearchParams();
539
+ if (year != null) q.set('year', String(year));
540
+ if (preview) q.set('preview', '1');
541
+ const query = q.toString();
542
+ return `${base}/public/cities/${encodeURIComponent(cityId)}/places/${encodeURIComponent(placeId)}/calendar-print${query ? `?${query}` : ''}`;
543
+ },
544
+ },
545
+ challenges: {
546
+ list: (cityId?: string, params?: { page?: number; pageSize?: number }) => {
547
+ const q = new URLSearchParams();
548
+ if (cityId != null) q.set('cityId', cityId);
549
+ if (params?.page != null) q.set('page', String(params.page));
550
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
551
+ const query = q.toString();
552
+ return requestPublic<PaginatedResponse<unknown>>(`/public/challenges${query ? `?${query}` : ''}`);
553
+ },
554
+ getById: (id: string) => requestPublic<unknown>(`/public/challenges/${id}`),
555
+ getSeries: (id: string) =>
556
+ requestPublic<unknown>(`/public/challenges/${id}/series`),
557
+ complete: (id: string, body: { deviceId: string; score: number; seriesId: string }) =>
558
+ requestPublic<unknown>(`/public/challenges/${id}/complete`, {
559
+ method: 'POST',
560
+ body: JSON.stringify(body),
561
+ }),
562
+ getProgress: (deviceId: string) =>
563
+ requestPublic<unknown[]>(`/public/challenges/progress/${encodeURIComponent(deviceId)}`),
564
+ },
565
+ blogPosts: {
566
+ list: (cityGroupId?: string, params?: { page?: number; pageSize?: number }) => {
567
+ const q = new URLSearchParams();
568
+ if (cityGroupId != null) q.set('cityGroupId', cityGroupId);
569
+ if (params?.page != null) q.set('page', String(params.page));
570
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
571
+ const query = q.toString();
572
+ return requestPublic<PaginatedResponse<unknown>>(`/public/blog-posts${query ? `?${query}` : ''}`);
573
+ },
574
+ getById: (id: string) => requestPublic<unknown>(`/public/blog-posts/${id}`),
575
+ },
576
+ announcements: {
577
+ listForCity: (cityId: string) =>
578
+ requestPublic<
579
+ Array<{
580
+ id: string;
581
+ title: string;
582
+ body: string;
583
+ imageUrl?: string | null;
584
+ sentAt?: string | null;
585
+ }>
586
+ >(`/public/announcements?cityId=${encodeURIComponent(cityId)}`),
587
+ },
588
+ cityGroups: {
589
+ searchStreetSectors: (cityGroupId: string, params: { q: string }) => {
590
+ const q = new URLSearchParams();
591
+ q.set('q', params.q);
592
+ return requestPublic<SearchStreetSectorsResponse>(
593
+ `/public/city-groups/${encodeURIComponent(cityGroupId)}/street-sectors?${q.toString()}`,
594
+ );
595
+ },
596
+ },
597
+ tickets: {
598
+ getForms: (params?: { page?: number; pageSize?: number }) => {
599
+ const q = new URLSearchParams();
600
+ if (params?.page != null) q.set('page', String(params.page));
601
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
602
+ const query = q.toString();
603
+ return requestPublic<PaginatedResponse<unknown>>(`/public/tickets/forms${query ? `?${query}` : ''}`);
604
+ },
605
+ getForm: (id: string) => requestPublic<unknown>(`/public/tickets/forms/${id}`),
606
+ create: (data: unknown) =>
607
+ requestPublic<unknown>('/public/tickets', {
608
+ method: 'POST',
609
+ body: JSON.stringify(data),
610
+ }),
611
+ getByAccessKey: (accessKey: string) =>
612
+ requestPublic<unknown>(`/public/tickets/${encodeURIComponent(accessKey)}`),
613
+ },
614
+ chat: {
615
+ getAvailability: (params: string | { cityId?: string; cityGroupId?: string }) => {
616
+ const q = new URLSearchParams();
617
+ if (typeof params === 'string') {
618
+ q.set('cityId', params);
619
+ } else {
620
+ if (params.cityGroupId) q.set('cityGroupId', params.cityGroupId);
621
+ if (params.cityId) q.set('cityId', params.cityId);
622
+ }
623
+ return requestPublic<{
624
+ enabled: boolean;
625
+ cityGroupId: string | null;
626
+ reason?: string;
627
+ }>(`/public/chat/availability?${q.toString()}`);
628
+ },
629
+ createConversation: (body: {
630
+ deviceId: string;
631
+ cityId?: string;
632
+ cityGroupId?: string;
633
+ }) =>
634
+ requestPublic<unknown>('/public/chat/conversations', {
635
+ method: 'POST',
636
+ body: JSON.stringify(body),
637
+ }),
638
+ setConversationLocation: (
639
+ conversationId: string,
640
+ body: { cityId: string; placeId?: string },
641
+ ) =>
642
+ requestPublic<unknown>(
643
+ `/public/chat/conversations/${encodeURIComponent(conversationId)}/location`,
644
+ {
645
+ method: 'PATCH',
646
+ body: JSON.stringify(body),
647
+ },
648
+ ),
649
+ getConversations: (deviceId: string, params?: { page?: number; pageSize?: number }) => {
650
+ const q = new URLSearchParams();
651
+ if (params?.page != null) q.set('page', String(params.page));
652
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
653
+ const query = q.toString();
654
+ return requestPublic<PaginatedResponse<unknown>>(`/public/chat/conversations/${encodeURIComponent(deviceId)}${query ? `?${query}` : ''}`);
655
+ },
656
+ getMessages: (conversationId: string) =>
657
+ requestPublic<unknown>(`/public/chat/conversations/${conversationId}/messages`),
658
+ sendMessage: (
659
+ conversationId: string,
660
+ body: { content: string; actionOptionId?: string; placeId?: string },
661
+ ) =>
662
+ requestPublic<unknown>(`/public/chat/conversations/${conversationId}/messages`, {
663
+ method: 'POST',
664
+ body: JSON.stringify(body),
665
+ }),
666
+ },
667
+ devices: {
668
+ registerPushToken: (deviceId: string, body: { token: string }) =>
669
+ requestPublic<void>(`/public/devices/${encodeURIComponent(deviceId)}/push-token`, {
670
+ method: 'POST',
671
+ body: JSON.stringify(body),
672
+ }),
673
+ },
674
+ analytics: {
675
+ track: (payload: unknown) =>
676
+ requestPublic<void>('/public/analytics/track', {
677
+ method: 'POST',
678
+ body: JSON.stringify(payload),
679
+ }),
680
+ delete: (deviceId: string) =>
681
+ requestPublic<void>(`/public/analytics/delete/${encodeURIComponent(deviceId)}`, {
682
+ method: 'DELETE',
683
+ }),
684
+ },
685
+ cms: {
686
+ getPageById: (id: string) =>
687
+ requestPublic<unknown>(`/public/cms/pages/${encodeURIComponent(id)}`),
688
+ getSiteBySlug: (slug: string) =>
689
+ requestPublic<unknown>(`/public/cms/sites/${encodeURIComponent(slug)}`),
690
+ getAppBySlug: (slug: string) =>
691
+ requestPublic<unknown>(`/public/cms/apps/${encodeURIComponent(slug)}`),
692
+ getAppMenuById: (appSlug: string, menuId: string) =>
693
+ requestPublic<unknown>(
694
+ `/public/cms/apps/${encodeURIComponent(appSlug)}/menus/by-id/${encodeURIComponent(menuId)}`,
695
+ ),
696
+ },
697
+ },
698
+ };
699
+ }
700
+
701
+ export type WastoClient = ReturnType<typeof createWastoClient>;