Compare commits
No commits in common. "a339540f4c6e17d342099ab87251a4e79a3db43f" and "82c67f6b01eb7de7249e5b2903613484118972e4" have entirely different histories.
a339540f4c
...
82c67f6b01
@ -1 +1 @@
|
|||||||
v20260103-03-reports-loading-fix
|
changes-v20260103-02-reports-delete
|
||||||
|
|||||||
@ -1,50 +1,16 @@
|
|||||||
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():
|
||||||
# Pre-render items so the page is usable even if JS fails to load/execute.
|
# Defaults are used by the Reports UI for quick testing. All values are UTC.
|
||||||
rows = (
|
period_end = datetime.utcnow().replace(microsecond=0)
|
||||||
db.session.query(ReportDefinition)
|
period_start = (period_end - timedelta(days=7)).replace(microsecond=0)
|
||||||
.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(),
|
||||||
)
|
)
|
||||||
@ -52,14 +18,6 @@ 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():
|
||||||
# Preload customers so the form remains usable if JS fails to load/execute.
|
return render_template("main/reports_new.html")
|
||||||
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,35 +36,10 @@
|
|||||||
</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><strong>{{ item.name }}</strong><div class="text-muted small">{{ item.description }}</div></td>
|
<td colspan="5" class="text-center text-muted py-4">Loading…</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>
|
||||||
{% endfor %}
|
</tbody>
|
||||||
{% 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,26 +94,13 @@
|
|||||||
|
|
||||||
<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 class="form-select" id="rep_customer_single"></select>
|
||||||
<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 class="form-select" id="rep_customer_multiple" multiple size="10"></select>
|
||||||
{% 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>
|
||||||
|
|
||||||
@ -238,10 +225,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,15 +12,6 @@
|
|||||||
- 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