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 @@
1
+ {"version":3,"sources":["../../src/hooks/sendCustomerEmail.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\ninterface EmailContext {\n appointmentDate: string\n appointmentTime: string\n customerEmail: string\n customerName: string\n serviceName: string\n status: string\n teamMemberName?: string\n}\n\n/**\n * Hook to send confirmation/update emails to customers after appointment changes\n * Runs after save on the Appointments collection\n */\nexport const sendCustomerEmail: CollectionAfterChangeHook = async ({\n doc,\n operation,\n previousDoc,\n req,\n}) => {\n // Skip for blockouts\n if (doc.type === 'blockout') {\n return doc\n }\n\n // Check if email sending is enabled\n try {\n const settings = await req.payload.findGlobal({\n slug: 'opening-times',\n })\n\n if (!settings?.sendConfirmationEmails) {\n return doc\n }\n } catch {\n // Settings not found, skip email\n return doc\n }\n\n // Determine if we should send email\n const isNewAppointment = operation === 'create'\n const statusChanged = previousDoc && previousDoc.status !== doc.status\n\n if (!isNewAppointment && !statusChanged) {\n return doc\n }\n\n // Build email context\n try {\n const context = await buildEmailContext(doc, req)\n\n if (!context) {\n return doc\n }\n\n // Determine email type\n let subject: string\n let template: string\n\n if (isNewAppointment) {\n subject = `Appointment Confirmed: ${context.serviceName}`\n template = buildCreatedEmail(context)\n } else if (doc.status === 'cancelled') {\n subject = `Appointment Cancelled: ${context.serviceName}`\n template = buildCancelledEmail(context)\n } else if (doc.status === 'confirmed') {\n subject = `Appointment Confirmed: ${context.serviceName}`\n template = buildConfirmedEmail(context)\n } else {\n subject = `Appointment Update: ${context.serviceName}`\n template = buildUpdatedEmail(context)\n }\n\n // Send email using Payload's email adapter\n await req.payload.sendEmail({\n html: template,\n subject,\n to: context.customerEmail,\n })\n\n req.payload.logger.info({\n msg: 'Appointment email sent',\n operation: isNewAppointment ? 'create' : 'update',\n status: doc.status,\n to: context.customerEmail,\n })\n } catch (error) {\n // Log error but don't fail the operation\n req.payload.logger.error({\n error,\n msg: 'Failed to send appointment email',\n })\n }\n\n return doc\n}\n\nasync function buildEmailContext(doc: any, req: any): Promise<EmailContext | null> {\n let customerName = ''\n let customerEmail = ''\n\n // Get customer info\n if (doc.customer) {\n try {\n const customerId = typeof doc.customer === 'object' ? doc.customer.id : doc.customer\n const customer = await req.payload.findByID({\n id: customerId,\n collection: 'users',\n })\n if (customer) {\n customerName = customer.name || customer.firstName || 'Customer'\n customerEmail = customer.email\n }\n } catch {\n return null\n }\n } else if (doc.guest) {\n try {\n const guestId = typeof doc.guest === 'object' ? doc.guest.id : doc.guest\n const guest = await req.payload.findByID({\n id: guestId,\n collection: 'guest-customers',\n })\n if (guest) {\n customerName = `${guest.firstName} ${guest.lastName}`.trim()\n customerEmail = guest.email\n }\n } catch {\n return null\n }\n }\n\n if (!customerEmail) {\n return null\n }\n\n // Get service info\n let serviceName = 'Appointment'\n if (doc.service) {\n try {\n const serviceId = typeof doc.service === 'object' ? doc.service.id : doc.service\n const service = await req.payload.findByID({\n id: serviceId,\n collection: 'services',\n })\n if (service?.name) {\n serviceName = service.name\n }\n } catch {\n // Use default\n }\n }\n\n // Get team member info\n let teamMemberName: string | undefined\n if (doc.teamMember) {\n try {\n const teamMemberId = typeof doc.teamMember === 'object' ? doc.teamMember.id : doc.teamMember\n const teamMember = await req.payload.findByID({\n id: teamMemberId,\n collection: 'team-members',\n })\n if (teamMember?.name) {\n teamMemberName = teamMember.name\n }\n } catch {\n // Skip team member\n }\n }\n\n // Format date/time\n const startDate = new Date(doc.startDateTime)\n const appointmentDate = startDate.toLocaleDateString('en-US', {\n day: 'numeric',\n month: 'long',\n weekday: 'long',\n year: 'numeric',\n })\n const appointmentTime = startDate.toLocaleTimeString('en-US', {\n hour: 'numeric',\n minute: '2-digit',\n })\n\n return {\n appointmentDate,\n appointmentTime,\n customerEmail,\n customerName,\n serviceName,\n status: doc.status,\n teamMemberName,\n }\n}\n\nfunction buildCreatedEmail(ctx: EmailContext): string {\n return `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <style>\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }\n .container { max-width: 600px; margin: 0 auto; padding: 20px; }\n .header { background: #10b981; color: white; padding: 20px; border-radius: 8px 8px 0 0; }\n .content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }\n .details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }\n .detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }\n .detail-label { font-weight: 600; width: 120px; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1 style=\"margin: 0;\">Appointment Confirmed</h1>\n </div>\n <div class=\"content\">\n <p>Hello ${ctx.customerName},</p>\n <p>Your appointment has been successfully scheduled!</p>\n <div class=\"details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Service:</span>\n <span>${ctx.serviceName}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span>${ctx.appointmentDate}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time:</span>\n <span>${ctx.appointmentTime}</span>\n </div>\n ${ctx.teamMemberName ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">With:</span>\n <span>${ctx.teamMemberName}</span>\n </div>\n ` : ''}\n </div>\n <p>If you need to make any changes, please contact us.</p>\n </div>\n </div>\n</body>\n</html>\n`\n}\n\nfunction buildConfirmedEmail(ctx: EmailContext): string {\n return `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <style>\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }\n .container { max-width: 600px; margin: 0 auto; padding: 20px; }\n .header { background: #3b82f6; color: white; padding: 20px; border-radius: 8px 8px 0 0; }\n .content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }\n .details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }\n .detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }\n .detail-label { font-weight: 600; width: 120px; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1 style=\"margin: 0;\">Appointment Confirmed</h1>\n </div>\n <div class=\"content\">\n <p>Hello ${ctx.customerName},</p>\n <p>Great news! Your appointment has been confirmed.</p>\n <div class=\"details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Service:</span>\n <span>${ctx.serviceName}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span>${ctx.appointmentDate}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time:</span>\n <span>${ctx.appointmentTime}</span>\n </div>\n </div>\n <p>We look forward to seeing you!</p>\n </div>\n </div>\n</body>\n</html>\n`\n}\n\nfunction buildCancelledEmail(ctx: EmailContext): string {\n return `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <style>\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }\n .container { max-width: 600px; margin: 0 auto; padding: 20px; }\n .header { background: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0; }\n .content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }\n .details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }\n .detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }\n .detail-label { font-weight: 600; width: 120px; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1 style=\"margin: 0;\">Appointment Cancelled</h1>\n </div>\n <div class=\"content\">\n <p>Hello ${ctx.customerName},</p>\n <p>Your appointment has been cancelled.</p>\n <div class=\"details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Service:</span>\n <span>${ctx.serviceName}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span>${ctx.appointmentDate}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time:</span>\n <span>${ctx.appointmentTime}</span>\n </div>\n </div>\n <p>If you would like to reschedule, please book a new appointment.</p>\n </div>\n </div>\n</body>\n</html>\n`\n}\n\nfunction buildUpdatedEmail(ctx: EmailContext): string {\n return `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <style>\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }\n .container { max-width: 600px; margin: 0 auto; padding: 20px; }\n .header { background: #f59e0b; color: white; padding: 20px; border-radius: 8px 8px 0 0; }\n .content { background: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px; }\n .details { background: white; padding: 15px; border-radius: 6px; margin: 15px 0; }\n .detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f3f4f6; }\n .detail-label { font-weight: 600; width: 120px; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1 style=\"margin: 0;\">Appointment Updated</h1>\n </div>\n <div class=\"content\">\n <p>Hello ${ctx.customerName},</p>\n <p>Your appointment details have been updated.</p>\n <div class=\"details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Service:</span>\n <span>${ctx.serviceName}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span>${ctx.appointmentDate}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time:</span>\n <span>${ctx.appointmentTime}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Status:</span>\n <span>${ctx.status}</span>\n </div>\n </div>\n <p>If you have any questions, please contact us.</p>\n </div>\n </div>\n</body>\n</html>\n`\n}\n"],"names":["sendCustomerEmail","doc","operation","previousDoc","req","type","settings","payload","findGlobal","slug","sendConfirmationEmails","isNewAppointment","statusChanged","status","context","buildEmailContext","subject","template","serviceName","buildCreatedEmail","buildCancelledEmail","buildConfirmedEmail","buildUpdatedEmail","sendEmail","html","to","customerEmail","logger","info","msg","error","customerName","customer","customerId","id","findByID","collection","name","firstName","email","guest","guestId","lastName","trim","service","serviceId","teamMemberName","teamMember","teamMemberId","startDate","Date","startDateTime","appointmentDate","toLocaleDateString","day","month","weekday","year","appointmentTime","toLocaleTimeString","hour","minute","ctx"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ /**
3
+ * Hook to automatically calculate endDateTime based on service duration
4
+ * Runs before save on the Appointments collection
5
+ */
6
+ export declare const setEndDateTime: CollectionBeforeChangeHook;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Hook to automatically calculate endDateTime based on service duration
3
+ * Runs before save on the Appointments collection
4
+ */ export const setEndDateTime = async ({ data, req })=>{
5
+ // Only process for appointments (not blockouts)
6
+ if (data.type !== 'appointment') {
7
+ // For blockouts, if no endDateTime is set, default to 1 hour
8
+ if (!data.endDateTime && data.startDateTime) {
9
+ const start = new Date(data.startDateTime);
10
+ const end = new Date(start.getTime() + 60 * 60 * 1000) // 1 hour default
11
+ ;
12
+ data.endDateTime = end.toISOString();
13
+ }
14
+ return data;
15
+ }
16
+ // Get service to determine duration
17
+ if (!data.service || !data.startDateTime) {
18
+ return data;
19
+ }
20
+ try {
21
+ // Resolve service ID (could be an object if populated or just an ID)
22
+ const serviceId = typeof data.service === 'object' ? data.service.id : data.service;
23
+ const service = await req.payload.findByID({
24
+ id: serviceId,
25
+ collection: 'services'
26
+ });
27
+ if (service && service.duration) {
28
+ const start = new Date(data.startDateTime);
29
+ const durationMs = service.duration * 60 * 1000 // Convert minutes to milliseconds
30
+ ;
31
+ const end = new Date(start.getTime() + durationMs);
32
+ data.endDateTime = end.toISOString();
33
+ }
34
+ } catch (error) {
35
+ // Log error but don't fail the operation
36
+ req.payload.logger.error({
37
+ error,
38
+ msg: 'Failed to calculate endDateTime'
39
+ });
40
+ }
41
+ return data;
42
+ };
43
+
44
+ //# sourceMappingURL=setEndDateTime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/setEndDateTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\n/**\n * Hook to automatically calculate endDateTime based on service duration\n * Runs before save on the Appointments collection\n */\nexport const setEndDateTime: CollectionBeforeChangeHook = async ({\n data,\n req,\n}) => {\n // Only process for appointments (not blockouts)\n if (data.type !== 'appointment') {\n // For blockouts, if no endDateTime is set, default to 1 hour\n if (!data.endDateTime && data.startDateTime) {\n const start = new Date(data.startDateTime)\n const end = new Date(start.getTime() + 60 * 60 * 1000) // 1 hour default\n data.endDateTime = end.toISOString()\n }\n return data\n }\n\n // Get service to determine duration\n if (!data.service || !data.startDateTime) {\n return data\n }\n\n try {\n // Resolve service ID (could be an object if populated or just an ID)\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n const service = await req.payload.findByID({\n id: serviceId,\n collection: 'services',\n })\n\n if (service && service.duration) {\n const start = new Date(data.startDateTime)\n const durationMs = service.duration * 60 * 1000 // Convert minutes to milliseconds\n const end = new Date(start.getTime() + durationMs)\n\n data.endDateTime = end.toISOString()\n }\n } catch (error) {\n // Log error but don't fail the operation\n req.payload.logger.error({\n error,\n msg: 'Failed to calculate endDateTime',\n })\n }\n\n return data\n}\n"],"names":["setEndDateTime","data","req","type","endDateTime","startDateTime","start","Date","end","getTime","toISOString","service","serviceId","id","payload","findByID","collection","duration","durationMs","error","logger","msg"],"mappings":"AAEA;;;CAGC,GACD,OAAO,MAAMA,iBAA6C,OAAO,EAC/DC,IAAI,EACJC,GAAG,EACJ;IACC,gDAAgD;IAChD,IAAID,KAAKE,IAAI,KAAK,eAAe;QAC/B,6DAA6D;QAC7D,IAAI,CAACF,KAAKG,WAAW,IAAIH,KAAKI,aAAa,EAAE;YAC3C,MAAMC,QAAQ,IAAIC,KAAKN,KAAKI,aAAa;YACzC,MAAMG,MAAM,IAAID,KAAKD,MAAMG,OAAO,KAAK,KAAK,KAAK,MAAM,iBAAiB;;YACxER,KAAKG,WAAW,GAAGI,IAAIE,WAAW;QACpC;QACA,OAAOT;IACT;IAEA,oCAAoC;IACpC,IAAI,CAACA,KAAKU,OAAO,IAAI,CAACV,KAAKI,aAAa,EAAE;QACxC,OAAOJ;IACT;IAEA,IAAI;QACF,qEAAqE;QACrE,MAAMW,YAAY,OAAOX,KAAKU,OAAO,KAAK,WAAWV,KAAKU,OAAO,CAACE,EAAE,GAAGZ,KAAKU,OAAO;QAEnF,MAAMA,UAAU,MAAMT,IAAIY,OAAO,CAACC,QAAQ,CAAC;YACzCF,IAAID;YACJI,YAAY;QACd;QAEA,IAAIL,WAAWA,QAAQM,QAAQ,EAAE;YAC/B,MAAMX,QAAQ,IAAIC,KAAKN,KAAKI,aAAa;YACzC,MAAMa,aAAaP,QAAQM,QAAQ,GAAG,KAAK,KAAK,kCAAkC;;YAClF,MAAMT,MAAM,IAAID,KAAKD,MAAMG,OAAO,KAAKS;YAEvCjB,KAAKG,WAAW,GAAGI,IAAIE,WAAW;QACpC;IACF,EAAE,OAAOS,OAAO;QACd,yCAAyC;QACzCjB,IAAIY,OAAO,CAACM,MAAM,CAACD,KAAK,CAAC;YACvBA;YACAE,KAAK;QACP;IACF;IAEA,OAAOpB;AACT,EAAC"}
@@ -0,0 +1,6 @@
1
+ import type { CollectionBeforeValidateHook } from 'payload';
2
+ /**
3
+ * Hook to validate that appointments have either a customer OR a guest, but not both
4
+ * Runs before validation on the Appointments collection
5
+ */
6
+ export declare const validateCustomerOrGuest: CollectionBeforeValidateHook;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Hook to validate that appointments have either a customer OR a guest, but not both
3
+ * Runs before validation on the Appointments collection
4
+ */ export const validateCustomerOrGuest = ({ data })=>{
5
+ // Only validate for appointments (not blockouts)
6
+ if (data?.type !== 'appointment') {
7
+ return data;
8
+ }
9
+ const hasCustomer = !!data.customer;
10
+ const hasGuest = !!data.guest;
11
+ // For appointments, must have exactly one: customer OR guest
12
+ if (!hasCustomer && !hasGuest) {
13
+ throw new Error('Appointments must have either a registered customer or a guest customer');
14
+ }
15
+ if (hasCustomer && hasGuest) {
16
+ throw new Error('Appointments cannot have both a registered customer and a guest customer. Please choose one.');
17
+ }
18
+ return data;
19
+ };
20
+
21
+ //# sourceMappingURL=validateCustomerOrGuest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/validateCustomerOrGuest.ts"],"sourcesContent":["import type { CollectionBeforeValidateHook } from 'payload'\n\n/**\n * Hook to validate that appointments have either a customer OR a guest, but not both\n * Runs before validation on the Appointments collection\n */\nexport const validateCustomerOrGuest: CollectionBeforeValidateHook = ({\n data,\n}) => {\n // Only validate for appointments (not blockouts)\n if (data?.type !== 'appointment') {\n return data\n }\n\n const hasCustomer = !!data.customer\n const hasGuest = !!data.guest\n\n // For appointments, must have exactly one: customer OR guest\n if (!hasCustomer && !hasGuest) {\n throw new Error('Appointments must have either a registered customer or a guest customer')\n }\n\n if (hasCustomer && hasGuest) {\n throw new Error('Appointments cannot have both a registered customer and a guest customer. Please choose one.')\n }\n\n return data\n}\n"],"names":["validateCustomerOrGuest","data","type","hasCustomer","customer","hasGuest","guest","Error"],"mappings":"AAEA;;;CAGC,GACD,OAAO,MAAMA,0BAAwD,CAAC,EACpEC,IAAI,EACL;IACC,iDAAiD;IACjD,IAAIA,MAAMC,SAAS,eAAe;QAChC,OAAOD;IACT;IAEA,MAAME,cAAc,CAAC,CAACF,KAAKG,QAAQ;IACnC,MAAMC,WAAW,CAAC,CAACJ,KAAKK,KAAK;IAE7B,6DAA6D;IAC7D,IAAI,CAACH,eAAe,CAACE,UAAU;QAC7B,MAAM,IAAIE,MAAM;IAClB;IAEA,IAAIJ,eAAeE,UAAU;QAC3B,MAAM,IAAIE,MAAM;IAClB;IAEA,OAAON;AACT,EAAC"}
@@ -0,0 +1,23 @@
1
+ import type { Config } from 'payload';
2
+ export type GoromboAppointmentsPluginConfig = {
3
+ /**
4
+ * Disable the plugin without removing the collections from the database
5
+ */
6
+ disabled?: boolean;
7
+ /**
8
+ * Require a Media collection with this slug for team member avatars
9
+ * If not provided, defaults to 'media'
10
+ */
11
+ mediaCollectionSlug?: string;
12
+ /**
13
+ * Require a Users collection with this slug for customer relationships
14
+ * If not provided, defaults to 'users'
15
+ */
16
+ usersCollectionSlug?: string;
17
+ };
18
+ export declare const goromboAppointmentsPlugin: (pluginOptions?: GoromboAppointmentsPluginConfig) => (config: Config) => Config;
19
+ export { Appointments } from './collections/Appointments.js';
20
+ export { GuestCustomers } from './collections/GuestCustomers.js';
21
+ export { Services } from './collections/Services.js';
22
+ export { TeamMembers } from './collections/TeamMembers.js';
23
+ export { OpeningTimes } from './globals/OpeningTimes.js';
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ import { Appointments } from './collections/Appointments.js';
2
+ import { GuestCustomers } from './collections/GuestCustomers.js';
3
+ import { Services } from './collections/Services.js';
4
+ import { TeamMembers } from './collections/TeamMembers.js';
5
+ import { getAvailableSlotsHandler } from './endpoints/getAvailableSlots.js';
6
+ import { OpeningTimes } from './globals/OpeningTimes.js';
7
+ import { addAdminTitle } from './hooks/addAdminTitle.js';
8
+ import { sendCustomerEmail } from './hooks/sendCustomerEmail.js';
9
+ import { setEndDateTime } from './hooks/setEndDateTime.js';
10
+ import { validateCustomerOrGuest } from './hooks/validateCustomerOrGuest.js';
11
+ export const goromboAppointmentsPlugin = (pluginOptions = {})=>(config)=>{
12
+ const { disabled = false, mediaCollectionSlug = 'media', usersCollectionSlug = 'users' } = pluginOptions;
13
+ // Initialize arrays if not present
14
+ if (!config.collections) {
15
+ config.collections = [];
16
+ }
17
+ if (!config.globals) {
18
+ config.globals = [];
19
+ }
20
+ if (!config.endpoints) {
21
+ config.endpoints = [];
22
+ }
23
+ if (!config.admin) {
24
+ config.admin = {};
25
+ }
26
+ if (!config.admin.components) {
27
+ config.admin.components = {};
28
+ }
29
+ // Create Appointments collection with hooks attached
30
+ const AppointmentsWithHooks = {
31
+ ...Appointments,
32
+ hooks: {
33
+ afterChange: [
34
+ sendCustomerEmail
35
+ ],
36
+ beforeChange: [
37
+ setEndDateTime,
38
+ addAdminTitle
39
+ ],
40
+ beforeValidate: [
41
+ validateCustomerOrGuest
42
+ ]
43
+ }
44
+ };
45
+ // Update customer field to use correct users collection if not default
46
+ if (usersCollectionSlug !== 'users') {
47
+ const customerFieldIndex = AppointmentsWithHooks.fields.findIndex((f)=>'name' in f && f.name === 'customer');
48
+ if (customerFieldIndex !== -1) {
49
+ const customerField = AppointmentsWithHooks.fields[customerFieldIndex];
50
+ if ('relationTo' in customerField) {
51
+ customerField.relationTo = usersCollectionSlug;
52
+ }
53
+ }
54
+ }
55
+ // Create TeamMembers with correct media collection
56
+ const TeamMembersWithMedia = {
57
+ ...TeamMembers
58
+ };
59
+ // Update avatar field if not using default media collection
60
+ if (mediaCollectionSlug !== 'media') {
61
+ const avatarFieldIndex = TeamMembersWithMedia.fields.findIndex((f)=>'name' in f && f.name === 'avatar');
62
+ if (avatarFieldIndex !== -1) {
63
+ const avatarField = TeamMembersWithMedia.fields[avatarFieldIndex];
64
+ if ('relationTo' in avatarField) {
65
+ avatarField.relationTo = mediaCollectionSlug;
66
+ }
67
+ }
68
+ }
69
+ // Add collections
70
+ config.collections.push(AppointmentsWithHooks);
71
+ config.collections.push(Services);
72
+ config.collections.push(TeamMembersWithMedia);
73
+ config.collections.push(GuestCustomers);
74
+ // Add globals
75
+ config.globals.push(OpeningTimes);
76
+ /**
77
+ * If the plugin is disabled, we still want to keep added collections/fields
78
+ * so the database schema is consistent which is important for migrations.
79
+ */ if (disabled) {
80
+ return config;
81
+ }
82
+ // Add endpoints
83
+ config.endpoints.push({
84
+ handler: getAvailableSlotsHandler,
85
+ method: 'get',
86
+ path: '/appointments/available-slots'
87
+ });
88
+ // Add admin components
89
+ if (!config.admin.components.beforeDashboard) {
90
+ config.admin.components.beforeDashboard = [];
91
+ }
92
+ config.admin.components.beforeDashboard.push(`gorombo-payload-appointments/client#BeforeDashboardClient`);
93
+ // Add initialization logic
94
+ const incomingOnInit = config.onInit;
95
+ config.onInit = async (payload)=>{
96
+ // Ensure we are executing any existing onInit functions before running our own
97
+ if (incomingOnInit) {
98
+ await incomingOnInit(payload);
99
+ }
100
+ // Seed default opening times if not set
101
+ try {
102
+ const existingOpeningTimes = await payload.findGlobal({
103
+ slug: 'opening-times'
104
+ });
105
+ // If schedule is empty, initialize with defaults
106
+ if (!existingOpeningTimes.schedule || existingOpeningTimes.schedule.length === 0) {
107
+ const defaultSchedule = [
108
+ {
109
+ closeTime: '17:00',
110
+ day: 'monday',
111
+ isOpen: true,
112
+ openTime: '09:00'
113
+ },
114
+ {
115
+ closeTime: '17:00',
116
+ day: 'tuesday',
117
+ isOpen: true,
118
+ openTime: '09:00'
119
+ },
120
+ {
121
+ closeTime: '17:00',
122
+ day: 'wednesday',
123
+ isOpen: true,
124
+ openTime: '09:00'
125
+ },
126
+ {
127
+ closeTime: '17:00',
128
+ day: 'thursday',
129
+ isOpen: true,
130
+ openTime: '09:00'
131
+ },
132
+ {
133
+ closeTime: '17:00',
134
+ day: 'friday',
135
+ isOpen: true,
136
+ openTime: '09:00'
137
+ },
138
+ {
139
+ closeTime: '17:00',
140
+ day: 'saturday',
141
+ isOpen: false,
142
+ openTime: '09:00'
143
+ },
144
+ {
145
+ closeTime: '17:00',
146
+ day: 'sunday',
147
+ isOpen: false,
148
+ openTime: '09:00'
149
+ }
150
+ ];
151
+ await payload.updateGlobal({
152
+ slug: 'opening-times',
153
+ data: {
154
+ allowGuestBooking: true,
155
+ maxAdvanceBookingDays: 30,
156
+ minAdvanceBookingHours: 1,
157
+ schedule: defaultSchedule,
158
+ sendConfirmationEmails: true,
159
+ slotDuration: 30,
160
+ timezone: 'America/New_York'
161
+ }
162
+ });
163
+ payload.logger.info({
164
+ msg: 'Gorombo Appointments Plugin: Initialized default opening times'
165
+ });
166
+ }
167
+ } catch (error) {
168
+ payload.logger.error({
169
+ error,
170
+ msg: 'Gorombo Appointments Plugin: Failed to initialize opening times'
171
+ });
172
+ }
173
+ };
174
+ return config;
175
+ };
176
+ // Re-export collections for type generation
177
+ export { Appointments } from './collections/Appointments.js';
178
+ export { GuestCustomers } from './collections/GuestCustomers.js';
179
+ export { Services } from './collections/Services.js';
180
+ export { TeamMembers } from './collections/TeamMembers.js';
181
+ export { OpeningTimes } from './globals/OpeningTimes.js';
182
+
183
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { CollectionConfig, Config } from 'payload'\n\nimport { Appointments } from './collections/Appointments.js'\nimport { GuestCustomers } from './collections/GuestCustomers.js'\nimport { Services } from './collections/Services.js'\nimport { TeamMembers } from './collections/TeamMembers.js'\nimport { getAvailableSlotsHandler } from './endpoints/getAvailableSlots.js'\nimport { OpeningTimes } from './globals/OpeningTimes.js'\nimport { addAdminTitle } from './hooks/addAdminTitle.js'\nimport { sendCustomerEmail } from './hooks/sendCustomerEmail.js'\nimport { setEndDateTime } from './hooks/setEndDateTime.js'\nimport { validateCustomerOrGuest } from './hooks/validateCustomerOrGuest.js'\n\nexport type GoromboAppointmentsPluginConfig = {\n /**\n * Disable the plugin without removing the collections from the database\n */\n disabled?: boolean\n /**\n * Require a Media collection with this slug for team member avatars\n * If not provided, defaults to 'media'\n */\n mediaCollectionSlug?: string\n /**\n * Require a Users collection with this slug for customer relationships\n * If not provided, defaults to 'users'\n */\n usersCollectionSlug?: string\n}\n\nexport const goromboAppointmentsPlugin =\n (pluginOptions: GoromboAppointmentsPluginConfig = {}) =>\n (config: Config): Config => {\n const { disabled = false, mediaCollectionSlug = 'media', usersCollectionSlug = 'users' } =\n pluginOptions\n\n // Initialize arrays if not present\n if (!config.collections) {\n config.collections = []\n }\n if (!config.globals) {\n config.globals = []\n }\n if (!config.endpoints) {\n config.endpoints = []\n }\n if (!config.admin) {\n config.admin = {}\n }\n if (!config.admin.components) {\n config.admin.components = {}\n }\n\n // Create Appointments collection with hooks attached\n const AppointmentsWithHooks: CollectionConfig = {\n ...Appointments,\n hooks: {\n afterChange: [sendCustomerEmail],\n beforeChange: [setEndDateTime, addAdminTitle],\n beforeValidate: [validateCustomerOrGuest],\n },\n }\n\n // Update customer field to use correct users collection if not default\n if (usersCollectionSlug !== 'users') {\n const customerFieldIndex = AppointmentsWithHooks.fields.findIndex(\n (f) => 'name' in f && f.name === 'customer'\n )\n if (customerFieldIndex !== -1) {\n const customerField = AppointmentsWithHooks.fields[customerFieldIndex]\n if ('relationTo' in customerField) {\n (customerField as { relationTo: string }).relationTo = usersCollectionSlug\n }\n }\n }\n\n // Create TeamMembers with correct media collection\n const TeamMembersWithMedia: CollectionConfig = { ...TeamMembers }\n\n // Update avatar field if not using default media collection\n if (mediaCollectionSlug !== 'media') {\n const avatarFieldIndex = TeamMembersWithMedia.fields.findIndex(\n (f) => 'name' in f && f.name === 'avatar'\n )\n if (avatarFieldIndex !== -1) {\n const avatarField = TeamMembersWithMedia.fields[avatarFieldIndex]\n if ('relationTo' in avatarField) {\n (avatarField as { relationTo: string }).relationTo = mediaCollectionSlug\n }\n }\n }\n\n // Add collections\n config.collections.push(AppointmentsWithHooks)\n config.collections.push(Services)\n config.collections.push(TeamMembersWithMedia)\n config.collections.push(GuestCustomers)\n\n // Add globals\n config.globals.push(OpeningTimes)\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields\n * so the database schema is consistent which is important for migrations.\n */\n if (disabled) {\n return config\n }\n\n // Add endpoints\n config.endpoints.push({\n handler: getAvailableSlotsHandler,\n method: 'get',\n path: '/appointments/available-slots',\n })\n\n // Add admin components\n if (!config.admin.components.beforeDashboard) {\n config.admin.components.beforeDashboard = []\n }\n\n config.admin.components.beforeDashboard.push(\n `gorombo-payload-appointments/client#BeforeDashboardClient`,\n )\n\n // Add initialization logic\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n // Ensure we are executing any existing onInit functions before running our own\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n\n // Seed default opening times if not set\n try {\n const existingOpeningTimes = await payload.findGlobal({\n slug: 'opening-times',\n })\n\n // If schedule is empty, initialize with defaults\n if (!existingOpeningTimes.schedule || existingOpeningTimes.schedule.length === 0) {\n const defaultSchedule = [\n { closeTime: '17:00', day: 'monday', isOpen: true, openTime: '09:00' },\n { closeTime: '17:00', day: 'tuesday', isOpen: true, openTime: '09:00' },\n { closeTime: '17:00', day: 'wednesday', isOpen: true, openTime: '09:00' },\n { closeTime: '17:00', day: 'thursday', isOpen: true, openTime: '09:00' },\n { closeTime: '17:00', day: 'friday', isOpen: true, openTime: '09:00' },\n { closeTime: '17:00', day: 'saturday', isOpen: false, openTime: '09:00' },\n { closeTime: '17:00', day: 'sunday', isOpen: false, openTime: '09:00' },\n ]\n\n await payload.updateGlobal({\n slug: 'opening-times',\n data: {\n allowGuestBooking: true,\n maxAdvanceBookingDays: 30,\n minAdvanceBookingHours: 1,\n schedule: defaultSchedule,\n sendConfirmationEmails: true,\n slotDuration: 30,\n timezone: 'America/New_York',\n },\n })\n\n payload.logger.info({\n msg: 'Gorombo Appointments Plugin: Initialized default opening times',\n })\n }\n } catch (error) {\n payload.logger.error({\n error,\n msg: 'Gorombo Appointments Plugin: Failed to initialize opening times',\n })\n }\n }\n\n return config\n }\n\n// Re-export collections for type generation\nexport { Appointments } from './collections/Appointments.js'\nexport { GuestCustomers } from './collections/GuestCustomers.js'\nexport { Services } from './collections/Services.js'\nexport { TeamMembers } from './collections/TeamMembers.js'\nexport { OpeningTimes } from './globals/OpeningTimes.js'\n"],"names":["Appointments","GuestCustomers","Services","TeamMembers","getAvailableSlotsHandler","OpeningTimes","addAdminTitle","sendCustomerEmail","setEndDateTime","validateCustomerOrGuest","goromboAppointmentsPlugin","pluginOptions","config","disabled","mediaCollectionSlug","usersCollectionSlug","collections","globals","endpoints","admin","components","AppointmentsWithHooks","hooks","afterChange","beforeChange","beforeValidate","customerFieldIndex","fields","findIndex","f","name","customerField","relationTo","TeamMembersWithMedia","avatarFieldIndex","avatarField","push","handler","method","path","beforeDashboard","incomingOnInit","onInit","payload","existingOpeningTimes","findGlobal","slug","schedule","length","defaultSchedule","closeTime","day","isOpen","openTime","updateGlobal","data","allowGuestBooking","maxAdvanceBookingDays","minAdvanceBookingHours","sendConfirmationEmails","slotDuration","timezone","logger","info","msg","error"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,135 @@
1
+ {
2
+ "name": "gorombo-payload-appointments",
3
+ "version": "1.0.0",
4
+ "description": "Appointments, scheduling, and booking plugin for PayloadCMS 3.x",
5
+ "author": "Daniel T Sasser II <dan@gorombo.com>",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "payload",
9
+ "payloadcms",
10
+ "plugin",
11
+ "appointments",
12
+ "booking",
13
+ "scheduling",
14
+ "calendar",
15
+ "payload-plugin"
16
+ ],
17
+ "homepage": "https://github.com/dansasser/gorombo-appointment-plugin#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/dansasser/gorombo-appointment-plugin.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/dansasser/gorombo-appointment-plugin/issues"
24
+ },
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./src/index.ts",
29
+ "types": "./src/index.ts",
30
+ "default": "./src/index.ts"
31
+ },
32
+ "./client": {
33
+ "import": "./src/exports/client.ts",
34
+ "types": "./src/exports/client.ts",
35
+ "default": "./src/exports/client.ts"
36
+ },
37
+ "./rsc": {
38
+ "import": "./src/exports/rsc.ts",
39
+ "types": "./src/exports/rsc.ts",
40
+ "default": "./src/exports/rsc.ts"
41
+ }
42
+ },
43
+ "main": "./src/index.ts",
44
+ "types": "./src/index.ts",
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "scripts": {
49
+ "build": "npm run copyfiles && npm run build:types && npm run build:swc",
50
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
51
+ "build:types": "tsc --outDir dist --rootDir ./src",
52
+ "clean": "rimraf dist *.tsbuildinfo",
53
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
54
+ "dev": "next dev dev --turbo",
55
+ "dev:generate-importmap": "npm run dev:payload -- generate:importmap",
56
+ "dev:generate-types": "npm run dev:payload -- generate:types",
57
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
58
+ "generate:importmap": "npm run dev:generate-importmap",
59
+ "generate:types": "npm run dev:generate-types",
60
+ "lint": "eslint ./src",
61
+ "lint:fix": "eslint ./src --fix",
62
+ "format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
63
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"",
64
+ "typecheck": "tsc --noEmit",
65
+ "prepublishOnly": "npm run clean && npm run build",
66
+ "test": "npm run test:int",
67
+ "test:int": "vitest run",
68
+ "test:watch": "vitest",
69
+ "test:e2e": "playwright test"
70
+ },
71
+ "devDependencies": {
72
+ "@eslint/eslintrc": "^3.2.0",
73
+ "@payloadcms/db-mongodb": "3.37.0",
74
+ "@payloadcms/db-postgres": "3.37.0",
75
+ "@payloadcms/db-sqlite": "3.37.0",
76
+ "@payloadcms/eslint-config": "3.9.0",
77
+ "@payloadcms/next": "3.37.0",
78
+ "@payloadcms/richtext-lexical": "3.37.0",
79
+ "@payloadcms/ui": "3.37.0",
80
+ "@playwright/test": "1.56.1",
81
+ "@swc-node/register": "1.10.9",
82
+ "@swc/cli": "0.6.0",
83
+ "@swc/core": "^1.15.7",
84
+ "@types/node": "^22.5.4",
85
+ "@types/react": "19.2.1",
86
+ "@types/react-dom": "19.2.1",
87
+ "copyfiles": "2.4.1",
88
+ "cross-env": "^7.0.3",
89
+ "eslint": "^9.23.0",
90
+ "eslint-config-next": "15.4.7",
91
+ "graphql": "^16.8.1",
92
+ "mongodb-memory-server": "10.1.4",
93
+ "next": "15.4.10",
94
+ "open": "^10.1.0",
95
+ "payload": "3.37.0",
96
+ "prettier": "^3.4.2",
97
+ "qs-esm": "7.0.2",
98
+ "react": "19.2.1",
99
+ "react-dom": "19.2.1",
100
+ "rimraf": "3.0.2",
101
+ "sharp": "0.34.2",
102
+ "sort-package-json": "^2.10.0",
103
+ "typescript": "5.7.3",
104
+ "vite-tsconfig-paths": "^5.1.4",
105
+ "vitest": "^3.1.2"
106
+ },
107
+ "peerDependencies": {
108
+ "payload": "^3.37.0"
109
+ },
110
+ "engines": {
111
+ "node": "^18.20.2 || >=20.9.0"
112
+ },
113
+ "publishConfig": {
114
+ "exports": {
115
+ ".": {
116
+ "import": "./dist/index.js",
117
+ "types": "./dist/index.d.ts",
118
+ "default": "./dist/index.js"
119
+ },
120
+ "./client": {
121
+ "import": "./dist/exports/client.js",
122
+ "types": "./dist/exports/client.d.ts",
123
+ "default": "./dist/exports/client.js"
124
+ },
125
+ "./rsc": {
126
+ "import": "./dist/exports/rsc.js",
127
+ "types": "./dist/exports/rsc.d.ts",
128
+ "default": "./dist/exports/rsc.js"
129
+ }
130
+ },
131
+ "main": "./dist/index.js",
132
+ "types": "./dist/index.d.ts"
133
+ },
134
+ "registry": "https://registry.npmjs.org/"
135
+ }