feat: enhance project and scenario detail pages with metrics, improved layouts, and updated styles
This commit is contained in:
@@ -12,3 +12,4 @@
|
|||||||
- Added scenario comparison validator, FastAPI comparison endpoint, and comprehensive unit tests to enforce FR-009 validation rules through API errors.
|
- Added scenario comparison validator, FastAPI comparison endpoint, and comprehensive unit tests to enforce FR-009 validation rules through API errors.
|
||||||
- Delivered a new dashboard experience with `templates/dashboard.html`, dedicated styling, and a FastAPI route supplying real project/scenario metrics via repository helpers.
|
- Delivered a new dashboard experience with `templates/dashboard.html`, dedicated styling, and a FastAPI route supplying real project/scenario metrics via repository helpers.
|
||||||
- Extended repositories with count/recency utilities and added pytest coverage, including a dashboard rendering smoke test validating empty-state messaging.
|
- Extended repositories with count/recency utilities and added pytest coverage, including a dashboard rendering smoke test validating empty-state messaging.
|
||||||
|
- Brought project and scenario detail pages plus their forms in line with the dashboard visuals, adding metric cards, layout grids, and refreshed CTA styles.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from dependencies import get_unit_of_work
|
from dependencies import get_unit_of_work
|
||||||
from models import MiningOperationType, Project
|
from models import MiningOperationType, Project, ScenarioStatus
|
||||||
from schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
|
from schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
|
||||||
from services.exceptions import EntityConflictError, EntityNotFoundError
|
from services.exceptions import EntityConflictError, EntityNotFoundError
|
||||||
from services.unit_of_work import UnitOfWork
|
from services.unit_of_work import UnitOfWork
|
||||||
@@ -205,12 +205,23 @@ def view_project(
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
scenarios = sorted(project.scenarios, key=lambda s: s.created_at)
|
scenarios = sorted(project.scenarios, key=lambda s: s.created_at)
|
||||||
|
scenario_stats = {
|
||||||
|
"total": len(scenarios),
|
||||||
|
"active": sum(1 for scenario in scenarios if scenario.status == ScenarioStatus.ACTIVE),
|
||||||
|
"draft": sum(1 for scenario in scenarios if scenario.status == ScenarioStatus.DRAFT),
|
||||||
|
"archived": sum(1 for scenario in scenarios if scenario.status == ScenarioStatus.ARCHIVED),
|
||||||
|
"latest_update": max(
|
||||||
|
(scenario.updated_at for scenario in scenarios if scenario.updated_at),
|
||||||
|
default=None,
|
||||||
|
),
|
||||||
|
}
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"projects/detail.html",
|
"projects/detail.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenarios": scenarios,
|
"scenarios": scenarios,
|
||||||
|
"scenario_stats": scenario_stats,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -339,12 +339,20 @@ def view_scenario(
|
|||||||
scenario.simulation_parameters, key=lambda item: item.created_at
|
scenario.simulation_parameters, key=lambda item: item.created_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
scenario_metrics = {
|
||||||
|
"financial_count": len(financial_inputs),
|
||||||
|
"parameter_count": len(simulation_parameters),
|
||||||
|
"currency": scenario.currency,
|
||||||
|
"primary_resource": scenario.primary_resource.value.replace('_', ' ').title() if scenario.primary_resource else None,
|
||||||
|
}
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"scenarios/detail.html",
|
"scenarios/detail.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenario": scenario,
|
"scenario": scenario,
|
||||||
|
"scenario_metrics": scenario_metrics,
|
||||||
"financial_inputs": financial_inputs,
|
"financial_inputs": financial_inputs,
|
||||||
"simulation_parameters": simulation_parameters,
|
"simulation_parameters": simulation_parameters,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,23 +4,59 @@
|
|||||||
--hover-highlight: rgba(241, 178, 26, 0.12);
|
--hover-highlight: rgba(241, 178, 26, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-table {
|
.header-actions {
|
||||||
width: 100%;
|
display: flex;
|
||||||
border-collapse: collapse;
|
gap: 0.75rem;
|
||||||
border-radius: var(--table-radius);
|
flex-wrap: wrap;
|
||||||
overflow: hidden;
|
justify-content: flex-end;
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-table th,
|
.project-metrics {
|
||||||
.projects-table td {
|
display: grid;
|
||||||
padding: 0.875rem 1rem;
|
gap: 1.5rem;
|
||||||
border-bottom: 1px solid var(--card-border);
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-table tbody tr:hover {
|
.metric-card h2 {
|
||||||
background: var(--hover-highlight);
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-caption {
|
||||||
|
color: var(--color-text-subtle);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-form {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 1.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.definition-list {
|
.definition-list {
|
||||||
@@ -62,6 +98,61 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: var(--table-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-radius: var(--table-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--card-border);
|
||||||
|
background: rgba(21, 27, 35, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: var(--hover-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-link {
|
||||||
|
color: var(--brand-2);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-link:hover,
|
||||||
|
.table-link:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.project-layout {
|
||||||
|
grid-template-columns: 1.1fr 1.9fr;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|||||||
@@ -37,44 +37,48 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.scenario-metrics {
|
||||||
display: inline-flex;
|
display: grid;
|
||||||
align-items: center;
|
gap: 1.5rem;
|
||||||
justify-content: center;
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
padding: 0.6rem 1.1rem;
|
margin-bottom: 2rem;
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.metric-card {
|
||||||
background: linear-gradient(90deg, var(--brand) 0%, var(--brand-2) 100%);
|
background: rgba(21, 27, 35, 0.85);
|
||||||
color: var(--color-text-dark);
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 18px rgba(241, 178, 26, 0.25);
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.metric-card h2 {
|
||||||
background: rgba(148, 197, 255, 0.2);
|
margin: 0;
|
||||||
color: var(--color-text-invert);
|
font-size: 1rem;
|
||||||
border: 1px solid rgba(148, 197, 255, 0.35);
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link {
|
.metric-value {
|
||||||
padding: 0.35rem 0.5rem;
|
font-size: 2rem;
|
||||||
color: var(--brand-3);
|
font-weight: 700;
|
||||||
text-decoration: none;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover,
|
.metric-caption {
|
||||||
.btn:focus {
|
color: var(--color-text-subtle);
|
||||||
transform: translateY(-1px);
|
font-size: 0.85rem;
|
||||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-filters {
|
.scenario-filters {
|
||||||
@@ -106,6 +110,17 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scenario-form {
|
||||||
|
background: rgba(21, 27, 35, 0.85);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 1.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -159,3 +174,24 @@
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scenario-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.header-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-layout {
|
||||||
|
grid-template-columns: 1.1fr 1.9fr;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,12 +16,36 @@
|
|||||||
<h1>{{ project.name }}</h1>
|
<h1>{{ project.name }}</h1>
|
||||||
<p class="text-muted">{{ project.operation_type.value.replace('_', ' ') | title }}</p>
|
<p class="text-muted">{{ project.operation_type.value.replace('_', ' ') | title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="header-actions">
|
||||||
<a class="btn btn-secondary" href="{{ url_for('projects.edit_project_form', project_id=project.id) }}">Edit</a>
|
<a class="btn" href="{{ url_for('projects.edit_project_form', project_id=project.id) }}">Edit Project</a>
|
||||||
<a class="btn btn-primary" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">New Scenario</a>
|
<a class="btn primary" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">New Scenario</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="project-metrics">
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Total Scenarios</h2>
|
||||||
|
<p class="metric-value">{{ scenario_stats.total }}</p>
|
||||||
|
<span class="metric-caption">Across this project</span>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Active</h2>
|
||||||
|
<p class="metric-value">{{ scenario_stats.active }}</p>
|
||||||
|
<span class="metric-caption">Currently live analyses</span>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Draft</h2>
|
||||||
|
<p class="metric-value">{{ scenario_stats.draft }}</p>
|
||||||
|
<span class="metric-caption">Awaiting validation</span>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Archived</h2>
|
||||||
|
<p class="metric-value">{{ scenario_stats.archived }}</p>
|
||||||
|
<span class="metric-caption">Historical references</span>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="project-layout">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Project Overview</h2>
|
<h2>Project Overview</h2>
|
||||||
<dl class="definition-list">
|
<dl class="definition-list">
|
||||||
@@ -41,15 +65,20 @@
|
|||||||
<dt>Updated</dt>
|
<dt>Updated</dt>
|
||||||
<dd>{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
<dd>{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Latest Scenario Update</dt>
|
||||||
|
<dd>{{ scenario_stats.latest_update.strftime('%Y-%m-%d %H:%M') if scenario_stats.latest_update else '—' }}</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<h2>Scenarios</h2>
|
<h2>Scenarios</h2>
|
||||||
<a class="btn btn-link" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">Add Scenario</a>
|
<a class="btn" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">Add Scenario</a>
|
||||||
</header>
|
</header>
|
||||||
{% if scenarios %}
|
{% if scenarios %}
|
||||||
|
<div class="table-responsive">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -57,7 +86,7 @@
|
|||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Currency</th>
|
<th>Currency</th>
|
||||||
<th>Primary Resource</th>
|
<th>Primary Resource</th>
|
||||||
<th></th>
|
<th class="text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -68,15 +97,17 @@
|
|||||||
<td>{{ scenario.currency or '—' }}</td>
|
<td>{{ scenario.currency or '—' }}</td>
|
||||||
<td>{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}</td>
|
<td>{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<a class="btn btn-link" href="{{ url_for('scenarios.view_scenario', scenario_id=scenario.id) }}">View</a>
|
<a class="table-link" href="{{ url_for('scenarios.view_scenario', scenario_id=scenario.id) }}">View</a>
|
||||||
<a class="btn btn-link" href="{{ url_for('scenarios.edit_scenario_form', scenario_id=scenario.id) }}">Edit</a>
|
<a class="table-link" href="{{ url_for('scenarios.edit_scenario_form', scenario_id=scenario.id) }}">Edit</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No scenarios yet.</p>
|
<p class="empty-state">No scenarios yet. <a href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">Create the first scenario.</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -21,13 +21,22 @@
|
|||||||
<h1>{% if project %}Edit Project{% else %}Create Project{% endif %}</h1>
|
<h1>{% if project %}Edit Project{% else %}Create Project{% endif %}</h1>
|
||||||
<p class="text-muted">Provide core information about the mining project.</p>
|
<p class="text-muted">Provide core information about the mining project.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||||
|
<button class="btn primary" type="submit">Save Project</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error">{{ error }}</div>
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="form" method="post" action="{{ form_action }}">
|
{% if error %}
|
||||||
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="form project-form" method="post" action="{{ form_action }}">
|
||||||
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input id="name" name="name" type="text" required value="{{ project.name if project else '' }}" />
|
<input id="name" name="name" type="text" required value="{{ project.name if project else '' }}" />
|
||||||
@@ -46,15 +55,16 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
<textarea id="description" name="description" rows="4">{{ project.description if project else '' }}</textarea>
|
<textarea id="description" name="description" rows="5">{{ project.description if project else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a class="btn btn-secondary" href="{{ cancel_url }}">Cancel</a>
|
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||||
<button class="btn btn-primary" type="submit">Save</button>
|
<button class="btn primary" type="submit">Save Project</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -17,9 +17,36 @@
|
|||||||
<h1>{{ scenario.name }}</h1>
|
<h1>{{ scenario.name }}</h1>
|
||||||
<p class="text-muted">Status: {{ scenario.status.value.title() }}</p>
|
<p class="text-muted">Status: {{ scenario.status.value.title() }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-secondary" href="{{ url_for('scenarios.edit_scenario_form', scenario_id=scenario.id) }}">Edit</a>
|
<div class="header-actions">
|
||||||
|
<a class="btn" href="{{ url_for('projects.view_project', project_id=project.id) }}">Back to Project</a>
|
||||||
|
<a class="btn primary" href="{{ url_for('scenarios.edit_scenario_form', scenario_id=scenario.id) }}">Edit Scenario</a>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="scenario-metrics">
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Financial Inputs</h2>
|
||||||
|
<p class="metric-value">{{ scenario_metrics.financial_count }}</p>
|
||||||
|
<span class="metric-caption">Line items captured</span>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Simulation Parameters</h2>
|
||||||
|
<p class="metric-value">{{ scenario_metrics.parameter_count }}</p>
|
||||||
|
<span class="metric-caption">Inputs driving forecasts</span>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Currency</h2>
|
||||||
|
<p class="metric-value">{{ scenario_metrics.currency or '—' }}</p>
|
||||||
|
<span class="metric-caption">Financial reporting</span>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<h2>Primary Resource</h2>
|
||||||
|
<p class="metric-value">{{ scenario_metrics.primary_resource or '—' }}</p>
|
||||||
|
<span class="metric-caption">Scenario focus</span>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="scenario-layout">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Scenario Details</h2>
|
<h2>Scenario Details</h2>
|
||||||
<dl class="definition-list">
|
<dl class="definition-list">
|
||||||
@@ -30,17 +57,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<dt>Timeline</dt>
|
<dt>Timeline</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if scenario.start_date %}
|
{{ scenario.start_date or '—' }} → {{ scenario.end_date or '—' }}
|
||||||
{{ scenario.start_date }}
|
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
|
||||||
→
|
|
||||||
{% if scenario.end_date %}
|
|
||||||
{{ scenario.end_date }}
|
|
||||||
{% else %}
|
|
||||||
—
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -48,12 +65,8 @@
|
|||||||
<dd>{{ scenario.discount_rate or '—' }}</dd>
|
<dd>{{ scenario.discount_rate or '—' }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Currency</dt>
|
<dt>Last Updated</dt>
|
||||||
<dd>{{ scenario.currency or '—' }}</dd>
|
<dd>{{ scenario.updated_at.strftime('%Y-%m-%d %H:%M') if scenario.updated_at else '—' }}</dd>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Primary Resource</dt>
|
|
||||||
<dd>{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}</dd>
|
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
@@ -84,7 +97,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No financial inputs recorded.</p>
|
<p class="empty-state">No financial inputs recorded yet.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -114,7 +127,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No simulation parameters defined.</p>
|
<p class="empty-state">No simulation parameters defined.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -21,13 +21,17 @@
|
|||||||
<h1>{% if scenario %}Edit Scenario{% else %}Create Scenario{% endif %}</h1>
|
<h1>{% if scenario %}Edit Scenario{% else %}Create Scenario{% endif %}</h1>
|
||||||
<p class="text-muted">Configure assumptions and metadata for this scenario.</p>
|
<p class="text-muted">Configure assumptions and metadata for this scenario.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||||
|
<button class="btn primary" type="submit">Save Scenario</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error">{{ error }}</div>
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="form" method="post" action="{{ form_action }}">
|
<form class="form scenario-form" method="post" action="{{ form_action }}">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
@@ -76,12 +80,12 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
<textarea id="description" name="description" rows="4">{{ scenario.description if scenario else '' }}</textarea>
|
<textarea id="description" name="description" rows="5">{{ scenario.description if scenario else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a class="btn btn-secondary" href="{{ cancel_url }}">Cancel</a>
|
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||||
<button class="btn btn-primary" type="submit">Save</button>
|
<button class="btn primary" type="submit">Save Scenario</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user