cost and price

This commit is contained in:
2026-03-14 23:23:43 -05:00
parent 9d233a0c3d
commit c21f7c2cee
10 changed files with 33 additions and 0 deletions

View File

@@ -156,6 +156,10 @@ export function InventoryDetailPage() {
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Default cost</dt>
<dd className="mt-2 text-sm text-text">{item.defaultCost == null ? "Not set" : `$${item.defaultCost.toFixed(2)}`}</dd>
</div>
<div>
<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">Flags</dt>
<dd className="mt-2 text-sm text-text">

View File

@@ -71,6 +71,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
isSellable: item.isSellable,
isPurchasable: item.isPurchasable,
defaultCost: item.defaultCost,
defaultPrice: item.defaultPrice,
notes: item.notes,
bomLines: item.bomLines.map((line) => ({
componentItemId: line.componentItemId,
@@ -199,6 +200,17 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Default price</span>
<input
type="number"
min={0}
step={0.01}
value={form.defaultPrice ?? ""}
onChange={(event) => updateField("defaultPrice", event.target.value === "" ? null : Number(event.target.value))}
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="grid gap-3 xl:grid-cols-4">
<label className="block">

View File

@@ -32,6 +32,7 @@ export const emptyInventoryItemInput: InventoryItemInput = {
isSellable: true,
isPurchasable: true,
defaultCost: null,
defaultPrice: null,
notes: "",
bomLines: [],
};

View File

@@ -321,6 +321,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
...line,
itemId: option.id,
description: line.description || option.name,
unitPrice: line.unitPrice > 0 ? line.unitPrice : (option.defaultPrice ?? 0),
});
updateLineSearchTerm(index, option.sku);
setActiveLinePicker(null);

View File

@@ -0,0 +1 @@
ALTER TABLE "InventoryItem" ADD COLUMN "defaultPrice" REAL;

View File

@@ -113,6 +113,7 @@ model InventoryItem {
isSellable Boolean @default(true)
isPurchasable Boolean @default(true)
defaultCost Float?
defaultPrice Float?
notes String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -176,6 +176,7 @@ export async function bootstrapAppData() {
isSellable: false,
isPurchasable: true,
defaultCost: 42.5,
defaultPrice: null,
notes: "Primary sheet stock for enclosure fabrication.",
},
});
@@ -191,6 +192,7 @@ export async function bootstrapAppData() {
isSellable: false,
isPurchasable: true,
defaultCost: 0.18,
defaultPrice: null,
notes: "Bulk hardware item.",
},
});
@@ -206,6 +208,7 @@ export async function bootstrapAppData() {
isSellable: true,
isPurchasable: false,
defaultCost: 118,
defaultPrice: 249,
notes: "Starter BOM for the inventory foundation slice.",
},
});

View File

@@ -37,6 +37,7 @@ const inventoryItemSchema = z.object({
isSellable: z.boolean(),
isPurchasable: z.boolean(),
defaultCost: z.number().nonnegative().nullable(),
defaultPrice: z.number().nonnegative().nullable(),
notes: z.string(),
bomLines: z.array(bomLineSchema),
});

View File

@@ -45,6 +45,7 @@ type InventoryDetailRecord = {
isSellable: boolean;
isPurchasable: boolean;
defaultCost: number | null;
defaultPrice: number | null;
notes: string;
createdAt: Date;
updatedAt: Date;
@@ -220,6 +221,7 @@ function mapDetail(record: InventoryDetailRecord): InventoryItemDetailDto {
}),
description: record.description,
defaultCost: record.defaultCost,
defaultPrice: record.defaultPrice,
notes: record.notes,
createdAt: record.createdAt.toISOString(),
bomLines: record.bomLines.slice().sort((a, b) => a.position - b.position).map(mapBomLine),
@@ -371,6 +373,7 @@ export async function listInventoryItemOptions() {
id: true,
sku: true,
name: true,
defaultPrice: true,
},
orderBy: [{ sku: "asc" }],
});
@@ -379,6 +382,7 @@ export async function listInventoryItemOptions() {
id: item.id,
sku: item.sku,
name: item.name,
defaultPrice: item.defaultPrice,
}));
}
@@ -517,6 +521,7 @@ export async function createInventoryItem(payload: InventoryItemInput) {
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: validatedBom.bomLines.length
? {
@@ -558,6 +563,7 @@ export async function updateInventoryItem(itemId: string, payload: InventoryItem
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: {
deleteMany: {},

View File

@@ -31,6 +31,7 @@ export interface InventoryItemOptionDto {
id: string;
sku: string;
name: string;
defaultPrice: number | null;
}
export interface WarehouseLocationOptionDto {
@@ -128,6 +129,7 @@ export interface InventoryTransactionInput {
export interface InventoryItemDetailDto extends InventoryItemSummaryDto {
description: string;
defaultCost: number | null;
defaultPrice: number | null;
notes: string;
createdAt: string;
bomLines: InventoryBomLineDto[];
@@ -146,6 +148,7 @@ export interface InventoryItemInput {
isSellable: boolean;
isPurchasable: boolean;
defaultCost: number | null;
defaultPrice: number | null;
notes: string;
bomLines: InventoryBomLineInput[];
}