gorombo-payload-appointments 1.0.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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/collections/Appointments.d.ts +2 -0
- package/dist/collections/Appointments.js +165 -0
- package/dist/collections/Appointments.js.map +1 -0
- package/dist/collections/GuestCustomers.d.ts +2 -0
- package/dist/collections/GuestCustomers.js +106 -0
- package/dist/collections/GuestCustomers.js.map +1 -0
- package/dist/collections/Services.d.ts +2 -0
- package/dist/collections/Services.js +147 -0
- package/dist/collections/Services.js.map +1 -0
- package/dist/collections/TeamMembers.d.ts +2 -0
- package/dist/collections/TeamMembers.js +184 -0
- package/dist/collections/TeamMembers.js.map +1 -0
- package/dist/components/BeforeDashboardClient.d.ts +2 -0
- package/dist/components/BeforeDashboardClient.js +162 -0
- package/dist/components/BeforeDashboardClient.js.map +1 -0
- package/dist/components/BeforeDashboardServer.d.ts +2 -0
- package/dist/components/BeforeDashboardServer.js +22 -0
- package/dist/components/BeforeDashboardServer.js.map +1 -0
- package/dist/components/BeforeDashboardServer.module.css +5 -0
- package/dist/components/calendar/Calendar.module.css +506 -0
- package/dist/components/calendar/CalendarContainer.d.ts +3 -0
- package/dist/components/calendar/CalendarContainer.js +246 -0
- package/dist/components/calendar/CalendarContainer.js.map +1 -0
- package/dist/components/calendar/DayView.d.ts +3 -0
- package/dist/components/calendar/DayView.js +192 -0
- package/dist/components/calendar/DayView.js.map +1 -0
- package/dist/components/calendar/EventPopover.d.ts +3 -0
- package/dist/components/calendar/EventPopover.js +257 -0
- package/dist/components/calendar/EventPopover.js.map +1 -0
- package/dist/components/calendar/EventRenderer.d.ts +3 -0
- package/dist/components/calendar/EventRenderer.js +76 -0
- package/dist/components/calendar/EventRenderer.js.map +1 -0
- package/dist/components/calendar/WeekView.d.ts +3 -0
- package/dist/components/calendar/WeekView.js +203 -0
- package/dist/components/calendar/WeekView.js.map +1 -0
- package/dist/components/calendar/index.d.ts +6 -0
- package/dist/components/calendar/index.js +7 -0
- package/dist/components/calendar/index.js.map +1 -0
- package/dist/components/calendar/types.d.ts +69 -0
- package/dist/components/calendar/types.js +3 -0
- package/dist/components/calendar/types.js.map +1 -0
- package/dist/endpoints/customEndpointHandler.d.ts +2 -0
- package/dist/endpoints/customEndpointHandler.js +7 -0
- package/dist/endpoints/customEndpointHandler.js.map +1 -0
- package/dist/endpoints/getAvailableSlots.d.ts +12 -0
- package/dist/endpoints/getAvailableSlots.js +291 -0
- package/dist/endpoints/getAvailableSlots.js.map +1 -0
- package/dist/exports/client.d.ts +3 -0
- package/dist/exports/client.js +4 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.js +3 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/globals/OpeningTimes.d.ts +2 -0
- package/dist/globals/OpeningTimes.js +196 -0
- package/dist/globals/OpeningTimes.js.map +1 -0
- package/dist/hooks/addAdminTitle.d.ts +7 -0
- package/dist/hooks/addAdminTitle.js +86 -0
- package/dist/hooks/addAdminTitle.js.map +1 -0
- package/dist/hooks/sendCustomerEmail.d.ts +6 -0
- package/dist/hooks/sendCustomerEmail.js +351 -0
- package/dist/hooks/sendCustomerEmail.js.map +1 -0
- package/dist/hooks/setEndDateTime.d.ts +6 -0
- package/dist/hooks/setEndDateTime.js +44 -0
- package/dist/hooks/setEndDateTime.js.map +1 -0
- package/dist/hooks/validateCustomerOrGuest.d.ts +6 -0
- package/dist/hooks/validateCustomerOrGuest.js +21 -0
- package/dist/hooks/validateCustomerOrGuest.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/package.json +135 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
export const OpeningTimes = {
|
|
2
|
+
slug: 'opening-times',
|
|
3
|
+
access: {
|
|
4
|
+
read: ()=>true,
|
|
5
|
+
update: ({ req })=>!!req.user
|
|
6
|
+
},
|
|
7
|
+
admin: {
|
|
8
|
+
description: 'Configure business hours and scheduling defaults',
|
|
9
|
+
group: 'Scheduling'
|
|
10
|
+
},
|
|
11
|
+
fields: [
|
|
12
|
+
{
|
|
13
|
+
name: 'timezone',
|
|
14
|
+
type: 'text',
|
|
15
|
+
admin: {
|
|
16
|
+
description: 'Business timezone (IANA format, e.g., "America/New_York")'
|
|
17
|
+
},
|
|
18
|
+
defaultValue: 'America/New_York',
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'schedule',
|
|
23
|
+
type: 'array',
|
|
24
|
+
admin: {
|
|
25
|
+
description: 'Weekly schedule - one entry per day'
|
|
26
|
+
},
|
|
27
|
+
fields: [
|
|
28
|
+
{
|
|
29
|
+
name: 'day',
|
|
30
|
+
type: 'select',
|
|
31
|
+
options: [
|
|
32
|
+
{
|
|
33
|
+
label: 'Monday',
|
|
34
|
+
value: 'monday'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'Tuesday',
|
|
38
|
+
value: 'tuesday'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: 'Wednesday',
|
|
42
|
+
value: 'wednesday'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'Thursday',
|
|
46
|
+
value: 'thursday'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: 'Friday',
|
|
50
|
+
value: 'friday'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'Saturday',
|
|
54
|
+
value: 'saturday'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: 'Sunday',
|
|
58
|
+
value: 'sunday'
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
required: true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'isOpen',
|
|
65
|
+
type: 'checkbox',
|
|
66
|
+
admin: {
|
|
67
|
+
description: 'Business is open on this day'
|
|
68
|
+
},
|
|
69
|
+
defaultValue: true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'openTime',
|
|
73
|
+
type: 'text',
|
|
74
|
+
admin: {
|
|
75
|
+
condition: (data, siblingData)=>siblingData?.isOpen === true,
|
|
76
|
+
description: 'Opening time (HH:MM format)'
|
|
77
|
+
},
|
|
78
|
+
defaultValue: '09:00'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'closeTime',
|
|
82
|
+
type: 'text',
|
|
83
|
+
admin: {
|
|
84
|
+
condition: (data, siblingData)=>siblingData?.isOpen === true,
|
|
85
|
+
description: 'Closing time (HH:MM format)'
|
|
86
|
+
},
|
|
87
|
+
defaultValue: '17:00'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'breakStart',
|
|
91
|
+
type: 'text',
|
|
92
|
+
admin: {
|
|
93
|
+
condition: (data, siblingData)=>siblingData?.isOpen === true,
|
|
94
|
+
description: 'Break start time (optional, HH:MM format)'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'breakEnd',
|
|
99
|
+
type: 'text',
|
|
100
|
+
admin: {
|
|
101
|
+
condition: (data, siblingData)=>siblingData?.isOpen === true,
|
|
102
|
+
description: 'Break end time (optional, HH:MM format)'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
maxRows: 7,
|
|
107
|
+
minRows: 7,
|
|
108
|
+
required: true
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'slotDuration',
|
|
112
|
+
type: 'number',
|
|
113
|
+
admin: {
|
|
114
|
+
description: 'Default time slot duration in minutes',
|
|
115
|
+
step: 5
|
|
116
|
+
},
|
|
117
|
+
defaultValue: 30,
|
|
118
|
+
max: 120,
|
|
119
|
+
min: 5,
|
|
120
|
+
required: true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'bufferBetweenAppointments',
|
|
124
|
+
type: 'number',
|
|
125
|
+
admin: {
|
|
126
|
+
description: 'Default buffer time between appointments (minutes)',
|
|
127
|
+
step: 5
|
|
128
|
+
},
|
|
129
|
+
defaultValue: 0,
|
|
130
|
+
min: 0
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'maxAdvanceBookingDays',
|
|
134
|
+
type: 'number',
|
|
135
|
+
admin: {
|
|
136
|
+
description: 'Maximum days in advance a customer can book'
|
|
137
|
+
},
|
|
138
|
+
defaultValue: 30,
|
|
139
|
+
min: 1
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'minAdvanceBookingHours',
|
|
143
|
+
type: 'number',
|
|
144
|
+
admin: {
|
|
145
|
+
description: 'Minimum hours notice required for booking'
|
|
146
|
+
},
|
|
147
|
+
defaultValue: 1,
|
|
148
|
+
min: 0
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'allowGuestBooking',
|
|
152
|
+
type: 'checkbox',
|
|
153
|
+
admin: {
|
|
154
|
+
description: 'Allow non-registered users to book appointments'
|
|
155
|
+
},
|
|
156
|
+
defaultValue: true
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'requireApproval',
|
|
160
|
+
type: 'checkbox',
|
|
161
|
+
admin: {
|
|
162
|
+
description: 'Appointments require admin approval before confirmation'
|
|
163
|
+
},
|
|
164
|
+
defaultValue: false
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'sendConfirmationEmails',
|
|
168
|
+
type: 'checkbox',
|
|
169
|
+
admin: {
|
|
170
|
+
description: 'Automatically send confirmation emails'
|
|
171
|
+
},
|
|
172
|
+
defaultValue: true
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'sendReminderEmails',
|
|
176
|
+
type: 'checkbox',
|
|
177
|
+
admin: {
|
|
178
|
+
description: 'Automatically send reminder emails'
|
|
179
|
+
},
|
|
180
|
+
defaultValue: true
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'reminderHoursBefore',
|
|
184
|
+
type: 'number',
|
|
185
|
+
admin: {
|
|
186
|
+
condition: (data)=>data?.sendReminderEmails === true,
|
|
187
|
+
description: 'Hours before appointment to send reminder'
|
|
188
|
+
},
|
|
189
|
+
defaultValue: 24,
|
|
190
|
+
min: 1
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
label: 'Opening Times'
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=OpeningTimes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/globals/OpeningTimes.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\n\nexport const OpeningTimes: GlobalConfig = {\n slug: 'opening-times',\n access: {\n read: () => true,\n update: ({ req }) => !!req.user,\n },\n admin: {\n description: 'Configure business hours and scheduling defaults',\n group: 'Scheduling',\n },\n fields: [\n {\n name: 'timezone',\n type: 'text',\n admin: {\n description: 'Business timezone (IANA format, e.g., \"America/New_York\")',\n },\n defaultValue: 'America/New_York',\n required: true,\n },\n {\n name: 'schedule',\n type: 'array',\n admin: {\n description: 'Weekly schedule - one entry per day',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n options: [\n { label: 'Monday', value: 'monday' },\n { label: 'Tuesday', value: 'tuesday' },\n { label: 'Wednesday', value: 'wednesday' },\n { label: 'Thursday', value: 'thursday' },\n { label: 'Friday', value: 'friday' },\n { label: 'Saturday', value: 'saturday' },\n { label: 'Sunday', value: 'sunday' },\n ],\n required: true,\n },\n {\n name: 'isOpen',\n type: 'checkbox',\n admin: {\n description: 'Business is open on this day',\n },\n defaultValue: true,\n },\n {\n name: 'openTime',\n type: 'text',\n admin: {\n condition: (data, siblingData) => siblingData?.isOpen === true,\n description: 'Opening time (HH:MM format)',\n },\n defaultValue: '09:00',\n },\n {\n name: 'closeTime',\n type: 'text',\n admin: {\n condition: (data, siblingData) => siblingData?.isOpen === true,\n description: 'Closing time (HH:MM format)',\n },\n defaultValue: '17:00',\n },\n {\n name: 'breakStart',\n type: 'text',\n admin: {\n condition: (data, siblingData) => siblingData?.isOpen === true,\n description: 'Break start time (optional, HH:MM format)',\n },\n },\n {\n name: 'breakEnd',\n type: 'text',\n admin: {\n condition: (data, siblingData) => siblingData?.isOpen === true,\n description: 'Break end time (optional, HH:MM format)',\n },\n },\n ],\n maxRows: 7,\n minRows: 7,\n required: true,\n },\n {\n name: 'slotDuration',\n type: 'number',\n admin: {\n description: 'Default time slot duration in minutes',\n step: 5,\n },\n defaultValue: 30,\n max: 120,\n min: 5,\n required: true,\n },\n {\n name: 'bufferBetweenAppointments',\n type: 'number',\n admin: {\n description: 'Default buffer time between appointments (minutes)',\n step: 5,\n },\n defaultValue: 0,\n min: 0,\n },\n {\n name: 'maxAdvanceBookingDays',\n type: 'number',\n admin: {\n description: 'Maximum days in advance a customer can book',\n },\n defaultValue: 30,\n min: 1,\n },\n {\n name: 'minAdvanceBookingHours',\n type: 'number',\n admin: {\n description: 'Minimum hours notice required for booking',\n },\n defaultValue: 1,\n min: 0,\n },\n {\n name: 'allowGuestBooking',\n type: 'checkbox',\n admin: {\n description: 'Allow non-registered users to book appointments',\n },\n defaultValue: true,\n },\n {\n name: 'requireApproval',\n type: 'checkbox',\n admin: {\n description: 'Appointments require admin approval before confirmation',\n },\n defaultValue: false,\n },\n {\n name: 'sendConfirmationEmails',\n type: 'checkbox',\n admin: {\n description: 'Automatically send confirmation emails',\n },\n defaultValue: true,\n },\n {\n name: 'sendReminderEmails',\n type: 'checkbox',\n admin: {\n description: 'Automatically send reminder emails',\n },\n defaultValue: true,\n },\n {\n name: 'reminderHoursBefore',\n type: 'number',\n admin: {\n condition: (data) => data?.sendReminderEmails === true,\n description: 'Hours before appointment to send reminder',\n },\n defaultValue: 24,\n min: 1,\n },\n ],\n label: 'Opening Times',\n}\n"],"names":["OpeningTimes","slug","access","read","update","req","user","admin","description","group","fields","name","type","defaultValue","required","options","label","value","condition","data","siblingData","isOpen","maxRows","minRows","step","max","min","sendReminderEmails"],"mappings":"AAEA,OAAO,MAAMA,eAA6B;IACxCC,MAAM;IACNC,QAAQ;QACNC,MAAM,IAAM;QACZC,QAAQ,CAAC,EAAEC,GAAG,EAAE,GAAK,CAAC,CAACA,IAAIC,IAAI;IACjC;IACAC,OAAO;QACLC,aAAa;QACbC,OAAO;IACT;IACAC,QAAQ;QACN;YACEC,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;YACdC,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAE,QAAQ;gBACN;oBACEC,MAAM;oBACNC,MAAM;oBACNG,SAAS;wBACP;4BAAEC,OAAO;4BAAUC,OAAO;wBAAS;wBACnC;4BAAED,OAAO;4BAAWC,OAAO;wBAAU;wBACrC;4BAAED,OAAO;4BAAaC,OAAO;wBAAY;wBACzC;4BAAED,OAAO;4BAAYC,OAAO;wBAAW;wBACvC;4BAAED,OAAO;4BAAUC,OAAO;wBAAS;wBACnC;4BAAED,OAAO;4BAAYC,OAAO;wBAAW;wBACvC;4BAAED,OAAO;4BAAUC,OAAO;wBAAS;qBACpC;oBACDH,UAAU;gBACZ;gBACA;oBACEH,MAAM;oBACNC,MAAM;oBACNL,OAAO;wBACLC,aAAa;oBACf;oBACAK,cAAc;gBAChB;gBACA;oBACEF,MAAM;oBACNC,MAAM;oBACNL,OAAO;wBACLW,WAAW,CAACC,MAAMC,cAAgBA,aAAaC,WAAW;wBAC1Db,aAAa;oBACf;oBACAK,cAAc;gBAChB;gBACA;oBACEF,MAAM;oBACNC,MAAM;oBACNL,OAAO;wBACLW,WAAW,CAACC,MAAMC,cAAgBA,aAAaC,WAAW;wBAC1Db,aAAa;oBACf;oBACAK,cAAc;gBAChB;gBACA;oBACEF,MAAM;oBACNC,MAAM;oBACNL,OAAO;wBACLW,WAAW,CAACC,MAAMC,cAAgBA,aAAaC,WAAW;wBAC1Db,aAAa;oBACf;gBACF;gBACA;oBACEG,MAAM;oBACNC,MAAM;oBACNL,OAAO;wBACLW,WAAW,CAACC,MAAMC,cAAgBA,aAAaC,WAAW;wBAC1Db,aAAa;oBACf;gBACF;aACD;YACDc,SAAS;YACTC,SAAS;YACTT,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;gBACbgB,MAAM;YACR;YACAX,cAAc;YACdY,KAAK;YACLC,KAAK;YACLZ,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;gBACbgB,MAAM;YACR;YACAX,cAAc;YACda,KAAK;QACP;QACA;YACEf,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;YACda,KAAK;QACP;QACA;YACEf,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;YACda,KAAK;QACP;QACA;YACEf,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;QAChB;QACA;YACEF,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;QAChB;QACA;YACEF,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;QAChB;QACA;YACEF,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLC,aAAa;YACf;YACAK,cAAc;QAChB;QACA;YACEF,MAAM;YACNC,MAAM;YACNL,OAAO;gBACLW,WAAW,CAACC,OAASA,MAAMQ,uBAAuB;gBAClDnB,aAAa;YACf;YACAK,cAAc;YACda,KAAK;QACP;KACD;IACDV,OAAO;AACT,EAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CollectionBeforeChangeHook } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to generate a descriptive admin title for appointments
|
|
4
|
+
* Format: "Service Name - Customer Name (Date)"
|
|
5
|
+
* Runs before save on the Appointments collection
|
|
6
|
+
*/
|
|
7
|
+
export declare const addAdminTitle: CollectionBeforeChangeHook;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to generate a descriptive admin title for appointments
|
|
3
|
+
* Format: "Service Name - Customer Name (Date)"
|
|
4
|
+
* Runs before save on the Appointments collection
|
|
5
|
+
*/ export const addAdminTitle = async ({ data, req })=>{
|
|
6
|
+
const parts = [];
|
|
7
|
+
// Handle blockouts
|
|
8
|
+
if (data.type === 'blockout') {
|
|
9
|
+
parts.push('Blockout');
|
|
10
|
+
if (data.blockoutReason) {
|
|
11
|
+
parts.push(`(${data.blockoutReason})`);
|
|
12
|
+
}
|
|
13
|
+
if (data.startDateTime) {
|
|
14
|
+
const date = new Date(data.startDateTime);
|
|
15
|
+
parts.push(`- ${date.toLocaleDateString('en-US', {
|
|
16
|
+
day: 'numeric',
|
|
17
|
+
month: 'short'
|
|
18
|
+
})}`);
|
|
19
|
+
}
|
|
20
|
+
data.title = parts.join(' ');
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
// Get service name
|
|
24
|
+
if (data.service) {
|
|
25
|
+
try {
|
|
26
|
+
const serviceId = typeof data.service === 'object' ? data.service.id : data.service;
|
|
27
|
+
const service = await req.payload.findByID({
|
|
28
|
+
id: serviceId,
|
|
29
|
+
collection: 'services'
|
|
30
|
+
});
|
|
31
|
+
if (service?.name) {
|
|
32
|
+
parts.push(service.name);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
parts.push('Appointment');
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
parts.push('Appointment');
|
|
39
|
+
}
|
|
40
|
+
// Get customer name
|
|
41
|
+
if (data.customer) {
|
|
42
|
+
try {
|
|
43
|
+
const customerId = typeof data.customer === 'object' ? data.customer.id : data.customer;
|
|
44
|
+
const customer = await req.payload.findByID({
|
|
45
|
+
id: customerId,
|
|
46
|
+
collection: 'users'
|
|
47
|
+
});
|
|
48
|
+
if (customer) {
|
|
49
|
+
// Try to get name from various fields
|
|
50
|
+
const name = customer.name || customer.firstName || customer.email?.split('@')[0] || 'Customer';
|
|
51
|
+
parts.push(`- ${name}`);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Customer lookup failed, skip
|
|
55
|
+
}
|
|
56
|
+
} else if (data.guest) {
|
|
57
|
+
try {
|
|
58
|
+
const guestId = typeof data.guest === 'object' ? data.guest.id : data.guest;
|
|
59
|
+
const guest = await req.payload.findByID({
|
|
60
|
+
id: guestId,
|
|
61
|
+
collection: 'guest-customers'
|
|
62
|
+
});
|
|
63
|
+
if (guest) {
|
|
64
|
+
const name = `${guest.firstName} ${guest.lastName}`.trim() || 'Guest';
|
|
65
|
+
parts.push(`- ${name}`);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
parts.push('- Guest');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Add date
|
|
72
|
+
if (data.startDateTime) {
|
|
73
|
+
const date = new Date(data.startDateTime);
|
|
74
|
+
const formattedDate = date.toLocaleDateString('en-US', {
|
|
75
|
+
day: 'numeric',
|
|
76
|
+
hour: 'numeric',
|
|
77
|
+
minute: '2-digit',
|
|
78
|
+
month: 'short'
|
|
79
|
+
});
|
|
80
|
+
parts.push(`(${formattedDate})`);
|
|
81
|
+
}
|
|
82
|
+
data.title = parts.join(' ');
|
|
83
|
+
return data;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=addAdminTitle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/addAdminTitle.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\n/**\n * Hook to generate a descriptive admin title for appointments\n * Format: \"Service Name - Customer Name (Date)\"\n * Runs before save on the Appointments collection\n */\nexport const addAdminTitle: CollectionBeforeChangeHook = async ({\n data,\n req,\n}) => {\n const parts: string[] = []\n\n // Handle blockouts\n if (data.type === 'blockout') {\n parts.push('Blockout')\n if (data.blockoutReason) {\n parts.push(`(${data.blockoutReason})`)\n }\n if (data.startDateTime) {\n const date = new Date(data.startDateTime)\n parts.push(`- ${date.toLocaleDateString('en-US', { day: 'numeric', month: 'short' })}`)\n }\n data.title = parts.join(' ')\n return data\n }\n\n // Get service name\n if (data.service) {\n try {\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n const service = await req.payload.findByID({\n id: serviceId,\n collection: 'services',\n })\n if (service?.name) {\n parts.push(service.name)\n }\n } catch {\n parts.push('Appointment')\n }\n } else {\n parts.push('Appointment')\n }\n\n // Get customer name\n if (data.customer) {\n try {\n const customerId = typeof data.customer === 'object' ? data.customer.id : data.customer\n const customer = await req.payload.findByID({\n id: customerId,\n collection: 'users',\n })\n if (customer) {\n // Try to get name from various fields\n const name = customer.name || customer.firstName || customer.email?.split('@')[0] || 'Customer'\n parts.push(`- ${name}`)\n }\n } catch {\n // Customer lookup failed, skip\n }\n } else if (data.guest) {\n try {\n const guestId = typeof data.guest === 'object' ? data.guest.id : data.guest\n const guest = await req.payload.findByID({\n id: guestId,\n collection: 'guest-customers',\n })\n if (guest) {\n const name = `${guest.firstName} ${guest.lastName}`.trim() || 'Guest'\n parts.push(`- ${name}`)\n }\n } catch {\n parts.push('- Guest')\n }\n }\n\n // Add date\n if (data.startDateTime) {\n const date = new Date(data.startDateTime)\n const formattedDate = date.toLocaleDateString('en-US', {\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n month: 'short',\n })\n parts.push(`(${formattedDate})`)\n }\n\n data.title = parts.join(' ')\n return data\n}\n"],"names":["addAdminTitle","data","req","parts","type","push","blockoutReason","startDateTime","date","Date","toLocaleDateString","day","month","title","join","service","serviceId","id","payload","findByID","collection","name","customer","customerId","firstName","email","split","guest","guestId","lastName","trim","formattedDate","hour","minute"],"mappings":"AAEA;;;;CAIC,GACD,OAAO,MAAMA,gBAA4C,OAAO,EAC9DC,IAAI,EACJC,GAAG,EACJ;IACC,MAAMC,QAAkB,EAAE;IAE1B,mBAAmB;IACnB,IAAIF,KAAKG,IAAI,KAAK,YAAY;QAC5BD,MAAME,IAAI,CAAC;QACX,IAAIJ,KAAKK,cAAc,EAAE;YACvBH,MAAME,IAAI,CAAC,CAAC,CAAC,EAAEJ,KAAKK,cAAc,CAAC,CAAC,CAAC;QACvC;QACA,IAAIL,KAAKM,aAAa,EAAE;YACtB,MAAMC,OAAO,IAAIC,KAAKR,KAAKM,aAAa;YACxCJ,MAAME,IAAI,CAAC,CAAC,EAAE,EAAEG,KAAKE,kBAAkB,CAAC,SAAS;gBAAEC,KAAK;gBAAWC,OAAO;YAAQ,IAAI;QACxF;QACAX,KAAKY,KAAK,GAAGV,MAAMW,IAAI,CAAC;QACxB,OAAOb;IACT;IAEA,mBAAmB;IACnB,IAAIA,KAAKc,OAAO,EAAE;QAChB,IAAI;YACF,MAAMC,YAAY,OAAOf,KAAKc,OAAO,KAAK,WAAWd,KAAKc,OAAO,CAACE,EAAE,GAAGhB,KAAKc,OAAO;YACnF,MAAMA,UAAU,MAAMb,IAAIgB,OAAO,CAACC,QAAQ,CAAC;gBACzCF,IAAID;gBACJI,YAAY;YACd;YACA,IAAIL,SAASM,MAAM;gBACjBlB,MAAME,IAAI,CAACU,QAAQM,IAAI;YACzB;QACF,EAAE,OAAM;YACNlB,MAAME,IAAI,CAAC;QACb;IACF,OAAO;QACLF,MAAME,IAAI,CAAC;IACb;IAEA,oBAAoB;IACpB,IAAIJ,KAAKqB,QAAQ,EAAE;QACjB,IAAI;YACF,MAAMC,aAAa,OAAOtB,KAAKqB,QAAQ,KAAK,WAAWrB,KAAKqB,QAAQ,CAACL,EAAE,GAAGhB,KAAKqB,QAAQ;YACvF,MAAMA,WAAW,MAAMpB,IAAIgB,OAAO,CAACC,QAAQ,CAAC;gBAC1CF,IAAIM;gBACJH,YAAY;YACd;YACA,IAAIE,UAAU;gBACZ,sCAAsC;gBACtC,MAAMD,OAAOC,SAASD,IAAI,IAAIC,SAASE,SAAS,IAAIF,SAASG,KAAK,EAAEC,MAAM,IAAI,CAAC,EAAE,IAAI;gBACrFvB,MAAME,IAAI,CAAC,CAAC,EAAE,EAAEgB,MAAM;YACxB;QACF,EAAE,OAAM;QACN,+BAA+B;QACjC;IACF,OAAO,IAAIpB,KAAK0B,KAAK,EAAE;QACrB,IAAI;YACF,MAAMC,UAAU,OAAO3B,KAAK0B,KAAK,KAAK,WAAW1B,KAAK0B,KAAK,CAACV,EAAE,GAAGhB,KAAK0B,KAAK;YAC3E,MAAMA,QAAQ,MAAMzB,IAAIgB,OAAO,CAACC,QAAQ,CAAC;gBACvCF,IAAIW;gBACJR,YAAY;YACd;YACA,IAAIO,OAAO;gBACT,MAAMN,OAAO,GAAGM,MAAMH,SAAS,CAAC,CAAC,EAAEG,MAAME,QAAQ,EAAE,CAACC,IAAI,MAAM;gBAC9D3B,MAAME,IAAI,CAAC,CAAC,EAAE,EAAEgB,MAAM;YACxB;QACF,EAAE,OAAM;YACNlB,MAAME,IAAI,CAAC;QACb;IACF;IAEA,WAAW;IACX,IAAIJ,KAAKM,aAAa,EAAE;QACtB,MAAMC,OAAO,IAAIC,KAAKR,KAAKM,aAAa;QACxC,MAAMwB,gBAAgBvB,KAAKE,kBAAkB,CAAC,SAAS;YACrDC,KAAK;YACLqB,MAAM;YACNC,QAAQ;YACRrB,OAAO;QACT;QACAT,MAAME,IAAI,CAAC,CAAC,CAAC,EAAE0B,cAAc,CAAC,CAAC;IACjC;IAEA9B,KAAKY,KAAK,GAAGV,MAAMW,IAAI,CAAC;IACxB,OAAOb;AACT,EAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to send confirmation/update emails to customers after appointment changes
|
|
4
|
+
* Runs after save on the Appointments collection
|
|
5
|
+
*/
|
|
6
|
+
export declare const sendCustomerEmail: CollectionAfterChangeHook;
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to send confirmation/update emails to customers after appointment changes
|
|
3
|
+
* Runs after save on the Appointments collection
|
|
4
|
+
*/ export const sendCustomerEmail = async ({ doc, operation, previousDoc, req })=>{
|
|
5
|
+
// Skip for blockouts
|
|
6
|
+
if (doc.type === 'blockout') {
|
|
7
|
+
return doc;
|
|
8
|
+
}
|
|
9
|
+
// Check if email sending is enabled
|
|
10
|
+
try {
|
|
11
|
+
const settings = await req.payload.findGlobal({
|
|
12
|
+
slug: 'opening-times'
|
|
13
|
+
});
|
|
14
|
+
if (!settings?.sendConfirmationEmails) {
|
|
15
|
+
return doc;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// Settings not found, skip email
|
|
19
|
+
return doc;
|
|
20
|
+
}
|
|
21
|
+
// Determine if we should send email
|
|
22
|
+
const isNewAppointment = operation === 'create';
|
|
23
|
+
const statusChanged = previousDoc && previousDoc.status !== doc.status;
|
|
24
|
+
if (!isNewAppointment && !statusChanged) {
|
|
25
|
+
return doc;
|
|
26
|
+
}
|
|
27
|
+
// Build email context
|
|
28
|
+
try {
|
|
29
|
+
const context = await buildEmailContext(doc, req);
|
|
30
|
+
if (!context) {
|
|
31
|
+
return doc;
|
|
32
|
+
}
|
|
33
|
+
// Determine email type
|
|
34
|
+
let subject;
|
|
35
|
+
let template;
|
|
36
|
+
if (isNewAppointment) {
|
|
37
|
+
subject = `Appointment Confirmed: ${context.serviceName}`;
|
|
38
|
+
template = buildCreatedEmail(context);
|
|
39
|
+
} else if (doc.status === 'cancelled') {
|
|
40
|
+
subject = `Appointment Cancelled: ${context.serviceName}`;
|
|
41
|
+
template = buildCancelledEmail(context);
|
|
42
|
+
} else if (doc.status === 'confirmed') {
|
|
43
|
+
subject = `Appointment Confirmed: ${context.serviceName}`;
|
|
44
|
+
template = buildConfirmedEmail(context);
|
|
45
|
+
} else {
|
|
46
|
+
subject = `Appointment Update: ${context.serviceName}`;
|
|
47
|
+
template = buildUpdatedEmail(context);
|
|
48
|
+
}
|
|
49
|
+
// Send email using Payload's email adapter
|
|
50
|
+
await req.payload.sendEmail({
|
|
51
|
+
html: template,
|
|
52
|
+
subject,
|
|
53
|
+
to: context.customerEmail
|
|
54
|
+
});
|
|
55
|
+
req.payload.logger.info({
|
|
56
|
+
msg: 'Appointment email sent',
|
|
57
|
+
operation: isNewAppointment ? 'create' : 'update',
|
|
58
|
+
status: doc.status,
|
|
59
|
+
to: context.customerEmail
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Log error but don't fail the operation
|
|
63
|
+
req.payload.logger.error({
|
|
64
|
+
error,
|
|
65
|
+
msg: 'Failed to send appointment email'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return doc;
|
|
69
|
+
};
|
|
70
|
+
async function buildEmailContext(doc, req) {
|
|
71
|
+
let customerName = '';
|
|
72
|
+
let customerEmail = '';
|
|
73
|
+
// Get customer info
|
|
74
|
+
if (doc.customer) {
|
|
75
|
+
try {
|
|
76
|
+
const customerId = typeof doc.customer === 'object' ? doc.customer.id : doc.customer;
|
|
77
|
+
const customer = await req.payload.findByID({
|
|
78
|
+
id: customerId,
|
|
79
|
+
collection: 'users'
|
|
80
|
+
});
|
|
81
|
+
if (customer) {
|
|
82
|
+
customerName = customer.name || customer.firstName || 'Customer';
|
|
83
|
+
customerEmail = customer.email;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
} else if (doc.guest) {
|
|
89
|
+
try {
|
|
90
|
+
const guestId = typeof doc.guest === 'object' ? doc.guest.id : doc.guest;
|
|
91
|
+
const guest = await req.payload.findByID({
|
|
92
|
+
id: guestId,
|
|
93
|
+
collection: 'guest-customers'
|
|
94
|
+
});
|
|
95
|
+
if (guest) {
|
|
96
|
+
customerName = `${guest.firstName} ${guest.lastName}`.trim();
|
|
97
|
+
customerEmail = guest.email;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!customerEmail) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
// Get service info
|
|
107
|
+
let serviceName = 'Appointment';
|
|
108
|
+
if (doc.service) {
|
|
109
|
+
try {
|
|
110
|
+
const serviceId = typeof doc.service === 'object' ? doc.service.id : doc.service;
|
|
111
|
+
const service = await req.payload.findByID({
|
|
112
|
+
id: serviceId,
|
|
113
|
+
collection: 'services'
|
|
114
|
+
});
|
|
115
|
+
if (service?.name) {
|
|
116
|
+
serviceName = service.name;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Use default
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Get team member info
|
|
123
|
+
let teamMemberName;
|
|
124
|
+
if (doc.teamMember) {
|
|
125
|
+
try {
|
|
126
|
+
const teamMemberId = typeof doc.teamMember === 'object' ? doc.teamMember.id : doc.teamMember;
|
|
127
|
+
const teamMember = await req.payload.findByID({
|
|
128
|
+
id: teamMemberId,
|
|
129
|
+
collection: 'team-members'
|
|
130
|
+
});
|
|
131
|
+
if (teamMember?.name) {
|
|
132
|
+
teamMemberName = teamMember.name;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Skip team member
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Format date/time
|
|
139
|
+
const startDate = new Date(doc.startDateTime);
|
|
140
|
+
const appointmentDate = startDate.toLocaleDateString('en-US', {
|
|
141
|
+
day: 'numeric',
|
|
142
|
+
month: 'long',
|
|
143
|
+
weekday: 'long',
|
|
144
|
+
year: 'numeric'
|
|
145
|
+
});
|
|
146
|
+
const appointmentTime = startDate.toLocaleTimeString('en-US', {
|
|
147
|
+
hour: 'numeric',
|
|
148
|
+
minute: '2-digit'
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
appointmentDate,
|
|
152
|
+
appointmentTime,
|
|
153
|
+
customerEmail,
|
|
154
|
+
customerName,
|
|
155
|
+
serviceName,
|
|
156
|
+
status: doc.status,
|
|
157
|
+
teamMemberName
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function buildCreatedEmail(ctx) {
|
|
161
|
+
return `
|
|
162
|
+
<!DOCTYPE html>
|
|
163
|
+
<html>
|
|
164
|
+
<head>
|
|
165
|
+
<meta charset="utf-8">
|
|
166
|
+
<style>
|
|
167
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
168
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
169
|
+
.header { background: #10b981; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
170
|
+
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }
|
|
171
|
+
.details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }
|
|
172
|
+
.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }
|
|
173
|
+
.detail-label { font-weight: 600; width: 120px; }
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<div class="container">
|
|
178
|
+
<div class="header">
|
|
179
|
+
<h1 style="margin: 0;">Appointment Confirmed</h1>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="content">
|
|
182
|
+
<p>Hello ${ctx.customerName},</p>
|
|
183
|
+
<p>Your appointment has been successfully scheduled!</p>
|
|
184
|
+
<div class="details">
|
|
185
|
+
<div class="detail-row">
|
|
186
|
+
<span class="detail-label">Service:</span>
|
|
187
|
+
<span>${ctx.serviceName}</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="detail-row">
|
|
190
|
+
<span class="detail-label">Date:</span>
|
|
191
|
+
<span>${ctx.appointmentDate}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="detail-row">
|
|
194
|
+
<span class="detail-label">Time:</span>
|
|
195
|
+
<span>${ctx.appointmentTime}</span>
|
|
196
|
+
</div>
|
|
197
|
+
${ctx.teamMemberName ? `
|
|
198
|
+
<div class="detail-row">
|
|
199
|
+
<span class="detail-label">With:</span>
|
|
200
|
+
<span>${ctx.teamMemberName}</span>
|
|
201
|
+
</div>
|
|
202
|
+
` : ''}
|
|
203
|
+
</div>
|
|
204
|
+
<p>If you need to make any changes, please contact us.</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</body>
|
|
208
|
+
</html>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
function buildConfirmedEmail(ctx) {
|
|
212
|
+
return `
|
|
213
|
+
<!DOCTYPE html>
|
|
214
|
+
<html>
|
|
215
|
+
<head>
|
|
216
|
+
<meta charset="utf-8">
|
|
217
|
+
<style>
|
|
218
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
219
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
220
|
+
.header { background: #3b82f6; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
221
|
+
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }
|
|
222
|
+
.details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }
|
|
223
|
+
.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }
|
|
224
|
+
.detail-label { font-weight: 600; width: 120px; }
|
|
225
|
+
</style>
|
|
226
|
+
</head>
|
|
227
|
+
<body>
|
|
228
|
+
<div class="container">
|
|
229
|
+
<div class="header">
|
|
230
|
+
<h1 style="margin: 0;">Appointment Confirmed</h1>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="content">
|
|
233
|
+
<p>Hello ${ctx.customerName},</p>
|
|
234
|
+
<p>Great news! Your appointment has been confirmed.</p>
|
|
235
|
+
<div class="details">
|
|
236
|
+
<div class="detail-row">
|
|
237
|
+
<span class="detail-label">Service:</span>
|
|
238
|
+
<span>${ctx.serviceName}</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="detail-row">
|
|
241
|
+
<span class="detail-label">Date:</span>
|
|
242
|
+
<span>${ctx.appointmentDate}</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="detail-row">
|
|
245
|
+
<span class="detail-label">Time:</span>
|
|
246
|
+
<span>${ctx.appointmentTime}</span>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<p>We look forward to seeing you!</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
function buildCancelledEmail(ctx) {
|
|
257
|
+
return `
|
|
258
|
+
<!DOCTYPE html>
|
|
259
|
+
<html>
|
|
260
|
+
<head>
|
|
261
|
+
<meta charset="utf-8">
|
|
262
|
+
<style>
|
|
263
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
264
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
265
|
+
.header { background: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
266
|
+
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }
|
|
267
|
+
.details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }
|
|
268
|
+
.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }
|
|
269
|
+
.detail-label { font-weight: 600; width: 120px; }
|
|
270
|
+
</style>
|
|
271
|
+
</head>
|
|
272
|
+
<body>
|
|
273
|
+
<div class="container">
|
|
274
|
+
<div class="header">
|
|
275
|
+
<h1 style="margin: 0;">Appointment Cancelled</h1>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="content">
|
|
278
|
+
<p>Hello ${ctx.customerName},</p>
|
|
279
|
+
<p>Your appointment has been cancelled.</p>
|
|
280
|
+
<div class="details">
|
|
281
|
+
<div class="detail-row">
|
|
282
|
+
<span class="detail-label">Service:</span>
|
|
283
|
+
<span>${ctx.serviceName}</span>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="detail-row">
|
|
286
|
+
<span class="detail-label">Date:</span>
|
|
287
|
+
<span>${ctx.appointmentDate}</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div class="detail-row">
|
|
290
|
+
<span class="detail-label">Time:</span>
|
|
291
|
+
<span>${ctx.appointmentTime}</span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<p>If you would like to reschedule, please book a new appointment.</p>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</body>
|
|
298
|
+
</html>
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
function buildUpdatedEmail(ctx) {
|
|
302
|
+
return `
|
|
303
|
+
<!DOCTYPE html>
|
|
304
|
+
<html>
|
|
305
|
+
<head>
|
|
306
|
+
<meta charset="utf-8">
|
|
307
|
+
<style>
|
|
308
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
309
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
310
|
+
.header { background: #f59e0b; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
311
|
+
.content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }
|
|
312
|
+
.details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }
|
|
313
|
+
.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }
|
|
314
|
+
.detail-label { font-weight: 600; width: 120px; }
|
|
315
|
+
</style>
|
|
316
|
+
</head>
|
|
317
|
+
<body>
|
|
318
|
+
<div class="container">
|
|
319
|
+
<div class="header">
|
|
320
|
+
<h1 style="margin: 0;">Appointment Updated</h1>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="content">
|
|
323
|
+
<p>Hello ${ctx.customerName},</p>
|
|
324
|
+
<p>Your appointment details have been updated.</p>
|
|
325
|
+
<div class="details">
|
|
326
|
+
<div class="detail-row">
|
|
327
|
+
<span class="detail-label">Service:</span>
|
|
328
|
+
<span>${ctx.serviceName}</span>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="detail-row">
|
|
331
|
+
<span class="detail-label">Date:</span>
|
|
332
|
+
<span>${ctx.appointmentDate}</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="detail-row">
|
|
335
|
+
<span class="detail-label">Time:</span>
|
|
336
|
+
<span>${ctx.appointmentTime}</span>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="detail-row">
|
|
339
|
+
<span class="detail-label">Status:</span>
|
|
340
|
+
<span>${ctx.status}</span>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
<p>If you have any questions, please contact us.</p>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</body>
|
|
347
|
+
</html>
|
|
348
|
+
`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//# sourceMappingURL=sendCustomerEmail.js.map
|