Browse Source

Bump to v3.0.0 selected portal invoice downloads

main
def 3 weeks ago
parent
commit
f621a87c5d
  1. 16
      PROJECT_STATE.md
  2. 16
      README.md
  3. 2
      VERSION
  4. 41
      backend/app.py
  5. 42
      templates/portal_dashboard.html

16
PROJECT_STATE.md

@ -1,3 +1,19 @@
## v3.0.0 selected portal invoice downloads - 2026-05-29 UTC
- Added invoice selection checkboxes to the client portal dashboard.
- Added a select-all checkbox in the portal invoice table header.
- Converted portal invoice ZIP download from all-only GET behavior to GET/POST behavior.
- Clicking “Download All Invoices” with no invoices selected still downloads all portal invoices.
- Clicking “Download All Invoices” with checked invoices downloads only the selected invoices.
- Selected downloads use `selected_invoices.zip`; full downloads continue using `all_invoices.zip`.
- Auto-refresh now pauses when one or more invoice checkboxes are selected so the page does not reload during selection.
- Existing portal invoice authorization is preserved: downloads are restricted to the logged-in client’s own invoices.
Verified:
- No selected invoices downloads all invoices.
- Selected invoices download only those invoice PDFs.
- Header checkbox selects/deselects visible invoices.
## v2.0.9 portal account-credit payments - 2026-05-29 UTC
- Added portal “Use available credit” payment option for unpaid CAD invoices.

16
README.md

@ -1,3 +1,19 @@
## v3.0.0 selected portal invoice downloads - 2026-05-29 UTC
- Added invoice selection checkboxes to the client portal dashboard.
- Added a select-all checkbox in the portal invoice table header.
- Converted portal invoice ZIP download from all-only GET behavior to GET/POST behavior.
- Clicking “Download All Invoices” with no invoices selected still downloads all portal invoices.
- Clicking “Download All Invoices” with checked invoices downloads only the selected invoices.
- Selected downloads use `selected_invoices.zip`; full downloads continue using `all_invoices.zip`.
- Auto-refresh now pauses when one or more invoice checkboxes are selected so the page does not reload during selection.
- Existing portal invoice authorization is preserved: downloads are restricted to the logged-in client’s own invoices.
Verified:
- No selected invoices downloads all invoices.
- Selected invoices download only those invoice PDFs.
- Header checkbox selects/deselects visible invoices.
## v2.0.9 portal account-credit payments - 2026-05-29 UTC
- Added portal “Use available credit” payment option for unpaid CAD invoices.

2
VERSION

@ -1 +1 @@
v2.0.9
v3.0.0

41
backend/app.py

@ -5463,7 +5463,7 @@ def portal_set_password():
@app.route("/portal/invoices/download-all")
@app.route("/portal/invoices/download-all", methods=["GET", "POST"])
def portal_download_all_invoices():
import io
import zipfile
@ -5471,19 +5471,44 @@ def portal_download_all_invoices():
client = _portal_current_client()
if not client:
return redirect("/portal")
if portal_terms_required(client):
return redirect("/portal/terms")
selected_ids = []
if request.method == "POST":
for raw_id in request.form.getlist("invoice_ids"):
raw_id = str(raw_id or "").strip()
if raw_id.isdigit():
selected_ids.append(int(raw_id))
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT id, invoice_number
FROM invoices
WHERE client_id = %s
ORDER BY id
""", (client["id"],))
if selected_ids:
placeholders = ",".join(["%s"] * len(selected_ids))
cursor.execute(f"""
SELECT id, invoice_number
FROM invoices
WHERE client_id = %s
AND id IN ({placeholders})
ORDER BY id
""", tuple([client["id"]] + selected_ids))
zip_name = "selected_invoices.zip"
else:
cursor.execute("""
SELECT id, invoice_number
FROM invoices
WHERE client_id = %s
ORDER BY id
""", (client["id"],))
zip_name = "all_invoices.zip"
invoices = cursor.fetchall()
conn.close()
if not invoices:
return redirect("/portal/dashboard")
memory_file = io.BytesIO()
with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_DEFLATED) as zf:
@ -5499,7 +5524,7 @@ def portal_download_all_invoices():
return send_file(
memory_file,
download_name="all_invoices.zip",
download_name=zip_name,
as_attachment=True,
mimetype="application/zip",
)

42
templates/portal_dashboard.html

@ -3,6 +3,7 @@
{% block title %}Client Dashboard - OutsideTheBox{% endblock %}
{% block portal_content %}
<form id="portal-invoice-download-form" method="post" action="/portal/invoices/download-all">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
@ -13,7 +14,7 @@
<div class="portal-toolbar" style="display:flex;flex-direction:column;align-items:flex-end;gap:10px;">
<div style="display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap;">
<a class="portal-btn primary" href="/portal/services">Services Here</a>
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<button type="submit" class="portal-btn primary" style="border:0;cursor:pointer;">Download All Invoices</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
@ -56,6 +57,7 @@
<table class="portal-table">
<thead>
<tr>
<th style="width:44px;"><input type="checkbox" id="select-all-invoices" aria-label="Select all invoices"></th>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
@ -67,6 +69,9 @@
<tbody>
{% for row in invoices %}
<tr>
<td>
<input type="checkbox" class="invoice-select" name="invoice_ids" value="{{ row.id }}" aria-label="Select invoice {{ row.invoice_number or row.id }}">
</td>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
@ -96,18 +101,49 @@
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
<td colspan="7">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
const selectAll = document.getElementById("select-all-invoices");
const invoiceChecks = Array.from(document.querySelectorAll(".invoice-select"));
function anyChecked() {
return invoiceChecks.some(function(cb) { return cb.checked; });
}
if (selectAll) {
selectAll.addEventListener("change", function() {
invoiceChecks.forEach(function(cb) {
cb.checked = selectAll.checked;
});
});
}
invoiceChecks.forEach(function(cb) {
cb.addEventListener("change", function() {
if (!selectAll) {
return;
}
const checkedCount = invoiceChecks.filter(function(x) { return x.checked; }).length;
selectAll.checked = checkedCount === invoiceChecks.length && invoiceChecks.length > 0;
selectAll.indeterminate = checkedCount > 0 && checkedCount < invoiceChecks.length;
});
});
setTimeout(function() {
if (!anyChecked()) {
window.location.reload();
}
}, 20000);
})();
</script>
{% endblock %}

Loading…
Cancel
Save