fix(po): add Submit for Approval button on draft PO detail and edit pages
A draft PO had no way to be submitted without creating a brand new PO. Two surfaces are now fixed: Edit page (/po/[id]/edit): - updatePo action now accepts intent "submit" (alongside "save" and "resubmit"), which transitions a DRAFT PO to MGR_REVIEW, creates a SUBMITTED action, and fires notifications — identical behaviour to the new-PO submit flow - EditPoForm shows a primary "Submit for Approval" button when the PO is in DRAFT status; "Resubmit for Approval" remains for EDITS_REQUESTED PO detail page (/po/[id]): - New submitDraftPo server action in po/[id]/actions.ts handles the same DRAFT → MGR_REVIEW transition without requiring the full edit form - New SubmitDraftButton client component renders next to the Edit link in the detail header for DRAFT POs owned by the current user - Button order: Edit · Submit for Approval · Discard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
48de2d08a2
commit
d297fd044f
5 changed files with 94 additions and 7 deletions
|
|
@ -48,6 +48,42 @@ export async function provideVendorId({
|
|||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function submitDraftPo(
|
||||
poId: string
|
||||
): Promise<{ ok: true } | { error: string }> {
|
||||
const session = await auth();
|
||||
if (!session?.user) return { error: "Unauthorized" };
|
||||
|
||||
const po = await db.purchaseOrder.findUnique({
|
||||
where: { id: poId },
|
||||
include: { submitter: true },
|
||||
});
|
||||
if (!po) return { error: "PO not found" };
|
||||
if (po.status !== "DRAFT") return { error: "Only draft purchase orders can be submitted." };
|
||||
if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
|
||||
return { error: "You can only submit your own purchase orders." };
|
||||
}
|
||||
|
||||
await db.purchaseOrder.update({
|
||||
where: { id: poId },
|
||||
data: {
|
||||
status: "MGR_REVIEW",
|
||||
submittedAt: new Date(),
|
||||
actions: {
|
||||
create: { actionType: "SUBMITTED", actorId: session.user.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const managers = await db.user.findMany({ where: { role: "MANAGER", isActive: true } });
|
||||
await notify({ event: "PO_SUBMITTED", po, recipients: [po.submitter, ...managers] });
|
||||
|
||||
revalidatePath(`/po/${poId}`);
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/my-orders");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function discardDraftPo(
|
||||
poId: string
|
||||
): Promise<{ ok: true } | { error: string }> {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export async function updatePo(
|
|||
return { error: "You can only edit your own purchase orders." };
|
||||
}
|
||||
|
||||
const intent = formData.get("intent") as "save" | "resubmit";
|
||||
const intent = formData.get("intent") as "save" | "submit" | "resubmit";
|
||||
|
||||
const parsed = createPoSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
|
|
@ -74,7 +74,9 @@ export async function updatePo(
|
|||
0
|
||||
);
|
||||
|
||||
const isSubmit = intent === "submit" && po.status === "DRAFT";
|
||||
const isResubmit = intent === "resubmit" && po.status === "EDITS_REQUESTED";
|
||||
const shouldSubmit = isSubmit || isResubmit;
|
||||
|
||||
// Before mutating, snapshot the current PO state so the manager can see
|
||||
// exactly what the submitter changed when they resubmit after edits requested.
|
||||
|
|
@ -150,8 +152,8 @@ export async function updatePo(
|
|||
tcPaymentTerms: data.tcPaymentTerms ?? null,
|
||||
tcOthers: data.tcOthers ?? null,
|
||||
totalAmount: total,
|
||||
status: isResubmit ? "MGR_REVIEW" : "DRAFT",
|
||||
submittedAt: isResubmit ? new Date() : po.submittedAt,
|
||||
status: shouldSubmit ? "MGR_REVIEW" : "DRAFT",
|
||||
submittedAt: shouldSubmit ? new Date() : po.submittedAt,
|
||||
lineItems: {
|
||||
deleteMany: {},
|
||||
create: data.lineItems.map((item, idx) => ({
|
||||
|
|
@ -168,18 +170,18 @@ export async function updatePo(
|
|||
})),
|
||||
},
|
||||
actions: {
|
||||
create: isResubmit
|
||||
create: shouldSubmit
|
||||
? {
|
||||
actionType: "SUBMITTED",
|
||||
actorId: session.user.id,
|
||||
...(resubmitSnapshot ? { metadata: { editSnapshot: resubmitSnapshot } } : {}),
|
||||
...(isResubmit && resubmitSnapshot ? { metadata: { editSnapshot: resubmitSnapshot } } : {}),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (isResubmit) {
|
||||
if (shouldSubmit) {
|
||||
const [fullPo, managers] = await Promise.all([
|
||||
db.purchaseOrder.findUnique({ where: { id: poId }, include: { submitter: true } }),
|
||||
db.user.findMany({ where: { role: "MANAGER", isActive: true } }),
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
|
|||
accountId: li.accountId ?? undefined,
|
||||
}))
|
||||
);
|
||||
const [submitting, setSubmitting] = useState<"save" | "resubmit" | null>(null);
|
||||
const [submitting, setSubmitting] = useState<"save" | "submit" | "resubmit" | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const hasPerLineAccounts = po.lineItems.some((li) => li.accountId);
|
||||
const [multiAccount, setMultiAccount] = useState(hasPerLineAccounts);
|
||||
const [defaultAccountId, setDefaultAccountId] = useState(po.accountId ?? "");
|
||||
|
||||
const canSubmit = po.status === "DRAFT";
|
||||
const canResubmit = po.status === "EDITS_REQUESTED";
|
||||
|
||||
async function handleSubmit(intent: "save" | "resubmit") {
|
||||
|
|
@ -283,6 +284,12 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
|
|||
className="rounded-lg border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60 transition-colors">
|
||||
{submitting === "save" ? "Saving…" : "Save Draft"}
|
||||
</button>
|
||||
{canSubmit && (
|
||||
<button type="button" onClick={() => handleSubmit("submit")} disabled={!!submitting}
|
||||
className="rounded-lg bg-primary-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors">
|
||||
{submitting === "submit" ? "Submitting…" : "Submit for Approval"}
|
||||
</button>
|
||||
)}
|
||||
{canResubmit && (
|
||||
<button type="button" onClick={() => handleSubmit("resubmit")} disabled={!!submitting}
|
||||
className="rounded-lg bg-primary-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { PoStatusBadge } from "@/components/po/po-status-badge";
|
||||
import { LineItemsEditor } from "@/components/po/po-line-items-editor";
|
||||
import { DiscardDraftButton } from "@/components/po/discard-draft-button";
|
||||
import { SubmitDraftButton } from "@/components/po/submit-draft-button";
|
||||
import { formatCurrency, formatDate, formatDateTime } from "@/lib/utils";
|
||||
import { generateDownloadUrl } from "@/lib/storage";
|
||||
import { TC_FIXED_LINE } from "@/lib/validations/po";
|
||||
|
|
@ -172,6 +173,11 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
Edit
|
||||
</Link>
|
||||
)}
|
||||
{po.status === "DRAFT" &&
|
||||
(po.submitter.id === currentUserId || currentRole === "SUPERUSER") &&
|
||||
!readOnly && (
|
||||
<SubmitDraftButton poId={po.id} />
|
||||
)}
|
||||
{po.status === "DRAFT" &&
|
||||
(po.submitter.id === currentUserId || ["MANAGER", "SUPERUSER"].includes(currentRole)) &&
|
||||
!readOnly && (
|
||||
|
|
|
|||
36
App/pelagia-portal/components/po/submit-draft-button.tsx
Normal file
36
App/pelagia-portal/components/po/submit-draft-button.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { submitDraftPo } from "@/app/(portal)/po/[id]/actions";
|
||||
|
||||
export function SubmitDraftButton({ poId }: { poId: string }) {
|
||||
const router = useRouter();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function handleSubmit() {
|
||||
setPending(true);
|
||||
setError("");
|
||||
const result = await submitDraftPo(poId);
|
||||
if ("error" in result) {
|
||||
setError(result.error);
|
||||
setPending(false);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={pending}
|
||||
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors"
|
||||
>
|
||||
{pending ? "Submitting…" : "Submit for Approval"}
|
||||
</button>
|
||||
{error && <span className="text-xs text-danger-700">{error}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue