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
|
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)
|
||||||
|
|||||||
@ -36,10 +36,35 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="rep_table_body">
|
<tbody id="rep_table_body">
|
||||||
<tr>
|
{% if initial_reports %}
|
||||||
<td colspan="5" class="text-center text-muted py-4">Loading…</td>
|
{% for item in initial_reports %}
|
||||||
</tr>
|
<tr>
|
||||||
</tbody>
|
<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>
|
||||||
|
{% 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>
|
||||||
|
|||||||
@ -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 = '';
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user