feat(po): show note author name on manager note banners

Derives the author from the most recent EDITS_REQUESTED / REJECTED /
APPROVED action that carries a note. PO detail banner now shows 'Note from
[name]', edit-page banner shows 'Edits requested by [name]', and the
closed-orders list prefixes the truncated note with the author's name.
No schema changes required - uses the already-fetched actions with actor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-27 04:41:53 +05:30
parent c92f136b09
commit 3e5d11c4ae
4 changed files with 36 additions and 6 deletions

View file

@ -24,6 +24,15 @@ export default async function MyOrdersPage() {
include: { include: {
vessel: { select: { name: true } }, vessel: { select: { name: true } },
account: { select: { name: true, code: true } }, account: { select: { name: true, code: true } },
actions: {
where: {
actionType: { in: ["EDITS_REQUESTED", "REJECTED", "APPROVED", "APPROVED_WITH_NOTE"] },
note: { not: null },
},
orderBy: { createdAt: "desc" },
take: 1,
select: { actor: { select: { name: true } } },
},
}, },
}); });
@ -62,6 +71,7 @@ type PoRow = {
account: { name: string; code: string }; account: { name: string; code: string };
updatedAt: Date; updatedAt: Date;
managerNote: string | null; managerNote: string | null;
actions: { actor: { name: string } }[];
}; };
function PoTable({ title, rows, className = "" }: { title: string; rows: PoRow[]; className?: string }) { function PoTable({ title, rows, className = "" }: { title: string; rows: PoRow[]; className?: string }) {
@ -95,7 +105,7 @@ function PoTable({ title, rows, className = "" }: { title: string; rows: PoRow[]
</Link> </Link>
{po.managerNote && ( {po.managerNote && (
<p className="mt-0.5 text-xs text-warning-700 italic truncate max-w-xs"> <p className="mt-0.5 text-xs text-warning-700 italic truncate max-w-xs">
Note: {po.managerNote} {po.actions[0]?.actor.name ? `${po.actions[0].actor.name}: ` : "Note: "}{po.managerNote}
</p> </p>
)} )}
</td> </td>

View file

@ -37,9 +37,10 @@ interface Props {
vessels: Vessel[]; vessels: Vessel[];
accounts: Account[]; accounts: Account[];
vendors: Vendor[]; vendors: Vendor[];
managerNoteAuthor?: string | null;
} }
export function EditPoForm({ po, vessels, accounts, vendors }: Props) { export function EditPoForm({ po, vessels, accounts, vendors, managerNoteAuthor }: Props) {
const router = useRouter(); const router = useRouter();
const [lineItems, setLineItems] = useState<LineItemInput[]>( const [lineItems, setLineItems] = useState<LineItemInput[]>(
po.lineItems.map((li) => ({ po.lineItems.map((li) => ({
@ -104,7 +105,9 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
<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()}>
{canResubmit && ( {canResubmit && (
<div className="rounded-lg border border-warning-100 bg-warning-50 px-4 py-3"> <div className="rounded-lg border border-warning-100 bg-warning-50 px-4 py-3">
<p className="text-sm font-medium text-warning-700">Edits requested</p> <p className="text-sm font-medium text-warning-700">
{managerNoteAuthor ? `Edits requested by ${managerNoteAuthor}` : "Edits requested"}
</p>
{po.managerNote && ( {po.managerNote && (
<p className="mt-1 text-sm text-warning-700 italic">"{po.managerNote}"</p> <p className="mt-1 text-sm text-warning-700 italic">"{po.managerNote}"</p>
)} )}

View file

@ -29,10 +29,17 @@ export default async function EditPoPage({ params }: Props) {
po.submitterId === session.user.id || session.user.role === "SUPERUSER"; po.submitterId === session.user.id || session.user.role === "SUPERUSER";
if (!canEdit) redirect(`/po/${id}`); if (!canEdit) redirect(`/po/${id}`);
const [vessels, accounts, vendors] = await Promise.all([ const [vessels, accounts, vendors, noteAction] = await Promise.all([
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }), db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
db.account.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }), db.account.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }), db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
po.status === "EDITS_REQUESTED"
? db.pOAction.findFirst({
where: { poId: po.id, actionType: "EDITS_REQUESTED", note: { not: null } },
orderBy: { createdAt: "desc" },
include: { actor: { select: { name: true } } },
})
: Promise.resolve(null),
]); ]);
const serializedPo = { const serializedPo = {
@ -53,7 +60,7 @@ export default async function EditPoPage({ params }: Props) {
<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={serializedPo} vessels={vessels} accounts={accounts} vendors={vendors} /> <EditPoForm po={serializedPo} vessels={vessels} accounts={accounts} vendors={vendors} managerNoteAuthor={noteAction?.actor.name ?? null} />
</div> </div>
); );
} }

View file

@ -106,6 +106,14 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
.reverse() .reverse()
.find((a) => a.actionType === "MANAGER_LINE_EDIT"); .find((a) => a.actionType === "MANAGER_LINE_EDIT");
const noteAction = [...po.actions]
.reverse()
.find((a) =>
["EDITS_REQUESTED", "REJECTED", "APPROVED", "APPROVED_WITH_NOTE"].includes(a.actionType) &&
a.note
);
const managerNoteAuthor = noteAction?.actor.name ?? null;
// Resubmit snapshot: stored in the most recent SUBMITTED action's metadata // Resubmit snapshot: stored in the most recent SUBMITTED action's metadata
// when the submitter resubmits after EDITS_REQUESTED. // when the submitter resubmits after EDITS_REQUESTED.
type ResubmitSnapshot = { type ResubmitSnapshot = {
@ -208,7 +216,9 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
{/* Manager note banner */} {/* Manager note banner */}
{po.managerNote && ( {po.managerNote && (
<div className="rounded-lg border border-warning-100 bg-warning-50 px-4 py-3"> <div className="rounded-lg border border-warning-100 bg-warning-50 px-4 py-3">
<p className="text-sm font-medium text-warning-700 mb-0.5">Manager note</p> <p className="text-sm font-medium text-warning-700 mb-0.5">
{managerNoteAuthor ? `Note from ${managerNoteAuthor}` : "Manager note"}
</p>
<p className="text-sm text-warning-700">{po.managerNote}</p> <p className="text-sm text-warning-700">{po.managerNote}</p>
</div> </div>
)} )}