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:
parent
c92f136b09
commit
3e5d11c4ae
4 changed files with 36 additions and 6 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue