This commit is contained in:
Mateusz Gruszczyński
2026-03-13 11:03:13 +01:00
commit 35571df778
132 changed files with 11197 additions and 0 deletions

View File

204
app/dashboard/routes.py Normal file
View File

@@ -0,0 +1,204 @@
from datetime import date, datetime
from decimal import Decimal
from flask import Blueprint, current_app, jsonify, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from sqlalchemy import extract
from app.extensions import csrf
from app.models.invoice import Invoice
from app.models.sync_log import SyncLog
from app.services.company_service import CompanyService
from app.services.health_service import HealthService
from app.services.redis_service import RedisService
from app.services.settings_service import SettingsService
from app.services.sync_service import SyncService
bp = Blueprint('dashboard', __name__)
def _load_dashboard_summary(company_id: int):
cache_key = f'dashboard.summary.company.{company_id}'
cached = RedisService.get_json(cache_key) or {}
base = Invoice.query.filter_by(company_id=company_id)
today = date.today()
if not cached:
month_invoices = base.filter(
extract('month', Invoice.issue_date) == today.month,
extract('year', Invoice.issue_date) == today.year,
).order_by(Invoice.issue_date.desc(), Invoice.id.desc()).all()
cached = {
'month_invoice_ids': [invoice.id for invoice in month_invoices],
'unread': base.filter_by(is_unread=True).count(),
'totals': {
'net': str(sum(Decimal(invoice.net_amount) for invoice in month_invoices)),
'vat': str(sum(Decimal(invoice.vat_amount) for invoice in month_invoices)),
'gross': str(sum(Decimal(invoice.gross_amount) for invoice in month_invoices)),
},
'recent_invoice_ids': [invoice.id for invoice in base.order_by(Invoice.created_at.desc(), Invoice.id.desc()).limit(200).all()],
}
RedisService.set_json(cache_key, cached, ttl=300)
month_ids = cached.get('month_invoice_ids', [])
month_invoices = Invoice.query.filter(Invoice.id.in_(month_ids)).all() if month_ids else []
month_invoices.sort(key=lambda item: month_ids.index(item.id) if item.id in month_ids else 9999)
totals = {
'net': Decimal(str(cached.get('totals', {}).get('net', '0'))),
'vat': Decimal(str(cached.get('totals', {}).get('vat', '0'))),
'gross': Decimal(str(cached.get('totals', {}).get('gross', '0'))),
}
return cached, month_invoices, totals
@bp.route('/')
@login_required
def index():
company = CompanyService.get_current_company()
health_service = HealthService()
health = health_service.get_cached_status(company.id if company else None) or health_service.get_status(company_id=company.id if company else None)
if not company:
return render_template(
'dashboard/index.html',
company=None,
month_invoices=[],
unread=0,
totals={'net': Decimal('0'), 'vat': Decimal('0'), 'gross': Decimal('0')},
recent_invoices=[],
last_sync_display='brak',
sync_status='inactive',
health=health,
current_user=current_user,
recent_pagination={'page': 1, 'pages': 1, 'has_prev': False, 'has_next': False, 'prev_num': 1, 'next_num': 1},
payment_details_map={},
redis_fallback=(health.get('redis') == 'fallback'),
)
read_only = SettingsService.read_only_enabled(company_id=company.id)
base = Invoice.query.filter_by(company_id=company.id)
last_sync_raw = SettingsService.get('ksef.last_sync_at', None, company_id=company.id)
last_sync = None
if isinstance(last_sync_raw, str) and last_sync_raw.strip():
try:
last_sync = datetime.fromisoformat(last_sync_raw.replace('Z', '+00:00'))
except Exception:
last_sync = last_sync_raw
elif last_sync_raw:
last_sync = last_sync_raw
if not last_sync:
latest_log = SyncLog.query.filter_by(company_id=company.id, status='finished').order_by(SyncLog.finished_at.desc()).first()
last_sync = latest_log.finished_at if latest_log and latest_log.finished_at else None
cached, month_invoices, totals = _load_dashboard_summary(company.id)
unread = cached.get('unread', 0)
recent_ids = cached.get('recent_invoice_ids', [])
per_page = 10
total_recent = len(recent_ids)
total_pages = max((total_recent + per_page - 1) // per_page, 1)
dashboard_page = min(max(request.args.get('dashboard_page', 1, type=int), 1), total_pages)
start = (dashboard_page - 1) * per_page
end = start + per_page
current_ids = recent_ids[start:end]
recent_invoices = Invoice.query.filter(Invoice.id.in_(current_ids)).all() if current_ids else []
recent_invoices.sort(key=lambda item: current_ids.index(item.id) if item.id in current_ids else 9999)
recent_pagination = {
'page': dashboard_page,
'pages': total_pages,
'has_prev': dashboard_page > 1,
'has_next': end < total_recent,
'prev_num': dashboard_page - 1,
'next_num': dashboard_page + 1,
}
from app.services.invoice_service import InvoiceService
payment_details_map = {invoice.id: InvoiceService().resolve_payment_details(invoice) for invoice in recent_invoices}
last_sync_display = last_sync.strftime('%Y-%m-%d %H:%M:%S') if hasattr(last_sync, 'strftime') else (last_sync or 'brak')
return render_template(
'dashboard/index.html',
company=company,
month_invoices=month_invoices,
unread=unread,
totals=totals,
recent_invoices=recent_invoices,
recent_pagination=recent_pagination,
payment_details_map=payment_details_map,
last_sync_display=last_sync_display,
last_sync_raw=last_sync,
sync_status=SettingsService.get('ksef.status', 'inactive', company_id=company.id),
health=health,
read_only=read_only,
redis_fallback=(health.get('redis') == 'fallback'),
)
@bp.route('/switch-company/<int:company_id>')
@login_required
def switch_company(company_id):
CompanyService.set_active_company(company_id)
return redirect(url_for('dashboard.index'))
@bp.post('/sync/manual')
@login_required
def manual_sync():
company = CompanyService.get_current_company()
if not company:
return redirect(url_for('dashboard.index'))
app = current_app._get_current_object()
log_id = SyncService.start_manual_sync_async(app, company.id)
return redirect(url_for('dashboard.index', started=log_id))
@bp.route('/sync/status')
@login_required
@csrf.exempt
def sync_status():
company = CompanyService.get_current_company()
if not company:
return jsonify({'status': 'no_company'})
log = SyncLog.query.filter_by(company_id=company.id).order_by(SyncLog.started_at.desc()).first()
if not log:
return jsonify({'status': 'idle'})
return jsonify(
{
'status': log.status,
'message': log.message,
'processed': log.processed,
'created': log.created,
'updated': log.updated,
'errors': log.errors,
'total': log.total,
}
)
@bp.post('/sync/start')
@login_required
def sync_start():
company = CompanyService.get_current_company()
if not company:
return jsonify({'error': 'no_company'}), 400
app = current_app._get_current_object()
log_id = SyncService.start_manual_sync_async(app, company.id)
return jsonify({'log_id': log_id})
@bp.get('/sync/status/<int:log_id>')
@login_required
def sync_status_by_id(log_id):
log = SyncLog.query.get_or_404(log_id)
total = log.total or 0
progress = int((log.processed / total) * 100) if total else (100 if log.status == 'finished' else 0)
return jsonify({
'status': log.status,
'message': log.message,
'processed': log.processed,
'created': log.created,
'updated': log.updated,
'errors': log.errors,
'total': total,
'progress': progress,
})