Update mobile purchase flow and locale formatting

This commit is contained in:
2026-05-20 15:49:21 +07:00
parent 8e8912ed5b
commit a9202c390b
18 changed files with 496 additions and 523 deletions

View File

@ -11,7 +11,6 @@ export async function GET(request: Request) {
OWNER: [
"dashboard",
"lots",
"receipts",
"washing",
"stock_adjustments",
"lot_transformations",
@ -33,7 +32,6 @@ export async function GET(request: Request) {
WAREHOUSE: [
"dashboard",
"lots",
"receipts",
"washing",
"stock_adjustments"
],
@ -54,7 +52,6 @@ export async function GET(request: Request) {
ADMIN: [
"dashboard",
"lots",
"receipts",
"washing",
"stock_adjustments",
"lot_transformations",
@ -69,7 +66,6 @@ export async function GET(request: Request) {
SYSTEM_ADMIN: [
"dashboard",
"lots",
"receipts",
"washing",
"stock_adjustments",
"lot_transformations",

View File

@ -7,6 +7,7 @@ import { requireApiAccess } from "@/lib/authorization";
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 SubmitTx = Prisma.TransactionClient & {
@ -36,6 +37,12 @@ export async function POST(request: Request, context: RouteContext) {
include: {
agent: { select: { id: true, name: true } },
profitShareScheme: { select: { id: true, shareAgent: true } },
receipts: {
select: {
id: true,
receiptNo: true
}
},
lines: {
include: {
grade: true,
@ -52,6 +59,13 @@ export async function POST(request: Request, context: RouteContext) {
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 });
}
@ -61,6 +75,8 @@ export async function POST(request: Request, context: RouteContext) {
}
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) {
@ -92,8 +108,45 @@ export async function POST(request: Request, context: RouteContext) {
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 enteredQtyLines) {
for (const line of receiptLines) {
if (!line.warehouseId) {
throw new Error("Warehouse belum diisi pada salah satu baris pembelian");
}
@ -107,7 +160,9 @@ export async function POST(request: Request, context: RouteContext) {
sourceType: "PURCHASE",
sourceRefId: purchaseId,
purchaseId: purchase.id,
purchaseLineId: line.id,
purchaseLineId: line.purchaseLineId,
receiptId: receipt.id,
receiptLineId: line.id,
gradeId: line.gradeId,
warehouseId: line.warehouseId,
warehouseLocationId: line.warehouseLocationId,
@ -158,7 +213,7 @@ export async function POST(request: Request, context: RouteContext) {
amountProfit: new Prisma.Decimal(0),
agentSharePercentSnapshot: purchase.profitShareScheme?.shareAgent ?? null,
agentAmount: new Prisma.Decimal(0),
notes: `Opening realization dari purchase line ${line.id.toString()}`
notes: `Opening realization dari purchase line ${line.purchaseLineId.toString()}`
}
});
@ -181,6 +236,10 @@ export async function POST(request: Request, context: RouteContext) {
return {
purchase: updated,
receipt: {
id: receipt.id.toString(),
receipt_no: receipt.receiptNo
},
lots
};
});
@ -193,10 +252,14 @@ export async function POST(request: Request, context: RouteContext) {
method: request.method,
pathname: new URL(request.url).pathname,
statusCode: 200,
summary: `Purchase ${purchase.purchaseNo} disubmit + lot dibuat`,
summary: `Purchase ${purchase.purchaseNo} disubmit + receipt + lot dibuat`,
metadata: buildAuditChangeMetadata(
{ status: purchase.status, lot_count: 0 },
{ status: generated.purchase.status, lot_count: generated.lots.length }
{ status: purchase.status, lot_count: 0, receipt_no: null },
{
status: generated.purchase.status,
lot_count: generated.lots.length,
receipt_no: generated.receipt.receipt_no
}
)
});
@ -208,7 +271,8 @@ export async function POST(request: Request, context: RouteContext) {
{
success: true,
status: generated.purchase.status,
lot_count: generated.lots.length
lot_count: generated.lots.length,
receipt: generated.receipt
},
{ status: 200 }
);

View File

@ -1,14 +1,17 @@
import { AppShell } from "@/components/layout/app-shell";
import { LotMixingClient } from "@/features/lot-transformations/components/lot-mixing-client";
import { getAppSettings } from "@/lib/app-settings";
export default async function SortingPage() {
const settings = await getAppSettings();
return (
<AppShell
pathname="/sorting"
title="Mixing Lot / Ubah Grade"
description="Campurkan beberapa lot aktif menjadi lot hasil baru dengan grade baru, lalu simpan jejak input-output secara penuh."
>
<LotMixingClient />
<LotMixingClient currencyCode={settings.currency_code} />
</AppShell>
);
}

View File

@ -1,14 +1,17 @@
import { AppShell } from "@/components/layout/app-shell";
import { WashingClient } from "@/features/washing/components/washing-client";
import { getAppSettings } from "@/lib/app-settings";
export default async function WashingPage() {
const settings = await getAppSettings();
return (
<AppShell
pathname="/washing"
title="Pencucian"
description="Kirim lot ke tempat cuci, simpan biaya dan resi, lalu selesaikan hasil cuci dengan pembaruan berat, grade, dan lokasi gudang."
>
<WashingClient />
<WashingClient currencyCode={settings.currency_code} />
</AppShell>
);
}