@blazeo.com/appointment-client 1.0.7 → 1.0.9

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.
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -40,9 +40,7 @@ function coerceMemberId(v) {
40
40
  * Canonical member id used in both `members[].id` and `openingHours[].member`.
41
41
  */
42
42
  function resolveParticipantMemberId(calPart) {
43
- const n = pick(calPart, "id", "Id");
44
- if (n != null && typeof n === "number" && !Number.isNaN(n))
45
- return n;
43
+ // Prefer the participantId GUID when available — it is the stable cross-endpoint identifier.
46
44
  const sid = pick(calPart, "participantId", "ParticipantId", "participant_id");
47
45
  if (sid != null && String(sid).trim() !== "") {
48
46
  const t = String(sid).trim();
@@ -50,6 +48,14 @@ function resolveParticipantMemberId(calPart) {
50
48
  return Number(t);
51
49
  return t;
52
50
  }
51
+ // Fall back to id — accepts both numeric and string (e.g. a GUID stored as id).
52
+ const n = pick(calPart, "id", "Id");
53
+ if (n != null) {
54
+ if (typeof n === "number" && !Number.isNaN(n))
55
+ return n;
56
+ if (typeof n === "string" && n.trim() !== "")
57
+ return n.trim();
58
+ }
53
59
  return "";
54
60
  }
55
61
  function dayOrderIndex(d) {
@@ -153,7 +153,8 @@ export async function fetchCalendarDetails(calendarId, options = {}) {
153
153
  const infoList = unwrapModelList(participantsInfoRaw);
154
154
  // Merge participantList and infoList to ensure we have all members
155
155
  const mergedParticipantsMap = new Map();
156
- const getAnyId = (obj) => obj.id ?? obj.Id ?? obj.participantId ?? obj.ParticipantId ?? obj.participant_id;
156
+ // Prefer the participantId GUID; fall back to numeric id.
157
+ const getAnyId = (obj) => obj.participantId ?? obj.ParticipantId ?? obj.participant_id ?? obj.id ?? obj.Id;
157
158
  // 1. Add from standard list
158
159
  participantList.forEach((p) => {
159
160
  const id = getAnyId(p);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Fetches all calendars for a company and populates each with its members (participants).
3
+ * Fetches both Participant List and Participant Info to ensure names and emails are included,
4
+ * while still skipping heavy data like opening hours.
5
+ */
6
+ export declare function getCalendarsByCompany(companyKey: string, connection?: {
7
+ baseUrl?: string;
8
+ consumer?: string;
9
+ }): Promise<any[]>;
@@ -0,0 +1,107 @@
1
+ import { CalendarModel, CalendarParticipantModel } from "@blazeo.com/calendar-client";
2
+ import { ensureBlazeoHttpReady } from "../config/ensureBlazeoHttpReady.js";
3
+ /**
4
+ * Fetches all calendars for a company and populates each with its members (participants).
5
+ * Fetches both Participant List and Participant Info to ensure names and emails are included,
6
+ * while still skipping heavy data like opening hours.
7
+ */
8
+ export async function getCalendarsByCompany(companyKey, connection = {}) {
9
+ const ready = ensureBlazeoHttpReady(connection);
10
+ if (!ready.ok) {
11
+ throw new Error(ready.error);
12
+ }
13
+ // 1. Get all calendars for the company
14
+ const result = await CalendarModel.getByCompany(companyKey);
15
+ const calendars = Array.isArray(result) ? result : result?.calendars ?? [];
16
+ if (!calendars || calendars.length === 0) {
17
+ return [];
18
+ }
19
+ // 2. Fetch lightweight members for each calendar in parallel
20
+ const enrichedCalendars = await Promise.all(calendars.map(async (cal) => {
21
+ const calendarId = cal.calendarId ?? String(cal.id ?? "");
22
+ if (!calendarId)
23
+ return null;
24
+ try {
25
+ // We need both List and Info to get the names/emails
26
+ const [partsRaw, infoRaw] = await Promise.all([
27
+ CalendarParticipantModel.getByCalendar(calendarId),
28
+ CalendarParticipantModel.getInfoByCalendar(calendarId)
29
+ ]);
30
+ const parts = Array.isArray(partsRaw) ? partsRaw : partsRaw?.participants ?? [];
31
+ const info = Array.isArray(infoRaw) ? infoRaw : infoRaw?.info ?? [];
32
+ // Merge logic to ensure names are matched to IDs
33
+ const membersMap = new Map();
34
+ // Use participantId GUID as the primary key
35
+ const getAnyId = (obj) => obj.participantId ?? obj.ParticipantId ?? obj.participant_id ?? obj.id ?? obj.Id;
36
+ // 1. Initialize with basic participant data
37
+ parts.forEach((p) => {
38
+ const mid = getAnyId(p);
39
+ if (mid) {
40
+ membersMap.set(String(mid).toLowerCase(), {
41
+ id: mid,
42
+ name: p.name ?? p.Name ?? "Member",
43
+ email: p.email ?? p.Email,
44
+ status: p.status ?? p.Status ?? 1,
45
+ uuId: mid
46
+ });
47
+ }
48
+ });
49
+ // 2. Enrich with detailed info (Name, Email, Alias)
50
+ info.forEach((i) => {
51
+ const mid = getAnyId(i);
52
+ if (!mid)
53
+ return;
54
+ const key = String(mid).toLowerCase();
55
+ const existing = membersMap.get(key);
56
+ const resolvedEmail = i.email ?? i.Email ?? i.userSsoEmail ?? i.UserSsoEmail ?? existing?.email;
57
+ const memberData = {
58
+ id: mid,
59
+ name: i.name ?? i.Name ?? i.alias ?? i.Alias ?? (existing?.name || "Member"),
60
+ email: resolvedEmail,
61
+ alias: i.alias ?? i.Alias ?? i.name ?? i.Name,
62
+ userSsoEmail: resolvedEmail,
63
+ uuId: mid,
64
+ status: i.status ?? i.Status ?? existing?.status ?? 1
65
+ };
66
+ if (!existing) {
67
+ membersMap.set(key, memberData);
68
+ }
69
+ else {
70
+ Object.assign(existing, memberData);
71
+ }
72
+ });
73
+ const members = Array.from(membersMap.values());
74
+ // Map to the EXACT schema requested by the user
75
+ return {
76
+ id: cal.id ?? cal.Id,
77
+ calendarLink: cal.calendarLink ?? cal.CalendarLink ?? "",
78
+ uuid: calendarId,
79
+ createdOn: cal.createdOn ?? cal.CreatedOn,
80
+ name: cal.name ?? cal.Name,
81
+ timeZoneId: cal.timeZoneId ?? cal.TimeZoneId,
82
+ description: cal.description ?? cal.Description ?? "",
83
+ assignmentType: cal.assignmentMethod ?? cal.AssignmentMethod ?? cal.assignmentType,
84
+ status: cal.status ?? cal.Status ?? 1,
85
+ location: cal.location ?? cal.Location ?? "",
86
+ members
87
+ };
88
+ }
89
+ catch (err) {
90
+ console.error(`[getCalendarsByCompany] Error fetching members for ${calendarId}:`, err);
91
+ return {
92
+ id: cal.id ?? cal.Id,
93
+ calendarLink: cal.calendarLink ?? cal.CalendarLink ?? "",
94
+ uuid: calendarId,
95
+ createdOn: cal.createdOn ?? cal.CreatedOn,
96
+ name: cal.name ?? cal.Name,
97
+ timeZoneId: cal.timeZoneId ?? cal.TimeZoneId,
98
+ description: cal.description ?? cal.Description ?? "",
99
+ assignmentType: cal.assignmentMethod ?? cal.AssignmentMethod ?? cal.assignmentType,
100
+ status: cal.status ?? cal.Status ?? 1,
101
+ location: cal.location ?? cal.Location ?? "",
102
+ members: []
103
+ };
104
+ }
105
+ }));
106
+ return enrichedCalendars.filter(c => c !== null);
107
+ }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { blazeoClientConfig } from "./config/blazeoClientDefaults.js";
6
6
  export { applyBlazeoClientConfig } from "./config/applyBlazeoDefaults.js";
7
7
  export { createCalendarRoot, CalendarRootModel, CalendarSlotModel, EventModel, ParticipantModel } from "./models/CalendarRootModel.js";
8
8
  export { fetchCalendarDetails, fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
9
+ export { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
9
10
  export { buildUnifiedCalendarView, type UnifiedCalendarMember, type UnifiedCalendarView, type UnifiedOpeningHourRow, type UnifiedParticipantWithHours, } from "./calendar/buildUnifiedCalendarView.js";
10
11
  export { fetchCalendarWithOpeningHours, unwrapCalendarGetData, pickOpeningHoursArrayFromCalendarPayload, normalizeParticipantOpeningHoursResponse } from "./calendar/fetchCalendarWithOpeningHours.js";
11
12
  export { getOpeningHours } from "./calendar/getOpeningHours.js";
@@ -18,7 +19,8 @@ export { CalendarCreation, createCalendarWithRelationsAsync, updateCalendarWithR
18
19
  export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./calendar/blazeoCalendarRelationMethods.js";
19
20
  export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
20
21
  export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
21
- export { CalendarModel, EventModel as CoreEventModel, ParticipantModel as CoreParticipantModel, CalendarParticipantModel, configure, getConfig } from "@blazeo.com/calendar-client";
22
+ import { CalendarModel as CoreCalendarModel, EventModel as CoreEventModel, ParticipantModel as CoreParticipantModel, CalendarParticipantModel as CoreCalendarParticipantModel, configure, getConfig } from "@blazeo.com/calendar-client";
23
+ export { CoreCalendarModel as CalendarModel, CoreEventModel as CoreEventModel, CoreParticipantModel as CoreParticipantModel, CoreCalendarParticipantModel as CalendarParticipantModel, configure, getConfig };
22
24
  export declare const packageName = "@blazeo.com/appointment-client";
23
25
  export declare class CalendarClient {
24
26
  name: string;
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { blazeoClientConfig } from "./config/blazeoClientDefaults.js";
6
6
  export { applyBlazeoClientConfig } from "./config/applyBlazeoDefaults.js";
7
7
  export { createCalendarRoot, CalendarRootModel, CalendarSlotModel, EventModel, ParticipantModel } from "./models/CalendarRootModel.js";
8
8
  export { fetchCalendarDetails, fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
9
+ export { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
9
10
  export { buildUnifiedCalendarView, } from "./calendar/buildUnifiedCalendarView.js";
10
11
  export { fetchCalendarWithOpeningHours, unwrapCalendarGetData, pickOpeningHoursArrayFromCalendarPayload, normalizeParticipantOpeningHoursResponse } from "./calendar/fetchCalendarWithOpeningHours.js";
11
12
  export { getOpeningHours } from "./calendar/getOpeningHours.js";
@@ -19,7 +20,16 @@ export { CalendarCreation, createCalendarWithRelationsAsync, updateCalendarWithR
19
20
  export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./calendar/blazeoCalendarRelationMethods.js";
20
21
  export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
21
22
  export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
22
- export { CalendarModel, EventModel as CoreEventModel, ParticipantModel as CoreParticipantModel, CalendarParticipantModel, configure, getConfig } from "@blazeo.com/calendar-client";
23
+ import { fetchCalendarDetails, fetchCalendarBundle } from "./calendar/fetchCalendarDetails.js";
24
+ import { fetchCalendarWithOpeningHours } from "./calendar/fetchCalendarWithOpeningHours.js";
25
+ import { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
26
+ import { CalendarModel as CoreCalendarModel, EventModel as CoreEventModel, ParticipantModel as CoreParticipantModel, CalendarParticipantModel as CoreCalendarParticipantModel, configure, getConfig } from "@blazeo.com/calendar-client";
27
+ // Attach new methods to CalendarModel for easier access
28
+ CoreCalendarModel.fetchCalendarDetails = fetchCalendarDetails;
29
+ CoreCalendarModel.fetchCalendarBundle = fetchCalendarBundle;
30
+ CoreCalendarModel.fetchCalendarWithOpeningHours = fetchCalendarWithOpeningHours;
31
+ CoreCalendarModel.getCalendarsByCompany = getCalendarsByCompany;
32
+ export { CoreCalendarModel as CalendarModel, CoreEventModel as CoreEventModel, CoreParticipantModel as CoreParticipantModel, CoreCalendarParticipantModel as CalendarParticipantModel, configure, getConfig };
23
33
  export const packageName = "@blazeo.com/appointment-client";
24
34
  export class CalendarClient {
25
35
  name = "CalendarClient";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazeo.com/appointment-client",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -6,6 +6,7 @@
6
6
  "": {
7
7
  "name": "appointment-client-sample",
8
8
  "dependencies": {
9
+ "@blazeo.com/appointment-client": "file:../blazeo.com-appointment-client-1.0.7.tgz",
9
10
  "appointment-client": "file:..",
10
11
  "react": "^19.0.0",
11
12
  "react-dom": "^19.0.0"
@@ -17,7 +18,7 @@
17
18
  },
18
19
  "..": {
19
20
  "name": "@blazeo.com/appointment-client",
20
- "version": "1.0.6",
21
+ "version": "1.0.7",
21
22
  "dependencies": {
22
23
  "@blazeo.com/calendar-client": "^1.0.18",
23
24
  "mobx": "^6.13.7",
@@ -319,6 +320,38 @@
319
320
  "node": ">=6.9.0"
320
321
  }
321
322
  },
323
+ "node_modules/@blazeo.com/appointment-client": {
324
+ "version": "1.0.7",
325
+ "resolved": "file:../blazeo.com-appointment-client-1.0.7.tgz",
326
+ "integrity": "sha512-b2E5TIdxvNJLH3oEvCQngDhzQ08iwXNolPcok3emAlGipO0f8iGDij5hnVUntt68c2Lr3IPz3cvr/BcUY6MofQ==",
327
+ "dependencies": {
328
+ "@blazeo.com/calendar-client": "^1.0.18",
329
+ "mobx": "^6.13.7",
330
+ "mobx-state-tree": "^7.0.2"
331
+ }
332
+ },
333
+ "node_modules/@blazeo.com/calendar-client": {
334
+ "version": "1.0.18",
335
+ "resolved": "https://registry.npmjs.org/@blazeo.com/calendar-client/-/calendar-client-1.0.18.tgz",
336
+ "integrity": "sha512-wHqOZKUAH4JDgsdwaktEG68H5YfArOW3LEouCXznIza4YqNrlkfdJEyqiwNf6KjY2nELi+s2tFM22/1MKaWf5Q==",
337
+ "license": "UNLICENSED",
338
+ "dependencies": {
339
+ "mobx": "^6.10.0",
340
+ "mobx-state-tree": "^5.4.0"
341
+ },
342
+ "engines": {
343
+ "node": ">=18"
344
+ }
345
+ },
346
+ "node_modules/@blazeo.com/calendar-client/node_modules/mobx-state-tree": {
347
+ "version": "5.4.2",
348
+ "resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-5.4.2.tgz",
349
+ "integrity": "sha512-SGXAh2KCBQbWVcxeQbZEr5pchTgcfNZmGVRL2a2Me+pSMH98bZWXD6EOuuijbTGbc0hOoOsbab3JdwJyr+fW7Q==",
350
+ "license": "MIT",
351
+ "peerDependencies": {
352
+ "mobx": "^6.3.0"
353
+ }
354
+ },
322
355
  "node_modules/@esbuild/aix-ppc64": {
323
356
  "version": "0.25.12",
324
357
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1425,6 +1458,28 @@
1425
1458
  "yallist": "^3.0.2"
1426
1459
  }
1427
1460
  },
1461
+ "node_modules/mobx": {
1462
+ "version": "6.15.3",
1463
+ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.15.3.tgz",
1464
+ "integrity": "sha512-6+ZSYDs5zgH5CdGfEU2q2Lsa5PztVryL1ys7kAImTU25n2A9LAMj/yneVsQpd03MfwMLDQF+7kakJR9Z+cQxSw==",
1465
+ "license": "MIT",
1466
+ "funding": {
1467
+ "type": "opencollective",
1468
+ "url": "https://opencollective.com/mobx"
1469
+ }
1470
+ },
1471
+ "node_modules/mobx-state-tree": {
1472
+ "version": "7.2.0",
1473
+ "resolved": "https://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-7.2.0.tgz",
1474
+ "integrity": "sha512-o46IsMI/lHITYCmkbQgNOCDT8M9Hd2WAgOlHt42hCg+XUZP21obF5ui5Vv9epIsQV8Wpp/GWNHu1YFhTx6iq0g==",
1475
+ "license": "MIT",
1476
+ "dependencies": {
1477
+ "ts-essentials": "^9.4.1"
1478
+ },
1479
+ "peerDependencies": {
1480
+ "mobx": "^6.3.0"
1481
+ }
1482
+ },
1428
1483
  "node_modules/ms": {
1429
1484
  "version": "2.1.3",
1430
1485
  "dev": true,
@@ -1598,6 +1653,20 @@
1598
1653
  "url": "https://github.com/sponsors/SuperchupuDev"
1599
1654
  }
1600
1655
  },
1656
+ "node_modules/ts-essentials": {
1657
+ "version": "9.4.2",
1658
+ "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.2.tgz",
1659
+ "integrity": "sha512-mB/cDhOvD7pg3YCLk2rOtejHjjdSi9in/IBYE13S+8WA5FBSraYf4V/ws55uvs0IvQ/l0wBOlXy5yBNZ9Bl8ZQ==",
1660
+ "license": "MIT",
1661
+ "peerDependencies": {
1662
+ "typescript": ">=4.1.0"
1663
+ },
1664
+ "peerDependenciesMeta": {
1665
+ "typescript": {
1666
+ "optional": true
1667
+ }
1668
+ }
1669
+ },
1601
1670
  "node_modules/update-browserslist-db": {
1602
1671
  "version": "1.2.3",
1603
1672
  "dev": true,
@@ -10,6 +10,7 @@
10
10
  "participants:info": "node ./scripts/getInfoByCalendar.mjs"
11
11
  },
12
12
  "dependencies": {
13
+ "@blazeo.com/appointment-client": "file:../blazeo.com-appointment-client-1.0.7.tgz",
13
14
  "appointment-client": "file:..",
14
15
  "react": "^19.0.0",
15
16
  "react-dom": "^19.0.0"
@@ -291,50 +291,18 @@ export function FetchCalendarTab() {
291
291
 
292
292
  setBusy(true);
293
293
  try {
294
- const byCompany = await CalendarModel.getByCompany(key);
295
- const list = byCompany?.calendars ?? byCompany;
296
- if (list == null || !Array.isArray(list) || list.length === 0) {
297
- setNote(
298
- "getByCompany returned null or an empty list. (calendar-client ≥1.0.17 returns { calendars, totalCount }.)"
299
- );
300
- setOutput(toDisplayJson(byCompany));
294
+ // Use the new optimized method that includes members automatically
295
+ const enriched = await CalendarModel.getCalendarsByCompany(key, {
296
+ baseUrl: effective.baseUrl,
297
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
298
+ });
299
+
300
+ if (!enriched || enriched.length === 0) {
301
+ setNote("getCalendarsByCompany returned an empty list.");
302
+ setOutput("[]");
301
303
  } else {
302
- const enriched = await Promise.all(
303
- list.map(async (c) => {
304
- const id = c.calendarId ?? String(c.id ?? "");
305
- if (!id) return { calendar: getSnapshot(c), openingHours: [], meta: { error: "no id" } };
306
- try {
307
- const b = await fetchCalendarDetails(id, {
308
- ...connectionOpts,
309
- baseUrl: effective.baseUrl,
310
- ...(effective.consumer ? { consumer: effective.consumer } : {}),
311
- });
312
- return {
313
- calendarView: b.calendarView,
314
- calendar: b.calendar ?? getSnapshot(c),
315
- openingHours: b.openingHours,
316
- participants: (b.participants ?? []).map((p) => (isStateTreeNode(p) ? getSnapshot(p) : p)),
317
- __openingHoursMeta: {
318
- fromCalendarGet: b.fromCalendarGet,
319
- fromParticipantApi: b.fromParticipantApi,
320
- calendarViewUsedAllParticipantOpeningHours: b.meta?.calendarViewUsedAllParticipantOpeningHours,
321
- },
322
- meta: b.meta,
323
- };
324
- } catch (err) {
325
- return {
326
- calendar: getSnapshot(c),
327
- openingHours: [],
328
- meta: {
329
- error: err instanceof Error ? err.message : String(err),
330
- },
331
- };
332
- }
333
- })
334
- );
335
- const total = byCompany?.totalCount ?? list.length;
336
304
  setNote(
337
- `Loaded ${list.length} calendar(s) (totalCount=${total}); opening hours (embed → participant API) + participants per calendar.`
305
+ `Loaded ${enriched.length} calendar(s) with members using optimized getCalendarsByCompany.`
338
306
  );
339
307
  setOutput(toDisplayJson(enriched));
340
308
  }
@@ -469,29 +437,29 @@ export function FetchCalendarTab() {
469
437
  <div className="card">
470
438
  <h2>Fetch calendar · calendarView</h2>
471
439
  <p className="muted small">
472
- Runs <code>fetchCalendarDetails(calendarId)</code>. JSON field <code>calendarView</code> is first: one object with
473
- calendar snapshot fields + <code>members</code> + <code>openingHours</code>, and a **new** <code>participants</code> array
474
- where each participant has their own <code>openingHours</code> nested inside.
440
+ Runs <code>fetchCalendarDetails(calendarId)</code>. The JSON shown below <strong>is</strong> the unified calendar view:
441
+ one object with calendar snapshot fields + <code>members</code> + <code>openingHours</code>, plus a{" "}
442
+ <code>participants</code> array where each participant may include nested <code>openingHours</code>. (
443
+ <code>fetchCalendarBundle(calendarId)</code> returns the same shape.)
475
444
  </p>
476
445
  <p className="muted small">
477
446
  Uses <code>fetchCalendarDetails</code>: legacy <code>openingHours</code> prefers embed on{" "}
478
447
  <code>CalendarModel.getRaw</code>, else <code>getParticipantOpeningHours</code>.{" "}
479
- <code>calendarView.openingHours</code> prefers <code>getAllParticipantOpeningHours</code> (
448
+ Unified <code>openingHours</code> prefers <code>getAllParticipantOpeningHours</code> (
480
449
  <code>GET /Calendar/Participant/OpeningHours/All/Get</code>) when the API returns rows. Members combine{" "}
481
450
  <code>CalendarModel.getParticipants</code> + <code>CalendarModel.getParticipantsInfo</code> (each member may
482
451
  include <code>participantInfo</code>).
483
452
  </p>
484
453
  <p className="muted small">
485
- <strong>Single object in code:</strong> <code>fetchCalendarBundle(calendarId)</code> after{" "}
486
- <code>initializeAppointmentClient(&#123; baseUrl, consumer &#125;)</code> same unified shape as{" "}
487
- <code>calendarView</code> below. This tab runs <code>fetchCalendarDetails</code> so extra arrays stay visible.
454
+ <strong>In code:</strong> <code>fetchCalendarBundle(calendarId)</code> (after{" "}
455
+ <code>initializeAppointmentClient(&#123; baseUrl, consumer &#125;)</code>) is an alias for the same unified fetch;
456
+ use either alongside explicit <code>baseUrl</code> / <code>consumer</code> options when needed.
488
457
  </p>
489
458
  <p className="muted small">
490
459
  <strong>DevTools Network:</strong> Each fetch fires <code>/Calendar/Get</code> <strong>twice</strong> (
491
460
  <code>CalendarModel.get</code> + <code>getRaw</code>). Other calls use different URLs — filter by{" "}
492
- <code>Participant</code>, <code>OpeningHours</code>, or <code>GetInfo</code>. Those power{" "}
493
- <code>calendarView</code>. If you only see <code>Calendar/Get</code> yet the UI JSON has members/hours,
494
- widen the Network filter (&quot;All&quot;) or disable search; if <code>calendarView</code> is empty/missing fields,
461
+ <code>Participant</code>, <code>OpeningHours</code>, or <code>GetInfo</code>. Those power the unified view. If you only see <code>Calendar/Get</code> yet the UI JSON has members/hours,
462
+ widen the Network filter (&quot;All&quot;) or disable search; if the unified object is empty/missing fields,
495
463
  check the <strong>Console</strong> for errors on the participant/opening-hours requests.
496
464
  </p>
497
465
  <p className="muted small">
@@ -567,11 +535,18 @@ export function FetchCalendarTab() {
567
535
  <div className="card">
568
536
  <h2>Fetch calendars by company</h2>
569
537
  <p className="muted small">
570
- Calls <code>CalendarModel.getByCompany</code> → <code>GET /Calendar/All</code>. If the UI shows{" "}
571
- <strong>Failed to fetch</strong> while Base URL points at Azure/production, that is usually{" "}
538
+ Step 1: <code>CalendarModel.getByCompany</code> → <code>GET /Calendar/All</code> (company key calendar
539
+ list). Step 2: for each calendar id, this tab runs the same{" "}
540
+ <code>fetchCalendarDetails(calendarId)</code> pipeline as <strong>Fetch calendar · calendarView</strong>, so
541
+ the JSON below is an <strong>array</strong> of unified objects (members, openingHours, participants — same
542
+ shape as a single fetch). If an id is missing from the list row, that entry falls back to the raw snapshot
543
+ only.
544
+ </p>
545
+ <p className="muted small">
546
+ If the UI shows <strong>Failed to fetch</strong> while Base URL points at Azure/production, that is usually{" "}
572
547
  <strong>CORS</strong>: enable proxy via <code>VITE_DEV_PROXY_TARGET</code> in{" "}
573
- <code>sample/.env.development</code> and Base URL <code>http://localhost:5173/blazeo-api</code>{" "}
574
- (restart dev server).
548
+ <code>sample/.env.development</code> and Base URL <code>http://localhost:5173/blazeo-api</code> (restart dev
549
+ server).
575
550
  </p>
576
551
  <form onSubmit={handleFetchByCompany} className="form">
577
552
  <label className="form__label">
@@ -37,14 +37,19 @@ function coerceMemberId(v: unknown): number | string | null {
37
37
  * Canonical member id used in both `members[].id` and `openingHours[].member`.
38
38
  */
39
39
  function resolveParticipantMemberId(calPart: Record<string, any>): number | string {
40
- const n = pick<number | null>(calPart, "id", "Id");
41
- if (n != null && typeof n === "number" && !Number.isNaN(n)) return n;
40
+ // Prefer the participantId GUID when available — it is the stable cross-endpoint identifier.
42
41
  const sid = pick<string>(calPart, "participantId", "ParticipantId", "participant_id");
43
42
  if (sid != null && String(sid).trim() !== "") {
44
43
  const t = String(sid).trim();
45
44
  if (/^\d+$/.test(t)) return Number(t);
46
45
  return t;
47
46
  }
47
+ // Fall back to id — accepts both numeric and string (e.g. a GUID stored as id).
48
+ const n = pick<number | string | null>(calPart, "id", "Id");
49
+ if (n != null) {
50
+ if (typeof n === "number" && !Number.isNaN(n)) return n;
51
+ if (typeof n === "string" && n.trim() !== "") return n.trim();
52
+ }
48
53
  return "";
49
54
  }
50
55
 
@@ -194,7 +194,9 @@ export async function fetchCalendarDetails(
194
194
  // Merge participantList and infoList to ensure we have all members
195
195
  const mergedParticipantsMap = new Map<string, any>();
196
196
 
197
- const getAnyId = (obj: any) => obj.id ?? obj.Id ?? obj.participantId ?? obj.ParticipantId ?? obj.participant_id;
197
+ // Prefer the participantId GUID; fall back to numeric id.
198
+ const getAnyId = (obj: any) =>
199
+ obj.participantId ?? obj.ParticipantId ?? obj.participant_id ?? obj.id ?? obj.Id;
198
200
 
199
201
  // 1. Add from standard list
200
202
  participantList.forEach((p: any) => {
@@ -0,0 +1,125 @@
1
+ import { CalendarModel, CalendarParticipantModel } from "@blazeo.com/calendar-client";
2
+ import { ensureBlazeoHttpReady } from "../config/ensureBlazeoHttpReady.js";
3
+
4
+ /**
5
+ * Fetches all calendars for a company and populates each with its members (participants).
6
+ * Fetches both Participant List and Participant Info to ensure names and emails are included,
7
+ * while still skipping heavy data like opening hours.
8
+ */
9
+ export async function getCalendarsByCompany(
10
+ companyKey: string,
11
+ connection: { baseUrl?: string; consumer?: string } = {}
12
+ ) {
13
+ const ready = ensureBlazeoHttpReady(connection);
14
+ if (!ready.ok) {
15
+ throw new Error(ready.error);
16
+ }
17
+
18
+ // 1. Get all calendars for the company
19
+ const result = await CalendarModel.getByCompany(companyKey);
20
+ const calendars = Array.isArray(result) ? result : (result as any)?.calendars ?? [];
21
+
22
+ if (!calendars || calendars.length === 0) {
23
+ return [];
24
+ }
25
+
26
+ // 2. Fetch lightweight members for each calendar in parallel
27
+ const enrichedCalendars = await Promise.all(
28
+ calendars.map(async (cal: any) => {
29
+ const calendarId = cal.calendarId ?? String(cal.id ?? "");
30
+ if (!calendarId) return null;
31
+
32
+ try {
33
+ // We need both List and Info to get the names/emails
34
+ const [partsRaw, infoRaw] = await Promise.all([
35
+ CalendarParticipantModel.getByCalendar(calendarId),
36
+ CalendarParticipantModel.getInfoByCalendar(calendarId)
37
+ ]);
38
+
39
+ const parts = Array.isArray(partsRaw) ? partsRaw : (partsRaw as any)?.participants ?? [];
40
+ const info = Array.isArray(infoRaw) ? infoRaw : (infoRaw as any)?.info ?? [];
41
+
42
+ // Merge logic to ensure names are matched to IDs
43
+ const membersMap = new Map<string, any>();
44
+
45
+ // Use participantId GUID as the primary key
46
+ const getAnyId = (obj: any) =>
47
+ obj.participantId ?? obj.ParticipantId ?? obj.participant_id ?? obj.id ?? obj.Id;
48
+
49
+ // 1. Initialize with basic participant data
50
+ parts.forEach((p: any) => {
51
+ const mid = getAnyId(p);
52
+ if (mid) {
53
+ membersMap.set(String(mid).toLowerCase(), {
54
+ id: mid,
55
+ name: p.name ?? p.Name ?? "Member",
56
+ email: p.email ?? p.Email,
57
+ status: p.status ?? p.Status ?? 1,
58
+ uuId: mid
59
+ });
60
+ }
61
+ });
62
+
63
+ // 2. Enrich with detailed info (Name, Email, Alias)
64
+ info.forEach((i: any) => {
65
+ const mid = getAnyId(i);
66
+ if (!mid) return;
67
+ const key = String(mid).toLowerCase();
68
+ const existing = membersMap.get(key);
69
+
70
+ const resolvedEmail = i.email ?? i.Email ?? i.userSsoEmail ?? i.UserSsoEmail ?? existing?.email;
71
+
72
+ const memberData = {
73
+ id: mid,
74
+ name: i.name ?? i.Name ?? i.alias ?? i.Alias ?? (existing?.name || "Member"),
75
+ email: resolvedEmail,
76
+ alias: i.alias ?? i.Alias ?? i.name ?? i.Name,
77
+ userSsoEmail: resolvedEmail,
78
+ uuId: mid,
79
+ status: i.status ?? i.Status ?? existing?.status ?? 1
80
+ };
81
+
82
+ if (!existing) {
83
+ membersMap.set(key, memberData);
84
+ } else {
85
+ Object.assign(existing, memberData);
86
+ }
87
+ });
88
+
89
+ const members = Array.from(membersMap.values());
90
+
91
+ // Map to the EXACT schema requested by the user
92
+ return {
93
+ id: cal.id ?? cal.Id,
94
+ calendarLink: cal.calendarLink ?? cal.CalendarLink ?? "",
95
+ uuid: calendarId,
96
+ createdOn: cal.createdOn ?? cal.CreatedOn,
97
+ name: cal.name ?? cal.Name,
98
+ timeZoneId: cal.timeZoneId ?? cal.TimeZoneId,
99
+ description: cal.description ?? cal.Description ?? "",
100
+ assignmentType: cal.assignmentMethod ?? cal.AssignmentMethod ?? cal.assignmentType,
101
+ status: cal.status ?? cal.Status ?? 1,
102
+ location: cal.location ?? cal.Location ?? "",
103
+ members
104
+ };
105
+ } catch (err) {
106
+ console.error(`[getCalendarsByCompany] Error fetching members for ${calendarId}:`, err);
107
+ return {
108
+ id: cal.id ?? cal.Id,
109
+ calendarLink: cal.calendarLink ?? cal.CalendarLink ?? "",
110
+ uuid: calendarId,
111
+ createdOn: cal.createdOn ?? cal.CreatedOn,
112
+ name: cal.name ?? cal.Name,
113
+ timeZoneId: cal.timeZoneId ?? cal.TimeZoneId,
114
+ description: cal.description ?? cal.Description ?? "",
115
+ assignmentType: cal.assignmentMethod ?? cal.AssignmentMethod ?? cal.assignmentType,
116
+ status: cal.status ?? cal.Status ?? 1,
117
+ location: cal.location ?? cal.Location ?? "",
118
+ members: []
119
+ };
120
+ }
121
+ })
122
+ );
123
+
124
+ return enrichedCalendars.filter(c => c !== null);
125
+ }
package/src/index.ts CHANGED
@@ -1,51 +1,72 @@
1
- import { getExampleSlots } from "./exampleData.js";
2
-
3
- export {
4
- initializeAppointmentClient,
5
- isAppointmentClientConfigured,
6
- } from "./config/initializeAppointmentClient.js";
7
- export { syncBlazeoConnection } from "./config/syncBlazeoConnection.js";
8
- export { ensureBlazeoHttpReady } from "./config/ensureBlazeoHttpReady.js";
9
- export type { EnsureBlazeoHttpOptions } from "./config/ensureBlazeoHttpReady.js";
10
- export { blazeoClientConfig } from "./config/blazeoClientDefaults.js";
11
- export { applyBlazeoClientConfig } from "./config/applyBlazeoDefaults.js";
12
- export { createCalendarRoot, CalendarRootModel, CalendarSlotModel, EventModel, ParticipantModel } from "./models/CalendarRootModel.js";
13
- export { fetchCalendarDetails, fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
14
- export {
15
- buildUnifiedCalendarView,
16
- type UnifiedCalendarMember,
17
- type UnifiedCalendarView,
18
- type UnifiedOpeningHourRow,
19
- type UnifiedParticipantWithHours,
20
- } from "./calendar/buildUnifiedCalendarView.js";
21
- export { fetchCalendarWithOpeningHours, unwrapCalendarGetData, pickOpeningHoursArrayFromCalendarPayload, normalizeParticipantOpeningHoursResponse } from "./calendar/fetchCalendarWithOpeningHours.js";
22
- export { getOpeningHours } from "./calendar/getOpeningHours.js";
23
- export { getParticipantOpeningHours } from "./calendar/getParticipantOpeningHours.js";
24
- export { getAllParticipantOpeningHours } from "./calendar/getAllParticipantOpeningHours.js";
25
- export { getParticipants } from "./calendar/getParticipants.js";
26
- export { getExampleSlots, getExampleEvents, getExampleParticipants, getExampleCalendarRoot, getExampleCalendarRootSnapshot } from "./exampleData.js";
27
-
28
- // Re-export core models from calendar-client for convenience
29
- export { createCalendarAsync, updateCalendarAsync, deleteCalendarAsync, resolveBlazeoConnection } from "./calendar/createCalendar.js";
30
- export { CalendarCreation, createCalendarWithRelationsAsync, updateCalendarWithRelationsAsync, resolveParticipantIdForOpeningHour } from "./calendar/calendarCreation.js";
31
- export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./calendar/blazeoCalendarRelationMethods.js";
32
- export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
33
- export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
34
-
35
- export {
36
- CalendarModel,
37
- EventModel as CoreEventModel,
38
- ParticipantModel as CoreParticipantModel,
39
- CalendarParticipantModel,
40
- configure,
41
- getConfig
42
- } from "@blazeo.com/calendar-client";
43
-
44
- export const packageName = "@blazeo.com/appointment-client";
45
-
46
- export class CalendarClient {
47
- name = "CalendarClient";
48
- getExampleSlots() {
49
- return getExampleSlots();
50
- }
51
- }
1
+ import { getExampleSlots } from "./exampleData.js";
2
+
3
+ export {
4
+ initializeAppointmentClient,
5
+ isAppointmentClientConfigured,
6
+ } from "./config/initializeAppointmentClient.js";
7
+ export { syncBlazeoConnection } from "./config/syncBlazeoConnection.js";
8
+ export { ensureBlazeoHttpReady } from "./config/ensureBlazeoHttpReady.js";
9
+ export type { EnsureBlazeoHttpOptions } from "./config/ensureBlazeoHttpReady.js";
10
+ export { blazeoClientConfig } from "./config/blazeoClientDefaults.js";
11
+ export { applyBlazeoClientConfig } from "./config/applyBlazeoDefaults.js";
12
+ export { createCalendarRoot, CalendarRootModel, CalendarSlotModel, EventModel, ParticipantModel } from "./models/CalendarRootModel.js";
13
+ export { fetchCalendarDetails, fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
14
+ export { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
15
+ export {
16
+ buildUnifiedCalendarView,
17
+ type UnifiedCalendarMember,
18
+ type UnifiedCalendarView,
19
+ type UnifiedOpeningHourRow,
20
+ type UnifiedParticipantWithHours,
21
+ } from "./calendar/buildUnifiedCalendarView.js";
22
+ export { fetchCalendarWithOpeningHours, unwrapCalendarGetData, pickOpeningHoursArrayFromCalendarPayload, normalizeParticipantOpeningHoursResponse } from "./calendar/fetchCalendarWithOpeningHours.js";
23
+ export { getOpeningHours } from "./calendar/getOpeningHours.js";
24
+ export { getParticipantOpeningHours } from "./calendar/getParticipantOpeningHours.js";
25
+ export { getAllParticipantOpeningHours } from "./calendar/getAllParticipantOpeningHours.js";
26
+ export { getParticipants } from "./calendar/getParticipants.js";
27
+ export { getExampleSlots, getExampleEvents, getExampleParticipants, getExampleCalendarRoot, getExampleCalendarRootSnapshot } from "./exampleData.js";
28
+
29
+ // Re-export core models from calendar-client for convenience
30
+ export { createCalendarAsync, updateCalendarAsync, deleteCalendarAsync, resolveBlazeoConnection } from "./calendar/createCalendar.js";
31
+ export { CalendarCreation, createCalendarWithRelationsAsync, updateCalendarWithRelationsAsync, resolveParticipantIdForOpeningHour } from "./calendar/calendarCreation.js";
32
+ export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./calendar/blazeoCalendarRelationMethods.js";
33
+ export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
34
+ export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
35
+
36
+ import { fetchCalendarDetails, fetchCalendarBundle } from "./calendar/fetchCalendarDetails.js";
37
+ import { fetchCalendarWithOpeningHours } from "./calendar/fetchCalendarWithOpeningHours.js";
38
+ import { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
39
+
40
+ import {
41
+ CalendarModel as CoreCalendarModel,
42
+ EventModel as CoreEventModel,
43
+ ParticipantModel as CoreParticipantModel,
44
+ CalendarParticipantModel as CoreCalendarParticipantModel,
45
+ configure,
46
+ getConfig
47
+ } from "@blazeo.com/calendar-client";
48
+
49
+ // Attach new methods to CalendarModel for easier access
50
+ (CoreCalendarModel as any).fetchCalendarDetails = fetchCalendarDetails;
51
+ (CoreCalendarModel as any).fetchCalendarBundle = fetchCalendarBundle;
52
+ (CoreCalendarModel as any).fetchCalendarWithOpeningHours = fetchCalendarWithOpeningHours;
53
+ (CoreCalendarModel as any).getCalendarsByCompany = getCalendarsByCompany;
54
+
55
+ export {
56
+ CoreCalendarModel as CalendarModel,
57
+ CoreEventModel as CoreEventModel,
58
+ CoreParticipantModel as CoreParticipantModel,
59
+ CoreCalendarParticipantModel as CalendarParticipantModel,
60
+ configure,
61
+ getConfig
62
+ };
63
+
64
+
65
+ export const packageName = "@blazeo.com/appointment-client";
66
+
67
+ export class CalendarClient {
68
+ name = "CalendarClient";
69
+ getExampleSlots() {
70
+ return getExampleSlots();
71
+ }
72
+ }