diff --git a/collections/Timeslots.ts b/collections/Timeslots.ts index cd6c04f..5907d7c 100644 --- a/collections/Timeslots.ts +++ b/collections/Timeslots.ts @@ -1,5 +1,12 @@ -import type { CollectionAfterChangeHook, CollectionConfig } from "payload"; -import { sendTimeslotChangedEmail } from "../lib/email"; +import type { + CollectionAfterChangeHook, + CollectionAfterDeleteHook, + CollectionConfig, +} from "payload"; +import { + sendTimeslotChangedEmail, + sendTimeslotDeletedEmail, +} from "../lib/email"; import { getPayloadClient } from "../lib/payload"; /** @@ -97,6 +104,81 @@ const notifyOnTimeslotChange: CollectionAfterChangeHook = async ({ return doc; }; +// ── afterDelete hook: notify customers when a booked timeslot is deleted ── +const notifyOnTimeslotDelete: CollectionAfterDeleteHook = async ({ + id, + req, +}) => { + // Fire-and-forget so the admin UI isn't blocked + void (async () => { + try { + const payload = req.payload ?? (await getPayloadClient()); + + // Find all orders that still reference the deleted timeslot + const { docs: orders } = await payload.find({ + collection: "orders", + where: { pickupTimeslot: { equals: id } }, + depth: 1, // populate the customer relationship + limit: 0, // no limit — fetch all + }); + + if (orders.length === 0) return; + + // Clear the dangling timeslot reference and email each customer + await Promise.allSettled( + orders.map(async (order) => { + // Remove the reference so the customer can re-select a slot + try { + await payload.update({ + collection: "orders", + id: order.id, + data: { pickupTimeslot: null }, + }); + } catch (error) { + console.error( + `[Timeslots] Failed to clear pickupTimeslot on order ${order.orderNumber}:`, + error, + ); + } + + const customer = order.customer as { + email?: string; + name?: string; + } | null; + + if (!customer?.email) return; + + await sendTimeslotDeletedEmail({ + to: customer.email, + customerName: customer.name || "Customer", + orderNumber: order.orderNumber, + files: (order.files || []) as { + fileName: string; + pageCount: number; + copies: number; + colorMode: string; + paperSize: string; + doubleSided: boolean; + }[], + pricing: order.pricing as + | { subtotal: number; tax: number; total: number } + | undefined, + }); + }), + ); + + console.log( + `[Timeslots] Notified ${orders.length} order(s) about deleted timeslot ${id}`, + ); + } catch (error) { + console.error( + `[Timeslots] Failed to send timeslot-deleted emails for ${id}:`, + error, + ); + } + })(); +}; + export const Timeslots: CollectionConfig = { slug: "timeslots", admin: { @@ -123,6 +205,7 @@ export const Timeslots: CollectionConfig = { }, hooks: { afterChange: [notifyOnTimeslotChange], + afterDelete: [notifyOnTimeslotDelete], }, fields: [ { diff --git a/lib/email.ts b/lib/email.ts index e94d4e4..2251876 100644 --- a/lib/email.ts +++ b/lib/email.ts @@ -281,6 +281,36 @@ export async function sendTimeslotChangedEmail(params: { }); } +/** + * Notify affected customers (one email per order) when an admin deletes a + * timeslot. Apologises and asks the customer to select a new pickup slot. + */ +export async function sendTimeslotDeletedEmail(params: { + to: string; + customerName: string; + orderNumber: string; + files: OrderFile[]; + pricing: PricingInfo | undefined; +}): Promise { + const { to, customerName, orderNumber, files, pricing } = params; + + const common = await buildCommonLocals(customerName); + + const html = renderTemplate("timeslotDeleted", { + ...common, + orderNumber, + files: formatFilesForTemplate(files), + ...formatPricingForTemplate(pricing), + }); + + await getTransporter().sendMail({ + from: fromAddress(), + to, + subject: `Action Needed: Select a New Pickup Time — ${orderNumber}`, + html, + }); +} + // ── Helpers ────────────────────────────────────────────────────────────── /** Convert Payload rich-text (Lexical/Slate) content into simple email-safe HTML, handling common node types. */ diff --git a/lib/templates/timeslotDeleted.pug b/lib/templates/timeslotDeleted.pug new file mode 100644 index 0000000..629b34f --- /dev/null +++ b/lib/templates/timeslotDeleted.pug @@ -0,0 +1,37 @@ +include _mixins + +doctype html +html + head + meta(charset="utf-8") + meta(name="viewport" content="width=device-width, initial-scale=1.0") + include _styles + body + .container + .header + h1 Pickup Timeslot No Longer Available + p Order ##{orderNumber} + + .content + p Hi #{customerName}, + + p We are sorry, but the pickup timeslot you selected for your order is no longer available and has been removed. We apologise for the inconvenience this may cause. + + p Your order itself is safe and has not been affected. All you need to do is choose a new pickup time. + + .section + .action-box + h3 Select a New Pickup Slot + p Please head to your orders page to choose a new time to collect your prints. + p + a.btn(href=ordersUrl) Select a New Pickup Slot + + +fileList(files) + + +pricingTable(subtotal, tax, total) + + p If you have any questions or need help choosing a new time, please contact us: + +contactAndFooter(contactEmail, contactPhone) + + .footer + p Primal Printing. Thank you for your order and for your patience.