Add 'orders' for customer button in the customers page. Fix bug in add line item

This commit is contained in:
2026-07-11 20:42:37 -04:00
parent 1eccda5cf3
commit 9fa6b146e5
9 changed files with 51 additions and 19 deletions
+5
View File
@@ -375,6 +375,11 @@ button.danger {
color: #666;
}
.invoice-terms-note {
font-weight: bold;
color: #111;
}
@media print {
html { font-size: 100%; } /* keep the printed invoice at normal scale so it still fits one page */
.no-print { display: none !important; }
+23 -10
View File
@@ -59,7 +59,9 @@ export function getOrderDetail(id: string | number) {
return { ...order, lineItems, workLog, totals };
}
router.get('/', (_req, res) => {
router.get('/', (req, res) => {
const customerId = req.query.customer_id ? Number(req.query.customer_id) : null;
const orders = db
.prepare(
`SELECT so.id, so.order_number, so.status, so.job_type, so.date_in, so.due_date,
@@ -68,6 +70,7 @@ router.get('/', (_req, res) => {
JOIN customers c ON c.id = so.customer_id
LEFT JOIN order_totals ot ON ot.service_order_id = so.id
WHERE so.deleted_at IS NULL
AND (? IS NULL OR so.customer_id = ?)
ORDER BY CASE so.status
WHEN 'OPEN' THEN 0
WHEN 'IN PROGRESS' THEN 1
@@ -78,9 +81,13 @@ router.get('/', (_req, res) => {
so.date_in DESC,
so.order_number DESC`
)
.all();
.all(customerId, customerId);
res.render('orders/list.njk', { orders });
const customerFilter = customerId
? db.prepare('SELECT id, name FROM customers WHERE id = ?').get(customerId)
: null;
res.render('orders/list.njk', { orders, customerFilter });
});
router.get('/orders/new', (_req, res) => {
@@ -137,8 +144,8 @@ router.post('/orders', (req, res) => {
const orderInfo = db
.prepare(
`INSERT INTO service_orders
(order_number, customer_id, clock_id, job_type, date_in, due_date, terms)
VALUES (?, ?, ?, ?, ?, ?, ?)`
(order_number, customer_id, clock_id, job_type, date_in, due_date, terms, internal_note)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
orderNumber,
@@ -147,7 +154,8 @@ router.post('/orders', (req, res) => {
b.job_type || null,
b.date_in || todayStr(),
b.due_date || null,
b.terms || null
b.terms || null,
b.internal_note || null
);
return Number(orderInfo.lastInsertRowid);
@@ -167,6 +175,11 @@ router.get('/orders/:id', (req, res) => {
res.render('orders/detail.njk', { order, jobTypes: JOB_TYPES, statuses: STATUSES });
});
router.get('/orders/:id/totals', (req, res) => {
const totals = db.prepare('SELECT * FROM order_totals WHERE service_order_id = ?').get(req.params.id);
res.render('orders/_totals.njk', { order: { id: req.params.id }, totals });
});
router.post('/orders/:id/line-items', (req, res) => {
const orderId = req.params.id;
const b = req.body as Record<string, string>;
@@ -187,9 +200,9 @@ router.post('/orders/:id/line-items', (req, res) => {
.run(orderId, kind, b.stock_code || null, b.description || null, quantity, unitPrice, countRow.n);
const line = db.prepare('SELECT * FROM line_items WHERE id = ?').get(info.lastInsertRowid);
const totals = db.prepare('SELECT * FROM order_totals WHERE service_order_id = ?').get(orderId);
res.render('orders/_line_item_added.njk', { line, order: { id: orderId }, totals, oob: true });
res.set('HX-Trigger', 'line-items-changed');
res.render('orders/_line_item_row.njk', { line, order: { id: orderId } });
});
router.delete('/orders/:id/line-items/:lineId', (req, res) => {
@@ -197,8 +210,8 @@ router.delete('/orders/:id/line-items/:lineId', (req, res) => {
req.params.lineId,
req.params.id
);
const totals = db.prepare('SELECT * FROM order_totals WHERE service_order_id = ?').get(req.params.id);
res.render('orders/_totals.njk', { totals, oob: true });
res.set('HX-Trigger', 'line-items-changed');
res.send('');
});
router.post('/orders/:id/work-log', (req, res) => {
+1
View File
@@ -20,6 +20,7 @@ env.addFilter('money', (cents: number) => formatCents(cents));
env.addFilter('lineTotal', (unitPrice: number, quantity: number) =>
formatCents(Math.round((quantity ?? 1) * (unitPrice ?? 0)))
);
env.addFilter('tel', (phone: string) => (phone || '').replace(/[^\d+]/g, ''));
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(process.cwd(), 'public')));
+4 -2
View File
@@ -18,22 +18,24 @@
<th>Phone</th>
<th>Email</th>
<th>Address</th>
<th></th>
</tr>
</thead>
<tbody>
{% for c in customers %}
<tr>
<td>{{ c.name }}</td>
<td>{{ c.phone or '' }}</td>
<td>{% if c.phone %}<a href="tel:{{ c.phone | tel }}">{{ c.phone }}</a>{% endif %}</td>
<td>{{ c.email or '' }}</td>
<td>
{%- if c.address_line1 -%}
{{ c.address_line1 }}{% if c.city %}, {{ c.city }}{% endif %}{% if c.state %}, {{ c.state }}{% endif %} {{ c.zip or '' }}
{%- endif -%}
</td>
<td><a class="button-link" href="/?customer_id={{ c.id }}">Orders</a></td>
</tr>
{% else %}
<tr><td colspan="4" class="empty">No customers found.</td></tr>
<tr><td colspan="5" class="empty">No customers found.</td></tr>
{% endfor %}
</tbody>
</table>
+3 -2
View File
@@ -14,8 +14,8 @@
<div class="invoice">
<header class="invoice-header">
<div>
<h1>Harding's Clocks</h1>
<p>Clock Sales &amp; Repair</p>
<h1>Jim Harding</h1>
<p>Clock Repair | Phone: 434 660 2030</p>
</div>
<div class="invoice-meta">
<p><strong>Invoice #{{ order.order_number }}</strong></p>
@@ -81,6 +81,7 @@
</section>
<footer class="invoice-footer">
<p class="invoice-terms-note">Cash or check only.</p>
<p>Thank you for your business.</p>
</footer>
</div>
-2
View File
@@ -1,2 +0,0 @@
{% include "orders/_line_item_row.njk" %}
{% include "orders/_totals.njk" %}
+4 -1
View File
@@ -1,4 +1,7 @@
<dl id="totals-summary" class="totals-summary"{% if oob %} hx-swap-oob="true"{% endif %}>
<dl id="totals-summary" class="totals-summary"
hx-get="/orders/{{ order.id }}/totals"
hx-trigger="line-items-changed from:body"
hx-swap="outerHTML">
<div><dt>Parts</dt><dd>{{ totals.total_parts | money }}</dd></div>
<div><dt>Labor</dt><dd>{{ totals.total_labor | money }}</dd></div>
<div><dt>Sublet</dt><dd>{{ totals.total_sublet | money }}</dd></div>
+7 -2
View File
@@ -2,8 +2,13 @@
{% block title %}Orders — Harding's Clocks{% endblock %}
{% block content %}
<div class="page-header">
<h1>Service Orders</h1>
<a class="button-link" href="/orders/new">+ New Order</a>
<h1>
{% if customerFilter %}Orders for {{ customerFilter.name }}{% else %}Service Orders{% endif %}
</h1>
<div class="header-actions">
{% if customerFilter %}<a class="button-link" href="/">Clear Filter</a>{% endif %}
<a class="button-link" href="/orders/new">+ New Order</a>
</div>
</div>
<table class="data-table">
+4
View File
@@ -67,6 +67,10 @@
<label>Due date <input type="date" name="due_date"></label>
<label>Terms <input type="text" name="terms"></label>
</div>
<label>
Note <span class="hint">(internal only — never printed on the invoice)</span>
<textarea name="internal_note" rows="3"></textarea>
</label>
</fieldset>
<button type="submit">Create Order</button>