@blazeo.com/appointment-client 1.0.11 → 1.0.13

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.
@@ -224,9 +224,12 @@ export async function fetchCalendarDetails(calendarId, options = {}) {
224
224
  allParticipantOpeningHours.length > 0;
225
225
  if (!calendarView)
226
226
  return null;
227
+ // Use the mapper to normalize the final output, ensuring all fields like duration,
228
+ // bookingPageTitle, calendarId, etc. are correctly picked and named.
229
+ const finalView = mapToDesiredCalendarResponse(payload, calendarView.openingHours, calendarView.members);
227
230
  // Attach metadata as non-enumerable properties so they don't show up in JSON.stringify
228
231
  // but are still accessible for debugging if needed.
229
- Object.defineProperties(calendarView, {
232
+ Object.defineProperties(finalView, {
230
233
  _cal: { value: cal, enumerable: false },
231
234
  _participants: { value: participantList, enumerable: false },
232
235
  _openingHours: { value: openingHours, enumerable: false },
@@ -241,7 +244,7 @@ export async function fetchCalendarDetails(calendarId, options = {}) {
241
244
  enumerable: false
242
245
  },
243
246
  });
244
- return calendarView;
247
+ return finalView;
245
248
  }
246
249
  /**
247
250
  * Single return value only: unified calendar **`calendarView`** —
@@ -1,7 +1,7 @@
1
1
  /**
2
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.
3
+ * Uses a highly optimized single-request approach per calendar to ensure speed in list views.
4
+ * Results are normalized via mapToDesiredCalendarResponse.
5
5
  */
6
6
  export declare function getCalendarsByCompany(companyKey: string, connection?: {
7
7
  baseUrl?: string;
@@ -1,9 +1,10 @@
1
1
  import { CalendarModel, CalendarParticipantModel } from "@blazeo.com/calendar-client";
2
2
  import { ensureBlazeoHttpReady } from "../config/ensureBlazeoHttpReady.js";
3
+ import { mapToDesiredCalendarResponse } from "./mapToDesiredResponse.js";
3
4
  /**
4
5
  * 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.
6
+ * Uses a highly optimized single-request approach per calendar to ensure speed in list views.
7
+ * Results are normalized via mapToDesiredCalendarResponse.
7
8
  */
8
9
  export async function getCalendarsByCompany(companyKey, connection = {}) {
9
10
  const ready = ensureBlazeoHttpReady(connection);
@@ -22,86 +23,40 @@ export async function getCalendarsByCompany(companyKey, connection = {}) {
22
23
  if (!calendarId)
23
24
  return null;
24
25
  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 ?? [];
26
+ // Optimization: Use only getInfoByCalendar to get names/emails in a single request.
27
+ // This is much faster for a list view than fetching both list and info records.
28
+ const infoRaw = await CalendarParticipantModel.getInfoByCalendar(calendarId);
31
29
  const info = Array.isArray(infoRaw) ? infoRaw : infoRaw?.info ?? [];
32
30
  // Merge logic to ensure names are matched to IDs
33
31
  const membersMap = new Map();
34
32
  // Use participantId GUID as the primary key
35
33
  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)
34
+ // 1. Process info list (Name, Email, Alias)
50
35
  info.forEach((i) => {
51
36
  const mid = getAnyId(i);
52
37
  if (!mid)
53
38
  return;
54
39
  const key = String(mid).toLowerCase();
55
- const existing = membersMap.get(key);
56
- const resolvedEmail = i.email ?? i.Email ?? i.userSsoEmail ?? i.UserSsoEmail ?? existing?.email;
40
+ const resolvedEmail = i.email ?? i.Email ?? i.userSsoEmail ?? i.UserSsoEmail;
57
41
  const memberData = {
58
42
  id: mid,
59
- name: i.name ?? i.Name ?? i.alias ?? i.Alias ?? (existing?.name || "Member"),
43
+ name: i.name ?? i.Name ?? i.alias ?? i.Alias ?? "Member",
60
44
  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
45
+ status: i.status ?? i.Status ?? 1,
65
46
  };
66
- if (!existing) {
47
+ if (!membersMap.has(key)) {
67
48
  membersMap.set(key, memberData);
68
49
  }
69
- else {
70
- Object.assign(existing, memberData);
71
- }
72
50
  });
73
51
  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
- };
52
+ // Use the unified mapper to ensure all properties (duration, calendarId, etc.) are included
53
+ return mapToDesiredCalendarResponse(cal, [], members);
88
54
  }
89
55
  catch (err) {
90
56
  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
- };
57
+ // Fallback to minimal mapping if enrichment fails
58
+ return mapToDesiredCalendarResponse(cal, [], []);
104
59
  }
105
60
  }));
106
- return enrichedCalendars.filter(c => c !== null);
61
+ return enrichedCalendars.filter(Boolean);
107
62
  }
@@ -15,6 +15,7 @@ export declare function mapToDesiredCalendarResponse(payload: any, openingHours?
15
15
  bufferTimeUnit: number | null;
16
16
  calendarLink: any;
17
17
  uuid: any;
18
+ calendarId: any;
18
19
  location: any;
19
20
  bookingPageTitle: any;
20
21
  reminderChannelStatuses: {
@@ -49,22 +50,7 @@ export declare function mapToDesiredCalendarResponse(payload: any, openingHours?
49
50
  logoUrl: any;
50
51
  __typename: string;
51
52
  } | null;
52
- openingHours: {
53
- id: any;
54
- createdOn: any;
55
- modifiedOn: any;
56
- member: any;
57
- openingHourId: any;
58
- calendarId: any;
59
- participantId: any;
60
- days: any;
61
- startHour: any;
62
- startMinute: any;
63
- endHour: any;
64
- endMinute: any;
65
- off: boolean;
66
- __typename: string;
67
- }[];
53
+ openingHours: any[];
68
54
  appointmentUserDefinedFields: any;
69
55
  __typename: string;
70
56
  } | null;
@@ -22,22 +22,32 @@ export function mapToDesiredCalendarResponse(payload, openingHours = [], members
22
22
  __typename: "Member"
23
23
  }));
24
24
  // Map opening hours with typename and raw fields
25
- const mappedOpeningHours = openingHours.map(oh => ({
26
- id: pick(oh, "id", "Id") ?? 0,
27
- createdOn: pick(oh, "createdOn", "CreatedOn", "created_on") ?? "0001-01-01T00:00:00.000Z",
28
- modifiedOn: pick(oh, "modifiedOn", "ModifiedOn", "modified_on") ?? "0001-01-01T00:00:00.000Z",
29
- member: pick(oh, "member", "Member"),
30
- openingHourId: pick(oh, "openingHourId", "OpeningHourId", "opening_hour_id") ?? "",
31
- calendarId: pick(oh, "calendarId", "CalendarId", "calendar_id") ?? "",
32
- participantId: pick(oh, "participantId", "ParticipantId", "participant_id") ?? "",
33
- days: oh.days ?? [],
34
- startHour: oh.startHour ?? pick(oh, "startHour", "StartHour") ?? 0,
35
- startMinute: oh.startMinute ?? pick(oh, "startMinute", "StartMinute") ?? 0,
36
- endHour: oh.endHour ?? pick(oh, "endHour", "EndHour") ?? 0,
37
- endMinute: oh.endMinute ?? pick(oh, "endMinute", "EndMinute") ?? 0,
38
- off: !!(oh.off ?? pick(oh, "off", "Off")),
39
- __typename: "OpeningHour"
40
- }));
25
+ const mappedOpeningHours = openingHours.map(oh => {
26
+ // If it's already a unified object (has day/start/end), preserve it but ensure __typename
27
+ if (oh.day !== undefined && oh.start !== undefined && oh.end !== undefined) {
28
+ return {
29
+ ...oh,
30
+ __typename: "OpeningHour"
31
+ };
32
+ }
33
+ // Otherwise, map from raw PascalCase or camelCase
34
+ return {
35
+ id: pick(oh, "id", "Id") ?? 0,
36
+ createdOn: pick(oh, "createdOn", "CreatedOn", "created_on") ?? "0001-01-01T00:00:00.000Z",
37
+ modifiedOn: pick(oh, "modifiedOn", "ModifiedOn", "modified_on") ?? "0001-01-01T00:00:00.000Z",
38
+ member: pick(oh, "member", "Member"),
39
+ openingHourId: pick(oh, "openingHourId", "OpeningHourId", "opening_hour_id") ?? "",
40
+ calendarId: pick(oh, "calendarId", "CalendarId", "calendar_id") ?? "",
41
+ participantId: pick(oh, "participantId", "ParticipantId", "participant_id") ?? "",
42
+ days: oh.days ?? [],
43
+ startHour: oh.startHour ?? pick(oh, "startHour", "StartHour") ?? 0,
44
+ startMinute: oh.startMinute ?? pick(oh, "startMinute", "StartMinute") ?? 0,
45
+ endHour: oh.endHour ?? pick(oh, "endHour", "EndHour") ?? 0,
46
+ endMinute: oh.endMinute ?? pick(oh, "endMinute", "EndMinute") ?? 0,
47
+ off: !!(oh.off ?? pick(oh, "off", "Off")),
48
+ __typename: "OpeningHour"
49
+ };
50
+ });
41
51
  // Map theme
42
52
  const rawTheme = pick(payload, "theme", "Theme");
43
53
  const theme = rawTheme ? {
@@ -63,6 +73,7 @@ export function mapToDesiredCalendarResponse(payload, openingHours = [], members
63
73
  })),
64
74
  __typename: "ReminderChannelStatus"
65
75
  })) : [];
76
+ const uuid = pick(payload, "uuid", "Uuid", "calendarId", "CalendarId");
66
77
  return {
67
78
  id: n(pick(payload, "id", "Id")),
68
79
  durationUnit: n(pick(payload, "durationUnit", "DurationUnit")),
@@ -75,7 +86,8 @@ export function mapToDesiredCalendarResponse(payload, openingHours = [], members
75
86
  bufferTime: n(pick(payload, "bufferTime", "BufferTime")),
76
87
  bufferTimeUnit: n(pick(payload, "bufferTimeUnit", "BufferTimeUnit")),
77
88
  calendarLink: pick(payload, "calendarLink", "CalendarLink"),
78
- uuid: pick(payload, "uuid", "Uuid", "calendarId", "CalendarId"),
89
+ uuid: uuid,
90
+ calendarId: uuid, // Explicit alias requested by user
79
91
  location: pick(payload, "location", "Location") ?? "",
80
92
  bookingPageTitle: pick(payload, "bookingPageTitle", "BookingPageTitle") ?? null,
81
93
  reminderChannelStatuses,
package/dist/index.d.ts CHANGED
@@ -5,8 +5,7 @@ export type { EnsureBlazeoHttpOptions } from "./config/ensureBlazeoHttpReady.js"
5
5
  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
- export { fetchCalendarDetails, fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
9
- export { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
8
+ export { fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
10
9
  export { buildUnifiedCalendarView, type UnifiedCalendarMember, type UnifiedCalendarView, type UnifiedOpeningHourRow, type UnifiedParticipantWithHours, } from "./calendar/buildUnifiedCalendarView.js";
11
10
  export { fetchCalendarWithOpeningHours, unwrapCalendarGetData, pickOpeningHoursArrayFromCalendarPayload, normalizeParticipantOpeningHoursResponse } from "./calendar/fetchCalendarWithOpeningHours.js";
12
11
  export { getOpeningHours } from "./calendar/getOpeningHours.js";
@@ -19,8 +18,48 @@ export { CalendarCreation, createCalendarWithRelationsAsync, updateCalendarWithR
19
18
  export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./calendar/blazeoCalendarRelationMethods.js";
20
19
  export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
21
20
  export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
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 };
21
+ import { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
22
+ import { fetchCalendarDetails } from "./calendar/fetchCalendarDetails.js";
23
+ export { getCalendarsByCompany, fetchCalendarDetails };
24
+ import { EventModel as CoreEventModel, ParticipantModel as CoreParticipantModel, CalendarParticipantModel as CoreCalendarParticipantModel, configure, getConfig } from "@blazeo.com/calendar-client";
25
+ export declare const CalendarModel: {
26
+ getCalendarsByCompany: typeof getCalendarsByCompany;
27
+ fetchCalendarDetails: typeof fetchCalendarDetails;
28
+ get(calendarId: string): Promise<unknown>;
29
+ getRaw(calendarId: string): Promise<{
30
+ status: string;
31
+ data?: unknown;
32
+ message?: string;
33
+ }>;
34
+ getByCompany(companyKey: string, opts?: {
35
+ skip?: number;
36
+ take?: number;
37
+ sortBy?: string;
38
+ sortOrder?: "ASC" | "DESC" | "asc" | "desc" | string;
39
+ sort?: string;
40
+ sort_column?: string;
41
+ sort_dir?: "asc" | "desc" | string;
42
+ page?: number;
43
+ page_size?: number;
44
+ }): Promise<{
45
+ calendars: unknown[];
46
+ totalCount: number;
47
+ } | null>;
48
+ getTimeZones(): Promise<unknown>;
49
+ getTimeZone(timezoneId: string): Promise<unknown>;
50
+ getParticipants(calendarId: string): Promise<unknown>;
51
+ getAllParticipantOpeningHours(calendarId: string): Promise<unknown[] | null>;
52
+ getCalendarParticipant(calendarId: string): Promise<unknown>;
53
+ getParticipantsInfo(calendarId: string): Promise<unknown>;
54
+ getMonth(calendarId: string, year: number, month: number): Promise<unknown>;
55
+ getEvents(calendarId: string): Promise<unknown>;
56
+ createWithParticipants(name: string, companyKey: string, participantIds: string[], description: string, calendarId?: string): Promise<unknown>;
57
+ editWithParticipants(calendarId: string, name: string, participantIds: string[], description: string): Promise<unknown>;
58
+ create(snapshot: object, options?: {
59
+ env?: object;
60
+ }): unknown;
61
+ };
62
+ export { CoreEventModel as CoreEventModel, CoreParticipantModel as CoreParticipantModel, CoreCalendarParticipantModel as CalendarParticipantModel, configure, getConfig };
24
63
  export declare const packageName = "@blazeo.com/appointment-client";
25
64
  export declare class CalendarClient {
26
65
  name: string;
package/dist/index.js CHANGED
@@ -5,8 +5,7 @@ export { ensureBlazeoHttpReady } from "./config/ensureBlazeoHttpReady.js";
5
5
  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
- export { fetchCalendarDetails, fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
9
- export { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
8
+ export { fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
10
9
  export { buildUnifiedCalendarView, } from "./calendar/buildUnifiedCalendarView.js";
11
10
  export { fetchCalendarWithOpeningHours, unwrapCalendarGetData, pickOpeningHoursArrayFromCalendarPayload, normalizeParticipantOpeningHoursResponse } from "./calendar/fetchCalendarWithOpeningHours.js";
12
11
  export { getOpeningHours } from "./calendar/getOpeningHours.js";
@@ -20,16 +19,17 @@ export { CalendarCreation, createCalendarWithRelationsAsync, updateCalendarWithR
20
19
  export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./calendar/blazeoCalendarRelationMethods.js";
21
20
  export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
22
21
  export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
23
- import { fetchCalendarDetails, fetchCalendarBundle } from "./calendar/fetchCalendarDetails.js";
24
- import { fetchCalendarWithOpeningHours } from "./calendar/fetchCalendarWithOpeningHours.js";
25
22
  import { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
23
+ import { fetchCalendarDetails } from "./calendar/fetchCalendarDetails.js";
24
+ export { getCalendarsByCompany, fetchCalendarDetails };
26
25
  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 };
26
+ // Enriched CalendarModel
27
+ export const CalendarModel = {
28
+ ...CoreCalendarModel,
29
+ getCalendarsByCompany,
30
+ fetchCalendarDetails
31
+ };
32
+ export { CoreEventModel as CoreEventModel, CoreParticipantModel as CoreParticipantModel, CoreCalendarParticipantModel as CalendarParticipantModel, configure, getConfig };
33
33
  export const packageName = "@blazeo.com/appointment-client";
34
34
  export class CalendarClient {
35
35
  name = "CalendarClient";
@@ -0,0 +1,56 @@
1
+ export type BlazeoLeadConnection = {
2
+ baseUrl?: string;
3
+ consumer?: string;
4
+ };
5
+ /** Paging / sort options forwarded to `LeadModel.getByCompany` → `GET /lead/company/get`. */
6
+ export type LeadsByCompanyListOpts = {
7
+ skip?: number;
8
+ take?: number;
9
+ sortBy?: string;
10
+ sortOrder?: "ASC" | "DESC" | "asc" | "desc" | string;
11
+ sort?: string;
12
+ sort_column?: string;
13
+ sort_dir?: "asc" | "desc" | string;
14
+ page?: number;
15
+ page_size?: number;
16
+ searchColumn?: string;
17
+ search_column?: string;
18
+ searchText?: string;
19
+ search_text?: string;
20
+ search?: string;
21
+ };
22
+ /**
23
+ * Lead by id: `LeadModel.getRaw` / `get` → `GET /lead/get?lead_id=…`.
24
+ * Returns the mapped MST snapshot when successful, plus the raw API envelope from `getRaw`.
25
+ */
26
+ export declare function fetchLeadDetails(leadId: string, connection?: BlazeoLeadConnection): Promise<{
27
+ ok: true;
28
+ lead: Record<string, unknown> | null;
29
+ rawGet: unknown;
30
+ } | {
31
+ ok: false;
32
+ reason: "missing_base_url";
33
+ detail: string;
34
+ }>;
35
+ /**
36
+ * Single lead by email + company: `GET /lead/getbyemail`.
37
+ */
38
+ export declare function fetchLeadByEmail(email: string, companyKey: string, connection?: BlazeoLeadConnection): Promise<{
39
+ ok: true;
40
+ lead: Record<string, unknown> | null;
41
+ } | {
42
+ ok: false;
43
+ reason: "missing_base_url";
44
+ detail: string;
45
+ }>;
46
+ /**
47
+ * Paged list: `LeadModel.getByCompany` → `GET /lead/company/get`.
48
+ */
49
+ export declare function fetchLeadsByCompany(companyKey: string, listOpts?: LeadsByCompanyListOpts, connection?: BlazeoLeadConnection): Promise<{
50
+ ok: true;
51
+ leads: Record<string, unknown>[];
52
+ } | {
53
+ ok: false;
54
+ reason: "missing_base_url";
55
+ detail: string;
56
+ }>;
@@ -0,0 +1,61 @@
1
+ import { LeadModel } from "@blazeo.com/calendar-client";
2
+ import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
3
+ import { ensureBlazeoHttpReady } from "../config/ensureBlazeoHttpReady.js";
4
+ function leadToPlain(lead) {
5
+ if (lead == null)
6
+ return null;
7
+ if (isStateTreeNode(lead)) {
8
+ return getSnapshot(lead);
9
+ }
10
+ if (typeof lead === "object")
11
+ return lead;
12
+ return null;
13
+ }
14
+ function leadsToPlain(list) {
15
+ if (!Array.isArray(list))
16
+ return [];
17
+ return list.map((x) => leadToPlain(x)).filter((x) => x != null);
18
+ }
19
+ /**
20
+ * Lead by id: `LeadModel.getRaw` / `get` → `GET /lead/get?lead_id=…`.
21
+ * Returns the mapped MST snapshot when successful, plus the raw API envelope from `getRaw`.
22
+ */
23
+ export async function fetchLeadDetails(leadId, connection = {}) {
24
+ const ready = ensureBlazeoHttpReady(connection);
25
+ if (!ready.ok) {
26
+ return { ok: false, reason: "missing_base_url", detail: ready.error };
27
+ }
28
+ const id = String(leadId ?? "").trim();
29
+ if (!id) {
30
+ return {
31
+ ok: true,
32
+ lead: null,
33
+ rawGet: { status: "failure", message: "leadId is empty" },
34
+ };
35
+ }
36
+ const rawGet = await LeadModel.getRaw(id);
37
+ const model = await LeadModel.get(id);
38
+ return { ok: true, lead: leadToPlain(model), rawGet };
39
+ }
40
+ /**
41
+ * Single lead by email + company: `GET /lead/getbyemail`.
42
+ */
43
+ export async function fetchLeadByEmail(email, companyKey, connection = {}) {
44
+ const ready = ensureBlazeoHttpReady(connection);
45
+ if (!ready.ok) {
46
+ return { ok: false, reason: "missing_base_url", detail: ready.error };
47
+ }
48
+ const model = await LeadModel.getByEmail(String(email).trim(), String(companyKey).trim());
49
+ return { ok: true, lead: leadToPlain(model) };
50
+ }
51
+ /**
52
+ * Paged list: `LeadModel.getByCompany` → `GET /lead/company/get`.
53
+ */
54
+ export async function fetchLeadsByCompany(companyKey, listOpts = {}, connection = {}) {
55
+ const ready = ensureBlazeoHttpReady(connection);
56
+ if (!ready.ok) {
57
+ return { ok: false, reason: "missing_base_url", detail: ready.error };
58
+ }
59
+ const models = await LeadModel.getByCompany(String(companyKey).trim(), listOpts);
60
+ return { ok: true, leads: leadsToPlain(models ?? []) };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazeo.com/appointment-client",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,9 +1,9 @@
1
1
  import { useMemo, useState } from "react";
2
2
  import {
3
+ CalendarCreation,
3
4
  CalendarModel,
4
5
  deleteCalendarAsync,
5
6
  ensureBlazeoHttpReady,
6
- fetchCalendarDetails,
7
7
  updateCalendarAsync,
8
8
  } from "appointment-client";
9
9
  import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
@@ -229,40 +229,24 @@ export function FetchCalendarTab() {
229
229
 
230
230
  setBusy(true);
231
231
  try {
232
- const details = await fetchCalendarDetails(id, {
232
+ const result = await CalendarModel.fetchCalendarDetails(id, {
233
233
  ...connectionOpts,
234
234
  baseUrl: effective.baseUrl,
235
235
  ...(effective.consumer ? { consumer: effective.consumer } : {}),
236
236
  });
237
237
 
238
- const meta = details?._meta ?? details?.meta;
239
- if (meta && !meta.ok && meta.reason === "missing_base_url") {
240
- setError(mapBlazeoDemoError(meta.detail ?? ""));
241
- return;
242
- }
243
-
244
- if (!details) {
245
- ensureBlazeoHttpReady({
246
- baseUrl: effective.baseUrl,
247
- ...(effective.consumer ? { consumer: effective.consumer } : {}),
248
- });
249
- const raw = await CalendarModel.getRaw(id);
250
- setNote("CalendarModel.get returned null. Showing CalendarModel.getRaw only.");
251
- setOutput(toDisplayJson(raw));
252
- return;
253
- }
254
-
255
- const snap = details._cal ? getSnapshot(details._cal) : null;
256
- if (snap) {
257
- setLastFetchUpdatePayload(JSON.stringify(calendarSnapshotToUpdatePayload(snap), null, 2));
258
- }
259
-
260
- // If it's the new flat response, just use it directly for output.
261
- setOutput(toDisplayJson(details));
262
-
263
- // We can still try to extract meta for the UI if needed
264
- if (meta) {
265
- setNote(`Source: ${meta.calendarViewUsedAllParticipantOpeningHours ? "AllParticipantOpeningHours" : "Embedded/ParticipantApi"}`);
238
+ if (result) {
239
+ setOutput(toDisplayJson(result));
240
+ setNote(`CalendarModel.fetchCalendarDetails Enriched Unified View (ID: ${id})`);
241
+ setUpdateJson(toDisplayJson(result));
242
+
243
+ const snap = result._cal ? getSnapshot(result._cal) : null;
244
+ if (snap) {
245
+ setLastFetchUpdatePayload(JSON.stringify(calendarSnapshotToUpdatePayload(snap), null, 2));
246
+ }
247
+ } else {
248
+ setNote(`Calendar not found or error fetching details for ID: ${id}`);
249
+ setOutput("{}");
266
250
  }
267
251
  } catch (err) {
268
252
  setError(explainFetchFailure(err, effective.baseUrl));
@@ -360,17 +344,19 @@ export function FetchCalendarTab() {
360
344
  }
361
345
  setBusy(true);
362
346
  try {
363
- const result = await updateCalendarAsync(payload, {
347
+ const result = await CalendarCreation.updateWithRelationsAsync(payload, {
364
348
  ...connectionOpts,
365
349
  baseUrl: effective.baseUrl,
366
350
  ...(effective.consumer ? { consumer: effective.consumer } : {}),
367
351
  });
368
352
  if (result.ok) {
369
- setMutateNote("updateCalendarAsync → POST /Calendar/Event/Update");
353
+ setMutateNote("updateWithRelationsAsync → POST /Calendar/Event/Update + Batch Opening Hours");
370
354
  setMutateOutput(
371
355
  JSON.stringify(
372
356
  {
373
- snapshot: getSnapshot(result.calendar),
357
+ snapshot: result.calendar ? getSnapshot(result.calendar) : null,
358
+ membersAdded: result.membersAdded,
359
+ openingHoursSaved: result.openingHoursSaved,
374
360
  apiResponse: result.apiResponse ?? null,
375
361
  },
376
362
  null,
@@ -0,0 +1,335 @@
1
+ import { useState } from "react";
2
+ import {
3
+ ensureBlazeoHttpReady,
4
+ fetchLeadByEmail,
5
+ fetchLeadDetails,
6
+ fetchLeadsByCompany,
7
+ } from "appointment-client";
8
+ import {
9
+ configureBlazeoFromEffective,
10
+ useBlazeoConnection,
11
+ } from "./BlazeoConnectionSettings.jsx";
12
+ import { mapBlazeoDemoError } from "./blazeoDemoError.js";
13
+
14
+ /** Browser `fetch` often surfaces blocked requests as TypeError "Failed to fetch" (e.g. CORS). */
15
+ function explainFetchFailure(err, configuredBaseUrl) {
16
+ const msg = err instanceof Error ? err.message : String(err);
17
+ const isNetwork =
18
+ msg === "Failed to fetch" ||
19
+ msg === "Load failed" ||
20
+ (err instanceof TypeError && (/fetch/i.test(msg) || /network/i.test(msg)));
21
+ if (!isNetwork) return mapBlazeoDemoError(msg);
22
+
23
+ const isRemote =
24
+ configuredBaseUrl &&
25
+ /^https?:\/\//i.test(configuredBaseUrl) &&
26
+ !/localhost|127\.0\.0\.1/i.test(configuredBaseUrl);
27
+
28
+ const proxyHint =
29
+ "Dev workaround (Vite proxy): create sample/.env.development with\n" +
30
+ " VITE_DEV_PROXY_TARGET=https://YOUR_API_ORIGIN\n" +
31
+ "restart npm run dev, then set Base URL to:\n" +
32
+ " http://localhost:5173/blazeo-api\n" +
33
+ "Consumer header stays the same.";
34
+
35
+ if (isRemote) {
36
+ return `${msg}\n\nLikely CORS / blocked browser cross-origin request.\nFix API CORS for http://localhost:5173, OR:\n${proxyHint}`;
37
+ }
38
+ return `${msg}\n\n${proxyHint}`;
39
+ }
40
+
41
+ function toDisplayJson(value) {
42
+ try {
43
+ return JSON.stringify(value, null, 2);
44
+ } catch {
45
+ return String(value);
46
+ }
47
+ }
48
+
49
+ export function LeadTab() {
50
+ const { effective, connectionOpts } = useBlazeoConnection();
51
+ const [leadId, setLeadId] = useState("");
52
+ const [email, setEmail] = useState("");
53
+ const [companyKey, setCompanyKey] = useState("");
54
+ const [listCompanyKey, setListCompanyKey] = useState("");
55
+ const [skip, setSkip] = useState("");
56
+ const [take, setTake] = useState("");
57
+ const [busy, setBusy] = useState(false);
58
+ const [error, setError] = useState("");
59
+ const [note, setNote] = useState("");
60
+ const [output, setOutput] = useState("");
61
+
62
+ function ensureBaseConfigured() {
63
+ if (!effective.baseUrl) {
64
+ setError("Set Base URL in the connection card above or in `blazeoClientDefaults.ts`.");
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+
70
+ async function handleFetchById(e) {
71
+ e.preventDefault();
72
+ setError("");
73
+ setNote("");
74
+ setOutput("");
75
+ const id = leadId.trim();
76
+ if (!id) {
77
+ setError("Enter a lead id.");
78
+ return;
79
+ }
80
+ if (!ensureBaseConfigured()) return;
81
+ configureBlazeoFromEffective(effective);
82
+ ensureBlazeoHttpReady({
83
+ baseUrl: effective.baseUrl,
84
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
85
+ });
86
+
87
+ setBusy(true);
88
+ try {
89
+ const res = await fetchLeadDetails(id, {
90
+ ...connectionOpts,
91
+ baseUrl: effective.baseUrl,
92
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
93
+ });
94
+ if (!res.ok) {
95
+ setError(mapBlazeoDemoError(res.detail ?? ""));
96
+ return;
97
+ }
98
+ setNote("LeadModel.get + getRaw → GET /lead/get (lead mapped snapshot + raw envelope).");
99
+ setOutput(toDisplayJson({ lead: res.lead, rawGet: res.rawGet }));
100
+ } catch (err) {
101
+ setError(explainFetchFailure(err, effective.baseUrl));
102
+ } finally {
103
+ setBusy(false);
104
+ }
105
+ }
106
+
107
+ async function handleFetchByEmail(e) {
108
+ e.preventDefault();
109
+ setError("");
110
+ setNote("");
111
+ setOutput("");
112
+ const em = email.trim();
113
+ const ck = companyKey.trim();
114
+ if (!em || !ck) {
115
+ setError("Enter email and company key.");
116
+ return;
117
+ }
118
+ if (!ensureBaseConfigured()) return;
119
+ configureBlazeoFromEffective(effective);
120
+ ensureBlazeoHttpReady({
121
+ baseUrl: effective.baseUrl,
122
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
123
+ });
124
+
125
+ setBusy(true);
126
+ try {
127
+ const res = await fetchLeadByEmail(em, ck, {
128
+ ...connectionOpts,
129
+ baseUrl: effective.baseUrl,
130
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
131
+ });
132
+ if (!res.ok) {
133
+ setError(mapBlazeoDemoError(res.detail ?? ""));
134
+ return;
135
+ }
136
+ setNote("LeadModel.getByEmail → GET /lead/getbyemail.");
137
+ setOutput(toDisplayJson({ lead: res.lead }));
138
+ } catch (err) {
139
+ setError(explainFetchFailure(err, effective.baseUrl));
140
+ } finally {
141
+ setBusy(false);
142
+ }
143
+ }
144
+
145
+ async function handleFetchByCompany(e) {
146
+ e.preventDefault();
147
+ setError("");
148
+ setNote("");
149
+ setOutput("");
150
+ const ck = listCompanyKey.trim();
151
+ if (!ck) {
152
+ setError("Enter company key for the list.");
153
+ return;
154
+ }
155
+ if (!ensureBaseConfigured()) return;
156
+ configureBlazeoFromEffective(effective);
157
+ ensureBlazeoHttpReady({
158
+ baseUrl: effective.baseUrl,
159
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
160
+ });
161
+
162
+ const listOpts = {};
163
+ const s = skip.trim();
164
+ const t = take.trim();
165
+ if (s !== "") {
166
+ const n = Number(s);
167
+ if (!Number.isFinite(n)) {
168
+ setError("Skip must be a number.");
169
+ return;
170
+ }
171
+ listOpts.skip = n;
172
+ }
173
+ if (t !== "") {
174
+ const n = Number(t);
175
+ if (!Number.isFinite(n)) {
176
+ setError("Take must be a number.");
177
+ return;
178
+ }
179
+ listOpts.take = n;
180
+ }
181
+
182
+ setBusy(true);
183
+ try {
184
+ const res = await fetchLeadsByCompany(ck, listOpts, {
185
+ ...connectionOpts,
186
+ baseUrl: effective.baseUrl,
187
+ ...(effective.consumer ? { consumer: effective.consumer } : {}),
188
+ });
189
+ if (!res.ok) {
190
+ setError(mapBlazeoDemoError(res.detail ?? ""));
191
+ return;
192
+ }
193
+ setNote(
194
+ `LeadModel.getByCompany → GET /lead/company/get (${res.leads.length} row(s), MST snapshots as plain objects).`
195
+ );
196
+ setOutput(toDisplayJson({ leads: res.leads }));
197
+ } catch (err) {
198
+ setError(explainFetchFailure(err, effective.baseUrl));
199
+ } finally {
200
+ setBusy(false);
201
+ }
202
+ }
203
+
204
+ return (
205
+ <>
206
+ <div className="card">
207
+ <h2>Lead by id</h2>
208
+ <p className="muted small">
209
+ Uses <code>fetchLeadDetails(leadId)</code> from <code>appointment-client</code>, which wraps{" "}
210
+ <code>LeadModel.getRaw</code> + <code>LeadModel.get</code> (<code>GET /lead/get</code>). Output shows both the
211
+ mapped lead snapshot and the raw API envelope.
212
+ </p>
213
+ <form onSubmit={handleFetchById} className="form">
214
+ <label className="form__label">
215
+ <span>Lead id</span>
216
+ <input
217
+ type="text"
218
+ className="form__input"
219
+ placeholder="lead_id"
220
+ value={leadId}
221
+ onChange={(e) => setLeadId(e.target.value)}
222
+ autoComplete="off"
223
+ />
224
+ </label>
225
+ <button type="submit" className="btn btn--primary" disabled={busy}>
226
+ {busy ? "Loading…" : "Fetch lead"}
227
+ </button>
228
+ </form>
229
+ </div>
230
+
231
+ <div className="card">
232
+ <h2>Lead by email + company</h2>
233
+ <p className="muted small">
234
+ <code>fetchLeadByEmail</code> → <code>LeadModel.getByEmail</code> (<code>GET /lead/getbyemail</code>).
235
+ </p>
236
+ <form onSubmit={handleFetchByEmail} className="form">
237
+ <label className="form__label">
238
+ <span>Email</span>
239
+ <input
240
+ type="email"
241
+ className="form__input"
242
+ placeholder="user@example.com"
243
+ value={email}
244
+ onChange={(e) => setEmail(e.target.value)}
245
+ autoComplete="off"
246
+ />
247
+ </label>
248
+ <label className="form__label">
249
+ <span>Company key</span>
250
+ <input
251
+ type="text"
252
+ className="form__input"
253
+ placeholder="company_key"
254
+ value={companyKey}
255
+ onChange={(e) => setCompanyKey(e.target.value)}
256
+ autoComplete="off"
257
+ />
258
+ </label>
259
+ <button type="submit" className="btn btn--secondary" disabled={busy}>
260
+ {busy ? "Loading…" : "Fetch lead"}
261
+ </button>
262
+ </form>
263
+ </div>
264
+
265
+ <div className="card">
266
+ <h2>Leads by company</h2>
267
+ <p className="muted small">
268
+ <code>fetchLeadsByCompany</code> → <code>LeadModel.getByCompany</code> (<code>GET /lead/company/get</code>).
269
+ Optional <code>skip</code> / <code>take</code> map to query params (see calendar-client).
270
+ </p>
271
+ <form onSubmit={handleFetchByCompany} className="form">
272
+ <label className="form__label">
273
+ <span>Company key</span>
274
+ <input
275
+ type="text"
276
+ className="form__input"
277
+ placeholder="company_key"
278
+ value={listCompanyKey}
279
+ onChange={(e) => setListCompanyKey(e.target.value)}
280
+ autoComplete="off"
281
+ />
282
+ </label>
283
+ <label className="form__label">
284
+ <span>Skip (optional)</span>
285
+ <input
286
+ type="text"
287
+ className="form__input"
288
+ inputMode="numeric"
289
+ placeholder="e.g. 0"
290
+ value={skip}
291
+ onChange={(e) => setSkip(e.target.value)}
292
+ autoComplete="off"
293
+ />
294
+ </label>
295
+ <label className="form__label">
296
+ <span>Take (optional)</span>
297
+ <input
298
+ type="text"
299
+ className="form__input"
300
+ inputMode="numeric"
301
+ placeholder="e.g. 50"
302
+ value={take}
303
+ onChange={(e) => setTake(e.target.value)}
304
+ autoComplete="off"
305
+ />
306
+ </label>
307
+ <button type="submit" className="btn btn--secondary" disabled={busy}>
308
+ {busy ? "Loading…" : "Fetch leads"}
309
+ </button>
310
+ </form>
311
+ </div>
312
+
313
+ {note ? (
314
+ <div className="card">
315
+ <h2>Note</h2>
316
+ <p className="muted small">{note}</p>
317
+ </div>
318
+ ) : null}
319
+
320
+ {error ? (
321
+ <div className="card card--error" role="alert">
322
+ <h2>Error</h2>
323
+ <pre className="pre-block">{error}</pre>
324
+ </div>
325
+ ) : null}
326
+
327
+ {output ? (
328
+ <div className="card">
329
+ <h2>JSON</h2>
330
+ <pre className="pre-block">{output}</pre>
331
+ </div>
332
+ ) : null}
333
+ </>
334
+ );
335
+ }
@@ -276,9 +276,17 @@ export async function fetchCalendarDetails(
276
276
 
277
277
  if (!calendarView) return null as any;
278
278
 
279
+ // Use the mapper to normalize the final output, ensuring all fields like duration,
280
+ // bookingPageTitle, calendarId, etc. are correctly picked and named.
281
+ const finalView = mapToDesiredCalendarResponse(
282
+ payload,
283
+ calendarView.openingHours,
284
+ calendarView.members
285
+ ) as any;
286
+
279
287
  // Attach metadata as non-enumerable properties so they don't show up in JSON.stringify
280
288
  // but are still accessible for debugging if needed.
281
- Object.defineProperties(calendarView, {
289
+ Object.defineProperties(finalView, {
282
290
  _cal: { value: cal, enumerable: false },
283
291
  _participants: { value: participantList, enumerable: false },
284
292
  _openingHours: { value: openingHours, enumerable: false },
@@ -294,7 +302,7 @@ export async function fetchCalendarDetails(
294
302
  },
295
303
  });
296
304
 
297
- return calendarView as any;
305
+ return finalView as any;
298
306
  }
299
307
 
300
308
  /**
@@ -1,10 +1,11 @@
1
1
  import { CalendarModel, CalendarParticipantModel } from "@blazeo.com/calendar-client";
2
2
  import { ensureBlazeoHttpReady } from "../config/ensureBlazeoHttpReady.js";
3
+ import { mapToDesiredCalendarResponse } from "./mapToDesiredResponse.js";
3
4
 
4
5
  /**
5
6
  * 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.
7
+ * Uses a highly optimized single-request approach per calendar to ensure speed in list views.
8
+ * Results are normalized via mapToDesiredCalendarResponse.
8
9
  */
9
10
  export async function getCalendarsByCompany(
10
11
  companyKey: string,
@@ -30,96 +31,48 @@ export async function getCalendarsByCompany(
30
31
  if (!calendarId) return null;
31
32
 
32
33
  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 ?? [];
34
+ // Optimization: Use only getInfoByCalendar to get names/emails in a single request.
35
+ // This is much faster for a list view than fetching both list and info records.
36
+ const infoRaw = await CalendarParticipantModel.getInfoByCalendar(calendarId);
40
37
  const info = Array.isArray(infoRaw) ? infoRaw : (infoRaw as any)?.info ?? [];
41
38
 
42
39
  // Merge logic to ensure names are matched to IDs
43
40
  const membersMap = new Map<string, any>();
44
-
41
+
45
42
  // Use participantId GUID as the primary key
46
43
  const getAnyId = (obj: any) =>
47
44
  obj.participantId ?? obj.ParticipantId ?? obj.participant_id ?? obj.id ?? obj.Id;
48
45
 
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)
46
+ // 1. Process info list (Name, Email, Alias)
64
47
  info.forEach((i: any) => {
65
48
  const mid = getAnyId(i);
66
49
  if (!mid) return;
67
50
  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
51
 
52
+ const resolvedEmail = i.email ?? i.Email ?? i.userSsoEmail ?? i.UserSsoEmail;
72
53
  const memberData = {
73
54
  id: mid,
74
- name: i.name ?? i.Name ?? i.alias ?? i.Alias ?? (existing?.name || "Member"),
55
+ name: i.name ?? i.Name ?? i.alias ?? i.Alias ?? "Member",
75
56
  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
57
+ status: i.status ?? i.Status ?? 1,
80
58
  };
81
59
 
82
- if (!existing) {
60
+ if (!membersMap.has(key)) {
83
61
  membersMap.set(key, memberData);
84
- } else {
85
- Object.assign(existing, memberData);
86
62
  }
87
63
  });
88
64
 
89
65
  const members = Array.from(membersMap.values());
90
66
 
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
- };
67
+ // Use the unified mapper to ensure all properties (duration, calendarId, etc.) are included
68
+ return mapToDesiredCalendarResponse(cal, [], members);
105
69
  } catch (err) {
106
70
  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
- };
71
+ // Fallback to minimal mapping if enrichment fails
72
+ return mapToDesiredCalendarResponse(cal, [], []);
120
73
  }
121
74
  })
122
75
  );
123
76
 
124
- return enrichedCalendars.filter(c => c !== null);
77
+ return enrichedCalendars.filter(Boolean);
125
78
  }
@@ -24,22 +24,33 @@ export function mapToDesiredCalendarResponse(payload: any, openingHours: any[] =
24
24
  }));
25
25
 
26
26
  // Map opening hours with typename and raw fields
27
- const mappedOpeningHours = openingHours.map(oh => ({
28
- id: pick(oh, "id", "Id") ?? 0,
29
- createdOn: pick(oh, "createdOn", "CreatedOn", "created_on") ?? "0001-01-01T00:00:00.000Z",
30
- modifiedOn: pick(oh, "modifiedOn", "ModifiedOn", "modified_on") ?? "0001-01-01T00:00:00.000Z",
31
- member: pick(oh, "member", "Member"),
32
- openingHourId: pick(oh, "openingHourId", "OpeningHourId", "opening_hour_id") ?? "",
33
- calendarId: pick(oh, "calendarId", "CalendarId", "calendar_id") ?? "",
34
- participantId: pick(oh, "participantId", "ParticipantId", "participant_id") ?? "",
35
- days: oh.days ?? [],
36
- startHour: oh.startHour ?? pick(oh, "startHour", "StartHour") ?? 0,
37
- startMinute: oh.startMinute ?? pick(oh, "startMinute", "StartMinute") ?? 0,
38
- endHour: oh.endHour ?? pick(oh, "endHour", "EndHour") ?? 0,
39
- endMinute: oh.endMinute ?? pick(oh, "endMinute", "EndMinute") ?? 0,
40
- off: !!(oh.off ?? pick(oh, "off", "Off")),
41
- __typename: "OpeningHour"
42
- }));
27
+ const mappedOpeningHours = openingHours.map(oh => {
28
+ // If it's already a unified object (has day/start/end), preserve it but ensure __typename
29
+ if (oh.day !== undefined && oh.start !== undefined && oh.end !== undefined) {
30
+ return {
31
+ ...oh,
32
+ __typename: "OpeningHour"
33
+ };
34
+ }
35
+
36
+ // Otherwise, map from raw PascalCase or camelCase
37
+ return {
38
+ id: pick(oh, "id", "Id") ?? 0,
39
+ createdOn: pick(oh, "createdOn", "CreatedOn", "created_on") ?? "0001-01-01T00:00:00.000Z",
40
+ modifiedOn: pick(oh, "modifiedOn", "ModifiedOn", "modified_on") ?? "0001-01-01T00:00:00.000Z",
41
+ member: pick(oh, "member", "Member"),
42
+ openingHourId: pick(oh, "openingHourId", "OpeningHourId", "opening_hour_id") ?? "",
43
+ calendarId: pick(oh, "calendarId", "CalendarId", "calendar_id") ?? "",
44
+ participantId: pick(oh, "participantId", "ParticipantId", "participant_id") ?? "",
45
+ days: oh.days ?? [],
46
+ startHour: oh.startHour ?? pick(oh, "startHour", "StartHour") ?? 0,
47
+ startMinute: oh.startMinute ?? pick(oh, "startMinute", "StartMinute") ?? 0,
48
+ endHour: oh.endHour ?? pick(oh, "endHour", "EndHour") ?? 0,
49
+ endMinute: oh.endMinute ?? pick(oh, "endMinute", "EndMinute") ?? 0,
50
+ off: !!(oh.off ?? pick(oh, "off", "Off")),
51
+ __typename: "OpeningHour"
52
+ };
53
+ });
43
54
 
44
55
  // Map theme
45
56
  const rawTheme = pick(payload, "theme", "Theme");
@@ -68,6 +79,8 @@ export function mapToDesiredCalendarResponse(payload: any, openingHours: any[] =
68
79
  __typename: "ReminderChannelStatus"
69
80
  })) : [];
70
81
 
82
+ const uuid = pick(payload, "uuid", "Uuid", "calendarId", "CalendarId");
83
+
71
84
  return {
72
85
  id: n(pick(payload, "id", "Id")),
73
86
  durationUnit: n(pick(payload, "durationUnit", "DurationUnit")),
@@ -80,7 +93,8 @@ export function mapToDesiredCalendarResponse(payload: any, openingHours: any[] =
80
93
  bufferTime: n(pick(payload, "bufferTime", "BufferTime")),
81
94
  bufferTimeUnit: n(pick(payload, "bufferTimeUnit", "BufferTimeUnit")),
82
95
  calendarLink: pick(payload, "calendarLink", "CalendarLink"),
83
- uuid: pick(payload, "uuid", "Uuid", "calendarId", "CalendarId"),
96
+ uuid: uuid,
97
+ calendarId: uuid, // Explicit alias requested by user
84
98
  location: pick(payload, "location", "Location") ?? "",
85
99
  bookingPageTitle: pick(payload, "bookingPageTitle", "BookingPageTitle") ?? null,
86
100
  reminderChannelStatuses,
package/src/index.ts CHANGED
@@ -10,8 +10,7 @@ export type { EnsureBlazeoHttpOptions } from "./config/ensureBlazeoHttpReady.js"
10
10
  export { blazeoClientConfig } from "./config/blazeoClientDefaults.js";
11
11
  export { applyBlazeoClientConfig } from "./config/applyBlazeoDefaults.js";
12
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";
13
+ export { fetchCalendarBundle, normalizeOpeningHours } from "./calendar/fetchCalendarDetails.js";
15
14
  export {
16
15
  buildUnifiedCalendarView,
17
16
  type UnifiedCalendarMember,
@@ -33,9 +32,9 @@ export { addParticipantToCalendar, removeParticipantFromCalendar, saveCalendarOp
33
32
  export { createAppointmentEventAsync, rescheduleAppointmentEventAsync, cancelAppointmentEventAsync } from "./events/appointmentEventFacade.js";
34
33
  export { mapAppointmentToEventSnapshot } from "./events/mapAppointmentToEventSnapshot.js";
35
34
 
36
- import { fetchCalendarDetails, fetchCalendarBundle } from "./calendar/fetchCalendarDetails.js";
37
- import { fetchCalendarWithOpeningHours } from "./calendar/fetchCalendarWithOpeningHours.js";
38
35
  import { getCalendarsByCompany } from "./calendar/getCalendarsByCompany.js";
36
+ import { fetchCalendarDetails } from "./calendar/fetchCalendarDetails.js";
37
+ export { getCalendarsByCompany, fetchCalendarDetails };
39
38
 
40
39
  import {
41
40
  CalendarModel as CoreCalendarModel,
@@ -46,14 +45,14 @@ import {
46
45
  getConfig
47
46
  } from "@blazeo.com/calendar-client";
48
47
 
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;
48
+ // Enriched CalendarModel
49
+ export const CalendarModel = {
50
+ ...CoreCalendarModel,
51
+ getCalendarsByCompany,
52
+ fetchCalendarDetails
53
+ };
54
54
 
55
55
  export {
56
- CoreCalendarModel as CalendarModel,
57
56
  CoreEventModel as CoreEventModel,
58
57
  CoreParticipantModel as CoreParticipantModel,
59
58
  CoreCalendarParticipantModel as CalendarParticipantModel,
@@ -61,7 +60,6 @@ export {
61
60
  getConfig
62
61
  };
63
62
 
64
-
65
63
  export const packageName = "@blazeo.com/appointment-client";
66
64
 
67
65
  export class CalendarClient {
@@ -0,0 +1,106 @@
1
+ import { LeadModel } from "@blazeo.com/calendar-client";
2
+ import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
3
+ import { ensureBlazeoHttpReady } from "../config/ensureBlazeoHttpReady.js";
4
+
5
+ export type BlazeoLeadConnection = {
6
+ baseUrl?: string;
7
+ consumer?: string;
8
+ };
9
+
10
+ /** Paging / sort options forwarded to `LeadModel.getByCompany` → `GET /lead/company/get`. */
11
+ export type LeadsByCompanyListOpts = {
12
+ skip?: number;
13
+ take?: number;
14
+ sortBy?: string;
15
+ sortOrder?: "ASC" | "DESC" | "asc" | "desc" | string;
16
+ sort?: string;
17
+ sort_column?: string;
18
+ sort_dir?: "asc" | "desc" | string;
19
+ page?: number;
20
+ page_size?: number;
21
+ searchColumn?: string;
22
+ search_column?: string;
23
+ searchText?: string;
24
+ search_text?: string;
25
+ search?: string;
26
+ };
27
+
28
+ function leadToPlain(lead: unknown): Record<string, unknown> | null {
29
+ if (lead == null) return null;
30
+ if (isStateTreeNode(lead)) {
31
+ return getSnapshot(lead) as Record<string, unknown>;
32
+ }
33
+ if (typeof lead === "object") return lead as Record<string, unknown>;
34
+ return null;
35
+ }
36
+
37
+ function leadsToPlain(list: unknown[] | null): Record<string, unknown>[] {
38
+ if (!Array.isArray(list)) return [];
39
+ return list.map((x) => leadToPlain(x)).filter((x): x is Record<string, unknown> => x != null);
40
+ }
41
+
42
+ /**
43
+ * Lead by id: `LeadModel.getRaw` / `get` → `GET /lead/get?lead_id=…`.
44
+ * Returns the mapped MST snapshot when successful, plus the raw API envelope from `getRaw`.
45
+ */
46
+ export async function fetchLeadDetails(
47
+ leadId: string,
48
+ connection: BlazeoLeadConnection = {}
49
+ ): Promise<
50
+ | { ok: true; lead: Record<string, unknown> | null; rawGet: unknown }
51
+ | { ok: false; reason: "missing_base_url"; detail: string }
52
+ > {
53
+ const ready = ensureBlazeoHttpReady(connection);
54
+ if (!ready.ok) {
55
+ return { ok: false, reason: "missing_base_url", detail: ready.error };
56
+ }
57
+ const id = String(leadId ?? "").trim();
58
+ if (!id) {
59
+ return {
60
+ ok: true,
61
+ lead: null,
62
+ rawGet: { status: "failure", message: "leadId is empty" },
63
+ };
64
+ }
65
+ const rawGet = await LeadModel.getRaw(id);
66
+ const model = await LeadModel.get(id);
67
+ return { ok: true, lead: leadToPlain(model), rawGet };
68
+ }
69
+
70
+ /**
71
+ * Single lead by email + company: `GET /lead/getbyemail`.
72
+ */
73
+ export async function fetchLeadByEmail(
74
+ email: string,
75
+ companyKey: string,
76
+ connection: BlazeoLeadConnection = {}
77
+ ): Promise<
78
+ | { ok: true; lead: Record<string, unknown> | null }
79
+ | { ok: false; reason: "missing_base_url"; detail: string }
80
+ > {
81
+ const ready = ensureBlazeoHttpReady(connection);
82
+ if (!ready.ok) {
83
+ return { ok: false, reason: "missing_base_url", detail: ready.error };
84
+ }
85
+ const model = await LeadModel.getByEmail(String(email).trim(), String(companyKey).trim());
86
+ return { ok: true, lead: leadToPlain(model) };
87
+ }
88
+
89
+ /**
90
+ * Paged list: `LeadModel.getByCompany` → `GET /lead/company/get`.
91
+ */
92
+ export async function fetchLeadsByCompany(
93
+ companyKey: string,
94
+ listOpts: LeadsByCompanyListOpts = {},
95
+ connection: BlazeoLeadConnection = {}
96
+ ): Promise<
97
+ | { ok: true; leads: Record<string, unknown>[] }
98
+ | { ok: false; reason: "missing_base_url"; detail: string }
99
+ > {
100
+ const ready = ensureBlazeoHttpReady(connection);
101
+ if (!ready.ok) {
102
+ return { ok: false, reason: "missing_base_url", detail: ready.error };
103
+ }
104
+ const models = await LeadModel.getByCompany(String(companyKey).trim(), listOpts);
105
+ return { ok: true, leads: leadsToPlain(models ?? []) };
106
+ }