"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { Resend } from "resend"; import { pbCreate, pbList, pbUpdate } from "@/lib/pocketbase"; import { getAuth } from "@/lib/auth"; import { pbId } from "@/lib/pb"; import { generateCustomerStatement } from "@/lib/pdf/customer-statement"; import type { Customer, DeliveryNote, FuelSettings } from "@/lib/types"; export type ActionResult = { ok: true; data?: any } | { ok: false; error: string }; const sendSchema = z.object({ customer: z.string().min(1, "Customer is required"), range: z.enum(["this-month", "last-month", "custom"]).default("this-month"), start: z.string().optional(), end: z.string().optional(), recipient_email: z.string().email("Valid email required"), include_pricing: z.coerce.boolean().default(true), }); function rangeDates(range: string, start?: string, end?: string): { start: string; end: string } { const now = new Date(); if (range === "this-month") { const s = new Date(now.getFullYear(), now.getMonth(), 1); const e = new Date(now.getFullYear(), now.getMonth() + 1, 0); return { start: s.toISOString().slice(0, 10), end: e.toISOString().slice(0, 10) }; } if (range === "last-month") { const s = new Date(now.getFullYear(), now.getMonth() - 1, 1); const e = new Date(now.getFullYear(), now.getMonth(), 0); return { start: s.toISOString().slice(0, 10), end: e.toISOString().slice(0, 10) }; } if (range === "custom" && start && end) { return { start, end }; } return { start: start || "", end: end || "" }; } export async function previewStatement(_: unknown, formData: FormData): Promise { // For preview, we just need to return the data — the actual PDF is generated by the API route. // But we can validate here too. const parsed = sendSchema.safeParse({ customer: formData.get("customer"), range: formData.get("range") || "this-month", start: formData.get("start") || undefined, end: formData.get("end") || undefined, recipient_email: formData.get("recipient_email"), include_pricing: formData.get("include_pricing") === "on" || formData.get("include_pricing") === "true", }); if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" }; return { ok: true, data: parsed.data }; } export async function sendStatement(_: unknown, formData: FormData): Promise { const { token, user } = await getAuth(); if (!token) return { ok: false, error: "Not signed in" }; const parsed = sendSchema.safeParse({ customer: formData.get("customer"), range: formData.get("range") || "this-month", start: formData.get("start") || undefined, end: formData.get("end") || undefined, recipient_email: formData.get("recipient_email"), include_pricing: formData.get("include_pricing") === "on" || formData.get("include_pricing") === "true", }); if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" }; const { start, end } = rangeDates(parsed.data.range, parsed.data.start, parsed.data.end); if (!start || !end) return { ok: false, error: "Date range is required" }; try { // Load customer const customer = await pbList("customers", { perPage: 1, filter: `id = "${parsed.data.customer}"` }, token); if (customer.items.length === 0) return { ok: false, error: "Customer not found" }; const cust = customer.items[0]; // Load notes in range const notes = await pbList( "delivery_notes", { perPage: 1000, filter: `customer = "${parsed.data.customer}" && date >= "${start}" && date <= "${end}"`, sort: "date", }, token, ); if (notes.items.length === 0) { return { ok: false, error: "No delivery notes in this range" }; } // Load settings const settings = await pbList("fuel_settings", { perPage: 1 }, token); const settingsRow = settings.items[0] || null; // Generate PDF const pdfBytes = await generateCustomerStatement({ customer: cust, notes: notes.items, startDate: start, endDate: end, includePricing: parsed.data.include_pricing, companySettings: settingsRow, }); // Email via Resend const apiKey = process.env.RESEND_API_KEY; if (!apiKey) return { ok: false, error: "RESEND_API_KEY not configured" }; const fromAddr = process.env.RESEND_FROM || settingsRow?.statement_from_email || "statements@rjbulk.co.za"; const resend = new Resend(apiKey); const cc = settingsRow?.statement_cc_email ? [settingsRow.statement_cc_email] : undefined; const totalQty = notes.items.reduce((a, n) => a + n.qty_litres_supplied, 0); const totalValue = notes.items.reduce((a, n) => a + (n.total_value || 0), 0); const result = await resend.emails.send({ from: fromAddr, to: parsed.data.recipient_email, cc, subject: `Delivery Statement — ${cust.name} (${start} to ${end})`, html: `

Hi ${cust.name},

Please find your delivery statement attached for the period ${start} to ${end}.

Total litres delivered: ${totalQty.toFixed(2)} L
Total value: R ${totalValue.toFixed(2)}

Thank you for your business.

— RJ Bulk Supplies

`, attachments: [ { filename: `delivery_statement_${cust.name.replace(/\s+/g, "_")}_${start.replace(/-/g, "")}.pdf`, content: Buffer.from(pdfBytes), }, ], }); if (result.error) { return { ok: false, error: result.error.message || "Email send failed" }; } // Notes stay "Pending" — the customer pays separately (EFT / CASH / CARD). // We don't auto-flip the status here; the owner marks notes as paid manually. // Log await pbCreate("statement_log", { id: pbId(), action: "Email Sent", details: `${cust.name} — ${notes.items.length} note(s) — ${start} to ${end}`, actor: user?.id, }, token); revalidatePath("/delivery-notes"); revalidatePath("/statements"); return { ok: true }; } catch (err: any) { return { ok: false, error: err?.message || "Failed to send statement" }; } } export async function logPreview(customerId: string, customerName: string, start: string, end: string): Promise { const { token, user } = await getAuth(); if (!token) return; try { await pbCreate("statement_log", { id: pbId(), action: "Preview Generated", details: `${customerName} — ${start} to ${end}`, actor: user?.id, }, token); } catch {} }