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 { 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,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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()}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue