@blazeo.com/appointment-client 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blazeo.com-appointment-client-1.0.4.tgz +0 -0
- package/dist/calendar/fetchCalendarWithOpeningHours.d.ts +21 -0
- package/dist/calendar/fetchCalendarWithOpeningHours.js +75 -0
- package/dist/config/initializeAppointmentClient.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +23 -39
- package/sample/index.html +13 -0
- package/sample/package-lock.json +1658 -0
- package/sample/package.json +19 -0
- package/sample/src/App2.jsx +148 -0
- package/sample/src/AvailabilityTab.jsx +83 -0
- package/sample/src/BlazeoConnectionSettings.jsx +118 -0
- package/sample/src/CalendarTab.jsx +37 -0
- package/sample/src/CreateCalendarTab.jsx +147 -0
- package/sample/src/EventTab.jsx +244 -0
- package/sample/src/FetchCalendarTab.jsx +566 -0
- package/sample/src/ParticipantTab.jsx +102 -0
- package/sample/src/main.jsx +19 -0
- package/sample/src/style.css +681 -0
- package/sample/src/vite-env.d.ts +12 -0
- package/sample/vite.config.js +47 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CalendarModel,
|
|
4
|
+
deleteCalendarAsync,
|
|
5
|
+
fetchCalendarWithOpeningHours,
|
|
6
|
+
updateCalendarAsync,
|
|
7
|
+
} from "appointment-client";
|
|
8
|
+
import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
|
|
9
|
+
import { configureBlazeoFromEffective, useBlazeoConnection } from "./BlazeoConnectionSettings.jsx";
|
|
10
|
+
import { getExampleCalendarBOInput } from "./CreateCalendarTab.jsx";
|
|
11
|
+
|
|
12
|
+
function pick(row, ...keys) {
|
|
13
|
+
if (row == null || typeof row !== "object") return undefined;
|
|
14
|
+
for (const k of keys) {
|
|
15
|
+
if (row[k] !== undefined && row[k] !== null) return row[k];
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pad2(n) {
|
|
21
|
+
const v = Number(n);
|
|
22
|
+
if (Number.isNaN(v)) return "—";
|
|
23
|
+
return String(v).padStart(2, "0");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Browser `fetch` often surfaces blocked requests as TypeError "Failed to fetch" (e.g. CORS).
|
|
28
|
+
*/
|
|
29
|
+
function explainFetchFailure(err, configuredBaseUrl) {
|
|
30
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
31
|
+
const isNetwork =
|
|
32
|
+
msg === "Failed to fetch" ||
|
|
33
|
+
msg === "Load failed" ||
|
|
34
|
+
(err instanceof TypeError && (/fetch/i.test(msg) || /network/i.test(msg)));
|
|
35
|
+
if (!isNetwork) return msg;
|
|
36
|
+
|
|
37
|
+
const isRemote =
|
|
38
|
+
configuredBaseUrl &&
|
|
39
|
+
/^https?:\/\//i.test(configuredBaseUrl) &&
|
|
40
|
+
!/localhost|127\.0\.0\.1/i.test(configuredBaseUrl);
|
|
41
|
+
|
|
42
|
+
const proxyHint =
|
|
43
|
+
"Dev workaround (Vite proxy): create sample/.env.development with\n" +
|
|
44
|
+
" VITE_DEV_PROXY_TARGET=https://YOUR_API_ORIGIN\n" +
|
|
45
|
+
"restart npm run dev, then set Base URL to:\n" +
|
|
46
|
+
" http://localhost:5173/blazeo-api\n" +
|
|
47
|
+
"Consumer header stays the same.";
|
|
48
|
+
|
|
49
|
+
if (isRemote) {
|
|
50
|
+
return `${msg}\n\nLikely CORS / blocked browser cross-origin request.\nFix API CORS for http://localhost:5173, OR:\n${proxyHint}`;
|
|
51
|
+
}
|
|
52
|
+
return `${msg}\n\n${proxyHint}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Opening hours list from participant API wrapper or embedded `calendar.openingHours`. */
|
|
56
|
+
function pickOpeningHoursListFromBundle(parsed) {
|
|
57
|
+
const fromCal = parsed?.calendar?.openingHours;
|
|
58
|
+
if (Array.isArray(fromCal) && fromCal.length > 0) return fromCal;
|
|
59
|
+
const oh = parsed?.openingHours;
|
|
60
|
+
if (oh == null) return null;
|
|
61
|
+
if (Array.isArray(oh)) return oh.length ? oh : null;
|
|
62
|
+
const d = oh.data ?? oh.Data ?? oh;
|
|
63
|
+
if (Array.isArray(d)) return d.length ? d : null;
|
|
64
|
+
if (d && typeof d === "object") {
|
|
65
|
+
if (Array.isArray(d.openingHours)) return d.openingHours;
|
|
66
|
+
if (Array.isArray(d.items)) return d.items;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function OpeningHoursSummary({ outputJson }) {
|
|
72
|
+
let list = null;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(outputJson);
|
|
75
|
+
list = pickOpeningHoursListFromBundle(parsed);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
80
|
+
return (
|
|
81
|
+
<p className="muted small" style={{ marginBottom: "0.75rem" }}>
|
|
82
|
+
No opening-hours rows parsed for the table. Check{" "}
|
|
83
|
+
<code>calendar.openingHours</code> or <code>openingHours</code> in the JSON below.
|
|
84
|
+
</p>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div style={{ marginBottom: "1rem" }}>
|
|
90
|
+
<h3 style={{ margin: "0 0 0.5rem", fontSize: "0.95rem", color: "#334155" }}>
|
|
91
|
+
Opening hours (quick view)
|
|
92
|
+
</h3>
|
|
93
|
+
<table className="table">
|
|
94
|
+
<thead>
|
|
95
|
+
<tr>
|
|
96
|
+
<th>Day(s)</th>
|
|
97
|
+
<th>Start</th>
|
|
98
|
+
<th>End</th>
|
|
99
|
+
<th>Off</th>
|
|
100
|
+
<th>Participant</th>
|
|
101
|
+
</tr>
|
|
102
|
+
</thead>
|
|
103
|
+
<tbody>
|
|
104
|
+
{list.map((row, idx) => {
|
|
105
|
+
const days = pick(row, "days", "Days");
|
|
106
|
+
const day = pick(row, "day", "Day");
|
|
107
|
+
const dayLabel = Array.isArray(days)
|
|
108
|
+
? days.join(", ")
|
|
109
|
+
: day != null
|
|
110
|
+
? String(day)
|
|
111
|
+
: "—";
|
|
112
|
+
const sh = pick(row, "startHour", "StartHour", "start_hour");
|
|
113
|
+
const sm = pick(row, "startMinute", "StartMinute", "start_minute");
|
|
114
|
+
const eh = pick(row, "endHour", "EndHour", "end_hour");
|
|
115
|
+
const em = pick(row, "endMinute", "EndMinute", "end_minute");
|
|
116
|
+
const off = pick(row, "off", "Off");
|
|
117
|
+
const pid = pick(row, "participantId", "ParticipantId", "participant_id");
|
|
118
|
+
const member = pick(row, "member", "Member");
|
|
119
|
+
const participantDisplay =
|
|
120
|
+
pid != null ? String(pid) : member != null ? String(member) : "—";
|
|
121
|
+
return (
|
|
122
|
+
<tr key={idx}>
|
|
123
|
+
<td className="table__code">{dayLabel}</td>
|
|
124
|
+
<td className="table__code">
|
|
125
|
+
{pad2(sh)}:{pad2(sm)}
|
|
126
|
+
</td>
|
|
127
|
+
<td className="table__code">
|
|
128
|
+
{pad2(eh)}:{pad2(em)}
|
|
129
|
+
</td>
|
|
130
|
+
<td className="table__code">{off === true || off === "true" ? "yes" : "no"}</td>
|
|
131
|
+
<td className="table__code">{participantDisplay}</td>
|
|
132
|
+
</tr>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
<p className="muted small" style={{ marginTop: "0.5rem" }}>
|
|
138
|
+
Source:{" "}
|
|
139
|
+
{(() => {
|
|
140
|
+
try {
|
|
141
|
+
const p = JSON.parse(outputJson);
|
|
142
|
+
if (p?.__openingHoursMeta?.fromCalendarGet) return "embedded on GET /Calendar/Get";
|
|
143
|
+
if (p?.__openingHoursMeta?.fromParticipantApi)
|
|
144
|
+
return "GET /Calendar/Participant/OpeningHours/Get";
|
|
145
|
+
return "see JSON";
|
|
146
|
+
} catch {
|
|
147
|
+
return "—";
|
|
148
|
+
}
|
|
149
|
+
})()}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toDisplayJson(value) {
|
|
156
|
+
if (value == null) return JSON.stringify(value, null, 2);
|
|
157
|
+
if (Array.isArray(value) && value.every(isStateTreeNode)) {
|
|
158
|
+
return JSON.stringify(value.map((n) => getSnapshot(n)), null, 2);
|
|
159
|
+
}
|
|
160
|
+
if (isStateTreeNode(value)) return JSON.stringify(getSnapshot(value), null, 2);
|
|
161
|
+
try {
|
|
162
|
+
return JSON.stringify(value, null, 2);
|
|
163
|
+
} catch {
|
|
164
|
+
return String(value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** MST `Calendar` snapshot uses `id` (server id). `CalendarInput` / update mapper expect `serverId`. */
|
|
169
|
+
function calendarSnapshotToUpdatePayload(snap) {
|
|
170
|
+
const copy = { ...snap };
|
|
171
|
+
const serverId = copy.id;
|
|
172
|
+
delete copy.id;
|
|
173
|
+
if (serverId != null) copy.serverId = serverId;
|
|
174
|
+
return copy;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function FetchCalendarTab() {
|
|
178
|
+
const { effective } = useBlazeoConnection();
|
|
179
|
+
const [calendarId, setCalendarId] = useState("");
|
|
180
|
+
const [companyKey, setCompanyKey] = useState("");
|
|
181
|
+
const [busy, setBusy] = useState(false);
|
|
182
|
+
const [note, setNote] = useState("");
|
|
183
|
+
const [output, setOutput] = useState("");
|
|
184
|
+
const [error, setError] = useState("");
|
|
185
|
+
|
|
186
|
+
const initialUpdateJson = useMemo(() => {
|
|
187
|
+
const ex = getExampleCalendarBOInput();
|
|
188
|
+
ex.calendarId = "paste-or-use-button-below";
|
|
189
|
+
return JSON.stringify(ex, null, 2);
|
|
190
|
+
}, []);
|
|
191
|
+
const [updateJson, setUpdateJson] = useState(initialUpdateJson);
|
|
192
|
+
const [mutateNote, setMutateNote] = useState("");
|
|
193
|
+
const [mutateOutput, setMutateOutput] = useState("");
|
|
194
|
+
const [lastFetchUpdatePayload, setLastFetchUpdatePayload] = useState(null);
|
|
195
|
+
|
|
196
|
+
function ensureBaseConfigured() {
|
|
197
|
+
if (!effective.baseUrl) {
|
|
198
|
+
setError("Set Base URL in the connection card above or in `blazeoClientDefaults.ts`.");
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function handleFetchCalendar(e) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
setError("");
|
|
207
|
+
setNote("");
|
|
208
|
+
setOutput("");
|
|
209
|
+
setLastFetchUpdatePayload(null);
|
|
210
|
+
const id = calendarId.trim();
|
|
211
|
+
if (!id) {
|
|
212
|
+
setError("Enter a calendar id.");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!ensureBaseConfigured()) return;
|
|
216
|
+
configureBlazeoFromEffective(effective);
|
|
217
|
+
|
|
218
|
+
setBusy(true);
|
|
219
|
+
try {
|
|
220
|
+
const bundle = await fetchCalendarWithOpeningHours(id);
|
|
221
|
+
|
|
222
|
+
if (bundle.cal == null) {
|
|
223
|
+
const raw = await CalendarModel.getRaw(id);
|
|
224
|
+
setNote(
|
|
225
|
+
"CalendarModel.get returned null. Showing CalendarModel.getRaw only (opening hours merge skipped)."
|
|
226
|
+
);
|
|
227
|
+
setOutput(toDisplayJson(raw));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const snap = getSnapshot(bundle.cal);
|
|
232
|
+
setLastFetchUpdatePayload(JSON.stringify(calendarSnapshotToUpdatePayload(snap), null, 2));
|
|
233
|
+
|
|
234
|
+
const [participants, participantsInfo] = await Promise.all([
|
|
235
|
+
bundle.cal.getParticipants(),
|
|
236
|
+
bundle.cal.getParticipantsInfo(),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const payload = {
|
|
240
|
+
calendar: bundle.calendar,
|
|
241
|
+
openingHours: bundle.openingHours,
|
|
242
|
+
...(bundle.participantOpeningHoursResponse != null
|
|
243
|
+
? { participantOpeningHoursApiResponse: bundle.participantOpeningHoursResponse }
|
|
244
|
+
: {}),
|
|
245
|
+
participants,
|
|
246
|
+
participantsInfo,
|
|
247
|
+
__openingHoursMeta: {
|
|
248
|
+
fromCalendarGet: bundle.fromCalendarGet,
|
|
249
|
+
fromParticipantApi: bundle.fromParticipantApi,
|
|
250
|
+
embeddedCount: bundle.embeddedFromGet?.length ?? 0,
|
|
251
|
+
resolvedCount: bundle.openingHours?.length ?? 0,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
setOutput(toDisplayJson(payload));
|
|
256
|
+
setNote(
|
|
257
|
+
bundle.fromCalendarGet
|
|
258
|
+
? "Opening hours: embedded on GET /Calendar/Get (`appointment-client` fetchCalendarWithOpeningHours)."
|
|
259
|
+
: bundle.fromParticipantApi
|
|
260
|
+
? "Opening hours: from GET /Calendar/Participant/OpeningHours/Get (calendar GET had none)."
|
|
261
|
+
: "Opening hours: none returned."
|
|
262
|
+
);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
setError(explainFetchFailure(err, effective.baseUrl));
|
|
265
|
+
} finally {
|
|
266
|
+
setBusy(false);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function handleFetchByCompany(e) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
setError("");
|
|
273
|
+
setNote("");
|
|
274
|
+
setOutput("");
|
|
275
|
+
setLastFetchUpdatePayload(null);
|
|
276
|
+
const key = companyKey.trim();
|
|
277
|
+
if (!key) {
|
|
278
|
+
setError("Enter a company key.");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!ensureBaseConfigured()) return;
|
|
282
|
+
configureBlazeoFromEffective(effective);
|
|
283
|
+
|
|
284
|
+
setBusy(true);
|
|
285
|
+
try {
|
|
286
|
+
const list = await CalendarModel.getByCompany(key);
|
|
287
|
+
if (list == null || list.length === 0) {
|
|
288
|
+
setNote("getByCompany returned null or an empty list.");
|
|
289
|
+
setOutput(toDisplayJson(list));
|
|
290
|
+
} else {
|
|
291
|
+
const enriched = await Promise.all(
|
|
292
|
+
list.map(async (c) => {
|
|
293
|
+
const id = c.calendarId ?? String(c.id ?? "");
|
|
294
|
+
if (!id) return { calendar: getSnapshot(c), openingHours: [], meta: { error: "no id" } };
|
|
295
|
+
try {
|
|
296
|
+
const b = await fetchCalendarWithOpeningHours(id);
|
|
297
|
+
return {
|
|
298
|
+
calendar: b.calendar ?? getSnapshot(c),
|
|
299
|
+
openingHours: b.openingHours,
|
|
300
|
+
__openingHoursMeta: {
|
|
301
|
+
fromCalendarGet: b.fromCalendarGet,
|
|
302
|
+
fromParticipantApi: b.fromParticipantApi,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return {
|
|
307
|
+
calendar: getSnapshot(c),
|
|
308
|
+
openingHours: [],
|
|
309
|
+
__openingHoursMeta: {
|
|
310
|
+
error: err instanceof Error ? err.message : String(err),
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
setNote(
|
|
317
|
+
`Loaded ${list.length} calendar(s); opening hours merged per calendar (GET embed → participant API fallback).`
|
|
318
|
+
);
|
|
319
|
+
setOutput(toDisplayJson(enriched));
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
setError(explainFetchFailure(err, effective.baseUrl));
|
|
323
|
+
} finally {
|
|
324
|
+
setBusy(false);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function handleFillUpdateFromLastFetch() {
|
|
329
|
+
if (lastFetchUpdatePayload == null) {
|
|
330
|
+
setError("Fetch one calendar first (successful CalendarModel.get), then use this button.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
setUpdateJson(lastFetchUpdatePayload);
|
|
334
|
+
setError("");
|
|
335
|
+
setMutateNote("");
|
|
336
|
+
setMutateOutput("");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function handleInjectCalendarIdIntoUpdateJson() {
|
|
340
|
+
const id = calendarId.trim();
|
|
341
|
+
if (!id) {
|
|
342
|
+
setError("Enter a calendar id above first, or edit the JSON manually.");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const obj = JSON.parse(updateJson);
|
|
347
|
+
obj.calendarId = id;
|
|
348
|
+
setUpdateJson(JSON.stringify(obj, null, 2));
|
|
349
|
+
setError("");
|
|
350
|
+
} catch (err) {
|
|
351
|
+
setError(err instanceof Error ? err.message : "Update JSON is not valid JSON.");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function handleUpdateCalendar(e) {
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
setError("");
|
|
358
|
+
setMutateNote("");
|
|
359
|
+
setMutateOutput("");
|
|
360
|
+
if (!ensureBaseConfigured()) return;
|
|
361
|
+
configureBlazeoFromEffective(effective);
|
|
362
|
+
let payload;
|
|
363
|
+
try {
|
|
364
|
+
payload = JSON.parse(updateJson);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
setError(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
setBusy(true);
|
|
370
|
+
try {
|
|
371
|
+
const result = await updateCalendarAsync(payload, {});
|
|
372
|
+
if (result.ok) {
|
|
373
|
+
setMutateNote("updateCalendarAsync → POST /Calendar/Event/Update");
|
|
374
|
+
setMutateOutput(
|
|
375
|
+
JSON.stringify(
|
|
376
|
+
{
|
|
377
|
+
snapshot: getSnapshot(result.calendar),
|
|
378
|
+
apiResponse: result.apiResponse ?? null,
|
|
379
|
+
},
|
|
380
|
+
null,
|
|
381
|
+
2
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
setError(result.error);
|
|
386
|
+
if (result.apiResponse != null) {
|
|
387
|
+
setMutateOutput(JSON.stringify(result.apiResponse, null, 2));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
setBusy(false);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function handleDeleteCalendar(e) {
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
setError("");
|
|
398
|
+
setMutateNote("");
|
|
399
|
+
setMutateOutput("");
|
|
400
|
+
const id = calendarId.trim();
|
|
401
|
+
if (!id) {
|
|
402
|
+
setError("Enter a calendar id to delete.");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (!ensureBaseConfigured()) return;
|
|
406
|
+
configureBlazeoFromEffective(effective);
|
|
407
|
+
if (
|
|
408
|
+
!window.confirm(
|
|
409
|
+
`Delete calendar "${id}"?\n\nThis calls GET /Calendar/Remove (cannot be undone on the server).`
|
|
410
|
+
)
|
|
411
|
+
) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
setBusy(true);
|
|
415
|
+
try {
|
|
416
|
+
const result = await deleteCalendarAsync(id, {});
|
|
417
|
+
if (result.ok) {
|
|
418
|
+
setMutateNote("deleteCalendarAsync → GET /Calendar/Remove");
|
|
419
|
+
setMutateOutput(JSON.stringify({ calendarId: id, apiResponse: result.apiResponse ?? null }, null, 2));
|
|
420
|
+
} else {
|
|
421
|
+
setError(result.error);
|
|
422
|
+
if (result.apiResponse != null) {
|
|
423
|
+
setMutateOutput(JSON.stringify(result.apiResponse, null, 2));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
setBusy(false);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<>
|
|
433
|
+
<div className="card">
|
|
434
|
+
<h2>Fetch calendar</h2>
|
|
435
|
+
<p className="muted small">
|
|
436
|
+
Uses <code>fetchCalendarWithOpeningHours</code> from <code>appointment-client</code>: merges
|
|
437
|
+
embedded <code>openingHours</code> from <code>GET /Calendar/Get</code> when present (MST omits
|
|
438
|
+
them); otherwise calls <code>calendar.getParticipantOpeningHours()</code> (
|
|
439
|
+
<code>GET /Calendar/Participant/OpeningHours/Get</code>), matching{" "}
|
|
440
|
+
<code>@blazeo.com/calendar-client</code>.
|
|
441
|
+
</p>
|
|
442
|
+
<p className="muted small">
|
|
443
|
+
Effective: <code>{effective.baseUrl || "(set connection or blazeoClientDefaults.ts)"}</code>
|
|
444
|
+
{effective.consumer ? (
|
|
445
|
+
<>
|
|
446
|
+
{" "}
|
|
447
|
+
· Consumer: <code>{effective.consumer}</code>
|
|
448
|
+
</>
|
|
449
|
+
) : null}
|
|
450
|
+
</p>
|
|
451
|
+
|
|
452
|
+
<form onSubmit={handleFetchCalendar} className="form">
|
|
453
|
+
<label className="form__label">
|
|
454
|
+
<span>Calendar id</span>
|
|
455
|
+
<input
|
|
456
|
+
type="text"
|
|
457
|
+
className="form__input"
|
|
458
|
+
placeholder="Calendar / third-party id"
|
|
459
|
+
value={calendarId}
|
|
460
|
+
onChange={(e) => setCalendarId(e.target.value)}
|
|
461
|
+
autoComplete="off"
|
|
462
|
+
/>
|
|
463
|
+
</label>
|
|
464
|
+
<button type="submit" className="btn btn--primary" disabled={busy}>
|
|
465
|
+
{busy ? "Loading…" : "Fetch calendar + opening hours"}
|
|
466
|
+
</button>
|
|
467
|
+
</form>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<div className="card">
|
|
471
|
+
<h2>Update calendar</h2>
|
|
472
|
+
<div className="form-actions form-actions--top">
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
className="btn btn--secondary"
|
|
476
|
+
disabled={busy || lastFetchUpdatePayload == null}
|
|
477
|
+
onClick={handleFillUpdateFromLastFetch}
|
|
478
|
+
>
|
|
479
|
+
Fill update form from last fetch
|
|
480
|
+
</button>
|
|
481
|
+
<button type="button" className="btn btn--secondary" disabled={busy} onClick={handleInjectCalendarIdIntoUpdateJson}>
|
|
482
|
+
Set calendarId from field above
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
<form onSubmit={handleUpdateCalendar} className="form">
|
|
486
|
+
<label className="form__label">
|
|
487
|
+
<span>Payload (JSON)</span>
|
|
488
|
+
<textarea
|
|
489
|
+
className="form__textarea"
|
|
490
|
+
value={updateJson}
|
|
491
|
+
onChange={(e) => setUpdateJson(e.target.value)}
|
|
492
|
+
spellCheck={false}
|
|
493
|
+
rows={14}
|
|
494
|
+
aria-label="Calendar update JSON"
|
|
495
|
+
/>
|
|
496
|
+
</label>
|
|
497
|
+
<button type="submit" className="btn btn--primary" disabled={busy}>
|
|
498
|
+
{busy ? "Working…" : "Update calendar"}
|
|
499
|
+
</button>
|
|
500
|
+
</form>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div className="card">
|
|
504
|
+
<h2>Delete calendar</h2>
|
|
505
|
+
<form onSubmit={handleDeleteCalendar} className="form">
|
|
506
|
+
<button type="submit" className="btn btn--secondary" disabled={busy}>
|
|
507
|
+
{busy ? "Working…" : "Delete calendar"}
|
|
508
|
+
</button>
|
|
509
|
+
</form>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<div className="card">
|
|
513
|
+
<h2>Fetch calendars by company</h2>
|
|
514
|
+
<p className="muted small">
|
|
515
|
+
Calls <code>CalendarModel.getByCompany</code> → <code>GET /Calendar/All</code>. If the UI shows{" "}
|
|
516
|
+
<strong>Failed to fetch</strong> while Base URL points at Azure/production, that is usually{" "}
|
|
517
|
+
<strong>CORS</strong>: enable proxy via <code>VITE_DEV_PROXY_TARGET</code> in{" "}
|
|
518
|
+
<code>sample/.env.development</code> and Base URL <code>http://localhost:5173/blazeo-api</code>{" "}
|
|
519
|
+
(restart dev server).
|
|
520
|
+
</p>
|
|
521
|
+
<form onSubmit={handleFetchByCompany} className="form">
|
|
522
|
+
<label className="form__label">
|
|
523
|
+
<span>Company key</span>
|
|
524
|
+
<input
|
|
525
|
+
type="text"
|
|
526
|
+
className="form__input"
|
|
527
|
+
placeholder="company_key"
|
|
528
|
+
value={companyKey}
|
|
529
|
+
onChange={(e) => setCompanyKey(e.target.value)}
|
|
530
|
+
autoComplete="off"
|
|
531
|
+
/>
|
|
532
|
+
</label>
|
|
533
|
+
<button type="submit" className="btn btn--secondary" disabled={busy}>
|
|
534
|
+
{busy ? "Loading…" : "Fetch calendars"}
|
|
535
|
+
</button>
|
|
536
|
+
</form>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
{error ? (
|
|
540
|
+
<div className="card card--error" role="alert">
|
|
541
|
+
<h2>Error</h2>
|
|
542
|
+
<pre className="pre-block">{error}</pre>
|
|
543
|
+
</div>
|
|
544
|
+
) : null}
|
|
545
|
+
|
|
546
|
+
{note ? <p className="muted small">{note}</p> : null}
|
|
547
|
+
|
|
548
|
+
{output ? (
|
|
549
|
+
<div className="card card--success">
|
|
550
|
+
<h2>Calendar + opening hours & participants</h2>
|
|
551
|
+
<OpeningHoursSummary outputJson={output} />
|
|
552
|
+
<pre className="pre-block">{output}</pre>
|
|
553
|
+
</div>
|
|
554
|
+
) : null}
|
|
555
|
+
|
|
556
|
+
{mutateNote ? <p className="muted small">{mutateNote}</p> : null}
|
|
557
|
+
|
|
558
|
+
{mutateOutput ? (
|
|
559
|
+
<div className="card card--success">
|
|
560
|
+
<h2>Update / delete result</h2>
|
|
561
|
+
<pre className="pre-block">{mutateOutput}</pre>
|
|
562
|
+
</div>
|
|
563
|
+
) : null}
|
|
564
|
+
</>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { getExampleParticipants, ParticipantModel } from "appointment-client";
|
|
3
|
+
import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
|
|
4
|
+
import {
|
|
5
|
+
configureBlazeoFromEffective,
|
|
6
|
+
useBlazeoConnection,
|
|
7
|
+
} from "./BlazeoConnectionSettings.jsx";
|
|
8
|
+
|
|
9
|
+
function toDisplayJson(value) {
|
|
10
|
+
if (value == null) return JSON.stringify(value, null, 2);
|
|
11
|
+
if (Array.isArray(value) && value.every(isStateTreeNode)) {
|
|
12
|
+
return JSON.stringify(value.map((n) => getSnapshot(n)), null, 2);
|
|
13
|
+
}
|
|
14
|
+
if (isStateTreeNode(value)) return JSON.stringify(getSnapshot(value), null, 2);
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value, null, 2);
|
|
17
|
+
} catch {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ParticipantTab() {
|
|
23
|
+
const { effective } = useBlazeoConnection();
|
|
24
|
+
const example = useMemo(() => getExampleParticipants(), []);
|
|
25
|
+
const [calendarId, setCalendarId] = useState("");
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
const [error, setError] = useState("");
|
|
28
|
+
const [output, setOutput] = useState("");
|
|
29
|
+
|
|
30
|
+
async function handleFetchParticipants(e) {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
setError("");
|
|
33
|
+
setOutput("");
|
|
34
|
+
const id = calendarId.trim();
|
|
35
|
+
if (!id) {
|
|
36
|
+
setError("Enter a calendar id.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!effective.baseUrl) {
|
|
40
|
+
setError("Set Base URL in the connection card above.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
configureBlazeoFromEffective(effective);
|
|
44
|
+
setBusy(true);
|
|
45
|
+
try {
|
|
46
|
+
const res = await ParticipantModel.getAllByCalendar(id);
|
|
47
|
+
setOutput(toDisplayJson(res));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
50
|
+
} finally {
|
|
51
|
+
setBusy(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<div className="card">
|
|
58
|
+
<h2>Participants</h2>
|
|
59
|
+
<p className="muted small">
|
|
60
|
+
Example participants from <code>getExampleParticipants()</code> plus a fetch helper.
|
|
61
|
+
</p>
|
|
62
|
+
<pre className="pre-block">{JSON.stringify(example, null, 2)}</pre>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="card">
|
|
66
|
+
<h2>Fetch participants by calendar</h2>
|
|
67
|
+
<p className="muted small">
|
|
68
|
+
Calls <code>ParticipantModel.getAllByCalendar(calendarId)</code>.
|
|
69
|
+
</p>
|
|
70
|
+
<form onSubmit={handleFetchParticipants} className="form">
|
|
71
|
+
<label className="form__label">
|
|
72
|
+
<span>Calendar id</span>
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
className="form__input"
|
|
76
|
+
value={calendarId}
|
|
77
|
+
onChange={(e) => setCalendarId(e.target.value)}
|
|
78
|
+
/>
|
|
79
|
+
</label>
|
|
80
|
+
<button type="submit" className="btn btn--secondary" disabled={busy}>
|
|
81
|
+
{busy ? "Loading…" : "Fetch participants"}
|
|
82
|
+
</button>
|
|
83
|
+
</form>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{error ? (
|
|
87
|
+
<div className="card card--error" role="alert">
|
|
88
|
+
<h2>Error</h2>
|
|
89
|
+
<pre className="pre-block">{error}</pre>
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
92
|
+
|
|
93
|
+
{output ? (
|
|
94
|
+
<div className="card card--success">
|
|
95
|
+
<h2>Result</h2>
|
|
96
|
+
<pre className="pre-block">{output}</pre>
|
|
97
|
+
</div>
|
|
98
|
+
) : null}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { applyBlazeoClientConfig } from "appointment-client";
|
|
2
|
+
import { StrictMode } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { App } from "./App2.jsx";
|
|
5
|
+
import "./style.css";
|
|
6
|
+
|
|
7
|
+
/** Apply file defaults (`blazeoClientDefaults.ts`) before React mounts. */
|
|
8
|
+
applyBlazeoClientConfig();
|
|
9
|
+
|
|
10
|
+
const rootEl = document.getElementById("app");
|
|
11
|
+
if (!rootEl) {
|
|
12
|
+
throw new Error("Missing #app element");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createRoot(rootEl).render(
|
|
16
|
+
<StrictMode>
|
|
17
|
+
<App />
|
|
18
|
+
</StrictMode>
|
|
19
|
+
);
|