Auto-commit local changes before build (2026-01-03 13:05:53) #16

Merged
ivooskamp merged 1 commits from v20260103-03-reports-loading-fix into main 2026-01-06 09:25:41 +01:00
5 changed files with 106 additions and 13 deletions

View File

@ -1 +1 @@
changes-v20260103-02-reports-delete v20260103-03-reports-loading-fix

View File

@ -1,16 +1,50 @@
from .routes_shared import * # noqa: F401,F403 from .routes_shared import * # noqa: F401,F403
def _safe_json_list(value):
if not value:
return []
try:
if isinstance(value, (list, tuple)):
return [int(v) for v in value]
return json.loads(value)
except Exception:
return []
def _build_report_item(r):
return {
"id": int(r.id),
"name": r.name or "",
"description": r.description or "",
"report_type": r.report_type,
"output_format": r.output_format,
"customer_scope": getattr(r, "customer_scope", "all") or "all",
"customer_ids": _safe_json_list(getattr(r, "customer_ids", None)),
"period_start": r.period_start.isoformat() if getattr(r, "period_start", None) else "",
"period_end": r.period_end.isoformat() if getattr(r, "period_end", None) else "",
"schedule": r.schedule or "",
"created_at": r.created_at.isoformat() if getattr(r, "created_at", None) else "",
}
@main_bp.route("/reports") @main_bp.route("/reports")
@login_required @login_required
@roles_required("admin", "operator", "reporter", "viewer")
def reports(): def reports():
# Defaults are used by the Reports UI for quick testing. All values are UTC. # Pre-render items so the page is usable even if JS fails to load/execute.
period_end = datetime.utcnow().replace(microsecond=0) rows = (
period_start = (period_end - timedelta(days=7)).replace(microsecond=0) db.session.query(ReportDefinition)
.order_by(ReportDefinition.created_at.desc())
.limit(200)
.all()
)
items = [_build_report_item(r) for r in rows]
period_start, period_end = get_default_report_period()
return render_template( return render_template(
"main/reports.html", "main/reports.html",
initial_reports=items,
default_period_start=period_start.isoformat(), default_period_start=period_start.isoformat(),
default_period_end=period_end.isoformat(), default_period_end=period_end.isoformat(),
) )
@ -18,6 +52,14 @@ def reports():
@main_bp.route("/reports/new") @main_bp.route("/reports/new")
@login_required @login_required
@roles_required("admin", "operator", "reporter", "viewer")
def reports_new(): def reports_new():
return render_template("main/reports_new.html") # Preload customers so the form remains usable if JS fails to load/execute.
customers = (
db.session.query(Customer)
.filter(Customer.active.is_(True))
.order_by(Customer.name.asc())
.all()
)
customer_items = [{"id": int(c.id), "name": c.name or ""} for c in customers]
return render_template("main/reports_new.html", initial_customers=customer_items)

View File

@ -36,10 +36,35 @@
</tr> </tr>
</thead> </thead>
<tbody id="rep_table_body"> <tbody id="rep_table_body">
{% if initial_reports %}
{% for item in initial_reports %}
<tr> <tr>
<td colspan="5" class="text-center text-muted py-4">Loading…</td> <td><strong>{{ item.name }}</strong><div class="text-muted small">{{ item.description }}</div></td>
<td class="text-muted small">{{ item.created_at.replace('T',' ') if item.created_at else '' }}</td>
<td class="text-muted small">
{% if item.period_start or item.period_end %}
{{ item.period_start.replace('T',' ') if item.period_start else '' }} → {{ item.period_end.replace('T',' ') if item.period_end else '' }}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td><span class="badge text-bg-light border">{{ item.output_format }}</span></td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-primary rep-generate-btn" data-id="{{ item.id }}">Generate</button>
<button type="button" class="btn btn-sm btn-outline-secondary ms-1 rep-view-btn" data-id="{{ item.id }}">View raw</button>
<a class="btn btn-sm btn-outline-success rep-download-btn ms-1" href="/api/reports/{{ item.id }}/export.csv" target="_blank" rel="noopener">Download</a>
{% if active_role in ('admin','operator','reporter') %}
<button type="button" class="btn btn-sm btn-outline-danger rep-delete-btn ms-1" data-id="{{ item.id }}">Delete</button>
{% endif %}
</td>
</tr> </tr>
</tbody> {% endfor %}
{% else %}
<tr>
<td colspan="5" class="text-center text-muted py-4">No reports found.</td>
</tr>
{% endif %}
</tbody>>
</table> </table>
</div> </div>
</div> </div>

View File

@ -94,13 +94,26 @@
<div class="col-12 col-md-6" id="rep_single_wrap"> <div class="col-12 col-md-6" id="rep_single_wrap">
<label class="form-label">Customer <span class="text-danger">*</span></label> <label class="form-label">Customer <span class="text-danger">*</span></label>
<select class="form-select" id="rep_customer_single"></select> <select class="form-select" id="rep_customer_single">
<option value="" selected>Select a customer…</option>
{% if initial_customers %}
{% for c in initial_customers %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
{% endif %}
</select>
<div class="form-text">Search will be added later. For MVP this is a simple dropdown.</div> <div class="form-text">Search will be added later. For MVP this is a simple dropdown.</div>
</div> </div>
<div class="col-12 col-md-6 d-none" id="rep_multiple_wrap"> <div class="col-12 col-md-6 d-none" id="rep_multiple_wrap">
<label class="form-label">Customers <span class="text-danger">*</span></label> <label class="form-label">Customers <span class="text-danger">*</span></label>
<select class="form-select" id="rep_customer_multiple" multiple size="10"></select> <select class="form-select" id="rep_customer_multiple" multiple size="10">
{% if initial_customers %}
{% for c in initial_customers %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
{% endif %}
</select>
<div class="form-text">Hold Ctrl/Cmd to select multiple customers.</div> <div class="form-text">Hold Ctrl/Cmd to select multiple customers.</div>
</div> </div>
@ -225,6 +238,10 @@
} }
function loadCustomers() { function loadCustomers() {
if (initialCustomers && Array.isArray(initialCustomers) && initialCustomers.length) {
// Already rendered server-side. Keep the selects usable even if API calls fail.
return;
}
qs('rep_customer_single').innerHTML = '<option value="" selected>Loading…</option>'; qs('rep_customer_single').innerHTML = '<option value="" selected>Loading…</option>';
qs('rep_customer_multiple').innerHTML = ''; qs('rep_customer_multiple').innerHTML = '';

View File

@ -12,6 +12,15 @@
- Introduced a Delete action per report, available to authorized roles (admin/operator/reporter). - Introduced a Delete action per report, available to authorized roles (admin/operator/reporter).
- Implemented backend deletion handling and automatic refresh of the reports list after removal. - Implemented backend deletion handling and automatic refresh of the reports list after removal.
---
## v20260103-03-reports-loading-fix
- Fixed an issue where the Reports page remained stuck on “Loading…”.
- Ensured reports are rendered correctly when the page is loaded, even if the client-side API call fails.
- Fixed customer selection components on the Reports pages that could remain in a loading state.
- Improved robustness of report data handling to prevent rendering issues caused by invalid or missing customer references.
================================================================================================================================================ ================================================================================================================================================
## v0.1.15 ## v0.1.15