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

@@ -0,0 +1,135 @@
{% extends 'base.html' %}
{% block title %}<i class="fa-solid fa-chart-pie me-2 text-primary"></i>Dashboard{% endblock %}
{% block content %}
{% if not company %}
{% set eyebrow='Pulpit firmy' %}{% set heading='Dashboard operacyjny' %}{% set description='Podsumowanie pracy na aktywnej firmie.' %}
{% include 'partials/page_header.html' with context %}
<div class="card"><div class="card-body py-5"><h4 class="mb-2">Brak wybranej firmy</h4><p class="text-secondary mb-3">Najpierw wybierz firmę z przełącznika w górnym pasku albo dodaj ją w panelu administracyjnym.</p><a class="btn btn-primary" href="{{ url_for('admin.company_form') if current_user.role == 'admin' else url_for('settings.index') }}">{{ 'Dodaj firmę' if current_user.role == 'admin' else 'Przejdź do ustawień' }}</a></div></div>
{% else %}
{% set eyebrow='Pulpit firmy' %}{% set heading='Dashboard operacyjny' %}{% set description='Podsumowanie bieżącego miesiąca, synchronizacji i ostatnich dokumentów.' %}
{% include 'partials/page_header.html' with context %}
<div class="row g-3 mb-4">
<div class="col-md-3"><div class="card stat-card stat-blue text-white"><div class="card-body"><div class="small opacity-75"><i class="fa-solid fa-file-invoice me-1"></i>Faktury w miesiącu</div><div class="display-6">{{ month_invoices|length }}</div><div class="small">{{ company.name }}</div></div></div></div>
<div class="col-md-3"><div class="card stat-card stat-green text-white"><div class="card-body"><div class="small opacity-75"><i class="fa-solid fa-envelope-open-text me-1"></i>Nowe</div><div class="display-6">{{ unread }}</div></div></div></div>
<div class="col-md-2"><div class="card stat-card stat-dark text-white"><div class="card-body"><div class="small opacity-75"><i class="fa-solid fa-wallet me-1"></i>Netto</div><div>{{ totals.net|pln }}</div></div></div></div>
<div class="col-md-2"><div class="card stat-card stat-purple text-white"><div class="card-body"><div class="small opacity-75"><i class="fa-solid fa-percent me-1"></i>VAT</div><div>{{ totals.vat|pln }}</div></div></div></div>
<div class="col-md-2"><div class="card stat-card stat-orange text-white"><div class="card-body"><div class="small opacity-75"><i class="fa-solid fa-sack-dollar me-1"></i>Brutto</div><div>{{ totals.gross|pln }}</div></div></div></div>
</div>
<div class="row g-3">
<div class="col-lg-8">
<div class="card mb-3"><div class="card-header d-flex justify-content-between align-items-center"><span><i class="fa-solid fa-rotate me-2"></i>Synchronizacja KSeF</span><button id="syncBtn" class="btn btn-sm btn-primary" data-sync-url="{{ url_for("dashboard.sync_start") }}" data-csrf-token="{{ csrf_token() }}"><i class="fa-solid fa-download me-1"></i>Pobierz ręcznie</button></div><div class="card-body"><div class="d-flex justify-content-between align-items-center flex-wrap gap-2 small mb-2"><span>Status: <span id="syncStatusText" class="badge text-bg-info">{{ sync_status }}</span></span><span class="d-inline-flex align-items-center gap-2"><span class="text-secondary">Ostatni sync:</span><span class="badge rounded-pill text-bg-light border">{{ last_sync_display }}</span></span></div><div class="progress" style="height: 22px;"><div id="syncProgressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%">0%</div></div><div id="syncMessage" class="small text-secondary mt-2">Kliknij "Pobierz ręcznie" aby pobrać faktury z KSeF.</div></div></div>
<div class="card"><div class="card-header"><i class="fa-solid fa-clock-rotate-left me-2"></i>Ostatnie faktury</div><div class="table-responsive"><table class="table table-sm mb-0"><thead><tr><th>Numer</th><th>Kontrahent</th><th>Brutto</th><th></th></tr></thead><tbody>{% for invoice in recent_invoices %}<tr><td>{{ invoice.invoice_number }}</td><td>{{ invoice.contractor_name }}</td><td>{{ invoice.gross_amount|pln }}</td><td class="text-end"><div class="d-inline-flex gap-2 flex-wrap justify-content-end"><a href="{{ url_for('invoices.detail', invoice_id=invoice.id) }}" class="btn btn-sm btn-outline-primary invoice-action-btn"><i class="fa-solid fa-folder-open me-1"></i>Otwórz</a><button type="button" class="btn btn-sm btn-success invoice-action-btn" data-bs-toggle="modal" data-bs-target="#payModalDashboard{{ invoice.id }}"><i class="fa-solid fa-wallet me-1"></i>Opłać</button></div>{% set payment_details = payment_details_map.get(invoice.id, {}) %}{% set modal_id = 'payModalDashboard' ~ invoice.id %}{% include 'partials/payment_modal.html' %}</td></tr>{% else %}<tr><td colspan="4" class="text-center text-secondary py-4">Brak danych.</td></tr>{% endfor %}</tbody></table></div><div class="card-body border-top py-2"><nav><ul class="pagination pagination-sm justify-content-end mb-0">{% if recent_pagination and recent_pagination.has_prev %}<li class="page-item"><a class="page-link" href="{{ url_for('dashboard.index', dashboard_page=recent_pagination.prev_num) }}">Poprz.</a></li>{% endif %}{% if recent_pagination and recent_pagination.pages > 1 %}{% for pg in range(1, recent_pagination.pages + 1) %}<li class="page-item {{ 'active' if pg == recent_pagination.page else '' }}"><a class="page-link" href="{{ url_for('dashboard.index', dashboard_page=pg) }}">{{ pg }}</a></li>{% endfor %}{% endif %}{% if recent_pagination and recent_pagination.has_next %}<li class="page-item"><a class="page-link" href="{{ url_for('dashboard.index', dashboard_page=recent_pagination.next_num) }}">Dalej</a></li>{% endif %}</ul></nav></div></div>
</div>
<div class="col-lg-4">
{% if health.critical %}
<div class="card mb-3 border-danger">
<div class="card-header bg-danger text-white">
<i class="fa-solid fa-triangle-exclamation me-2"></i>Raport krytyczny
</div>
<div class="card-body small">
{% if health.ksef != 'ok' %}
<div>API KSeF: {{ health.ksef }}</div>
{% if health.ksef_message %}
<div class="text-muted">{{ health.ksef_message }}</div>
{% endif %}
{% endif %}
{% if health.ceidg != 'ok' %}
<div>API CEIDG: {{ health.ceidg }}</div>
{% if health.ceidg_message %}
<div class="text-muted">{{ health.ceidg_message }}</div>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<i class="fa-solid fa-building-shield me-2"></i>Harmonogram
</div>
<div class="card-body small">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>Automatyczna synchronizacja</span>
<span class="badge {{ 'bg-success' if company.sync_enabled else 'bg-secondary' }}">
{{ 'włączona' if company.sync_enabled else 'wyłączona' }}
</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span>Interwał</span>
<span class="badge bg-info text-dark">
{{ company.sync_interval_minutes }} min
</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Tryb pracy</span>
{% if read_only %}
<span class="badge bg-warning text-dark">tylko pobieranie</span>
{% else %}
<span class="badge bg-primary">pełna synchronizacja</span>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
let syncTimer = null;
const syncBtn = document.getElementById('syncBtn');
const statusMap = {queued: 'W kolejce', started: 'W toku', finished: 'Zakończona', error: 'Błąd'};
syncBtn?.addEventListener('click', async () => {
syncBtn.disabled = true;
syncBtn.classList.add('disabled');
document.getElementById('syncMessage').textContent = 'Uruchamianie ręcznego pobierania...';
try {
const res = await fetch(syncBtn.dataset.syncUrl, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': syncBtn.dataset.csrfToken,
},
credentials: 'same-origin',
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.log_id) {
const message = data.error || data.message || 'Nie udało się uruchomić ręcznego pobierania.';
document.getElementById('syncMessage').textContent = message;
return;
}
document.getElementById('syncMessage').textContent = 'Rozpoczęto pobieranie...';
syncTimer = setInterval(async () => {
const r = await fetch(`/sync/status/${data.log_id}`, {credentials: 'same-origin'});
const s = await r.json();
const bar = document.getElementById('syncProgressBar');
bar.style.width = `${s.progress}%`;
bar.textContent = `${s.progress}%`;
document.getElementById('syncStatusText').textContent = statusMap[s.status] || s.status || '—';
document.getElementById('syncMessage').textContent = s.message || '';
if (s.status === 'finished' || s.status === 'error') {
clearInterval(syncTimer);
syncBtn.disabled = false;
syncBtn.classList.remove('disabled');
if (s.status === 'finished') {
window.location.reload();
}
}
}, 1200);
} catch (err) {
document.getElementById('syncMessage').textContent = 'Nie udało się połączyć z usługą synchronizacji.';
syncBtn.disabled = false;
syncBtn.classList.remove('disabled');
}
});
</script>
{% endblock %}