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:
Hardik 2026-05-09 17:51:54 +05:30
parent dd7a40e523
commit 17586e6ea1
4 changed files with 74 additions and 28 deletions

View file

@ -6,10 +6,25 @@ import { managerEditPo } from "./manager-po-edit-actions";
import { LineItemsEditor } from "@/components/po/po-line-items-editor"; import { LineItemsEditor } from "@/components/po/po-line-items-editor";
import { TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po"; import { TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po";
import type { LineItemInput } 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 & { type SerializedLineItem = {
lineItems: POLineItem[]; 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 }; vessel: { id: string; name: string };
account: { id: string; name: string; code: string }; account: { id: string; name: string; code: string };
vendor: { id: string; name: string; vendorId: string | null } | null; 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[]>( const [lineItems, setLineItems] = useState<LineItemInput[]>(
po.lineItems.map((li) => ({ po.lineItems.map((li) => ({
description: li.description, description: li.description,
quantity: Number(li.quantity), quantity: li.quantity,
unit: li.unit, unit: li.unit,
size: li.size ?? undefined, size: li.size ?? undefined,
unitPrice: Number(li.unitPrice), unitPrice: li.unitPrice,
gstRate: Number((li as POLineItem & { gstRate?: unknown }).gstRate ?? 0.18), gstRate: li.gstRate,
})) }))
); );

View file

@ -43,6 +43,18 @@ export default async function ApprovalDetailPage({ params }: Props) {
if (!po) notFound(); if (!po) notFound();
if (po.status !== "MGR_REVIEW") redirect(`/po/${id}`); 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 ( return (
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="mb-6 flex items-center justify-between"> <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 /> <PoDetail po={po} currentUserId={session.user.id} currentRole={session.user.role} readOnly />
<ManagerEditPoForm <ManagerEditPoForm
po={po} po={serializedPo}
vessels={vessels} vessels={vessels}
accounts={accounts} accounts={accounts}
vendors={vendors} vendors={vendors}

View file

@ -3,7 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { updatePo } from "./actions"; 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 { LineItemsEditor } from "@/components/po/po-line-items-editor";
import type { LineItemInput } from "@/lib/validations/po"; import type { LineItemInput } from "@/lib/validations/po";
import { TC_DEFAULTS, TC_FIXED_LINE } 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 = 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"; "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 { interface Props {
po: PoWithItems; po: PoWithItems;
@ -25,11 +42,11 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
const [lineItems, setLineItems] = useState<LineItemInput[]>( const [lineItems, setLineItems] = useState<LineItemInput[]>(
po.lineItems.map((li) => ({ po.lineItems.map((li) => ({
description: li.description, description: li.description,
quantity: Number(li.quantity), quantity: li.quantity,
unit: li.unit, unit: li.unit,
size: li.size ?? undefined, size: li.size ?? undefined,
unitPrice: Number(li.unitPrice), unitPrice: li.unitPrice,
gstRate: Number((li as POLineItem & { gstRate: unknown }).gstRate ?? 0.18), gstRate: li.gstRate,
})) }))
); );
const [submitting, setSubmitting] = useState<"save" | "resubmit" | null>(null); const [submitting, setSubmitting] = useState<"save" | "resubmit" | null>(null);
@ -64,24 +81,14 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
const dateValue = po.dateRequired const dateValue = po.dateRequired
? new Date(po.dateRequired).toISOString().split("T")[0] ? new Date(po.dateRequired).toISOString().split("T")[0]
: ""; : "";
const piDateValue = (po as PurchaseOrder & { piQuotationDate: Date | null }).piQuotationDate const piDateValue = po.piQuotationDate
? new Date((po as PurchaseOrder & { piQuotationDate: Date | null }).piQuotationDate!).toISOString().split("T")[0] ? new Date(po.piQuotationDate).toISOString().split("T")[0]
: ""; : "";
const reqDateValue = (po as PurchaseOrder & { requisitionDate: Date | null }).requisitionDate const reqDateValue = po.requisitionDate
? new Date((po as PurchaseOrder & { requisitionDate: Date | null }).requisitionDate!).toISOString().split("T")[0] ? new Date(po.requisitionDate).toISOString().split("T")[0]
: ""; : "";
const extPo = po as PurchaseOrder & { const extPo = po;
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;
};
return ( return (
<form id="edit-po-form" className="space-y-6" onSubmit={(e) => e.preventDefault()}> <form id="edit-po-form" className="space-y-6" onSubmit={(e) => e.preventDefault()}>

View file

@ -35,13 +35,25 @@ export default async function EditPoPage({ params }: Props) {
db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }), 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 ( return (
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-2xl font-semibold text-neutral-900">Edit Purchase Order</h1> <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> <p className="mt-1 text-sm text-neutral-500 font-mono">{po.poNumber}</p>
</div> </div>
<EditPoForm po={po} vessels={vessels} accounts={accounts} vendors={vendors} /> <EditPoForm po={serializedPo} vessels={vessels} accounts={accounts} vendors={vendors} />
</div> </div>
); );
} }