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.
|
||||
- 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.
|
||||
- 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 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 services.exceptions import EntityConflictError, EntityNotFoundError
|
||||
from services.unit_of_work import UnitOfWork
|
||||
@@ -205,12 +205,23 @@ def view_project(
|
||||
) from exc
|
||||
|
||||
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(
|
||||
"projects/detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"project": project,
|
||||
"scenarios": scenarios,
|
||||
"scenario_stats": scenario_stats,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -339,12 +339,20 @@ def view_scenario(
|
||||
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(
|
||||
"scenarios/detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"project": project,
|
||||
"scenario": scenario,
|
||||
"scenario_metrics": scenario_metrics,
|
||||
"financial_inputs": financial_inputs,
|
||||
"simulation_parameters": simulation_parameters,
|
||||
},
|
||||
|
||||
@@ -4,23 +4,59 @@
|
||||
--hover-highlight: rgba(241, 178, 26, 0.12);
|
||||
}
|
||||
|
||||
.projects-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-radius: var(--table-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.projects-table th,
|
||||
.projects-table td {
|
||||
padding: 0.875rem 1rem;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
.project-metrics {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
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 {
|
||||
background: var(--hover-highlight);
|
||||
.metric-card h2 {
|
||||
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 {
|
||||
@@ -62,6 +98,61 @@
|
||||
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 {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
@@ -37,44 +37,48 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.6rem 1.1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
.scenario-metrics {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(90deg, var(--brand) 0%, var(--brand-2) 100%);
|
||||
color: var(--color-text-dark);
|
||||
box-shadow: 0 8px 18px rgba(241, 178, 26, 0.25);
|
||||
.metric-card {
|
||||
background: rgba(21, 27, 35, 0.85);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(148, 197, 255, 0.2);
|
||||
color: var(--color-text-invert);
|
||||
border: 1px solid rgba(148, 197, 255, 0.35);
|
||||
.metric-card h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 0.35rem 0.5rem;
|
||||
color: var(--brand-3);
|
||||
text-decoration: none;
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.btn:focus {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25);
|
||||
.metric-caption {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.scenario-filters {
|
||||
@@ -106,6 +110,17 @@
|
||||
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 {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
@@ -159,3 +174,24 @@
|
||||
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,67 +16,98 @@
|
||||
<h1>{{ project.name }}</h1>
|
||||
<p class="text-muted">{{ project.operation_type.value.replace('_', ' ') | title }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-secondary" href="{{ url_for('projects.edit_project_form', project_id=project.id) }}">Edit</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">New Scenario</a>
|
||||
<div class="header-actions">
|
||||
<a class="btn" href="{{ url_for('projects.edit_project_form', project_id=project.id) }}">Edit Project</a>
|
||||
<a class="btn primary" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">New Scenario</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<h2>Project Overview</h2>
|
||||
<dl class="definition-list">
|
||||
<div>
|
||||
<dt>Location</dt>
|
||||
<dd>{{ project.location or '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Description</dt>
|
||||
<dd>{{ project.description or 'No description provided.' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Created</dt>
|
||||
<dd>{{ project.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Updated</dt>
|
||||
<dd>{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<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>
|
||||
|
||||
<section class="card">
|
||||
<header class="card-header">
|
||||
<h2>Scenarios</h2>
|
||||
<a class="btn btn-link" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">Add Scenario</a>
|
||||
</header>
|
||||
{% if scenarios %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Currency</th>
|
||||
<th>Primary Resource</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scenario in scenarios %}
|
||||
<tr>
|
||||
<td>{{ scenario.name }}</td>
|
||||
<td>{{ scenario.status.value.title() }}</td>
|
||||
<td>{{ scenario.currency or '—' }}</td>
|
||||
<td>{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}</td>
|
||||
<td class="text-right">
|
||||
<a class="btn btn-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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No scenarios yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<div class="project-layout">
|
||||
<section class="card">
|
||||
<h2>Project Overview</h2>
|
||||
<dl class="definition-list">
|
||||
<div>
|
||||
<dt>Location</dt>
|
||||
<dd>{{ project.location or '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Description</dt>
|
||||
<dd>{{ project.description or 'No description provided.' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Created</dt>
|
||||
<dd>{{ project.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Updated</dt>
|
||||
<dd>{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<header class="card-header">
|
||||
<h2>Scenarios</h2>
|
||||
<a class="btn" href="{{ url_for('scenarios.create_scenario_form', project_id=project.id) }}">Add Scenario</a>
|
||||
</header>
|
||||
{% if scenarios %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Currency</th>
|
||||
<th>Primary Resource</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scenario in scenarios %}
|
||||
<tr>
|
||||
<td>{{ scenario.name }}</td>
|
||||
<td>{{ scenario.status.value.title() }}</td>
|
||||
<td>{{ scenario.currency or '—' }}</td>
|
||||
<td>{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}</td>
|
||||
<td class="text-right">
|
||||
<a class="table-link" href="{{ url_for('scenarios.view_scenario', scenario_id=scenario.id) }}">View</a>
|
||||
<a class="table-link" href="{{ url_for('scenarios.edit_scenario_form', scenario_id=scenario.id) }}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<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 %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -21,40 +21,50 @@
|
||||
<h1>{% if project %}Edit Project{% else %}Create Project{% endif %}</h1>
|
||||
<p class="text-muted">Provide core information about the mining project.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||
<button class="btn primary" type="submit">Save Project</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="form" method="post" action="{{ form_action }}">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" type="text" required value="{{ project.name if project else '' }}" />
|
||||
</div>
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="location">Location</label>
|
||||
<input id="location" name="location" type="text" value="{{ project.location if project else '' }}" />
|
||||
</div>
|
||||
<form class="form project-form" method="post" action="{{ form_action }}">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" type="text" required value="{{ project.name if project else '' }}" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="operation_type">Operation Type</label>
|
||||
<select id="operation_type" name="operation_type" required>
|
||||
{% for value, label in operation_types %}
|
||||
<option value="{{ value }}" {% if project and project.operation_type.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-group">
|
||||
<label for="location">Location</label>
|
||||
<input id="location" name="location" type="text" value="{{ project.location if project else '' }}" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="operation_type">Operation Type</label>
|
||||
<select id="operation_type" name="operation_type" required>
|
||||
{% for value, label in operation_types %}
|
||||
<option value="{{ value }}" {% if project and project.operation_type.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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 class="form-actions">
|
||||
<a class="btn btn-secondary" href="{{ cancel_url }}">Cancel</a>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||
<button class="btn primary" type="submit">Save Project</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,104 +17,118 @@
|
||||
<h1>{{ scenario.name }}</h1>
|
||||
<p class="text-muted">Status: {{ scenario.status.value.title() }}</p>
|
||||
</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>
|
||||
|
||||
<section class="card">
|
||||
<h2>Scenario Details</h2>
|
||||
<dl class="definition-list">
|
||||
<div>
|
||||
<dt>Description</dt>
|
||||
<dd>{{ scenario.description or 'No description provided.' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Timeline</dt>
|
||||
<dd>
|
||||
{% if scenario.start_date %}
|
||||
{{ scenario.start_date }}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
→
|
||||
{% if scenario.end_date %}
|
||||
{{ scenario.end_date }}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Discount Rate</dt>
|
||||
<dd>{{ scenario.discount_rate or '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Currency</dt>
|
||||
<dd>{{ scenario.currency or '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Primary Resource</dt>
|
||||
<dd>{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<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>
|
||||
|
||||
<section class="card">
|
||||
<h2>Financial Inputs</h2>
|
||||
{% if financial_inputs %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Currency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in financial_inputs %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.category.value.title() }}</td>
|
||||
<td>{{ '{:,.2f}'.format(item.amount) }}</td>
|
||||
<td>{{ item.currency or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No financial inputs recorded.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<div class="scenario-layout">
|
||||
<section class="card">
|
||||
<h2>Scenario Details</h2>
|
||||
<dl class="definition-list">
|
||||
<div>
|
||||
<dt>Description</dt>
|
||||
<dd>{{ scenario.description or 'No description provided.' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Timeline</dt>
|
||||
<dd>
|
||||
{{ scenario.start_date or '—' }} → {{ scenario.end_date or '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Discount Rate</dt>
|
||||
<dd>{{ scenario.discount_rate or '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Last Updated</dt>
|
||||
<dd>{{ scenario.updated_at.strftime('%Y-%m-%d %H:%M') if scenario.updated_at else '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Simulation Parameters</h2>
|
||||
{% if simulation_parameters %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Distribution</th>
|
||||
<th>Variable</th>
|
||||
<th>Resource</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in simulation_parameters %}
|
||||
<section class="card">
|
||||
<h2>Financial Inputs</h2>
|
||||
{% if financial_inputs %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ param.name }}</td>
|
||||
<td>{{ param.distribution.value.title() }}</td>
|
||||
<td>{{ param.variable.value.replace('_', ' ') | title if param.variable else '—' }}</td>
|
||||
<td>{{ param.resource_type.value.replace('_', ' ') | title if param.resource_type else '—' }}</td>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Currency</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No simulation parameters defined.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in financial_inputs %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.category.value.title() }}</td>
|
||||
<td>{{ '{:,.2f}'.format(item.amount) }}</td>
|
||||
<td>{{ item.currency or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No financial inputs recorded yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Simulation Parameters</h2>
|
||||
{% if simulation_parameters %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Distribution</th>
|
||||
<th>Variable</th>
|
||||
<th>Resource</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in simulation_parameters %}
|
||||
<tr>
|
||||
<td>{{ param.name }}</td>
|
||||
<td>{{ param.distribution.value.title() }}</td>
|
||||
<td>{{ param.variable.value.replace('_', ' ') | title if param.variable else '—' }}</td>
|
||||
<td>{{ param.resource_type.value.replace('_', ' ') | title if param.resource_type else '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No simulation parameters defined.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -21,13 +21,17 @@
|
||||
<h1>{% if scenario %}Edit Scenario{% else %}Create Scenario{% endif %}</h1>
|
||||
<p class="text-muted">Configure assumptions and metadata for this scenario.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||
<button class="btn primary" type="submit">Save Scenario</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% 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-group">
|
||||
<label for="name">Name</label>
|
||||
@@ -76,12 +80,12 @@
|
||||
|
||||
<div class="form-group">
|
||||
<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 class="form-actions">
|
||||
<a class="btn btn-secondary" href="{{ cancel_url }}">Cancel</a>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<a class="btn" href="{{ cancel_url }}">Cancel</a>
|
||||
<button class="btn primary" type="submit">Save Scenario</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user