2026-06-18 01:40:12 +00:00
{{define "title"}}Add receipt{{end}}
{{define "content"}}
< header >
< span > {{.Subject}}< / span >
2026-06-19 02:04:42 +00:00
< span > < a href = "/tally" > Tally< / a > · < a href = "/recent" > Recent< / a > · < a href = "/manage" > Manage< / a > · < a href = "/export" > Export< / a > · < a href = "/logout" > Log out< / a > < / span >
2026-06-18 01:40:12 +00:00
< / header >
< h1 > Add receipt< / h1 >
{{range .Errors}}< div class = "err" > {{.}}< / div > {{end}}
2026-06-19 02:04:42 +00:00
< form method = "post" action = "/upload" enctype = "multipart/form-data" id = "upload-form" >
2026-06-18 01:40:12 +00:00
< label for = "receipt" > Receipt photo or PDF< / label >
< input id = "receipt" type = "file" name = "receipt" accept = "image/*,application/pdf" capture = "environment" required >
2026-06-19 02:04:42 +00:00
{{if .ClassifyEnabled}}
< label class = "check" > < input type = "checkbox" id = "skip-ai" > Skip AI auto-fill (enter fields by hand)< / label >
< p class = "footnote" id = "classify-note" > Receipt is read automatically by < code > {{.ClassifyModel}}< / code > to pre-fill the fields below — review before submitting.< / p >
{{else}}
< p class = "footnote" > Auto-fill is off (no API key configured). Enter the fields manually.< / p >
{{end}}
2026-06-18 01:40:12 +00:00
< label for = "amount" > Amount ($)< / label >
< input id = "amount" type = "number" name = "amount" step = "0.01" min = "0.01" inputmode = "decimal" value = "{{.Amount}}" required >
< label for = "receipt_date" > Date on receipt< / label >
< input id = "receipt_date" type = "date" name = "receipt_date" value = "{{.Date}}" required >
< label for = "category_id" > Category< / label >
< select id = "category_id" name = "category_id" >
{{$sc := .CategoryID}}
{{range .Categories}}< option value = "{{.ID}}" { { if eq ( printf " % d " . ID ) $ sc } } selected { { end } } > {{.Label}}< / option > {{end}}
< / select >
< label for = "person_id" > Who< / label >
< select id = "person_id" name = "person_id" >
{{$sp := .PersonID}}
< option value = "" > —< / option >
{{range .People}}< option value = "{{.ID}}" { { if eq ( printf " % d " . ID ) $ sp } } selected { { end } } > {{.Label}}< / option > {{end}}
< / select >
2026-06-19 11:06:15 +00:00
< label for = "attachments" > Additional files (optional)< / label >
< input id = "attachments" type = "file" name = "attachments" accept = "image/*,application/pdf" multiple >
2026-06-19 02:04:42 +00:00
< div class = "dup-warn" id = "dup-warn" hidden >
< strong > Possible duplicate< / strong > — a receipt with this date and amount is already saved:
< ul id = "dup-list" > < / ul >
< label class = "check" > < input type = "checkbox" id = "dup-ack" > Add it anyway< / label >
< / div >
< button type = "submit" class = "primary" id = "submit-btn" > Submit< / button >
2026-06-18 01:40:12 +00:00
< / form >
2026-06-19 02:04:42 +00:00
< script >
(function () {
var form = document.getElementById("upload-form");
var amount = document.getElementById("amount");
var dateEl = document.getElementById("receipt_date");
var dupWarn = document.getElementById("dup-warn");
var dupList = document.getElementById("dup-list");
var dupAck = document.getElementById("dup-ack");
var submitBtn = document.getElementById("submit-btn");
var dupVisible = false;
// --- Duplicate check (runs whenever date + amount are both known) ---
function updateSubmitState() {
submitBtn.disabled = dupVisible & & !dupAck.checked;
}
dupAck.addEventListener("change", updateSubmitState);
function checkDuplicates() {
var a = amount.value.trim(), d = dateEl.value.trim();
if (!a || !d) { hideDup(); return; }
var url = "/duplicates?date=" + encodeURIComponent(d) + "& amount=" + encodeURIComponent(a);
fetch(url)
.then(function (r) { return r.ok ? r.json() : { matches: [] }; })
.then(function (data) {
if (!data.matches || !data.matches.length) { hideDup(); return; }
dupList.innerHTML = "";
data.matches.forEach(function (m) {
var li = document.createElement("li");
var who = m.who ? " · " + m.who : "";
li.textContent = "$" + m.amount + " · " + m.date + " · " + m.category + who +
" — " + m.filename + " (uploaded " + m.uploaded + " by " + m.by + ")";
dupList.appendChild(li);
});
dupWarn.hidden = false;
dupVisible = true;
dupAck.checked = false;
updateSubmitState();
})
.catch(hideDup);
}
function hideDup() {
dupWarn.hidden = true;
dupVisible = false;
updateSubmitState();
}
amount.addEventListener("change", checkDuplicates);
dateEl.addEventListener("change", checkDuplicates);
checkDuplicates(); // in case the form was pre-filled (e.g. re-render after error)
})();
< / script >
{{if .ClassifyEnabled}}
< script >
(function () {
var fileInput = document.getElementById("receipt");
var skip = document.getElementById("skip-ai");
var note = document.getElementById("classify-note");
var model = {{.ClassifyModel}};
var baseNote = note.innerHTML;
skip.addEventListener("change", function () {
note.style.opacity = skip.checked ? "0.5" : "";
if (skip.checked) note.textContent = "AI auto-fill skipped — enter the fields by hand.";
else note.innerHTML = baseNote;
});
fileInput.addEventListener("change", function () {
var file = fileInput.files[0];
if (!file || skip.checked) return;
note.style.opacity = "";
note.textContent = "Reading receipt with " + model + "…";
var body = new FormData();
body.append("receipt", file);
fetch("/classify", { method: "POST", body: body })
.then(function (resp) {
if (!resp.ok) throw new Error("classify failed (" + resp.status + ")");
return resp.json();
})
.then(function (d) {
if (d.amount) setValue("amount", d.amount);
if (d.date) setValue("receipt_date", d.date);
if (d.category_id != null) setValue("category_id", String(d.category_id));
if (d.person_id != null) setValue("person_id", String(d.person_id));
var cost = (d.cost_cents != null) ? " · " + d.cost_cents.toFixed(2) + "¢" : "";
note.textContent = "Pre-filled by " + (d.model || model) + cost + " — review and submit.";
// Pre-filled date+amount may match an existing receipt — re-check.
document.getElementById("receipt_date").dispatchEvent(new Event("change"));
})
.catch(function (err) {
note.innerHTML = baseNote;
var e = document.createElement("span");
e.className = "err-inline";
e.textContent = " Auto-fill failed: " + err.message + ". Enter the fields manually.";
note.appendChild(e);
});
});
function setValue(id, val) {
var el = document.getElementById(id);
if (el) el.value = val;
}
})();
< / script >
{{end}}
2026-06-18 01:40:12 +00:00
{{end}}