@wasto/sdk 1.0.3 → 1.0.5

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 +60 -3
  2. package/dist/index.d.ts +60 -3
  3. package/dist/index.js +79 -0
  4. package/dist/index.mjs +79 -0
  5. package/package.json +1 -1
  6. package/src/client.ts +791 -692
  7. package/src/dashboard-apps.ts +367 -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 +34 -33
  56. package/src/types/waste-types.types.ts +21 -21
package/src/client.ts CHANGED
@@ -1,692 +1,791 @@
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
+ surveys: {
456
+ findAll: (params?: {
457
+ page?: number;
458
+ pageSize?: number;
459
+ cityId?: string;
460
+ cityGroupId?: string;
461
+ status?: string;
462
+ light?: boolean;
463
+ }) => {
464
+ const q = new URLSearchParams();
465
+ if (params?.page != null) q.set('page', String(params.page));
466
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
467
+ if (params?.cityId) q.set('cityId', params.cityId);
468
+ if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
469
+ if (params?.status) q.set('status', params.status);
470
+ if (params?.light) q.set('light', '1');
471
+ const query = q.toString();
472
+ return request<PaginatedResponse<unknown>>(
473
+ `/surveys${query ? `?${query}` : ''}`,
474
+ );
475
+ },
476
+ getById: (id: string) => request<unknown>(`/surveys/${id}`),
477
+ getStats: (id: string) => request<unknown>(`/surveys/${id}/stats`),
478
+ getTerrainOverview: (params?: {
479
+ cityId?: string;
480
+ cityGroupId?: string;
481
+ }) => {
482
+ const q = new URLSearchParams();
483
+ if (params?.cityId) q.set('cityId', params.cityId);
484
+ if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
485
+ const query = q.toString();
486
+ return request<unknown>(
487
+ `/surveys/terrain/overview${query ? `?${query}` : ''}`,
488
+ );
489
+ },
490
+ },
491
+
492
+ fieldReports: {
493
+ findAllForms: (params?: {
494
+ page?: number;
495
+ pageSize?: number;
496
+ cityId?: string;
497
+ cityGroupId?: string;
498
+ status?: string;
499
+ light?: boolean;
500
+ }) => {
501
+ const q = new URLSearchParams();
502
+ if (params?.page != null) q.set('page', String(params.page));
503
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
504
+ if (params?.cityId) q.set('cityId', params.cityId);
505
+ if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
506
+ if (params?.status) q.set('status', params.status);
507
+ if (params?.light) q.set('light', '1');
508
+ const query = q.toString();
509
+ return request<PaginatedResponse<unknown>>(
510
+ `/field-reports/forms${query ? `?${query}` : ''}`,
511
+ );
512
+ },
513
+ getForm: (id: string) =>
514
+ request<unknown>(`/field-reports/forms/${id}`),
515
+ findAllReports: (params?: {
516
+ page?: number;
517
+ pageSize?: number;
518
+ cityId?: string;
519
+ cityGroupId?: string;
520
+ status?: string;
521
+ formId?: string;
522
+ }) => {
523
+ const q = new URLSearchParams();
524
+ if (params?.page != null) q.set('page', String(params.page));
525
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
526
+ if (params?.cityId) q.set('cityId', params.cityId);
527
+ if (params?.cityGroupId) q.set('cityGroupId', params.cityGroupId);
528
+ if (params?.status) q.set('status', params.status);
529
+ if (params?.formId) q.set('formId', params.formId);
530
+ const query = q.toString();
531
+ return request<PaginatedResponse<unknown>>(
532
+ `/field-reports${query ? `?${query}` : ''}`,
533
+ );
534
+ },
535
+ getReport: (id: string) => request<unknown>(`/field-reports/${id}`),
536
+ },
537
+
538
+ public: {
539
+ cities: {
540
+ findAll: (params?: { page?: number; pageSize?: number }) => {
541
+ const q = new URLSearchParams();
542
+ if (params?.page != null) q.set('page', String(params.page));
543
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
544
+ const query = q.toString();
545
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities${query ? `?${query}` : ''}`);
546
+ },
547
+ getById: (id: string) => requestPublic<unknown>(`/public/cities/${id}`),
548
+ /**
549
+ * Cold-start agrégé : ville + types de déchets + lieux de collecte + points carte.
550
+ * Remplace 4 appels paginés au lancement de l’app générée.
551
+ */
552
+ getAppPreload: (cityId: string) =>
553
+ requestPublic<CityAppPreload>(
554
+ `/public/cities/${encodeURIComponent(cityId)}/app-preload`,
555
+ ),
556
+ getWasteTypes: (cityId: string, params?: { page?: number; pageSize?: number }) => {
557
+ const q = new URLSearchParams();
558
+ if (params?.page != null) q.set('page', String(params.page));
559
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
560
+ const query = q.toString();
561
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/waste-types${query ? `?${query}` : ''}`);
562
+ },
563
+ getWasteCollectionPlaces: (cityId: string, params?: { page?: number; pageSize?: number }) => {
564
+ const q = new URLSearchParams();
565
+ if (params?.page != null) q.set('page', String(params.page));
566
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
567
+ const query = q.toString();
568
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/waste-collection-places${query ? `?${query}` : ''}`);
569
+ },
570
+ getRecyclingPoints: (cityId: string, params?: { page?: number; pageSize?: number }) => {
571
+ const q = new URLSearchParams();
572
+ if (params?.page != null) q.set('page', String(params.page));
573
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
574
+ const query = q.toString();
575
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/recycling-points${query ? `?${query}` : ''}`);
576
+ },
577
+ getChallenges: (cityId: string, params?: { page?: number; pageSize?: number }) => {
578
+ const q = new URLSearchParams();
579
+ if (params?.page != null) q.set('page', String(params.page));
580
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
581
+ const query = q.toString();
582
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/challenges${query ? `?${query}` : ''}`);
583
+ },
584
+ getBadges: (cityId: string, params?: { page?: number; pageSize?: number }) => {
585
+ const q = new URLSearchParams();
586
+ if (params?.page != null) q.set('page', String(params.page));
587
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
588
+ const query = q.toString();
589
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/badges${query ? `?${query}` : ''}`);
590
+ },
591
+ getOperationalZones: (cityId: string, params?: { page?: number; pageSize?: number }) => {
592
+ const q = new URLSearchParams();
593
+ if (params?.page != null) q.set('page', String(params.page));
594
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
595
+ const query = q.toString();
596
+ return requestPublic<PaginatedResponse<unknown>>(`/public/cities/${encodeURIComponent(cityId)}/operational-zones${query ? `?${query}` : ''}`);
597
+ },
598
+ searchStreetSectors: (
599
+ cityId: string,
600
+ params: SearchStreetSectorsQuery,
601
+ ) => {
602
+ const q = new URLSearchParams();
603
+ q.set('q', params.q);
604
+ if (params.cityGroupId) q.set('cityGroupId', params.cityGroupId);
605
+ return requestPublic<SearchStreetSectorsResponse>(
606
+ `/public/cities/${encodeURIComponent(cityId)}/street-sectors?${q.toString()}`,
607
+ );
608
+ },
609
+ getCalendarPdfUrl: (cityId: string, placeId: string, year?: number) => {
610
+ const q = new URLSearchParams();
611
+ if (year != null) q.set('year', String(year));
612
+ const query = q.toString();
613
+ return `${base}/public/cities/${encodeURIComponent(cityId)}/places/${encodeURIComponent(placeId)}/calendar-pdf${query ? `?${query}` : ''}`;
614
+ },
615
+ getCalendarPrintUrl: (
616
+ cityId: string,
617
+ placeId: string,
618
+ year?: number,
619
+ preview = false,
620
+ ) => {
621
+ const q = new URLSearchParams();
622
+ if (year != null) q.set('year', String(year));
623
+ if (preview) q.set('preview', '1');
624
+ const query = q.toString();
625
+ return `${base}/public/cities/${encodeURIComponent(cityId)}/places/${encodeURIComponent(placeId)}/calendar-print${query ? `?${query}` : ''}`;
626
+ },
627
+ },
628
+ challenges: {
629
+ list: (cityId?: string, params?: { page?: number; pageSize?: number }) => {
630
+ const q = new URLSearchParams();
631
+ if (cityId != null) q.set('cityId', cityId);
632
+ if (params?.page != null) q.set('page', String(params.page));
633
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
634
+ const query = q.toString();
635
+ return requestPublic<PaginatedResponse<unknown>>(`/public/challenges${query ? `?${query}` : ''}`);
636
+ },
637
+ getById: (id: string) => requestPublic<unknown>(`/public/challenges/${id}`),
638
+ getSeries: (id: string) =>
639
+ requestPublic<unknown>(`/public/challenges/${id}/series`),
640
+ complete: (id: string, body: { deviceId: string; score: number; seriesId: string }) =>
641
+ requestPublic<unknown>(`/public/challenges/${id}/complete`, {
642
+ method: 'POST',
643
+ body: JSON.stringify(body),
644
+ }),
645
+ getProgress: (deviceId: string) =>
646
+ requestPublic<unknown[]>(`/public/challenges/progress/${encodeURIComponent(deviceId)}`),
647
+ },
648
+ blogPosts: {
649
+ list: (cityGroupId?: string, params?: { page?: number; pageSize?: number }) => {
650
+ const q = new URLSearchParams();
651
+ if (cityGroupId != null) q.set('cityGroupId', cityGroupId);
652
+ if (params?.page != null) q.set('page', String(params.page));
653
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
654
+ const query = q.toString();
655
+ return requestPublic<PaginatedResponse<unknown>>(`/public/blog-posts${query ? `?${query}` : ''}`);
656
+ },
657
+ getById: (id: string) => requestPublic<unknown>(`/public/blog-posts/${id}`),
658
+ },
659
+ announcements: {
660
+ listForCity: (cityId: string) =>
661
+ requestPublic<
662
+ Array<{
663
+ id: string;
664
+ title: string;
665
+ body: string;
666
+ imageUrl?: string | null;
667
+ sentAt?: string | null;
668
+ }>
669
+ >(`/public/announcements?cityId=${encodeURIComponent(cityId)}`),
670
+ },
671
+ cityGroups: {
672
+ searchStreetSectors: (cityGroupId: string, params: { q: string }) => {
673
+ const q = new URLSearchParams();
674
+ q.set('q', params.q);
675
+ return requestPublic<SearchStreetSectorsResponse>(
676
+ `/public/city-groups/${encodeURIComponent(cityGroupId)}/street-sectors?${q.toString()}`,
677
+ );
678
+ },
679
+ },
680
+ tickets: {
681
+ getForms: (params?: { page?: number; pageSize?: number }) => {
682
+ const q = new URLSearchParams();
683
+ if (params?.page != null) q.set('page', String(params.page));
684
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
685
+ const query = q.toString();
686
+ return requestPublic<PaginatedResponse<unknown>>(`/public/tickets/forms${query ? `?${query}` : ''}`);
687
+ },
688
+ getForm: (id: string) => requestPublic<unknown>(`/public/tickets/forms/${id}`),
689
+ create: (data: unknown) =>
690
+ requestPublic<unknown>('/public/tickets', {
691
+ method: 'POST',
692
+ body: JSON.stringify(data),
693
+ }),
694
+ getByAccessKey: (accessKey: string) =>
695
+ requestPublic<unknown>(`/public/tickets/${encodeURIComponent(accessKey)}`),
696
+ },
697
+ chat: {
698
+ getAvailability: (params: string | { cityId?: string; cityGroupId?: string }) => {
699
+ const q = new URLSearchParams();
700
+ if (typeof params === 'string') {
701
+ q.set('cityId', params);
702
+ } else {
703
+ if (params.cityGroupId) q.set('cityGroupId', params.cityGroupId);
704
+ if (params.cityId) q.set('cityId', params.cityId);
705
+ }
706
+ return requestPublic<{
707
+ enabled: boolean;
708
+ cityGroupId: string | null;
709
+ reason?: string;
710
+ }>(`/public/chat/availability?${q.toString()}`);
711
+ },
712
+ createConversation: (body: {
713
+ deviceId: string;
714
+ cityId?: string;
715
+ cityGroupId?: string;
716
+ }) =>
717
+ requestPublic<unknown>('/public/chat/conversations', {
718
+ method: 'POST',
719
+ body: JSON.stringify(body),
720
+ }),
721
+ setConversationLocation: (
722
+ conversationId: string,
723
+ body: { cityId: string; placeId?: string },
724
+ ) =>
725
+ requestPublic<unknown>(
726
+ `/public/chat/conversations/${encodeURIComponent(conversationId)}/location`,
727
+ {
728
+ method: 'PATCH',
729
+ body: JSON.stringify(body),
730
+ },
731
+ ),
732
+ getConversations: (deviceId: string, params?: { page?: number; pageSize?: number }) => {
733
+ const q = new URLSearchParams();
734
+ if (params?.page != null) q.set('page', String(params.page));
735
+ if (params?.pageSize != null) q.set('pageSize', String(params.pageSize));
736
+ const query = q.toString();
737
+ return requestPublic<PaginatedResponse<unknown>>(`/public/chat/conversations/${encodeURIComponent(deviceId)}${query ? `?${query}` : ''}`);
738
+ },
739
+ getMessages: (conversationId: string) =>
740
+ requestPublic<unknown>(`/public/chat/conversations/${conversationId}/messages`),
741
+ sendMessage: (
742
+ conversationId: string,
743
+ body: { content: string; actionOptionId?: string; placeId?: string },
744
+ ) =>
745
+ requestPublic<unknown>(`/public/chat/conversations/${conversationId}/messages`, {
746
+ method: 'POST',
747
+ body: JSON.stringify(body),
748
+ }),
749
+ },
750
+ devices: {
751
+ registerPushToken: (
752
+ deviceId: string,
753
+ body: {
754
+ token: string;
755
+ cityId?: string | null;
756
+ wasteCollectionPlaceId?: string | null;
757
+ },
758
+ ) =>
759
+ requestPublic<void>(`/public/devices/${encodeURIComponent(deviceId)}/push-token`, {
760
+ method: 'POST',
761
+ body: JSON.stringify(body),
762
+ }),
763
+ },
764
+ analytics: {
765
+ track: (payload: unknown) =>
766
+ requestPublic<void>('/public/analytics/track', {
767
+ method: 'POST',
768
+ body: JSON.stringify(payload),
769
+ }),
770
+ delete: (deviceId: string) =>
771
+ requestPublic<void>(`/public/analytics/delete/${encodeURIComponent(deviceId)}`, {
772
+ method: 'DELETE',
773
+ }),
774
+ },
775
+ cms: {
776
+ getPageById: (id: string) =>
777
+ requestPublic<unknown>(`/public/cms/pages/${encodeURIComponent(id)}`),
778
+ getSiteBySlug: (slug: string) =>
779
+ requestPublic<unknown>(`/public/cms/sites/${encodeURIComponent(slug)}`),
780
+ getAppBySlug: (slug: string) =>
781
+ requestPublic<unknown>(`/public/cms/apps/${encodeURIComponent(slug)}`),
782
+ getAppMenuById: (appSlug: string, menuId: string) =>
783
+ requestPublic<unknown>(
784
+ `/public/cms/apps/${encodeURIComponent(appSlug)}/menus/by-id/${encodeURIComponent(menuId)}`,
785
+ ),
786
+ },
787
+ },
788
+ };
789
+ }
790
+
791
+ export type WastoClient = ReturnType<typeof createWastoClient>;