backfill from projects

This commit is contained in:
2026-03-18 11:54:22 -05:00
parent c18de77640
commit f12744f05d
15 changed files with 281 additions and 9 deletions

View File

@@ -427,7 +427,18 @@ export function PurchaseDetailPage() {
</dl>
</article>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project Link</p>
{activeDocument.projectId ? (
<div className="mt-3 space-y-2">
<Link to={`/projects/${activeDocument.projectId}`} className="inline-flex items-center rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text hover:bg-page/70">
{activeDocument.projectNumber} / {activeDocument.projectName}
</Link>
<p className="text-sm text-muted">This purchase order is linked to the project context used by project cockpit and downstream rollups.</p>
</div>
) : (
<p className="mt-3 text-sm text-muted">No linked project is currently attached to this purchase order.</p>
)}
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{activeDocument.notes || "No notes recorded for this document."}</p>
</article>
</div>

View File

@@ -14,6 +14,9 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
const { orderId } = useParams();
const [searchParams] = useSearchParams();
const seededVendorId = searchParams.get("vendorId");
const seededProjectId = searchParams.get("projectId");
const seededProjectNumber = searchParams.get("projectNumber");
const seededProjectName = searchParams.get("projectName");
const planningOrderId = searchParams.get("planningOrderId");
const selectedPlanningItemId = searchParams.get("itemId");
const [form, setForm] = useState<PurchaseOrderInput>(emptyPurchaseOrderInput);
@@ -57,6 +60,12 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
api.getInventoryItemOptions(token).then((options) => setItemOptions(options.filter((option: InventoryItemOptionDto) => option.isPurchasable))).catch(() => setItemOptions([]));
}, [mode, seededVendorId, token]);
useEffect(() => {
if (mode === "create" && seededProjectId) {
setForm((current) => ({ ...current, projectId: current.projectId || seededProjectId }));
}
}, [mode, seededProjectId]);
useEffect(() => {
if (!token || mode !== "create" || !planningOrderId || itemOptions.length === 0) {
return;
@@ -103,6 +112,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
setForm((current) => ({
...current,
vendorId: current.vendorId || autoVendorId || "",
projectId: current.projectId || seededProjectId || null,
notes: current.notes || `Demand-planning recommendation from sales order ${planning.documentNumber}.`,
lines: current.lines.length > 0 ? current.lines : recommendedLines,
}));
@@ -124,7 +134,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
.catch(() => {
setStatus("Unable to load demand-planning recommendations.");
});
}, [itemOptions, mode, planningOrderId, seededVendorId, selectedPlanningItemId, token, vendors]);
}, [itemOptions, mode, planningOrderId, seededProjectId, seededVendorId, selectedPlanningItemId, token, vendors]);
useEffect(() => {
if (!token || mode !== "edit" || !orderId) {
@@ -135,6 +145,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
.then((document) => {
setForm({
vendorId: document.vendorId,
projectId: document.projectId,
status: document.status,
issueDate: document.issueDate,
taxPercent: document.taxPercent,
@@ -341,6 +352,19 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
<input type="date" value={form.issueDate.slice(0, 10)} onChange={(event) => updateField("issueDate", new Date(event.target.value).toISOString())} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3 text-sm">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Linked Project</div>
<div className="mt-2 font-semibold text-text">
{mode === "edit"
? (form.projectId ? "Project context saved on this purchase order." : "No project linked.")
: (seededProjectId ? `${seededProjectNumber || "Project"}${seededProjectName ? ` - ${seededProjectName}` : ""}` : "Will auto-link from sales-order demand when possible.")}
</div>
<div className="mt-1 text-xs text-muted">
{mode === "edit"
? "This header link is used for downstream project cockpit and finance rollups."
: "Generated purchasing from a project-linked sales order will carry project context automatically."}
</div>
</div>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Notes</span>
<textarea value={form.notes} onChange={(event) => updateField("notes", event.target.value)} rows={3} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />

View File

@@ -22,6 +22,7 @@ export const purchaseStatusPalette: Record<PurchaseOrderStatus, string> = {
export const emptyPurchaseOrderInput: PurchaseOrderInput = {
vendorId: "",
projectId: null,
status: "DRAFT",
issueDate: new Date().toISOString(),
taxPercent: 0,

View File

@@ -173,6 +173,9 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
status: "DRAFT",
notes: `Generated from sales order ${activeDocument.documentNumber} demand planning.`,
});
if (activeDocument.linkedProjectId) {
params.set("projectId", activeDocument.linkedProjectId);
}
return `/manufacturing/work-orders/new?${params.toString()}`;
}
@@ -186,6 +189,15 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
if (vendorId) {
params.set("vendorId", vendorId);
}
if (activeDocument.linkedProjectId) {
params.set("projectId", activeDocument.linkedProjectId);
}
if (activeDocument.linkedProjectNumber) {
params.set("projectNumber", activeDocument.linkedProjectNumber);
}
if (activeDocument.linkedProjectName) {
params.set("projectName", activeDocument.linkedProjectName);
}
return `/purchasing/orders/new?${params.toString()}`;
}
@@ -521,7 +533,18 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
</dl>
</article>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project Link</p>
{activeDocument.linkedProjectId ? (
<div className="mt-3 space-y-2">
<Link to={`/projects/${activeDocument.linkedProjectId}`} className="inline-flex items-center rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text hover:bg-page/70">
{activeDocument.linkedProjectNumber} / {activeDocument.linkedProjectName}
</Link>
<p className="text-sm text-muted">This {entity === "quote" ? "quote" : "sales order"} is already linked to a project, and downstream WO/PO launches will carry that project context.</p>
</div>
) : (
<p className="mt-3 text-sm text-muted">No linked project is currently attached to this {entity === "quote" ? "quote" : "sales order"}.</p>
)}
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{activeDocument.notes || "No notes recorded for this document."}</p>
</article>
</div>