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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/collections/Appointments.d.ts +2 -0
  4. package/dist/collections/Appointments.js +165 -0
  5. package/dist/collections/Appointments.js.map +1 -0
  6. package/dist/collections/GuestCustomers.d.ts +2 -0
  7. package/dist/collections/GuestCustomers.js +106 -0
  8. package/dist/collections/GuestCustomers.js.map +1 -0
  9. package/dist/collections/Services.d.ts +2 -0
  10. package/dist/collections/Services.js +147 -0
  11. package/dist/collections/Services.js.map +1 -0
  12. package/dist/collections/TeamMembers.d.ts +2 -0
  13. package/dist/collections/TeamMembers.js +184 -0
  14. package/dist/collections/TeamMembers.js.map +1 -0
  15. package/dist/components/BeforeDashboardClient.d.ts +2 -0
  16. package/dist/components/BeforeDashboardClient.js +162 -0
  17. package/dist/components/BeforeDashboardClient.js.map +1 -0
  18. package/dist/components/BeforeDashboardServer.d.ts +2 -0
  19. package/dist/components/BeforeDashboardServer.js +22 -0
  20. package/dist/components/BeforeDashboardServer.js.map +1 -0
  21. package/dist/components/BeforeDashboardServer.module.css +5 -0
  22. package/dist/components/calendar/Calendar.module.css +506 -0
  23. package/dist/components/calendar/CalendarContainer.d.ts +3 -0
  24. package/dist/components/calendar/CalendarContainer.js +246 -0
  25. package/dist/components/calendar/CalendarContainer.js.map +1 -0
  26. package/dist/components/calendar/DayView.d.ts +3 -0
  27. package/dist/components/calendar/DayView.js +192 -0
  28. package/dist/components/calendar/DayView.js.map +1 -0
  29. package/dist/components/calendar/EventPopover.d.ts +3 -0
  30. package/dist/components/calendar/EventPopover.js +257 -0
  31. package/dist/components/calendar/EventPopover.js.map +1 -0
  32. package/dist/components/calendar/EventRenderer.d.ts +3 -0
  33. package/dist/components/calendar/EventRenderer.js +76 -0
  34. package/dist/components/calendar/EventRenderer.js.map +1 -0
  35. package/dist/components/calendar/WeekView.d.ts +3 -0
  36. package/dist/components/calendar/WeekView.js +203 -0
  37. package/dist/components/calendar/WeekView.js.map +1 -0
  38. package/dist/components/calendar/index.d.ts +6 -0
  39. package/dist/components/calendar/index.js +7 -0
  40. package/dist/components/calendar/index.js.map +1 -0
  41. package/dist/components/calendar/types.d.ts +69 -0
  42. package/dist/components/calendar/types.js +3 -0
  43. package/dist/components/calendar/types.js.map +1 -0
  44. package/dist/endpoints/customEndpointHandler.d.ts +2 -0
  45. package/dist/endpoints/customEndpointHandler.js +7 -0
  46. package/dist/endpoints/customEndpointHandler.js.map +1 -0
  47. package/dist/endpoints/getAvailableSlots.d.ts +12 -0
  48. package/dist/endpoints/getAvailableSlots.js +291 -0
  49. package/dist/endpoints/getAvailableSlots.js.map +1 -0
  50. package/dist/exports/client.d.ts +3 -0
  51. package/dist/exports/client.js +4 -0
  52. package/dist/exports/client.js.map +1 -0
  53. package/dist/exports/rsc.d.ts +1 -0
  54. package/dist/exports/rsc.js +3 -0
  55. package/dist/exports/rsc.js.map +1 -0
  56. package/dist/globals/OpeningTimes.d.ts +2 -0
  57. package/dist/globals/OpeningTimes.js +196 -0
  58. package/dist/globals/OpeningTimes.js.map +1 -0
  59. package/dist/hooks/addAdminTitle.d.ts +7 -0
  60. package/dist/hooks/addAdminTitle.js +86 -0
  61. package/dist/hooks/addAdminTitle.js.map +1 -0
  62. package/dist/hooks/sendCustomerEmail.d.ts +6 -0
  63. package/dist/hooks/sendCustomerEmail.js +351 -0
  64. package/dist/hooks/sendCustomerEmail.js.map +1 -0
  65. package/dist/hooks/setEndDateTime.d.ts +6 -0
  66. package/dist/hooks/setEndDateTime.js +44 -0
  67. package/dist/hooks/setEndDateTime.js.map +1 -0
  68. package/dist/hooks/validateCustomerOrGuest.d.ts +6 -0
  69. package/dist/hooks/validateCustomerOrGuest.js +21 -0
  70. package/dist/hooks/validateCustomerOrGuest.js.map +1 -0
  71. package/dist/index.d.ts +23 -0
  72. package/dist/index.js +183 -0
  73. package/dist/index.js.map +1 -0
  74. 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