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
157 lines
6.1 KiB
TypeScript
157 lines
6.1 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import { pbCreate, pbDelete, pbList, pbUpdate } from "@/lib/pocketbase";
|
|
import { getAuth } from "@/lib/auth";
|
|
import { pbId } from "@/lib/pb";
|
|
|
|
export type ActionResult = { ok: true } | { ok: false; error: string };
|
|
|
|
const ruleSchema = z.object({
|
|
customer: z.string().min(1, "Customer is required"),
|
|
year: z.coerce.number().int().min(2000).max(2100),
|
|
month: z.coerce.number().int().min(1).max(12),
|
|
price_per_litre: z.coerce.number().positive("Price must be > 0"),
|
|
special_price: z.coerce.number().optional(),
|
|
});
|
|
|
|
export async function createPricingRule(_: unknown, formData: FormData): Promise<ActionResult> {
|
|
const { token, user } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
const parsed = ruleSchema.safeParse({
|
|
customer: formData.get("customer"),
|
|
year: formData.get("year"),
|
|
month: formData.get("month"),
|
|
price_per_litre: formData.get("price_per_litre"),
|
|
special_price: formData.get("special_price") || undefined,
|
|
});
|
|
if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
|
|
try {
|
|
await pbCreate(
|
|
"customer_pricing",
|
|
{
|
|
id: pbId(),
|
|
...parsed.data,
|
|
...(user ? { created_by: user.id } : {}),
|
|
},
|
|
token,
|
|
);
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to create rule" };
|
|
}
|
|
revalidatePath("/pricing");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function updatePricingRule(id: string, _: unknown, formData: FormData): Promise<ActionResult> {
|
|
const { token } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
const parsed = ruleSchema.safeParse({
|
|
customer: formData.get("customer"),
|
|
year: formData.get("year"),
|
|
month: formData.get("month"),
|
|
price_per_litre: formData.get("price_per_litre"),
|
|
special_price: formData.get("special_price") || undefined,
|
|
});
|
|
if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
|
|
try {
|
|
await pbUpdate("customer_pricing", id, parsed.data, token);
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to update rule" };
|
|
}
|
|
revalidatePath("/pricing");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deletePricingRule(id: string): Promise<ActionResult> {
|
|
const { token } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
try {
|
|
await pbDelete("customer_pricing", id, token);
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to delete" };
|
|
}
|
|
revalidatePath("/pricing");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function updateDefaultPrice(price: number): Promise<ActionResult> {
|
|
const { token } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
if (isNaN(price) || price <= 0) return { ok: false, error: "Price must be > 0" };
|
|
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, { default_price_per_litre: price }, token);
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to update" };
|
|
}
|
|
revalidatePath("/pricing");
|
|
revalidatePath("/settings");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function applyMonthlyIncrease(year: number, month: number, deltaCents: number): Promise<ActionResult> {
|
|
const { token, user } = await getAuth();
|
|
if (!token) return { ok: false, error: "Not signed in" };
|
|
if (isNaN(deltaCents) || deltaCents === 0) return { ok: false, error: "Delta must be non-zero" };
|
|
|
|
try {
|
|
// 1. Log the increase
|
|
const existing = await pbList<any>("monthly_price_increases", { perPage: 1, filter: `year = ${year} && month = ${month}` }, token);
|
|
if (existing.items.length > 0) {
|
|
await pbUpdate("monthly_price_increases", existing.items[0].id, { percentage_increase: deltaCents, applied_by: user?.id }, token);
|
|
} else {
|
|
await pbCreate("monthly_price_increases", { id: pbId(), year, month, percentage_increase: deltaCents, applied_by: user?.id }, token);
|
|
}
|
|
|
|
// 2. Get all active customers
|
|
const customers = (await pbList<any>("customers", { perPage: 500 }, token)).items;
|
|
|
|
// 3. For each customer, create a new pricing rule with the increased price (if no special price exists)
|
|
for (const c of customers) {
|
|
// Check existing rule for this month
|
|
const filter = `customer = "${c.id}" && year = ${year} && month = ${month}`;
|
|
const existingRule = await pbList<any>("customer_pricing", { perPage: 1, filter }, token);
|
|
// Get the previous month's price
|
|
const prev = new Date(year, month - 2, 1);
|
|
const prevFilter = `customer = "${c.id}" && year = ${prev.getFullYear()} && month = ${prev.getMonth() + 1}`;
|
|
const prevRule = await pbList<any>("customer_pricing", { perPage: 1, filter: prevFilter, sort: "-id" }, token);
|
|
|
|
let basePrice: number;
|
|
if (prevRule.items.length > 0) {
|
|
basePrice = Number(prevRule.items[0].special_price ?? prevRule.items[0].price_per_litre);
|
|
} else {
|
|
// Use default price
|
|
const settings = await pbList<any>("fuel_settings", { perPage: 1 }, token);
|
|
basePrice = Number(settings.items[0]?.default_price_per_litre || 0);
|
|
}
|
|
|
|
// Apply cents increase (not percentage — field is misnamed)
|
|
const newPrice = Number((basePrice + deltaCents / 100).toFixed(2));
|
|
|
|
if (existingRule.items.length > 0) {
|
|
// Update the rule (keep special_price if it existed)
|
|
const row = existingRule.items[0];
|
|
await pbUpdate("customer_pricing", row.id, { price_per_litre: newPrice }, token);
|
|
} else {
|
|
// Create new rule
|
|
await pbCreate("customer_pricing", {
|
|
id: pbId(),
|
|
customer: c.id,
|
|
year,
|
|
month,
|
|
price_per_litre: newPrice,
|
|
created_by: user?.id,
|
|
}, token);
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
return { ok: false, error: err?.message || "Failed to apply increase" };
|
|
}
|
|
revalidatePath("/pricing");
|
|
return { ok: true };
|
|
}
|