rewrite styles

This commit is contained in:
Mateusz Gruszczyński
2026-07-14 12:12:46 +02:00
parent de7153e298
commit 1f4d459a64
4 changed files with 214 additions and 21 deletions
+131 -16
View File
@@ -4,7 +4,7 @@ from decimal import Decimal
from functools import wraps
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, abort
from flask_login import login_required, current_user
from sqlalchemy import extract
from sqlalchemy import extract, or_
from sqlalchemy.exc import IntegrityError
from .extensions import db
from .models import User, Vehicle, FuelEntry, CompanySettings, AppSetting, OrlenPrice, FuelCardPolicy, FuelStationCompany, FuelStationPoint, FuelCard, FuelCardStationRule
@@ -212,25 +212,140 @@ def orlen_price():
@login_required
def orlen_data():
settings = CompanySettings.query.first()
selected_year = request.args.get("year", type=int) or date.today().year
selected_fuels = [f.upper() for f in request.args.getlist("fuel") if f.upper() in FUEL_TYPES] or ["PB95"]
today = date.today()
mode = request.args.get("mode", "history")
if mode not in {"history", "compare"}:
mode = "history"
include_vat = request.args.get("gross") == "1"
show_lpg_regions = request.args.get('show_lpg_regions') == '1'
available_years = [
year for (year,) in db.session.query(extract("year", OrlenPrice.effective_date))
.distinct().order_by(extract("year", OrlenPrice.effective_date).desc()).all()
if year is not None
]
available_years = [int(year) for year in available_years]
if today.year not in available_years:
available_years.insert(0, today.year)
selected_year = request.args.get("year", type=int) or today.year
selected_fuels = [f.upper() for f in request.args.getlist("fuel") if f.upper() in FUEL_TYPES] or ["PB95"]
selected_regions = [r.lower() for r in request.args.getlist("region") if r.lower() in POLISH_REGIONS]
if not selected_regions: selected_regions=[settings.region.lower()]
if not show_lpg_regions: selected_regions=[settings.region.lower()]
query = OrlenPrice.query.filter(OrlenPrice.fuel_type.in_(selected_fuels), extract("year", OrlenPrice.effective_date)==selected_year)
rows = query.order_by(OrlenPrice.effective_date, OrlenPrice.fuel_type).all()
if not selected_regions:
selected_regions = [settings.region.lower()]
rows = []
chart = {}
for fuel in selected_fuels:
fuel_rows = [r for r in rows if r.fuel_type == fuel and (fuel != "LPG" or r.region in selected_regions)]
chart_key = fuel if fuel != "LPG" else None
if fuel == "LPG":
for region in selected_regions:
chart[f"LPG · {region}"] = [{"date": r.effective_date.isoformat(), "value": round(float(r.price_per_liter) * (1 + float(effective_vat_rate(settings.vat_rate, r.effective_date)) / 100 if include_vat else 1), 4)} for r in fuel_rows if r.region == region]
comparison_summary = []
comparison_type = request.args.get("comparison_type", "years")
compare_fuel = request.args.get("compare_fuel", "PB95").upper()
if compare_fuel not in FUEL_TYPES:
compare_fuel = "PB95"
compare_region = request.args.get("compare_region", settings.region).lower()
if compare_region not in POLISH_REGIONS:
compare_region = settings.region.lower()
compare_year = request.args.get("compare_year", type=int) or (selected_year - 1)
def priced_value(row):
multiplier = 1 + float(effective_vat_rate(settings.vat_rate, row.effective_date)) / 100 if include_vat else 1
return round(float(row.price_per_liter) * multiplier, 4)
def summary(label, source_rows):
values = [priced_value(row) for row in source_rows]
return {
"label": label,
"count": len(values),
"average": round(sum(values) / len(values), 4) if values else None,
"minimum": min(values) if values else None,
"maximum": max(values) if values else None,
"first": values[0] if values else None,
"last": values[-1] if values else None,
"change": round(values[-1] - values[0], 4) if len(values) > 1 else None,
}
if mode == "compare":
if comparison_type not in {"years", "periods"}:
comparison_type = "years"
base_query = OrlenPrice.query.filter_by(fuel_type=compare_fuel)
if compare_fuel == "LPG":
base_query = base_query.filter_by(region=compare_region)
if comparison_type == "years":
year_a, year_b = selected_year, compare_year
all_rows = base_query.filter(extract("year", OrlenPrice.effective_date).in_([year_a, year_b])).order_by(OrlenPrice.effective_date).all()
for year in (year_a, year_b):
period_rows = [row for row in all_rows if row.effective_date.year == year]
chart[str(year)] = [{"date": row.effective_date.strftime("%m-%d"), "value": priced_value(row)} for row in period_rows]
comparison_summary.append(summary(str(year), period_rows))
rows = all_rows
period_a_from = date(year_a, 1, 1)
period_a_to = date(year_a, 12, 31)
period_b_from = date(year_b, 1, 1)
period_b_to = date(year_b, 12, 31)
else:
chart[fuel] = [{"date": r.effective_date.isoformat(), "value": round(float(r.price_per_liter) * (1 + float(effective_vat_rate(settings.vat_rate, r.effective_date)) / 100 if include_vat else 1), 4)} for r in fuel_rows]
return render_template("orlen.html", rows=rows, chart=chart, selected_fuels=selected_fuels, selected_year=selected_year, settings=settings, fuel_types=FUEL_TYPES, include_vat=include_vat, regions=POLISH_REGIONS, selected_regions=selected_regions, show_lpg_regions=show_lpg_regions)
def parse_date_arg(name, fallback):
try:
return date.fromisoformat(request.args.get(name, ""))
except ValueError:
return fallback
period_a_from = parse_date_arg("period_a_from", today.replace(month=1, day=1))
period_a_to = parse_date_arg("period_a_to", today)
previous_year = today.year - 1
period_b_from = parse_date_arg("period_b_from", date(previous_year, period_a_from.month, min(period_a_from.day, 28)))
period_b_to = parse_date_arg("period_b_to", date(previous_year, period_a_to.month, min(period_a_to.day, 28)))
if period_a_from > period_a_to:
period_a_from, period_a_to = period_a_to, period_a_from
if period_b_from > period_b_to:
period_b_from, period_b_to = period_b_to, period_b_from
ranges = [
("Okres A", period_a_from, period_a_to),
("Okres B", period_b_from, period_b_to),
]
all_rows = base_query.filter(
or_(
OrlenPrice.effective_date.between(period_a_from, period_a_to),
OrlenPrice.effective_date.between(period_b_from, period_b_to),
)
).order_by(OrlenPrice.effective_date).all()
for label, start, end in ranges:
period_rows = [row for row in all_rows if start <= row.effective_date <= end]
chart[label] = [{"date": f"Dzień {(row.effective_date - start).days + 1:03d}", "value": priced_value(row)} for row in period_rows]
comparison_summary.append(summary(f"{label}: {start.isoformat()} {end.isoformat()}", period_rows))
rows = all_rows
else:
query = OrlenPrice.query.filter(OrlenPrice.fuel_type.in_(selected_fuels), extract("year", OrlenPrice.effective_date) == selected_year)
rows = query.order_by(OrlenPrice.effective_date, OrlenPrice.fuel_type).all()
for fuel in selected_fuels:
fuel_rows = [row for row in rows if row.fuel_type == fuel and (fuel != "LPG" or row.region in selected_regions)]
if fuel == "LPG":
for region in selected_regions:
chart[f"LPG · {region}"] = [{"date": row.effective_date.isoformat(), "value": priced_value(row)} for row in fuel_rows if row.region == region]
else:
chart[fuel] = [{"date": row.effective_date.isoformat(), "value": priced_value(row)} for row in fuel_rows]
period_a_from = period_a_to = period_b_from = period_b_to = None
summary_difference = None
if len(comparison_summary) == 2 and comparison_summary[0]["average"] is not None and comparison_summary[1]["average"] is not None:
first_average = comparison_summary[0]["average"]
second_average = comparison_summary[1]["average"]
summary_difference = {
"value": round(first_average - second_average, 4),
"percent": round((first_average - second_average) / second_average * 100, 2) if second_average else None,
}
return render_template(
"orlen.html", rows=rows, chart=chart, mode=mode,
selected_fuels=selected_fuels, selected_year=selected_year,
compare_year=compare_year, compare_fuel=compare_fuel,
comparison_type=comparison_type, comparison_summary=comparison_summary,
summary_difference=summary_difference, compare_region=compare_region,
period_a_from=period_a_from, period_a_to=period_a_to,
period_b_from=period_b_from, period_b_to=period_b_to,
settings=settings, fuel_types=FUEL_TYPES, include_vat=include_vat,
regions=POLISH_REGIONS, selected_regions=selected_regions,
available_years=available_years,
)
@main.post("/orlen/sync")
@login_required
+1
View File
@@ -1 +1,2 @@
(()=>{const F=FuelTrack;F.bindLpgRegionPicker=()=>{const select=F.qs('#lpg-region-select');if(!select||select.dataset.choicesBound)return;select.dataset.choicesBound='1';const choices=new Choices(select,{removeItemButton:true,searchEnabled:true,shouldSort:false,position:'bottom',placeholder:true,placeholderValue:'Wybierz województwa',noResultsText:'Brak województwa',noChoicesText:'Brak opcji',itemSelectText:'Wybierz'});const modal=F.qs('#lpgRegionsModal');modal?.addEventListener('hidden.bs.modal',()=>{const count=F.qsa('option:checked',select).length;const badge=F.qs('#lpg-region-count');if(badge)badge.textContent=count?`${count} wybrano`:'Domyślne województwo'})};F.bindOrlenSync=()=>{const btn=F.qs('#orlen-sync-btn');if(!btn||btn.dataset.bound)return;btn.dataset.bound='1';btn.addEventListener('click',async()=>{const form=F.qs('#orlen-filter-form'),fuels=F.qsa('[name=fuel]:checked',form).map(x=>x.value),year=F.qs('[name=year]',form).value,status=F.qs('#orlen-sync-status');if(!fuels.length){status.textContent='Wybierz co najmniej jedno paliwo.';return}btn.disabled=true;const old=btn.innerHTML;btn.innerHTML='Pobieranie…';try{const regions=F.qsa('[name=region] option:checked',form).map(x=>x.value);const r=await fetch('/api/orlen/sync',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({fuels,year,regions})}),d=await r.json();if(!r.ok)throw new Error(d.error||'Błąd pobierania');status.textContent=d.message;await F.replaceMain(`/orlen?${new URLSearchParams(new FormData(form))}`,{push:true});F.notify(d.message)}catch(e){status.textContent=`Błąd: ${e.message}`;F.notify(e.message,'danger')}finally{btn.disabled=false;btn.innerHTML=old}})}})();
;(()=>{const F=FuelTrack;F.bindOrlenComparison=()=>{const type=F.qs('#comparison-type'),fuel=F.qs('#compare-fuel');if(type&&!type.dataset.compareBound){type.dataset.compareBound='1';const update=()=>{F.qs('#year-comparison-fields')?.classList.toggle('d-none',type.value!=='years');F.qs('#period-comparison-fields')?.classList.toggle('d-none',type.value!=='periods')};type.addEventListener('change',update);update()}if(fuel&&!fuel.dataset.compareBound){fuel.dataset.compareBound='1';const update=()=>F.qs('#compare-region-wrap')?.classList.toggle('d-none',fuel.value!=='LPG');fuel.addEventListener('change',update);update()}}})();
+49 -5
View File
@@ -1,8 +1,52 @@
{% extends 'base.html' %}{% block title %}Ceny Orlen{% endblock %}{% block content %}
<div class="mb-4"><h1 class="h2 mb-1">Dane cenowe Orlen</h1><p class="text-body-secondary mb-0">Porównuj kilka paliw na jednym wykresie i zapisuj dane w bazie.</p></div>
<div class="card mb-4"><div class="card-body"><form method="get" id="orlen-filter-form" class="row g-3 align-items-end ajax-nav-form"><div class="col-lg-6"><label class="form-label d-block">Paliwa na wykresie</label><div class="d-flex flex-wrap gap-3">{% for f in fuel_types %}<label class="form-check"><input class="form-check-input" type="checkbox" name="fuel" value="{{f}}" {% if f in selected_fuels %}checked{% endif %}><span class="form-check-label">{{f}}</span></label>{% endfor %}</div></div><div class="col-lg-6"><label class="form-label d-block">Województwa LPG</label><button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#lpgRegionsModal">Wybierz województwa</button> <span id="lpg-region-count" class="small text-body-secondary">{% if selected_regions %}{{selected_regions|length}} wybrano{% else %}Domyślne województwo{% endif %}</span></div><div class="col-md-2"><label class="form-label">Rok</label><input class="form-control" type="number" min="2020" max="2100" name="year" value="{{selected_year}}"></div><div class="col-md-2"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="gross" value="1" id="gross-price" {% if include_vat %}checked{% endif %}><label class="form-check-label" for="gross-price">Dolicz VAT</label></div></div><div class="col-md-2"><button class="btn btn-secondary w-100">Pokaż</button></div></form>{% if current_user.role in ['boss','admin'] %}<div class="d-flex align-items-center gap-3 mt-3"><button type="button" id="orlen-sync-btn" class="btn btn-primary">Pobierz z API</button><span id="orlen-sync-status" class="small text-body-secondary" aria-live="polite"></span></div>{% endif %}</div></div>
{% extends 'base.html' %}
{% block title %}Ceny Orlen{% endblock %}
{% block content %}
<div class="page-header">
<div><div class="page-kicker">Analiza cen</div><h1 class="h2 mb-1">Dane cenowe Orlen</h1><p class="text-body-secondary mb-0">Przeglądaj historię i porównuj ceny tego samego paliwa między okresami.</p></div>
</div>
<ul class="nav nav-tabs mb-4" aria-label="Tryb analizy cen">
<li class="nav-item"><a class="nav-link {% if mode == 'history' %}active{% endif %}" href="{{ url_for('main.orlen_data', mode='history', year=selected_year) }}">Przegląd danych</a></li>
<li class="nav-item"><a class="nav-link {% if mode == 'compare' %}active{% endif %}" href="{{ url_for('main.orlen_data', mode='compare', year=selected_year, compare_year=compare_year, compare_fuel=compare_fuel) }}">Porównanie okresów</a></li>
</ul>
{% if mode == 'history' %}
<div class="card mb-4"><div class="card-body">
<form method="get" id="orlen-filter-form" class="row g-3 align-items-end ajax-nav-form">
<input type="hidden" name="mode" value="history">
<div class="col-lg-6"><label class="form-label d-block">Paliwa na wykresie</label><div class="d-flex flex-wrap gap-3">{% for f in fuel_types %}<label class="form-check"><input class="form-check-input" type="checkbox" name="fuel" value="{{f}}" {% if f in selected_fuels %}checked{% endif %}><span class="form-check-label">{{f}}</span></label>{% endfor %}</div></div>
<div class="col-lg-6"><label class="form-label d-block">Województwa LPG</label><button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#lpgRegionsModal">Wybierz województwa</button> <span id="lpg-region-count" class="small text-body-secondary">{% if selected_regions %}{{selected_regions|length}} wybrano{% else %}Domyślne województwo{% endif %}</span></div>
<div class="col-md-3"><label class="form-label">Rok</label><select class="form-select" name="year">{% for year in available_years %}<option value="{{year}}" {% if year == selected_year %}selected{% endif %}>{{year}}</option>{% endfor %}</select></div>
<div class="col-md-3"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="gross" value="1" id="gross-price" {% if include_vat %}checked{% endif %}><label class="form-check-label" for="gross-price">Dolicz VAT</label></div></div>
<div class="col-md-3"><button class="btn btn-secondary w-100">Pokaż dane</button></div>
</form>
{% if current_user.role in ['boss','admin'] %}<div class="d-flex align-items-center gap-3 mt-3"><button type="button" id="orlen-sync-btn" class="btn btn-primary">Pobierz z API</button><span id="orlen-sync-status" class="small text-body-secondary" aria-live="polite"></span></div>{% endif %}
</div></div>
<div class="modal fade lpg-region-modal" id="lpgRegionsModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h2 class="modal-title fs-5">Województwa LPG</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><label class="form-label" for="lpg-region-select">Wybierz jedno lub wiele województw</label><select class="form-select" name="region" id="lpg-region-select" form="orlen-filter-form" multiple>{% for r in regions %}<option value="{{r}}" {% if r in selected_regions %}selected{% endif %}>{{r}}</option>{% endfor %}</select></div><div class="modal-footer"><button type="button" class="btn btn-primary" data-bs-dismiss="modal">Gotowe</button></div></div></div></div>
<div class="card mb-4"><div class="card-body"><h2 class="h5">Trend ceny {% if include_vat %}brutto{% else %}netto{% endif %} za litr</h2><div class="chart-wrap"><canvas id="orlen-chart"></canvas><div id="orlen-chart-empty" class="chart-empty d-none">Brak danych dla wybranych filtrów.</div></div></div></div>
{% else %}
<div class="card mb-4"><div class="card-body">
<form method="get" id="orlen-filter-form" class="row g-3 align-items-end ajax-nav-form">
<input type="hidden" name="mode" value="compare">
<div class="col-md-4"><label class="form-label">Paliwo</label><select class="form-select" name="compare_fuel" id="compare-fuel">{% for f in fuel_types %}<option value="{{f}}" {% if f == compare_fuel %}selected{% endif %}>{{f}}</option>{% endfor %}</select><div class="form-text">Porównanie zawsze dotyczy jednego rodzaju paliwa.</div></div>
<div class="col-md-4 {% if compare_fuel != 'LPG' %}d-none{% endif %}" id="compare-region-wrap"><label class="form-label">Województwo LPG</label><select class="form-select" name="compare_region">{% for r in regions %}<option value="{{r}}" {% if r == compare_region %}selected{% endif %}>{{r}}</option>{% endfor %}</select></div>
<div class="col-md-4"><label class="form-label">Rodzaj porównania</label><select class="form-select" name="comparison_type" id="comparison-type"><option value="years" {% if comparison_type == 'years' %}selected{% endif %}>Rok do roku</option><option value="periods" {% if comparison_type == 'periods' %}selected{% endif %}>Dowolne okresy</option></select></div>
<div class="col-12"><div id="year-comparison-fields" class="row g-3 {% if comparison_type != 'years' %}d-none{% endif %}"><div class="col-md-4"><label class="form-label">Rok A</label><select class="form-select" name="year">{% for year in available_years %}<option value="{{year}}" {% if year == selected_year %}selected{% endif %}>{{year}}</option>{% endfor %}</select></div><div class="col-md-4"><label class="form-label">Rok B</label><select class="form-select" name="compare_year">{% for year in available_years %}<option value="{{year}}" {% if year == compare_year %}selected{% endif %}>{{year}}</option>{% endfor %}</select></div></div></div>
<div class="col-12"><div id="period-comparison-fields" class="row g-3 {% if comparison_type != 'periods' %}d-none{% endif %}"><div class="col-md-3"><label class="form-label">Okres A od</label><input class="form-control" type="date" name="period_a_from" value="{{period_a_from.isoformat() if period_a_from else ''}}"></div><div class="col-md-3"><label class="form-label">Okres A do</label><input class="form-control" type="date" name="period_a_to" value="{{period_a_to.isoformat() if period_a_to else ''}}"></div><div class="col-md-3"><label class="form-label">Okres B od</label><input class="form-control" type="date" name="period_b_from" value="{{period_b_from.isoformat() if period_b_from else ''}}"></div><div class="col-md-3"><label class="form-label">Okres B do</label><input class="form-control" type="date" name="period_b_to" value="{{period_b_to.isoformat() if period_b_to else ''}}"></div></div></div>
<div class="col-md-3"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="gross" value="1" id="gross-price" {% if include_vat %}checked{% endif %}><label class="form-check-label" for="gross-price">Dolicz VAT</label></div></div>
<div class="col-md-3"><button class="btn btn-primary w-100">Porównaj</button></div>
</form>
</div></div>
{% if comparison_summary %}<div class="row g-3 mb-4">{% for item in comparison_summary %}<div class="col-md-6"><div class="card metric h-100"><div class="card-body"><small>{{item.label}}</small><strong>{% if item.average is not none %}{{'%.4f'|format(item.average)}} zł/l{% else %}Brak danych{% endif %}</strong>{% if item.count %}<div class="small text-body-secondary mt-2">Min. {{'%.4f'|format(item.minimum)}} · Maks. {{'%.4f'|format(item.maximum)}} · {{item.count}} pomiarów</div>{% endif %}</div></div></div>{% endfor %}</div>{% endif %}
{% if summary_difference %}<div class="alert {% if summary_difference.value > 0 %}alert-warning{% elif summary_difference.value < 0 %}alert-success{% else %}alert-secondary{% endif %} mb-4">Średnia w pierwszym okresie jest <strong>{% if summary_difference.value > 0 %}wyższa{% elif summary_difference.value < 0 %}niższa{% else %}taka sama{% endif %}</strong> o {{'%.4f'|format(summary_difference.value|abs)}} zł/l{% if summary_difference.percent is not none %} ({{'%.2f'|format(summary_difference.percent|abs)}}%){% endif %}.</div>{% endif %}
{% endif %}
<div class="card mb-4"><div class="card-body"><div class="section-heading"><div><h2 class="h5 mb-1">{% if mode == 'compare' %}Porównanie: {{compare_fuel}}{% else %}Trend ceny{% endif %} {% if include_vat %}brutto{% else %}netto{% endif %}</h2><p class="small text-body-secondary mb-0">Cena za litr w złotych.</p></div></div><div class="chart-wrap"><canvas id="orlen-chart"></canvas><div id="orlen-chart-empty" class="chart-empty d-none">Brak danych dla wybranych filtrów.</div></div></div></div>
<div class="card"><div class="card-body"><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Data</th><th>Paliwo</th><th>Cena netto zł/l</th><th>Region</th><th>Pobrano</th></tr></thead><tbody>{% for r in rows|reverse %}<tr><td>{{r.effective_date}}</td><td>{{r.product_name}}</td><td>{{'%.4f'|format(r.price_per_liter|float)}}</td><td>{{r.region or '—'}}</td><td>{{r.fetched_at.strftime('%Y-%m-%d %H:%M')}}</td></tr>{% else %}<tr><td colspan="5" class="text-center text-body-secondary py-4">Brak danych w bazie.</td></tr>{% endfor %}</tbody></table></div></div></div>
{% endblock %}{% block scripts %}<script>FuelTrack.renderMultiLineChart('orlen-chart', {{chart|tojson}}, {{include_vat|tojson}});FuelTrack.bindOrlenSync();</script>{% endblock %}
{% endblock %}
{% block scripts %}<script>FuelTrack.renderMultiLineChart('orlen-chart', {{chart|tojson}}, {{include_vat|tojson}});FuelTrack.bindOrlenSync();FuelTrack.bindOrlenComparison();</script>{% endblock %}