feat(receipt): allow partial receipt confirmation with per-item delivery tracking
Submitters can now mark individual item quantities as received when confirming delivery, rather than treating a PO as all-or-nothing. Schema (migration: 20260516103013_partial_receipt): - POStatus: new PARTIALLY_CLOSED value between PAID_DELIVERED and CLOSED - ActionType: new PARTIAL_RECEIPT_CONFIRMED value - POLineItem: new deliveredQuantity Decimal? field — accumulates delivered qty across multiple receipt events State machine: - PAID_DELIVERED → confirm_partial_receipt → PARTIALLY_CLOSED (new) - PARTIALLY_CLOSED → confirm_receipt → CLOSED (all delivered) - PARTIALLY_CLOSED → confirm_partial_receipt → PARTIALLY_CLOSED (more partial) Receipt page / form: - Loads line items with ordered qty, previously delivered qty, and remaining qty - Per-row numeric input for "receiving now" defaulting to all remaining - "Mark all remaining" shortcut - Dynamic button: "Confirm Partial Receipt" vs "Confirm Receipt & Close PO" - Info banner telling user if the PO will stay open or close Receipt action: - Accumulates deliveredQuantity per line item - If all lines fully delivered → CLOSED + fires notifications + updates inventory - If any line still outstanding → PARTIALLY_CLOSED (no notifications yet) - Inventory auto-update runs per-event for the delivered quantities only Dashboard & PO detail: - Open Orders count now includes PARTIALLY_CLOSED - "Confirm Receipt" CTA in po-detail handles PARTIALLY_CLOSED with distinct amber styling and "Confirm Remaining" label - Activity log shows PARTIAL_RECEIPT_CONFIRMED with appropriate label - PARTIALLY_CLOSED gets warning (amber) badge variant Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b2bfa63f61
commit
3b3a26eafe
9 changed files with 378 additions and 93 deletions
|
|
@ -31,7 +31,7 @@ export default async function DashboardPage() {
|
|||
async function SubmitterDashboard({ userId }: { userId: string }) {
|
||||
const [openCount, pendingCount, closedCount, recentPos] = await Promise.all([
|
||||
db.purchaseOrder.count({
|
||||
where: { submitterId: userId, status: { in: ["DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING", "EDITS_REQUESTED"] } },
|
||||
where: { submitterId: userId, status: { in: ["DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING", "EDITS_REQUESTED", "PARTIALLY_CLOSED"] } },
|
||||
}),
|
||||
db.purchaseOrder.count({
|
||||
where: { submitterId: userId, status: "MGR_REVIEW" },
|
||||
|
|
|
|||
|
|
@ -9,10 +9,17 @@ import { revalidatePath } from "next/cache";
|
|||
export async function confirmReceipt({
|
||||
poId,
|
||||
notes,
|
||||
deliveries,
|
||||
}: {
|
||||
poId: string;
|
||||
notes?: string;
|
||||
}): Promise<{ ok: true } | { error: string }> {
|
||||
/**
|
||||
* Per-line delivery quantities for this receipt event.
|
||||
* Key = line item id, value = quantity delivered now (not cumulative).
|
||||
* If omitted or empty, all items are treated as fully delivered.
|
||||
*/
|
||||
deliveries?: Record<string, number>;
|
||||
}): Promise<{ ok: true; partial: boolean } | { error: string }> {
|
||||
const session = await auth();
|
||||
if (!session?.user) return { error: "Unauthorized" };
|
||||
|
||||
|
|
@ -25,44 +32,110 @@ export async function confirmReceipt({
|
|||
},
|
||||
});
|
||||
if (!po) return { error: "PO not found" };
|
||||
if (!canPerformAction(po.status, "confirm_receipt", session.user.role)) {
|
||||
return { error: "You cannot confirm receipt on this PO." };
|
||||
|
||||
const isAllowedStatus =
|
||||
po.status === "PAID_DELIVERED" || po.status === "PARTIALLY_CLOSED";
|
||||
if (!isAllowedStatus) {
|
||||
return { error: "You cannot confirm receipt on this PO in its current state." };
|
||||
}
|
||||
if (
|
||||
!canPerformAction(po.status, "confirm_receipt", session.user.role) &&
|
||||
!canPerformAction(po.status, "confirm_partial_receipt", session.user.role)
|
||||
) {
|
||||
return { error: "You do not have permission to confirm receipt on this PO." };
|
||||
}
|
||||
if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
|
||||
return { error: "You can only confirm receipt on your own purchase orders." };
|
||||
}
|
||||
|
||||
// Compute the updated deliveredQuantity for each line item
|
||||
const lineUpdates = po.lineItems.map((li) => {
|
||||
const prevDelivered = Number(li.deliveredQuantity ?? 0);
|
||||
const nowDelivered = deliveries ? (deliveries[li.id] ?? 0) : Number(li.quantity);
|
||||
const totalDelivered = prevDelivered + nowDelivered;
|
||||
const ordered = Number(li.quantity);
|
||||
return {
|
||||
id: li.id,
|
||||
productId: li.productId,
|
||||
quantity: ordered,
|
||||
deliveredQuantity: Math.min(totalDelivered, ordered),
|
||||
nowDelivered: Math.min(nowDelivered, ordered - prevDelivered),
|
||||
};
|
||||
});
|
||||
|
||||
// Determine if all items are now fully delivered
|
||||
const allDelivered = lineUpdates.every((u) => u.deliveredQuantity >= u.quantity);
|
||||
const newStatus = allDelivered ? "CLOSED" : "PARTIALLY_CLOSED";
|
||||
const isPartial = !allDelivered;
|
||||
|
||||
// Persist delivery quantities
|
||||
await Promise.all(
|
||||
lineUpdates.map((u) =>
|
||||
db.pOLineItem.update({
|
||||
where: { id: u.id },
|
||||
data: { deliveredQuantity: u.deliveredQuantity },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Update PO status and log action
|
||||
await db.purchaseOrder.update({
|
||||
where: { id: poId },
|
||||
data: {
|
||||
status: "CLOSED",
|
||||
closedAt: new Date(),
|
||||
receipt: notes ? { create: { storageKey: "", fileName: "no-file", notes } } : undefined,
|
||||
status: newStatus,
|
||||
closedAt: allDelivered ? new Date() : undefined,
|
||||
receipt: notes
|
||||
? { create: { storageKey: "", fileName: "no-file", notes } }
|
||||
: undefined,
|
||||
actions: {
|
||||
create: { actionType: "RECEIPT_CONFIRMED", actorId: session.user.id, note: notes ?? null },
|
||||
create: {
|
||||
actionType: isPartial ? "PARTIAL_RECEIPT_CONFIRMED" : "RECEIPT_CONFIRMED",
|
||||
actorId: session.user.id,
|
||||
note: notes ?? null,
|
||||
metadata: isPartial
|
||||
? {
|
||||
deliveries: lineUpdates.map((u) => ({
|
||||
lineItemId: u.id,
|
||||
deliveredNow: u.nowDelivered,
|
||||
totalDelivered: u.deliveredQuantity,
|
||||
ordered: u.quantity,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-update inventory: use PO siteId, fall back to vessel's home site
|
||||
const siteId = (po as typeof po & { siteId?: string | null }).siteId ?? po.vessel?.site?.id ?? null;
|
||||
// Auto-update inventory for delivered quantities
|
||||
const siteId =
|
||||
(po as typeof po & { siteId?: string | null }).siteId ??
|
||||
po.vessel?.site?.id ??
|
||||
null;
|
||||
|
||||
if (siteId) {
|
||||
for (const li of po.lineItems) {
|
||||
if (!li.productId) continue;
|
||||
const qty = Number(li.quantity);
|
||||
for (const u of lineUpdates) {
|
||||
if (!u.productId || u.nowDelivered <= 0) continue;
|
||||
await db.itemInventory.upsert({
|
||||
where: { productId_siteId: { productId: li.productId, siteId } },
|
||||
update: { quantity: { increment: qty } },
|
||||
create: { productId: li.productId, siteId, quantity: qty },
|
||||
where: { productId_siteId: { productId: u.productId, siteId } },
|
||||
update: { quantity: { increment: u.nowDelivered } },
|
||||
create: { productId: u.productId, siteId, quantity: u.nowDelivered },
|
||||
});
|
||||
}
|
||||
revalidatePath(`/admin/sites/${siteId}`);
|
||||
}
|
||||
|
||||
// Notify on full close only
|
||||
if (allDelivered) {
|
||||
const [managers, accounts] = await Promise.all([
|
||||
db.user.findMany({ where: { role: "MANAGER", isActive: true } }),
|
||||
db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }),
|
||||
]);
|
||||
await notify({ event: "RECEIPT_CONFIRMED", po, recipients: [...managers, ...accounts] });
|
||||
}
|
||||
|
||||
revalidatePath(`/po/${poId}`);
|
||||
revalidatePath("/dashboard");
|
||||
return { ok: true };
|
||||
revalidatePath("/my-orders");
|
||||
return { ok: true, partial: isPartial };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,24 +18,59 @@ export default async function ReceiptPage({ params }: Props) {
|
|||
|
||||
const po = await db.purchaseOrder.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, poNumber: true, title: true, status: true, submitterId: true },
|
||||
select: {
|
||||
id: true,
|
||||
poNumber: true,
|
||||
title: true,
|
||||
status: true,
|
||||
submitterId: true,
|
||||
lineItems: {
|
||||
orderBy: { sortOrder: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
quantity: true,
|
||||
unit: true,
|
||||
deliveredQuantity: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!po) notFound();
|
||||
if (po.status !== "PAID_DELIVERED") redirect(`/po/${id}`);
|
||||
if (po.status !== "PAID_DELIVERED" && po.status !== "PARTIALLY_CLOSED") {
|
||||
redirect(`/po/${id}`);
|
||||
}
|
||||
if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
|
||||
redirect(`/po/${id}`);
|
||||
}
|
||||
|
||||
const lineItems = po.lineItems.map((li) => ({
|
||||
id: li.id,
|
||||
name: li.name,
|
||||
quantity: Number(li.quantity),
|
||||
unit: li.unit,
|
||||
deliveredQuantity: li.deliveredQuantity !== null ? Number(li.deliveredQuantity) : null,
|
||||
}));
|
||||
|
||||
const isPartiallyReceived = po.status === "PARTIALLY_CLOSED";
|
||||
|
||||
return (
|
||||
<div className="max-w-lg">
|
||||
<div className="max-w-2xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-neutral-900">Confirm Receipt</h1>
|
||||
<h1 className="text-2xl font-semibold text-neutral-900">
|
||||
{isPartiallyReceived ? "Confirm Remaining Receipt" : "Confirm Receipt"}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-neutral-500">
|
||||
{po.poNumber} — {po.title}
|
||||
</p>
|
||||
{isPartiallyReceived && (
|
||||
<p className="mt-2 text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
|
||||
This PO has been partially received. Mark the quantities delivered in this batch.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ReceiptForm poId={po.id} />
|
||||
<ReceiptForm poId={po.id} lineItems={lineItems} isPartiallyReceived={isPartiallyReceived} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,18 +6,57 @@ import { confirmReceipt } from "./actions";
|
|||
import { FileUploader } from "@/components/po/file-uploader";
|
||||
import { uploadAndLinkFiles } from "@/lib/upload-files";
|
||||
|
||||
export function ReceiptForm({ poId }: { poId: string }) {
|
||||
interface LineItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
deliveredQuantity: number | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
poId: string;
|
||||
lineItems: LineItem[];
|
||||
isPartiallyReceived: boolean;
|
||||
}
|
||||
|
||||
export function ReceiptForm({ poId, lineItems, isPartiallyReceived }: Props) {
|
||||
const router = useRouter();
|
||||
const [notes, setNotes] = useState("");
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Per-item: how many are being delivered in this batch
|
||||
const [deliveries, setDeliveries] = useState<Record<string, number>>(() => {
|
||||
const init: Record<string, number> = {};
|
||||
for (const li of lineItems) {
|
||||
const remaining = li.quantity - (li.deliveredQuantity ?? 0);
|
||||
init[li.id] = remaining; // default: all remaining
|
||||
}
|
||||
return init;
|
||||
});
|
||||
|
||||
function setDelivery(id: string, value: number) {
|
||||
const li = lineItems.find((l) => l.id === id)!;
|
||||
const remaining = li.quantity - (li.deliveredQuantity ?? 0);
|
||||
setDeliveries((prev) => ({ ...prev, [id]: Math.max(0, Math.min(value, remaining)) }));
|
||||
}
|
||||
|
||||
function markAllRemaining() {
|
||||
const all: Record<string, number> = {};
|
||||
for (const li of lineItems) {
|
||||
all[li.id] = li.quantity - (li.deliveredQuantity ?? 0);
|
||||
}
|
||||
setDeliveries(all);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setError("");
|
||||
const result = await confirmReceipt({ poId, notes });
|
||||
|
||||
const result = await confirmReceipt({ poId, notes, deliveries });
|
||||
if ("error" in result) {
|
||||
setError(result.error);
|
||||
setSubmitting(false);
|
||||
|
|
@ -35,24 +74,106 @@ export function ReceiptForm({ poId }: { poId: string }) {
|
|||
router.refresh();
|
||||
}
|
||||
|
||||
const allFullyDelivered = lineItems.every(
|
||||
(li) => (li.deliveredQuantity ?? 0) + (deliveries[li.id] ?? 0) >= li.quantity
|
||||
);
|
||||
const nothingDelivered = Object.values(deliveries).every((v) => v === 0);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
||||
<div className="space-y-5">
|
||||
{/* Line items delivery tracker */}
|
||||
<div className="rounded-lg border border-neutral-200 bg-white overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-neutral-100 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-neutral-800">Items to Receive</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={markAllRemaining}
|
||||
className="text-xs text-primary-600 hover:text-primary-700 font-medium"
|
||||
>
|
||||
Mark all remaining
|
||||
</button>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-neutral-50 border-b border-neutral-100">
|
||||
<tr>
|
||||
<th className="px-4 py-2.5 text-left font-medium text-neutral-600">Item</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Ordered</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Previously Received</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Remaining</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Receiving Now</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-100">
|
||||
{lineItems.map((li) => {
|
||||
const prevDelivered = li.deliveredQuantity ?? 0;
|
||||
const remaining = li.quantity - prevDelivered;
|
||||
const nowDelivering = deliveries[li.id] ?? 0;
|
||||
const isFullyReceived = remaining <= 0;
|
||||
|
||||
return (
|
||||
<tr key={li.id} className={isFullyReceived ? "bg-neutral-50" : ""}>
|
||||
<td className="px-4 py-3 font-medium text-neutral-900">
|
||||
{li.name}
|
||||
{isFullyReceived && (
|
||||
<span className="ml-2 text-xs text-success-600 font-normal">✓ fully received</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-neutral-600">
|
||||
{li.quantity} {li.unit}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-neutral-500">
|
||||
{isPartiallyReceived || prevDelivered > 0 ? `${prevDelivered} ${li.unit}` : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs">
|
||||
<span className={remaining > 0 ? "text-amber-700 font-medium" : "text-neutral-400"}>
|
||||
{remaining > 0 ? `${remaining} ${li.unit}` : "—"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{isFullyReceived ? (
|
||||
<span className="text-xs text-neutral-400">—</span>
|
||||
) : (
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={remaining}
|
||||
step="any"
|
||||
value={nowDelivering}
|
||||
onChange={(e) => setDelivery(li.id, parseFloat(e.target.value) || 0)}
|
||||
className="w-24 rounded border border-neutral-300 px-2 py-1 text-right text-sm font-mono focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500/30"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-5">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<p className="text-sm text-neutral-600">
|
||||
Confirming receipt will close this purchase order. Please verify that all
|
||||
items have been received before proceeding.
|
||||
{allFullyDelivered ? (
|
||||
<p className="text-sm text-success-700 bg-success-50 border border-success-100 rounded-lg px-3 py-2">
|
||||
All items will be marked as received. Confirming will fully close this PO.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
|
||||
Not all items are being received. The PO will remain open as “Partially Received” until
|
||||
all items are delivered.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
|
||||
Delivery notes (optional)
|
||||
</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
rows={3}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="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 resize-none"
|
||||
placeholder="Any notes about the delivery…"
|
||||
placeholder="Any notes about this delivery batch…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -77,13 +198,22 @@ export function ReceiptForm({ poId }: { poId: string }) {
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="rounded-lg bg-success px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
|
||||
disabled={submitting || nothingDelivered}
|
||||
className={`rounded-lg px-4 py-2.5 text-sm font-semibold text-white disabled:opacity-60 transition-opacity ${
|
||||
allFullyDelivered
|
||||
? "bg-success hover:opacity-90"
|
||||
: "bg-primary-600 hover:bg-primary-700"
|
||||
}`}
|
||||
>
|
||||
{submitting ? "Confirming…" : "Confirm Receipt & Close PO"}
|
||||
{submitting
|
||||
? "Confirming…"
|
||||
: allFullyDelivered
|
||||
? "Confirm Receipt & Close PO"
|
||||
: "Confirm Partial Receipt"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ const ACTION_LABELS: Record<string, string> = {
|
|||
VENDOR_ID_PROVIDED: "Vendor ID provided",
|
||||
PAYMENT_SENT: "Payment confirmed",
|
||||
RECEIPT_CONFIRMED: "Receipt confirmed",
|
||||
PARTIAL_RECEIPT_CONFIRMED: "Partial receipt confirmed",
|
||||
CLOSED: "Closed",
|
||||
MANAGER_LINE_EDIT: "Manager amended line items",
|
||||
PRODUCT_PRICE_UPDATED: "Product prices updated",
|
||||
|
|
@ -140,7 +141,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
);
|
||||
|
||||
const canConfirmReceipt =
|
||||
po.status === "PAID_DELIVERED" &&
|
||||
(po.status === "PAID_DELIVERED" || po.status === "PARTIALLY_CLOSED") &&
|
||||
(po.submitter.id === currentUserId || currentRole === "SUPERUSER") &&
|
||||
!readOnly;
|
||||
|
||||
|
|
@ -388,16 +389,28 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
|
||||
{/* Confirm receipt CTA */}
|
||||
{canConfirmReceipt && (
|
||||
<div className="rounded-lg border border-success-100 bg-success-50 p-5 flex items-center justify-between">
|
||||
<div className={`rounded-lg border p-5 flex items-center justify-between ${
|
||||
po.status === "PARTIALLY_CLOSED"
|
||||
? "border-warning-100 bg-warning-50"
|
||||
: "border-success-100 bg-success-50"
|
||||
}`}>
|
||||
<div>
|
||||
<p className="font-medium text-success-700">Payment confirmed</p>
|
||||
<p className="text-sm text-success-700 mt-0.5">Please confirm that you have received all items.</p>
|
||||
<p className={`font-medium ${po.status === "PARTIALLY_CLOSED" ? "text-warning-700" : "text-success-700"}`}>
|
||||
{po.status === "PARTIALLY_CLOSED" ? "Partially received" : "Payment confirmed"}
|
||||
</p>
|
||||
<p className={`text-sm mt-0.5 ${po.status === "PARTIALLY_CLOSED" ? "text-warning-700" : "text-success-700"}`}>
|
||||
{po.status === "PARTIALLY_CLOSED"
|
||||
? "Some items are still outstanding. Confirm remaining deliveries."
|
||||
: "Please confirm that you have received all items."}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/po/${po.id}/receipt`}
|
||||
className="rounded-lg bg-success px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90"
|
||||
className={`rounded-lg px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 ${
|
||||
po.status === "PARTIALLY_CLOSED" ? "bg-warning-600" : "bg-success"
|
||||
}`}
|
||||
>
|
||||
Confirm Receipt
|
||||
{po.status === "PARTIALLY_CLOSED" ? "Confirm Remaining" : "Confirm Receipt"}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ export type POAction =
|
|||
| "provide_vendor_id"
|
||||
| "process_payment"
|
||||
| "mark_paid"
|
||||
| "confirm_receipt";
|
||||
| "confirm_receipt"
|
||||
| "confirm_partial_receipt";
|
||||
|
||||
export type SideEffect =
|
||||
| "EMAIL_MANAGER"
|
||||
|
|
@ -110,6 +111,26 @@ const TRANSITIONS: Partial<Record<POStatus, TransitionMap>> = {
|
|||
requiresNote: false,
|
||||
sideEffects: ["EMAIL_MANAGER", "EMAIL_ACCOUNTS"],
|
||||
},
|
||||
confirm_partial_receipt: {
|
||||
to: "PARTIALLY_CLOSED",
|
||||
allowedRoles: ["TECHNICAL", "MANNING", "SUPERUSER"],
|
||||
requiresNote: false,
|
||||
sideEffects: [],
|
||||
},
|
||||
},
|
||||
PARTIALLY_CLOSED: {
|
||||
confirm_receipt: {
|
||||
to: "CLOSED",
|
||||
allowedRoles: ["TECHNICAL", "MANNING", "SUPERUSER"],
|
||||
requiresNote: false,
|
||||
sideEffects: ["EMAIL_MANAGER", "EMAIL_ACCOUNTS"],
|
||||
},
|
||||
confirm_partial_receipt: {
|
||||
to: "PARTIALLY_CLOSED",
|
||||
allowedRoles: ["TECHNICAL", "MANNING", "SUPERUSER"],
|
||||
requiresNote: false,
|
||||
sideEffects: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export const PO_STATUS_LABELS: Record<POStatus, string> = {
|
|||
MGR_APPROVED: "Approved",
|
||||
SENT_FOR_PAYMENT: "Sent for Payment",
|
||||
PAID_DELIVERED: "Paid",
|
||||
PARTIALLY_CLOSED: "Partially Received",
|
||||
CLOSED: "Closed",
|
||||
};
|
||||
|
||||
|
|
@ -69,5 +70,6 @@ export const PO_STATUS_VARIANTS: Record<POStatus, BadgeVariant> = {
|
|||
MGR_APPROVED: "success",
|
||||
SENT_FOR_PAYMENT: "default",
|
||||
PAID_DELIVERED: "success",
|
||||
PARTIALLY_CLOSED: "warning",
|
||||
CLOSED: "secondary",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "ActionType" ADD VALUE 'PARTIAL_RECEIPT_CONFIRMED';
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "POStatus" ADD VALUE 'PARTIALLY_CLOSED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "POLineItem" ADD COLUMN "deliveredQuantity" DECIMAL(10,3);
|
||||
|
|
@ -27,6 +27,7 @@ enum POStatus {
|
|||
MGR_APPROVED
|
||||
SENT_FOR_PAYMENT
|
||||
PAID_DELIVERED
|
||||
PARTIALLY_CLOSED
|
||||
CLOSED
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ enum ActionType {
|
|||
VENDOR_ID_PROVIDED
|
||||
PAYMENT_SENT
|
||||
RECEIPT_CONFIRMED
|
||||
PARTIAL_RECEIPT_CONFIRMED
|
||||
CLOSED
|
||||
REASSIGNED
|
||||
PRODUCT_PRICE_UPDATED
|
||||
|
|
@ -254,6 +256,7 @@ model POLineItem {
|
|||
gstRate Decimal @default(0.18) @db.Decimal(5, 4)
|
||||
sortOrder Int @default(0)
|
||||
size String?
|
||||
deliveredQuantity Decimal? @db.Decimal(10, 3)
|
||||
productId String?
|
||||
product Product? @relation(fields: [productId], references: [id])
|
||||
accountId String?
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue