Adds complete initial codebase for the RJ Bulk Supplies fuel delivery backoffice: - Authentication pages and protected route layouts - Customer, supplier, fuel purchase, and delivery note management - Custom reporting and customer statement sending tools - Full UI component library and Tailwind styling - Server actions, API routes, and PocketBase integration - Development scripts and project configuration updates - Updated README and package metadata
128 lines
4.8 KiB
TypeScript
128 lines
4.8 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import PocketBase from "pocketbase";
|
|
import { Resend } from "resend";
|
|
import { pbCreate, pbList, pbUpdate } from "@/lib/pocketbase";
|
|
import { getAuth } from "@/lib/auth";
|
|
import { pbId } from "@/lib/pb";
|
|
import { PB_URL } from "@/lib/pocketbase";
|
|
|
|
export type ActionResult = { ok: true } | { ok: false; error: string };
|
|
|
|
const emailSettingsSchema = z.object({
|
|
statement_from_email: z.string().email("Valid email").or(z.literal("")).optional(),
|
|
statement_cc_email: z.string().email("Valid email").or(z.literal("")).optional(),
|
|
statement_app_email: z.string().email("Valid email").or(z.literal("")).optional(),
|
|
statement_email_password: z.string().optional(),
|
|
});
|
|
|
|
function clean(obj: any) {
|
|
const out: any = {};
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
if (v === "" || v === null || v === undefined) continue;
|
|
out[k] = v;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function updateEmailSettings(_: unknown, formData: FormData): Promise<ActionResult> {
|
|
const { token } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
const parsed = emailSettingsSchema.safeParse({
|
|
statement_from_email: formData.get("statement_from_email") || "",
|
|
statement_cc_email: formData.get("statement_cc_email") || "",
|
|
statement_app_email: formData.get("statement_app_email") || "",
|
|
statement_email_password: formData.get("statement_email_password") || "",
|
|
});
|
|
if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
|
|
try {
|
|
const settings = await pbList<any>("fuel_settings", { perPage: 1 }, token);
|
|
if (settings.items.length === 0) return { ok: false, error: "Settings not configured" };
|
|
await pbUpdate("fuel_settings", settings.items[0].id, clean(parsed.data), token);
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to update" };
|
|
}
|
|
revalidatePath("/settings");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function sendTestEmail(to: string): Promise<ActionResult> {
|
|
if (!to) return { ok: false, error: "Recipient email required" };
|
|
const apiKey = process.env.RESEND_API_KEY;
|
|
if (!apiKey) return { ok: false, error: "RESEND_API_KEY not configured" };
|
|
const fromAddr = process.env.RESEND_FROM || "statements@rjbulk.co.za";
|
|
try {
|
|
const resend = new Resend(apiKey);
|
|
const result = await resend.emails.send({
|
|
from: fromAddr,
|
|
to,
|
|
subject: "RJ Bulk test email",
|
|
html: "<p>This is a test email from your RJ Bulk back-office. If you got this, email is working correctly.</p>",
|
|
});
|
|
if (result.error) {
|
|
return { ok: false, error: result.error.message || "Send failed" };
|
|
}
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Send failed" };
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
const addUserSchema = z.object({
|
|
name: z.string().min(1, "Name is required"),
|
|
username: z.string().min(3, "Username min 3 chars"),
|
|
email: z.string().email("Valid email"),
|
|
password: z.string().min(8, "Password min 8 chars"),
|
|
});
|
|
|
|
export async function addUser(_: unknown, formData: FormData): Promise<ActionResult> {
|
|
const { token } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
const parsed = addUserSchema.safeParse({
|
|
name: formData.get("name"),
|
|
username: formData.get("username"),
|
|
email: formData.get("email"),
|
|
password: formData.get("password"),
|
|
});
|
|
if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
|
|
try {
|
|
// Use superuser auth for creating new auth users (PocketBase limitation)
|
|
const pb = new PocketBase(PB_URL);
|
|
await pb.admins.authWithPassword(
|
|
process.env.PB_ADMIN_EMAIL || "David.els1@outlook.com",
|
|
process.env.PB_ADMIN_PASS || "1234567890",
|
|
);
|
|
await pb.collection("users").create({
|
|
email: parsed.data.email,
|
|
username: parsed.data.username,
|
|
password: parsed.data.password,
|
|
passwordConfirm: parsed.data.password,
|
|
name: parsed.data.name,
|
|
emailVisibility: true,
|
|
verified: true,
|
|
});
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to create user" };
|
|
}
|
|
revalidatePath("/settings");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deleteUser(userId: string): Promise<ActionResult> {
|
|
const { token } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
try {
|
|
const pb = new PocketBase(PB_URL);
|
|
await pb.admins.authWithPassword(
|
|
process.env.PB_ADMIN_EMAIL || "David.els1@outlook.com",
|
|
process.env.PB_ADMIN_PASS || "1234567890",
|
|
);
|
|
await pb.collection("users").delete(userId);
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to delete" };
|
|
}
|
|
revalidatePath("/settings");
|
|
return { ok: true };
|
|
}
|