Add 'orders' for customer button in the customers page. Fix bug in add line item
This commit is contained in:
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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')));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<div class="invoice">
|
||||
<header class="invoice-header">
|
||||
<div>
|
||||
<h1>Harding's Clocks</h1>
|
||||
<p>Clock Sales & 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>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{% include "orders/_line_item_row.njk" %}
|
||||
{% include "orders/_totals.njk" %}
|
||||
@@ -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>
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
{% block title %}Orders — Harding's Clocks{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Service Orders</h1>
|
||||
<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">
|
||||
<thead>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user