PO logic
This commit is contained in:
@@ -252,6 +252,10 @@ export function InventoryDetailPage() {
|
||||
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Default price</dt>
|
||||
<dd className="mt-2 text-sm text-text">{item.defaultPrice == null ? "Not set" : `$${item.defaultPrice.toFixed(2)}`}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Preferred vendor</dt>
|
||||
<dd className="mt-2 text-sm text-text">{item.preferredVendorName ?? "Not set"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Flags</dt>
|
||||
<dd className="mt-2 text-sm text-text">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PurchaseVendorOptionDto } from "@mrp/shared";
|
||||
import type { InventoryBomLineInput, InventoryItemInput, InventoryItemOperationInput, InventoryItemOptionDto } from "@mrp/shared/dist/inventory/types.js";
|
||||
import type { ManufacturingStationDto } from "@mrp/shared";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -18,8 +19,11 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
const [form, setForm] = useState<InventoryItemInput>(emptyInventoryItemInput);
|
||||
const [componentOptions, setComponentOptions] = useState<InventoryItemOptionDto[]>([]);
|
||||
const [stations, setStations] = useState<ManufacturingStationDto[]>([]);
|
||||
const [vendorOptions, setVendorOptions] = useState<PurchaseVendorOptionDto[]>([]);
|
||||
const [componentSearchTerms, setComponentSearchTerms] = useState<string[]>([]);
|
||||
const [activeComponentPicker, setActiveComponentPicker] = useState<number | null>(null);
|
||||
const [vendorSearchTerm, setVendorSearchTerm] = useState("");
|
||||
const [vendorPickerOpen, setVendorPickerOpen] = useState(false);
|
||||
const [status, setStatus] = useState(mode === "create" ? "Create a new inventory item." : "Loading inventory item...");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
@@ -72,6 +76,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
unitOfMeasure: item.unitOfMeasure,
|
||||
isSellable: item.isSellable,
|
||||
isPurchasable: item.isPurchasable,
|
||||
preferredVendorId: item.preferredVendorId,
|
||||
defaultCost: item.defaultCost,
|
||||
defaultPrice: item.defaultPrice,
|
||||
notes: item.notes,
|
||||
@@ -93,6 +98,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
});
|
||||
setComponentSearchTerms(item.bomLines.map((line) => line.componentSku));
|
||||
setStatus("Inventory item loaded.");
|
||||
setVendorSearchTerm(item.preferredVendorName ?? "");
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const message = error instanceof ApiError ? error.message : "Unable to load inventory item.";
|
||||
@@ -106,12 +112,21 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
}
|
||||
|
||||
api.getManufacturingStations(token).then(setStations).catch(() => setStations([]));
|
||||
api.getPurchaseVendors(token).then(setVendorOptions).catch(() => setVendorOptions([]));
|
||||
}, [token]);
|
||||
|
||||
function updateField<Key extends keyof InventoryItemInput>(key: Key, value: InventoryItemInput[Key]) {
|
||||
setForm((current) => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
function getSelectedVendorName(vendorId: string | null) {
|
||||
if (!vendorId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return vendorOptions.find((vendor) => vendor.id === vendorId)?.name ?? "";
|
||||
}
|
||||
|
||||
function updateBomLine(index: number, nextLine: InventoryBomLineInput) {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
@@ -315,6 +330,76 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label className="block xl:max-w-xl">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Preferred vendor</span>
|
||||
<div className="relative">
|
||||
<input
|
||||
value={vendorSearchTerm}
|
||||
onChange={(event) => {
|
||||
setVendorSearchTerm(event.target.value);
|
||||
updateField("preferredVendorId", null);
|
||||
setVendorPickerOpen(true);
|
||||
}}
|
||||
onFocus={() => setVendorPickerOpen(true)}
|
||||
onBlur={() => {
|
||||
window.setTimeout(() => {
|
||||
setVendorPickerOpen(false);
|
||||
if (form.preferredVendorId) {
|
||||
setVendorSearchTerm(getSelectedVendorName(form.preferredVendorId));
|
||||
}
|
||||
}, 120);
|
||||
}}
|
||||
disabled={!form.isPurchasable}
|
||||
placeholder={form.isPurchasable ? "Search vendor" : "Enable purchasable to assign sourcing"}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
{vendorPickerOpen && form.isPurchasable ? (
|
||||
<div className="absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel">
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
updateField("preferredVendorId", null);
|
||||
setVendorSearchTerm("");
|
||||
setVendorPickerOpen(false);
|
||||
}}
|
||||
className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70"
|
||||
>
|
||||
<div className="font-semibold text-text">No preferred vendor</div>
|
||||
</button>
|
||||
{vendorOptions
|
||||
.filter((vendor) => {
|
||||
const query = vendorSearchTerm.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return vendor.name.toLowerCase().includes(query) || vendor.email.toLowerCase().includes(query);
|
||||
})
|
||||
.slice(0, 12)
|
||||
.map((vendor) => (
|
||||
<button
|
||||
key={vendor.id}
|
||||
type="button"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
updateField("preferredVendorId", vendor.id);
|
||||
setVendorSearchTerm(vendor.name);
|
||||
setVendorPickerOpen(false);
|
||||
}}
|
||||
className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70"
|
||||
>
|
||||
<div className="font-semibold text-text">{vendor.name}</div>
|
||||
<div className="mt-1 text-xs text-muted">{vendor.email}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-muted">
|
||||
{form.preferredVendorId ? getSelectedVendorName(form.preferredVendorId) : "Demand planning uses this vendor when creating buy recommendations."}
|
||||
</div>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Description</span>
|
||||
<textarea
|
||||
|
||||
@@ -41,6 +41,7 @@ export const emptyInventoryItemInput: InventoryItemInput = {
|
||||
unitOfMeasure: "EA",
|
||||
isSellable: true,
|
||||
isPurchasable: true,
|
||||
preferredVendorId: null,
|
||||
defaultCost: null,
|
||||
defaultPrice: null,
|
||||
notes: "",
|
||||
|
||||
Reference in New Issue
Block a user