diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index f60d23d..7348b98 100644 --- a/PROJECT_STATE.md +++ b/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. diff --git a/README.md b/README.md index 264270d..8e8469c 100644 --- a/README.md +++ b/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. diff --git a/VERSION b/VERSION index c03bb3d..ad55eb8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.9 +v3.0.0 diff --git a/backend/app.py b/backend/app.py index 92b02e1..aeec769 100644 --- a/backend/app.py +++ b/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", ) diff --git a/templates/portal_dashboard.html b/templates/portal_dashboard.html index aeecda5..e7760bf 100644 --- a/templates/portal_dashboard.html +++ b/templates/portal_dashboard.html @@ -3,6 +3,7 @@ {% block title %}Client Dashboard - OutsideTheBox{% endblock %} {% block portal_content %} +

Client Dashboard

@@ -13,7 +14,7 @@
Services Here - Download All Invoices + Customer Support Logout
@@ -56,6 +57,7 @@ + @@ -67,6 +69,9 @@ {% for row in invoices %} + {% else %} - + {% endfor %}
Invoice Status Created
+ + {{ row.invoice_number or ("INV-" ~ row.id) }} @@ -96,18 +101,49 @@
No invoices available.No invoices available.
+ {% endblock %} {% block scripts %} {% endblock %}