@wasto/sdk 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.d.mts +2562 -0
  2. package/dist/index.d.ts +2562 -0
  3. package/dist/index.js +2566 -0
  4. package/dist/index.mjs +2405 -0
  5. package/package.json +30 -0
  6. package/src/client.ts +692 -0
  7. package/src/dashboard-apps.ts +350 -0
  8. package/src/dto/cms-app-settings.ts +91 -0
  9. package/src/enums.ts +298 -0
  10. package/src/free-tier.ts +56 -0
  11. package/src/index.ts +35 -0
  12. package/src/opening-hours/constants.ts +48 -0
  13. package/src/opening-hours/convert.ts +276 -0
  14. package/src/opening-hours/format.ts +99 -0
  15. package/src/opening-hours/index.ts +9 -0
  16. package/src/opening-hours/is-open-now.ts +38 -0
  17. package/src/opening-hours/resolve.ts +71 -0
  18. package/src/opening-hours/time.ts +74 -0
  19. package/src/opening-hours/timezone.ts +34 -0
  20. package/src/opening-hours/types.ts +53 -0
  21. package/src/opening-hours/validate.ts +182 -0
  22. package/src/roi/calculations.ts +732 -0
  23. package/src/roi/index.ts +1 -0
  24. package/src/types/asset-bookings.types.ts +19 -0
  25. package/src/types/asset-interventions.types.ts +24 -0
  26. package/src/types/assets.types.ts +68 -0
  27. package/src/types/auth.types.ts +37 -0
  28. package/src/types/blog-posts.types.ts +19 -0
  29. package/src/types/calendar-pdf.types.ts +24 -0
  30. package/src/types/calendo-events.types.ts +64 -0
  31. package/src/types/challenges.types.ts +30 -0
  32. package/src/types/chatbot.types.ts +82 -0
  33. package/src/types/cities.types.ts +27 -0
  34. package/src/types/city-groups.types.ts +46 -0
  35. package/src/types/cms.types.ts +87 -0
  36. package/src/types/field-reports.types.ts +66 -0
  37. package/src/types/free-tier.types.ts +54 -0
  38. package/src/types/index.ts +32 -0
  39. package/src/types/iot.types.ts +31 -0
  40. package/src/types/maintenance.types.ts +44 -0
  41. package/src/types/mapso.types.ts +146 -0
  42. package/src/types/notifications.types.ts +68 -0
  43. package/src/types/onboarding.types.ts +117 -0
  44. package/src/types/operational-zones.types.ts +70 -0
  45. package/src/types/pagination.types.ts +33 -0
  46. package/src/types/providers.types.ts +29 -0
  47. package/src/types/quiz-questions.types.ts +59 -0
  48. package/src/types/recycling-points.types.ts +47 -0
  49. package/src/types/roi.types.ts +60 -0
  50. package/src/types/surveys.types.ts +103 -0
  51. package/src/types/tickets.types.ts +65 -0
  52. package/src/types/users.types.ts +30 -0
  53. package/src/types/waste-collection-places-query.types.ts +4 -0
  54. package/src/types/waste-collection-places.types.ts +33 -0
  55. package/src/types/waste-types.types.ts +21 -0
package/src/client.ts ADDED
@@ -0,0 +1,692 @@
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>;