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;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-terms-note {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
html { font-size: 100%; } /* keep the printed invoice at normal scale so it still fits one page */
|
html { font-size: 100%; } /* keep the printed invoice at normal scale so it still fits one page */
|
||||||
.no-print { display: none !important; }
|
.no-print { display: none !important; }
|
||||||
|
|||||||
+23
-10
@@ -59,7 +59,9 @@ export function getOrderDetail(id: string | number) {
|
|||||||
return { ...order, lineItems, workLog, totals };
|
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
|
const orders = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT so.id, so.order_number, so.status, so.job_type, so.date_in, so.due_date,
|
`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
|
JOIN customers c ON c.id = so.customer_id
|
||||||
LEFT JOIN order_totals ot ON ot.service_order_id = so.id
|
LEFT JOIN order_totals ot ON ot.service_order_id = so.id
|
||||||
WHERE so.deleted_at IS NULL
|
WHERE so.deleted_at IS NULL
|
||||||
|
AND (? IS NULL OR so.customer_id = ?)
|
||||||
ORDER BY CASE so.status
|
ORDER BY CASE so.status
|
||||||
WHEN 'OPEN' THEN 0
|
WHEN 'OPEN' THEN 0
|
||||||
WHEN 'IN PROGRESS' THEN 1
|
WHEN 'IN PROGRESS' THEN 1
|
||||||
@@ -78,9 +81,13 @@ router.get('/', (_req, res) => {
|
|||||||
so.date_in DESC,
|
so.date_in DESC,
|
||||||
so.order_number 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) => {
|
router.get('/orders/new', (_req, res) => {
|
||||||
@@ -137,8 +144,8 @@ router.post('/orders', (req, res) => {
|
|||||||
const orderInfo = db
|
const orderInfo = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO service_orders
|
`INSERT INTO service_orders
|
||||||
(order_number, customer_id, clock_id, job_type, date_in, due_date, terms)
|
(order_number, customer_id, clock_id, job_type, date_in, due_date, terms, internal_note)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
orderNumber,
|
orderNumber,
|
||||||
@@ -147,7 +154,8 @@ router.post('/orders', (req, res) => {
|
|||||||
b.job_type || null,
|
b.job_type || null,
|
||||||
b.date_in || todayStr(),
|
b.date_in || todayStr(),
|
||||||
b.due_date || null,
|
b.due_date || null,
|
||||||
b.terms || null
|
b.terms || null,
|
||||||
|
b.internal_note || null
|
||||||
);
|
);
|
||||||
|
|
||||||
return Number(orderInfo.lastInsertRowid);
|
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 });
|
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) => {
|
router.post('/orders/:id/line-items', (req, res) => {
|
||||||
const orderId = req.params.id;
|
const orderId = req.params.id;
|
||||||
const b = req.body as Record<string, string>;
|
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);
|
.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 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) => {
|
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.lineId,
|
||||||
req.params.id
|
req.params.id
|
||||||
);
|
);
|
||||||
const totals = db.prepare('SELECT * FROM order_totals WHERE service_order_id = ?').get(req.params.id);
|
res.set('HX-Trigger', 'line-items-changed');
|
||||||
res.render('orders/_totals.njk', { totals, oob: true });
|
res.send('');
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/orders/:id/work-log', (req, res) => {
|
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) =>
|
env.addFilter('lineTotal', (unitPrice: number, quantity: number) =>
|
||||||
formatCents(Math.round((quantity ?? 1) * (unitPrice ?? 0)))
|
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.urlencoded({ extended: true }));
|
||||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||||
|
|||||||
@@ -18,22 +18,24 @@
|
|||||||
<th>Phone</th>
|
<th>Phone</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Address</th>
|
<th>Address</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for c in customers %}
|
{% for c in customers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ c.name }}</td>
|
<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>{{ c.email or '' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{%- if c.address_line1 -%}
|
{%- if c.address_line1 -%}
|
||||||
{{ c.address_line1 }}{% if c.city %}, {{ c.city }}{% endif %}{% if c.state %}, {{ c.state }}{% endif %} {{ c.zip or '' }}
|
{{ c.address_line1 }}{% if c.city %}, {{ c.city }}{% endif %}{% if c.state %}, {{ c.state }}{% endif %} {{ c.zip or '' }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
|
<td><a class="button-link" href="/?customer_id={{ c.id }}">Orders</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="4" class="empty">No customers found.</td></tr>
|
<tr><td colspan="5" class="empty">No customers found.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
<div class="invoice">
|
<div class="invoice">
|
||||||
<header class="invoice-header">
|
<header class="invoice-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Harding's Clocks</h1>
|
<h1>Jim Harding</h1>
|
||||||
<p>Clock Sales & Repair</p>
|
<p>Clock Repair | Phone: 434 660 2030</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-meta">
|
<div class="invoice-meta">
|
||||||
<p><strong>Invoice #{{ order.order_number }}</strong></p>
|
<p><strong>Invoice #{{ order.order_number }}</strong></p>
|
||||||
@@ -81,6 +81,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="invoice-footer">
|
<footer class="invoice-footer">
|
||||||
|
<p class="invoice-terms-note">Cash or check only.</p>
|
||||||
<p>Thank you for your business.</p>
|
<p>Thank you for your business.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</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>Parts</dt><dd>{{ totals.total_parts | money }}</dd></div>
|
||||||
<div><dt>Labor</dt><dd>{{ totals.total_labor | 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>
|
<div><dt>Sublet</dt><dd>{{ totals.total_sublet | money }}</dd></div>
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
{% block title %}Orders — Harding's Clocks{% endblock %}
|
{% block title %}Orders — Harding's Clocks{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>Service Orders</h1>
|
<h1>
|
||||||
<a class="button-link" href="/orders/new">+ New Order</a>
|
{% 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>
|
</div>
|
||||||
|
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
|
|||||||
@@ -67,6 +67,10 @@
|
|||||||
<label>Due date <input type="date" name="due_date"></label>
|
<label>Due date <input type="date" name="due_date"></label>
|
||||||
<label>Terms <input type="text" name="terms"></label>
|
<label>Terms <input type="text" name="terms"></label>
|
||||||
</div>
|
</div>
|
||||||
|
<label>
|
||||||
|
Note <span class="hint">(internal only — never printed on the invoice)</span>
|
||||||
|
<textarea name="internal_note" rows="3"></textarea>
|
||||||
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<button type="submit">Create Order</button>
|
<button type="submit">Create Order</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user