Total Scenarios
+{{ scenario_stats.total }}
+ Across this project +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.operation_type.value.replace('_', ' ') | title }}
-{{ scenario_stats.total }}
+ Across this project +{{ scenario_stats.active }}
+ Currently live analyses +{{ scenario_stats.draft }}
+ Awaiting validation +{{ scenario_stats.archived }}
+ Historical references +| Name | -Status | -Currency | -Primary Resource | -- |
|---|---|---|---|---|
| {{ scenario.name }} | -{{ scenario.status.value.title() }} | -{{ scenario.currency or '—' }} | -{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }} | -- View - Edit - | -
No scenarios yet.
- {% endif %} -| Name | +Status | +Currency | +Primary Resource | +Actions | +
|---|---|---|---|---|
| {{ scenario.name }} | +{{ scenario.status.value.title() }} | +{{ scenario.currency or '—' }} | +{{ scenario.primary_resource.value.replace('_', ' ') | title if scenario.primary_resource else '—' }} | ++ View + Edit + | +
No scenarios yet. Create the first scenario.
+ {% endif %} +Provide core information about the mining project.