feat: implement export functionality for projects and scenarios with CSV and Excel support
This commit is contained in:
@@ -1,132 +1,178 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard · CalMiner{% endblock %}
|
||||
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
|
||||
block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css" />
|
||||
{% endblock %} {% block content %}
|
||||
<section class="page-header dashboard-header">
|
||||
<div>
|
||||
<h1>Welcome back</h1>
|
||||
<p class="page-subtitle">
|
||||
Monitor project progress and scenario insights at a glance.
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a class="btn primary" href="{{ url_for('projects.create_project_form') }}"
|
||||
>New Project</a
|
||||
>
|
||||
<a class="btn" href="#">Import Data</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header dashboard-header">
|
||||
<div>
|
||||
<h1>Welcome back</h1>
|
||||
<p class="page-subtitle">Monitor project progress and scenario insights at a glance.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a class="btn primary" href="{{ url_for('projects.create_project_form') }}">New Project</a>
|
||||
<a class="btn" href="#">Import Data</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-metrics">
|
||||
<article class="metric-card">
|
||||
<h2>Total Projects</h2>
|
||||
<p class="metric-value">{{ metrics.total_projects }}</p>
|
||||
<span class="metric-caption">Across all operation types</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<h2>Active Scenarios</h2>
|
||||
<p class="metric-value">{{ metrics.active_scenarios }}</p>
|
||||
<span class="metric-caption">Ready for analysis</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<h2>Pending Simulations</h2>
|
||||
<p class="metric-value">{{ metrics.pending_simulations }}</p>
|
||||
<span class="metric-caption">Awaiting execution</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<h2>Last Data Import</h2>
|
||||
<p class="metric-value">{{ metrics.last_import or '—' }}</p>
|
||||
<span class="metric-caption">UTC timestamp</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<div class="grid-main">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Recent Projects</h2>
|
||||
<a class="btn btn-link" href="{{ url_for('projects.project_list_page') }}">View all</a>
|
||||
</header>
|
||||
{% if recent_projects %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Operation</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for project in recent_projects %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="table-link" href="{{ url_for('projects.view_project', project_id=project.id) }}">{{ project.name }}</a>
|
||||
</td>
|
||||
<td>{{ project.operation_type.value.replace('_', ' ') | title }}</td>
|
||||
<td>{{ project.updated_at.strftime('%Y-%m-%d') if project.updated_at else '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty-state">No recent projects. <a href="{{ url_for('projects.create_project_form') }}">Create one now.</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Simulation Pipeline</h2>
|
||||
</header>
|
||||
{% if simulation_updates %}
|
||||
<ul class="timeline">
|
||||
{% for update in simulation_updates %}
|
||||
<li>
|
||||
<span class="timeline-label">{{ update.timestamp_label or '—' }}</span>
|
||||
<div>
|
||||
<strong>{{ update.title }}</strong>
|
||||
<p>{{ update.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">No simulation runs yet. Configure a scenario to start simulations.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="grid-sidebar">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Scenario Alerts</h2>
|
||||
</header>
|
||||
{% if scenario_alerts %}
|
||||
<ul class="alerts-list">
|
||||
{% for alert in scenario_alerts %}
|
||||
<li>
|
||||
<strong>{{ alert.title }}</strong>
|
||||
<p>{{ alert.message }}</p>
|
||||
{% if alert.link %}
|
||||
<a class="btn btn-link" href="{{ alert.link }}">Review</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">All scenarios look good. We'll highlight issues here.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Resources</h2>
|
||||
</header>
|
||||
<ul class="links-list">
|
||||
<li><a href="https://github.com/" target="_blank">CalMiner Repository</a></li>
|
||||
<li><a href="https://example.com/docs" target="_blank">Documentation</a></li>
|
||||
<li><a href="mailto:support@example.com">Contact Support</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
<section class="dashboard-metrics">
|
||||
<article class="metric-card">
|
||||
<h2>Total Projects</h2>
|
||||
<p class="metric-value">{{ metrics.total_projects }}</p>
|
||||
<span class="metric-caption">Across all operation types</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<h2>Active Scenarios</h2>
|
||||
<p class="metric-value">{{ metrics.active_scenarios }}</p>
|
||||
<span class="metric-caption">Ready for analysis</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<h2>Pending Simulations</h2>
|
||||
<p class="metric-value">{{ metrics.pending_simulations }}</p>
|
||||
<span class="metric-caption">Awaiting execution</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<h2>Last Data Import</h2>
|
||||
<p class="metric-value">{{ metrics.last_import or '—' }}</p>
|
||||
<span class="metric-caption">UTC timestamp</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<div class="grid-main">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Recent Projects</h2>
|
||||
<a
|
||||
class="btn btn-link"
|
||||
href="{{ url_for('projects.project_list_page') }}"
|
||||
>View all</a
|
||||
>
|
||||
</header>
|
||||
{% if recent_projects %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Operation</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for project in recent_projects %}
|
||||
<tr>
|
||||
<td class="table-cell-actions">
|
||||
<a
|
||||
class="table-link"
|
||||
href="{{ url_for('projects.view_project', project_id=project.id) }}"
|
||||
>{{ project.name }}</a
|
||||
>
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
data-export-trigger
|
||||
data-export-target="projects"
|
||||
title="Export projects dataset"
|
||||
>
|
||||
<span aria-hidden="true">⇩</span>
|
||||
<span class="sr-only">Export</span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{{ project.operation_type.value.replace('_', ' ') | title }}
|
||||
</td>
|
||||
<td>
|
||||
{{ project.updated_at.strftime('%Y-%m-%d') if project.updated_at
|
||||
else '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty-state">
|
||||
No recent projects.
|
||||
<a href="{{ url_for('projects.create_project_form') }}"
|
||||
>Create one now.</a
|
||||
>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Simulation Pipeline</h2>
|
||||
</header>
|
||||
{% if simulation_updates %}
|
||||
<ul class="timeline">
|
||||
{% for update in simulation_updates %}
|
||||
<li>
|
||||
<span class="timeline-label"
|
||||
>{{ update.timestamp_label or '—' }}</span
|
||||
>
|
||||
<div>
|
||||
<strong>{{ update.title }}</strong>
|
||||
<p>{{ update.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">
|
||||
No simulation runs yet. Configure a scenario to start simulations.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="grid-sidebar">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Scenario Alerts</h2>
|
||||
</header>
|
||||
{% if scenario_alerts %}
|
||||
<ul class="alerts-list">
|
||||
{% for alert in scenario_alerts %}
|
||||
<li>
|
||||
<strong>{{ alert.title }}</strong>
|
||||
<p>{{ alert.message }}</p>
|
||||
{% if alert.link %}
|
||||
<a class="btn btn-link" href="{{ alert.link }}">Review</a>
|
||||
{% endif %}
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
data-export-trigger
|
||||
data-export-target="scenarios"
|
||||
title="Export scenarios dataset"
|
||||
>
|
||||
<span aria-hidden="true">⇩</span>
|
||||
<span class="sr-only">Export</span>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">
|
||||
All scenarios look good. We'll highlight issues here.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Resources</h2>
|
||||
</header>
|
||||
<ul class="links-list">
|
||||
<li>
|
||||
<a href="https://github.com/" target="_blank">CalMiner Repository</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://example.com/docs" target="_blank">Documentation</a>
|
||||
</li>
|
||||
<li><a href="mailto:support@example.com">Contact Support</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}CalMiner{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css" />
|
||||
<link rel="stylesheet" href="/static/css/imports.css" />
|
||||
{% block head_extra %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
@@ -20,7 +21,9 @@
|
||||
</div>
|
||||
</div>
|
||||
{% block scripts %}{% endblock %}
|
||||
<script src="/static/js/projects.js" defer></script>
|
||||
<script src="/static/js/projects.js" defer></script>
|
||||
<script src="/static/js/exports.js" defer></script>
|
||||
<script src="/static/js/imports.js" defer></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
51
templates/exports/modal.html
Normal file
51
templates/exports/modal.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<div
|
||||
class="modal"
|
||||
id="export-modal-{{ dataset }}"
|
||||
data-export-dataset="{{ dataset }}"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Export {{ dataset|capitalize }}</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<form method="post" action="{{ submit_url }}" data-export-form>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="export-format">Format</label>
|
||||
<select class="form-select" id="export-format" name="format">
|
||||
<option value="csv">CSV</option>
|
||||
<option value="xlsx">Excel (.xlsx)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="true"
|
||||
id="include-metadata"
|
||||
name="include_metadata"
|
||||
/>
|
||||
<label class="form-check-label" for="include-metadata">
|
||||
Include metadata sheet (Excel only)
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted"
|
||||
>Filters can be adjusted in the advanced export section.</small
|
||||
>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">Download</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
10
templates/partials/alerts.html
Normal file
10
templates/partials/alerts.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% macro toast(id, hidden=True, level="info", message="") %}
|
||||
<div id="{{ id }}" class="toast toast--{{ level }}{% if hidden %} hidden{% endif %}" role="alert">
|
||||
<span class="toast__icon" aria-hidden="true"></span>
|
||||
<p class="toast__message">{{ message }}</p>
|
||||
<button type="button" class="toast__close" data-toast-close>
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Dismiss</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
17
templates/partials/import_preview_table.html
Normal file
17
templates/partials/import_preview_table.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% from "partials/components.html" import table_container %}
|
||||
|
||||
{% call table_container("import-preview-container", hidden=True, aria_label="Import preview table", heading=table_heading or "Preview Rows") %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Row</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Issues</th>
|
||||
{% for column in columns %}
|
||||
<th scope="col">{{ column }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-import-preview-body>
|
||||
<!-- Rows injected via JavaScript -->
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
25
templates/partials/import_upload.html
Normal file
25
templates/partials/import_upload.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% from "partials/components.html" import feedback %}
|
||||
|
||||
<section class="import-upload" data-import-upload>
|
||||
<header class="import-upload__header">
|
||||
<h3>{{ title or "Bulk Import" }}</h3>
|
||||
{% if description %}<p class="import-upload__description">{{ description }}</p>{% endif %}
|
||||
</header>
|
||||
|
||||
<div class="import-upload__dropzone" data-import-dropzone>
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<p>Drag & drop CSV/XLSX files here or</p>
|
||||
<label class="btn secondary">
|
||||
Browse
|
||||
<input type="file" name="import-file" accept=".csv,.xlsx" hidden />
|
||||
</label>
|
||||
<p class="import-upload__hint">Maximum size {{ max_size_hint or "10 MB" }}. UTF-8 encoding required.</p>
|
||||
</div>
|
||||
|
||||
<div class="import-upload__actions">
|
||||
<button type="button" class="btn primary" data-import-upload-trigger disabled>Upload & Preview</button>
|
||||
<button type="button" class="btn" data-import-reset hidden>Reset</button>
|
||||
</div>
|
||||
|
||||
{{ feedback("import-upload-feedback", hidden=True, role="alert") }}
|
||||
</section>
|
||||
Reference in New Issue
Block a user