Add project filter and zip export with photos

- Filter dropdown to display receipts by project
- Total banner updates to reflect filtered view
- Export produces a zip with Excel + receipt photos
- Photo filenames use project_date_amount_category format

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 00:11:58 -06:00
parent be2ac4eaf7
commit fde6fcb724
2 changed files with 126 additions and 12 deletions

View File

@@ -484,6 +484,22 @@
border-radius: 4px;
}
.manage-item button:hover { background: #fee; }
/* Filter bar */
.filter-bar {
max-width: 600px;
margin: 0 auto;
padding: 8px 16px 0;
}
.filter-bar select {
width: 100%;
padding: 8px 12px;
border: 1.5px solid #ddd;
border-radius: 8px;
font-size: 0.9rem;
background: #fafafa;
color: #333;
}
</style>
</head>
<body>
@@ -499,6 +515,12 @@
</div>
</header>
<div class="filter-bar" id="filter-bar" style="display:none;">
<select id="filter-project">
<option value="">All receipts</option>
</select>
</div>
<div class="container" id="receipt-list">
<div class="empty-state" id="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 14l-4 4m0 0l4 4m-4-4h11a4 4 0 000-8h-1"/></svg>
@@ -662,9 +684,12 @@
let editingId = null;
let currentPhotoBlob = null; // Blob from resized image
let currentPhotoUrl = null; // Object URL or /api/photos/<id> URL for display
let activeFilter = ""; // "" = all, "none" = no project, or a project id
// DOM refs
const list = document.getElementById("receipt-list");
const filterBar = document.getElementById("filter-bar");
const filterProject = document.getElementById("filter-project");
const emptyState = document.getElementById("empty-state");
const overlay = document.getElementById("modal-overlay");
const modalTitle = document.getElementById("modal-title");
@@ -689,7 +714,8 @@
}
function updateTotal() {
const total = receipts.reduce((s, r) => s + Number(r.amount), 0);
const visible = filteredReceipts();
const total = visible.reduce((s, r) => s + Number(r.amount), 0);
totalBanner.textContent = "Total: " + formatMoney(total);
}
@@ -701,6 +727,29 @@
return settings.customers.find(c => c.id === id) || null;
}
function renderFilterDropdown() {
const prev = activeFilter;
let html = '<option value="">All receipts</option><option value="none">No project</option>';
settings.customers.forEach(c => {
const projects = settings.projects.filter(p => p.customerId === c.id);
if (projects.length === 0) return;
html += `<optgroup label="${escapeHtml(c.name)}">`;
projects.forEach(p => {
const sel = p.id === prev ? " selected" : "";
html += `<option value="${p.id}"${sel}>${escapeHtml(p.name)}</option>`;
});
html += '</optgroup>';
});
filterProject.innerHTML = html;
filterBar.style.display = settings.projects.length > 0 ? "" : "none";
}
function filteredReceipts() {
if (!activeFilter) return receipts;
if (activeFilter === "none") return receipts.filter(r => !r.projectId);
return receipts.filter(r => r.projectId === activeFilter);
}
function renderProjectDropdown(selectedId) {
let html = '<option value="">No project</option>';
settings.customers.forEach(c => {
@@ -724,15 +773,17 @@
function render() {
list.querySelectorAll(".receipt-card").forEach(el => el.remove());
renderFilterDropdown();
if (receipts.length === 0) {
const visible = filteredReceipts();
if (visible.length === 0) {
emptyState.style.display = "";
updateTotal();
return;
}
emptyState.style.display = "none";
[...receipts].reverse().forEach(r => {
[...visible].reverse().forEach(r => {
const card = document.createElement("div");
card.className = "receipt-card";
@@ -1333,8 +1384,11 @@
// Export to Excel
document.getElementById("export-btn").addEventListener("click", () => {
if (receipts.length === 0) { alert("No receipts to export."); return; }
window.location.href = "/api/export";
const visible = filteredReceipts();
if (visible.length === 0) { alert("No receipts to export."); return; }
let url = "/api/export";
if (activeFilter) url += "?project=" + encodeURIComponent(activeFilter);
window.location.href = url;
});
// --- Manage Customers & Projects ---
@@ -1392,6 +1446,11 @@
});
}
filterProject.addEventListener("change", () => {
activeFilter = filterProject.value;
render();
});
document.getElementById("manage-btn").addEventListener("click", () => {
renderManageModal();
manageOverlay.classList.add("open");