'use server' import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { prisma } from "@/lib/prisma"; import { requireCurrentUser } from "@/lib/auth"; import { Prisma } from "@prisma/client"; import type { OrderWithDetails, DashboardStats, FormState, OrderStatus, } from "@/lib/types"; // --------------------------------------------------------------------------- // Types for parsed JSON payloads from hidden form inputs // --------------------------------------------------------------------------- interface ParsedCustomer { id: string; name: string; } interface ParsedItem { id: string; customerId: string | null; itemName: string; initPrice: number; myPrice: number; taxRatio: number; quantity: number; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function computePrices(item: ParsedItem) { const netPrice = (item.initPrice + item.initPrice * item.taxRatio) * item.quantity; const myNetPrice = (item.myPrice + item.myPrice * item.taxRatio) * item.quantity; const finalPrice = netPrice; return { netPrice, myNetPrice, finalPrice }; } /** * Resolve a parsed customer to a database Customer id. * First tries to find an existing customer by id, then by name. * If neither exists, creates a new Customer record. */ async function resolveCustomer( customer: ParsedCustomer, tx: Parameters[0]>[0], ): Promise { // Try by id first const existing = await tx.customer.findUnique({ where: { id: customer.id }, }); if (existing) return existing.id; // Try by name (prevents duplicates from re-adding) const byName = await tx.customer.findUnique({ where: { name: customer.name }, }); if (byName) return byName.id; // Create new const created = await tx.customer.create({ data: { name: customer.name }, }); return created.id; } // --------------------------------------------------------------------------- // createOrder // --------------------------------------------------------------------------- export async function createOrder( prevState: FormState | undefined, formData: FormData, ): Promise { await requireCurrentUser(); const title = (formData.get("title") as string) ?? ""; const status = ((formData.get("status") as string) ?? "pending") as OrderStatus; const notes = (formData.get("notes") as string) || null; const customersRaw = formData.get("customers") as string | null; const itemsRaw = formData.get("items") as string | null; const customers: ParsedCustomer[] = customersRaw ? JSON.parse(customersRaw) : []; const items: ParsedItem[] = itemsRaw ? JSON.parse(itemsRaw) : []; // Validation const errors: Record = {}; if (!title.trim()) { errors.title = ["Title is required"]; } if (items.length === 0) { errors.items = ["At least one item is required"]; } if (Object.keys(errors).length > 0) { return { errors }; } // Transaction const order = await prisma.$transaction(async (tx) => { const created = await tx.order.create({ data: { title: title.trim(), status, notes }, }); // Resolve customer IDs (maps client-side "new-xxx" IDs to real DB IDs) const customerIdMap: Record = {}; for (const c of customers) { const dbId = await resolveCustomer(c, tx); customerIdMap[c.id] = dbId; await tx.orderCustomer.create({ data: { orderId: created.id, customerId: dbId }, }); } for (const item of items) { const { netPrice, myNetPrice, finalPrice } = computePrices(item); // Resolve item's customerId through the map (handles "new-xxx" references) const resolvedCustomerId = item.customerId ? (customerIdMap[item.customerId] ?? item.customerId) : null; await tx.orderItem.create({ data: { orderId: created.id, customerId: resolvedCustomerId, itemName: item.itemName, initPrice: item.initPrice, myPrice: item.myPrice, taxRatio: item.taxRatio, quantity: item.quantity, netPrice, myNetPrice, finalPrice, }, }); } return created; }); revalidatePath("/"); redirect(`/orders/${order.id}`); } // --------------------------------------------------------------------------- // updateOrder // --------------------------------------------------------------------------- export async function updateOrder( orderId: string, prevState: FormState | undefined, formData: FormData, ): Promise { await requireCurrentUser(); // Fetch existing order and verify it is not closed const existing = await prisma.order.findUnique({ where: { id: orderId } }); if (!existing) { return { message: "Order not found" }; } if (existing.status === "closed") { return { message: "Cannot edit a closed order" }; } const title = (formData.get("title") as string) ?? ""; const status = ((formData.get("status") as string) ?? existing.status) as OrderStatus; const notes = (formData.get("notes") as string) || null; const customersRaw = formData.get("customers") as string | null; const itemsRaw = formData.get("items") as string | null; const customers: ParsedCustomer[] = customersRaw ? JSON.parse(customersRaw) : []; const items: ParsedItem[] = itemsRaw ? JSON.parse(itemsRaw) : []; // Validation const errors: Record = {}; if (!title.trim()) { errors.title = ["Title is required"]; } if (items.length === 0) { errors.items = ["At least one item is required"]; } if (Object.keys(errors).length > 0) { return { errors }; } // Transaction: delete old relations, update order, recreate relations await prisma.$transaction(async (tx) => { await tx.orderCustomer.deleteMany({ where: { orderId } }); await tx.orderItem.deleteMany({ where: { orderId } }); await tx.order.update({ where: { id: orderId }, data: { title: title.trim(), status, notes }, }); // Resolve customer IDs (maps client-side "new-xxx" IDs to real DB IDs) const customerIdMap: Record = {}; for (const c of customers) { const dbId = await resolveCustomer(c, tx); customerIdMap[c.id] = dbId; await tx.orderCustomer.create({ data: { orderId, customerId: dbId }, }); } for (const item of items) { const { netPrice, myNetPrice, finalPrice } = computePrices(item); // Resolve item's customerId through the map (handles "new-xxx" references) const resolvedCustomerId = item.customerId ? (customerIdMap[item.customerId] ?? item.customerId) : null; await tx.orderItem.create({ data: { orderId, customerId: resolvedCustomerId, itemName: item.itemName, initPrice: item.initPrice, myPrice: item.myPrice, taxRatio: item.taxRatio, quantity: item.quantity, netPrice, myNetPrice, finalPrice, }, }); } }); revalidatePath("/"); revalidatePath(`/orders/${orderId}`); return { message: "Order saved successfully", success: true }; } // --------------------------------------------------------------------------- // getOrders // --------------------------------------------------------------------------- export async function getOrders( search?: string, statusFilter?: string, ): Promise { await requireCurrentUser(); const where: Prisma.OrderWhereInput = {}; if (search) { where.OR = [ { title: { contains: search } }, { customers: { some: { customer: { name: { contains: search } } } } }, ]; } if (statusFilter) { where.status = statusFilter; } const orders = await prisma.order.findMany({ where, include: { customers: { include: { customer: true } }, items: { include: { customer: true } }, }, orderBy: { updatedAt: "desc" }, }); return orders.map((order) => ({ id: order.id, title: order.title, status: order.status as OrderStatus, notes: order.notes, createdAt: order.createdAt, updatedAt: order.updatedAt, customers: order.customers.map((oc) => ({ customer: { id: oc.customer.id, name: oc.customer.name }, })), items: order.items.map((item) => ({ id: item.id, orderId: item.orderId, customerId: item.customerId, customerName: item.customer?.name ?? null, itemName: item.itemName, initPrice: item.initPrice, myPrice: item.myPrice, taxRatio: item.taxRatio, quantity: item.quantity, netPrice: item.netPrice, myNetPrice: item.myNetPrice, finalPrice: item.finalPrice, })), })); } // --------------------------------------------------------------------------- // getOrderById // --------------------------------------------------------------------------- export async function getOrderById( orderId: string, ): Promise { await requireCurrentUser(); const order = await prisma.order.findUnique({ where: { id: orderId }, include: { customers: { include: { customer: true } }, items: { include: { customer: true } }, }, }); if (!order) return null; return { id: order.id, title: order.title, status: order.status as OrderStatus, notes: order.notes, createdAt: order.createdAt, updatedAt: order.updatedAt, customers: order.customers.map((oc) => ({ customer: { id: oc.customer.id, name: oc.customer.name }, })), items: order.items.map((item) => ({ id: item.id, orderId: item.orderId, customerId: item.customerId, customerName: item.customer?.name ?? null, itemName: item.itemName, initPrice: item.initPrice, myPrice: item.myPrice, taxRatio: item.taxRatio, quantity: item.quantity, netPrice: item.netPrice, myNetPrice: item.myNetPrice, finalPrice: item.finalPrice, })), }; } // --------------------------------------------------------------------------- // getDashboardStats // --------------------------------------------------------------------------- export async function getDashboardStats(): Promise { await requireCurrentUser(); const groups = await prisma.order.groupBy({ by: ["status"], _count: { id: true }, }); const counts: Record = {}; for (const g of groups) { counts[g.status] = g._count.id; } return { pending: counts["pending"] ?? 0, purchased: counts["purchased"] ?? 0, delivered: counts["delivered"] ?? 0, closed: counts["closed"] ?? 0, totalOrders: Object.values(counts).reduce((sum, n) => sum + n, 0), }; }