Files
Order-Loop/app/actions/orders.ts
2026-06-02 10:23:09 +03:00

376 lines
11 KiB
TypeScript

'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<Parameters<typeof prisma.$transaction>[0]>[0],
): Promise<string> {
// 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<FormState> {
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<string, string[]> = {};
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<string, string> = {};
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<FormState> {
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<string, string[]> = {};
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<string, string> = {};
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<OrderWithDetails[]> {
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<OrderWithDetails | null> {
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<DashboardStats> {
await requireCurrentUser();
const groups = await prisma.order.groupBy({
by: ["status"],
_count: { id: true },
});
const counts: Record<string, number> = {};
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),
};
}