mirror of
https://gitea.baerentsen.space/FrederikBaerentsen/BrickTracker.git
synced 2025-12-30 13:19:59 -06:00
Added charts, env var for charts, fixed formatting and table columns
This commit is contained in:
@@ -395,3 +395,12 @@
|
||||
# - "bricktracker_wishes"."number_of_parts": set number of parts
|
||||
# Default: "bricktracker_wishes"."rowid" DESC
|
||||
# BK_WISHES_DEFAULT_ORDER="bricktracker_wishes"."set" DESC
|
||||
|
||||
# Optional: Show collection growth charts on the statistics page
|
||||
# Default: true
|
||||
# BK_STATISTICS_SHOW_CHARTS=false
|
||||
|
||||
# Optional: Default state of statistics page sections (expanded or collapsed)
|
||||
# When true, all sections start expanded. When false, all sections start collapsed.
|
||||
# Default: true
|
||||
# BK_STATISTICS_DEFAULT_EXPANDED=false
|
||||
|
||||
@@ -90,4 +90,6 @@ CONFIG: Final[list[dict[str, Any]]] = [
|
||||
{'n': 'TIMEZONE', 'd': 'Etc/UTC'},
|
||||
{'n': 'USE_REMOTE_IMAGES', 'c': bool},
|
||||
{'n': 'WISHES_DEFAULT_ORDER', 'd': '"bricktracker_wishes"."rowid" DESC'},
|
||||
{'n': 'STATISTICS_SHOW_CHARTS', 'd': True, 'c': bool},
|
||||
{'n': 'STATISTICS_DEFAULT_EXPANDED', 'd': True, 'c': bool},
|
||||
]
|
||||
|
||||
@@ -15,7 +15,9 @@ SELECT
|
||||
ROUND(AVG("bricktracker_sets"."purchase_price"), 2) AS "avg_price",
|
||||
-- Problem statistics per theme
|
||||
COALESCE(SUM("problem_stats"."missing_parts"), 0) AS "missing_parts",
|
||||
COALESCE(SUM("problem_stats"."damaged_parts"), 0) AS "damaged_parts"
|
||||
COALESCE(SUM("problem_stats"."damaged_parts"), 0) AS "damaged_parts",
|
||||
-- Minifigure statistics per theme
|
||||
COALESCE(SUM("minifigure_stats"."minifigure_count"), 0) AS "total_minifigures"
|
||||
FROM "bricktracker_sets"
|
||||
INNER JOIN "rebrickable_sets" ON "bricktracker_sets"."set" = "rebrickable_sets"."set"
|
||||
LEFT JOIN (
|
||||
@@ -26,5 +28,12 @@ LEFT JOIN (
|
||||
FROM "bricktracker_parts"
|
||||
GROUP BY "bricktracker_parts"."id"
|
||||
) "problem_stats" ON "bricktracker_sets"."id" = "problem_stats"."id"
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
"bricktracker_minifigures"."id",
|
||||
SUM("bricktracker_minifigures"."quantity") AS "minifigure_count"
|
||||
FROM "bricktracker_minifigures"
|
||||
GROUP BY "bricktracker_minifigures"."id"
|
||||
) "minifigure_stats" ON "bricktracker_sets"."id" = "minifigure_stats"."id"
|
||||
GROUP BY "rebrickable_sets"."theme_id"
|
||||
ORDER BY "set_count" DESC, "rebrickable_sets"."theme_id" ASC
|
||||
@@ -5,7 +5,7 @@ Provides statistics and analytics pages
|
||||
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, render_template, request, url_for, redirect
|
||||
from flask import Blueprint, render_template, request, url_for, redirect, current_app
|
||||
from werkzeug.wrappers.response import Response
|
||||
|
||||
from .exceptions import exception_handler
|
||||
@@ -34,6 +34,11 @@ def overview() -> str:
|
||||
purchases_by_year_stats = stats.get_purchases_by_year_statistics()
|
||||
year_summary = stats.get_year_summary()
|
||||
|
||||
# Prepare chart data for visualization (only if charts are enabled)
|
||||
chart_data = {}
|
||||
if current_app.config['STATISTICS_SHOW_CHARTS']:
|
||||
chart_data = prepare_chart_data(sets_by_year_stats, purchases_by_year_stats)
|
||||
|
||||
# Get filter parameters for clickable statistics
|
||||
filter_type = request.args.get('filter_type')
|
||||
filter_value = request.args.get('filter_value')
|
||||
@@ -53,6 +58,7 @@ def overview() -> str:
|
||||
sets_by_year_statistics=sets_by_year_stats,
|
||||
purchases_by_year_statistics=purchases_by_year_stats,
|
||||
year_summary=year_summary,
|
||||
chart_data=chart_data,
|
||||
title="Statistics Overview"
|
||||
)
|
||||
|
||||
@@ -124,4 +130,60 @@ def purchase_locations() -> str:
|
||||
'statistics_purchase_locations.html',
|
||||
purchase_location_statistics=purchase_stats,
|
||||
title="Purchase Location Statistics"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def prepare_chart_data(sets_by_year_stats, purchases_by_year_stats):
|
||||
"""Prepare data for Chart.js visualization"""
|
||||
import json
|
||||
|
||||
# Get all years from both datasets
|
||||
all_years = set()
|
||||
|
||||
# Add years from sets by year
|
||||
if sets_by_year_stats:
|
||||
for year_stat in sets_by_year_stats:
|
||||
if 'year' in year_stat:
|
||||
all_years.add(year_stat['year'])
|
||||
|
||||
# Add years from purchases by year
|
||||
if purchases_by_year_stats:
|
||||
for year_stat in purchases_by_year_stats:
|
||||
if 'purchase_year' in year_stat:
|
||||
all_years.add(int(year_stat['purchase_year']))
|
||||
|
||||
# Create sorted list of years
|
||||
years = sorted(list(all_years))
|
||||
|
||||
# Initialize data arrays
|
||||
sets_data = []
|
||||
parts_data = []
|
||||
minifigs_data = []
|
||||
|
||||
# Create lookup dictionaries for quick access
|
||||
sets_by_year_lookup = {}
|
||||
if sets_by_year_stats:
|
||||
for year_stat in sets_by_year_stats:
|
||||
if 'year' in year_stat:
|
||||
sets_by_year_lookup[year_stat['year']] = year_stat
|
||||
|
||||
# Fill data arrays
|
||||
for year in years:
|
||||
# Get sets and parts data from sets_by_year
|
||||
year_data = sets_by_year_lookup.get(year)
|
||||
if year_data:
|
||||
sets_data.append(year_data.get('total_sets', 0))
|
||||
parts_data.append(year_data.get('total_parts', 0))
|
||||
# Use actual minifigure count from the database
|
||||
minifigs_data.append(year_data.get('total_minifigures', 0))
|
||||
else:
|
||||
sets_data.append(0)
|
||||
parts_data.append(0)
|
||||
minifigs_data.append(0)
|
||||
|
||||
return {
|
||||
'years': json.dumps(years),
|
||||
'sets_data': json.dumps(sets_data),
|
||||
'parts_data': json.dumps(parts_data),
|
||||
'minifigs_data': json.dumps(minifigs_data)
|
||||
}
|
||||
153
static/scripts/statistics.js
Normal file
153
static/scripts/statistics.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Statistics page chart functionality
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if charts are enabled and chart data exists
|
||||
if (typeof window.chartData === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: Log chart data to console
|
||||
console.log('Chart Data:', window.chartData);
|
||||
|
||||
// Common chart configuration
|
||||
const commonConfig = {
|
||||
type: 'line',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Release Year'
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Count'
|
||||
},
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
cornerRadius: 4
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 3,
|
||||
hoverRadius: 5
|
||||
},
|
||||
line: {
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Sets Chart
|
||||
const setsCanvas = document.getElementById('setsChart');
|
||||
if (setsCanvas) {
|
||||
const setsCtx = setsCanvas.getContext('2d');
|
||||
new Chart(setsCtx, {
|
||||
...commonConfig,
|
||||
data: {
|
||||
labels: window.chartData.years,
|
||||
datasets: [{
|
||||
label: 'Sets',
|
||||
data: window.chartData.sets,
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Parts Chart
|
||||
const partsCanvas = document.getElementById('partsChart');
|
||||
if (partsCanvas) {
|
||||
const partsCtx = partsCanvas.getContext('2d');
|
||||
new Chart(partsCtx, {
|
||||
...commonConfig,
|
||||
data: {
|
||||
labels: window.chartData.years,
|
||||
datasets: [{
|
||||
label: 'Parts',
|
||||
data: window.chartData.parts,
|
||||
borderColor: '#198754',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...commonConfig.options,
|
||||
scales: {
|
||||
...commonConfig.options.scales,
|
||||
y: {
|
||||
...commonConfig.options.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Parts Count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Minifigures Chart
|
||||
const minifigsCanvas = document.getElementById('minifigsChart');
|
||||
if (minifigsCanvas) {
|
||||
const minifigsCtx = minifigsCanvas.getContext('2d');
|
||||
new Chart(minifigsCtx, {
|
||||
...commonConfig,
|
||||
data: {
|
||||
labels: window.chartData.years,
|
||||
datasets: [{
|
||||
label: 'Minifigures',
|
||||
data: window.chartData.minifigs,
|
||||
borderColor: '#fd7e14',
|
||||
backgroundColor: 'rgba(253, 126, 20, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...commonConfig.options,
|
||||
scales: {
|
||||
...commonConfig.options.scales,
|
||||
y: {
|
||||
...commonConfig.options.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Minifigures Count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.3.4/dist/css/datepicker-bs5.min.css">
|
||||
<link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet">
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="{{ url_for('static', filename='brick.png') }}">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
|
||||
@@ -109,6 +110,9 @@
|
||||
<script src="{{ url_for('static', filename='scripts/parts-bulk-operations.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/set-details.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'statistics.overview' %}
|
||||
<script src="{{ url_for('static', filename='scripts/statistics.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'instructions.download' or request.endpoint == 'instructions.do_download' %}
|
||||
<script src="{{ url_for('static', filename='scripts/socket/peeron.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
@@ -21,42 +21,42 @@
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('set.list') }}" class="text-decoration-none">
|
||||
<div class="h4 text-primary mb-0">{{ collection_summary.total_sets }}</div>
|
||||
<small class="text-muted">Total Sets</small>
|
||||
<small class="text-dark">Total Sets</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="h4 text-info mb-0">{{ collection_summary.unique_sets }}</div>
|
||||
<small class="text-muted">Unique Sets</small>
|
||||
<div class="h4 text-dark mb-0">{{ collection_summary.unique_sets }}</div>
|
||||
<small class="text-dark">Unique Sets</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('part.list') }}" class="text-decoration-none">
|
||||
<div class="h4 text-success mb-0">{{ "{:,}".format(collection_summary.total_parts_count) }}</div>
|
||||
<small class="text-muted">Total Parts</small>
|
||||
<div class="h4 text-primary mb-0">{{ "{:,}".format(collection_summary.total_parts_count) }}</div>
|
||||
<small class="text-dark">Total Parts</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="h4 text-secondary mb-0">{{ collection_summary.unique_parts }}</div>
|
||||
<small class="text-muted">Unique Parts</small>
|
||||
<div class="h4 text-dark mb-0">{{ collection_summary.unique_parts }}</div>
|
||||
<small class="text-dark">Unique Parts</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('minifigure.list') }}" class="text-decoration-none">
|
||||
<div class="h4 text-warning mb-0">{{ collection_summary.total_minifigures_count }}</div>
|
||||
<small class="text-muted">Minifigures</small>
|
||||
<div class="h4 text-primary mb-0">{{ collection_summary.total_minifigures_count }}</div>
|
||||
<small class="text-dark">Minifigures</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="h4 text-info mb-0">{{ collection_summary.unique_minifigures }}</div>
|
||||
<small class="text-muted">Unique Figs</small>
|
||||
<div class="h4 text-dark mb-0">{{ collection_summary.unique_minifigures }}</div>
|
||||
<small class="text-dark">Unique Figs</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,30 +75,30 @@
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-success mb-0">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.total_cost) }}</div>
|
||||
<small class="text-muted">Total Investment</small>
|
||||
<small class="text-dark">Total Investment</small>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="h5 text-primary mb-0">{{ financial_summary.sets_with_price }}</div>
|
||||
<small class="text-muted">Sets with Price</small>
|
||||
<div class="h5 text-dark mb-0">{{ financial_summary.sets_with_price }}</div>
|
||||
<small class="text-dark">Sets with Price</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="h5 text-info mb-0">{{ financial_summary.percentage_with_price }}%</div>
|
||||
<small class="text-muted">Price Coverage</small>
|
||||
<div class="h5 text-dark mb-0">{{ financial_summary.percentage_with_price }}%</div>
|
||||
<small class="text-dark">Price Coverage</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="text-muted small">Average</div>
|
||||
<div class="text-dark small">Average</div>
|
||||
<div class="fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.average_cost) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="text-muted small">Range</div>
|
||||
<div class="text-dark small">Range</div>
|
||||
<div class="fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.minimum_cost) }} - {{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.maximum_cost) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,34 +121,34 @@
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('part.problem') }}" class="text-decoration-none">
|
||||
<div class="h4 text-danger mb-0">{{ collection_summary.total_missing_parts }}</div>
|
||||
<small class="text-muted">Missing Parts</small>
|
||||
<small class="text-dark">Missing Parts</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('part.problem') }}" class="text-decoration-none">
|
||||
<div class="h4 text-warning mb-0">{{ collection_summary.total_damaged_parts }}</div>
|
||||
<small class="text-muted">Damaged Parts</small>
|
||||
<div class="h4 text-danger mb-0">{{ collection_summary.total_damaged_parts }}</div>
|
||||
<small class="text-dark">Damaged Parts</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
{% if config['HIDE_ALL_STORAGES'] %}
|
||||
<div class="h4 text-info mb-0">{{ collection_summary.storage_locations_used }}</div>
|
||||
<div class="h4 text-dark mb-0">{{ collection_summary.storage_locations_used }}</div>
|
||||
{% else %}
|
||||
<a href="{{ url_for('storage.list') }}" class="text-decoration-none">
|
||||
<div class="h4 text-info mb-0">{{ collection_summary.storage_locations_used }}</div>
|
||||
<div class="h4 text-primary mb-0">{{ collection_summary.storage_locations_used }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
<small class="text-muted">Storage Locations</small>
|
||||
<small class="text-dark">Storage Locations</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="h4 text-secondary mb-0">{{ collection_summary.purchase_locations_used }}</div>
|
||||
<small class="text-muted">Purchase Locations</small>
|
||||
<div class="h4 text-dark mb-0">{{ collection_summary.purchase_locations_used }}</div>
|
||||
<small class="text-dark">Purchase Locations</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,6 +157,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collection Growth Charts -->
|
||||
{% if config['STATISTICS_SHOW_CHARTS'] %}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseCharts" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseCharts">
|
||||
<i class="ri-line-chart-line"></i> Collection Growth Over Time
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseCharts">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-3">
|
||||
<h6 class="text-center text-dark">Sets by Release Year</h6>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="setsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 mb-3">
|
||||
<h6 class="text-center text-dark">Parts by Release Year</h6>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="partsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 mb-3">
|
||||
<h6 class="text-center text-dark">Minifigures by Release Year</h6>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="minifigsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Detailed Statistics Tables -->
|
||||
<div class="row g-3">
|
||||
<!-- Theme Statistics -->
|
||||
@@ -165,10 +207,14 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ri-palette-line"></i> Sets by Theme
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseThemes" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseThemes">
|
||||
<i class="ri-palette-line"></i> Sets by Theme
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseThemes">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
@@ -176,6 +222,7 @@
|
||||
<th>Theme</th>
|
||||
<th class="text-center">Sets</th>
|
||||
<th class="text-center">Parts</th>
|
||||
<th class="text-center">Minifigures</th>
|
||||
<th class="text-center">Spent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -188,18 +235,19 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('set.list', theme=theme.theme_id) }}" class="text-decoration-none">
|
||||
<span class="badge bg-primary">{{ theme.set_count }}</span>
|
||||
</a>
|
||||
<small class="text-dark">{{ theme.set_count }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-muted">{{ "{:,}".format(theme.total_parts) }}</small>
|
||||
<small class="text-dark">{{ "{:,}".format(theme.total_parts) }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ theme.total_minifigures }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if theme.total_spent %}
|
||||
<small class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(theme.total_spent) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(theme.total_spent) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -209,9 +257,10 @@
|
||||
</div>
|
||||
{% if theme_statistics|length > 10 %}
|
||||
<div class="card-footer text-center">
|
||||
<small class="text-muted">Showing top 10 themes</small>
|
||||
<small class="text-dark">Showing top 10 themes</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,10 +272,14 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ri-archive-2-line"></i> Sets by Storage
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseStorage" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseStorage">
|
||||
<i class="ri-archive-2-line"></i> Sets by Storage
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseStorage">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
@@ -234,6 +287,7 @@
|
||||
<th>Storage Location</th>
|
||||
<th class="text-center">Sets</th>
|
||||
<th class="text-center">Parts</th>
|
||||
<th class="text-center">Minifigures</th>
|
||||
<th class="text-center">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -246,18 +300,19 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('set.list', storage=storage.storage_id) }}" class="text-decoration-none">
|
||||
<span class="badge bg-info">{{ storage.set_count }}</span>
|
||||
</a>
|
||||
<small class="text-dark">{{ storage.set_count }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-muted">{{ "{:,}".format(storage.total_parts) }}</small>
|
||||
<small class="text-dark">{{ "{:,}".format(storage.total_parts) }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ storage.total_minifigures }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if storage.total_value %}
|
||||
<small class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(storage.total_value) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(storage.total_value) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -265,6 +320,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,21 +334,25 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ri-shopping-cart-line"></i> Sets by Purchase Location
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapsePurchase" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapsePurchase">
|
||||
<i class="ri-shopping-cart-line"></i> Sets by Purchase Location
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapsePurchase">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Purchase Location</th>
|
||||
<th class="text-center">Sets</th>
|
||||
<th class="text-center">Total Parts</th>
|
||||
<th class="text-center">Parts</th>
|
||||
<th class="text-center">Minifigures</th>
|
||||
<th class="text-center">Total Spent</th>
|
||||
<th class="text-center">Avg. Price</th>
|
||||
<th class="text-center">Price Range</th>
|
||||
<th class="text-center">Issues</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -304,39 +364,33 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('set.list', purchase_location=location.location_id) }}" class="text-decoration-none">
|
||||
<span class="badge bg-secondary">{{ location.set_count }}</span>
|
||||
</a>
|
||||
<small class="text-dark">{{ location.set_count }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-muted">{{ "{:,}".format(location.total_parts) }}</small>
|
||||
<small class="text-dark">{{ "{:,}".format(location.total_parts) }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ location.total_minifigures }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if location.total_spent %}
|
||||
<span class="text-success fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.total_spent) }}</span>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.total_spent) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if location.avg_price %}
|
||||
<small class="text-info">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.avg_price) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.avg_price) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if location.min_price and location.max_price %}
|
||||
<small class="text-muted">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(location.min_price) }}-{{ "%.0f"|format(location.max_price) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(location.min_price) }}-{{ "%.0f"|format(location.max_price) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if location.missing_parts or location.damaged_parts %}
|
||||
<small class="text-warning">{{ location.missing_parts + location.damaged_parts }}</small>
|
||||
{% else %}
|
||||
<small class="text-success">✓</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -344,6 +398,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,11 +413,15 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ri-calendar-line"></i> Sets by Release Year
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseReleaseYear" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseReleaseYear">
|
||||
<i class="ri-calendar-line"></i> Sets by Release Year
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
<small class="text-muted">Statistics grouped by when LEGO released the sets</small>
|
||||
<small class="text-dark">Statistics grouped by when LEGO released the sets</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseReleaseYear">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 table-sm">
|
||||
<thead class="table-light">
|
||||
@@ -371,6 +430,7 @@
|
||||
<th class="text-center">Unique</th>
|
||||
<th class="text-center">Total</th>
|
||||
<th class="text-center">Parts</th>
|
||||
<th class="text-center">Minifigures</th>
|
||||
<th class="text-center">Spent</th>
|
||||
<th class="text-center">Avg/Set</th>
|
||||
</tr>
|
||||
@@ -383,34 +443,33 @@
|
||||
<strong>{{ year.year }}</strong>
|
||||
</a>
|
||||
{% if year.unique_themes > 1 %}
|
||||
<small class="text-muted d-block">{{ year.unique_themes }} themes</small>
|
||||
<small class="text-dark d-block">{{ year.unique_themes }} themes</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('set.list', year=year.year) }}" class="year-filter-link text-decoration-none" data-year="{{ year.year }}">
|
||||
<span class="badge bg-secondary">{{ year.unique_sets }}</span>
|
||||
</a>
|
||||
<small class="text-dark">{{ year.unique_sets }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('set.list', year=year.year) }}" class="year-filter-link text-decoration-none" data-year="{{ year.year }}">
|
||||
<span class="badge bg-primary">{{ year.total_sets }}</span>
|
||||
</a>
|
||||
<small class="text-dark">{{ year.total_sets }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-muted">{{ "{:,}".format(year.total_parts) }}</small>
|
||||
<small class="text-dark">{{ "{:,}".format(year.total_parts) }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ year.total_minifigures }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.total_spent %}
|
||||
<small class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.total_spent) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.total_spent) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.avg_price_per_set %}
|
||||
<small class="text-info">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -420,9 +479,10 @@
|
||||
</div>
|
||||
{% if sets_by_year_statistics|length > 15 %}
|
||||
<div class="card-footer text-center">
|
||||
<small class="text-muted">Showing last 15 years</small>
|
||||
<small class="text-dark">Showing last 15 years</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,11 +494,15 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ri-shopping-cart-line"></i> Purchases by Year
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapsePurchaseYear" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapsePurchaseYear">
|
||||
<i class="ri-shopping-cart-line"></i> Purchases by Year
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
<small class="text-muted">Statistics grouped by when you purchased the sets</small>
|
||||
<small class="text-dark">Statistics grouped by when you purchased the sets</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapsePurchaseYear">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 table-sm">
|
||||
<thead class="table-light">
|
||||
@@ -447,6 +511,7 @@
|
||||
<th class="text-center">Unique</th>
|
||||
<th class="text-center">Total</th>
|
||||
<th class="text-center">Parts</th>
|
||||
<th class="text-center">Minifigures</th>
|
||||
<th class="text-center">Spent</th>
|
||||
<th class="text-center">Avg/Set</th>
|
||||
</tr>
|
||||
@@ -456,32 +521,31 @@
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ year.purchase_year }}</strong>
|
||||
<small class="text-muted d-block">
|
||||
{% if year.months_with_purchases > 1 %}{{ year.months_with_purchases }} months{% endif %}
|
||||
<span class="text-info">Purchase year</span>
|
||||
</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-secondary">{{ year.unique_sets }}</span>
|
||||
<small class="text-dark">{{ year.unique_sets }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-primary">{{ year.total_sets }}</span>
|
||||
<small class="text-dark">{{ year.total_sets }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-muted">{{ "{:,}".format(year.total_parts) }}</small>
|
||||
<small class="text-dark">{{ "{:,}".format(year.total_parts) }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ year.total_minifigures }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.total_spent %}
|
||||
<strong class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.total_spent) }}</strong>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.total_spent) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.avg_price_per_set %}
|
||||
<small class="text-info">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">-</small>
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -493,22 +557,23 @@
|
||||
<div class="card-footer">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Peak Year</small><br>
|
||||
<small class="text-dark">Peak Year</small><br>
|
||||
{% if year_summary.peak_spending_year %}
|
||||
<strong>{{ year_summary.peak_spending_year }}</strong>
|
||||
<small class="text-success d-block">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year_summary.max_spending) }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted">N/A</small>
|
||||
<small class="text-dark">N/A</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Active Years</small><br>
|
||||
<small class="text-dark">Active Years</small><br>
|
||||
<strong>{{ year_summary.years_with_purchases }}</strong>
|
||||
<small class="text-muted d-block">years</small>
|
||||
<small class="text-dark d-block">years</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -524,25 +589,25 @@
|
||||
{% if year_summary.oldest_set_year and year_summary.newest_set_year %}
|
||||
<div class="col-md-3">
|
||||
<strong>{{ year_summary.oldest_set_year }} - {{ year_summary.newest_set_year }}</strong><br>
|
||||
<small class="text-muted">Set Year Range</small>
|
||||
<small class="text-dark">Set Year Range</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if year_summary.peak_collection_year %}
|
||||
<div class="col-md-3">
|
||||
<strong>{{ year_summary.peak_collection_year }}</strong><br>
|
||||
<small class="text-muted">Most Sets from Year ({{ year_summary.max_sets_in_year }} sets)</small>
|
||||
<small class="text-dark">Most Sets from Year ({{ year_summary.max_sets_in_year }} sets)</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if year_summary.years_represented %}
|
||||
<div class="col-md-3">
|
||||
<strong>{{ year_summary.years_represented }}</strong><br>
|
||||
<small class="text-muted">Years Represented</small>
|
||||
<small class="text-dark">Years Represented</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if year_summary.years_with_purchases %}
|
||||
<div class="col-md-3">
|
||||
<strong>{{ year_summary.years_with_purchases }}</strong><br>
|
||||
<small class="text-muted">Years with Purchases</small>
|
||||
<small class="text-dark">Years with Purchases</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -551,5 +616,18 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% if config['STATISTICS_SHOW_CHARTS'] %}
|
||||
<script type="text/javascript">
|
||||
// Initialize chart data for statistics.js
|
||||
window.chartData = {
|
||||
years: {{ chart_data.years|safe }},
|
||||
sets: {{ chart_data.sets_data|safe }},
|
||||
parts: {{ chart_data.parts_data|safe }},
|
||||
minifigs: {{ chart_data.minifigs_data|safe }}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user