@microboxlabs/miot-calendar-client 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @microboxlabs/miot-calendar-client
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@microboxlabs/miot-calendar-client)](https://www.npmjs.com/package/@microboxlabs/miot-calendar-client)
4
+ [![License](https://img.shields.io/npm/l/@microboxlabs/miot-calendar-client)](./LICENSE)
5
+
3
6
  TypeScript client for the ModularIoT Calendar API. Zero dependencies — uses the native `fetch` API.
4
7
 
5
8
  ## Installation
@@ -10,6 +13,8 @@ npm install @microboxlabs/miot-calendar-client
10
13
 
11
14
  ## Quick Start
12
15
 
16
+ A complete workflow: create a calendar, define a time window, generate slots, and book one.
17
+
13
18
  ```ts
14
19
  import { createMiotCalendarClient } from "@microboxlabs/miot-calendar-client";
15
20
 
@@ -17,76 +22,686 @@ const client = createMiotCalendarClient({
17
22
  baseUrl: "https://your-api-host.com",
18
23
  });
19
24
 
20
- // List calendars
21
- const calendars = await client.calendars.list();
25
+ // 1. Create a calendar
26
+ const calendar = await client.calendars.create({
27
+ code: "maintenance",
28
+ name: "Vehicle Maintenance",
29
+ timezone: "America/New_York", // defaults to "UTC" if omitted
30
+ });
31
+
32
+ // 2. Add a time window (Mon–Fri, 8 AM – 5 PM, 30-min slots)
33
+ const timeWindow = await client.calendars.createTimeWindow(calendar.id, {
34
+ name: "Business Hours",
35
+ startHour: 8,
36
+ endHour: 17,
37
+ validFrom: "2026-03-01",
38
+ daysOfWeek: "1,2,3,4,5", // Mon–Fri
39
+ slotDurationMinutes: 30, // default: 30
40
+ capacityPerSlot: 2, // default: 1
41
+ });
42
+
43
+ // 3. Generate slots for the first two weeks
44
+ const result = await client.slots.generate({
45
+ calendarId: calendar.id,
46
+ startDate: "2026-03-01",
47
+ endDate: "2026-03-14", // max range: 90 days
48
+ });
49
+ console.log(`Created ${result.slotsCreated} slots`);
22
50
 
23
- // Create a booking
24
- const booking = await client.bookings.create({
25
- calendarId: "cal-123",
26
- resource: { id: "truck-42", type: "vehicle", label: "Truck 42" },
27
- slot: { date: "2026-03-01", hour: 10, minutes: 0 },
51
+ // 4. Find available slots
52
+ const { data: slots } = await client.slots.list({
53
+ calendarId: calendar.id, // required
54
+ available: true,
55
+ startDate: "2026-03-03",
56
+ endDate: "2026-03-07",
28
57
  });
58
+
59
+ // 5. Book the first available slot
60
+ const booking = await client.bookings.create(
61
+ {
62
+ calendarId: calendar.id,
63
+ resource: { id: "truck-42", type: "vehicle", label: "Truck 42" },
64
+ slot: { date: slots[0].slotDate, hour: slots[0].slotHour, minutes: slots[0].slotMinutes },
65
+ },
66
+ { userId: "user-abc" }, // sets X-User-Id header → stored as createdBy
67
+ );
29
68
  ```
30
69
 
31
70
  ## Configuration
32
71
 
33
72
  ```ts
34
- createMiotCalendarClient({
35
- baseUrl: "https://your-api-host.com", // Required
36
- headers: { Authorization: "Bearer ..." }, // Optional default headers
37
- fetch: customFetch, // Optional fetch implementation
73
+ const client = createMiotCalendarClient({
74
+ baseUrl: "https://your-api-host.com", // Required — API base URL
75
+ headers: { Authorization: "Bearer ..." }, // Optional merged into every request
76
+ fetch: customFetch, // Optional — custom fetch implementation
38
77
  });
39
78
  ```
40
79
 
80
+ | Option | Type | Required | Description |
81
+ |--------|------|----------|-------------|
82
+ | `baseUrl` | `string` | Yes | Base URL of the Calendar API |
83
+ | `headers` | `Record<string, string>` | No | Default headers sent with every request |
84
+ | `fetch` | `typeof fetch` | No | Custom `fetch` implementation (defaults to `globalThis.fetch`) |
85
+
41
86
  ## API Reference
42
87
 
43
- ### `client.calendars`
44
-
45
- | Method | Description |
46
- |---|---|
47
- | `list(params?)` | List calendars. Filter by `active`. |
48
- | `get(id)` | Get a calendar by ID. |
49
- | `create(body)` | Create a calendar. |
50
- | `update(id, body)` | Update a calendar. |
51
- | `deactivate(id)` | Deactivate a calendar. |
52
- | `listTimeWindows(calendarId)` | List time windows for a calendar. |
53
- | `createTimeWindow(calendarId, body)` | Create a time window. |
54
- | `updateTimeWindow(calendarId, timeWindowId, body)` | Update a time window. |
55
-
56
- ### `client.bookings`
57
-
58
- | Method | Description |
59
- |---|---|
60
- | `list(params?)` | List bookings. Filter by `calendarId`, `startDate`, `endDate`. |
61
- | `get(id)` | Get a booking by ID. |
62
- | `create(body, options?)` | Create a booking. Pass `{ userId }` to set `X-User-Id` header. |
63
- | `cancel(id)` | Cancel a booking. |
64
- | `listByResource(resourceId)` | List bookings for a specific resource. |
65
-
66
- ### `client.slots`
67
-
68
- | Method | Description |
69
- |---|---|
70
- | `list(params)` | List slots. Filter by `calendarId`, `available`, `startDate`, `endDate`. |
71
- | `get(id)` | Get a slot by ID. |
72
- | `generate(body)` | Generate slots for a date range. |
73
- | `updateStatus(id, body)` | Update slot status (`OPEN`, `FULL`, `CLOSED`). |
88
+ ### Calendars
89
+
90
+ #### `calendars.list(params?)`
91
+
92
+ List all calendars, optionally filtered by active status or group membership.
93
+
94
+ | Param | Type | Required | Description |
95
+ |-------|------|----------|-------------|
96
+ | `active` | `boolean` | No | Filter by active status |
97
+ | `groupCode` | `string` | No | Filter calendars belonging to this group code |
98
+
99
+ **Returns:** `CalendarResponse[]`
100
+
101
+ #### `calendars.get(id)`
102
+
103
+ Get a single calendar by ID.
104
+
105
+ | Param | Type | Required | Description |
106
+ |-------|------|----------|-------------|
107
+ | `id` | `string` | Yes | Calendar ID |
108
+
109
+ **Returns:** `CalendarResponse`
110
+
111
+ **Throws:** `MiotCalendarApiError` with status `404` if not found.
112
+
113
+ #### `calendars.create(body)`
114
+
115
+ Create a new calendar.
116
+
117
+ | Field | Type | Required | Default | Description |
118
+ |-------|------|----------|---------|-------------|
119
+ | `code` | `string` | Yes | — | Unique code identifier |
120
+ | `name` | `string` | Yes | — | Display name |
121
+ | `description` | `string` | No | — | Optional description |
122
+ | `timezone` | `string` | No | `"UTC"` | IANA timezone |
123
+ | `active` | `boolean` | No | `true` | Whether the calendar is active |
124
+ | `groups` | `string[]` | No | — | Group codes to assign. `null` = no change; `[]` = remove all; `["code"]` = replace all |
125
+
126
+ **Returns:** `CalendarResponse`
127
+
128
+ #### `calendars.update(id, body)`
129
+
130
+ Replace a calendar's fields. Takes the same body as `create`.
131
+
132
+ | Param | Type | Required | Description |
133
+ |-------|------|----------|-------------|
134
+ | `id` | `string` | Yes | Calendar ID |
135
+ | `body` | `CalendarRequest` | Yes | Updated calendar data (include `groups` to reassign group membership) |
136
+
137
+ **Returns:** `CalendarResponse`
138
+
139
+ #### `calendars.deactivate(id)`
140
+
141
+ Deactivate a calendar (soft delete).
142
+
143
+ | Param | Type | Required | Description |
144
+ |-------|------|----------|-------------|
145
+ | `id` | `string` | Yes | Calendar ID |
146
+
147
+ **Returns:** `void` (HTTP 204 — no content)
148
+
149
+ ---
150
+
151
+ ### Time Windows
152
+
153
+ Time windows are managed through the `calendars` namespace.
154
+
155
+ #### `calendars.listTimeWindows(calendarId)`
156
+
157
+ List all time windows for a calendar.
158
+
159
+ | Param | Type | Required | Description |
160
+ |-------|------|----------|-------------|
161
+ | `calendarId` | `string` | Yes | Calendar ID |
162
+
163
+ **Returns:** `TimeWindowResponse[]`
164
+
165
+ #### `calendars.createTimeWindow(calendarId, body)`
166
+
167
+ Create a time window within a calendar.
168
+
169
+ | Field | Type | Required | Default | Description |
170
+ |-------|------|----------|---------|-------------|
171
+ | `name` | `string` | Yes | — | Time window name |
172
+ | `startHour` | `number` | Yes | — | Start hour (0–23) |
173
+ | `endHour` | `number` | Yes | — | End hour (0–23, must be > startHour) |
174
+ | `validFrom` | `string` | Yes | — | Start date (`YYYY-MM-DD`) |
175
+ | `validTo` | `string` | No | — | End date (`YYYY-MM-DD`) |
176
+ | `slotDurationMinutes` | `number` | No | `30` | Slot duration in minutes |
177
+ | `capacityPerSlot` | `number` | No | `1` | Max bookings per slot |
178
+ | `daysOfWeek` | `string` | No | — | Comma-separated days (1=Mon … 7=Sun) |
179
+ | `active` | `boolean` | No | `true` | Whether the time window is active |
180
+
181
+ **Returns:** `TimeWindowResponse`
182
+
183
+ #### `calendars.updateTimeWindow(calendarId, timeWindowId, body)`
184
+
185
+ Update a time window. Takes the same body as `createTimeWindow`.
186
+
187
+ | Param | Type | Required | Description |
188
+ |-------|------|----------|-------------|
189
+ | `calendarId` | `string` | Yes | Calendar ID |
190
+ | `timeWindowId` | `string` | Yes | Time window ID |
191
+ | `body` | `TimeWindowRequest` | Yes | Updated time window data |
192
+
193
+ **Returns:** `TimeWindowResponse`
194
+
195
+ ---
196
+
197
+ ### Groups
198
+
199
+ Calendar groups let you organize calendars into named collections. Calendars can belong to multiple groups; use `CalendarRequest.groups` on create or update to manage membership.
200
+
201
+ #### `groups.list(params?)`
202
+
203
+ List all calendar groups, optionally filtered by active status.
204
+
205
+ | Param | Type | Required | Description |
206
+ |-------|------|----------|-------------|
207
+ | `active` | `boolean` | No | Filter by active status |
208
+
209
+ **Returns:** `CalendarGroupResponse[]`
210
+
211
+ #### `groups.get(id)`
212
+
213
+ Get a single calendar group by ID.
214
+
215
+ | Param | Type | Required | Description |
216
+ |-------|------|----------|-------------|
217
+ | `id` | `string` | Yes | Group ID |
218
+
219
+ **Returns:** `CalendarGroupResponse`
220
+
221
+ **Throws:** `MiotCalendarApiError` with status `404` if not found.
222
+
223
+ #### `groups.create(body)`
224
+
225
+ Create a new calendar group.
226
+
227
+ | Field | Type | Required | Default | Description |
228
+ |-------|------|----------|---------|-------------|
229
+ | `code` | `string` | Yes | — | Unique code identifier (max 50 chars) |
230
+ | `name` | `string` | Yes | — | Display name (max 255 chars) |
231
+ | `description` | `string` | No | — | Optional description |
232
+ | `active` | `boolean` | No | `true` | Whether the group is active |
233
+
234
+ **Returns:** `CalendarGroupResponse`
235
+
236
+ **Throws:** `MiotCalendarApiError` with status `400` if the code is already taken.
237
+
238
+ #### `groups.update(id, body)`
239
+
240
+ Replace a calendar group's fields. Takes the same body as `create`.
241
+
242
+ | Param | Type | Required | Description |
243
+ |-------|------|----------|-------------|
244
+ | `id` | `string` | Yes | Group ID |
245
+ | `body` | `CalendarGroupRequest` | Yes | Updated group data |
246
+
247
+ **Returns:** `CalendarGroupResponse`
248
+
249
+ #### `groups.deactivate(id)`
250
+
251
+ Deactivate a calendar group (soft delete). Calendars in the group are not affected.
252
+
253
+ | Param | Type | Required | Description |
254
+ |-------|------|----------|-------------|
255
+ | `id` | `string` | Yes | Group ID |
256
+
257
+ **Returns:** `void` (HTTP 204 — no content)
258
+
259
+ ---
260
+
261
+ ### Slots
262
+
263
+ #### `slots.list(params)`
264
+
265
+ List slots for a calendar. The `calendarId` parameter is **required**.
266
+
267
+ | Param | Type | Required | Default | Description |
268
+ |-------|------|----------|---------|-------------|
269
+ | `calendarId` | `string` | Yes | — | Calendar ID |
270
+ | `available` | `boolean` | No | — | Filter by availability |
271
+ | `startDate` | `string` | No | today | Start of date range (`YYYY-MM-DD`) |
272
+ | `endDate` | `string` | No | today + 7 days | End of date range (`YYYY-MM-DD`) |
273
+
274
+ **Returns:** `SlotListResponse` — `{ data: SlotResponse[], total: number }`
275
+
276
+ #### `slots.get(id)`
277
+
278
+ Get a single slot by ID.
279
+
280
+ | Param | Type | Required | Description |
281
+ |-------|------|----------|-------------|
282
+ | `id` | `string` | Yes | Slot ID |
283
+
284
+ **Returns:** `SlotResponse`
285
+
286
+ **Throws:** `MiotCalendarApiError` with status `404` if not found.
287
+
288
+ #### `slots.generate(body)`
289
+
290
+ Generate slots for a calendar based on its time windows.
291
+
292
+ | Field | Type | Required | Description |
293
+ |-------|------|----------|-------------|
294
+ | `calendarId` | `string` | Yes | Calendar ID |
295
+ | `startDate` | `string` | Yes | Start date (`YYYY-MM-DD`) |
296
+ | `endDate` | `string` | Yes | End date (`YYYY-MM-DD`) |
297
+
298
+ > **Note:** The date range must not exceed **90 days**. The API returns `400 Bad Request` if the range is larger.
299
+
300
+ **Returns:** `GenerateSlotsResponse` — `{ slotsCreated: number, slotsSkipped: number, message: string }`
301
+
302
+ #### `slots.updateStatus(id, body)`
303
+
304
+ Manually change a slot's status.
305
+
306
+ | Field | Type | Required | Description |
307
+ |-------|------|----------|-------------|
308
+ | `id` | `string` | Yes | Slot ID |
309
+ | `status` | `"OPEN" \| "CLOSED"` | Yes | New status |
310
+
311
+ > **Note:** Only `OPEN` and `CLOSED` are accepted. `FULL` is managed automatically by the system based on occupancy and cannot be set manually. Passing `FULL` returns `400 Bad Request`.
312
+
313
+ **Returns:** `SlotResponse`
314
+
315
+ ---
316
+
317
+ ### Bookings
318
+
319
+ #### `bookings.list(params?)`
320
+
321
+ List bookings, optionally filtered by calendar and date range.
322
+
323
+ | Param | Type | Required | Default | Description |
324
+ |-------|------|----------|---------|-------------|
325
+ | `calendarId` | `string` | No | — | Filter by calendar |
326
+ | `startDate` | `string` | No | today | Start of date range (`YYYY-MM-DD`) |
327
+ | `endDate` | `string` | No | today + 30 days | End of date range (`YYYY-MM-DD`) |
328
+
329
+ **Returns:** `BookingListResponse` — `{ data: BookingResponse[], total: number }`
330
+
331
+ #### `bookings.get(id)`
332
+
333
+ Get a single booking by ID.
334
+
335
+ | Param | Type | Required | Description |
336
+ |-------|------|----------|-------------|
337
+ | `id` | `string` | Yes | Booking ID |
338
+
339
+ **Returns:** `BookingResponse`
340
+
341
+ **Throws:** `MiotCalendarApiError` with status `404` if not found.
342
+
343
+ #### `bookings.create(body, options?)`
344
+
345
+ Create a booking for a specific slot.
346
+
347
+ **Body (`BookingRequest`):**
348
+
349
+ | Field | Type | Required | Description |
350
+ |-------|------|----------|-------------|
351
+ | `calendarId` | `string` | Yes | Calendar ID |
352
+ | `resource` | `ResourceData` | Yes | The resource being booked |
353
+ | `slot` | `SlotData` | Yes | Target slot (date + time) |
354
+
355
+ **Options:**
356
+
357
+ | Field | Type | Required | Description |
358
+ |-------|------|----------|-------------|
359
+ | `userId` | `string` | No | Sets `X-User-Id` header — stored as `createdBy` in the response |
360
+
361
+ **Returns:** `BookingResponse`
362
+
363
+ **Throws:**
364
+
365
+ - `404` — Calendar or slot not found
366
+ - `409` — Slot is full (no available capacity)
367
+
368
+ #### `bookings.cancel(id)`
369
+
370
+ Cancel a booking.
371
+
372
+ | Param | Type | Required | Description |
373
+ |-------|------|----------|-------------|
374
+ | `id` | `string` | Yes | Booking ID |
375
+
376
+ **Returns:** `void` (HTTP 204 — no content)
377
+
378
+ #### `bookings.listByResource(resourceId)`
379
+
380
+ List all bookings for a specific resource.
381
+
382
+ | Param | Type | Required | Description |
383
+ |-------|------|----------|-------------|
384
+ | `resourceId` | `string` | Yes | Resource ID |
385
+
386
+ **Returns:** `BookingListResponse`
387
+
388
+ ## Types
389
+
390
+ ### `CalendarGroupRequest`
391
+
392
+ ```ts
393
+ interface CalendarGroupRequest {
394
+ code: string; // Unique code identifier (max 50 chars)
395
+ name: string; // Display name (max 255 chars)
396
+ description?: string; // Optional description
397
+ active?: boolean; // Active status (default: true)
398
+ }
399
+ ```
400
+
401
+ ### `CalendarGroupResponse`
402
+
403
+ ```ts
404
+ interface CalendarGroupResponse {
405
+ id: string;
406
+ code: string;
407
+ name: string;
408
+ description?: string;
409
+ active: boolean;
410
+ createdAt: string; // ISO 8601
411
+ updatedAt: string; // ISO 8601
412
+ }
413
+ ```
414
+
415
+ ### `CalendarRequest`
416
+
417
+ ```ts
418
+ interface CalendarRequest {
419
+ code: string; // Unique code identifier
420
+ name: string; // Display name
421
+ description?: string; // Optional description
422
+ timezone?: string; // IANA timezone (default: "UTC")
423
+ active?: boolean; // Active status (default: true)
424
+ groups?: string[]; // Group codes to assign. null = no change; [] = remove all; ["code"] = replace all
425
+ }
426
+ ```
427
+
428
+ ### `CalendarResponse`
429
+
430
+ ```ts
431
+ interface CalendarResponse {
432
+ id: string;
433
+ code: string;
434
+ name: string;
435
+ description?: string;
436
+ timezone: string; // Always present (default: "UTC")
437
+ active: boolean; // Always present (default: true)
438
+ createdAt: string; // ISO 8601
439
+ updatedAt: string; // ISO 8601
440
+ groups?: CalendarGroupResponse[]; // Groups this calendar belongs to
441
+ }
442
+ ```
443
+
444
+ ### `TimeWindowRequest`
445
+
446
+ ```ts
447
+ interface TimeWindowRequest {
448
+ name: string; // Time window name
449
+ startHour: number; // 0–23
450
+ endHour: number; // 0–23 (must be > startHour)
451
+ validFrom: string; // YYYY-MM-DD
452
+ validTo?: string; // YYYY-MM-DD
453
+ slotDurationMinutes?: number; // Default: 30
454
+ capacityPerSlot?: number; // Default: 1
455
+ daysOfWeek?: string; // Comma-separated: "1,2,3,4,5" (1=Mon, 7=Sun)
456
+ active?: boolean; // Default: true
457
+ }
458
+ ```
459
+
460
+ ### `TimeWindowResponse`
461
+
462
+ ```ts
463
+ interface TimeWindowResponse {
464
+ id: string;
465
+ calendarId: string;
466
+ name: string;
467
+ startHour: number;
468
+ endHour: number;
469
+ slotDurationMinutes: number; // Always present (default: 30)
470
+ capacityPerSlot: number; // Always present (default: 1)
471
+ daysOfWeek: string;
472
+ validFrom: string;
473
+ validTo?: string;
474
+ active: boolean; // Always present (default: true)
475
+ createdAt: string; // ISO 8601
476
+ updatedAt: string; // ISO 8601
477
+ }
478
+ ```
479
+
480
+ ### `SlotResponse`
481
+
482
+ ```ts
483
+ interface SlotResponse {
484
+ id: string;
485
+ calendarId: string;
486
+ timeWindowId?: string; // May be absent for manually created slots
487
+ slotDate: string; // YYYY-MM-DD
488
+ slotHour: number; // 0–23
489
+ slotMinutes: number; // 0–59
490
+ capacity: number; // Total capacity
491
+ currentOccupancy: number; // Current bookings count
492
+ availableCapacity: number; // capacity - currentOccupancy
493
+ status: SlotStatus; // "OPEN" | "FULL" | "CLOSED"
494
+ createdAt: string; // ISO 8601
495
+ updatedAt: string; // ISO 8601
496
+ }
497
+ ```
498
+
499
+ ### `SlotListResponse`
500
+
501
+ ```ts
502
+ interface SlotListResponse {
503
+ data: SlotResponse[];
504
+ total: number;
505
+ }
506
+ ```
507
+
508
+ ### `GenerateSlotsRequest`
509
+
510
+ ```ts
511
+ interface GenerateSlotsRequest {
512
+ calendarId: string;
513
+ startDate: string; // YYYY-MM-DD
514
+ endDate: string; // YYYY-MM-DD (max 90 days from startDate)
515
+ }
516
+ ```
517
+
518
+ ### `GenerateSlotsResponse`
519
+
520
+ ```ts
521
+ interface GenerateSlotsResponse {
522
+ slotsCreated: number;
523
+ slotsSkipped: number;
524
+ message: string;
525
+ }
526
+ ```
527
+
528
+ ### `UpdateSlotStatusRequest`
529
+
530
+ ```ts
531
+ interface UpdateSlotStatusRequest {
532
+ status: SlotStatus; // Only "OPEN" or "CLOSED" accepted
533
+ }
534
+ ```
535
+
536
+ ### `BookingRequest`
537
+
538
+ ```ts
539
+ interface BookingRequest {
540
+ calendarId: string;
541
+ resource: ResourceData;
542
+ slot: SlotData;
543
+ }
544
+ ```
545
+
546
+ ### `BookingResponse`
547
+
548
+ ```ts
549
+ interface BookingResponse {
550
+ id: string;
551
+ calendarId: string;
552
+ resource: ResourceData;
553
+ slot: SlotData;
554
+ createdAt: string; // ISO 8601
555
+ createdBy?: string; // Set via X-User-Id header on create
556
+ }
557
+ ```
558
+
559
+ ### `BookingListResponse`
560
+
561
+ ```ts
562
+ interface BookingListResponse {
563
+ data: BookingResponse[];
564
+ total: number;
565
+ }
566
+ ```
567
+
568
+ ### `ResourceData`
569
+
570
+ ```ts
571
+ interface ResourceData {
572
+ id: string; // Resource identifier
573
+ type?: string; // Resource type (e.g. "vehicle", "room")
574
+ label?: string; // Human-readable label
575
+ data?: Record<string, unknown>; // Arbitrary metadata
576
+ }
577
+ ```
578
+
579
+ ### `SlotData`
580
+
581
+ ```ts
582
+ interface SlotData {
583
+ date: string; // YYYY-MM-DD
584
+ hour: number; // 0–23
585
+ minutes: number; // 0–59
586
+ }
587
+ ```
588
+
589
+ ### `SlotStatus`
590
+
591
+ ```ts
592
+ type SlotStatus = "OPEN" | "FULL" | "CLOSED";
593
+ ```
594
+
595
+ ### `ErrorResponse`
596
+
597
+ ```ts
598
+ interface ErrorResponse {
599
+ error: string; // Error type (e.g. "Bad Request")
600
+ message: string; // Detailed error message
601
+ status: number; // HTTP status code
602
+ timestamp: string; // ISO 8601
603
+ }
604
+ ```
605
+
606
+ ### `ClientConfig`
607
+
608
+ ```ts
609
+ interface ClientConfig {
610
+ baseUrl: string;
611
+ headers?: Record<string, string>;
612
+ fetch?: typeof fetch;
613
+ }
614
+ ```
74
615
 
75
616
  ## Error Handling
76
617
 
618
+ All API errors throw `MiotCalendarApiError`.
619
+
77
620
  ```ts
78
621
  import { MiotCalendarApiError } from "@microboxlabs/miot-calendar-client";
79
622
 
80
623
  try {
81
- await client.bookings.get("non-existent");
624
+ await client.bookings.create({
625
+ calendarId: "cal-123",
626
+ resource: { id: "truck-42", type: "vehicle", label: "Truck 42" },
627
+ slot: { date: "2026-03-01", hour: 10, minutes: 0 },
628
+ });
82
629
  } catch (error) {
83
630
  if (error instanceof MiotCalendarApiError) {
84
- console.log(error.status); // HTTP status code
85
- console.log(error.body); // ErrorResponse object or raw string
631
+ console.log(error.status); // HTTP status code (e.g. 409)
632
+ console.log(error.message); // Error message string
633
+ console.log(error.body); // ErrorResponse object or raw string
86
634
  }
87
635
  }
88
636
  ```
89
637
 
638
+ ### Error Codes
639
+
640
+ | Status | Meaning | Common Causes |
641
+ |--------|---------|---------------|
642
+ | `400` | Bad Request | Validation errors, slot generation range > 90 days, setting status to `FULL` manually |
643
+ | `404` | Not Found | Calendar, slot, or booking does not exist |
644
+ | `409` | Conflict | Slot is full (no available capacity for booking) |
645
+
646
+ ### Error Body
647
+
648
+ The `body` property on `MiotCalendarApiError` can be either:
649
+
650
+ - An **`ErrorResponse` object** with `error`, `message`, `status`, and `timestamp` fields — returned when the API sends a JSON error response.
651
+ - A **plain `string`** — returned as a fallback when the API response is not valid JSON (e.g. gateway errors, load balancer timeouts).
652
+
653
+ ```ts
654
+ if (error instanceof MiotCalendarApiError) {
655
+ if (typeof error.body === "string") {
656
+ // Non-JSON error (gateway, timeout, etc.)
657
+ console.log("Raw error:", error.body);
658
+ } else {
659
+ // Structured API error
660
+ console.log(error.body.error); // "Conflict"
661
+ console.log(error.body.message); // "Slot is full"
662
+ console.log(error.body.timestamp); // "2026-03-01T10:00:00Z"
663
+ }
664
+ }
665
+ ```
666
+
667
+ ## Custom Fetch
668
+
669
+ Pass a custom `fetch` implementation for server-side rendering, auth interceptors, or testing.
670
+
671
+ ### Auth interceptor
672
+
673
+ ```ts
674
+ const client = createMiotCalendarClient({
675
+ baseUrl: "https://your-api-host.com",
676
+ fetch: async (input, init) => {
677
+ const token = await getAccessToken(); // your auth logic
678
+ const headers = new Headers(init?.headers);
679
+ headers.set("Authorization", `Bearer ${token}`);
680
+ return globalThis.fetch(input, { ...init, headers });
681
+ },
682
+ });
683
+ ```
684
+
685
+ ### Testing with a mock fetch
686
+
687
+ ```ts
688
+ import { createMiotCalendarClient } from "@microboxlabs/miot-calendar-client";
689
+
690
+ const mockFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
691
+ return new Response(JSON.stringify({ id: "cal-1", code: "test", name: "Test" }), {
692
+ status: 200,
693
+ headers: { "Content-Type": "application/json" },
694
+ });
695
+ };
696
+
697
+ const client = createMiotCalendarClient({
698
+ baseUrl: "https://mock-api",
699
+ fetch: mockFetch,
700
+ });
701
+
702
+ const calendar = await client.calendars.get("cal-1");
703
+ ```
704
+
90
705
  ## License
91
706
 
92
707
  [Apache-2.0](./LICENSE)
package/dist/index.d.ts CHANGED
@@ -27,12 +27,28 @@ interface BookingListResponse {
27
27
  data: BookingResponse[];
28
28
  total: number;
29
29
  }
30
+ interface CalendarGroupRequest {
31
+ code: string;
32
+ name: string;
33
+ description?: string;
34
+ active?: boolean;
35
+ }
36
+ interface CalendarGroupResponse {
37
+ id: string;
38
+ code: string;
39
+ name: string;
40
+ description?: string;
41
+ active: boolean;
42
+ createdAt: string;
43
+ updatedAt: string;
44
+ }
30
45
  interface CalendarRequest {
31
46
  code: string;
32
47
  name: string;
33
48
  description?: string;
34
49
  timezone?: string;
35
50
  active?: boolean;
51
+ groups?: string[];
36
52
  }
37
53
  interface CalendarResponse {
38
54
  id: string;
@@ -43,6 +59,7 @@ interface CalendarResponse {
43
59
  active: boolean;
44
60
  createdAt: string;
45
61
  updatedAt: string;
62
+ groups?: CalendarGroupResponse[];
46
63
  }
47
64
  interface TimeWindowRequest {
48
65
  name: string;
@@ -130,6 +147,7 @@ declare function createMiotCalendarClient(config: ClientConfig): {
130
147
  calendars: {
131
148
  list(params?: {
132
149
  active?: boolean;
150
+ groupCode?: string;
133
151
  }): Promise<CalendarResponse[]>;
134
152
  get(id: string): Promise<CalendarResponse>;
135
153
  create(body: CalendarRequest): Promise<CalendarResponse>;
@@ -139,6 +157,15 @@ declare function createMiotCalendarClient(config: ClientConfig): {
139
157
  createTimeWindow(calendarId: string, body: TimeWindowRequest): Promise<TimeWindowResponse>;
140
158
  updateTimeWindow(calendarId: string, timeWindowId: string, body: TimeWindowRequest): Promise<TimeWindowResponse>;
141
159
  };
160
+ groups: {
161
+ list(params?: {
162
+ active?: boolean;
163
+ }): Promise<CalendarGroupResponse[]>;
164
+ get(id: string): Promise<CalendarGroupResponse>;
165
+ create(body: CalendarGroupRequest): Promise<CalendarGroupResponse>;
166
+ update(id: string, body: CalendarGroupRequest): Promise<CalendarGroupResponse>;
167
+ deactivate(id: string): Promise<void>;
168
+ };
142
169
  slots: {
143
170
  list(params: {
144
171
  calendarId: string;
@@ -159,4 +186,4 @@ declare class MiotCalendarApiError extends Error {
159
186
  constructor(status: number, body: ErrorResponse | string);
160
187
  }
161
188
 
162
- export { type BookingListResponse, type BookingRequest, type BookingResponse, type CalendarRequest, type CalendarResponse, type ClientConfig, type ErrorResponse, type GenerateSlotsRequest, type GenerateSlotsResponse, MiotCalendarApiError, type ResourceData, type SlotData, type SlotListResponse, type SlotResponse, type SlotStatus, type TimeWindowRequest, type TimeWindowResponse, type UpdateSlotStatusRequest, createMiotCalendarClient };
189
+ export { type BookingListResponse, type BookingRequest, type BookingResponse, type CalendarGroupRequest, type CalendarGroupResponse, type CalendarRequest, type CalendarResponse, type ClientConfig, type ErrorResponse, type GenerateSlotsRequest, type GenerateSlotsResponse, MiotCalendarApiError, type ResourceData, type SlotData, type SlotListResponse, type SlotResponse, type SlotStatus, type TimeWindowRequest, type TimeWindowResponse, type UpdateSlotStatusRequest, createMiotCalendarClient };
package/dist/index.js CHANGED
@@ -66,9 +66,9 @@ function createCalendarsApi(fetcher) {
66
66
  };
67
67
  }
68
68
 
69
- // src/resources/slots.ts
70
- var BASE3 = "/api/v1/miot-calendar/slots";
71
- function createSlotsApi(fetcher) {
69
+ // src/resources/groups.ts
70
+ var BASE3 = "/api/v1/miot-calendar/groups";
71
+ function createGroupsApi(fetcher) {
72
72
  return {
73
73
  list(params) {
74
74
  return fetcher("GET", BASE3, { query: params });
@@ -76,11 +76,33 @@ function createSlotsApi(fetcher) {
76
76
  get(id) {
77
77
  return fetcher("GET", `${BASE3}/${id}`);
78
78
  },
79
+ create(body) {
80
+ return fetcher("POST", BASE3, { body });
81
+ },
82
+ update(id, body) {
83
+ return fetcher("PUT", `${BASE3}/${id}`, { body });
84
+ },
85
+ deactivate(id) {
86
+ return fetcher("DELETE", `${BASE3}/${id}`);
87
+ }
88
+ };
89
+ }
90
+
91
+ // src/resources/slots.ts
92
+ var BASE4 = "/api/v1/miot-calendar/slots";
93
+ function createSlotsApi(fetcher) {
94
+ return {
95
+ list(params) {
96
+ return fetcher("GET", BASE4, { query: params });
97
+ },
98
+ get(id) {
99
+ return fetcher("GET", `${BASE4}/${id}`);
100
+ },
79
101
  generate(body) {
80
- return fetcher("POST", `${BASE3}/generate`, { body });
102
+ return fetcher("POST", `${BASE4}/generate`, { body });
81
103
  },
82
104
  updateStatus(id, body) {
83
- return fetcher("PATCH", `${BASE3}/${id}/status`, { body });
105
+ return fetcher("PATCH", `${BASE4}/${id}/status`, { body });
84
106
  }
85
107
  };
86
108
  }
@@ -130,6 +152,7 @@ function createMiotCalendarClient(config) {
130
152
  return {
131
153
  bookings: createBookingsApi(fetcher),
132
154
  calendars: createCalendarsApi(fetcher),
155
+ groups: createGroupsApi(fetcher),
133
156
  slots: createSlotsApi(fetcher)
134
157
  };
135
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microboxlabs/miot-calendar-client",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",