224 lines
7.9 KiB
TypeScript
224 lines
7.9 KiB
TypeScript
import { Resend } from "resend";
|
|
import { db } from "@/lib/db";
|
|
import type { PurchaseOrder, User } from "@prisma/client";
|
|
|
|
const isDev = process.env.NODE_ENV === "development";
|
|
const resend = isDev ? null : new Resend(process.env.RESEND_API_KEY);
|
|
const FROM = `${process.env.EMAIL_FROM_NAME ?? "PPMS"} <${process.env.EMAIL_FROM ?? "noreply@ppms.pelagiamarine.com"}>`;
|
|
|
|
export type NotificationEvent =
|
|
| "PO_SUBMITTED"
|
|
| "PO_APPROVED"
|
|
| "PO_APPROVED_WITH_NOTE"
|
|
| "PO_REJECTED"
|
|
| "EDITS_REQUESTED"
|
|
| "VENDOR_ID_REQUESTED"
|
|
| "VENDOR_ID_PROVIDED"
|
|
| "PAYMENT_PROCESSING"
|
|
| "PAYMENT_SENT"
|
|
| "RECEIPT_CONFIRMED";
|
|
|
|
interface NotifyParams {
|
|
event: NotificationEvent;
|
|
po: PurchaseOrder & { submitter: User };
|
|
recipients: User[];
|
|
note?: string;
|
|
}
|
|
|
|
export async function notify({ event, po, recipients, note }: NotifyParams) {
|
|
const subject = buildSubject(event, po.poNumber);
|
|
if (!subject) return;
|
|
|
|
await Promise.allSettled(
|
|
recipients.map(async (recipient) => {
|
|
const body = buildInAppBody(event, po, recipient);
|
|
const link = buildInAppLink(event, po, recipient);
|
|
|
|
let status = "sent";
|
|
if (isDev) {
|
|
console.log(
|
|
`\n📧 [DEV EMAIL] To: ${recipient.email}\n Subject: ${subject}\n Body: ${buildEmailBody(event, po, note)}\n`
|
|
);
|
|
} else {
|
|
try {
|
|
const { error } = await resend!.emails.send({
|
|
from: FROM,
|
|
to: recipient.email,
|
|
subject,
|
|
html: buildHtml(event, po, recipient, note),
|
|
});
|
|
if (error) status = "failed";
|
|
} catch {
|
|
status = "failed";
|
|
}
|
|
}
|
|
|
|
await db.notification.create({
|
|
data: { subject, body, link, status, poId: po.id, userId: recipient.id },
|
|
});
|
|
})
|
|
);
|
|
}
|
|
|
|
// ── In-app message ────────────────────────────────────────────────────────────
|
|
|
|
function buildInAppBody(
|
|
event: NotificationEvent,
|
|
po: PurchaseOrder & { submitter: User },
|
|
recipient: User
|
|
): string {
|
|
const pn = po.poNumber;
|
|
const submitter = po.submitter.name;
|
|
|
|
switch (event) {
|
|
case "PO_SUBMITTED":
|
|
return recipient.id === po.submitterId
|
|
? `${pn} submitted for review`
|
|
: `${submitter} submitted ${pn} for your review`;
|
|
|
|
case "PO_APPROVED":
|
|
case "PO_APPROVED_WITH_NOTE":
|
|
return recipient.id === po.submitterId
|
|
? `${pn} approved`
|
|
: `${pn} approved — ready for payment`;
|
|
|
|
case "PO_REJECTED":
|
|
return `${pn} rejected`;
|
|
|
|
case "EDITS_REQUESTED":
|
|
return `Edits requested on ${pn}`;
|
|
|
|
case "VENDOR_ID_REQUESTED":
|
|
return `Vendor ID needed before ${pn} can be approved`;
|
|
|
|
case "VENDOR_ID_PROVIDED":
|
|
return `Vendor ID provided for ${pn} — ready to review`;
|
|
|
|
case "PAYMENT_PROCESSING":
|
|
return `Payment processing for ${pn}`;
|
|
|
|
case "PAYMENT_SENT":
|
|
return `Payment confirmed for ${pn} — please confirm receipt`;
|
|
|
|
case "RECEIPT_CONFIRMED":
|
|
return `Receipt confirmed — ${pn} closed`;
|
|
|
|
default:
|
|
return `Update on ${pn}`;
|
|
}
|
|
}
|
|
|
|
function buildInAppLink(
|
|
event: NotificationEvent,
|
|
po: PurchaseOrder & { submitter: User },
|
|
recipient: User
|
|
): string {
|
|
const isManager = recipient.role === "MANAGER" || recipient.role === "SUPERUSER";
|
|
const isAccounts = recipient.role === "ACCOUNTS";
|
|
const isSubmitter = recipient.id === po.submitterId;
|
|
|
|
switch (event) {
|
|
// Manager needs to act on the approval queue
|
|
case "PO_SUBMITTED":
|
|
case "VENDOR_ID_PROVIDED":
|
|
return isManager ? `/approvals/${po.id}` : `/po/${po.id}`;
|
|
|
|
// Accounts needs to process payment; everyone else sees the PO
|
|
case "PO_APPROVED":
|
|
case "PO_APPROVED_WITH_NOTE":
|
|
return isAccounts ? `/payments` : `/po/${po.id}`;
|
|
|
|
// Submitter needs to open the edit form
|
|
case "EDITS_REQUESTED":
|
|
return isSubmitter ? `/po/${po.id}/edit` : `/po/${po.id}`;
|
|
|
|
// Submitter needs to confirm receipt
|
|
case "PAYMENT_SENT":
|
|
return isSubmitter ? `/po/${po.id}/receipt` : `/po/${po.id}`;
|
|
|
|
default:
|
|
return `/po/${po.id}`;
|
|
}
|
|
}
|
|
|
|
// ── Email subject ─────────────────────────────────────────────────────────────
|
|
|
|
function buildSubject(event: NotificationEvent, poNumber: string): string | null {
|
|
const base = `PO ${poNumber}`;
|
|
const map: Record<NotificationEvent, string> = {
|
|
PO_SUBMITTED: `${base} submitted for review`,
|
|
PO_APPROVED: `${base} has been approved`,
|
|
PO_APPROVED_WITH_NOTE: `${base} has been approved`,
|
|
PO_REJECTED: `${base} has been rejected`,
|
|
EDITS_REQUESTED: `Edits requested on ${base}`,
|
|
VENDOR_ID_REQUESTED: `Vendor ID needed for ${base}`,
|
|
VENDOR_ID_PROVIDED: `Vendor ID provided for ${base}`,
|
|
PAYMENT_PROCESSING: `Payment initiated for ${base}`,
|
|
PAYMENT_SENT: `Payment confirmed for ${base}`,
|
|
RECEIPT_CONFIRMED: `Receipt confirmed — ${base} closed`,
|
|
};
|
|
return map[event] ?? null;
|
|
}
|
|
|
|
// ── Email HTML ────────────────────────────────────────────────────────────────
|
|
|
|
function buildHtml(
|
|
event: NotificationEvent,
|
|
po: PurchaseOrder & { submitter: User },
|
|
recipient: User,
|
|
note?: string
|
|
): string {
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family:Inter,sans-serif;max-width:560px;margin:0 auto;padding:32px 24px;color:#111827;">
|
|
<div style="border-bottom:2px solid #2563eb;padding-bottom:12px;margin-bottom:24px;">
|
|
<span style="font-size:20px;font-weight:700;color:#1d4ed8;">PPMS</span>
|
|
</div>
|
|
<p style="margin:0 0 16px;">Hi ${recipient.name},</p>
|
|
<p style="margin:0 0 24px;">${buildEmailBody(event, po, note)}</p>
|
|
<div style="background:#f3f4f6;border-radius:8px;padding:16px;font-size:13px;color:#374151;">
|
|
<strong>PO Number:</strong> ${po.poNumber}<br/>
|
|
<strong>Title:</strong> ${po.title}<br/>
|
|
<strong>Submitted by:</strong> ${po.submitter.name}
|
|
</div>
|
|
<p style="margin-top:32px;font-size:12px;color:#9ca3af;">
|
|
You received this message because you are a stakeholder on this purchase order.
|
|
</p>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function buildEmailBody(
|
|
event: NotificationEvent,
|
|
po: PurchaseOrder & { submitter: User },
|
|
note?: string
|
|
): string {
|
|
const noteHtml = note
|
|
? `<br/><br/><em style="color:#374151;">"${note}"</em>`
|
|
: "";
|
|
|
|
switch (event) {
|
|
case "PO_SUBMITTED":
|
|
return `<strong>${po.submitter.name}</strong> has submitted purchase order <strong>${po.poNumber}</strong> for your review.`;
|
|
case "PO_APPROVED":
|
|
case "PO_APPROVED_WITH_NOTE":
|
|
return `Your purchase order <strong>${po.poNumber}</strong> has been <span style="color:#16a34a;font-weight:600;">approved</span>.${noteHtml}`;
|
|
case "PO_REJECTED":
|
|
return `Your purchase order <strong>${po.poNumber}</strong> has been <span style="color:#dc2626;font-weight:600;">rejected</span>.${noteHtml}`;
|
|
case "EDITS_REQUESTED":
|
|
return `Edits have been requested on <strong>${po.poNumber}</strong>. Please update the order and resubmit.${noteHtml}`;
|
|
case "VENDOR_ID_REQUESTED":
|
|
return `A vendor ID is required before <strong>${po.poNumber}</strong> can be approved.`;
|
|
case "VENDOR_ID_PROVIDED":
|
|
return `The vendor ID has been provided for <strong>${po.poNumber}</strong>. It is ready for your review.`;
|
|
case "PAYMENT_PROCESSING":
|
|
return `Payment is being processed for <strong>${po.poNumber}</strong>.`;
|
|
case "PAYMENT_SENT":
|
|
return `Payment has been confirmed for <strong>${po.poNumber}</strong>. Please confirm delivery/receipt when the goods arrive.`;
|
|
case "RECEIPT_CONFIRMED":
|
|
return `Receipt has been confirmed for <strong>${po.poNumber}</strong>. The order is now closed.`;
|
|
default:
|
|
return `There has been an update on purchase order ${po.poNumber}.`;
|
|
}
|
|
}
|