93 lines
4.8 KiB
TypeScript
93 lines
4.8 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|