Adds an "Email to vendor" button on the PO detail (available once approved,
through CLOSED, and again after payment) that opens an Outlook draft addressed
to the vendor's primary contact with a time-limited PDF download link.
Since mailto: can't attach files, the PDF is rendered and stored, and the draft
carries a link (the approach chosen for this issue):
- PdfService/: new standalone Express + Playwright microservice (GstService/
EpfoService pattern) — POST /pdf { url } renders a page to a real PDF via
headless Chromium. SSRF-guarded (shared token + optional origin allowlist).
- export route: accepts a server-only `svc` token (PDF_SERVICE_TOKEN) so
PdfService can fetch /api/po/[id]/export?format=pdf without a user session;
`pdf=1` drops the print button + window.print() auto-trigger.
- lib/pdf-service.ts renderPoPdf(); prepareVendorEmail() server action renders →
uploads to R2 (po-pdf/…) → presigns a 7-day link → returns a mailto draft.
- po-detail: EmailVendorButton, shown when approved + vendor has a contact email.
- Gated by PDF_SERVICE_URL/PDF_SERVICE_TOKEN; friendly error if unconfigured.
- No DB model/migration. Tests: prepareVendorEmail (6, PdfService/storage mocked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
1.3 KiB
TypeScript
40 lines
1.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { prepareVendorEmail } from "@/app/(portal)/po/[id]/email-actions";
|
|
|
|
/**
|
|
* "Email to vendor" (issue #14): generates the PO PDF, stores it, and opens an
|
|
* Outlook (default mail client) draft addressed to the vendor's primary contact
|
|
* with a download link in the body. The user reviews and sends it themselves.
|
|
*/
|
|
export function EmailVendorButton({ poId }: { poId: string }) {
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
async function handleClick() {
|
|
setPending(true);
|
|
setError("");
|
|
const result = await prepareVendorEmail(poId);
|
|
setPending(false);
|
|
if ("error" in result) {
|
|
setError(result.error);
|
|
} else {
|
|
// Opens the default mail client (Outlook) with a pre-filled draft.
|
|
window.location.href = result.mailto;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="inline-flex flex-col items-start gap-1">
|
|
<button
|
|
onClick={handleClick}
|
|
disabled={pending}
|
|
className="rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60"
|
|
>
|
|
{pending ? "Preparing…" : "Email to vendor"}
|
|
</button>
|
|
{error && <span className="text-xs text-danger-700 max-w-xs">{error}</span>}
|
|
</div>
|
|
);
|
|
}
|