@blazeo.com/appointment-client 1.0.9 → 1.0.11

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.
@@ -63,18 +63,33 @@ function dayOrderIndex(d) {
63
63
  const i = DAY_NAMES.indexOf(u);
64
64
  return i >= 0 ? i : 999;
65
65
  }
66
- /** Merge rows that share participant + time span + off into one row with combined `days`. */
66
+ /** Merge rows that share participant + time span into one row with combined active `days`. */
67
67
  function mergeOpeningHoursBySlot(rows) {
68
68
  const map = new Map();
69
69
  for (const r of rows) {
70
- const key = [r.member, r.startHour, r.startMinute, r.endHour, r.endMinute, r.off].join("|");
70
+ // Key excludes 'off' because we want to merge ON and OFF records for the same time slot
71
+ const key = [r.member, r.startHour, r.startMinute, r.endHour, r.endMinute].join("|");
71
72
  const existing = map.get(key);
73
+ // We only want to add the day to the 'days' array if it is NOT marked as OFF.
74
+ // If the whole record was marked as OFF, we still process it to establish the slot,
75
+ // but its days won't be listed as 'active'.
76
+ const activeDaysFromThisRow = r.off ? [] : r.days;
72
77
  if (!existing) {
73
- map.set(key, { ...r, days: [...r.days] });
78
+ map.set(key, { ...r, days: [...activeDaysFromThisRow], off: false });
74
79
  }
75
80
  else {
76
- const set = new Set([...existing.days, ...r.days]);
81
+ const set = new Set([...existing.days, ...activeDaysFromThisRow]);
77
82
  existing.days = Array.from(set).sort((a, b) => dayOrderIndex(a) - dayOrderIndex(b));
83
+ // If we encounter any record that is NOT off, the whole merged slot is NOT off.
84
+ if (!r.off)
85
+ existing.off = false;
86
+ }
87
+ }
88
+ // Final Pass: If a slot has NO active days, it should be marked as off: true
89
+ // (though in Plan V2, these usually just disappear from the UI's 'days' list)
90
+ for (const group of map.values()) {
91
+ if (group.days.length === 0) {
92
+ group.off = true;
78
93
  }
79
94
  }
80
95
  rows.length = 0;
@@ -1,5 +1,5 @@
1
1
  import { getSnapshot } from "mobx-state-tree";
2
- import { addParticipantToCalendar, saveCalendarOpeningHour } from "./blazeoCalendarRelationMethods.js";
2
+ import { addParticipantToCalendar, saveCalendarOpeningHoursBatch } from "./blazeoCalendarRelationMethods.js";
3
3
  import { createCalendarAsync, updateCalendarAsync, deleteCalendarAsync } from "./createCalendar.js";
4
4
  function isFailureStatus(res) {
5
5
  return res.status !== "success" && res.status !== "Success";
@@ -90,8 +90,10 @@ async function runMembersAndOpeningHoursAfterCalendarSave(calendar, calendarNode
90
90
  }
91
91
  membersAdded += 1;
92
92
  }
93
- let openingHoursSaved = 0;
94
- for (const oh of calendar.openingHours ?? []) {
93
+ // 2. Save Opening Hours (Plan V2: Grouped by participant with explicit off-days per slot)
94
+ const openingHours = calendar.openingHours ?? [];
95
+ const hoursByParticipant = new Map();
96
+ for (const oh of openingHours) {
95
97
  const participantId = resolveParticipantIdForOpeningHour(oh);
96
98
  if (!participantId) {
97
99
  return {
@@ -99,8 +101,17 @@ async function runMembersAndOpeningHoursAfterCalendarSave(calendar, calendarNode
99
101
  error: `Opening hour id ${oh.id}: participantId is required.`,
100
102
  };
101
103
  }
102
- for (const day of oh.days ?? []) {
103
- const payload = {
104
+ if (!hoursByParticipant.has(participantId)) {
105
+ hoursByParticipant.set(participantId, []);
106
+ }
107
+ // Plan V2 Logic: For every opening hour object, generate EXACTLY 7 entries (days 0-6).
108
+ // If the day is in oh.days, it's ON. If not, it's OFF.
109
+ const activeDays = oh.days ?? [];
110
+ const openingHourId = oh.openingHourId?.trim() || newOpeningHourId();
111
+ for (let day = 0; day <= 6; day++) {
112
+ const isIncluded = activeDays.includes(day);
113
+ const isOff = isIncluded ? !!oh.off : true; // If not in days array, it's explicitly OFF
114
+ hoursByParticipant.get(participantId)?.push({
104
115
  calendarId: calendarIdStr,
105
116
  participantId,
106
117
  day,
@@ -108,22 +119,35 @@ async function runMembersAndOpeningHoursAfterCalendarSave(calendar, calendarNode
108
119
  startMinute: oh.startMinute,
109
120
  endHour: oh.endHour,
110
121
  endMinute: oh.endMinute,
111
- off: oh.off,
112
- openingHourId: oh.openingHourId?.trim() || newOpeningHourId(),
122
+ off: isOff,
123
+ // Plan V2 Optimization: Generate a unique ID for EVERY day record.
124
+ // This prevents the backend from deduplicating/overwriting when multiple
125
+ // records for the same participant + slot are sent in one batch.
126
+ openingHourId: newOpeningHourId(),
127
+ });
128
+ }
129
+ }
130
+ let openingHoursSaved = 0;
131
+ for (const [participantId, payload] of hoursByParticipant.entries()) {
132
+ if (payload.length === 0)
133
+ continue;
134
+ // Plan V2 Optimization: Clear existing records for this participant first.
135
+ // This ensures that when we save the new batch (with unique per-day IDs),
136
+ // we don't leak orphaned records or create duplicates during updates.
137
+ await calendarNode.removeParticipantOpeningHours(participantId);
138
+ // Use the batch save method (plural)
139
+ const res = await saveCalendarOpeningHoursBatch(calendarNode, payload);
140
+ if (isFailureStatus(res)) {
141
+ const msg = res.message ??
142
+ (typeof res.data === "string" ? res.data : undefined) ??
143
+ JSON.stringify(res);
144
+ return {
145
+ ok: false,
146
+ error: `saveOpeningHours batch failed for participant ${participantId}: ${msg}`,
147
+ apiResponse: res,
113
148
  };
114
- const res = await saveCalendarOpeningHour(calendarNode, payload);
115
- if (isFailureStatus(res)) {
116
- const msg = res.message ??
117
- (typeof res.data === "string" ? res.data : undefined) ??
118
- JSON.stringify(res);
119
- return {
120
- ok: false,
121
- error: `saveOpeningHour failed (opening hour ${oh.id}, day ${day}): ${msg}`,
122
- apiResponse: res,
123
- };
124
- }
125
- openingHoursSaved += 1;
126
149
  }
150
+ openingHoursSaved += payload.length;
127
151
  }
128
152
  return {
129
153
  ...baseSuccess,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazeo.com/appointment-client",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -91,19 +91,37 @@ function dayOrderIndex(d: string): number {
91
91
  return i >= 0 ? i : 999;
92
92
  }
93
93
 
94
- /** Merge rows that share participant + time span + off into one row with combined `days`. */
94
+ /** Merge rows that share participant + time span into one row with combined active `days`. */
95
95
  function mergeOpeningHoursBySlot(rows: UnifiedOpeningHourRow[]) {
96
96
  const map = new Map<string, UnifiedOpeningHourRow>();
97
97
  for (const r of rows) {
98
- const key = [r.member, r.startHour, r.startMinute, r.endHour, r.endMinute, r.off].join("|");
98
+ // Key excludes 'off' because we want to merge ON and OFF records for the same time slot
99
+ const key = [r.member, r.startHour, r.startMinute, r.endHour, r.endMinute].join("|");
99
100
  const existing = map.get(key);
101
+
102
+ // We only want to add the day to the 'days' array if it is NOT marked as OFF.
103
+ // If the whole record was marked as OFF, we still process it to establish the slot,
104
+ // but its days won't be listed as 'active'.
105
+ const activeDaysFromThisRow = r.off ? [] : r.days;
106
+
100
107
  if (!existing) {
101
- map.set(key, { ...r, days: [...r.days] });
108
+ map.set(key, { ...r, days: [...activeDaysFromThisRow], off: false });
102
109
  } else {
103
- const set = new Set([...existing.days, ...r.days]);
110
+ const set = new Set([...existing.days, ...activeDaysFromThisRow]);
104
111
  existing.days = Array.from(set).sort((a, b) => dayOrderIndex(a) - dayOrderIndex(b));
112
+ // If we encounter any record that is NOT off, the whole merged slot is NOT off.
113
+ if (!r.off) existing.off = false;
114
+ }
115
+ }
116
+
117
+ // Final Pass: If a slot has NO active days, it should be marked as off: true
118
+ // (though in Plan V2, these usually just disappear from the UI's 'days' list)
119
+ for (const group of map.values()) {
120
+ if (group.days.length === 0) {
121
+ group.off = true;
105
122
  }
106
123
  }
124
+
107
125
  rows.length = 0;
108
126
  rows.push(...map.values());
109
127
  }
@@ -1,5 +1,5 @@
1
1
  import { getSnapshot } from "mobx-state-tree";
2
- import { addParticipantToCalendar, saveCalendarOpeningHour } from "./blazeoCalendarRelationMethods.js";
2
+ import { addParticipantToCalendar, saveCalendarOpeningHour, saveCalendarOpeningHoursBatch } from "./blazeoCalendarRelationMethods.js";
3
3
  import { createCalendarAsync, updateCalendarAsync, deleteCalendarAsync } from "./createCalendar.js";
4
4
 
5
5
  function isFailureStatus(res: any) {
@@ -97,8 +97,11 @@ async function runMembersAndOpeningHoursAfterCalendarSave(calendar: any, calenda
97
97
  membersAdded += 1;
98
98
  }
99
99
 
100
- let openingHoursSaved = 0;
101
- for (const oh of calendar.openingHours ?? []) {
100
+ // 2. Save Opening Hours (Plan V2: Grouped by participant with explicit off-days per slot)
101
+ const openingHours = calendar.openingHours ?? [];
102
+ const hoursByParticipant = new Map<string, any[]>();
103
+
104
+ for (const oh of openingHours) {
102
105
  const participantId = resolveParticipantIdForOpeningHour(oh);
103
106
  if (!participantId) {
104
107
  return {
@@ -106,8 +109,21 @@ async function runMembersAndOpeningHoursAfterCalendarSave(calendar: any, calenda
106
109
  error: `Opening hour id ${oh.id}: participantId is required.`,
107
110
  };
108
111
  }
109
- for (const day of oh.days ?? []) {
110
- const payload = {
112
+
113
+ if (!hoursByParticipant.has(participantId)) {
114
+ hoursByParticipant.set(participantId, []);
115
+ }
116
+
117
+ // Plan V2 Logic: For every opening hour object, generate EXACTLY 7 entries (days 0-6).
118
+ // If the day is in oh.days, it's ON. If not, it's OFF.
119
+ const activeDays = oh.days ?? [];
120
+ const openingHourId = oh.openingHourId?.trim() || newOpeningHourId();
121
+
122
+ for (let day = 0; day <= 6; day++) {
123
+ const isIncluded = activeDays.includes(day);
124
+ const isOff = isIncluded ? !!oh.off : true; // If not in days array, it's explicitly OFF
125
+
126
+ hoursByParticipant.get(participantId)?.push({
111
127
  calendarId: calendarIdStr,
112
128
  participantId,
113
129
  day,
@@ -115,23 +131,39 @@ async function runMembersAndOpeningHoursAfterCalendarSave(calendar: any, calenda
115
131
  startMinute: oh.startMinute,
116
132
  endHour: oh.endHour,
117
133
  endMinute: oh.endMinute,
118
- off: oh.off,
119
- openingHourId: oh.openingHourId?.trim() || newOpeningHourId(),
134
+ off: isOff,
135
+ // Plan V2 Optimization: Generate a unique ID for EVERY day record.
136
+ // This prevents the backend from deduplicating/overwriting when multiple
137
+ // records for the same participant + slot are sent in one batch.
138
+ openingHourId: newOpeningHourId(),
139
+ });
140
+ }
141
+ }
142
+
143
+ let openingHoursSaved = 0;
144
+ for (const [participantId, payload] of hoursByParticipant.entries()) {
145
+ if (payload.length === 0) continue;
146
+
147
+ // Plan V2 Optimization: Clear existing records for this participant first.
148
+ // This ensures that when we save the new batch (with unique per-day IDs),
149
+ // we don't leak orphaned records or create duplicates during updates.
150
+ await (calendarNode as any).removeParticipantOpeningHours(participantId);
151
+
152
+ // Use the batch save method (plural)
153
+ const res = await saveCalendarOpeningHoursBatch(calendarNode, payload);
154
+
155
+ if (isFailureStatus(res)) {
156
+ const msg =
157
+ res.message ??
158
+ (typeof res.data === "string" ? res.data : undefined) ??
159
+ JSON.stringify(res);
160
+ return {
161
+ ok: false,
162
+ error: `saveOpeningHours batch failed for participant ${participantId}: ${msg}`,
163
+ apiResponse: res,
120
164
  };
121
- const res = await saveCalendarOpeningHour(calendarNode, payload);
122
- if (isFailureStatus(res)) {
123
- const msg =
124
- res.message ??
125
- (typeof res.data === "string" ? res.data : undefined) ??
126
- JSON.stringify(res);
127
- return {
128
- ok: false,
129
- error: `saveOpeningHour failed (opening hour ${oh.id}, day ${day}): ${msg}`,
130
- apiResponse: res,
131
- };
132
- }
133
- openingHoursSaved += 1;
134
165
  }
166
+ openingHoursSaved += payload.length;
135
167
  }
136
168
 
137
169
  return {