fixes
This commit is contained in:
104
lib/actions.ts
104
lib/actions.ts
@@ -22,6 +22,11 @@ type ParsedFulfillmentLine = {
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
type RelationalFulfillmentLine = {
|
||||
lineId: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
function db() {
|
||||
return getDb();
|
||||
}
|
||||
@@ -121,6 +126,32 @@ function parseFulfillmentLines(raw: string): ParsedFulfillmentLine[] {
|
||||
});
|
||||
}
|
||||
|
||||
function parseRelationalFulfillmentLines(raw: string): RelationalFulfillmentLine[] {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Array<Record<string, unknown>>;
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||
throw new Error("No fulfillment lines selected.");
|
||||
}
|
||||
|
||||
return parsed.map((line) => {
|
||||
const lineId = Number(line.lineId);
|
||||
const quantity = Number(line.quantity);
|
||||
|
||||
if (!Number.isInteger(lineId) || lineId <= 0) {
|
||||
throw new Error("Invalid line selection.");
|
||||
}
|
||||
|
||||
if (!Number.isFinite(quantity) || quantity <= 0) {
|
||||
throw new Error("Invalid fulfillment quantity.");
|
||||
}
|
||||
|
||||
return { lineId, quantity };
|
||||
});
|
||||
} catch {
|
||||
throw new Error("Invalid relational fulfillment payload.");
|
||||
}
|
||||
}
|
||||
|
||||
function getPartIdBySku(sku: string) {
|
||||
const row = db().prepare(`SELECT id FROM parts WHERE sku = ?`).get(sku) as { id: number } | undefined;
|
||||
if (!row) {
|
||||
@@ -274,8 +305,12 @@ export async function buildAssembly(formData: FormData) {
|
||||
const assemblySku = getText(formData, "assemblySku");
|
||||
const buildQuantity = getNumber(formData, "quantity");
|
||||
|
||||
if (!assemblySku) {
|
||||
redirect("/assemblies?error=Select an assembly before building.");
|
||||
}
|
||||
|
||||
if (buildQuantity <= 0) {
|
||||
throw new Error("Build quantity must be greater than zero.");
|
||||
redirect("/assemblies?error=Build quantity must be greater than zero.");
|
||||
}
|
||||
|
||||
const assemblyId = getPartIdBySku(assemblySku);
|
||||
@@ -304,13 +339,13 @@ export async function buildAssembly(formData: FormData) {
|
||||
}>;
|
||||
|
||||
if (components.length === 0) {
|
||||
throw new Error("Assembly has no bill of materials defined.");
|
||||
redirect("/assemblies?error=Assembly has no bill of materials defined.");
|
||||
}
|
||||
|
||||
for (const component of components) {
|
||||
const needed = component.componentQuantity * buildQuantity;
|
||||
if (component.quantityOnHand < needed) {
|
||||
throw new Error(`Not enough stock for component ${component.sku}. Need ${needed}, have ${component.quantityOnHand}.`);
|
||||
redirect(`/assemblies?error=${encodeURIComponent(`Not enough stock for component ${component.sku}. Need ${needed}, have ${component.quantityOnHand}.`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +389,7 @@ export async function buildAssembly(formData: FormData) {
|
||||
revalidatePath("/");
|
||||
revalidatePath("/parts");
|
||||
revalidatePath("/assemblies");
|
||||
redirect(`/assemblies?success=${encodeURIComponent(`Built ${buildQuantity} of ${assemblySku}.`)}`);
|
||||
}
|
||||
|
||||
export async function recordAdjustment(formData: FormData) {
|
||||
@@ -548,18 +584,18 @@ export async function shipSalesOrder(formData: FormData) {
|
||||
throw new Error("Sales order has no lines.");
|
||||
}
|
||||
|
||||
const requestedLines = parseFulfillmentLines(getText(formData, "lines"));
|
||||
const fulfilledLines = requestedLines.length
|
||||
? requestedLines.map((request) => {
|
||||
const matchingLine = orderLines.find((line) => line.sku === request.sku);
|
||||
const relationalPayload = getText(formData, "fulfillmentLines");
|
||||
const fulfilledLines = relationalPayload
|
||||
? parseRelationalFulfillmentLines(relationalPayload).map((request) => {
|
||||
const matchingLine = orderLines.find((line) => line.lineId === request.lineId);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this sales order.`);
|
||||
throw new Error("Selected sales order line is invalid.");
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.shippedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot ship ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
throw new Error(`Cannot ship ${request.quantity} of ${matchingLine.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
if (matchingLine.quantityOnHand < request.quantity) {
|
||||
@@ -568,7 +604,26 @@ export async function shipSalesOrder(formData: FormData) {
|
||||
|
||||
return { ...matchingLine, shipQuantity: request.quantity };
|
||||
})
|
||||
: orderLines
|
||||
: parseFulfillmentLines(getText(formData, "lines")).length
|
||||
? parseFulfillmentLines(getText(formData, "lines")).map((request) => {
|
||||
const matchingLine = orderLines.find((line) => line.sku === request.sku);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this sales order.`);
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.shippedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot ship ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
if (matchingLine.quantityOnHand < request.quantity) {
|
||||
throw new Error(`Insufficient stock for ${matchingLine.sku}. Need ${request.quantity}, have ${matchingLine.quantityOnHand}.`);
|
||||
}
|
||||
|
||||
return { ...matchingLine, shipQuantity: request.quantity };
|
||||
})
|
||||
: orderLines
|
||||
.map((line) => {
|
||||
const remaining = line.quantity - line.shippedQuantity;
|
||||
return remaining > 0 ? { ...line, shipQuantity: remaining } : null;
|
||||
@@ -718,23 +773,38 @@ export async function receivePurchaseOrder(formData: FormData) {
|
||||
throw new Error("Purchase order has no lines.");
|
||||
}
|
||||
|
||||
const requestedLines = parseFulfillmentLines(getText(formData, "lines"));
|
||||
const fulfilledLines = requestedLines.length
|
||||
? requestedLines.map((request) => {
|
||||
const matchingLine = lines.find((line) => line.sku === request.sku);
|
||||
const relationalPayload = getText(formData, "fulfillmentLines");
|
||||
const fulfilledLines = relationalPayload
|
||||
? parseRelationalFulfillmentLines(relationalPayload).map((request) => {
|
||||
const matchingLine = lines.find((line) => line.lineId === request.lineId);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this purchase order.`);
|
||||
throw new Error("Selected purchase order line is invalid.");
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.receivedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot receive ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
throw new Error(`Cannot receive ${request.quantity} of ${matchingLine.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
return { ...matchingLine, receiveQuantity: request.quantity };
|
||||
})
|
||||
: lines
|
||||
: parseFulfillmentLines(getText(formData, "lines")).length
|
||||
? parseFulfillmentLines(getText(formData, "lines")).map((request) => {
|
||||
const matchingLine = lines.find((line) => line.sku === request.sku);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this purchase order.`);
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.receivedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot receive ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
return { ...matchingLine, receiveQuantity: request.quantity };
|
||||
})
|
||||
: lines
|
||||
.map((line) => {
|
||||
const remaining = line.quantity - line.receivedQuantity;
|
||||
return remaining > 0 ? { ...line, receiveQuantity: remaining } : null;
|
||||
|
||||
@@ -11,7 +11,9 @@ import type {
|
||||
OrderItemOption,
|
||||
PartRow,
|
||||
PurchaseOrderListRow,
|
||||
PurchaseOrderLineDetailRow,
|
||||
SalesOrderListRow,
|
||||
SalesOrderLineDetailRow,
|
||||
VendorBillRow
|
||||
} from "@/lib/types";
|
||||
|
||||
@@ -190,6 +192,54 @@ export function getPurchaseOrders(): PurchaseOrderListRow[] {
|
||||
.all() as PurchaseOrderListRow[];
|
||||
}
|
||||
|
||||
export function getSalesOrderLineDetails(): SalesOrderLineDetailRow[] {
|
||||
return db()
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
sol.id AS lineId,
|
||||
sol.sales_order_id AS salesOrderId,
|
||||
sol.part_id AS partId,
|
||||
p.sku,
|
||||
p.name AS partName,
|
||||
sol.quantity,
|
||||
sol.shipped_quantity AS fulfilledQuantity,
|
||||
sol.quantity - sol.shipped_quantity AS remainingQuantity,
|
||||
sol.unit_price AS unitPrice,
|
||||
COALESCE(ib.quantity_on_hand, 0) AS quantityOnHand,
|
||||
p.unit_of_measure AS unitOfMeasure
|
||||
FROM sales_order_lines sol
|
||||
INNER JOIN parts p ON p.id = sol.part_id
|
||||
LEFT JOIN inventory_balances ib ON ib.part_id = p.id
|
||||
ORDER BY sol.sales_order_id DESC, sol.id ASC
|
||||
`
|
||||
)
|
||||
.all() as SalesOrderLineDetailRow[];
|
||||
}
|
||||
|
||||
export function getPurchaseOrderLineDetails(): PurchaseOrderLineDetailRow[] {
|
||||
return db()
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
pol.id AS lineId,
|
||||
pol.purchase_order_id AS purchaseOrderId,
|
||||
pol.part_id AS partId,
|
||||
p.sku,
|
||||
p.name AS partName,
|
||||
pol.quantity,
|
||||
pol.received_quantity AS fulfilledQuantity,
|
||||
pol.quantity - pol.received_quantity AS remainingQuantity,
|
||||
pol.unit_cost AS unitCost,
|
||||
p.unit_of_measure AS unitOfMeasure
|
||||
FROM purchase_order_lines pol
|
||||
INNER JOIN parts p ON p.id = pol.part_id
|
||||
ORDER BY pol.purchase_order_id DESC, pol.id ASC
|
||||
`
|
||||
)
|
||||
.all() as PurchaseOrderLineDetailRow[];
|
||||
}
|
||||
|
||||
export function getJournalEntries(): JournalEntryRow[] {
|
||||
const entries = db()
|
||||
.prepare(
|
||||
|
||||
27
lib/types.ts
27
lib/types.ts
@@ -141,3 +141,30 @@ export type VendorBillRow = {
|
||||
paidAmount: number;
|
||||
balanceDue: number;
|
||||
};
|
||||
|
||||
export type SalesOrderLineDetailRow = {
|
||||
lineId: number;
|
||||
salesOrderId: number;
|
||||
partId: number;
|
||||
sku: string;
|
||||
partName: string;
|
||||
quantity: number;
|
||||
fulfilledQuantity: number;
|
||||
remainingQuantity: number;
|
||||
unitPrice: number;
|
||||
quantityOnHand: number;
|
||||
unitOfMeasure: string;
|
||||
};
|
||||
|
||||
export type PurchaseOrderLineDetailRow = {
|
||||
lineId: number;
|
||||
purchaseOrderId: number;
|
||||
partId: number;
|
||||
sku: string;
|
||||
partName: string;
|
||||
quantity: number;
|
||||
fulfilledQuantity: number;
|
||||
remainingQuantity: number;
|
||||
unitCost: number;
|
||||
unitOfMeasure: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user