initial version

This commit is contained in:
2026-07-11 19:47:40 -04:00
parent b819a0dc6c
commit 362806413e
26 changed files with 3400 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
<option value="">— new clock —</option>
{% for clock in clocks %}
<option value="{{ clock.id }}">
{{- clock.manufacturer or '' }} {{ clock.model or '' }}{% if clock.description %} ({{ clock.description }}){% endif -%}
</option>
{% endfor %}
+40
View File
@@ -0,0 +1,40 @@
{% extends "layout.njk" %}
{% block title %}Customers — Harding's Clocks{% endblock %}
{% block content %}
<div class="page-header">
<h1>Customers</h1>
</div>
<form class="search-form" method="get" action="/customers">
<input type="search" name="q" placeholder="Search name, phone, or email…" value="{{ q }}">
<button type="submit">Search</button>
{% if q %}<a class="button-link" href="/customers">Clear</a>{% endif %}
</form>
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
<th>Address</th>
</tr>
</thead>
<tbody>
{% for c in customers %}
<tr>
<td>{{ c.name }}</td>
<td>{{ c.phone or '' }}</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>
</tr>
{% else %}
<tr><td colspan="4" class="empty">No customers found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
+93
View File
@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Invoice #{{ order.order_number }} — Harding's Clocks</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="invoice-body">
<div class="invoice-toolbar no-print">
<button type="button" onclick="window.print()">Print</button>
</div>
<div class="invoice">
<header class="invoice-header">
<div>
<h1>Harding's Clocks</h1>
<p>Clock Sales &amp; Repair</p>
</div>
<div class="invoice-meta">
<p><strong>Invoice #{{ order.order_number }}</strong></p>
<p>Date In: {{ order.date_in or '' }}</p>
{% if order.date_out %}<p>Date Out: {{ order.date_out }}</p>{% endif %}
{% if order.due_date %}<p>Due: {{ order.due_date }}</p>{% endif %}
<p>Status: {{ order.status }}</p>
</div>
</header>
<section class="invoice-parties">
<div>
<h2>Ship To</h2>
<p>{{ order.customer_name }}</p>
{% if order.address_line1 %}<p>{{ order.address_line1 }}</p>{% endif %}
{% if order.address_line2 %}<p>{{ order.address_line2 }}</p>{% endif %}
{% if order.city or order.state or order.zip %}<p>{{ order.city or '' }}{% if order.state %}, {{ order.state }}{% endif %} {{ order.zip or '' }}</p>{% endif %}
{% if order.phone %}<p>{{ order.phone }}</p>{% endif %}
</div>
<div>
<h2>Clock</h2>
{% if order.manufacturer or order.model %}<p>{{ order.manufacturer or '' }} {{ order.model or '' }}</p>{% endif %}
{% if order.serial_number %}<p>Serial: {{ order.serial_number }}</p>{% endif %}
{% if order.reference %}<p>Reference: {{ order.reference }}</p>{% endif %}
</div>
<div>
<h2>Order Info</h2>
<p>Job Type: {{ order.job_type or '' }}</p>
{% if order.service_rep %}<p>Rep: {{ order.service_rep }}</p>{% endif %}
{% if order.technician %}<p>Technician: {{ order.technician }}</p>{% endif %}
{% if order.cust_po %}<p>PO: {{ order.cust_po }}</p>{% endif %}
{% if order.terms %}<p>Terms: {{ order.terms }}</p>{% endif %}
</div>
</section>
<table class="invoice-table">
<thead>
<tr><th>Type</th><th>Stock #</th><th>Description</th><th>Qty</th><th>Unit Price</th><th>Total</th></tr>
</thead>
<tbody>
{% for line in order.lineItems %}
<tr>
<td>{{ line.kind }}</td>
<td>{{ line.stock_code or '' }}</td>
<td>{{ line.description or '' }}</td>
<td>{{ line.quantity }}</td>
<td>{{ line.unit_price | money }}</td>
<td>{{ line.unit_price | lineTotal(line.quantity) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<section class="invoice-totals">
<dl>
<div><dt>Parts</dt><dd>{{ order.totals.total_parts | money }}</dd></div>
<div><dt>Labor</dt><dd>{{ order.totals.total_labor | money }}</dd></div>
<div><dt>Sublet</dt><dd>{{ order.totals.total_sublet | money }}</dd></div>
<div><dt>Tax</dt><dd>{{ order.tax | money }}</dd></div>
<div><dt>Shipping</dt><dd>{{ order.shipping | money }}</dd></div>
<div><dt>Pickup/Delivery</dt><dd>{{ order.pickup_delivery | money }}</dd></div>
<div><dt>Service Call</dt><dd>{{ order.service_call | money }}</dd></div>
<div><dt>Disposal</dt><dd>{{ order.disposal | money }}</dd></div>
<div class="grand"><dt>Grand Total</dt><dd>{{ order.totals.grand_total | money }}</dd></div>
<div><dt>Amount Paid</dt><dd>{{ order.amount_paid | money }}</dd></div>
<div class="balance-due"><dt>Balance Due</dt><dd>{{ order.totals.balance_due | money }}</dd></div>
</dl>
</section>
<footer class="invoice-footer">
<p>Thank you for your business.</p>
</footer>
</div>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Harding's Clocks{% endblock %}</title>
<link rel="stylesheet" href="/css/style.css">
<script src="/vendor/htmx/htmx.min.js" defer></script>
</head>
<body>
<nav class="no-print">
<a class="brand" href="/">Harding's Clocks</a>
<a href="/">Orders</a>
<a href="/orders/new">New Order</a>
<a href="/customers">Customers</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
{% include "orders/_line_item_row.njk" %}
{% include "orders/_totals.njk" %}
+15
View File
@@ -0,0 +1,15 @@
<tr id="line-{{ line.id }}">
<td>{{ line.kind }}</td>
<td>{{ line.stock_code or '' }}</td>
<td>{{ line.description or '' }}</td>
<td>{{ line.quantity }}</td>
<td>{{ line.unit_price | money }}</td>
<td>{{ line.unit_price | lineTotal(line.quantity) }}</td>
<td>
<button type="button" class="icon-button"
hx-delete="/orders/{{ order.id }}/line-items/{{ line.id }}"
hx-target="closest tr"
hx-swap="outerHTML"
hx-confirm="Remove this line?">✕</button>
</td>
</tr>
+44
View File
@@ -0,0 +1,44 @@
{% set totals = order.totals %}
<div id="order-summary" class="card">
<form hx-post="/orders/{{ order.id }}" hx-target="#order-summary" hx-swap="outerHTML">
<div class="grid-2">
<label>
Status
<select name="status">
{% for s in statuses %}
<option value="{{ s }}" {% if order.status == s %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
</label>
<label>
Job type
<select name="job_type">
<option value="">— select —</option>
{% for jt in jobTypes %}
<option value="{{ jt }}" {% if order.job_type == jt %}selected{% endif %}>{{ jt }}</option>
{% endfor %}
</select>
</label>
<label>Service rep <input type="text" name="service_rep" value="{{ order.service_rep or '' }}"></label>
<label>Technician <input type="text" name="technician" value="{{ order.technician or '' }}"></label>
<label>Date in <input type="text" value="{{ order.date_in or '' }}" disabled></label>
<label>Date out <input type="date" name="date_out" value="{{ order.date_out or '' }}"></label>
<label>Due date <input type="date" name="due_date" value="{{ order.due_date or '' }}"></label>
<label>Customer PO <input type="text" name="cust_po" value="{{ order.cust_po or '' }}"></label>
<label>Terms <input type="text" name="terms" value="{{ order.terms or '' }}"></label>
</div>
<div class="grid-2">
<label>Tax <input type="text" name="tax" value="{{ (order.tax / 100) | round(2) }}"></label>
<label>Shipping <input type="text" name="shipping" value="{{ (order.shipping / 100) | round(2) }}"></label>
<label>Pickup/Delivery <input type="text" name="pickup_delivery" value="{{ (order.pickup_delivery / 100) | round(2) }}"></label>
<label>Service call <input type="text" name="service_call" value="{{ (order.service_call / 100) | round(2) }}"></label>
<label>Disposal <input type="text" name="disposal" value="{{ (order.disposal / 100) | round(2) }}"></label>
<label>Amount paid <input type="text" name="amount_paid" value="{{ (order.amount_paid / 100) | round(2) }}"></label>
</div>
<button type="submit">Save</button>
</form>
{% include "orders/_totals.njk" %}
</div>
+7
View File
@@ -0,0 +1,7 @@
<dl id="totals-summary" class="totals-summary"{% if oob %} hx-swap-oob="true"{% endif %}>
<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>
<div><dt>Grand Total</dt><dd>{{ totals.grand_total | money }}</dd></div>
<div class="balance-due"><dt>Balance Due</dt><dd>{{ totals.balance_due | money }}</dd></div>
</dl>
+4
View File
@@ -0,0 +1,4 @@
<li id="log-{{ entry.id }}" class="work-log-item">
<time>{{ entry.logged_at }}</time>
<p>{{ entry.note }}</p>
</li>
+90
View File
@@ -0,0 +1,90 @@
{% extends "layout.njk" %}
{% block title %}Order #{{ order.order_number }} — Harding's Clocks{% endblock %}
{% block content %}
<div class="page-header">
<h1>Order #{{ order.order_number }}</h1>
<div class="header-actions">
<a class="button-link" href="/orders/{{ order.id }}/invoice" target="_blank">Print Invoice</a>
<button type="button" class="danger"
hx-delete="/orders/{{ order.id }}"
hx-confirm="Delete this order? It can be recovered later if needed.">
Delete Order
</button>
</div>
</div>
<section class="card two-col">
<div>
<h2>Customer</h2>
<p>{{ order.customer_name }}</p>
{% if order.address_line1 %}<p>{{ order.address_line1 }}{% if order.address_line2 %}, {{ order.address_line2 }}{% endif %}</p>{% endif %}
{% if order.city or order.state or order.zip %}<p>{{ order.city or '' }}{% if order.state %}, {{ order.state }}{% endif %} {{ order.zip or '' }}</p>{% endif %}
{% if order.phone %}<p>{{ order.phone }}{% if order.alt_phone %} / {{ order.alt_phone }}{% endif %}</p>{% endif %}
{% if order.email %}<p>{{ order.email }}</p>{% endif %}
</div>
<div>
<h2>Clock</h2>
{% if order.manufacturer or order.model %}
<p>{{ order.manufacturer or '' }} {{ order.model or '' }}</p>
{% endif %}
{% if order.serial_number %}<p>Serial: {{ order.serial_number }}</p>{% endif %}
{% if order.alternate_id %}<p>Alt ID: {{ order.alternate_id }}</p>{% endif %}
{% if order.reference %}<p>Reference: {{ order.reference }}</p>{% endif %}
{% if order.clock_description %}<p>{{ order.clock_description }}</p>{% endif %}
{% if not order.manufacturer and not order.model and not order.clock_description %}
<p class="empty">No clock on file.</p>
{% endif %}
</div>
</section>
{% include "orders/_order_summary.njk" %}
<section class="card">
<h2>Line Items</h2>
<table class="data-table">
<thead>
<tr><th>Type</th><th>Stock #</th><th>Description</th><th>Qty</th><th>Unit Price</th><th>Total</th><th></th></tr>
</thead>
<tbody id="lines-body">
{% for line in order.lineItems %}
{% include "orders/_line_item_row.njk" %}
{% endfor %}
</tbody>
</table>
<form class="inline-form"
hx-post="/orders/{{ order.id }}/line-items"
hx-target="#lines-body"
hx-swap="beforeend"
hx-on::after-request="this.reset()">
<select name="kind">
<option value="labor">Labor</option>
<option value="part">Part</option>
<option value="sublet">Sublet</option>
</select>
<input type="text" name="stock_code" placeholder="Stock #">
<input type="text" name="description" placeholder="Description" required>
<input type="number" name="quantity" placeholder="Qty" value="1" step="0.25" min="0">
<input type="text" name="unit_price" placeholder="Unit price">
<button type="submit">Add Line</button>
</form>
</section>
<section class="card">
<h2>Work Log</h2>
<ul id="work-log-list" class="work-log-list">
{% for entry in order.workLog %}
{% include "orders/_work_log_item.njk" %}
{% endfor %}
</ul>
<form class="inline-form"
hx-post="/orders/{{ order.id }}/work-log"
hx-target="#work-log-list"
hx-swap="afterbegin"
hx-on::after-request="this.reset()">
<input type="text" name="note" placeholder="Add a note…" required>
<button type="submit">Add Note</button>
</form>
</section>
{% endblock %}
+37
View File
@@ -0,0 +1,37 @@
{% extends "layout.njk" %}
{% 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>
</div>
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Customer</th>
<th>Job Type</th>
<th>Status</th>
<th>Date In</th>
<th>Due</th>
<th>Balance Due</th>
</tr>
</thead>
<tbody>
{% for o in orders %}
<tr onclick="location.href='/orders/{{ o.id }}'" class="row-link">
<td>{{ o.order_number }}</td>
<td>{{ o.customer_name }}</td>
<td>{{ o.job_type or '' }}</td>
<td><span class="status-badge status-{{ o.status | lower | replace(' ', '-') }}">{{ o.status }}</span></td>
<td>{{ o.date_in or '' }}</td>
<td>{{ o.due_date or '' }}</td>
<td>{{ o.balance_due | money }}</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">No orders yet. <a href="/orders/new">Create one</a>.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
+81
View File
@@ -0,0 +1,81 @@
{% extends "layout.njk" %}
{% block title %}New Order — Harding's Clocks{% endblock %}
{% block content %}
<div class="page-header">
<h1>New Service Order</h1>
</div>
<form method="post" action="/orders" class="stacked-form">
<fieldset>
<legend>Customer</legend>
<label>
Existing customer
<select name="existing_customer_id" id="existing_customer_id"
hx-get="/clocks/by-customer"
hx-trigger="change"
hx-target="#clock-select"
hx-include="this">
<option value="">— new customer —</option>
{% for c in customers %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
</label>
<div class="grid-2">
<label>Name <input type="text" name="new_customer_name"></label>
<label>Phone <input type="text" name="new_customer_phone"></label>
<label>Alt phone <input type="text" name="new_customer_alt_phone"></label>
<label>Email <input type="email" name="new_customer_email"></label>
<label>Address line 1 <input type="text" name="new_customer_address_line1"></label>
<label>Address line 2 <input type="text" name="new_customer_address_line2"></label>
<label>City <input type="text" name="new_customer_city"></label>
<label>State <input type="text" name="new_customer_state"></label>
<label>Zip <input type="text" name="new_customer_zip"></label>
</div>
<p class="hint">Leave the name blank above and pick an existing customer instead, if applicable.</p>
</fieldset>
<fieldset>
<legend>Clock</legend>
<label>
Existing clock
<select name="existing_clock_id" id="clock-select">
<option value="">— new clock —</option>
</select>
</label>
<div class="grid-2">
<label>Manufacturer <input type="text" name="new_clock_manufacturer"></label>
<label>Model <input type="text" name="new_clock_model"></label>
<label>Serial number <input type="text" name="new_clock_serial_number"></label>
<label>Alternate ID <input type="text" name="new_clock_alternate_id"></label>
<label>Reference <input type="text" name="new_clock_reference"></label>
<label>Description <input type="text" name="new_clock_description"></label>
</div>
</fieldset>
<fieldset>
<legend>Order</legend>
<div class="grid-2">
<label>
Job type
<select name="job_type">
<option value="">— select —</option>
{% for jt in jobTypes %}
<option value="{{ jt }}">{{ jt }}</option>
{% endfor %}
</select>
</label>
<label>Service rep <input type="text" name="service_rep"></label>
<label>Technician <input type="text" name="technician"></label>
<label>Date in <input type="date" name="date_in" value="{{ today }}"></label>
<label>Due date <input type="date" name="due_date"></label>
<label>Customer PO <input type="text" name="cust_po"></label>
<label>Terms <input type="text" name="terms"></label>
</div>
</fieldset>
<button type="submit">Create Order</button>
</form>
{% endblock %}