diff --git a/changelog.md b/changelog.md index 272b138..642ee46 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/routes/projects.py b/routes/projects.py index fb937c6..57a1c30 100644 --- a/routes/projects.py +++ b/routes/projects.py @@ -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, }, ) diff --git a/routes/scenarios.py b/routes/scenarios.py index bce46f1..565cb64 100644 --- a/routes/scenarios.py +++ b/routes/scenarios.py @@ -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, }, diff --git a/static/css/projects.css b/static/css/projects.css index bc0daf2..8463267 100644 --- a/static/css/projects.css +++ b/static/css/projects.css @@ -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); diff --git a/static/css/scenarios.css b/static/css/scenarios.css index 981f7a3..fa9d981 100644 --- a/static/css/scenarios.css +++ b/static/css/scenarios.css @@ -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; + } +} diff --git a/templates/projects/detail.html b/templates/projects/detail.html index 001ec4c..10517bb 100644 --- a/templates/projects/detail.html +++ b/templates/projects/detail.html @@ -16,67 +16,98 @@

{{ project.name }}

{{ project.operation_type.value.replace('_', ' ') | title }}

-
- Edit - New Scenario +
+ Edit Project + New Scenario
-
-

Project Overview

-
-
-
Location
-
{{ project.location or '—' }}
-
-
-
Description
-
{{ project.description or 'No description provided.' }}
-
-
-
Created
-
{{ project.created_at.strftime('%Y-%m-%d %H:%M') }}
-
-
-
Updated
-
{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}
-
-
+
+
+

Total Scenarios

+

{{ scenario_stats.total }}

+ Across this project +
+
+

Active

+

{{ scenario_stats.active }}

+ Currently live analyses +
+
+

Draft

+

{{ scenario_stats.draft }}

+ Awaiting validation +
+
+

Archived

+

{{ scenario_stats.archived }}

+ Historical references +
-
-
-

Scenarios

- Add Scenario -
- {% if scenarios %} - - - - - - - - - - - - {% for scenario in scenarios %} - - - - - - - - {% endfor %} - -
NameStatusCurrencyPrimary Resource
{{ scenario.name }}{{ scenario.status.value.title() }}{{ scenario.currency or '—' }}{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }} - View - Edit -
- {% else %} -

No scenarios yet.

- {% endif %} -
+
+
+

Project Overview

+
+
+
Location
+
{{ project.location or '—' }}
+
+
+
Description
+
{{ project.description or 'No description provided.' }}
+
+
+
Created
+
{{ project.created_at.strftime('%Y-%m-%d %H:%M') }}
+
+
+
Updated
+
{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}
+
+
+
Latest Scenario Update
+
{{ scenario_stats.latest_update.strftime('%Y-%m-%d %H:%M') if scenario_stats.latest_update else '—' }}
+
+
+
+ +
+
+

Scenarios

+ Add Scenario +
+ {% if scenarios %} +
+ + + + + + + + + + + + {% for scenario in scenarios %} + + + + + + + + {% endfor %} + +
NameStatusCurrencyPrimary ResourceActions
{{ scenario.name }}{{ scenario.status.value.title() }}{{ scenario.currency or '—' }}{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }} + View + Edit +
+
+ {% else %} +

No scenarios yet. Create the first scenario.

+ {% endif %} +
+
{% endblock %} diff --git a/templates/projects/form.html b/templates/projects/form.html index 88f83f0..505f19a 100644 --- a/templates/projects/form.html +++ b/templates/projects/form.html @@ -21,40 +21,50 @@

{% if project %}Edit Project{% else %}Create Project{% endif %}

Provide core information about the mining project.

+
+ Cancel + +
{% if error %}
{{ error }}
{% endif %} -
-
- - -
+ {% if error %} +
{{ error }}
+ {% endif %} -
- - -
+ +
+
+ + +
-
- - +
+ + +
+ +
+ + +
- +
- Cancel - + Cancel +
{% endblock %} diff --git a/templates/scenarios/detail.html b/templates/scenarios/detail.html index eb6980a..9f99ae1 100644 --- a/templates/scenarios/detail.html +++ b/templates/scenarios/detail.html @@ -17,104 +17,118 @@

{{ scenario.name }}

Status: {{ scenario.status.value.title() }}

- Edit +
+ Back to Project + Edit Scenario +
-
-

Scenario Details

-
-
-
Description
-
{{ scenario.description or 'No description provided.' }}
-
-
-
Timeline
-
- {% if scenario.start_date %} - {{ scenario.start_date }} - {% else %} - — - {% endif %} - → - {% if scenario.end_date %} - {{ scenario.end_date }} - {% else %} - — - {% endif %} -
-
-
-
Discount Rate
-
{{ scenario.discount_rate or '—' }}
-
-
-
Currency
-
{{ scenario.currency or '—' }}
-
-
-
Primary Resource
-
{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }}
-
-
+
+
+

Financial Inputs

+

{{ scenario_metrics.financial_count }}

+ Line items captured +
+
+

Simulation Parameters

+

{{ scenario_metrics.parameter_count }}

+ Inputs driving forecasts +
+
+

Currency

+

{{ scenario_metrics.currency or '—' }}

+ Financial reporting +
+
+

Primary Resource

+

{{ scenario_metrics.primary_resource or '—' }}

+ Scenario focus +
-
-

Financial Inputs

- {% if financial_inputs %} -
- - - - - - - - - - - {% for item in financial_inputs %} - - - - - - - {% endfor %} - -
NameCategoryAmountCurrency
{{ item.name }}{{ item.category.value.title() }}{{ '{:,.2f}'.format(item.amount) }}{{ item.currency or '—' }}
-
- {% else %} -

No financial inputs recorded.

- {% endif %} -
+
+
+

Scenario Details

+
+
+
Description
+
{{ scenario.description or 'No description provided.' }}
+
+
+
Timeline
+
+ {{ scenario.start_date or '—' }} → {{ scenario.end_date or '—' }} +
+
+
+
Discount Rate
+
{{ scenario.discount_rate or '—' }}
+
+
+
Last Updated
+
{{ scenario.updated_at.strftime('%Y-%m-%d %H:%M') if scenario.updated_at else '—' }}
+
+
+
-
-

Simulation Parameters

- {% if simulation_parameters %} -
- - - - - - - - - - - {% for param in simulation_parameters %} +
+

Financial Inputs

+ {% if financial_inputs %} +
+
NameDistributionVariableResource
+ - - - - + + + + - {% endfor %} - -
{{ param.name }}{{ param.distribution.value.title() }}{{ param.variable.value.replace('_', ' ') | title if param.variable else '—' }}{{ param.resource_type.value.replace('_', ' ') | title if param.resource_type else '—' }}NameCategoryAmountCurrency
-
- {% else %} -

No simulation parameters defined.

- {% endif %} -
+ + + {% for item in financial_inputs %} + + {{ item.name }} + {{ item.category.value.title() }} + {{ '{:,.2f}'.format(item.amount) }} + {{ item.currency or '—' }} + + {% endfor %} + + +
+ {% else %} +

No financial inputs recorded yet.

+ {% endif %} +
+ +
+

Simulation Parameters

+ {% if simulation_parameters %} +
+ + + + + + + + + + + {% for param in simulation_parameters %} + + + + + + + {% endfor %} + +
NameDistributionVariableResource
{{ param.name }}{{ param.distribution.value.title() }}{{ param.variable.value.replace('_', ' ') | title if param.variable else '—' }}{{ param.resource_type.value.replace('_', ' ') | title if param.resource_type else '—' }}
+
+ {% else %} +

No simulation parameters defined.

+ {% endif %} +
+ {% endblock %} diff --git a/templates/scenarios/form.html b/templates/scenarios/form.html index c651037..0ddea13 100644 --- a/templates/scenarios/form.html +++ b/templates/scenarios/form.html @@ -21,13 +21,17 @@

{% if scenario %}Edit Scenario{% else %}Create Scenario{% endif %}

Configure assumptions and metadata for this scenario.

+
+ Cancel + +
{% if error %}
{{ error }}
{% endif %} -
+
@@ -76,12 +80,12 @@
- +
- Cancel - + Cancel +
{% endblock %}