initial release testing

This commit is contained in:
2026-03-23 16:16:45 -05:00
parent f079fdca62
commit 6659707890
37 changed files with 3374 additions and 37 deletions

92
app/accounting/page.tsx Normal file
View File

@@ -0,0 +1,92 @@
import { createAccount, createManualJournalEntry } from "@/lib/actions";
import { formatCurrency, formatDate } from "@/lib/format";
import { getAccountBalances, getAccounts, getJournalEntries } from "@/lib/repository";
export default function AccountingPage() {
const entries = getJournalEntries();
const balances = getAccountBalances();
const accounts = getAccounts();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Chart of Accounts</h2>
<p className="section-copy">Seeded operational accounts are ready, and you can add your own accounts here.</p>
<form action={createAccount} className="form-grid">
<div className="form-row"><label htmlFor="account-code">Code</label><input className="input" id="account-code" name="code" required /></div>
<div className="form-row"><label htmlFor="account-name">Name</label><input className="input" id="account-name" name="name" required /></div>
<div className="form-row">
<label htmlFor="account-category">Category</label>
<select className="select" id="account-category" name="category" defaultValue="expense">
<option value="asset">Asset</option>
<option value="liability">Liability</option>
<option value="equity">Equity</option>
<option value="revenue">Revenue</option>
<option value="expense">Expense</option>
</select>
</div>
<button className="button secondary" type="submit">Add Account</button>
</form>
</article>
<article className="panel">
<h2 className="section-title">Manual Journal Entry</h2>
<p className="section-copy">Enter one line per row as `account code,debit,credit`. Debits and credits must balance.</p>
<form action={createManualJournalEntry} className="form-grid">
<div className="form-row"><label htmlFor="description">Description</label><input className="input" id="description" name="description" /></div>
<div className="form-row"><label htmlFor="lines">Lines</label><textarea className="textarea" id="lines" name="lines" placeholder={"1000,250,0\n3000,0,250"} required /></div>
<button className="button" type="submit">Post Journal Entry</button>
</form>
<div className="muted">
Available accounts: {accounts.map((account) => `${account.code} ${account.name}`).join(" | ")}
</div>
</article>
</section>
<section className="panel">
<h2 className="section-title">Account Balances</h2>
<p className="section-copy">Balances are rolled up from every journal line currently posted.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Code</th><th>Account</th><th>Category</th><th>Debits</th><th>Credits</th><th>Balance</th></tr></thead>
<tbody>
{balances.map((balance) => (
<tr key={balance.code}>
<td>{balance.code}</td>
<td>{balance.name}</td>
<td>{balance.category}</td>
<td>{formatCurrency(balance.debitTotal)}</td>
<td>{formatCurrency(balance.creditTotal)}</td>
<td>{formatCurrency(balance.balance)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="panel">
<h2 className="section-title">Accounting Journal</h2>
<p className="section-copy">Purchase receipts and sales shipments automatically create operational journal entries.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>When</th><th>Type</th><th>Reference</th><th>Description</th><th>Lines</th></tr></thead>
<tbody>
{entries.length === 0 ? (
<tr><td colSpan={5} className="muted">Journal entries appear after PO receipts and SO shipments.</td></tr>
) : (
entries.map((entry) => (
<tr key={entry.id}>
<td>{formatDate(entry.createdAt)}</td>
<td>{entry.entryType}</td>
<td>{entry.referenceType} #{entry.referenceId ?? "n/a"}</td>
<td>{entry.description}</td>
<td>{entry.lines.map((line, index) => <div key={`${entry.id}-${index}`} className="muted">{line.accountCode} {line.accountName}: {formatCurrency(line.debit)} / {formatCurrency(line.credit)}</div>)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

11
app/api/health/route.ts Normal file
View File

@@ -0,0 +1,11 @@
import { NextResponse } from "next/server";
import { getDashboardStats } from "@/lib/repository";
export function GET() {
return NextResponse.json({
status: "ok",
timestamp: new Date().toISOString(),
modules: ["parts", "assemblies", "sales-orders", "purchase-orders", "customers", "vendors", "accounting"],
stats: getDashboardStats()
});
}

63
app/assemblies/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
import { addKitComponent, buildAssembly } from "@/lib/actions";
import { getAssembliesWithComponents, getParts } from "@/lib/repository";
export default function AssembliesPage() {
const parts = getParts();
const assemblies = parts.filter((part) => part.kind === "assembly");
const components = parts.filter((part) => part.kind === "part");
const kitRows = getAssembliesWithComponents();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Bill of Materials</h2>
<p className="section-copy">Define which stocked parts are consumed to build each assembly.</p>
<form action={addKitComponent} className="form-grid">
<div className="form-row">
<label htmlFor="assemblySku">Assembly SKU</label>
<select className="select" id="assemblySku" name="assemblySku">{assemblies.map((assembly) => <option key={assembly.id} value={assembly.sku}>{assembly.sku} - {assembly.name}</option>)}</select>
</div>
<div className="form-row">
<label htmlFor="componentSku">Component SKU</label>
<select className="select" id="componentSku" name="componentSku">{components.map((component) => <option key={component.id} value={component.sku}>{component.sku} - {component.name}</option>)}</select>
</div>
<div className="form-row"><label htmlFor="component-qty">Quantity Per Assembly</label><input className="input" id="component-qty" name="quantity" type="number" min="0.01" step="0.01" defaultValue="1" /></div>
<button className="button" type="submit">Save Component</button>
</form>
</article>
<article className="panel">
<h2 className="section-title">Build Assembly</h2>
<p className="section-copy">Consume component stock and create finished kit inventory in one transaction flow.</p>
<form action={buildAssembly} className="form-grid">
<div className="form-row">
<label htmlFor="build-assembly">Assembly SKU</label>
<select className="select" id="build-assembly" name="assemblySku">{assemblies.map((assembly) => <option key={assembly.id} value={assembly.sku}>{assembly.sku} - {assembly.name}</option>)}</select>
</div>
<div className="form-row"><label htmlFor="build-qty">Build Quantity</label><input className="input" id="build-qty" name="quantity" type="number" min="1" step="1" defaultValue="1" /></div>
<button className="button secondary" type="submit">Build Now</button>
</form>
</article>
</section>
<section className="panel">
<h2 className="section-title">Current Assemblies</h2>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Assembly</th><th>Name</th><th>Component</th><th>Component Name</th><th>Qty Per</th></tr></thead>
<tbody>
{kitRows.length === 0 ? (
<tr><td colSpan={5} className="muted">Add an assembly on the Parts page, then define its bill of materials here.</td></tr>
) : (
kitRows.map((row, index) => (
<tr key={`${row.assemblySku}-${row.componentSku}-${index}`}>
<td>{row.assemblySku}</td><td>{row.assemblyName}</td><td>{row.componentSku}</td><td>{row.componentName}</td><td>{row.quantity}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

43
app/customers/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
import { createCustomer } from "@/lib/actions";
import { getCustomers } from "@/lib/repository";
export default function CustomersPage() {
const customers = getCustomers();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Add Customer</h2>
<p className="section-copy">Customer records anchor sales orders, shipping, and accounts receivable activity.</p>
<form action={createCustomer} className="form-grid">
<div className="form-row"><label htmlFor="customer-code">Customer Code</label><input className="input" id="customer-code" name="code" required /></div>
<div className="form-row"><label htmlFor="customer-name">Name</label><input className="input" id="customer-name" name="name" required /></div>
<div className="form-row"><label htmlFor="customer-email">Email</label><input className="input" id="customer-email" name="email" type="email" /></div>
<div className="form-row"><label htmlFor="customer-phone">Phone</label><input className="input" id="customer-phone" name="phone" /></div>
<div className="form-row"><label htmlFor="billingAddress">Billing Address</label><textarea className="textarea" id="billingAddress" name="billingAddress" /></div>
<div className="form-row"><label htmlFor="shippingAddress">Shipping Address</label><textarea className="textarea" id="shippingAddress" name="shippingAddress" /></div>
<button className="button" type="submit">Save Customer</button>
</form>
</article>
<article className="panel">
<h2 className="section-title">Customer Directory</h2>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Code</th><th>Name</th><th>Email</th><th>Phone</th></tr></thead>
<tbody>
{customers.length === 0 ? (
<tr><td colSpan={4} className="muted">No customers yet.</td></tr>
) : (
customers.map((customer) => (
<tr key={customer.id}><td>{customer.code}</td><td>{customer.name}</td><td>{customer.email || "—"}</td><td>{customer.phone || "—"}</td></tr>
))
)}
</tbody>
</table>
</div>
</article>
</section>
</div>
);
}

255
app/globals.css Normal file
View File

@@ -0,0 +1,255 @@
:root {
--bg: #f4f1e8;
--surface: rgba(255, 252, 246, 0.88);
--text: #1e1b16;
--muted: #6b6258;
--line: rgba(45, 39, 31, 0.14);
--accent: #a54b1a;
--accent-strong: #7b2d12;
--success: #265d3d;
--warning: #9a6400;
--danger: #8f2718;
--shadow: 0 18px 45px rgba(64, 40, 18, 0.12);
--radius-lg: 24px;
--font-body: "Segoe UI", "Helvetica Neue", sans-serif;
}
* {
box-sizing: border-box;
}
html {
background:
radial-gradient(circle at top left, rgba(215, 116, 57, 0.24), transparent 30%),
radial-gradient(circle at right 20%, rgba(100, 139, 120, 0.18), transparent 25%),
linear-gradient(180deg, #fbf6ee 0%, #efe6d8 100%);
min-height: 100%;
}
body {
margin: 0;
color: var(--text);
font-family: var(--font-body);
min-height: 100vh;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
select,
textarea {
font: inherit;
}
.shell {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.hero,
.sidebar,
.card,
.panel {
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow);
border-radius: var(--radius-lg);
}
.hero {
padding: 28px;
display: grid;
gap: 12px;
}
.hero h1,
.section-title {
margin: 0;
letter-spacing: -0.03em;
}
.muted,
.section-copy,
.hero p {
margin: 0;
color: var(--muted);
}
.layout {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 24px;
margin-top: 24px;
}
.sidebar {
position: sticky;
top: 24px;
align-self: start;
padding: 20px;
}
.brand {
display: grid;
gap: 6px;
margin-bottom: 20px;
}
.brand-mark {
width: 42px;
height: 42px;
border-radius: 14px;
background: linear-gradient(135deg, var(--accent), #d0834d);
}
.nav {
display: grid;
gap: 8px;
}
.nav a {
padding: 11px 14px;
border-radius: 12px;
color: var(--muted);
}
.nav a:hover,
.nav a[data-active="true"] {
color: var(--text);
background: rgba(165, 75, 26, 0.1);
}
.content,
.grid,
.form-grid {
display: grid;
gap: 18px;
}
.grid.cards {
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.card,
.panel {
padding: 22px;
}
.stats-value {
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.04em;
margin-top: 8px;
}
.two-up {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.form-row {
display: grid;
gap: 8px;
}
.form-row label {
font-size: 0.95rem;
font-weight: 600;
}
.input,
.select,
.textarea {
width: 100%;
border: 1px solid rgba(59, 48, 39, 0.16);
border-radius: 12px;
padding: 11px 12px;
background: rgba(255, 252, 247, 0.95);
}
.textarea {
min-height: 120px;
resize: vertical;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 999px;
padding: 11px 18px;
background: var(--accent);
color: white;
font-weight: 700;
cursor: pointer;
}
.button.secondary {
background: rgba(165, 75, 26, 0.12);
color: var(--accent-strong);
}
.table-wrap {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
text-align: left;
padding: 12px 10px;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
.table th {
color: var(--muted);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.pill {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
background: rgba(38, 93, 61, 0.12);
color: var(--success);
}
.pill.warning {
background: rgba(154, 100, 0, 0.12);
color: var(--warning);
}
@media (max-width: 980px) {
.layout,
.two-up {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
}
}

51
app/invoices/page.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { receiveCustomerPayment } from "@/lib/actions";
import { formatCurrency, formatDate } from "@/lib/format";
import { getInvoices } from "@/lib/repository";
export default function InvoicesPage() {
const invoices = getInvoices();
return (
<div className="grid">
<section className="panel">
<h2 className="section-title">Customer Invoices</h2>
<p className="section-copy">Invoices are created automatically when shipped sales orders post accounts receivable.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Invoice</th><th>Customer</th><th>Status</th><th>Invoice Date</th><th>Due</th><th>Total</th><th>Paid</th><th>Balance</th><th>Payment</th></tr></thead>
<tbody>
{invoices.length === 0 ? (
<tr><td colSpan={9} className="muted">No invoices yet. Ship a sales order to create one.</td></tr>
) : (
invoices.map((invoice) => (
<tr key={invoice.id}>
<td>{invoice.invoiceNumber}</td>
<td>{invoice.customerName}</td>
<td><span className={`pill ${invoice.status === "paid" ? "" : "warning"}`}>{invoice.status}</span></td>
<td>{formatDate(invoice.invoiceDate)}</td>
<td>{invoice.dueDate ? formatDate(invoice.dueDate) : "-"}</td>
<td>{formatCurrency(invoice.totalAmount)}</td>
<td>{formatCurrency(invoice.paidAmount)}</td>
<td>{formatCurrency(invoice.balanceDue)}</td>
<td>
{invoice.status === "paid" ? (
<span className="muted">Paid</span>
) : (
<form action={receiveCustomerPayment} className="form-grid">
<input type="hidden" name="invoiceId" value={invoice.id} />
<input className="input" name="amount" type="number" min="0.01" step="0.01" defaultValue={invoice.balanceDue.toFixed(2)} />
<input className="input" name="notes" placeholder="Payment notes" />
<button className="button secondary" type="submit">Receive</button>
</form>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

47
app/layout.tsx Normal file
View File

@@ -0,0 +1,47 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
import { LogoutButton } from "@/components/logout-button";
import { Sidebar } from "@/components/sidebar";
import { getSession } from "@/lib/auth";
import "./globals.css";
export const metadata: Metadata = {
title: "Inven",
description: "Inventory management with kits, sales, purchasing, and accounting."
};
export default async function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
const session = await getSession();
return (
<html lang="en">
<body>
<div className="shell">
<section className="hero">
<h1>Inven Control Center</h1>
<p>
A single-container operating system for stocked parts, kit assemblies, purchasing, shipping, customer
records, vendor records, and accounting visibility.
</p>
{session ? (
<div className="toolbar">
<span className="muted">
Signed in as {session.email} ({session.role})
</span>
<LogoutButton />
</div>
) : null}
</section>
{session ? (
<div className="layout">
<Sidebar />
<main className="content">{children}</main>
</div>
) : (
<main className="content" style={{ marginTop: 24 }}>{children}</main>
)}
</div>
</body>
</html>
);
}

25
app/login/page.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { loginAction } from "@/lib/actions";
export default function LoginPage() {
return (
<div className="grid">
<section className="panel" style={{ maxWidth: 520 }}>
<h2 className="section-title">Sign In</h2>
<p className="section-copy">Use the bootstrap admin credentials configured through environment variables.</p>
<form action={loginAction} className="form-grid">
<div className="form-row">
<label htmlFor="email">Email</label>
<input className="input" id="email" name="email" type="email" required />
</div>
<div className="form-row">
<label htmlFor="password">Password</label>
<input className="input" id="password" name="password" type="password" required />
</div>
<button className="button" type="submit">
Sign In
</button>
</form>
</section>
</div>
);
}

99
app/page.tsx Normal file
View File

@@ -0,0 +1,99 @@
import { formatCurrency } from "@/lib/format";
import { getDashboardStats, getLowStockParts, getPurchaseOrders, getSalesOrders } from "@/lib/repository";
export default function DashboardPage() {
const stats = getDashboardStats();
const salesOrders = getSalesOrders().slice(0, 5);
const purchaseOrders = getPurchaseOrders().slice(0, 5);
const lowStockParts = getLowStockParts().slice(0, 8);
return (
<div className="grid">
<section className="grid cards">
<article className="card"><div className="muted">Stocked Parts</div><div className="stats-value">{stats.totalParts}</div></article>
<article className="card"><div className="muted">Assemblies</div><div className="stats-value">{stats.totalAssemblies}</div></article>
<article className="card"><div className="muted">Open Sales Orders</div><div className="stats-value">{stats.openSalesOrders}</div></article>
<article className="card"><div className="muted">Open Purchase Orders</div><div className="stats-value">{stats.openPurchaseOrders}</div></article>
<article className="card"><div className="muted">Open Invoices</div><div className="stats-value">{stats.openInvoices}</div></article>
<article className="card"><div className="muted">Open Vendor Bills</div><div className="stats-value">{stats.openVendorBills}</div></article>
<article className="card"><div className="muted">Low Stock Alerts</div><div className="stats-value">{stats.lowStockCount}</div></article>
<article className="card"><div className="muted">Inventory Value</div><div className="stats-value">{formatCurrency(stats.inventoryValue)}</div></article>
<article className="card"><div className="muted">Accounts Receivable</div><div className="stats-value">{formatCurrency(stats.accountsReceivable)}</div></article>
<article className="card"><div className="muted">Accounts Payable</div><div className="stats-value">{formatCurrency(stats.accountsPayable)}</div></article>
</section>
<section className="two-up">
<article className="panel">
<h2 className="section-title">Recent Sales Orders</h2>
<p className="section-copy">Track outbound work and spot orders that are ready to ship.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Order</th><th>Customer</th><th>Status</th><th>Total</th></tr></thead>
<tbody>
{salesOrders.length === 0 ? (
<tr><td colSpan={4} className="muted">No sales orders yet.</td></tr>
) : (
salesOrders.map((order) => (
<tr key={order.id}>
<td>{order.orderNumber}</td>
<td>{order.customerName}</td>
<td><span className={`pill ${order.status === "shipped" ? "" : "warning"}`}>{order.status}</span></td>
<td>{formatCurrency(order.totalAmount)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</article>
<article className="panel">
<h2 className="section-title">Recent Purchase Orders</h2>
<p className="section-copy">Move supply from ordered to received and feed inventory automatically.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Order</th><th>Vendor</th><th>Status</th><th>Total</th></tr></thead>
<tbody>
{purchaseOrders.length === 0 ? (
<tr><td colSpan={4} className="muted">No purchase orders yet.</td></tr>
) : (
purchaseOrders.map((order) => (
<tr key={order.id}>
<td>{order.orderNumber}</td>
<td>{order.vendorName}</td>
<td><span className={`pill ${order.status === "received" ? "" : "warning"}`}>{order.status}</span></td>
<td>{formatCurrency(order.totalAmount)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</article>
</section>
<section className="panel">
<h2 className="section-title">Replenishment Watchlist</h2>
<p className="section-copy">Items at or below reorder point, with a suggested quantity to bring them back up.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>SKU</th><th>Name</th><th>On Hand</th><th>Reorder Point</th><th>Suggested Reorder</th><th>Last Vendor</th></tr></thead>
<tbody>
{lowStockParts.length === 0 ? (
<tr><td colSpan={6} className="muted">No low stock items right now.</td></tr>
) : (
lowStockParts.map((part) => (
<tr key={part.id}>
<td>{part.sku}</td>
<td>{part.name}</td>
<td>{part.quantityOnHand} {part.unitOfMeasure}</td>
<td>{part.reorderPoint}</td>
<td>{part.suggestedReorderQuantity}</td>
<td>{part.preferredVendorName || "—"}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

65
app/parts/page.tsx Normal file
View File

@@ -0,0 +1,65 @@
import { createPart, recordAdjustment } from "@/lib/actions";
import { formatCurrency } from "@/lib/format";
import { getParts } from "@/lib/repository";
export default function PartsPage() {
const parts = getParts();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Create Part or Assembly</h2>
<p className="section-copy">Maintain a single item master for stocked components and sellable kits.</p>
<form action={createPart} className="form-grid">
<div className="form-row"><label htmlFor="sku">SKU</label><input className="input" id="sku" name="sku" required /></div>
<div className="form-row"><label htmlFor="name">Name</label><input className="input" id="name" name="name" required /></div>
<div className="form-row"><label htmlFor="description">Description</label><textarea className="textarea" id="description" name="description" /></div>
<div className="form-row">
<label htmlFor="kind">Item Type</label>
<select className="select" id="kind" name="kind" defaultValue="part">
<option value="part">Part</option>
<option value="assembly">Assembly</option>
</select>
</div>
<div className="form-row"><label htmlFor="unitCost">Unit Cost</label><input className="input" id="unitCost" name="unitCost" type="number" min="0" step="0.01" defaultValue="0" /></div>
<div className="form-row"><label htmlFor="salePrice">Sale Price</label><input className="input" id="salePrice" name="salePrice" type="number" min="0" step="0.01" defaultValue="0" /></div>
<div className="form-row"><label htmlFor="reorderPoint">Reorder Point</label><input className="input" id="reorderPoint" name="reorderPoint" type="number" min="0" step="0.01" defaultValue="0" /></div>
<div className="form-row"><label htmlFor="unitOfMeasure">Unit of Measure</label><input className="input" id="unitOfMeasure" name="unitOfMeasure" defaultValue="ea" /></div>
<button className="button" type="submit">Save Item</button>
</form>
</article>
<article className="panel">
<h2 className="section-title">Inventory Adjustment</h2>
<p className="section-copy">Apply opening balances, cycle-count corrections, or manual stock adjustments.</p>
<form action={recordAdjustment} className="form-grid">
<div className="form-row"><label htmlFor="adjust-sku">SKU</label><input className="input" id="adjust-sku" name="sku" required /></div>
<div className="form-row"><label htmlFor="quantityDelta">Quantity Delta</label><input className="input" id="quantityDelta" name="quantityDelta" type="number" step="0.01" required /></div>
<div className="form-row"><label htmlFor="adjust-unit-cost">Unit Cost</label><input className="input" id="adjust-unit-cost" name="unitCost" type="number" min="0" step="0.01" defaultValue="0" /></div>
<div className="form-row"><label htmlFor="notes">Notes</label><textarea className="textarea" id="notes" name="notes" /></div>
<button className="button secondary" type="submit">Post Adjustment</button>
</form>
</article>
</section>
<section className="panel">
<h2 className="section-title">Item Master</h2>
<div className="table-wrap">
<table className="table">
<thead><tr><th>SKU</th><th>Name</th><th>Type</th><th>On Hand</th><th>Reorder</th><th>Cost</th><th>Price</th></tr></thead>
<tbody>
{parts.length === 0 ? (
<tr><td colSpan={7} className="muted">Add your first part or assembly to get started.</td></tr>
) : (
parts.map((part) => (
<tr key={part.id}>
<td>{part.sku}</td><td>{part.name}</td><td>{part.kind}</td><td>{part.quantityOnHand}</td><td>{part.reorderPoint}</td><td>{formatCurrency(part.unitCost)}</td><td>{formatCurrency(part.salePrice)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,92 @@
import { createPurchaseOrder, receivePurchaseOrder } from "@/lib/actions";
import { formatCurrency, formatDate } from "@/lib/format";
import { getLowStockParts, getPurchaseOrders, getVendors } from "@/lib/repository";
export default function PurchaseOrdersPage() {
const vendors = getVendors();
const orders = getPurchaseOrders();
const lowStockParts = getLowStockParts();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Create Purchase Order</h2>
<p className="section-copy">Enter one line per row as `SKU,quantity,unit cost`.</p>
<form action={createPurchaseOrder} className="form-grid">
<div className="form-row">
<label htmlFor="vendorCode">Vendor Code</label>
<select className="select" id="vendorCode" name="vendorCode">
{vendors.map((vendor) => <option key={vendor.id} value={vendor.code}>{vendor.code} - {vendor.name}</option>)}
</select>
</div>
<div className="form-row"><label htmlFor="purchase-lines">Line Items</label><textarea className="textarea" id="purchase-lines" name="lines" placeholder={"PART-001,10,31.50\nPART-002,24,6.25"} required /></div>
<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>
</article>
<article className="panel">
<h2 className="section-title">Receiving Flow</h2>
<p className="section-copy">Leave line quantities blank to receive the remaining balance, or enter `SKU,quantity` rows for a partial receipt.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Order</th><th>Vendor</th><th>Status</th><th>Total</th><th>Qty Progress</th><th>Created</th><th>Action</th></tr></thead>
<tbody>
{orders.length === 0 ? (
<tr><td colSpan={7} className="muted">No purchase orders yet.</td></tr>
) : (
orders.map((order) => (
<tr key={order.id}>
<td>{order.orderNumber}</td>
<td>{order.vendorName}</td>
<td><span className={`pill ${order.status === "received" ? "" : "warning"}`}>{order.status}</span></td>
<td>{formatCurrency(order.totalAmount)}</td>
<td>{order.fulfilledQuantity} / {order.orderedQuantity}</td>
<td>{formatDate(order.createdAt)}</td>
<td>
{order.status === "received" ? (
<span className="muted">Received</span>
) : (
<form action={receivePurchaseOrder} className="form-grid">
<input type="hidden" name="orderId" value={order.id} />
<textarea className="textarea" name="lines" placeholder={"PART-001,4\nPART-002,10"} />
<button className="button secondary" type="submit">Receive</button>
</form>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</article>
</section>
<section className="panel">
<h2 className="section-title">Restock Recommendations</h2>
<p className="section-copy">Use this list to plan the next purchase order from items already below target.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>SKU</th><th>Name</th><th>On Hand</th><th>Reorder Point</th><th>Suggested Qty</th><th>Last Vendor</th></tr></thead>
<tbody>
{lowStockParts.length === 0 ? (
<tr><td colSpan={6} className="muted">No parts currently need restocking.</td></tr>
) : (
lowStockParts.map((part) => (
<tr key={part.id}>
<td>{part.sku}</td>
<td>{part.name}</td>
<td>{part.quantityOnHand} {part.unitOfMeasure}</td>
<td>{part.reorderPoint}</td>
<td>{part.suggestedReorderQuantity}</td>
<td>{part.preferredVendorName || "-"}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

66
app/sales-orders/page.tsx Normal file
View File

@@ -0,0 +1,66 @@
import { createSalesOrder, shipSalesOrder } from "@/lib/actions";
import { formatCurrency, formatDate } from "@/lib/format";
import { getCustomers, getSalesOrders } from "@/lib/repository";
export default function SalesOrdersPage() {
const customers = getCustomers();
const orders = getSalesOrders();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Create Sales Order</h2>
<p className="section-copy">Enter one line per row as `SKU,quantity,unit price`.</p>
<form action={createSalesOrder} className="form-grid">
<div className="form-row">
<label htmlFor="customerCode">Customer Code</label>
<select className="select" id="customerCode" name="customerCode">
{customers.map((customer) => <option key={customer.id} value={customer.code}>{customer.code} - {customer.name}</option>)}
</select>
</div>
<div className="form-row"><label htmlFor="sales-lines">Line Items</label><textarea className="textarea" id="sales-lines" name="lines" placeholder={"PART-001,2,79.99\nKIT-100,1,249.00"} required /></div>
<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>
</article>
<article className="panel">
<h2 className="section-title">Shipping Flow</h2>
<p className="section-copy">Leave line quantities blank to ship the remaining balance, or enter `SKU,quantity` rows for a partial shipment.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Order</th><th>Customer</th><th>Status</th><th>Total</th><th>Qty Progress</th><th>Created</th><th>Action</th></tr></thead>
<tbody>
{orders.length === 0 ? (
<tr><td colSpan={7} className="muted">No sales orders yet.</td></tr>
) : (
orders.map((order) => (
<tr key={order.id}>
<td>{order.orderNumber}</td>
<td>{order.customerName}</td>
<td><span className={`pill ${order.status === "shipped" ? "" : "warning"}`}>{order.status}</span></td>
<td>{formatCurrency(order.totalAmount)}</td>
<td>{order.fulfilledQuantity} / {order.orderedQuantity}</td>
<td>{formatDate(order.createdAt)}</td>
<td>
{order.status === "shipped" ? (
<span className="muted">Shipped</span>
) : (
<form action={shipSalesOrder} className="form-grid">
<input type="hidden" name="orderId" value={order.id} />
<textarea className="textarea" name="lines" placeholder={"PART-001,1\nKIT-100,2"} />
<button className="button secondary" type="submit">Ship</button>
</form>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</article>
</section>
</div>
);
}

51
app/vendor-bills/page.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { payVendorBill } from "@/lib/actions";
import { formatCurrency, formatDate } from "@/lib/format";
import { getVendorBills } from "@/lib/repository";
export default function VendorBillsPage() {
const bills = getVendorBills();
return (
<div className="grid">
<section className="panel">
<h2 className="section-title">Vendor Bills</h2>
<p className="section-copy">Vendor bills are created automatically when purchase order receipts post accounts payable.</p>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Bill</th><th>Vendor</th><th>Status</th><th>Bill Date</th><th>Due</th><th>Total</th><th>Paid</th><th>Balance</th><th>Payment</th></tr></thead>
<tbody>
{bills.length === 0 ? (
<tr><td colSpan={9} className="muted">No vendor bills yet. Receive a purchase order to create one.</td></tr>
) : (
bills.map((bill) => (
<tr key={bill.id}>
<td>{bill.billNumber}</td>
<td>{bill.vendorName}</td>
<td><span className={`pill ${bill.status === "paid" ? "" : "warning"}`}>{bill.status}</span></td>
<td>{formatDate(bill.billDate)}</td>
<td>{bill.dueDate ? formatDate(bill.dueDate) : "-"}</td>
<td>{formatCurrency(bill.totalAmount)}</td>
<td>{formatCurrency(bill.paidAmount)}</td>
<td>{formatCurrency(bill.balanceDue)}</td>
<td>
{bill.status === "paid" ? (
<span className="muted">Paid</span>
) : (
<form action={payVendorBill} className="form-grid">
<input type="hidden" name="vendorBillId" value={bill.id} />
<input className="input" name="amount" type="number" min="0.01" step="0.01" defaultValue={bill.balanceDue.toFixed(2)} />
<input className="input" name="notes" placeholder="Payment notes" />
<button className="button secondary" type="submit">Pay</button>
</form>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
);
}

42
app/vendors/page.tsx vendored Normal file
View File

@@ -0,0 +1,42 @@
import { createVendor } from "@/lib/actions";
import { getVendors } from "@/lib/repository";
export default function VendorsPage() {
const vendors = getVendors();
return (
<div className="grid">
<section className="two-up">
<article className="panel">
<h2 className="section-title">Add Vendor</h2>
<p className="section-copy">Vendor records support purchasing, receipts, and accounts payable activity.</p>
<form action={createVendor} className="form-grid">
<div className="form-row"><label htmlFor="vendor-code">Vendor Code</label><input className="input" id="vendor-code" name="code" required /></div>
<div className="form-row"><label htmlFor="vendor-name">Name</label><input className="input" id="vendor-name" name="name" required /></div>
<div className="form-row"><label htmlFor="vendor-email">Email</label><input className="input" id="vendor-email" name="email" type="email" /></div>
<div className="form-row"><label htmlFor="vendor-phone">Phone</label><input className="input" id="vendor-phone" name="phone" /></div>
<div className="form-row"><label htmlFor="address">Address</label><textarea className="textarea" id="address" name="address" /></div>
<button className="button" type="submit">Save Vendor</button>
</form>
</article>
<article className="panel">
<h2 className="section-title">Vendor Directory</h2>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Code</th><th>Name</th><th>Email</th><th>Phone</th></tr></thead>
<tbody>
{vendors.length === 0 ? (
<tr><td colSpan={4} className="muted">No vendors yet.</td></tr>
) : (
vendors.map((vendor) => (
<tr key={vendor.id}><td>{vendor.code}</td><td>{vendor.name}</td><td>{vendor.email || "—"}</td><td>{vendor.phone || "—"}</td></tr>
))
)}
</tbody>
</table>
</div>
</article>
</section>
</div>
);
}