Split purchase submit from receipt finalization

This commit is contained in:
2026-05-26 12:39:20 +07:00
parent 76b37cd4c8
commit 44146a4a22
8 changed files with 301 additions and 336 deletions

View File

@ -130,12 +130,27 @@ export async function PUT(request: Request, context: RouteContext) {
const existing = await prisma.purchase.findUnique({ const existing = await prisma.purchase.findUnique({
where: { id: parsedId }, where: { id: parsedId },
include: { include: {
lines: true lines: true,
_count: {
select: {
receipts: true,
lots: true
}
}
} }
}); });
if (!existing) { if (!existing) {
return NextResponse.json({ message: "Purchase not found" }, { status: 404 }); return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
} }
if (existing._count.receipts > 0 || existing._count.lots > 0) {
return NextResponse.json(
{
message:
"Purchase sudah memiliki receipt/lot, sehingga baris pembelian tidak bisa diubah dari form purchase. Gunakan koreksi stok atau buat purchase baru untuk perubahan setelah barang diterima."
},
{ status: 409 }
);
}
const purchaseDate = toDateOrThrow(`${payload.purchase_date}T00:00:00.000Z`, "Tanggal pembelian"); const purchaseDate = toDateOrThrow(`${payload.purchase_date}T00:00:00.000Z`, "Tanggal pembelian");
const receivedAt = toDateOrThrow(payload.received_at, "Waktu diterima"); const receivedAt = toDateOrThrow(payload.received_at, "Waktu diterima");
@ -268,6 +283,15 @@ export async function PUT(request: Request, context: RouteContext) {
return NextResponse.json({ data: serializePurchaseDetail(purchase) }); return NextResponse.json({ data: serializePurchaseDetail(purchase) });
} catch (error) { } catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
return NextResponse.json(
{
message:
"Purchase masih terkait receipt/lot, sehingga baris pembelian tidak bisa diubah dari form purchase."
},
{ status: 409 }
);
}
if (error instanceof Prisma.PrismaClientKnownRequestError || error instanceof Prisma.PrismaClientValidationError) { if (error instanceof Prisma.PrismaClientKnownRequestError || error instanceof Prisma.PrismaClientValidationError) {
return NextResponse.json({ message: error.message }, { status: 400 }); return NextResponse.json({ message: error.message }, { status: 400 });
} }

View File

@ -1,20 +1,12 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { createAuditTrailSafe } from "@/lib/audit-trail"; import { createAuditTrailSafe } from "@/lib/audit-trail";
import { buildAuditChangeMetadata } from "@/lib/audit-trail-diff"; import { buildAuditChangeMetadata } from "@/lib/audit-trail-diff";
import { requireApiAccess } from "@/lib/authorization"; import { requireApiAccess } from "@/lib/authorization";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { recalculatePurchaseRealizationSummary } from "@/features/purchase-realization/lib/recalculate-purchase-realization-summary";
import { generateLotCode } from "@/features/receipts/lib/generate-lot-code";
import { generateReceiptNo } from "@/features/receipts/lib/generate-receipt-no";
type RouteContext = { params: Promise<{ id: string }> }; type RouteContext = { params: Promise<{ id: string }> };
type SubmitTx = Prisma.TransactionClient & {
lotPurchaseAllocation: typeof prisma.lotPurchaseAllocation;
purchaseRealizationEntry: typeof prisma.purchaseRealizationEntry;
purchaseRealizationSummary: typeof prisma.purchaseRealizationSummary;
};
const parseId = (id: string) => { const parseId = (id: string) => {
try { try {
return BigInt(id); return BigInt(id);
@ -30,271 +22,75 @@ export async function POST(request: Request, context: RouteContext) {
const parsedId = parseId((await context.params).id); const parsedId = parseId((await context.params).id);
if (parsedId === null) return NextResponse.json({ message: "Invalid id" }, { status: 400 }); if (parsedId === null) return NextResponse.json({ message: "Invalid id" }, { status: 400 });
try { const purchase = await prisma.purchase.findUnique({
const purchaseId = parsedId; where: { id: parsedId },
const purchase = await prisma.purchase.findUnique({ include: {
where: { id: purchaseId }, _count: {
include: { select: {
agent: { select: { id: true, name: true } }, receipts: true,
profitShareScheme: { select: { id: true, shareAgent: true } }, lots: true
receipts: {
select: {
id: true,
receiptNo: true
}
},
lines: {
include: {
grade: true,
unit: true,
warehouse: true,
warehouseLocation: true
}
} }
}
});
if (!purchase) return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
if (purchase.status === "SUBMITTED") {
return NextResponse.json({ message: "Purchase sudah disubmit" }, { status: 409 });
}
if (purchase.receipts.length > 0) {
return NextResponse.json(
{ message: `Purchase sudah memiliki receipt ${purchase.receipts[0]?.receiptNo ?? ""}`.trim() },
{ status: 409 }
);
}
if (!purchase.receivedByEmployeeId) {
return NextResponse.json({ message: "Penerima belum dipilih" }, { status: 400 });
}
if (!purchase.receivedAt) {
return NextResponse.json({ message: "Waktu diterima belum diisi" }, { status: 400 });
}
const receivedAt = purchase.receivedAt;
const receiptDate = new Date(receivedAt.toISOString().slice(0, 10));
const receiptNo = await generateReceiptNo(receiptDate);
const sourceCode = purchase.agent?.name || purchase.purchaseNo;
const enteredQtyLines = purchase.lines.filter((line) => Number(line.qtyAccepted.toNumber()) > 0);
if (enteredQtyLines.length === 0) {
return NextResponse.json(
{ message: "Tidak ada baris dengan qty yang masuk > 0 untuk generate lot" },
{ status: 400 }
);
}
const missingWarehouseLine = enteredQtyLines.find((line) => !line.warehouseId);
if (missingWarehouseLine) {
return NextResponse.json(
{ message: "Warehouse belum diisi pada salah satu baris pembelian" },
{ status: 400 }
);
}
const generated = await prisma.$transaction(async (rawTx) => {
const tx = rawTx as SubmitTx;
const baseLotCode = await generateLotCode(receivedAt, sourceCode);
const codeMatch = baseLotCode.match(/-(\d+)$/);
if (!codeMatch) {
throw new Error("Gagal generate kode lot");
}
const lotPrefix = baseLotCode.replace(/-\d+$/, "");
let lotSuffix = Number(codeMatch[1] ?? "0") || 1;
const availableLotCount = await tx.inventoryLot.count({ where: { purchaseId } });
if (availableLotCount > 0) {
throw new Error("Lot untuk purchase ini sudah pernah digenerate");
}
const existingReceiptCount = await tx.receipt.count({ where: { purchaseId } });
if (existingReceiptCount > 0) {
throw new Error("Receipt untuk purchase ini sudah pernah dibuat");
}
const receipt = await tx.receipt.create({
data: {
receiptNo,
purchaseId: purchase.id,
receiptDate,
status: "FINALIZED",
notes: purchase.notes || null,
receivedById: BigInt(auth.user.id)
}
});
await tx.receiptLine.createMany({
data: enteredQtyLines.map((line) => ({
receiptId: receipt.id,
purchaseLineId: line.id,
gradeId: line.gradeId,
qtyReceived: line.qtyReceived,
qtyAccepted: line.qtyAccepted,
qtyRejected: line.qtyRejected,
unitId: line.unitId,
unitCost: line.unitCost,
warehouseId: line.warehouseId!,
warehouseLocationId: line.warehouseLocationId,
notes: line.notes || null
}))
});
const receiptLines = await tx.receiptLine.findMany({
where: { receiptId: receipt.id },
orderBy: { id: "asc" }
});
const lots = [];
for (const line of receiptLines) {
if (!line.warehouseId) {
throw new Error("Warehouse belum diisi pada salah satu baris pembelian");
}
const availableQty = Number(line.qtyAccepted.toNumber());
const lotCode = `${lotPrefix}-${String(lotSuffix).padStart(3, "0")}`;
lotSuffix += 1;
const lot = await tx.inventoryLot.create({
data: {
lotCode,
sourceType: "PURCHASE",
sourceRefId: purchaseId,
purchaseId: purchase.id,
purchaseLineId: line.purchaseLineId,
receiptId: receipt.id,
receiptLineId: line.id,
gradeId: line.gradeId,
warehouseId: line.warehouseId,
warehouseLocationId: line.warehouseLocationId,
originalQty: availableQty,
availableQty,
unitId: line.unitId,
unitCost: line.unitCost,
receivedAt,
status: "ACTIVE",
qrCodeValue: lotCode,
barcodeValue: lotCode
}
});
const costTotalAllocated = Number(
(availableQty * line.unitCost.toNumber()).toFixed(2)
);
const allocation = await tx.lotPurchaseAllocation.create({
data: {
lotId: lot.id,
purchaseId: purchase.id,
purchaseLineId: line.id,
sourceType: "PURCHASE",
sourceRefId: purchase.id,
agentIdSnapshot: purchase.agentId,
profitShareSchemeIdSnapshot: purchase.profitShareSchemeId,
qtyAllocated: new Prisma.Decimal(availableQty),
costTotalAllocated: new Prisma.Decimal(costTotalAllocated),
unitCostSnapshot: line.unitCost
}
});
await tx.purchaseRealizationEntry.create({
data: {
purchaseId: purchase.id,
lotId: lot.id,
allocationId: allocation.id,
eventType: "OPENING_COST",
referenceType: "PURCHASE",
referenceId: purchase.id,
occurredAt: receivedAt,
qtyIn: new Prisma.Decimal(availableQty),
qtyOut: new Prisma.Decimal(0),
qtyShrinkage: new Prisma.Decimal(0),
amountCost: new Prisma.Decimal(costTotalAllocated),
amountRevenue: new Prisma.Decimal(0),
amountExpense: new Prisma.Decimal(0),
amountProfit: new Prisma.Decimal(0),
agentSharePercentSnapshot: purchase.profitShareScheme?.shareAgent ?? null,
agentAmount: new Prisma.Decimal(0),
notes: `Opening realization dari purchase line ${line.purchaseLineId.toString()}`
}
});
lots.push({
id: lot.id.toString(),
lot_code: lot.lotCode
});
}
const updated = await tx.purchase.update({
where: { id: purchaseId },
data: { status: "SUBMITTED" }
});
await recalculatePurchaseRealizationSummary(
tx,
purchase.id,
purchase.profitShareScheme?.shareAgent.toNumber() ?? null
);
return {
purchase: updated,
receipt: {
id: receipt.id.toString(),
receipt_no: receipt.receiptNo
},
lots
};
});
await createAuditTrailSafe({
userId: auth.user.id,
action: "PURCHASE_SUBMITTED",
entityType: "PURCHASE",
entityId: generated.purchase.id,
method: request.method,
pathname: new URL(request.url).pathname,
statusCode: 200,
summary: `Purchase ${purchase.purchaseNo} disubmit + receipt + lot dibuat`,
metadata: buildAuditChangeMetadata(
{ status: purchase.status, lot_count: 0, receipt_no: null },
{
status: generated.purchase.status,
lot_count: generated.lots.length,
receipt_no: generated.receipt.receipt_no
}
)
});
if (generated.lots.length === 0) {
return NextResponse.json({ message: "Lot untuk purchase ini sudah pernah digenerate" }, { status: 409 });
}
return NextResponse.json(
{
success: true,
status: generated.purchase.status,
lot_count: generated.lots.length,
receipt: generated.receipt
}, },
{ status: 200 } lines: true
}
});
if (!purchase) return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
if (purchase._count.receipts > 0 || purchase._count.lots > 0) {
return NextResponse.json(
{ message: "Purchase sudah diterima dan memiliki receipt/lot." },
{ status: 409 }
); );
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
return NextResponse.json(
{ message: "Kode lot duplikat saat submit. Coba lagi." },
{ status: 409 }
);
}
if (error instanceof Error) {
console.error("PURCHASE_SUBMIT_ERROR", {
purchaseId: parsedId?.toString(),
message: error.message,
stack: error.stack
});
return NextResponse.json({ message: error.message }, { status: 400 });
}
console.error("PURCHASE_SUBMIT_ERROR", {
purchaseId: parsedId?.toString(),
message: "Unknown error"
});
return NextResponse.json({ message: "Submit gagal karena kesalahan internal" }, { status: 500 });
} }
if (!purchase.receivedByEmployeeId) {
return NextResponse.json({ message: "Penerima belum dipilih" }, { status: 400 });
}
if (!purchase.receivedAt) {
return NextResponse.json({ message: "Waktu diterima belum diisi" }, { status: 400 });
}
const enteredQtyLines = purchase.lines.filter((line) => Number(line.qtyAccepted.toNumber()) > 0);
if (enteredQtyLines.length === 0) {
return NextResponse.json(
{ message: "Tidak ada baris dengan qty yang masuk > 0 untuk diajukan" },
{ status: 400 }
);
}
if (purchase.status === "SUBMITTED") {
return NextResponse.json({
success: true,
status: purchase.status,
message: "Purchase sudah diajukan dan menunggu penerimaan."
});
}
const updated = await prisma.purchase.update({
where: { id: parsedId },
data: { status: "SUBMITTED" }
});
await createAuditTrailSafe({
userId: auth.user.id,
action: "PURCHASE_SUBMITTED",
entityType: "PURCHASE",
entityId: updated.id,
method: request.method,
pathname: new URL(request.url).pathname,
statusCode: 200,
summary: `Purchase ${updated.purchaseNo} diajukan untuk penerimaan`,
metadata: buildAuditChangeMetadata(
{ status: purchase.status },
{ status: updated.status }
)
});
return NextResponse.json({
success: true,
status: updated.status,
message: "Purchase diajukan. Buat receipt dan lot dari menu Penerimaan."
});
} }

View File

@ -84,6 +84,12 @@ export async function GET(request: Request) {
select: { select: {
subtotal: true subtotal: true
} }
},
_count: {
select: {
receipts: true,
lots: true
}
} }
}, },
orderBy: [{ createdAt: "desc" }] orderBy: [{ createdAt: "desc" }]

View File

@ -1,11 +1,20 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { recalculatePurchaseRealizationSummary } from "@/features/purchase-realization/lib/recalculate-purchase-realization-summary";
import { generateLotCode } from "@/features/receipts/lib/generate-lot-code"; import { generateLotCode } from "@/features/receipts/lib/generate-lot-code";
import { createAuditTrailSafe } from "@/lib/audit-trail"; import { createAuditTrailSafe } from "@/lib/audit-trail";
import { requireApiAccess } from "@/lib/authorization"; import { requireApiAccess } from "@/lib/authorization";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
type RouteContext = { params: Promise<{ id: string }> }; type RouteContext = { params: Promise<{ id: string }> };
type GenerateLotsTx = Prisma.TransactionClient & {
lotPurchaseAllocation: typeof prisma.lotPurchaseAllocation;
purchaseRealizationEntry: typeof prisma.purchaseRealizationEntry;
purchaseRealizationSummary: typeof prisma.purchaseRealizationSummary;
};
const parseId = (id: string) => { const parseId = (id: string) => {
try { try {
return BigInt(id); return BigInt(id);
@ -26,7 +35,12 @@ export async function POST(request: Request, context: RouteContext) {
const existingReceipt = await prisma.receipt.findUnique({ const existingReceipt = await prisma.receipt.findUnique({
where: { id: parsedId }, where: { id: parsedId },
include: { include: {
purchase: true, purchase: {
include: {
agent: { select: { id: true, name: true } },
profitShareScheme: { select: { id: true, shareAgent: true } }
}
},
lines: true, lines: true,
lots: true lots: true
} }
@ -43,72 +57,156 @@ export async function POST(request: Request, context: RouteContext) {
); );
} }
const generated = await prisma.$transaction(async (tx) => { const acceptedLines = existingReceipt.lines.filter((line) => Number(line.qtyAccepted.toNumber()) > 0);
const lots = []; if (acceptedLines.length === 0) {
for (const line of existingReceipt.lines) { return NextResponse.json(
if (Number(line.qtyAccepted) <= 0) { { message: "Tidak ada baris receipt dengan qty accepted > 0." },
continue; { status: 400 }
);
}
try {
const generated = await prisma.$transaction(async (rawTx) => {
const tx = rawTx as GenerateLotsTx;
const sourceCode = existingReceipt.purchase.agent?.name || existingReceipt.purchase.purchaseNo;
const baseLotCode = await generateLotCode(existingReceipt.receiptDate, sourceCode);
const codeMatch = baseLotCode.match(/-(\d+)$/);
if (!codeMatch) {
throw new Error("Gagal generate kode lot");
}
const lotPrefix = baseLotCode.replace(/-\d+$/, "");
let lotSuffix = Number(codeMatch[1] ?? "0") || 1;
const existingLotCount = await tx.inventoryLot.count({
where: { receiptId: existingReceipt.id }
});
if (existingLotCount > 0) {
throw new Error("Lot untuk receipt ini sudah pernah dibuat");
} }
const lotCode = await generateLotCode( const lots = [];
existingReceipt.receiptDate, for (const line of acceptedLines) {
existingReceipt.purchaseId.toString() const availableQty = Number(line.qtyAccepted.toNumber());
const lotCode = `${lotPrefix}-${String(lotSuffix).padStart(3, "0")}`;
lotSuffix += 1;
const lot = await tx.inventoryLot.create({
data: {
lotCode,
sourceType: "RECEIPT",
sourceRefId: line.id,
purchaseId: existingReceipt.purchaseId,
purchaseLineId: line.purchaseLineId,
receiptId: existingReceipt.id,
receiptLineId: line.id,
gradeId: line.gradeId,
warehouseId: line.warehouseId,
warehouseLocationId: line.warehouseLocationId,
originalQty: line.qtyAccepted,
availableQty: line.qtyAccepted,
unitId: line.unitId,
unitCost: line.unitCost,
receivedAt: existingReceipt.receiptDate,
status: "ACTIVE",
qrCodeValue: lotCode,
barcodeValue: lotCode
}
});
const costTotalAllocated = Number((availableQty * line.unitCost.toNumber()).toFixed(2));
const allocation = await tx.lotPurchaseAllocation.create({
data: {
lotId: lot.id,
purchaseId: existingReceipt.purchaseId,
purchaseLineId: line.purchaseLineId,
sourceType: "RECEIPT",
sourceRefId: existingReceipt.id,
agentIdSnapshot: existingReceipt.purchase.agentId,
profitShareSchemeIdSnapshot: existingReceipt.purchase.profitShareSchemeId,
qtyAllocated: new Prisma.Decimal(availableQty),
costTotalAllocated: new Prisma.Decimal(costTotalAllocated),
unitCostSnapshot: line.unitCost
}
});
await tx.purchaseRealizationEntry.create({
data: {
purchaseId: existingReceipt.purchaseId,
lotId: lot.id,
allocationId: allocation.id,
eventType: "OPENING_COST",
referenceType: "RECEIPT",
referenceId: existingReceipt.id,
occurredAt: existingReceipt.receiptDate,
qtyIn: new Prisma.Decimal(availableQty),
qtyOut: new Prisma.Decimal(0),
qtyShrinkage: new Prisma.Decimal(0),
amountCost: new Prisma.Decimal(costTotalAllocated),
amountRevenue: new Prisma.Decimal(0),
amountExpense: new Prisma.Decimal(0),
amountProfit: new Prisma.Decimal(0),
agentSharePercentSnapshot: existingReceipt.purchase.profitShareScheme?.shareAgent ?? null,
agentAmount: new Prisma.Decimal(0),
notes: `Opening realization dari receipt line ${line.id.toString()}`
}
});
lots.push(lot);
}
await tx.receipt.update({
where: { id: existingReceipt.id },
data: { status: "FINALIZED" }
});
await tx.purchase.update({
where: { id: existingReceipt.purchaseId },
data: { status: "SUBMITTED" }
});
await recalculatePurchaseRealizationSummary(
tx,
existingReceipt.purchaseId,
existingReceipt.purchase.profitShareScheme?.shareAgent.toNumber() ?? null
); );
const lot = await tx.inventoryLot.create({ return lots;
data: {
lotCode,
sourceType: "RECEIPT",
sourceRefId: line.id,
purchaseId: existingReceipt.purchaseId,
purchaseLineId: line.purchaseLineId,
receiptId: existingReceipt.id,
receiptLineId: line.id,
gradeId: line.gradeId,
warehouseId: line.warehouseId,
warehouseLocationId: line.warehouseLocationId,
originalQty: line.qtyAccepted,
availableQty: line.qtyAccepted,
unitId: line.unitId,
unitCost: line.unitCost,
receivedAt: existingReceipt.receiptDate,
status: "ACTIVE",
qrCodeValue: lotCode,
barcodeValue: lotCode
}
});
lots.push(lot);
}
await tx.receipt.update({
where: { id: existingReceipt.id },
data: { status: "FINALIZED" }
}); });
return lots; await createAuditTrailSafe({
}); userId: auth.user.id,
action: "LOTS_GENERATED",
entityType: "RECEIPT",
entityId: existingReceipt.id,
method: request.method,
pathname: new URL(request.url).pathname,
statusCode: 200,
summary: `${generated.length} lot dibuat dari receipt ${existingReceipt.receiptNo}`,
metadata: {
receipt_no: existingReceipt.receiptNo,
purchase_no: existingReceipt.purchase.purchaseNo,
lot_codes: generated.map((lot) => lot.lotCode)
}
});
await createAuditTrailSafe({ return NextResponse.json({
userId: auth.user.id, receipt_id: existingReceipt.id.toString(),
action: "LOTS_GENERATED", lots: generated.map((lot) => ({
entityType: "RECEIPT", id: lot.id.toString(),
entityId: existingReceipt.id, lot_code: lot.lotCode,
method: request.method, qr_code_value: lot.qrCodeValue
pathname: new URL(request.url).pathname, }))
statusCode: 200, });
summary: `${generated.length} lot dibuat dari receipt ${existingReceipt.receiptNo}`, } catch (error) {
metadata: { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
receipt_no: existingReceipt.receiptNo, return NextResponse.json(
lot_codes: generated.map((lot) => lot.lotCode) { message: "Kode lot duplikat saat membuat lot. Coba lagi." },
{ status: 409 }
);
} }
}); if (error instanceof Error) {
return NextResponse.json({ message: error.message }, { status: 400 });
return NextResponse.json({ }
receipt_id: existingReceipt.id.toString(), throw error;
lots: generated.map((lot) => ({ }
id: lot.id.toString(),
lot_code: lot.lotCode,
qr_code_value: lot.qrCodeValue
}))
});
} }

View File

@ -55,11 +55,42 @@ export async function POST(request: Request) {
const systemUser = await ensureSystemUser(); const systemUser = await ensureSystemUser();
const receiptDate = new Date(`${parsed.data.receipt_date}T00:00:00.000Z`); const receiptDate = new Date(`${parsed.data.receipt_date}T00:00:00.000Z`);
const receiptNo = await generateReceiptNo(receiptDate); const receiptNo = await generateReceiptNo(receiptDate);
const purchaseId = BigInt(parsed.data.purchase_id);
const purchase = await prisma.purchase.findUnique({
where: { id: purchaseId },
include: {
_count: {
select: {
receipts: true,
lots: true
}
}
}
});
if (!purchase) {
return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
}
if (purchase.status !== "SUBMITTED") {
return NextResponse.json(
{ message: "Purchase harus diajukan sebelum dibuatkan receipt." },
{ status: 409 }
);
}
if (purchase._count.receipts > 0 || purchase._count.lots > 0) {
return NextResponse.json(
{ message: "Purchase sudah memiliki receipt/lot." },
{ status: 409 }
);
}
const receipt = await prisma.receipt.create({ const receipt = await prisma.receipt.create({
data: { data: {
receiptNo, receiptNo,
purchaseId: BigInt(parsed.data.purchase_id), purchaseId,
receiptDate, receiptDate,
status: "DRAFT", status: "DRAFT",
notes: parsed.data.notes || null, notes: parsed.data.notes || null,

View File

@ -1348,7 +1348,9 @@ export function PurchasesClient({ currencyCode }: PurchasesClientProps) {
<td> <td>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="button" onClick={() => void openDetail(item.id)} className="ops-btn-secondary">{dict.common.detail}</button> <button type="button" onClick={() => void openDetail(item.id)} className="ops-btn-secondary">{dict.common.detail}</button>
<button type="button" onClick={() => void startEdit(item.id)} className="ops-btn-secondary">{dict.common.edit}</button> {item.receipt_count === 0 && item.lot_count === 0 ? (
<button type="button" onClick={() => void startEdit(item.id)} className="ops-btn-secondary">{dict.common.edit}</button>
) : null}
{(item.status === "DRAFT" || item.status === "CANCELLED") ? ( {(item.status === "DRAFT" || item.status === "CANCELLED") ? (
<button type="button" onClick={() => void handleDeleteDraftPurchase(item.id)} className="ops-btn-danger"> <button type="button" onClick={() => void handleDeleteDraftPurchase(item.id)} className="ops-btn-danger">
{dict.common.delete} {dict.common.delete}

View File

@ -10,6 +10,10 @@ type PurchaseListSerializable = {
receivedAt: Date | null; receivedAt: Date | null;
agent: { id: bigint; name: string } | null; agent: { id: bigint; name: string } | null;
lines: Array<{ subtotal: DecimalLike }>; lines: Array<{ subtotal: DecimalLike }>;
_count?: {
receipts: number;
lots: number;
};
}; };
type PurchaseDetailSerializable = { type PurchaseDetailSerializable = {
@ -93,6 +97,8 @@ export function serializePurchaseListItem(purchase: PurchaseListSerializable): P
: null, : null,
status: purchase.status, status: purchase.status,
line_count: purchase.lines.length, line_count: purchase.lines.length,
receipt_count: purchase._count?.receipts ?? 0,
lot_count: purchase._count?.lots ?? 0,
grand_total: purchase.lines.reduce((sum, line) => sum + line.subtotal.toNumber(), 0) grand_total: purchase.lines.reduce((sum, line) => sum + line.subtotal.toNumber(), 0)
}; };
} }

View File

@ -9,6 +9,8 @@ export type PurchaseListItem = {
} | null; } | null;
status: string; status: string;
line_count: number; line_count: number;
receipt_count: number;
lot_count: number;
grand_total: number; grand_total: number;
}; };