@weaver-apps/calendar 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,746 @@
1
+ import { a as calendarIconNames, s as calendarHost, t as CALENDAR_GATEWAY } from "./weaver-apps-calendar-calendar-gateway-DqiL9VbI.mjs";
2
+ import * as i0 from "@angular/core";
3
+ import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, resource, signal } from "@angular/core";
4
+ import { TranslocoPipe, TranslocoService, translateObjectSignal } from "@jsverse/transloco";
5
+ import { toSignal } from "@angular/core/rxjs-interop";
6
+ import { ActivatedRoute } from "@angular/router";
7
+ import { map } from "rxjs";
8
+ import { DialogRef } from "@loomweaver/plugin-sdk";
9
+ import { DataGrid, ListFilter, openOnRow } from "@weaver-apps/data-grid";
10
+ const daysPerWeek = 7;
11
+ const sunday = 0;
12
+ const daysFromSundayToMonday = 6;
13
+ function startOfWeek(date) {
14
+ const start = new Date(date.getFullYear(), date.getMonth(), date.getDate());
15
+ const offset = start.getDay() === sunday ? daysFromSundayToMonday : start.getDay() - 1;
16
+ start.setDate(start.getDate() - offset);
17
+ return start;
18
+ }
19
+ function addWeeks(weekStart, weeks) {
20
+ const moved = new Date(weekStart);
21
+ moved.setDate(moved.getDate() + weeks * daysPerWeek);
22
+ return moved;
23
+ }
24
+ function weekRange(weekStart) {
25
+ return {
26
+ from: weekStart,
27
+ until: addWeeks(weekStart, 1)
28
+ };
29
+ }
30
+ function localDate(value) {
31
+ const pad = (part) => `${part}`.padStart(2, "0");
32
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
33
+ }
34
+ function localTime(value) {
35
+ const pad = (part) => `${part}`.padStart(2, "0");
36
+ return `${pad(value.getHours())}:${pad(value.getMinutes())}`;
37
+ }
38
+ function combine(date, time) {
39
+ const combined = /* @__PURE__ */ new Date(`${date}T${time}`);
40
+ return Number.isNaN(combined.getTime()) ? null : combined;
41
+ }
42
+ function activeLocale() {
43
+ const transloco = inject(TranslocoService);
44
+ return toSignal(transloco.langChanges$, { initialValue: transloco.getActiveLang() });
45
+ }
46
+ const AppointmentStatus = {
47
+ scheduled: "scheduled",
48
+ confirmed: "confirmed",
49
+ completed: "completed",
50
+ noShow: "noShow",
51
+ cancelled: "cancelled"
52
+ };
53
+ const defaultStart = "09:00";
54
+ const defaultEnd = "10:00";
55
+ const settableStatuses = [
56
+ AppointmentStatus.scheduled,
57
+ AppointmentStatus.confirmed,
58
+ AppointmentStatus.completed,
59
+ AppointmentStatus.noShow
60
+ ];
61
+ var AppointmentDialog = class AppointmentDialog {
62
+ dialog = inject(DialogRef);
63
+ gateway = inject(CALENDAR_GATEWAY);
64
+ data = this.dialog.data;
65
+ existing = this.data.appointment;
66
+ locale = activeLocale();
67
+ editing = this.existing !== null;
68
+ statuses = settableStatuses;
69
+ title = signal(this.existing?.title ?? "", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
70
+ date = signal(localDate(this.existing ? new Date(this.existing.start) : this.data.day), ...ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []);
71
+ start = signal(this.existing ? localTime(new Date(this.existing.start)) : defaultStart, ...ngDevMode ? [{ debugName: "start" }] : /* istanbul ignore next */ []);
72
+ endDate = signal(localDate(this.existing ? new Date(this.existing.end) : this.data.day), ...ngDevMode ? [{ debugName: "endDate" }] : /* istanbul ignore next */ []);
73
+ end = signal(this.existing ? localTime(new Date(this.existing.end)) : defaultEnd, ...ngDevMode ? [{ debugName: "end" }] : /* istanbul ignore next */ []);
74
+ location = signal(this.existing?.location ?? "", ...ngDevMode ? [{ debugName: "location" }] : /* istanbul ignore next */ []);
75
+ contactName = signal(this.existing?.contactName ?? "", ...ngDevMode ? [{ debugName: "contactName" }] : /* istanbul ignore next */ []);
76
+ contactPhone = signal(this.existing?.contactPhone ?? "", ...ngDevMode ? [{ debugName: "contactPhone" }] : /* istanbul ignore next */ []);
77
+ contactEmail = signal(this.existing?.contactEmail ?? "", ...ngDevMode ? [{ debugName: "contactEmail" }] : /* istanbul ignore next */ []);
78
+ description = signal(this.existing?.description ?? "", ...ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []);
79
+ status = signal(this.existing?.status ?? AppointmentStatus.scheduled, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
80
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
81
+ problem = signal({ kind: "none" }, ...ngDevMode ? [{ debugName: "problem" }] : /* istanbul ignore next */ []);
82
+ persisted = false;
83
+ problemKey = computed(() => {
84
+ const problem = this.problem();
85
+ return problem.kind === "message" ? problem.key : "";
86
+ }, ...ngDevMode ? [{ debugName: "problemKey" }] : /* istanbul ignore next */ []);
87
+ conflictTimes = computed(() => {
88
+ const problem = this.problem();
89
+ if (problem.kind !== "conflict") return [];
90
+ const format = new Intl.DateTimeFormat(this.locale(), {
91
+ dateStyle: "short",
92
+ timeStyle: "short"
93
+ });
94
+ return problem.conflicts.map((conflict) => `${format.format(new Date(conflict.start))} – ${format.format(new Date(conflict.end))}`);
95
+ }, ...ngDevMode ? [{ debugName: "conflictTimes" }] : /* istanbul ignore next */ []);
96
+ bind(target, event) {
97
+ target.set(event.target.value);
98
+ }
99
+ bindDate(event) {
100
+ const value = event.target.value;
101
+ if (this.endDate() === this.date()) this.endDate.set(value);
102
+ this.date.set(value);
103
+ }
104
+ chooseStatus(event) {
105
+ const chosen = settableStatuses.find((status) => status === event.target.value);
106
+ if (chosen) this.status.set(chosen);
107
+ }
108
+ close() {
109
+ this.dialog.close(this.persisted);
110
+ }
111
+ async save() {
112
+ const request = this.request();
113
+ if (!request) return;
114
+ this.saving.set(true);
115
+ try {
116
+ const outcome = this.existing ? await this.gateway.update(this.data.calendarId, this.existing.id, request) : await this.gateway.book(this.data.calendarId, request);
117
+ const final = await this.applyStatus(outcome);
118
+ this.settle(final);
119
+ } catch {
120
+ this.problem.set({
121
+ kind: "message",
122
+ key: "weaver-apps-calendar.errors.save"
123
+ });
124
+ } finally {
125
+ this.saving.set(false);
126
+ }
127
+ }
128
+ request() {
129
+ const startsAt = combine(this.date(), this.start());
130
+ const endsAt = combine(this.endDate(), this.end());
131
+ if (!this.title().trim()) {
132
+ this.problem.set({
133
+ kind: "message",
134
+ key: "weaver-apps-calendar.dialog.titleRequired"
135
+ });
136
+ return null;
137
+ }
138
+ if (!startsAt || !endsAt || endsAt <= startsAt) {
139
+ this.problem.set({
140
+ kind: "message",
141
+ key: "weaver-apps-calendar.dialog.periodInvalid"
142
+ });
143
+ return null;
144
+ }
145
+ return {
146
+ title: this.title().trim(),
147
+ start: startsAt.toISOString(),
148
+ end: endsAt.toISOString(),
149
+ description: this.optional(this.description()),
150
+ location: this.optional(this.location()),
151
+ contactName: this.optional(this.contactName()),
152
+ contactPhone: this.optional(this.contactPhone()),
153
+ contactEmail: this.optional(this.contactEmail())
154
+ };
155
+ }
156
+ async applyStatus(outcome) {
157
+ if (outcome.kind === "saved") {
158
+ this.existing = outcome.appointment;
159
+ this.persisted = true;
160
+ }
161
+ if (outcome.kind !== "saved" || outcome.appointment.status === this.status()) return outcome;
162
+ return this.gateway.setStatus(this.data.calendarId, outcome.appointment.id, this.status());
163
+ }
164
+ settle(outcome) {
165
+ switch (outcome.kind) {
166
+ case "saved":
167
+ this.dialog.close(true);
168
+ return;
169
+ case "conflict":
170
+ this.problem.set({
171
+ kind: "conflict",
172
+ conflicts: outcome.conflicts
173
+ });
174
+ return;
175
+ case "closed":
176
+ this.problem.set({
177
+ kind: "message",
178
+ key: "weaver-apps-calendar.dialog.closed"
179
+ });
180
+ return;
181
+ case "changed":
182
+ this.problem.set({
183
+ kind: "message",
184
+ key: "weaver-apps-calendar.dialog.changed"
185
+ });
186
+ return;
187
+ case "invalid":
188
+ this.problem.set({
189
+ kind: "message",
190
+ key: "weaver-apps-calendar.dialog.invalid"
191
+ });
192
+ return;
193
+ default: this.problem.set({
194
+ kind: "message",
195
+ key: "weaver-apps-calendar.dialog.missing"
196
+ });
197
+ }
198
+ }
199
+ optional(value) {
200
+ return value.trim() ? value.trim() : null;
201
+ }
202
+ static ɵfac = i0.ɵɵngDeclareFactory({
203
+ minVersion: "12.0.0",
204
+ version: "22.2.3",
205
+ ngImport: i0,
206
+ type: AppointmentDialog,
207
+ deps: [],
208
+ target: i0.ɵɵFactoryTarget.Component
209
+ });
210
+ static ɵcmp = i0.ɵɵngDeclareComponent({
211
+ minVersion: "17.0.0",
212
+ version: "22.2.3",
213
+ type: AppointmentDialog,
214
+ isStandalone: true,
215
+ selector: "wa-appointment-dialog",
216
+ ngImport: i0,
217
+ template: "<form class=\"flex flex-col gap-4\" (submit)=\"$event.preventDefault(); save()\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.title' | transloco }}\n <input class=\"lw-field\" required [value]=\"title()\" (input)=\"bind(title, $event)\" />\n </label>\n\n <div class=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.date' | transloco }}\n <input class=\"lw-field\" type=\"date\" required [value]=\"date()\" (input)=\"bindDate($event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.start' | transloco }}\n <input class=\"lw-field\" type=\"time\" required [value]=\"start()\" (input)=\"bind(start, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.endDate' | transloco }}\n <input class=\"lw-field\" type=\"date\" required [value]=\"endDate()\" (input)=\"bind(endDate, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.end' | transloco }}\n <input class=\"lw-field\" type=\"time\" required [value]=\"end()\" (input)=\"bind(end, $event)\" />\n </label>\n </div>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.contactName' | transloco }}\n <input class=\"lw-field\" [value]=\"contactName()\" (input)=\"bind(contactName, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.contactPhone' | transloco }}\n <input class=\"lw-field\" type=\"tel\" [value]=\"contactPhone()\" (input)=\"bind(contactPhone, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.contactEmail' | transloco }}\n <input class=\"lw-field\" type=\"email\" [value]=\"contactEmail()\" (input)=\"bind(contactEmail, $event)\" />\n </label>\n </div>\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.location' | transloco }}\n <input class=\"lw-field\" [value]=\"location()\" (input)=\"bind(location, $event)\" />\n </label>\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.description' | transloco }}\n <textarea class=\"lw-field\" rows=\"3\" [value]=\"description()\" (input)=\"bind(description, $event)\"></textarea>\n </label>\n\n @if (editing) {\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.status' | transloco }}\n <select class=\"lw-field\" [value]=\"status()\" (change)=\"chooseStatus($event)\">\n @for (choice of statuses; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === status()\">{{ 'weaver-apps-calendar.status.' + choice | transloco }}</option>\n }\n </select>\n </label>\n }\n\n @switch (problem().kind) {\n @case ('message') {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ problemKey() | transloco }}</p>\n }\n @case ('conflict') {\n <div class=\"text-sm text-negative\" role=\"alert\">\n <p>{{ 'weaver-apps-calendar.dialog.conflict' | transloco }}</p>\n <ul class=\"list-disc pl-5\">\n @for (time of conflictTimes(); track time) {\n <li>{{ time }}</li>\n }\n </ul>\n </div>\n }\n }\n\n <div class=\"flex justify-end gap-2\">\n <button type=\"button\" class=\"lw-btn lw-btn--default\" (click)=\"close()\">{{ 'weaver-apps-calendar.dialog.cancel' | transloco }}</button>\n <button type=\"submit\" class=\"lw-btn lw-btn--primary\" [disabled]=\"saving()\">{{ 'weaver-apps-calendar.dialog.save' | transloco }}</button>\n </div>\n</form>\n",
218
+ dependencies: [{
219
+ kind: "pipe",
220
+ type: TranslocoPipe,
221
+ name: "transloco"
222
+ }],
223
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
224
+ });
225
+ };
226
+ i0.ɵɵngDeclareClassMetadata({
227
+ minVersion: "12.0.0",
228
+ version: "22.2.3",
229
+ ngImport: i0,
230
+ type: AppointmentDialog,
231
+ decorators: [{
232
+ type: Component,
233
+ args: [{
234
+ selector: "wa-appointment-dialog",
235
+ imports: [TranslocoPipe],
236
+ changeDetection: ChangeDetectionStrategy.OnPush,
237
+ template: "<form class=\"flex flex-col gap-4\" (submit)=\"$event.preventDefault(); save()\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.title' | transloco }}\n <input class=\"lw-field\" required [value]=\"title()\" (input)=\"bind(title, $event)\" />\n </label>\n\n <div class=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.date' | transloco }}\n <input class=\"lw-field\" type=\"date\" required [value]=\"date()\" (input)=\"bindDate($event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.start' | transloco }}\n <input class=\"lw-field\" type=\"time\" required [value]=\"start()\" (input)=\"bind(start, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.endDate' | transloco }}\n <input class=\"lw-field\" type=\"date\" required [value]=\"endDate()\" (input)=\"bind(endDate, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.end' | transloco }}\n <input class=\"lw-field\" type=\"time\" required [value]=\"end()\" (input)=\"bind(end, $event)\" />\n </label>\n </div>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.contactName' | transloco }}\n <input class=\"lw-field\" [value]=\"contactName()\" (input)=\"bind(contactName, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.contactPhone' | transloco }}\n <input class=\"lw-field\" type=\"tel\" [value]=\"contactPhone()\" (input)=\"bind(contactPhone, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.contactEmail' | transloco }}\n <input class=\"lw-field\" type=\"email\" [value]=\"contactEmail()\" (input)=\"bind(contactEmail, $event)\" />\n </label>\n </div>\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.location' | transloco }}\n <input class=\"lw-field\" [value]=\"location()\" (input)=\"bind(location, $event)\" />\n </label>\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.description' | transloco }}\n <textarea class=\"lw-field\" rows=\"3\" [value]=\"description()\" (input)=\"bind(description, $event)\"></textarea>\n </label>\n\n @if (editing) {\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'weaver-apps-calendar.fields.status' | transloco }}\n <select class=\"lw-field\" [value]=\"status()\" (change)=\"chooseStatus($event)\">\n @for (choice of statuses; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === status()\">{{ 'weaver-apps-calendar.status.' + choice | transloco }}</option>\n }\n </select>\n </label>\n }\n\n @switch (problem().kind) {\n @case ('message') {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ problemKey() | transloco }}</p>\n }\n @case ('conflict') {\n <div class=\"text-sm text-negative\" role=\"alert\">\n <p>{{ 'weaver-apps-calendar.dialog.conflict' | transloco }}</p>\n <ul class=\"list-disc pl-5\">\n @for (time of conflictTimes(); track time) {\n <li>{{ time }}</li>\n }\n </ul>\n </div>\n }\n }\n\n <div class=\"flex justify-end gap-2\">\n <button type=\"button\" class=\"lw-btn lw-btn--default\" (click)=\"close()\">{{ 'weaver-apps-calendar.dialog.cancel' | transloco }}</button>\n <button type=\"submit\" class=\"lw-btn lw-btn--primary\" [disabled]=\"saving()\">{{ 'weaver-apps-calendar.dialog.save' | transloco }}</button>\n </div>\n</form>\n"
238
+ }]
239
+ }]
240
+ });
241
+ function appointmentMenu(appointment, runs) {
242
+ const cancellable = appointment.status !== AppointmentStatus.cancelled;
243
+ return [
244
+ {
245
+ label: "weaver-apps-calendar.actions.edit",
246
+ icon: "edit",
247
+ run: runs.edit
248
+ },
249
+ ...cancellable ? [{
250
+ label: "weaver-apps-calendar.actions.cancel",
251
+ icon: calendarIconNames.cancel,
252
+ run: runs.cancel
253
+ }] : [],
254
+ {
255
+ label: "weaver-apps-calendar.actions.delete",
256
+ icon: "trash",
257
+ run: runs.remove
258
+ }
259
+ ];
260
+ }
261
+ var AppointmentCell = class AppointmentCell {
262
+ params = null;
263
+ locale = signal("en", ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
264
+ kind = signal("title", ...ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []);
265
+ appointment = signal(null, ...ngDevMode ? [{ debugName: "appointment" }] : /* istanbul ignore next */ []);
266
+ cancelled = computed(() => this.appointment()?.status === "cancelled", ...ngDevMode ? [{ debugName: "cancelled" }] : /* istanbul ignore next */ []);
267
+ when = computed(() => {
268
+ const appointment = this.appointment();
269
+ if (!appointment) return {
270
+ day: "",
271
+ hours: ""
272
+ };
273
+ const day = new Intl.DateTimeFormat(this.locale(), {
274
+ weekday: "short",
275
+ day: "2-digit",
276
+ month: "2-digit",
277
+ year: "numeric"
278
+ });
279
+ const time = new Intl.DateTimeFormat(this.locale(), {
280
+ hour: "2-digit",
281
+ minute: "2-digit"
282
+ });
283
+ return {
284
+ day: day.format(new Date(appointment.start)),
285
+ hours: `${time.format(new Date(appointment.start))} – ${time.format(new Date(appointment.end))}`
286
+ };
287
+ }, ...ngDevMode ? [{ debugName: "when" }] : /* istanbul ignore next */ []);
288
+ agInit(params) {
289
+ this.params = params;
290
+ this.kind.set(params.kind);
291
+ this.appointment.set(params.data ?? null);
292
+ this.locale.set(params.locale());
293
+ }
294
+ refresh(params) {
295
+ this.agInit(params);
296
+ return true;
297
+ }
298
+ openActions(event) {
299
+ const appointment = this.appointment();
300
+ if (appointment) this.params?.actions({
301
+ appointment,
302
+ ...menuAnchor(event)
303
+ });
304
+ }
305
+ static ɵfac = i0.ɵɵngDeclareFactory({
306
+ minVersion: "12.0.0",
307
+ version: "22.2.3",
308
+ ngImport: i0,
309
+ type: AppointmentCell,
310
+ deps: [],
311
+ target: i0.ɵɵFactoryTarget.Component
312
+ });
313
+ static ɵcmp = i0.ɵɵngDeclareComponent({
314
+ minVersion: "17.0.0",
315
+ version: "22.2.3",
316
+ type: AppointmentCell,
317
+ isStandalone: true,
318
+ selector: "wa-appointment-cell",
319
+ ngImport: i0,
320
+ template: "@if (appointment(); as appointment) {\n @switch (kind()) {\n @case ('when') {\n <span class=\"font-medium\" [class.text-content]=\"!cancelled()\" [class.line-through]=\"cancelled()\">{{ when().day }}</span>\n <span class=\"ml-2 text-content-faint\" [class.line-through]=\"cancelled()\">{{ when().hours }}</span>\n }\n @case ('title') {\n <span class=\"font-medium\" [class.text-content]=\"!cancelled()\" [class.line-through]=\"cancelled()\">{{ appointment.title }}</span>\n }\n @case ('contact') {\n <span [class.line-through]=\"cancelled()\">{{ appointment.contactName ?? '–' }}</span>\n @if (appointment.contactPhone) {\n <span class=\"ml-2 text-content-faint\" [class.line-through]=\"cancelled()\">{{ appointment.contactPhone }}</span>\n }\n }\n @case ('location') {\n <span class=\"text-content-faint\" [class.line-through]=\"cancelled()\">{{ appointment.location ?? '–' }}</span>\n }\n @case ('status') {\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs text-content\" data-testid=\"appointment-status\">{{ 'weaver-apps-calendar.status.' + appointment.status | transloco }}</span>\n }\n @case ('actions') {\n <button type=\"button\" class=\"relative lw-btn lw-btn--ghost lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'weaver-apps-calendar.actions.more' | transloco\" (click)=\"openActions($event)\">\n <lw-icon name=\"more\" size=\"1rem\"></lw-icon>\n </button>\n }\n }\n}\n",
321
+ dependencies: [{
322
+ kind: "pipe",
323
+ type: TranslocoPipe,
324
+ name: "transloco"
325
+ }],
326
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
327
+ });
328
+ };
329
+ i0.ɵɵngDeclareClassMetadata({
330
+ minVersion: "12.0.0",
331
+ version: "22.2.3",
332
+ ngImport: i0,
333
+ type: AppointmentCell,
334
+ decorators: [{
335
+ type: Component,
336
+ args: [{
337
+ selector: "wa-appointment-cell",
338
+ imports: [TranslocoPipe],
339
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
340
+ changeDetection: ChangeDetectionStrategy.OnPush,
341
+ template: "@if (appointment(); as appointment) {\n @switch (kind()) {\n @case ('when') {\n <span class=\"font-medium\" [class.text-content]=\"!cancelled()\" [class.line-through]=\"cancelled()\">{{ when().day }}</span>\n <span class=\"ml-2 text-content-faint\" [class.line-through]=\"cancelled()\">{{ when().hours }}</span>\n }\n @case ('title') {\n <span class=\"font-medium\" [class.text-content]=\"!cancelled()\" [class.line-through]=\"cancelled()\">{{ appointment.title }}</span>\n }\n @case ('contact') {\n <span [class.line-through]=\"cancelled()\">{{ appointment.contactName ?? '–' }}</span>\n @if (appointment.contactPhone) {\n <span class=\"ml-2 text-content-faint\" [class.line-through]=\"cancelled()\">{{ appointment.contactPhone }}</span>\n }\n }\n @case ('location') {\n <span class=\"text-content-faint\" [class.line-through]=\"cancelled()\">{{ appointment.location ?? '–' }}</span>\n }\n @case ('status') {\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs text-content\" data-testid=\"appointment-status\">{{ 'weaver-apps-calendar.status.' + appointment.status | transloco }}</span>\n }\n @case ('actions') {\n <button type=\"button\" class=\"relative lw-btn lw-btn--ghost lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'weaver-apps-calendar.actions.more' | transloco\" (click)=\"openActions($event)\">\n <lw-icon name=\"more\" size=\"1rem\"></lw-icon>\n </button>\n }\n }\n}\n"
342
+ }]
343
+ }]
344
+ });
345
+ function menuAnchor(event) {
346
+ if (event.detail > 0) return {
347
+ x: event.clientX,
348
+ y: event.clientY
349
+ };
350
+ const bounds = event.currentTarget.getBoundingClientRect();
351
+ return {
352
+ x: bounds.left,
353
+ y: bounds.bottom
354
+ };
355
+ }
356
+ const appointmentColumn = {
357
+ when: "start",
358
+ title: "title",
359
+ contact: "contact",
360
+ location: "location",
361
+ status: "status",
362
+ actions: "actions"
363
+ };
364
+ const actionsWidthPx = 72;
365
+ const whenMinWidthPx = 230;
366
+ const textMinWidthPx = 110;
367
+ const locationMinWidthPx = 90;
368
+ const statusMinWidthPx = 112;
369
+ function byStart(left, right) {
370
+ return Date.parse(left ?? "") - Date.parse(right ?? "");
371
+ }
372
+ function statusOf(appointment) {
373
+ return appointment.status;
374
+ }
375
+ var AppointmentTable = class AppointmentTable {
376
+ table = translateObjectSignal("weaver-apps-calendar.table");
377
+ statuses = translateObjectSignal("weaver-apps-calendar.status");
378
+ api = signal(null, ...ngDevMode ? [{ debugName: "api" }] : /* istanbul ignore next */ []);
379
+ appointments = input.required(...ngDevMode ? [{ debugName: "appointments" }] : /* istanbul ignore next */ []);
380
+ locale = input.required(...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
381
+ editAppointment = output();
382
+ appointmentActions = output();
383
+ translated = computed(() => typeof this.table() === "object" && typeof this.table()["title"] === "string", ...ngDevMode ? [{ debugName: "translated" }] : /* istanbul ignore next */ []);
384
+ empty = computed(() => ({ noRowsToShow: this.table()["empty"] ?? "" }), ...ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []);
385
+ options = computed(() => ({
386
+ rowModelType: "clientSide",
387
+ columnDefs: this.columns(this.table()),
388
+ defaultColDef: {
389
+ sortable: false,
390
+ resizable: true,
391
+ suppressHeaderMenuButton: true
392
+ },
393
+ getRowId: (params) => params.data.id,
394
+ initialState: { sort: { sortModel: [{
395
+ colId: appointmentColumn.when,
396
+ sort: "asc"
397
+ }] } },
398
+ rowClassRules: { "text-content-faint": (params) => params.data?.status === AppointmentStatus.cancelled },
399
+ ...openOnRow((appointment) => this.editAppointment.emit(appointment), { except: [appointmentColumn.actions] })
400
+ }), ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
401
+ constructor() {
402
+ effect(() => {
403
+ const api = this.api();
404
+ const appointments = this.appointments();
405
+ if (api && !api.isDestroyed()) api.setGridOption("rowData", [...appointments]);
406
+ });
407
+ }
408
+ ready(api) {
409
+ this.api.set(api);
410
+ }
411
+ columns(table) {
412
+ return [
413
+ {
414
+ ...this.cell(appointmentColumn.when, "when", table["when"]),
415
+ field: "start",
416
+ sortable: true,
417
+ comparator: byStart,
418
+ flex: 2,
419
+ minWidth: whenMinWidthPx
420
+ },
421
+ {
422
+ ...this.cell(appointmentColumn.title, "title", table["title"]),
423
+ field: "title",
424
+ sortable: true,
425
+ flex: 2,
426
+ minWidth: textMinWidthPx
427
+ },
428
+ {
429
+ ...this.cell(appointmentColumn.contact, "contact", table["contact"]),
430
+ valueGetter: (params) => params.data?.contactName ?? "",
431
+ sortable: true,
432
+ flex: 2,
433
+ minWidth: textMinWidthPx
434
+ },
435
+ {
436
+ ...this.cell(appointmentColumn.location, "location", table["location"]),
437
+ field: "location",
438
+ sortable: true,
439
+ flex: 1,
440
+ minWidth: locationMinWidthPx
441
+ },
442
+ {
443
+ ...this.cell(appointmentColumn.status, "status", table["status"]),
444
+ field: "status",
445
+ sortable: true,
446
+ flex: 1,
447
+ minWidth: statusMinWidthPx,
448
+ filter: ListFilter,
449
+ filterParams: this.statusChoices()
450
+ },
451
+ {
452
+ ...this.cell(appointmentColumn.actions, "actions", ""),
453
+ width: actionsWidthPx,
454
+ resizable: false
455
+ }
456
+ ];
457
+ }
458
+ cell(colId, kind, headerName) {
459
+ return {
460
+ colId,
461
+ headerName: headerName ?? "",
462
+ cellRenderer: AppointmentCell,
463
+ cellRendererParams: {
464
+ kind,
465
+ locale: () => this.locale(),
466
+ actions: (actions) => this.appointmentActions.emit(actions)
467
+ }
468
+ };
469
+ }
470
+ statusChoices() {
471
+ const values = Object.values(AppointmentStatus);
472
+ return {
473
+ options: () => values.map((value) => ({
474
+ value,
475
+ label: this.statuses()[value] ?? value
476
+ })),
477
+ rowValue: statusOf
478
+ };
479
+ }
480
+ static ɵfac = i0.ɵɵngDeclareFactory({
481
+ minVersion: "12.0.0",
482
+ version: "22.2.3",
483
+ ngImport: i0,
484
+ type: AppointmentTable,
485
+ deps: [],
486
+ target: i0.ɵɵFactoryTarget.Component
487
+ });
488
+ static ɵcmp = i0.ɵɵngDeclareComponent({
489
+ minVersion: "17.0.0",
490
+ version: "22.2.3",
491
+ type: AppointmentTable,
492
+ isStandalone: true,
493
+ selector: "wa-appointment-table",
494
+ inputs: {
495
+ appointments: {
496
+ classPropertyName: "appointments",
497
+ publicName: "appointments",
498
+ isSignal: true,
499
+ isRequired: true,
500
+ transformFunction: null
501
+ },
502
+ locale: {
503
+ classPropertyName: "locale",
504
+ publicName: "locale",
505
+ isSignal: true,
506
+ isRequired: true,
507
+ transformFunction: null
508
+ }
509
+ },
510
+ outputs: {
511
+ editAppointment: "editAppointment",
512
+ appointmentActions: "appointmentActions"
513
+ },
514
+ host: { classAttribute: "block h-full w-full" },
515
+ ngImport: i0,
516
+ template: "@if (translated()) {\n <wa-data-grid [options]=\"options()\" [localeOverrides]=\"empty()\" (ready)=\"ready($event)\" />\n}\n",
517
+ dependencies: [{
518
+ kind: "component",
519
+ type: DataGrid,
520
+ selector: "wa-data-grid",
521
+ inputs: ["options", "localeOverrides"],
522
+ outputs: ["ready"]
523
+ }],
524
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
525
+ });
526
+ };
527
+ i0.ɵɵngDeclareClassMetadata({
528
+ minVersion: "12.0.0",
529
+ version: "22.2.3",
530
+ ngImport: i0,
531
+ type: AppointmentTable,
532
+ decorators: [{
533
+ type: Component,
534
+ args: [{
535
+ selector: "wa-appointment-table",
536
+ imports: [DataGrid],
537
+ changeDetection: ChangeDetectionStrategy.OnPush,
538
+ host: { class: "block h-full w-full" },
539
+ template: "@if (translated()) {\n <wa-data-grid [options]=\"options()\" [localeOverrides]=\"empty()\" (ready)=\"ready($event)\" />\n}\n"
540
+ }]
541
+ }],
542
+ ctorParameters: () => [],
543
+ propDecorators: {
544
+ appointments: [{
545
+ type: i0.Input,
546
+ args: [{
547
+ isSignal: true,
548
+ alias: "appointments",
549
+ required: true
550
+ }]
551
+ }],
552
+ locale: [{
553
+ type: i0.Input,
554
+ args: [{
555
+ isSignal: true,
556
+ alias: "locale",
557
+ required: true
558
+ }]
559
+ }],
560
+ editAppointment: [{
561
+ type: i0.Output,
562
+ args: ["editAppointment"]
563
+ }],
564
+ appointmentActions: [{
565
+ type: i0.Output,
566
+ args: ["appointmentActions"]
567
+ }]
568
+ }
569
+ });
570
+ const searchDelayMs = 300;
571
+ var CalendarPage = class CalendarPage {
572
+ gateway = inject(CALENDAR_GATEWAY);
573
+ route = inject(ActivatedRoute);
574
+ searchTimer;
575
+ locale = activeLocale();
576
+ calendarId = toSignal(this.route.paramMap.pipe(map((params) => params.get("calendarId") ?? "")), { initialValue: this.route.snapshot.paramMap.get("calendarId") ?? "" });
577
+ weekStart = signal(startOfWeek(/* @__PURE__ */ new Date()), ...ngDevMode ? [{ debugName: "weekStart" }] : /* istanbul ignore next */ []);
578
+ contact = signal("", ...ngDevMode ? [{ debugName: "contact" }] : /* istanbul ignore next */ []);
579
+ calendar = resource({
580
+ ...ngDevMode ? { debugName: "calendar" } : /* istanbul ignore next */ {},
581
+ params: () => this.calendarId(),
582
+ loader: ({ params }) => this.gateway.calendar(params)
583
+ });
584
+ appointments = resource({
585
+ ...ngDevMode ? { debugName: "appointments" } : /* istanbul ignore next */ {},
586
+ params: () => ({
587
+ calendarId: this.calendarId(),
588
+ range: weekRange(this.weekStart()),
589
+ contact: this.contact().trim()
590
+ }),
591
+ loader: ({ params, abortSignal }) => this.gateway.appointments(params.calendarId, params.range, params.contact || void 0, abortSignal)
592
+ });
593
+ constructor() {
594
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.searchTimer));
595
+ }
596
+ weekLabel = computed(() => {
597
+ const range = weekRange(this.weekStart());
598
+ const last = addWeeks(range.until, 0);
599
+ last.setDate(last.getDate() - 1);
600
+ const format = new Intl.DateTimeFormat(this.locale(), {
601
+ day: "2-digit",
602
+ month: "2-digit",
603
+ year: "numeric"
604
+ });
605
+ return `${format.format(range.from)} – ${format.format(last)}`;
606
+ }, ...ngDevMode ? [{ debugName: "weekLabel" }] : /* istanbul ignore next */ []);
607
+ previousWeek() {
608
+ this.weekStart.update((start) => addWeeks(start, -1));
609
+ }
610
+ nextWeek() {
611
+ this.weekStart.update((start) => addWeeks(start, 1));
612
+ }
613
+ thisWeek() {
614
+ this.weekStart.set(startOfWeek(/* @__PURE__ */ new Date()));
615
+ }
616
+ search(event) {
617
+ const value = event.target.value;
618
+ clearTimeout(this.searchTimer);
619
+ this.searchTimer = setTimeout(() => this.contact.set(value), searchDelayMs);
620
+ }
621
+ add() {
622
+ const today = /* @__PURE__ */ new Date();
623
+ const shown = weekRange(this.weekStart());
624
+ const day = today >= shown.from && today < shown.until ? today : this.weekStart();
625
+ this.openDialog({
626
+ calendarId: this.calendarId(),
627
+ appointment: null,
628
+ day
629
+ }, "weaver-apps-calendar.dialog.addTitle");
630
+ }
631
+ edit(appointment) {
632
+ this.openDialog({
633
+ calendarId: this.calendarId(),
634
+ appointment,
635
+ day: new Date(appointment.start)
636
+ }, "weaver-apps-calendar.dialog.editTitle");
637
+ }
638
+ actions({ appointment, x, y }) {
639
+ calendarHost.ui.openMenu(appointmentMenu(appointment, {
640
+ edit: () => this.edit(appointment),
641
+ cancel: () => void this.cancel(appointment),
642
+ remove: () => void this.remove(appointment)
643
+ }), {
644
+ x,
645
+ y
646
+ });
647
+ }
648
+ async cancel(appointment) {
649
+ const reason = await calendarHost.ui.prompt({
650
+ title: "weaver-apps-calendar.cancel.title",
651
+ message: "weaver-apps-calendar.cancel.message",
652
+ confirmLabel: "weaver-apps-calendar.cancel.confirm",
653
+ tone: "warning"
654
+ });
655
+ if (reason === null) return;
656
+ await this.attempt(async () => {
657
+ const outcome = await this.gateway.cancel(this.calendarId(), appointment.id, reason.trim() || null);
658
+ this.report(outcome.kind === "saved" ? "weaver-apps-calendar.cancel.done" : "weaver-apps-calendar.errors.save", outcome.kind === "saved");
659
+ });
660
+ }
661
+ async remove(appointment) {
662
+ if (!await calendarHost.ui.confirm({
663
+ title: "weaver-apps-calendar.delete.title",
664
+ message: "weaver-apps-calendar.delete.message",
665
+ confirmLabel: "weaver-apps-calendar.delete.confirm",
666
+ tone: "danger"
667
+ })) return;
668
+ await this.attempt(async () => {
669
+ await this.gateway.remove(this.calendarId(), appointment.id);
670
+ this.report("weaver-apps-calendar.delete.done", true);
671
+ });
672
+ }
673
+ async attempt(work) {
674
+ try {
675
+ await work();
676
+ } catch {
677
+ this.report("weaver-apps-calendar.errors.save", false);
678
+ }
679
+ }
680
+ openDialog(data, title) {
681
+ calendarHost.ui.open(AppointmentDialog, {
682
+ data,
683
+ title,
684
+ size: "lg",
685
+ dismiss: "explicit"
686
+ }).closed.then((saved) => {
687
+ if (saved) this.report("weaver-apps-calendar.dialog.saved", true);
688
+ });
689
+ }
690
+ report(message, success) {
691
+ calendarHost.ui.toast({
692
+ message,
693
+ kind: success ? "success" : "error"
694
+ });
695
+ this.appointments.reload();
696
+ }
697
+ static ɵfac = i0.ɵɵngDeclareFactory({
698
+ minVersion: "12.0.0",
699
+ version: "22.2.3",
700
+ ngImport: i0,
701
+ type: CalendarPage,
702
+ deps: [],
703
+ target: i0.ɵɵFactoryTarget.Component
704
+ });
705
+ static ɵcmp = i0.ɵɵngDeclareComponent({
706
+ minVersion: "17.0.0",
707
+ version: "22.2.3",
708
+ type: CalendarPage,
709
+ isStandalone: true,
710
+ selector: "wa-calendar-page",
711
+ ngImport: i0,
712
+ template: "<div class=\"flex h-full flex-col gap-4 p-6\">\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <h2 class=\"text-lg font-semibold text-content\">{{ calendar.value()?.name ?? ('weaver-apps-calendar.title' | transloco) }}</h2>\n <div class=\"flex flex-wrap items-center gap-2\">\n <input\n type=\"search\"\n class=\"lw-field w-56\"\n [attr.aria-label]=\"'weaver-apps-calendar.search.label' | transloco\"\n [attr.placeholder]=\"'weaver-apps-calendar.search.placeholder' | transloco\"\n (input)=\"search($event)\"\n />\n <button type=\"button\" class=\"lw-btn lw-btn--primary\" (click)=\"add()\">\n <lw-icon name=\"add\" size=\"1rem\"></lw-icon>\n {{ 'weaver-apps-calendar.actions.add' | transloco }}\n </button>\n </div>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'weaver-apps-calendar.week.previous' | transloco\" (click)=\"previousWeek()\">\n <lw-icon name=\"weaver-apps-calendar.previous\" size=\"1rem\"></lw-icon>\n <lw-tooltip [attr.text]=\"'weaver-apps-calendar.week.previous' | transloco\" position=\"bottom\"></lw-tooltip>\n </button>\n <button type=\"button\" class=\"lw-btn lw-btn--default lw-btn--sm\" (click)=\"thisWeek()\">{{ 'weaver-apps-calendar.week.current' | transloco }}</button>\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'weaver-apps-calendar.week.next' | transloco\" (click)=\"nextWeek()\">\n <lw-icon name=\"weaver-apps-calendar.next\" size=\"1rem\"></lw-icon>\n <lw-tooltip [attr.text]=\"'weaver-apps-calendar.week.next' | transloco\" position=\"bottom\"></lw-tooltip>\n </button>\n <span class=\"text-sm font-medium text-content\" data-testid=\"week-label\">{{ weekLabel() }}</span>\n </div>\n\n @if (appointments.error()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ 'weaver-apps-calendar.errors.load' | transloco }}</p>\n } @else {\n <div class=\"min-h-80 flex-1\">\n <wa-appointment-table\n [appointments]=\"appointments.value() ?? []\"\n [locale]=\"locale()\"\n (editAppointment)=\"edit($event)\"\n (appointmentActions)=\"actions($event)\"\n />\n </div>\n }\n</div>\n",
713
+ dependencies: [{
714
+ kind: "component",
715
+ type: AppointmentTable,
716
+ selector: "wa-appointment-table",
717
+ inputs: ["appointments", "locale"],
718
+ outputs: ["editAppointment", "appointmentActions"]
719
+ }, {
720
+ kind: "pipe",
721
+ type: TranslocoPipe,
722
+ name: "transloco"
723
+ }],
724
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
725
+ });
726
+ };
727
+ i0.ɵɵngDeclareClassMetadata({
728
+ minVersion: "12.0.0",
729
+ version: "22.2.3",
730
+ ngImport: i0,
731
+ type: CalendarPage,
732
+ decorators: [{
733
+ type: Component,
734
+ args: [{
735
+ selector: "wa-calendar-page",
736
+ imports: [TranslocoPipe, AppointmentTable],
737
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
738
+ changeDetection: ChangeDetectionStrategy.OnPush,
739
+ template: "<div class=\"flex h-full flex-col gap-4 p-6\">\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <h2 class=\"text-lg font-semibold text-content\">{{ calendar.value()?.name ?? ('weaver-apps-calendar.title' | transloco) }}</h2>\n <div class=\"flex flex-wrap items-center gap-2\">\n <input\n type=\"search\"\n class=\"lw-field w-56\"\n [attr.aria-label]=\"'weaver-apps-calendar.search.label' | transloco\"\n [attr.placeholder]=\"'weaver-apps-calendar.search.placeholder' | transloco\"\n (input)=\"search($event)\"\n />\n <button type=\"button\" class=\"lw-btn lw-btn--primary\" (click)=\"add()\">\n <lw-icon name=\"add\" size=\"1rem\"></lw-icon>\n {{ 'weaver-apps-calendar.actions.add' | transloco }}\n </button>\n </div>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'weaver-apps-calendar.week.previous' | transloco\" (click)=\"previousWeek()\">\n <lw-icon name=\"weaver-apps-calendar.previous\" size=\"1rem\"></lw-icon>\n <lw-tooltip [attr.text]=\"'weaver-apps-calendar.week.previous' | transloco\" position=\"bottom\"></lw-tooltip>\n </button>\n <button type=\"button\" class=\"lw-btn lw-btn--default lw-btn--sm\" (click)=\"thisWeek()\">{{ 'weaver-apps-calendar.week.current' | transloco }}</button>\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'weaver-apps-calendar.week.next' | transloco\" (click)=\"nextWeek()\">\n <lw-icon name=\"weaver-apps-calendar.next\" size=\"1rem\"></lw-icon>\n <lw-tooltip [attr.text]=\"'weaver-apps-calendar.week.next' | transloco\" position=\"bottom\"></lw-tooltip>\n </button>\n <span class=\"text-sm font-medium text-content\" data-testid=\"week-label\">{{ weekLabel() }}</span>\n </div>\n\n @if (appointments.error()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ 'weaver-apps-calendar.errors.load' | transloco }}</p>\n } @else {\n <div class=\"min-h-80 flex-1\">\n <wa-appointment-table\n [appointments]=\"appointments.value() ?? []\"\n [locale]=\"locale()\"\n (editAppointment)=\"edit($event)\"\n (appointmentActions)=\"actions($event)\"\n />\n </div>\n }\n</div>\n"
740
+ }]
741
+ }],
742
+ ctorParameters: () => []
743
+ });
744
+ export { CalendarPage };
745
+
746
+ //# sourceMappingURL=weaver-apps-calendar-calendar-page-BjJzPlTa.mjs.map