@porulle/plugin-appointments 0.1.0
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/README.md +60 -0
- package/dist/analytics-models.d.ts +4 -0
- package/dist/analytics-models.d.ts.map +1 -0
- package/dist/analytics-models.js +27 -0
- package/dist/hooks.d.ts +13 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +12 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/routes/availability.d.ts +8 -0
- package/dist/routes/availability.d.ts.map +1 -0
- package/dist/routes/availability.js +118 -0
- package/dist/routes/bookings.d.ts +8 -0
- package/dist/routes/bookings.d.ts.map +1 -0
- package/dist/routes/bookings.js +122 -0
- package/dist/routes/my-bookings.d.ts +6 -0
- package/dist/routes/my-bookings.d.ts.map +1 -0
- package/dist/routes/my-bookings.js +22 -0
- package/dist/routes/providers.d.ts +6 -0
- package/dist/routes/providers.d.ts.map +1 -0
- package/dist/routes/providers.js +84 -0
- package/dist/routes/services.d.ts +6 -0
- package/dist/routes/services.d.ts.map +1 -0
- package/dist/routes/services.js +68 -0
- package/dist/routes/util.d.ts +4 -0
- package/dist/routes/util.d.ts.map +1 -0
- package/dist/routes/util.js +12 -0
- package/dist/schema.d.ts +1476 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +128 -0
- package/dist/services/booking-service.d.ts +156 -0
- package/dist/services/booking-service.d.ts.map +1 -0
- package/dist/services/booking-service.js +249 -0
- package/dist/services/provider-service.d.ts +328 -0
- package/dist/services/provider-service.d.ts.map +1 -0
- package/dist/services/provider-service.js +152 -0
- package/dist/services/slot-generation.d.ts +15 -0
- package/dist/services/slot-generation.d.ts.map +1 -0
- package/dist/services/slot-generation.js +121 -0
- package/dist/services/slot-service.d.ts +11 -0
- package/dist/services/slot-service.d.ts.map +1 -0
- package/dist/services/slot-service.js +91 -0
- package/dist/tasks.d.ts +43 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +107 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +60 -0
- package/src/analytics-models.ts +30 -0
- package/src/hooks.ts +15 -0
- package/src/index.ts +74 -0
- package/src/routes/availability.ts +153 -0
- package/src/routes/bookings.ts +141 -0
- package/src/routes/my-bookings.ts +28 -0
- package/src/routes/providers.ts +97 -0
- package/src/routes/services.ts +78 -0
- package/src/routes/util.ts +11 -0
- package/src/schema.ts +144 -0
- package/src/services/booking-service.ts +340 -0
- package/src/services/provider-service.ts +223 -0
- package/src/services/slot-generation.ts +156 -0
- package/src/services/slot-service.ts +128 -0
- package/src/tasks.ts +141 -0
- package/src/types.ts +61 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { Db } from "../types.js";
|
|
2
|
+
export declare class ProviderService {
|
|
3
|
+
private db;
|
|
4
|
+
constructor(db: Db);
|
|
5
|
+
createServiceType(orgId: string, data: {
|
|
6
|
+
name: string;
|
|
7
|
+
slug: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
durationMinutes?: number;
|
|
10
|
+
priceCents?: number;
|
|
11
|
+
currency?: string;
|
|
12
|
+
bufferBeforeMinutes?: number;
|
|
13
|
+
bufferAfterMinutes?: number;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
id: string;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
name: string;
|
|
18
|
+
organizationId: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
description: string | null;
|
|
21
|
+
durationMinutes: number;
|
|
22
|
+
priceCents: number;
|
|
23
|
+
currency: string;
|
|
24
|
+
mode: string;
|
|
25
|
+
bufferBeforeMinutes: number;
|
|
26
|
+
bufferAfterMinutes: number;
|
|
27
|
+
maxAdvanceDays: number;
|
|
28
|
+
minNoticeMins: number;
|
|
29
|
+
isActive: boolean;
|
|
30
|
+
metadata: Record<string, unknown> | null;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
} | undefined>;
|
|
33
|
+
getServiceType(id: string): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
organizationId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
description: string | null;
|
|
39
|
+
durationMinutes: number;
|
|
40
|
+
priceCents: number;
|
|
41
|
+
currency: string;
|
|
42
|
+
mode: string;
|
|
43
|
+
bufferBeforeMinutes: number;
|
|
44
|
+
bufferAfterMinutes: number;
|
|
45
|
+
maxAdvanceDays: number;
|
|
46
|
+
minNoticeMins: number;
|
|
47
|
+
isActive: boolean;
|
|
48
|
+
metadata: Record<string, unknown> | null;
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
updatedAt: Date;
|
|
51
|
+
} | null>;
|
|
52
|
+
getServiceTypeBySlug(slug: string): Promise<{
|
|
53
|
+
id: string;
|
|
54
|
+
organizationId: string;
|
|
55
|
+
name: string;
|
|
56
|
+
slug: string;
|
|
57
|
+
description: string | null;
|
|
58
|
+
durationMinutes: number;
|
|
59
|
+
priceCents: number;
|
|
60
|
+
currency: string;
|
|
61
|
+
mode: string;
|
|
62
|
+
bufferBeforeMinutes: number;
|
|
63
|
+
bufferAfterMinutes: number;
|
|
64
|
+
maxAdvanceDays: number;
|
|
65
|
+
minNoticeMins: number;
|
|
66
|
+
isActive: boolean;
|
|
67
|
+
metadata: Record<string, unknown> | null;
|
|
68
|
+
createdAt: Date;
|
|
69
|
+
updatedAt: Date;
|
|
70
|
+
} | null>;
|
|
71
|
+
listServiceTypes(): Promise<{
|
|
72
|
+
id: string;
|
|
73
|
+
organizationId: string;
|
|
74
|
+
name: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
description: string | null;
|
|
77
|
+
durationMinutes: number;
|
|
78
|
+
priceCents: number;
|
|
79
|
+
currency: string;
|
|
80
|
+
mode: string;
|
|
81
|
+
bufferBeforeMinutes: number;
|
|
82
|
+
bufferAfterMinutes: number;
|
|
83
|
+
maxAdvanceDays: number;
|
|
84
|
+
minNoticeMins: number;
|
|
85
|
+
isActive: boolean;
|
|
86
|
+
metadata: Record<string, unknown> | null;
|
|
87
|
+
createdAt: Date;
|
|
88
|
+
updatedAt: Date;
|
|
89
|
+
}[]>;
|
|
90
|
+
updateServiceType(id: string, data: Partial<{
|
|
91
|
+
name: string;
|
|
92
|
+
description: string;
|
|
93
|
+
durationMinutes: number;
|
|
94
|
+
priceCents: number;
|
|
95
|
+
currency: string;
|
|
96
|
+
bufferBeforeMinutes: number;
|
|
97
|
+
bufferAfterMinutes: number;
|
|
98
|
+
isActive: boolean;
|
|
99
|
+
}>): Promise<{
|
|
100
|
+
id: string;
|
|
101
|
+
organizationId: string;
|
|
102
|
+
name: string;
|
|
103
|
+
slug: string;
|
|
104
|
+
description: string | null;
|
|
105
|
+
durationMinutes: number;
|
|
106
|
+
priceCents: number;
|
|
107
|
+
currency: string;
|
|
108
|
+
mode: string;
|
|
109
|
+
bufferBeforeMinutes: number;
|
|
110
|
+
bufferAfterMinutes: number;
|
|
111
|
+
maxAdvanceDays: number;
|
|
112
|
+
minNoticeMins: number;
|
|
113
|
+
isActive: boolean;
|
|
114
|
+
metadata: Record<string, unknown> | null;
|
|
115
|
+
createdAt: Date;
|
|
116
|
+
updatedAt: Date;
|
|
117
|
+
} | null>;
|
|
118
|
+
deleteServiceType(id: string): Promise<{
|
|
119
|
+
id: string;
|
|
120
|
+
createdAt: Date;
|
|
121
|
+
name: string;
|
|
122
|
+
organizationId: string;
|
|
123
|
+
slug: string;
|
|
124
|
+
description: string | null;
|
|
125
|
+
durationMinutes: number;
|
|
126
|
+
priceCents: number;
|
|
127
|
+
currency: string;
|
|
128
|
+
mode: string;
|
|
129
|
+
bufferBeforeMinutes: number;
|
|
130
|
+
bufferAfterMinutes: number;
|
|
131
|
+
maxAdvanceDays: number;
|
|
132
|
+
minNoticeMins: number;
|
|
133
|
+
isActive: boolean;
|
|
134
|
+
metadata: Record<string, unknown> | null;
|
|
135
|
+
updatedAt: Date;
|
|
136
|
+
} | null>;
|
|
137
|
+
createProvider(orgId: string, data: {
|
|
138
|
+
name: string;
|
|
139
|
+
email?: string;
|
|
140
|
+
phone?: string;
|
|
141
|
+
timezone?: string;
|
|
142
|
+
metadata?: Record<string, unknown>;
|
|
143
|
+
}): Promise<{
|
|
144
|
+
id: string;
|
|
145
|
+
createdAt: Date;
|
|
146
|
+
name: string;
|
|
147
|
+
organizationId: string;
|
|
148
|
+
isActive: boolean;
|
|
149
|
+
metadata: Record<string, unknown> | null;
|
|
150
|
+
updatedAt: Date;
|
|
151
|
+
userId: string | null;
|
|
152
|
+
bio: string | null;
|
|
153
|
+
email: string | null;
|
|
154
|
+
phone: string | null;
|
|
155
|
+
timezone: string;
|
|
156
|
+
} | undefined>;
|
|
157
|
+
getProvider(id: string): Promise<{
|
|
158
|
+
id: string;
|
|
159
|
+
organizationId: string;
|
|
160
|
+
userId: string | null;
|
|
161
|
+
name: string;
|
|
162
|
+
bio: string | null;
|
|
163
|
+
email: string | null;
|
|
164
|
+
phone: string | null;
|
|
165
|
+
timezone: string;
|
|
166
|
+
isActive: boolean;
|
|
167
|
+
metadata: Record<string, unknown> | null;
|
|
168
|
+
createdAt: Date;
|
|
169
|
+
updatedAt: Date;
|
|
170
|
+
} | null>;
|
|
171
|
+
listProviders(): Promise<{
|
|
172
|
+
id: string;
|
|
173
|
+
organizationId: string;
|
|
174
|
+
userId: string | null;
|
|
175
|
+
name: string;
|
|
176
|
+
bio: string | null;
|
|
177
|
+
email: string | null;
|
|
178
|
+
phone: string | null;
|
|
179
|
+
timezone: string;
|
|
180
|
+
isActive: boolean;
|
|
181
|
+
metadata: Record<string, unknown> | null;
|
|
182
|
+
createdAt: Date;
|
|
183
|
+
updatedAt: Date;
|
|
184
|
+
}[]>;
|
|
185
|
+
updateProvider(id: string, data: Partial<{
|
|
186
|
+
name: string;
|
|
187
|
+
email: string;
|
|
188
|
+
phone: string;
|
|
189
|
+
timezone: string;
|
|
190
|
+
isActive: boolean;
|
|
191
|
+
metadata: Record<string, unknown>;
|
|
192
|
+
}>): Promise<{
|
|
193
|
+
id: string;
|
|
194
|
+
organizationId: string;
|
|
195
|
+
userId: string | null;
|
|
196
|
+
name: string;
|
|
197
|
+
bio: string | null;
|
|
198
|
+
email: string | null;
|
|
199
|
+
phone: string | null;
|
|
200
|
+
timezone: string;
|
|
201
|
+
isActive: boolean;
|
|
202
|
+
metadata: Record<string, unknown> | null;
|
|
203
|
+
createdAt: Date;
|
|
204
|
+
updatedAt: Date;
|
|
205
|
+
} | null>;
|
|
206
|
+
deleteProvider(id: string): Promise<{
|
|
207
|
+
id: string;
|
|
208
|
+
createdAt: Date;
|
|
209
|
+
name: string;
|
|
210
|
+
organizationId: string;
|
|
211
|
+
isActive: boolean;
|
|
212
|
+
metadata: Record<string, unknown> | null;
|
|
213
|
+
updatedAt: Date;
|
|
214
|
+
userId: string | null;
|
|
215
|
+
bio: string | null;
|
|
216
|
+
email: string | null;
|
|
217
|
+
phone: string | null;
|
|
218
|
+
timezone: string;
|
|
219
|
+
} | null>;
|
|
220
|
+
linkServiceType(providerId: string, serviceTypeId: string, overrides?: {
|
|
221
|
+
customDurationMinutes?: number;
|
|
222
|
+
customPriceCents?: number;
|
|
223
|
+
}): Promise<{
|
|
224
|
+
id: string;
|
|
225
|
+
providerId: string;
|
|
226
|
+
serviceTypeId: string;
|
|
227
|
+
createdAt: Date;
|
|
228
|
+
customDurationMinutes: number | null;
|
|
229
|
+
customPriceCents: number | null;
|
|
230
|
+
} | undefined>;
|
|
231
|
+
getProviderServices(providerId: string): Promise<{
|
|
232
|
+
id: string;
|
|
233
|
+
providerId: string;
|
|
234
|
+
serviceTypeId: string;
|
|
235
|
+
customDurationMinutes: number | null;
|
|
236
|
+
customPriceCents: number | null;
|
|
237
|
+
createdAt: Date;
|
|
238
|
+
}[]>;
|
|
239
|
+
setWeeklyAvailability(providerId: string, schedules: Array<{
|
|
240
|
+
dayOfWeek: number;
|
|
241
|
+
startTime: string;
|
|
242
|
+
endTime: string;
|
|
243
|
+
}>): Promise<{
|
|
244
|
+
id: string;
|
|
245
|
+
providerId: string;
|
|
246
|
+
startTime: string;
|
|
247
|
+
createdAt: Date;
|
|
248
|
+
dayOfWeek: number;
|
|
249
|
+
endTime: string;
|
|
250
|
+
}[]>;
|
|
251
|
+
getWeeklyAvailability(providerId: string): Promise<{
|
|
252
|
+
id: string;
|
|
253
|
+
providerId: string;
|
|
254
|
+
dayOfWeek: number;
|
|
255
|
+
startTime: string;
|
|
256
|
+
endTime: string;
|
|
257
|
+
createdAt: Date;
|
|
258
|
+
}[]>;
|
|
259
|
+
addBreak(providerId: string, data: {
|
|
260
|
+
dayOfWeek?: number | undefined;
|
|
261
|
+
startTime: string;
|
|
262
|
+
endTime: string;
|
|
263
|
+
label?: string | undefined;
|
|
264
|
+
}): Promise<{
|
|
265
|
+
id: string;
|
|
266
|
+
providerId: string;
|
|
267
|
+
startTime: string;
|
|
268
|
+
createdAt: Date;
|
|
269
|
+
dayOfWeek: number | null;
|
|
270
|
+
endTime: string;
|
|
271
|
+
label: string | null;
|
|
272
|
+
} | undefined>;
|
|
273
|
+
getBreaks(providerId: string): Promise<{
|
|
274
|
+
id: string;
|
|
275
|
+
providerId: string;
|
|
276
|
+
dayOfWeek: number | null;
|
|
277
|
+
startTime: string;
|
|
278
|
+
endTime: string;
|
|
279
|
+
label: string | null;
|
|
280
|
+
createdAt: Date;
|
|
281
|
+
}[]>;
|
|
282
|
+
deleteBreak(id: string): Promise<{
|
|
283
|
+
id: string;
|
|
284
|
+
providerId: string;
|
|
285
|
+
startTime: string;
|
|
286
|
+
createdAt: Date;
|
|
287
|
+
dayOfWeek: number | null;
|
|
288
|
+
endTime: string;
|
|
289
|
+
label: string | null;
|
|
290
|
+
} | null>;
|
|
291
|
+
addOverride(providerId: string, data: {
|
|
292
|
+
date: string;
|
|
293
|
+
isAvailable: boolean;
|
|
294
|
+
startTime?: string | undefined;
|
|
295
|
+
endTime?: string | undefined;
|
|
296
|
+
reason?: string | undefined;
|
|
297
|
+
}): Promise<{
|
|
298
|
+
id: string;
|
|
299
|
+
providerId: string;
|
|
300
|
+
startTime: string | null;
|
|
301
|
+
createdAt: Date;
|
|
302
|
+
date: string;
|
|
303
|
+
endTime: string | null;
|
|
304
|
+
isAvailable: boolean;
|
|
305
|
+
reason: string | null;
|
|
306
|
+
} | undefined>;
|
|
307
|
+
getOverrides(providerId: string): Promise<{
|
|
308
|
+
id: string;
|
|
309
|
+
providerId: string;
|
|
310
|
+
date: string;
|
|
311
|
+
isAvailable: boolean;
|
|
312
|
+
startTime: string | null;
|
|
313
|
+
endTime: string | null;
|
|
314
|
+
reason: string | null;
|
|
315
|
+
createdAt: Date;
|
|
316
|
+
}[]>;
|
|
317
|
+
deleteOverride(id: string): Promise<{
|
|
318
|
+
id: string;
|
|
319
|
+
providerId: string;
|
|
320
|
+
startTime: string | null;
|
|
321
|
+
createdAt: Date;
|
|
322
|
+
date: string;
|
|
323
|
+
endTime: string | null;
|
|
324
|
+
isAvailable: boolean;
|
|
325
|
+
reason: string | null;
|
|
326
|
+
} | null>;
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=provider-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-service.d.ts","sourceRoot":"","sources":["../../src/services/provider-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { eq, desc } from "@porulle/core/drizzle";
|
|
2
|
+
import { serviceTypes, providers, providerServices, weeklyAvailability, availabilityOverrides, breaks, } from "../schema.js";
|
|
3
|
+
export class ProviderService {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
// ─── Service Types ──────────────────────────────────────────────────────────
|
|
9
|
+
async createServiceType(orgId, data) {
|
|
10
|
+
const [row] = await this.db.insert(serviceTypes).values({
|
|
11
|
+
organizationId: orgId,
|
|
12
|
+
name: data.name,
|
|
13
|
+
slug: data.slug,
|
|
14
|
+
description: data.description,
|
|
15
|
+
durationMinutes: data.durationMinutes ?? 30,
|
|
16
|
+
priceCents: data.priceCents ?? 0,
|
|
17
|
+
currency: data.currency ?? "USD",
|
|
18
|
+
bufferBeforeMinutes: data.bufferBeforeMinutes ?? 0,
|
|
19
|
+
bufferAfterMinutes: data.bufferAfterMinutes ?? 0,
|
|
20
|
+
}).returning();
|
|
21
|
+
return row;
|
|
22
|
+
}
|
|
23
|
+
async getServiceType(id) {
|
|
24
|
+
const [row] = await this.db.select().from(serviceTypes).where(eq(serviceTypes.id, id));
|
|
25
|
+
return row ?? null;
|
|
26
|
+
}
|
|
27
|
+
async getServiceTypeBySlug(slug) {
|
|
28
|
+
const [row] = await this.db.select().from(serviceTypes).where(eq(serviceTypes.slug, slug));
|
|
29
|
+
return row ?? null;
|
|
30
|
+
}
|
|
31
|
+
async listServiceTypes() {
|
|
32
|
+
return this.db.select().from(serviceTypes).where(eq(serviceTypes.isActive, true)).orderBy(desc(serviceTypes.createdAt));
|
|
33
|
+
}
|
|
34
|
+
async updateServiceType(id, data) {
|
|
35
|
+
const [updated] = await this.db.update(serviceTypes)
|
|
36
|
+
.set({ ...data, updatedAt: new Date() })
|
|
37
|
+
.where(eq(serviceTypes.id, id))
|
|
38
|
+
.returning();
|
|
39
|
+
return updated ?? null;
|
|
40
|
+
}
|
|
41
|
+
async deleteServiceType(id) {
|
|
42
|
+
const [deleted] = await this.db.delete(serviceTypes).where(eq(serviceTypes.id, id)).returning();
|
|
43
|
+
return deleted ?? null;
|
|
44
|
+
}
|
|
45
|
+
// ─── Providers ──────────────────────────────────────────────────────────────
|
|
46
|
+
async createProvider(orgId, data) {
|
|
47
|
+
const [row] = await this.db.insert(providers).values({
|
|
48
|
+
organizationId: orgId,
|
|
49
|
+
name: data.name,
|
|
50
|
+
email: data.email,
|
|
51
|
+
phone: data.phone,
|
|
52
|
+
timezone: data.timezone ?? "UTC",
|
|
53
|
+
metadata: data.metadata ?? {},
|
|
54
|
+
}).returning();
|
|
55
|
+
return row;
|
|
56
|
+
}
|
|
57
|
+
async getProvider(id) {
|
|
58
|
+
const [row] = await this.db.select().from(providers).where(eq(providers.id, id));
|
|
59
|
+
return row ?? null;
|
|
60
|
+
}
|
|
61
|
+
async listProviders() {
|
|
62
|
+
return this.db.select().from(providers).where(eq(providers.isActive, true)).orderBy(desc(providers.createdAt));
|
|
63
|
+
}
|
|
64
|
+
async updateProvider(id, data) {
|
|
65
|
+
const [updated] = await this.db.update(providers)
|
|
66
|
+
.set({ ...data, updatedAt: new Date() })
|
|
67
|
+
.where(eq(providers.id, id))
|
|
68
|
+
.returning();
|
|
69
|
+
return updated ?? null;
|
|
70
|
+
}
|
|
71
|
+
async deleteProvider(id) {
|
|
72
|
+
const [deleted] = await this.db.delete(providers).where(eq(providers.id, id)).returning();
|
|
73
|
+
return deleted ?? null;
|
|
74
|
+
}
|
|
75
|
+
// ─── Provider–ServiceType Links ─────────────────────────────────────────────
|
|
76
|
+
async linkServiceType(providerId, serviceTypeId, overrides) {
|
|
77
|
+
const [row] = await this.db.insert(providerServices).values({
|
|
78
|
+
providerId,
|
|
79
|
+
serviceTypeId,
|
|
80
|
+
customDurationMinutes: overrides?.customDurationMinutes,
|
|
81
|
+
customPriceCents: overrides?.customPriceCents,
|
|
82
|
+
}).returning();
|
|
83
|
+
return row;
|
|
84
|
+
}
|
|
85
|
+
async getProviderServices(providerId) {
|
|
86
|
+
return this.db.select().from(providerServices)
|
|
87
|
+
.where(eq(providerServices.providerId, providerId));
|
|
88
|
+
}
|
|
89
|
+
// ─── Weekly Availability ────────────────────────────────────────────────────
|
|
90
|
+
async setWeeklyAvailability(providerId, schedules) {
|
|
91
|
+
// Delete existing weekly availability for this provider
|
|
92
|
+
await this.db.delete(weeklyAvailability)
|
|
93
|
+
.where(eq(weeklyAvailability.providerId, providerId));
|
|
94
|
+
if (schedules.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const rows = await this.db.insert(weeklyAvailability)
|
|
97
|
+
.values(schedules.map((s) => ({
|
|
98
|
+
providerId,
|
|
99
|
+
dayOfWeek: s.dayOfWeek,
|
|
100
|
+
startTime: s.startTime,
|
|
101
|
+
endTime: s.endTime,
|
|
102
|
+
})))
|
|
103
|
+
.returning();
|
|
104
|
+
return rows;
|
|
105
|
+
}
|
|
106
|
+
async getWeeklyAvailability(providerId) {
|
|
107
|
+
return this.db.select().from(weeklyAvailability)
|
|
108
|
+
.where(eq(weeklyAvailability.providerId, providerId))
|
|
109
|
+
.orderBy(weeklyAvailability.dayOfWeek);
|
|
110
|
+
}
|
|
111
|
+
// ─── Breaks ─────────────────────────────────────────────────────────────────
|
|
112
|
+
async addBreak(providerId, data) {
|
|
113
|
+
const [row] = await this.db.insert(breaks).values({
|
|
114
|
+
providerId,
|
|
115
|
+
dayOfWeek: data.dayOfWeek,
|
|
116
|
+
startTime: data.startTime,
|
|
117
|
+
endTime: data.endTime,
|
|
118
|
+
label: data.label,
|
|
119
|
+
}).returning();
|
|
120
|
+
return row;
|
|
121
|
+
}
|
|
122
|
+
async getBreaks(providerId) {
|
|
123
|
+
return this.db.select().from(breaks)
|
|
124
|
+
.where(eq(breaks.providerId, providerId));
|
|
125
|
+
}
|
|
126
|
+
async deleteBreak(id) {
|
|
127
|
+
const [deleted] = await this.db.delete(breaks).where(eq(breaks.id, id)).returning();
|
|
128
|
+
return deleted ?? null;
|
|
129
|
+
}
|
|
130
|
+
// ─── Availability Overrides ─────────────────────────────────────────────────
|
|
131
|
+
async addOverride(providerId, data) {
|
|
132
|
+
const [row] = await this.db.insert(availabilityOverrides).values({
|
|
133
|
+
providerId,
|
|
134
|
+
date: data.date,
|
|
135
|
+
isAvailable: data.isAvailable,
|
|
136
|
+
startTime: data.startTime,
|
|
137
|
+
endTime: data.endTime,
|
|
138
|
+
reason: data.reason,
|
|
139
|
+
}).returning();
|
|
140
|
+
return row;
|
|
141
|
+
}
|
|
142
|
+
async getOverrides(providerId) {
|
|
143
|
+
return this.db.select().from(availabilityOverrides)
|
|
144
|
+
.where(eq(availabilityOverrides.providerId, providerId))
|
|
145
|
+
.orderBy(availabilityOverrides.date);
|
|
146
|
+
}
|
|
147
|
+
async deleteOverride(id) {
|
|
148
|
+
const [deleted] = await this.db.delete(availabilityOverrides)
|
|
149
|
+
.where(eq(availabilityOverrides.id, id)).returning();
|
|
150
|
+
return deleted ?? null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TimeSlot, SlotGenerationParams } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure function: generates available time slots for a given date.
|
|
4
|
+
*
|
|
5
|
+
* No DB access — all inputs passed as parameters.
|
|
6
|
+
* Handles: schedule hours, breaks, buffer time, existing bookings,
|
|
7
|
+
* min notice, max advance, and timezone conversion.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateSlots(params: SlotGenerationParams): TimeSlot[];
|
|
10
|
+
/**
|
|
11
|
+
* Parses a time string (HH:mm) on a given date in a specific timezone,
|
|
12
|
+
* returning a UTC Date object.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseTimeInTimezone(date: Date, time: string, timezone: string): Date;
|
|
15
|
+
//# sourceMappingURL=slot-generation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-generation.d.ts","sourceRoot":"","sources":["../../src/services/slot-generation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAElE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,oBAAoB,GAAG,QAAQ,EAAE,CAyEtE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAmEpF"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure function: generates available time slots for a given date.
|
|
3
|
+
*
|
|
4
|
+
* No DB access — all inputs passed as parameters.
|
|
5
|
+
* Handles: schedule hours, breaks, buffer time, existing bookings,
|
|
6
|
+
* min notice, max advance, and timezone conversion.
|
|
7
|
+
*/
|
|
8
|
+
export function generateSlots(params) {
|
|
9
|
+
const { date, schedule, durationMinutes, bufferBeforeMinutes = 0, bufferAfterMinutes = 0, breaks = [], existingBookings = [], minNoticeMinutes = 0, maxAdvanceDays = 0, timezone, now = new Date(), } = params;
|
|
10
|
+
// Day off — no slots
|
|
11
|
+
if (!schedule)
|
|
12
|
+
return [];
|
|
13
|
+
// Parse schedule times into absolute Date objects for the given date in the provider's timezone
|
|
14
|
+
const dayStart = parseTimeInTimezone(date, schedule.startTime, timezone);
|
|
15
|
+
const dayEnd = parseTimeInTimezone(date, schedule.endTime, timezone);
|
|
16
|
+
if (dayEnd <= dayStart)
|
|
17
|
+
return [];
|
|
18
|
+
// Generate candidate slots
|
|
19
|
+
const totalSlotMinutes = bufferBeforeMinutes + durationMinutes + bufferAfterMinutes;
|
|
20
|
+
const candidates = [];
|
|
21
|
+
let cursor = dayStart.getTime();
|
|
22
|
+
while (cursor + totalSlotMinutes * 60_000 <= dayEnd.getTime()) {
|
|
23
|
+
const slotStart = new Date(cursor + bufferBeforeMinutes * 60_000);
|
|
24
|
+
const slotEnd = new Date(slotStart.getTime() + durationMinutes * 60_000);
|
|
25
|
+
const blockEnd = new Date(slotEnd.getTime() + bufferAfterMinutes * 60_000);
|
|
26
|
+
candidates.push({ start: slotStart, end: slotEnd });
|
|
27
|
+
// Advance by duration + buffer after (so buffers don't overlap)
|
|
28
|
+
cursor = blockEnd.getTime();
|
|
29
|
+
}
|
|
30
|
+
// Filter out slots that overlap with breaks
|
|
31
|
+
const breakRanges = breaks.map((b) => ({
|
|
32
|
+
start: parseTimeInTimezone(date, b.startTime, timezone),
|
|
33
|
+
end: parseTimeInTimezone(date, b.endTime, timezone),
|
|
34
|
+
}));
|
|
35
|
+
let filtered = candidates.filter((slot) => {
|
|
36
|
+
// The slot's full block (including buffers) must not overlap any break
|
|
37
|
+
const blockStart = new Date(slot.start.getTime() - bufferBeforeMinutes * 60_000);
|
|
38
|
+
const blockEnd = new Date(slot.end.getTime() + bufferAfterMinutes * 60_000);
|
|
39
|
+
return !breakRanges.some((br) => blockStart < br.end && blockEnd > br.start);
|
|
40
|
+
});
|
|
41
|
+
// Filter out slots that overlap with existing bookings
|
|
42
|
+
filtered = filtered.filter((slot) => {
|
|
43
|
+
return !existingBookings.some((booking) => slot.start < booking.endTime && slot.end > booking.startTime);
|
|
44
|
+
});
|
|
45
|
+
// Filter out slots that violate min notice
|
|
46
|
+
if (minNoticeMinutes > 0) {
|
|
47
|
+
const earliestStart = new Date(now.getTime() + minNoticeMinutes * 60_000);
|
|
48
|
+
filtered = filtered.filter((slot) => slot.start >= earliestStart);
|
|
49
|
+
}
|
|
50
|
+
// Filter out slots that violate max advance
|
|
51
|
+
if (maxAdvanceDays > 0) {
|
|
52
|
+
const latestStart = new Date(now.getTime() + maxAdvanceDays * 24 * 60 * 60_000);
|
|
53
|
+
filtered = filtered.filter((slot) => slot.start <= latestStart);
|
|
54
|
+
}
|
|
55
|
+
return filtered;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parses a time string (HH:mm) on a given date in a specific timezone,
|
|
59
|
+
* returning a UTC Date object.
|
|
60
|
+
*/
|
|
61
|
+
export function parseTimeInTimezone(date, time, timezone) {
|
|
62
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
63
|
+
// Build a date string in the target timezone
|
|
64
|
+
const year = date.getFullYear();
|
|
65
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
66
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
67
|
+
const h = String(hours).padStart(2, "0");
|
|
68
|
+
const m = String(minutes).padStart(2, "0");
|
|
69
|
+
// Create date in the provider's timezone using Intl
|
|
70
|
+
const dateStr = `${year}-${month}-${day}T${h}:${m}:00`;
|
|
71
|
+
// Use a formatter to figure out the UTC offset for this timezone at this date/time
|
|
72
|
+
const utcDate = new Date(dateStr + "Z");
|
|
73
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
74
|
+
timeZone: timezone,
|
|
75
|
+
year: "numeric",
|
|
76
|
+
month: "2-digit",
|
|
77
|
+
day: "2-digit",
|
|
78
|
+
hour: "2-digit",
|
|
79
|
+
minute: "2-digit",
|
|
80
|
+
second: "2-digit",
|
|
81
|
+
hour12: false,
|
|
82
|
+
});
|
|
83
|
+
// Find the offset by comparing what UTC time produces the desired local time
|
|
84
|
+
// Binary search-like approach: try the UTC date, see what local time it maps to,
|
|
85
|
+
// and adjust.
|
|
86
|
+
const parts = formatter.formatToParts(utcDate);
|
|
87
|
+
const localHour = Number(parts.find((p) => p.type === "hour")?.value ?? 0);
|
|
88
|
+
const localMinute = Number(parts.find((p) => p.type === "minute")?.value ?? 0);
|
|
89
|
+
const localDay = Number(parts.find((p) => p.type === "day")?.value ?? 0);
|
|
90
|
+
const localMonth = Number(parts.find((p) => p.type === "month")?.value ?? 0);
|
|
91
|
+
// Calculate the difference between the local time at UTC and our target
|
|
92
|
+
const targetMinutes = hours * 60 + minutes;
|
|
93
|
+
let localMinutes = localHour * 60 + localMinute;
|
|
94
|
+
// Handle day/month boundary crossings
|
|
95
|
+
const targetDay = date.getDate();
|
|
96
|
+
if (localDay !== targetDay || localMonth !== date.getMonth() + 1) {
|
|
97
|
+
// Day changed — adjust by a full day if needed
|
|
98
|
+
if (localDay > targetDay || localMonth > date.getMonth() + 1) {
|
|
99
|
+
localMinutes += 24 * 60; // local is ahead
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
localMinutes -= 24 * 60; // local is behind
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const offsetMinutes = localMinutes - targetMinutes;
|
|
106
|
+
// The UTC time = target time + offset (since local = UTC - offset means UTC = local + offset)
|
|
107
|
+
// Actually: if local shows 14:30 when UTC is 09:00, offset = +5:30
|
|
108
|
+
// We want UTC time that corresponds to target local time
|
|
109
|
+
// UTC = targetTime - offset... no: local = UTC + offset => UTC = local - offset
|
|
110
|
+
// offsetMinutes = local - target. If we set UTC = utcDate - offsetMinutes * 60000
|
|
111
|
+
// that gives us: local_of_result = (utcDate - offsetMinutes*60000) + offset
|
|
112
|
+
// = utcDate + offset - offsetMinutes*60000
|
|
113
|
+
// = utcDate + offset - (local - target)*60000
|
|
114
|
+
// Hmm, let me think differently.
|
|
115
|
+
//
|
|
116
|
+
// We know: formatter(utcDate) = localTime
|
|
117
|
+
// We want: formatter(result) = targetTime
|
|
118
|
+
// So: result = utcDate - (localTime - targetTime) * 60000
|
|
119
|
+
// = utcDate - offsetMinutes * 60000
|
|
120
|
+
return new Date(utcDate.getTime() - offsetMinutes * 60_000);
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Db, TimeSlot } from "../types.js";
|
|
2
|
+
export declare class SlotService {
|
|
3
|
+
private db;
|
|
4
|
+
private defaults;
|
|
5
|
+
constructor(db: Db, defaults?: {
|
|
6
|
+
minNoticeMinutes: number;
|
|
7
|
+
maxAdvanceDays: number;
|
|
8
|
+
});
|
|
9
|
+
getAvailableSlots(providerId: string, serviceTypeId: string, date: Date, durationMinutes: number, bufferBeforeMinutes: number, bufferAfterMinutes: number, timezone: string, now?: Date): Promise<TimeSlot[]>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=slot-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-service.d.ts","sourceRoot":"","sources":["../../src/services/slot-service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAA6C,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE3F,qBAAa,WAAW;IAEpB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,QAAQ;gBADR,EAAE,EAAE,EAAE,EACN,QAAQ,GAAE;QAChB,gBAAgB,EAAE,MAAM,CAAC;QACzB,cAAc,EAAE,MAAM,CAAC;KACsB;IAG3C,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,IAAI,EACV,eAAe,EAAE,MAAM,EACvB,mBAAmB,EAAE,MAAM,EAC3B,kBAAkB,EAAE,MAAM,EAC1B,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,QAAQ,EAAE,CAAC;CAiGvB"}
|