This commit is contained in:
2026-03-23 16:56:23 -05:00
parent 1f0986a94d
commit ee26ffe75c
6 changed files with 128 additions and 38 deletions

View File

@@ -11,6 +11,12 @@ type ParsedLine = {
amount: number;
};
type RelationalOrderLine = {
partId: number;
quantity: number;
amount: number;
};
type ParsedFulfillmentLine = {
sku: string;
quantity: number;
@@ -55,6 +61,44 @@ function parseLines(raw: string): ParsedLine[] {
});
}
function parseRelationalOrderLines(raw: string): RelationalOrderLine[] {
try {
const parsed = JSON.parse(raw) as Array<Record<string, unknown>>;
if (!Array.isArray(parsed) || parsed.length === 0) {
throw new Error("Order must contain at least one line.");
}
const lines = parsed.map((line) => {
const partId = Number(line.partId);
const quantity = Number(line.quantity);
const amount = Number(line.amount);
if (!Number.isInteger(partId) || partId <= 0) {
throw new Error("Invalid item selection.");
}
if (!Number.isFinite(quantity) || quantity <= 0) {
throw new Error("Invalid line quantity.");
}
if (!Number.isFinite(amount) || amount < 0) {
throw new Error("Invalid line amount.");
}
return { partId, quantity, amount };
});
const uniquePartIds = new Set(lines.map((line) => line.partId));
if (uniquePartIds.size !== lines.length) {
throw new Error("Each inventory item can only appear once per order.");
}
return lines;
} catch {
throw new Error("Invalid relational order payload.");
}
}
function parseFulfillmentLines(raw: string): ParsedFulfillmentLine[] {
return raw
.split(/\r?\n/)
@@ -85,6 +129,18 @@ function getPartIdBySku(sku: string) {
return row.id;
}
function getExistingPart(partId: number) {
const row = db()
.prepare(`SELECT id FROM parts WHERE id = ?`)
.get(partId) as { id: number } | undefined;
if (!row) {
throw new Error(`Selected inventory item ${partId} does not exist.`);
}
return row.id;
}
function getOrderNumber(prefix: string, table: "sales_orders" | "purchase_orders") {
const row = db().prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number };
return `${prefix}-${String((row.count ?? 0) + 1).padStart(5, "0")}`;
@@ -399,12 +455,19 @@ export async function createVendor(formData: FormData) {
}
export async function createSalesOrder(formData: FormData) {
const customerCode = getText(formData, "customerCode");
const lines = parseLines(getText(formData, "lines"));
const customerRow = db().prepare(`SELECT id FROM customers WHERE code = ?`).get(customerCode) as { id: number } | undefined;
const customerId = Number(getText(formData, "customerId"));
const relationalLinesPayload = getText(formData, "lineItems");
const lines = relationalLinesPayload
? parseRelationalOrderLines(relationalLinesPayload)
: parseLines(getText(formData, "lines")).map((line) => ({
partId: getPartIdBySku(line.sku),
quantity: line.quantity,
amount: line.amount
}));
const customerRow = db().prepare(`SELECT id FROM customers WHERE id = ?`).get(customerId) as { id: number } | undefined;
if (!customerRow) {
throw new Error(`Customer "${customerCode}" does not exist.`);
throw new Error("Selected customer does not exist.");
}
const tx = db().transaction(() => {
@@ -426,7 +489,7 @@ export async function createSalesOrder(formData: FormData) {
);
for (const line of lines) {
insertLine.run(orderId, getPartIdBySku(line.sku), line.quantity, line.amount);
insertLine.run(orderId, getExistingPart(line.partId), line.quantity, line.amount);
}
});
@@ -567,12 +630,19 @@ export async function shipSalesOrder(formData: FormData) {
}
export async function createPurchaseOrder(formData: FormData) {
const vendorCode = getText(formData, "vendorCode");
const lines = parseLines(getText(formData, "lines"));
const vendorRow = db().prepare(`SELECT id FROM vendors WHERE code = ?`).get(vendorCode) as { id: number } | undefined;
const vendorId = Number(getText(formData, "vendorId"));
const relationalLinesPayload = getText(formData, "lineItems");
const lines = relationalLinesPayload
? parseRelationalOrderLines(relationalLinesPayload)
: parseLines(getText(formData, "lines")).map((line) => ({
partId: getPartIdBySku(line.sku),
quantity: line.quantity,
amount: line.amount
}));
const vendorRow = db().prepare(`SELECT id FROM vendors WHERE id = ?`).get(vendorId) as { id: number } | undefined;
if (!vendorRow) {
throw new Error(`Vendor "${vendorCode}" does not exist.`);
throw new Error("Selected vendor does not exist.");
}
const tx = db().transaction(() => {
@@ -594,7 +664,7 @@ export async function createPurchaseOrder(formData: FormData) {
);
for (const line of lines) {
insertLine.run(orderId, getPartIdBySku(line.sku), line.quantity, line.amount);
insertLine.run(orderId, getExistingPart(line.partId), line.quantity, line.amount);
}
});

View File

@@ -8,6 +8,7 @@ import type {
JournalEntryRow,
KitRow,
LowStockRow,
OrderItemOption,
PartRow,
PurchaseOrderListRow,
SalesOrderListRow,
@@ -94,6 +95,28 @@ export function getParts(): PartRow[] {
.all() as PartRow[];
}
export function getOrderItemOptions(): OrderItemOption[] {
return db()
.prepare(
`
SELECT
p.id,
p.sku,
p.name,
p.kind,
COALESCE(ib.quantity_on_hand, 0) AS quantityOnHand,
p.sale_price AS salePrice,
p.unit_cost AS unitCost,
p.unit_of_measure AS unitOfMeasure
FROM parts p
LEFT JOIN inventory_balances ib ON ib.part_id = p.id
WHERE p.is_active = 1
ORDER BY p.kind DESC, p.sku ASC
`
)
.all() as OrderItemOption[];
}
export function getAssembliesWithComponents(): KitRow[] {
return db()
.prepare(

View File

@@ -41,6 +41,17 @@ export type ContactRow = {
phone: string | null;
};
export type OrderItemOption = {
id: number;
sku: string;
name: string;
kind: "part" | "assembly";
quantityOnHand: number;
salePrice: number;
unitCost: number;
unitOfMeasure: string;
};
export type SalesOrderListRow = {
id: number;
orderNumber: string;