159 lines
6.1 KiB
TypeScript
159 lines
6.1 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import { sendEmail } from "@/lib/email";
|
|
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<ActionResult> {
|
|
// 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<ActionResult> {
|
|
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<Customer>("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<DeliveryNote>(
|
|
"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<FuelSettings>("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 Gmail SMTP
|
|
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);
|
|
|
|
await sendEmail({
|
|
to: parsed.data.recipient_email,
|
|
subject: `Delivery Statement — ${cust.name} (${start} to ${end})`,
|
|
html: `<p>Hi ${cust.name},</p>
|
|
<p>Please find your delivery statement attached for the period <strong>${start}</strong> to <strong>${end}</strong>.</p>
|
|
<p>Total litres delivered: <strong>${totalQty.toFixed(2)} L</strong><br/>
|
|
Total value: <strong>R ${totalValue.toFixed(2)}</strong></p>
|
|
<p>Thank you for your business.</p>
|
|
<p>— RJ Bulk Supplies</p>`,
|
|
attachments: [
|
|
{
|
|
filename: `delivery_statement_${cust.name.replace(/\s+/g, "_")}_${start.replace(/-/g, "")}.pdf`,
|
|
content: Buffer.from(pdfBytes),
|
|
},
|
|
],
|
|
});
|
|
|
|
// 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<void> {
|
|
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 {}
|
|
}
|