fix: serialize Prisma Decimal fields before server→client boundary
Convert quantity, unitPrice, totalPrice, gstRate, and totalAmount to plain numbers in server pages before passing to client components, preventing Next.js serialization errors on Decimal objects. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dd7a40e523
commit
17586e6ea1
4 changed files with 74 additions and 28 deletions
|
|
@ -6,10 +6,25 @@ import { managerEditPo } from "./manager-po-edit-actions";
|
|||
import { LineItemsEditor } from "@/components/po/po-line-items-editor";
|
||||
import { TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po";
|
||||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
import type { Vessel, Account, Vendor, PurchaseOrder, POLineItem } from "@prisma/client";
|
||||
import type { Vessel, Account, Vendor, PurchaseOrder } from "@prisma/client";
|
||||
|
||||
type PoFull = PurchaseOrder & {
|
||||
lineItems: POLineItem[];
|
||||
type SerializedLineItem = {
|
||||
id: string;
|
||||
poId: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
size: string | null;
|
||||
unitPrice: number;
|
||||
totalPrice: number;
|
||||
gstRate: number;
|
||||
sortOrder: number;
|
||||
productId: string | null;
|
||||
};
|
||||
|
||||
type PoFull = Omit<PurchaseOrder, "totalAmount"> & {
|
||||
totalAmount: number;
|
||||
lineItems: SerializedLineItem[];
|
||||
vessel: { id: string; name: string };
|
||||
account: { id: string; name: string; code: string };
|
||||
vendor: { id: string; name: string; vendorId: string | null } | null;
|
||||
|
|
@ -45,11 +60,11 @@ export function ManagerEditPoForm({ po, vessels, accounts, vendors }: Props) {
|
|||
const [lineItems, setLineItems] = useState<LineItemInput[]>(
|
||||
po.lineItems.map((li) => ({
|
||||
description: li.description,
|
||||
quantity: Number(li.quantity),
|
||||
quantity: li.quantity,
|
||||
unit: li.unit,
|
||||
size: li.size ?? undefined,
|
||||
unitPrice: Number(li.unitPrice),
|
||||
gstRate: Number((li as POLineItem & { gstRate?: unknown }).gstRate ?? 0.18),
|
||||
unitPrice: li.unitPrice,
|
||||
gstRate: li.gstRate,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
if (!po) notFound();
|
||||
if (po.status !== "MGR_REVIEW") redirect(`/po/${id}`);
|
||||
|
||||
const serializedPo = {
|
||||
...po,
|
||||
totalAmount: po.totalAmount.toNumber(),
|
||||
lineItems: po.lineItems.map((li) => ({
|
||||
...li,
|
||||
quantity: li.quantity.toNumber(),
|
||||
unitPrice: li.unitPrice.toNumber(),
|
||||
totalPrice: li.totalPrice.toNumber(),
|
||||
gstRate: li.gstRate.toNumber(),
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
|
|
@ -55,7 +67,7 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
<PoDetail po={po} currentUserId={session.user.id} currentRole={session.user.role} readOnly />
|
||||
|
||||
<ManagerEditPoForm
|
||||
po={po}
|
||||
po={serializedPo}
|
||||
vessels={vessels}
|
||||
accounts={accounts}
|
||||
vendors={vendors}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updatePo } from "./actions";
|
||||
import type { Vessel, Account, Vendor, PurchaseOrder, POLineItem } from "@prisma/client";
|
||||
import type { Vessel, Account, Vendor, PurchaseOrder } from "@prisma/client";
|
||||
import { LineItemsEditor } from "@/components/po/po-line-items-editor";
|
||||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
import { TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po";
|
||||
|
|
@ -11,7 +11,24 @@ import { TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po";
|
|||
const INPUT_CLS =
|
||||
"w-full rounded-lg border border-neutral-300 px-3 py-2.5 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20";
|
||||
|
||||
type PoWithItems = PurchaseOrder & { lineItems: POLineItem[] };
|
||||
type SerializedLineItem = {
|
||||
id: string;
|
||||
poId: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
size: string | null;
|
||||
unitPrice: number;
|
||||
totalPrice: number;
|
||||
gstRate: number;
|
||||
sortOrder: number;
|
||||
productId: string | null;
|
||||
};
|
||||
|
||||
type PoWithItems = Omit<PurchaseOrder, "totalAmount"> & {
|
||||
totalAmount: number;
|
||||
lineItems: SerializedLineItem[];
|
||||
};
|
||||
|
||||
interface Props {
|
||||
po: PoWithItems;
|
||||
|
|
@ -25,11 +42,11 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
|
|||
const [lineItems, setLineItems] = useState<LineItemInput[]>(
|
||||
po.lineItems.map((li) => ({
|
||||
description: li.description,
|
||||
quantity: Number(li.quantity),
|
||||
quantity: li.quantity,
|
||||
unit: li.unit,
|
||||
size: li.size ?? undefined,
|
||||
unitPrice: Number(li.unitPrice),
|
||||
gstRate: Number((li as POLineItem & { gstRate: unknown }).gstRate ?? 0.18),
|
||||
unitPrice: li.unitPrice,
|
||||
gstRate: li.gstRate,
|
||||
}))
|
||||
);
|
||||
const [submitting, setSubmitting] = useState<"save" | "resubmit" | null>(null);
|
||||
|
|
@ -64,24 +81,14 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
|
|||
const dateValue = po.dateRequired
|
||||
? new Date(po.dateRequired).toISOString().split("T")[0]
|
||||
: "";
|
||||
const piDateValue = (po as PurchaseOrder & { piQuotationDate: Date | null }).piQuotationDate
|
||||
? new Date((po as PurchaseOrder & { piQuotationDate: Date | null }).piQuotationDate!).toISOString().split("T")[0]
|
||||
const piDateValue = po.piQuotationDate
|
||||
? new Date(po.piQuotationDate).toISOString().split("T")[0]
|
||||
: "";
|
||||
const reqDateValue = (po as PurchaseOrder & { requisitionDate: Date | null }).requisitionDate
|
||||
? new Date((po as PurchaseOrder & { requisitionDate: Date | null }).requisitionDate!).toISOString().split("T")[0]
|
||||
const reqDateValue = po.requisitionDate
|
||||
? new Date(po.requisitionDate).toISOString().split("T")[0]
|
||||
: "";
|
||||
|
||||
const extPo = po as PurchaseOrder & {
|
||||
piQuotationNo: string | null;
|
||||
requisitionNo: string | null;
|
||||
placeOfDelivery: string | null;
|
||||
tcDelivery: string | null;
|
||||
tcDispatch: string | null;
|
||||
tcInspection: string | null;
|
||||
tcTransitInsurance: string | null;
|
||||
tcPaymentTerms: string | null;
|
||||
tcOthers: string | null;
|
||||
};
|
||||
const extPo = po;
|
||||
|
||||
return (
|
||||
<form id="edit-po-form" className="space-y-6" onSubmit={(e) => e.preventDefault()}>
|
||||
|
|
|
|||
|
|
@ -35,13 +35,25 @@ export default async function EditPoPage({ params }: Props) {
|
|||
db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
|
||||
]);
|
||||
|
||||
const serializedPo = {
|
||||
...po,
|
||||
totalAmount: po.totalAmount.toNumber(),
|
||||
lineItems: po.lineItems.map((li) => ({
|
||||
...li,
|
||||
quantity: li.quantity.toNumber(),
|
||||
unitPrice: li.unitPrice.toNumber(),
|
||||
totalPrice: li.totalPrice.toNumber(),
|
||||
gstRate: li.gstRate.toNumber(),
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-neutral-900">Edit Purchase Order</h1>
|
||||
<p className="mt-1 text-sm text-neutral-500 font-mono">{po.poNumber}</p>
|
||||
</div>
|
||||
<EditPoForm po={po} vessels={vessels} accounts={accounts} vendors={vendors} />
|
||||
<EditPoForm po={serializedPo} vessels={vessels} accounts={accounts} vendors={vendors} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue