Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 85 additions & 2 deletions collections/Timeslots.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand Down Expand Up @@ -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: {
Expand All @@ -123,6 +205,7 @@ export const Timeslots: CollectionConfig = {
},
hooks: {
afterChange: [notifyOnTimeslotChange],
afterDelete: [notifyOnTimeslotDelete],
},
fields: [
{
Expand Down
30 changes: 30 additions & 0 deletions lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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. */
Expand Down
37 changes: 37 additions & 0 deletions lib/templates/timeslotDeleted.pug
Original file line number Diff line number Diff line change
@@ -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.
Loading