init app
This commit is contained in:
375
app/actions/orders.ts
Normal file
375
app/actions/orders.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
'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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user