all
This commit is contained in:
153
components/purchase-order-create-form.tsx
Normal file
153
components/purchase-order-create-form.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createPurchaseOrder } from "@/lib/actions";
|
||||
import type { ContactRow, OrderItemOption } from "@/lib/types";
|
||||
|
||||
type PurchaseOrderCreateFormProps = {
|
||||
vendors: ContactRow[];
|
||||
items: OrderItemOption[];
|
||||
};
|
||||
|
||||
type DraftLine = {
|
||||
rowId: number;
|
||||
partId: number | "";
|
||||
quantity: number;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
function createEmptyLine(rowId: number): DraftLine {
|
||||
return {
|
||||
rowId,
|
||||
partId: "",
|
||||
quantity: 1,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function PurchaseOrderCreateForm({ vendors, items }: PurchaseOrderCreateFormProps) {
|
||||
const [lines, setLines] = useState<DraftLine[]>([createEmptyLine(1)]);
|
||||
|
||||
function updateLine(rowId: number, patch: Partial<DraftLine>) {
|
||||
setLines((current) =>
|
||||
current.map((line) => {
|
||||
if (line.rowId !== rowId) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const nextLine = { ...line, ...patch };
|
||||
if (patch.partId && typeof patch.partId === "number") {
|
||||
const item = items.find((candidate) => candidate.id === patch.partId);
|
||||
if (item) {
|
||||
nextLine.amount = item.unitCost;
|
||||
}
|
||||
}
|
||||
return nextLine;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function addLine() {
|
||||
setLines((current) => [...current, createEmptyLine(Date.now())]);
|
||||
}
|
||||
|
||||
function removeLine(rowId: number) {
|
||||
setLines((current) => (current.length === 1 ? current : current.filter((line) => line.rowId !== rowId)));
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(
|
||||
lines
|
||||
.filter((line) => typeof line.partId === "number")
|
||||
.map((line) => ({
|
||||
partId: line.partId,
|
||||
quantity: line.quantity,
|
||||
amount: line.amount
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={createPurchaseOrder} className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor="vendorId">Vendor</label>
|
||||
<select className="select" id="vendorId" name="vendorId">
|
||||
{vendors.map((vendor) => (
|
||||
<option key={vendor.id} value={vendor.id}>
|
||||
{vendor.code} - {vendor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>Line Items</label>
|
||||
<div className="grid">
|
||||
{lines.map((line, index) => (
|
||||
<div className="panel" key={line.rowId}>
|
||||
<div className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor={`po-part-${line.rowId}`}>Item {index + 1}</label>
|
||||
<select
|
||||
className="select"
|
||||
id={`po-part-${line.rowId}`}
|
||||
value={line.partId}
|
||||
onChange={(event) =>
|
||||
updateLine(line.rowId, {
|
||||
partId: event.target.value ? Number(event.target.value) : ""
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Select inventory item</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.sku} - {item.name} ({item.quantityOnHand} {item.unitOfMeasure} on hand)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`po-qty-${line.rowId}`}>Quantity</label>
|
||||
<input
|
||||
className="input"
|
||||
id={`po-qty-${line.rowId}`}
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
value={line.quantity}
|
||||
onChange={(event) => updateLine(line.rowId, { quantity: Number(event.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`po-cost-${line.rowId}`}>Unit Cost</label>
|
||||
<input
|
||||
className="input"
|
||||
id={`po-cost-${line.rowId}`}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={line.amount}
|
||||
onChange={(event) => updateLine(line.rowId, { amount: Number(event.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<button className="button secondary" type="button" onClick={() => removeLine(line.rowId)}>
|
||||
Remove Line
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<button className="button secondary" type="button" onClick={addLine}>
|
||||
Add Line
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="lineItems" value={payload} />
|
||||
<div className="form-row">
|
||||
<label htmlFor="purchase-notes">Notes</label>
|
||||
<textarea className="textarea" id="purchase-notes" name="notes" />
|
||||
</div>
|
||||
<button className="button" type="submit">
|
||||
Save Purchase Order
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
124
components/purchase-order-form.tsx
Normal file
124
components/purchase-order-form.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createPurchaseOrder } from "@/lib/actions";
|
||||
import type { ContactRow, OrderItemOption } from "@/lib/types";
|
||||
|
||||
type PurchaseOrderFormProps = {
|
||||
vendors: ContactRow[];
|
||||
items: OrderItemOption[];
|
||||
};
|
||||
|
||||
type DraftLine = {
|
||||
rowId: number;
|
||||
partId: number | "";
|
||||
quantity: number;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
function createEmptyLine(rowId: number): DraftLine {
|
||||
return { rowId, partId: "", quantity: 1, amount: 0 };
|
||||
}
|
||||
|
||||
export function PurchaseOrderForm({ vendors, items }: PurchaseOrderFormProps) {
|
||||
const [lines, setLines] = useState<DraftLine[]>([createEmptyLine(1)]);
|
||||
|
||||
function updateLine(rowId: number, patch: Partial<DraftLine>) {
|
||||
setLines((current) =>
|
||||
current.map((line) => {
|
||||
if (line.rowId !== rowId) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const nextLine = { ...line, ...patch };
|
||||
if (patch.partId && typeof patch.partId === "number") {
|
||||
const item = items.find((candidate) => candidate.id === patch.partId);
|
||||
if (item) {
|
||||
nextLine.amount = item.unitCost;
|
||||
}
|
||||
}
|
||||
return nextLine;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function addLine() {
|
||||
setLines((current) => [...current, createEmptyLine(Date.now())]);
|
||||
}
|
||||
|
||||
function removeLine(rowId: number) {
|
||||
setLines((current) => (current.length === 1 ? current : current.filter((line) => line.rowId !== rowId)));
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(
|
||||
lines
|
||||
.filter((line) => typeof line.partId === "number")
|
||||
.map((line) => ({ partId: line.partId, quantity: line.quantity, amount: line.amount }))
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={createPurchaseOrder} className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor="vendorId">Vendor</label>
|
||||
<select className="select" id="vendorId" name="vendorId">
|
||||
{vendors.map((vendor) => (
|
||||
<option key={vendor.id} value={vendor.id}>
|
||||
{vendor.code} - {vendor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>Line Items</label>
|
||||
<div className="grid">
|
||||
{lines.map((line, index) => (
|
||||
<div className="panel" key={line.rowId}>
|
||||
<div className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor={`po-part-${line.rowId}`}>Item {index + 1}</label>
|
||||
<select
|
||||
className="select"
|
||||
id={`po-part-${line.rowId}`}
|
||||
value={line.partId}
|
||||
onChange={(event) => updateLine(line.rowId, { partId: event.target.value ? Number(event.target.value) : "" })}
|
||||
>
|
||||
<option value="">Select inventory item</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.sku} - {item.name} ({item.quantityOnHand} {item.unitOfMeasure} on hand)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`po-qty-${line.rowId}`}>Quantity</label>
|
||||
<input className="input" id={`po-qty-${line.rowId}`} type="number" min="0.01" step="0.01" value={line.quantity} onChange={(event) => updateLine(line.rowId, { quantity: Number(event.target.value) || 0 })} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`po-cost-${line.rowId}`}>Unit Cost</label>
|
||||
<input className="input" id={`po-cost-${line.rowId}`} type="number" min="0" step="0.01" value={line.amount} onChange={(event) => updateLine(line.rowId, { amount: Number(event.target.value) || 0 })} />
|
||||
</div>
|
||||
<button className="button secondary" type="button" onClick={() => removeLine(line.rowId)}>
|
||||
Remove Line
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<button className="button secondary" type="button" onClick={addLine}>
|
||||
Add Line
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="lineItems" value={payload} />
|
||||
<div className="form-row">
|
||||
<label htmlFor="purchase-notes">Notes</label>
|
||||
<textarea className="textarea" id="purchase-notes" name="notes" />
|
||||
</div>
|
||||
<button className="button" type="submit">
|
||||
Save Purchase Order
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
153
components/sales-order-create-form.tsx
Normal file
153
components/sales-order-create-form.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createSalesOrder } from "@/lib/actions";
|
||||
import type { ContactRow, OrderItemOption } from "@/lib/types";
|
||||
|
||||
type SalesOrderCreateFormProps = {
|
||||
customers: ContactRow[];
|
||||
items: OrderItemOption[];
|
||||
};
|
||||
|
||||
type DraftLine = {
|
||||
rowId: number;
|
||||
partId: number | "";
|
||||
quantity: number;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
function createEmptyLine(rowId: number): DraftLine {
|
||||
return {
|
||||
rowId,
|
||||
partId: "",
|
||||
quantity: 1,
|
||||
amount: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function SalesOrderCreateForm({ customers, items }: SalesOrderCreateFormProps) {
|
||||
const [lines, setLines] = useState<DraftLine[]>([createEmptyLine(1)]);
|
||||
|
||||
function updateLine(rowId: number, patch: Partial<DraftLine>) {
|
||||
setLines((current) =>
|
||||
current.map((line) => {
|
||||
if (line.rowId !== rowId) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const nextLine = { ...line, ...patch };
|
||||
if (patch.partId && typeof patch.partId === "number") {
|
||||
const item = items.find((candidate) => candidate.id === patch.partId);
|
||||
if (item) {
|
||||
nextLine.amount = item.salePrice;
|
||||
}
|
||||
}
|
||||
return nextLine;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function addLine() {
|
||||
setLines((current) => [...current, createEmptyLine(Date.now())]);
|
||||
}
|
||||
|
||||
function removeLine(rowId: number) {
|
||||
setLines((current) => (current.length === 1 ? current : current.filter((line) => line.rowId !== rowId)));
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(
|
||||
lines
|
||||
.filter((line) => typeof line.partId === "number")
|
||||
.map((line) => ({
|
||||
partId: line.partId,
|
||||
quantity: line.quantity,
|
||||
amount: line.amount
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={createSalesOrder} className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor="customerId">Customer</label>
|
||||
<select className="select" id="customerId" name="customerId">
|
||||
{customers.map((customer) => (
|
||||
<option key={customer.id} value={customer.id}>
|
||||
{customer.code} - {customer.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>Line Items</label>
|
||||
<div className="grid">
|
||||
{lines.map((line, index) => (
|
||||
<div className="panel" key={line.rowId}>
|
||||
<div className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor={`so-part-${line.rowId}`}>Item {index + 1}</label>
|
||||
<select
|
||||
className="select"
|
||||
id={`so-part-${line.rowId}`}
|
||||
value={line.partId}
|
||||
onChange={(event) =>
|
||||
updateLine(line.rowId, {
|
||||
partId: event.target.value ? Number(event.target.value) : ""
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Select inventory item</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.sku} - {item.name} ({item.quantityOnHand} {item.unitOfMeasure} on hand)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`so-qty-${line.rowId}`}>Quantity</label>
|
||||
<input
|
||||
className="input"
|
||||
id={`so-qty-${line.rowId}`}
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
value={line.quantity}
|
||||
onChange={(event) => updateLine(line.rowId, { quantity: Number(event.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`so-price-${line.rowId}`}>Unit Price</label>
|
||||
<input
|
||||
className="input"
|
||||
id={`so-price-${line.rowId}`}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={line.amount}
|
||||
onChange={(event) => updateLine(line.rowId, { amount: Number(event.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<button className="button secondary" type="button" onClick={() => removeLine(line.rowId)}>
|
||||
Remove Line
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<button className="button secondary" type="button" onClick={addLine}>
|
||||
Add Line
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="lineItems" value={payload} />
|
||||
<div className="form-row">
|
||||
<label htmlFor="sales-notes">Notes</label>
|
||||
<textarea className="textarea" id="sales-notes" name="notes" />
|
||||
</div>
|
||||
<button className="button" type="submit">
|
||||
Save Sales Order
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
124
components/sales-order-form.tsx
Normal file
124
components/sales-order-form.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createSalesOrder } from "@/lib/actions";
|
||||
import type { ContactRow, OrderItemOption } from "@/lib/types";
|
||||
|
||||
type SalesOrderFormProps = {
|
||||
customers: ContactRow[];
|
||||
items: OrderItemOption[];
|
||||
};
|
||||
|
||||
type DraftLine = {
|
||||
rowId: number;
|
||||
partId: number | "";
|
||||
quantity: number;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
function createEmptyLine(rowId: number): DraftLine {
|
||||
return { rowId, partId: "", quantity: 1, amount: 0 };
|
||||
}
|
||||
|
||||
export function SalesOrderForm({ customers, items }: SalesOrderFormProps) {
|
||||
const [lines, setLines] = useState<DraftLine[]>([createEmptyLine(1)]);
|
||||
|
||||
function updateLine(rowId: number, patch: Partial<DraftLine>) {
|
||||
setLines((current) =>
|
||||
current.map((line) => {
|
||||
if (line.rowId !== rowId) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const nextLine = { ...line, ...patch };
|
||||
if (patch.partId && typeof patch.partId === "number") {
|
||||
const item = items.find((candidate) => candidate.id === patch.partId);
|
||||
if (item) {
|
||||
nextLine.amount = item.salePrice;
|
||||
}
|
||||
}
|
||||
return nextLine;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function addLine() {
|
||||
setLines((current) => [...current, createEmptyLine(Date.now())]);
|
||||
}
|
||||
|
||||
function removeLine(rowId: number) {
|
||||
setLines((current) => (current.length === 1 ? current : current.filter((line) => line.rowId !== rowId)));
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(
|
||||
lines
|
||||
.filter((line) => typeof line.partId === "number")
|
||||
.map((line) => ({ partId: line.partId, quantity: line.quantity, amount: line.amount }))
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={createSalesOrder} className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor="customerId">Customer</label>
|
||||
<select className="select" id="customerId" name="customerId">
|
||||
{customers.map((customer) => (
|
||||
<option key={customer.id} value={customer.id}>
|
||||
{customer.code} - {customer.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>Line Items</label>
|
||||
<div className="grid">
|
||||
{lines.map((line, index) => (
|
||||
<div className="panel" key={line.rowId}>
|
||||
<div className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor={`so-part-${line.rowId}`}>Item {index + 1}</label>
|
||||
<select
|
||||
className="select"
|
||||
id={`so-part-${line.rowId}`}
|
||||
value={line.partId}
|
||||
onChange={(event) => updateLine(line.rowId, { partId: event.target.value ? Number(event.target.value) : "" })}
|
||||
>
|
||||
<option value="">Select inventory item</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.sku} - {item.name} ({item.quantityOnHand} {item.unitOfMeasure} on hand)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`so-qty-${line.rowId}`}>Quantity</label>
|
||||
<input className="input" id={`so-qty-${line.rowId}`} type="number" min="0.01" step="0.01" value={line.quantity} onChange={(event) => updateLine(line.rowId, { quantity: Number(event.target.value) || 0 })} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor={`so-price-${line.rowId}`}>Unit Price</label>
|
||||
<input className="input" id={`so-price-${line.rowId}`} type="number" min="0" step="0.01" value={line.amount} onChange={(event) => updateLine(line.rowId, { amount: Number(event.target.value) || 0 })} />
|
||||
</div>
|
||||
<button className="button secondary" type="button" onClick={() => removeLine(line.rowId)}>
|
||||
Remove Line
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<button className="button secondary" type="button" onClick={addLine}>
|
||||
Add Line
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="lineItems" value={payload} />
|
||||
<div className="form-row">
|
||||
<label htmlFor="sales-notes">Notes</label>
|
||||
<textarea className="textarea" id="sales-notes" name="notes" />
|
||||
</div>
|
||||
<button className="button" type="submit">
|
||||
Save Sales Order
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user