Compare commits
2 Commits
82c67f6b01
...
a339540f4c
| Author | SHA1 | Date | |
|---|---|---|---|
| a339540f4c | |||
| 750815dcff |
@ -1 +1 @@
|
||||
changes-v20260103-02-reports-delete
|
||||
v20260103-03-reports-loading-fix
|
||||
|
||||
@ -1,16 +1,50 @@
|
||||
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")
|
||||
@login_required
|
||||
@roles_required("admin", "operator", "reporter", "viewer")
|
||||
def reports():
|
||||
# Defaults are used by the Reports UI for quick testing. All values are UTC.
|
||||
period_end = datetime.utcnow().replace(microsecond=0)
|
||||
period_start = (period_end - timedelta(days=7)).replace(microsecond=0)
|
||||
# Pre-render items so the page is usable even if JS fails to load/execute.
|
||||
rows = (
|
||||
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(
|
||||
"main/reports.html",
|
||||
initial_reports=items,
|
||||
default_period_start=period_start.isoformat(),
|
||||
default_period_end=period_end.isoformat(),
|
||||
)
|
||||
@ -18,6 +52,14 @@ def reports():
|
||||
|
||||
@main_bp.route("/reports/new")
|
||||
@login_required
|
||||
@roles_required("admin", "operator", "reporter", "viewer")
|
||||
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)
|
||||
|
||||
@ -36,10 +36,35 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rep_table_body">
|
||||
{% if initial_reports %}
|
||||
{% for item in initial_reports %}
|
||||
<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>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">No reports found.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -94,13 +94,26 @@
|
||||
|
||||
<div class="col-12 col-md-6" id="rep_single_wrap">
|
||||
<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>
|
||||
|
||||
<div class="col-12 col-md-6 d-none" id="rep_multiple_wrap">
|
||||
<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>
|
||||
|
||||
@ -225,6 +238,10 @@
|
||||
}
|
||||
|
||||
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_multiple').innerHTML = '';
|
||||
|
||||
|
||||
@ -12,6 +12,15 @@
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user