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/') @login_required def switch_company(company_id): if not CompanyService.switch_company(company_id): abort(403) 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/') @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, })