push
This commit is contained in:
21
app/templates/admin/_nav.html
Normal file
21
app/templates/admin/_nav.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.index' else 'btn-outline-primary' }}" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-shield-halved me-1"></i>Start</a>
|
||||
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint in ['admin.users', 'admin.user_form', 'admin.user_access', 'admin.reset_password'] else 'btn-outline-primary' }}" href="{{ url_for('admin.users') }}"><i class="fa-solid fa-users me-1"></i>Użytkownicy</a>
|
||||
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint in ['admin.companies', 'admin.company_form'] else 'btn-outline-primary' }}" href="{{ url_for('admin.companies') }}"><i class="fa-solid fa-building me-1"></i>Firmy</a>
|
||||
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.audit' else 'btn-outline-primary' }}" href="{{ url_for('admin.audit') }}"><i class="fa-solid fa-clipboard-check me-1"></i>Logi audytu</a>
|
||||
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.global_settings' else 'btn-outline-primary' }}" href="{{ url_for('admin.global_settings') }}"><i class="fa-solid fa-sliders me-1"></i>Ustawienia globalne</a>
|
||||
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint in ['admin.system_data', 'admin.health'] else 'btn-outline-primary' }}" href="{{ url_for('admin.system_data') }}"><i class="fa-solid fa-microchip me-1"></i>Dane systemowe</a>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.maintenance' else 'btn-outline-primary' }} dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-toolbox me-1"></i>Narzędzia
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.maintenance') }}"><i class="fa-solid fa-screwdriver-wrench me-2"></i>Logi i backupy</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.system_data') }}#integrations"><i class="fa-solid fa-plug me-2"></i>Integracje</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
5
app/templates/admin/admin_base.html
Normal file
5
app/templates/admin/admin_base.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
{% include 'admin/_nav.html' %}
|
||||
{% block admin_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
5
app/templates/admin/audit.html
Normal file
5
app/templates/admin/audit.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}Audit log{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="card shadow-sm"><div class="card-body"><div class="table-responsive"><table class="table table-sm"><thead><tr><th>Czas</th><th>Akcja</th><th>Typ</th><th>ID</th><th>Szczegóły</th><th>IP</th></tr></thead><tbody>{% for log in logs %}<tr><td>{{ log.created_at }}</td><td>{{ log.action }}</td><td>{{ log.target_type }}</td><td>{{ log.target_id or '' }}</td><td>{{ log.details }}</td><td>{{ log.remote_addr }}</td></tr>{% endfor %}</tbody></table></div></div></div>
|
||||
{% endblock %}
|
||||
6
app/templates/admin/companies.html
Normal file
6
app/templates/admin/companies.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-building me-2 text-primary"></i>Firmy{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3"><div><h4 class="mb-0">Firmy</h4><div class="text-secondary small">Oddzielne ustawienia KSeF, certyfikaty, powiadomienia i harmonogramy.</div></div><a class="btn btn-primary" href="{{ url_for('admin.company_form') }}"><i class="fa-solid fa-plus me-2"></i>Dodaj firmę</a></div>
|
||||
<div class="card shadow-sm"><div class="table-responsive"><table class="table table-hover align-middle mb-0"><thead><tr><th>Firma</th><th>Harmonogram</th><th>Status</th><th>Adres / konto</th><th>Notatka</th><th></th></tr></thead><tbody>{% for company in companies %}<tr><td><div class="fw-semibold">{{ company.name }}</div><div class="small text-secondary">NIP: {{ company.tax_id or 'brak' }}{% if company.regon %} · REGON: {{ company.regon }}{% endif %}</div></td><td><span class="badge text-bg-light border">co {{ company.sync_interval_minutes }} min</span> {% if company.sync_enabled %}<span class="badge text-bg-success">włączony</span>{% else %}<span class="badge text-bg-secondary">wyłączony</span>{% endif %}</td><td>{% if company.is_active %}<span class="badge text-bg-success">aktywna</span>{% else %}<span class="badge text-bg-secondary">nieaktywna</span>{% endif %}</td><td class="text-secondary small">{{ company.address or '—' }}<div>{% if company.bank_account %}Konto: {{ company.bank_account }}{% endif %}</div></td><td class="text-secondary small">{{ company.note or '—' }}</td><td class="text-end text-nowrap"><a class="btn btn-sm btn-outline-secondary" href="{{ url_for('dashboard.switch_company', company_id=company.id) }}"><i class="fa-solid fa-check"></i> Wybierz</a> <a class="btn btn-sm btn-outline-primary" href="{{ url_for('admin.company_form', company_id=company.id) }}"><i class="fa-solid fa-pen"></i> Edytuj</a></td></tr>{% endfor %}</tbody></table></div></div>
|
||||
{% endblock %}
|
||||
48
app/templates/admin/company_form.html
Normal file
48
app/templates/admin/company_form.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}{{ 'Edycja firmy' if company else 'Nowa firma' }}{% endblock %}
|
||||
{% block admin_content %}
|
||||
<form method="post" class="card shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1">{{ 'Edycja firmy' if company else 'Nowa firma' }}</h4>
|
||||
<div class="text-secondary">Podaj NIP a następnie klikniej z Pobierz z CEIDG aby wypełnić pola.</div>
|
||||
|
||||
</div>
|
||||
{% if company %}<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('dashboard.switch_company', company_id=company.id) }}">Wybierz tę firmę</a>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8">
|
||||
<div class="card border-0 bg-body-tertiary h-100">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-7">{{ form.name.label(class='form-label') }}{{ form.name(class='form-control', placeholder='Po pobraniu z CEIDG pole uzupełni się automatycznie') }}</div>
|
||||
<div class="col-md-3">{{ form.tax_id.label(class='form-label') }}{{ form.tax_id(class='form-control', placeholder='NIP') }}</div>
|
||||
<div class="col-md-2 d-grid">{{ form.fetch_submit(class='btn btn-outline-secondary btn-sm') }}</div>
|
||||
<div class="col-md-4">{{ form.regon.label(class='form-label') }}{{ form.regon(class='form-control') }}</div>
|
||||
<div class="col-md-8">{{ form.address.label(class='form-label') }}{{ form.address(class='form-control') }}</div>
|
||||
<div class="col-md-6">{{ form.bank_account.label(class='form-label') }}{{ form.bank_account(class='form-control', placeholder='np. 11 1111 1111 1111 1111 1111 1111') }}</div>
|
||||
<div class="col-md-6">{{ form.note.label(class='form-label') }}{{ form.note(class='form-control', rows='3') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card border-0 bg-body-tertiary h-100">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary text-uppercase mb-3">Ustawienia</div>
|
||||
<div class="form-check form-switch mb-3">{{ form.is_active(class='form-check-input') }} {{ form.is_active.label(class='form-check-label') }}</div>
|
||||
<div class="form-check form-switch mb-3">{{ form.sync_enabled(class='form-check-input') }} {{ form.sync_enabled.label(class='form-check-label') }}</div>
|
||||
<div class="form-check form-switch mb-3">{{ form.mock_mode(class='form-check-input') }} {{ form.mock_mode.label(class='form-check-label') }}</div>
|
||||
<div class="mt-4">{{ form.sync_interval_minutes.label(class='form-label') }}{{ form.sync_interval_minutes(class='form-control') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">{{ form.submit(class='btn btn-primary') }}</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
15
app/templates/admin/global_settings.html
Normal file
15
app/templates/admin/global_settings.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-sliders me-2 text-primary"></i>Ustawienia globalne{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div><h4 class="mb-1">Ustawienia globalne</h4><div class="text-secondary">Wspólna konfiguracja systemu dla SMTP, Pushover i NFZ oraz model współdzielonego profilu KSeF per firma.</div></div>
|
||||
</div>
|
||||
<div class="alert alert-info">KSeF nie jest ustawieniem w pełni globalnym dla całego systemu. Administrator ustawia parametry domyślne oraz osobny profil współdzielony dla aktywnej firmy. Użytkownik może świadomie wybrać profil współdzielony albo własny.</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-6"><div class="card"><div class="card-header">SMTP globalne</div><div class="card-body"><form method="post" class="row g-3">{{ mail_form.hidden_tag() }}<div class="col-md-6">{{ mail_form.server.label(class='form-label') }}{{ mail_form.server(class='form-control') }}</div><div class="col-md-3">{{ mail_form.port.label(class='form-label') }}{{ mail_form.port(class='form-control') }}</div><div class="col-md-3">{{ mail_form.username.label(class='form-label') }}{{ mail_form.username(class='form-control') }}</div><div class="col-md-6">{{ mail_form.password.label(class='form-label') }}{{ mail_form.password(class='form-control', placeholder='Pozostaw puste aby zachować hasło') }}</div><div class="col-md-6">{{ mail_form.sender.label(class='form-label') }}{{ mail_form.sender(class='form-control') }}</div><div class="col-md-6">{{ mail_form.security_mode.label(class='form-label') }}{{ mail_form.security_mode(class='form-select') }}</div><div class="col-12">{{ mail_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-header">Pushover globalny</div><div class="card-body"><form method="post" class="row g-3">{{ notify_form.hidden_tag() }}<div class="col-md-6">{{ notify_form.pushover_user_key.label(class='form-label') }}{{ notify_form.pushover_user_key(class='form-control') }}</div><div class="col-md-6">{{ notify_form.pushover_api_token.label(class='form-label') }}{{ notify_form.pushover_api_token(class='form-control', placeholder='Pozostaw puste aby zachować token') }}</div><div class="col-md-4">{{ notify_form.min_amount.label(class='form-label') }}{{ notify_form.min_amount(class='form-control') }}</div><div class="col-md-8">{{ notify_form.quiet_hours.label(class='form-label') }}{{ notify_form.quiet_hours(class='form-control') }}</div><div class="col-12 form-check">{{ notify_form.enabled(class='form-check-input') }}{{ notify_form.enabled.label(class='form-check-label') }}</div><div class="col-12">{{ notify_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-header">NFZ globalnie</div><div class="card-body"><form method="post">{{ nfz_form.hidden_tag() }}<div class="form-check form-switch fs-5 mb-3">{{ nfz_form.enabled(class='form-check-input') }}{{ nfz_form.enabled.label(class='form-check-label') }}</div>{{ nfz_form.submit(class='btn btn-primary') }}</form></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-header">Domyślne parametry KSeF</div><div class="card-body"><form method="post" class="row g-3">{{ ksef_defaults_form.hidden_tag() }}<div class="col-md-4">{{ ksef_defaults_form.environment.label(class='form-label') }}{{ ksef_defaults_form.environment(class='form-select') }}</div><div class="col-md-4">{{ ksef_defaults_form.auth_mode.label(class='form-label') }}{{ ksef_defaults_form.auth_mode(class='form-select') }}</div><div class="col-md-4">{{ ksef_defaults_form.client_id.label(class='form-label') }}{{ ksef_defaults_form.client_id(class='form-control') }}</div><div class="col-12"><div class="form-text">Te parametry podpowiadają start nowej konfiguracji, ale nie nadpisują sekretów użytkowników ani współdzielonych profili firm.</div></div><div class="col-12">{{ ksef_defaults_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
<div class="col-12"><div class="card"><div class="card-header">Współdzielony profil KSeF dla aktywnej firmy</div><div class="card-body"><div class="mb-3 small text-secondary">Aktywna firma: <strong>{{ current_company.name if current_company else 'brak' }}</strong>. Ten profil mogą wybrać użytkownicy tej firmy zamiast własnych danych KSeF.</div><form method="post" class="row g-3">{{ shared_ksef_form.hidden_tag() }}<div class="col-md-3">{{ shared_ksef_form.environment.label(class='form-label') }}{{ shared_ksef_form.environment(class='form-select') }}</div><div class="col-md-3">{{ shared_ksef_form.auth_mode.label(class='form-label') }}{{ shared_ksef_form.auth_mode(class='form-select') }}</div><div class="col-md-3">{{ shared_ksef_form.client_id.label(class='form-label') }}{{ shared_ksef_form.client_id(class='form-control') }}</div><div class="col-md-3">{{ shared_ksef_form.certificate_name.label(class='form-label') }}{{ shared_ksef_form.certificate_name(class='form-control') }}</div><div class="col-md-6">{{ shared_ksef_form.token.label(class='form-label') }}{{ shared_ksef_form.token(class='form-control', placeholder='Pozostaw puste aby zachować token') }}<div class="form-text">{{ 'Token zapisany.' if shared_token_configured else 'Brak zapisanego tokena.' }}</div></div><div class="col-md-6">{{ shared_ksef_form.certificate_data.label(class='form-label') }}{{ shared_ksef_form.certificate_data(class='form-control', placeholder='Pozostaw puste aby zachować certyfikat') }}<div class="form-text">{{ 'Certyfikat zapisany.' if shared_cert_configured else 'Brak zapisanego certyfikatu.' }}</div></div><div class="col-12">{{ shared_ksef_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
app/templates/admin/health.html
Normal file
11
app/templates/admin/health.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-heart-pulse me-2 text-danger"></i>Zdrowie systemu{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-solid fa-database me-2"></i>Baza danych</div><div class="fw-bold fs-5">{{ status.db }}</div></div></div></div>
|
||||
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-solid fa-envelope me-2"></i>SMTP</div><div class="fw-bold fs-5">{{ status.smtp }}</div></div></div></div>
|
||||
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-brands fa-redis me-2"></i>Redis</div><div class="fw-bold fs-5">{{ status.redis }}</div></div></div></div>
|
||||
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-solid fa-network-wired me-2"></i>KSeF</div><div class="fw-bold fs-5">{{ status.ksef }}</div></div></div></div>
|
||||
</div>
|
||||
<div class="card mt-3 shadow-sm"><div class="card-header"><i class="fa-solid fa-link me-2"></i>Status połączenia do API KSeF</div><div class="card-body"><p class="mb-2">{{ status.ksef_message }}</p>{% if status.mock_mode %}<div class="alert alert-info mb-0"><i class="fa-solid fa-flask me-2"></i>Tryb mock jest włączony. Synchronizacja i wystawianie faktur działają lokalnie i nie wysyłają danych do produkcyjnego KSeF.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
229
app/templates/admin/index.html
Normal file
229
app/templates/admin/index.html
Normal file
@@ -0,0 +1,229 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-shield-halved me-2 text-primary"></i>Panel admina{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1">Administracja systemem</h4>
|
||||
<div class="text-secondary">
|
||||
Jedno miejsce do zarządzania konfiguracją globalną, firmami, użytkownikami i danymi testowymi.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm h-100 border-0">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary mb-1">Użytkownicy</div>
|
||||
<div class="display-6">{{ users }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm h-100 border-0">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary mb-1">Firmy</div>
|
||||
<div class="display-6">{{ companies }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm h-100 border-0">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary mb-1">Firmy z mock</div>
|
||||
<div class="display-6">{{ mock_enabled }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-6">
|
||||
<div class="card shadow-sm h-100 border-0">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<div class="small text-secondary mb-1">Tryb tylko do odczytu</div>
|
||||
<h5 class="mb-1">Globalna blokada zapisów</h5>
|
||||
<div class="text-secondary">
|
||||
Szybkie przełączenie systemu między pracą operacyjną i bezpiecznym trybem tylko do odczytu.
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill {{ 'text-bg-warning' if global_ro else 'text-bg-light border text-secondary' }}">
|
||||
{{ 'Aktywna' if global_ro else 'Wyłączona' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form method="post" action="{{ url_for('admin.toggle_global_read_only') }}" class="m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="enabled" value="1">
|
||||
<button class="btn btn-sm btn-outline-warning" {{ 'disabled' if global_ro else '' }}>
|
||||
<i class="fa-solid fa-eye me-1"></i>Włącz R/O
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ url_for('admin.toggle_global_read_only') }}" class="m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="enabled" value="0">
|
||||
<button class="btn btn-sm btn-outline-secondary" {{ '' if global_ro else 'disabled' }}>
|
||||
<i class="fa-solid fa-pen me-1"></i>Wyłącz R/O
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6">
|
||||
<div class="card shadow-sm h-100 border-0">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<div class="small text-secondary mb-1">Dane testowe i mock</div>
|
||||
<h5 class="mb-1">Środowisko demonstracyjne</h5>
|
||||
<div class="text-secondary">
|
||||
Generowanie zestawu startowego i szybkie czyszczenie danych do testów prezentacyjnych.
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill text-bg-light border text-secondary">
|
||||
{{ mock_enabled }} firm
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form method="post" action="{{ url_for('admin.generate_mock_data') }}" class="m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-sm btn-outline-primary">
|
||||
<i class="fa-solid fa-database me-1"></i>Generuj dane mock
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ url_for('admin.clear_mock_data') }}" class="m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-sm btn-outline-danger">
|
||||
<i class="fa-solid fa-trash-can me-1"></i>Usuń dane mock
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-body-tertiary">
|
||||
<div class="fw-semibold">
|
||||
<i class="fa-solid fa-link me-2"></i>API CEIDG
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<form method="post" action="{{ url_for('admin.save_ceidg_settings') }}" id="ceidg-config-form">
|
||||
{{ ceidg_form.hidden_tag() }}
|
||||
{{ ceidg_form.environment(value='production') }}
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded-3 p-3 h-100 bg-body-tertiary-subtle">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
||||
<label class="form-label mb-0 fw-semibold">Środowisko CEIDG</label>
|
||||
<span
|
||||
class="text-secondary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="CEIDG działa wyłącznie w środowisku produkcyjnym. Wybór środowiska został ukryty."
|
||||
>
|
||||
<i class="fa-regular fa-circle-question"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="badge rounded-pill text-bg-success-subtle text-success-emphasis border border-success-subtle">
|
||||
PROD
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-text mt-2 mb-0">
|
||||
Tryb produkcyjny jest używany stale.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded-3 p-3 h-100 bg-body-tertiary-subtle">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
||||
<label class="form-label mb-0 fw-semibold">Status klucza</label>
|
||||
<span
|
||||
class="text-secondary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="{% if ceidg_api_key_configured %}W systemie zapisano klucz CEIDG. Pozostawienie pustego pola nie usuwa obecnej wartości.{% else %}Klucz CEIDG nie został jeszcze zapisany. Wklej go poniżej i zapisz ustawienia.{% endif %}"
|
||||
>
|
||||
<i class="fa-regular fa-circle-question"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="badge rounded-pill {{ 'text-bg-success-subtle text-success-emphasis border border-success-subtle' if ceidg_api_key_configured else 'text-bg-danger-subtle text-danger-emphasis border border-danger-subtle' }}">
|
||||
{{ 'Klucz dodany' if ceidg_api_key_configured else 'Brak klucza' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-text mt-2 mb-0">
|
||||
{% if ceidg_api_key_configured %}
|
||||
Klucz jest zapisany w systemie.
|
||||
{% else %}
|
||||
Wprowadź klucz i zapisz formularz.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label d-flex align-items-center gap-2" for="{{ ceidg_form.api_key.id }}">
|
||||
<span>{{ ceidg_form.api_key.label.text }}</span>
|
||||
<span
|
||||
class="text-secondary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Wklej nowy token CEIDG. Pozostawienie pustego pola zachowuje aktualnie zapisany token."
|
||||
>
|
||||
<i class="fa-regular fa-circle-question"></i>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="input-group">
|
||||
{{ ceidg_form.api_key(class='form-control', autocomplete='off', placeholder='Wklej nowy token CEIDG') }}
|
||||
<span class="input-group-text">
|
||||
<i class="fa-solid fa-key text-secondary"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Pozostaw puste, aby zachować zapisany klucz. CEIDG działa stale na PROD.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 pt-2">
|
||||
{{ ceidg_form.submit(class='btn btn-primary px-4') }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (window.bootstrap && bootstrap.Tooltip) {
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
|
||||
new bootstrap.Tooltip(el);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
79
app/templates/admin/maintenance.html
Normal file
79
app/templates/admin/maintenance.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-toolbox me-2 text-primary"></i>Narzędzia administracyjne{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1">Logi i backupy</h4>
|
||||
<div class="text-secondary">Podstrona administracyjna do porządków technicznych, kopii bazy i operacji pomocniczych.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary mb-2">Baza danych</div>
|
||||
<h5 class="mb-3">Kopia bazy</h5>
|
||||
<div class="small text-secondary mb-1">Silnik</div>
|
||||
<div class="mb-3">{{ backup_meta.engine }}</div>
|
||||
<div class="small text-secondary mb-1">Katalog backupów</div>
|
||||
<div class="small text-break mb-3">{{ backup_meta.backup_dir }}</div>
|
||||
<div class="alert alert-{{ 'success' if backup_meta.sqlite_supported else 'warning' }} py-2 mb-3">
|
||||
{% if backup_meta.sqlite_supported %}
|
||||
Kopia z panelu działa bezpośrednio dla SQLite.
|
||||
{% else %}
|
||||
Kopia z panelu nie wykonuje natywnego dumpa dla tego silnika.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if backup_meta.sqlite_path %}
|
||||
<div class="small text-secondary mb-1">Plik SQLite</div>
|
||||
<div class="small text-break mb-3">{{ backup_meta.sqlite_path }}</div>
|
||||
{% endif %}
|
||||
<ul class="small text-secondary ps-3 mb-3">
|
||||
{% for note in backup_meta.notes %}
|
||||
<li>{{ note }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form method="post" action="{{ url_for('admin.database_backup') }}">
|
||||
{{ backup_form.hidden_tag() }}
|
||||
{{ backup_form.submit(class='btn btn-primary w-100') }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary mb-2">Porządki techniczne</div>
|
||||
<h5 class="mb-3">Czyszczenie starych logów</h5>
|
||||
<div class="text-secondary small mb-3">Usuwa rekordy logów i stare rotowane pliki `.log.*` starsze niż wskazana liczba dni.</div>
|
||||
<form method="post" action="{{ url_for('admin.cleanup_logs') }}">
|
||||
{{ cleanup_form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ cleanup_form.days.label(class='form-label') }}
|
||||
{{ cleanup_form.days(class='form-control') }}
|
||||
</div>
|
||||
{{ cleanup_form.submit(class='btn btn-outline-danger w-100') }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary mb-2">Szybkie informacje</div>
|
||||
<h5 class="mb-3">Podsumowanie</h5>
|
||||
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Użytkownicy</span><strong>{{ users }}</strong></div>
|
||||
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Firmy</span><strong>{{ companies }}</strong></div>
|
||||
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Logi audytu</span><strong>{{ audits }}</strong></div>
|
||||
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Mock aktywny</span><strong>{{ mock_enabled }}</strong></div>
|
||||
<div class="d-flex justify-content-between py-2 small"><span>Tryb R/O</span><strong>{{ 'ON' if global_ro else 'OFF' }}</strong></div>
|
||||
<hr>
|
||||
<div class="small text-secondary">Przy bazach innych niż SQLite przycisk backupu zapisze plik informacyjny, a właściwą kopię należy wykonać narzędziem serwera bazy.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
5
app/templates/admin/reset_password.html
Normal file
5
app/templates/admin/reset_password.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}Reset hasła{% endblock %}
|
||||
{% block admin_content %}
|
||||
<form method="post" class="card shadow-sm"><div class="card-body">{{ form.hidden_tag() }}<p>Użytkownik: <strong>{{ user.email }}</strong></p><div class="mb-3">{{ form.password.label(class='form-label') }}{{ form.password(class='form-control') }}</div><div class="form-check mb-3">{{ form.force_password_change(class='form-check-input') }} {{ form.force_password_change.label(class='form-check-label') }}</div>{{ form.submit(class='btn btn-warning') }}</div></form>
|
||||
{% endblock %}
|
||||
215
app/templates/admin/system_data.html
Normal file
215
app/templates/admin/system_data.html
Normal file
@@ -0,0 +1,215 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-microchip me-2 text-primary"></i>Dane systemowe{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1">Dane systemowe</h4>
|
||||
<div class="text-secondary">Skrócony widok techniczny: proces, health, baza, integracje i katalogi.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-{{ 'warning' if data.health.redis in ['fallback', 'error'] else 'secondary' }} py-2 mb-3">
|
||||
<div class="d-flex flex-wrap justify-content-between gap-2 align-items-center">
|
||||
<strong>Redis: {{ data.health.redis|upper }}</strong>
|
||||
<span class="small text-break">{{ data.health.redis_details or 'brak szczegółów' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
{% for card in data.overview %}
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<div>
|
||||
<div class="small text-secondary text-uppercase mb-1">{{ card.label }}</div>
|
||||
<div class="fs-4 fw-semibold mb-1">{{ card.value }}</div>
|
||||
<div class="small text-secondary">{{ card.subvalue }}</div>
|
||||
</div>
|
||||
<span class="badge text-bg-{{ card.tone }} rounded-pill"><i class="fa-solid {{ card.icon }}"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-xl-7">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-body-tertiary py-2"><strong>Proces i health</strong></div>
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="row g-2 small">
|
||||
<div class="col-6"><div class="text-secondary">CPU</div><div class="fw-semibold">{{ data.process.cpu_percent }}%</div></div>
|
||||
<div class="col-6"><div class="text-secondary">RAM</div><div class="fw-semibold">{{ data.process.rss_human }}</div></div>
|
||||
<div class="col-6"><div class="text-secondary">PID</div><div>{{ data.process.pid }}</div></div>
|
||||
<div class="col-6"><div class="text-secondary">Wątki</div><div>{{ data.process.threads }}</div></div>
|
||||
<div class="col-6"><div class="text-secondary">Otwarte pliki</div><div>{{ data.process.open_files }}</div></div>
|
||||
<div class="col-6"><div class="text-secondary">Pamięć hosta</div><div>{{ data.process.system_memory_percent }}%</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row g-2">
|
||||
{% set ok_values = ['ok', 'mock', 'configured', 'fallback'] %}
|
||||
{% set health_items = [('Baza', data.health.db), ('SMTP', data.health.smtp), ('Redis', data.health.redis), ('KSeF', data.health.ksef), ('CEIDG', data.health.ceidg)] %}
|
||||
{% for label, value in health_items %}
|
||||
<div class="col-6">
|
||||
<div class="border rounded p-2 h-100 small">
|
||||
<div class="text-secondary">{{ label }}</div>
|
||||
<div class="fw-semibold">
|
||||
<span class="badge text-bg-{{ 'success' if value in ok_values else 'danger' if value == 'error' else 'secondary' }} rounded-pill">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="col-12">
|
||||
<div class="border rounded p-2 small d-flex justify-content-between align-items-center">
|
||||
<span>Podsumowanie health</span><strong>{{ data.overview[5].value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row g-2 small text-secondary">
|
||||
<div class="col-md-6"><strong>Python:</strong> {{ data.process.python }}</div>
|
||||
<div class="col-md-6 text-break"><strong>Platforma:</strong> {{ data.process.platform }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-5">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-body-tertiary py-2"><strong>Aplikacja</strong></div>
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2 mb-3 text-center">
|
||||
<div class="col-6"><div class="border rounded p-2"><div class="small text-secondary">Użytkownicy</div><div class="fs-4 fw-semibold">{{ data.app.users_count }}</div></div></div>
|
||||
<div class="col-6"><div class="border rounded p-2"><div class="small text-secondary">Firmy</div><div class="fs-4 fw-semibold">{{ data.app.companies_count }}</div></div></div>
|
||||
<div class="col-12"><div class="border rounded p-2 d-flex justify-content-between align-items-center"><span>Tryb tylko do odczytu</span><span class="badge text-bg-{{ 'warning' if data.app.read_only_global else 'success' }} rounded-pill">{{ 'ON' if data.app.read_only_global else 'OFF' }}</span></div></div>
|
||||
</div>
|
||||
<div class="small text-secondary mb-1">Aktywna firma</div>
|
||||
<div class="mb-2">{{ data.app.current_company }}</div>
|
||||
<div class="small text-secondary mb-1">Strefa czasowa</div>
|
||||
<div class="mb-3">{{ data.app.app_timezone }}</div>
|
||||
<div class="small text-secondary mb-2">Największe zbiory danych</div>
|
||||
{% for item in data.app.counts_top[:5] %}
|
||||
<div class="d-flex justify-content-between small border-bottom py-1"><span>{{ item.label }}</span><strong>{{ item.count }}</strong></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-xl-5">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-body-tertiary py-2"><strong>Baza danych</strong></div>
|
||||
<div class="card-body p-3 small">
|
||||
<div class="mb-1"><span class="text-secondary">Silnik:</span> {{ data.database.engine }}</div>
|
||||
<div class="mb-2 text-break"><span class="text-secondary">Połączenie:</span> {{ data.database.uri }}</div>
|
||||
{% if data.database.sqlite_path %}
|
||||
<div class="mb-2 text-break"><span class="text-secondary">SQLite:</span> {{ data.database.sqlite_path }} <span class="text-secondary">({{ data.database.sqlite_size }})</span></div>
|
||||
{% endif %}
|
||||
<div class="small text-secondary mb-2">Największe tabele</div>
|
||||
{% for item in data.database.largest_tables %}
|
||||
<div class="d-flex justify-content-between border-bottom py-1"><span>{{ item.table }}</span><strong>{{ item.rows }}</strong></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-7">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-body-tertiary py-2"><strong>Katalogi robocze</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead><tr><th>Katalog</th><th>Rozmiar</th><th>Wolne</th><th class="text-end">Zajęcie</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in data.storage %}
|
||||
<tr>
|
||||
<td><div>{{ item.label }}</div><div class="small text-secondary text-break">{{ item.path }}</div></td>
|
||||
<td>{{ item.size_human }}</td>
|
||||
<td>{{ item.disk_free }}</td>
|
||||
<td class="text-end">{{ item.disk_percent }}%</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="integrations" class="row g-3 mb-3">
|
||||
<div class="col-xl-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-body-tertiary py-2">
|
||||
<strong>Połączenie KSeF</strong>
|
||||
<span class="badge text-bg-{{ 'success' if data.integrations.ksef.status in ['ok', 'mock'] else 'danger' }} rounded-pill">{{ data.integrations.ksef.status }}</span>
|
||||
</div>
|
||||
<div class="card-body p-3 small">
|
||||
<div class="mb-2"><span class="text-secondary">Komunikat:</span> {{ data.integrations.ksef.message }}</div>
|
||||
<div class="mb-2 text-break"><span class="text-secondary">Endpoint:</span> {{ data.integrations.ksef.base_url or '—' }}</div>
|
||||
{% if data.integrations.ksef.auth_mode %}<div class="mb-2"><span class="text-secondary">Tryb autoryzacji:</span> {{ data.integrations.ksef.auth_mode }}</div>{% endif %}
|
||||
<details>
|
||||
<summary class="text-secondary">Przykładowa odpowiedź API</summary>
|
||||
<pre class="small bg-body-tertiary p-3 rounded overflow-auto mt-2" style="max-height:18rem;">{{ json_preview(data.integrations.ksef.sample) }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-body-tertiary py-2">
|
||||
<strong>Połączenie CEIDG</strong>
|
||||
<span class="badge text-bg-{{ 'success' if data.integrations.ceidg.status == 'ok' else 'danger' }} rounded-pill">{{ data.integrations.ceidg.status }}</span>
|
||||
</div>
|
||||
<div class="card-body p-3 small">
|
||||
<div class="mb-2"><span class="text-secondary">Komunikat:</span> {{ data.integrations.ceidg.message }}</div>
|
||||
<div class="mb-2"><span class="text-secondary">Tryb:</span> {{ data.integrations.ceidg.environment }}</div>
|
||||
<div class="mb-2 text-break"><span class="text-secondary">Endpoint:</span> {{ data.integrations.ceidg.url }}</div>
|
||||
{% if data.integrations.ceidg.technical_details %}<div class="mb-2 text-break"><span class="text-secondary">Szczegóły:</span> {{ data.integrations.ceidg.technical_details }}</div>{% endif %}
|
||||
<details>
|
||||
<summary class="text-secondary">Przykładowa odpowiedź API</summary>
|
||||
<pre class="small bg-body-tertiary p-3 rounded overflow-auto mt-2" style="max-height:18rem;">{{ json_preview(data.integrations.ceidg.sample) }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-body-tertiary py-2"><strong>Modele aplikacji</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead><tr><th>Obiekt</th><th class="text-end">Liczba</th></tr></thead>
|
||||
<tbody>{% for item in data.app.counts %}<tr><td>{{ item.label }}</td><td class="text-end">{{ item.count }}</td></tr>{% endfor %}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-body-tertiary py-2"><strong>Tabele bazy</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead><tr><th>Tabela</th><th class="text-end">Rekordy</th></tr></thead>
|
||||
<tbody>{% for item in data.database.table_rows %}<tr><td>{{ item.table }}</td><td class="text-end">{{ item.rows }}</td></tr>{% endfor %}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
8
app/templates/admin/user_access.html
Normal file
8
app/templates/admin/user_access.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}Uprawnienia: {{ user.name }}{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-5"><form method="post" class="card shadow-sm"><div class="card-body">{{ form.hidden_tag() }}<div class="mb-3">{{ form.company_id.label(class='form-label') }}{{ form.company_id(class='form-select') }}</div><div class="mb-3">{{ form.access_level.label(class='form-label') }}{{ form.access_level(class='form-select') }}</div>{{ form.submit(class='btn btn-primary') }}</div></form></div>
|
||||
<div class="col-lg-7"><div class="card shadow-sm"><div class="card-body"><table class="table"><thead><tr><th>Firma</th><th>Dostęp</th><th></th></tr></thead><tbody>{% for access in accesses %}<tr><td>{{ access.company.name }}</td><td><span class="badge text-bg-{{ 'warning' if access.access_level=='readonly' else 'success' }}">{{ access.access_level }}</span></td><td class="text-end"><form method="post" action="{{ url_for('admin.delete_access', user_id=user.id, access_id=access.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn btn-sm btn-outline-danger">Usuń</button></form></td></tr>{% endfor %}</tbody></table></div></div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
38
app/templates/admin/user_form.html
Normal file
38
app/templates/admin/user_form.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}{{ 'Edycja użytkownika' if user else 'Nowy użytkownik' }}{% endblock %}
|
||||
{% block admin_content %}
|
||||
<form method="post" class="card shadow-sm">
|
||||
<div class="card-body">{{ form.hidden_tag() }}
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="border rounded-4 p-3 h-100">
|
||||
<h5 class="mb-3">Dane użytkownika</h5>
|
||||
<div class="mb-3">{{ form.name.label(class='form-label') }}{{ form.name(class='form-control') }}</div>
|
||||
<div class="mb-3">{{ form.email.label(class='form-label') }}{{ form.email(class='form-control') }}</div>
|
||||
<div class="mb-3">{{ form.role.label(class='form-label') }}{{ form.role(class='form-select') }}</div>
|
||||
<div class="form-check mb-2">{{ form.is_blocked(class='form-check-input') }} {{ form.is_blocked.label(class='form-check-label') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="border rounded-4 p-3 h-100">
|
||||
<h5 class="mb-3">Hasło i dostęp startowy</h5>
|
||||
<div class="mb-3">{{ form.password.label(class='form-label') }}{{ form.password(class='form-control') }}<div class="form-text">Pozostaw puste, aby nie zmieniać hasła.</div></div>
|
||||
<div class="form-check mb-3">{{ form.force_password_change(class='form-check-input') }} {{ form.force_password_change.label(class='form-check-label') }}</div>
|
||||
<div class="mb-3">{{ form.company_id.label(class='form-label') }}{{ form.company_id(class='form-select') }}</div>
|
||||
<div class="mb-3">{{ form.access_level.label(class='form-label') }}{{ form.access_level(class='form-select') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user %}
|
||||
<div class="col-12">
|
||||
<div class="border rounded-4 p-3">
|
||||
<h5 class="mb-3">Przypisane firmy</h5>
|
||||
<div class="d-flex gap-2 flex-wrap">{% for access in accesses %}<span class="badge text-bg-{{ 'warning' if access.access_level=='readonly' else 'primary' }} p-2">{{ access.company.name }} / {{ access.access_level }}</span>{% else %}<span class="text-secondary">Brak przypisanych firm.</span>{% endfor %}</div>
|
||||
<div class="mt-3"><a class="btn btn-outline-secondary" href="{{ url_for('admin.user_access', user_id=user.id) }}">Zarządzaj wieloma firmami</a></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-3">{{ form.submit(class='btn btn-primary') }}</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
9
app/templates/admin/users.html
Normal file
9
app/templates/admin/users.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'admin/admin_base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-users me-2 text-primary"></i>Użytkownicy{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3"><div><h4 class="mb-0">Użytkownicy</h4><div class="text-secondary small">Zarządzanie kontami, blokadami i resetem hasła.</div></div><a class="btn btn-primary" href="{{ url_for('admin.user_form') }}"><i class="fa-solid fa-user-plus me-2"></i>Dodaj użytkownika</a></div>
|
||||
<div class="card shadow-sm"><div class="table-responsive"><table class="table table-hover align-middle mb-0">
|
||||
<thead><tr><th>Użytkownik</th><th>Rola</th><th>Status</th><th>Dostęp do firm</th><th></th></tr></thead>
|
||||
<tbody>{% for user in users %}<tr><td><div class="fw-semibold">{{ user.name }}</div><div class="small text-secondary">{{ user.email }}</div></td><td><span class="badge text-bg-light border">{{ user.role }}</span></td><td>{% if user.is_blocked %}<span class="badge text-bg-danger">zablokowany</span>{% else %}<span class="badge text-bg-success">aktywny</span>{% endif %}{% if user.force_password_change %}<span class="badge text-bg-warning ms-1">zmiana hasła</span>{% endif %}</td><td>{% for access in user.company_access %}<span class="badge text-bg-{{ 'warning' if access.access_level=='readonly' else 'primary' }} me-1 mb-1">{{ access.company.name }} / {{ access.access_level }}</span>{% else %}<span class="text-secondary small">brak przypisanych firm</span>{% endfor %}</td><td class="text-end text-nowrap"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('admin.user_form', user_id=user.id) }}"><i class="fa-solid fa-pen"></i></a> <a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin.user_access', user_id=user.id) }}"><i class="fa-solid fa-key"></i></a> <a class="btn btn-sm btn-outline-warning" href="{{ url_for('admin.reset_password', user_id=user.id) }}"><i class="fa-solid fa-unlock-keyhole"></i></a> <form class="d-inline" method="post" action="{{ url_for('admin.toggle_block', user_id=user.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn btn-sm btn-outline-danger">{% if user.is_blocked %}<i class="fa-solid fa-user-check"></i>{% else %}<i class="fa-solid fa-user-lock"></i>{% endif %}</button></form></td></tr>{% endfor %}</tbody>
|
||||
</table></div></div>
|
||||
{% endblock %}
|
||||
41
app/templates/auth/login.html
Normal file
41
app/templates/auth/login.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html lang="pl" data-bs-theme="{{ 'dark' if theme == 'dark' else 'light' }}">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
|
||||
<link href="{{ static_asset('css/app.css') }}" rel="stylesheet">
|
||||
<title>Logowanie | KSeF Manager</title>
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="container-fluid">
|
||||
<div class="row min-vh-100">
|
||||
<div class="col-lg-6 login-hero d-none d-lg-flex">
|
||||
<div class="login-hero-card">
|
||||
<span class="brand-icon mb-4"><i class="fa-solid fa-file-invoice-dollar"></i></span>
|
||||
<h1 class="mt-4 mb-3">KSeF Manager</h1>
|
||||
<p class="lead text-secondary">Logowanie do panelu faktur, KSeF, powiadomień i konfiguracji administracyjnej.</p>
|
||||
<div class="login-feature-list">
|
||||
<div><i class="fa-solid fa-check text-primary me-2"></i>Zarządzj fakturami w jednym miejscu</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 d-flex align-items-center justify-content-center p-4">
|
||||
<div class="card login-form-card shadow-lg border-0 w-100">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<span class="brand-icon"><i class="fa-solid fa-lock"></i></span>
|
||||
<div><h3 class="mb-1">Logowanie</h3><div class="text-secondary small">Zaloguj się, aby przejść do panelu.</div></div>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}{% for category, message in messages if message != 'Please log in to access this page.' %}<div class="alert alert-{{ category }}">{{ message }}</div>{% endfor %}{% endwith %}
|
||||
<form method="post" class="row g-3">{{ form.hidden_tag() }}
|
||||
<div class="col-12">{{ form.email.label(class='form-label') }}{{ form.email(class='form-control form-control-lg', placeholder='twoj@email.pl') }}</div>
|
||||
<div class="col-12">{{ form.password.label(class='form-label') }}{{ form.password(class='form-control form-control-lg', placeholder='Hasło') }}</div>
|
||||
<div class="col-12 d-grid">{{ form.submit(class='btn btn-primary btn-lg') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
96
app/templates/base.html
Normal file
96
app/templates/base.html
Normal file
@@ -0,0 +1,96 @@
|
||||
<!doctype html>
|
||||
<html lang="pl" data-bs-theme="{{ 'dark' if theme == 'dark' else 'light' }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ app_name }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
|
||||
<link href="{{ static_asset('css/app.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell d-flex">
|
||||
<aside class="sidebar bg-body-tertiary border-end p-3">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="brand-icon"><i class="fa-solid fa-file-invoice-dollar"></i></span>
|
||||
<div>
|
||||
<h5 class="mb-0">{{ app_name }}</h5>
|
||||
<div class="small text-secondary">Panel KSeF i archiwum</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section-label">Główne</div>
|
||||
<nav class="nav flex-column gap-1 mb-3">
|
||||
<a class="nav-link" href="{{ url_for('dashboard.index') }}"><i class="fa-solid fa-chart-column me-2"></i>Dashboard</a>
|
||||
<a class="nav-link" href="{{ url_for('invoices.index') }}"><i class="fa-solid fa-table-list me-2"></i>Faktury Otrzymane</a>
|
||||
<a class="nav-link" href="{{ url_for('invoices.issued_list') }}"><i class="fa-solid fa-paper-plane me-2"></i>Faktury Wystawione</a>
|
||||
<a class="nav-link nav-link-accent" href="{{ url_for('invoices.issued_new') }}"><i class="fa-solid fa-square-plus me-2"></i>Wystaw fakturę</a>
|
||||
{% if nfz_module_enabled %}
|
||||
<a class="nav-link nav-link-highlight" href="{{ url_for('nfz.index') }}"><i class="fa-solid fa-hospital me-2"></i>Faktury NFZ</a>
|
||||
{% endif %}
|
||||
<a class="nav-link" href="{{ url_for('invoices.monthly') }}"><i class="fa-solid fa-calendar-days me-2"></i>Zestawienia</a>
|
||||
</nav>
|
||||
|
||||
<div class="menu-section-label">Kartoteki</div>
|
||||
<nav class="nav flex-column gap-1 mb-3">
|
||||
<a class="nav-link" href="{{ url_for('invoices.customers') }}"><i class="fa-solid fa-address-book me-2"></i>Kontrahenci</a>
|
||||
<a class="nav-link" href="{{ url_for('invoices.products') }}"><i class="fa-solid fa-boxes-stacked me-2"></i>Towary i usługi</a>
|
||||
</nav>
|
||||
|
||||
<div class="menu-section-label">Konfiguracja</div>
|
||||
<nav class="nav flex-column gap-1">
|
||||
<a class="nav-link" href="{{ url_for('notifications.index') }}"><i class="fa-solid fa-bell me-2"></i>Powiadomienia</a>
|
||||
<a class="nav-link" href="{{ url_for('settings.index') }}"><i class="fa-solid fa-gear me-2"></i>Ustawienia</a>
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||
<a class="nav-link" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-shield-halved me-2"></i>Admin</a>
|
||||
{% endif %}
|
||||
<a class="nav-link text-danger-emphasis" href="{{ url_for('auth.logout') }}"><i class="fa-solid fa-right-from-bracket me-2"></i>Wyloguj</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="flex-grow-1 main-column">
|
||||
<header class="border-bottom px-4 py-3 bg-body sticky-top page-topbar">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="text-secondary small mb-1">{{ current_company.name if current_company else 'Brak aktywnej firmy' }}</div>
|
||||
<strong class="page-title">{% block title %}{% endblock %}</strong>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center flex-wrap">
|
||||
{% if current_company %}
|
||||
<div class="d-flex align-items-center gap-2 top-chip">
|
||||
<i class="fa-solid fa-building text-primary"></i>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">{{ current_company.name }}</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% for item in available_companies %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('dashboard.switch_company', company_id=item.id) }}">{{ item.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="small text-secondary top-chip"><i class="fa-solid fa-circle-half-stroke me-1"></i>{{ 'Ciemny' if theme == 'dark' else 'Jasny' }}</span>
|
||||
{% if read_only_mode %}<span class="readonly-pill readonly-pill-compact"><i class="fa-solid fa-eye me-1"></i>R/O</span>{% endif %}
|
||||
<a class="btn btn-sm btn-primary" href="{{ url_for('dashboard.index') }}" title="Strona główna"><i class="fa-solid fa-house"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="p-4 page-content-wrap">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} shadow-sm alert-dismissible fade show" role="alert"><i class="fa-solid fa-circle-info me-2"></i>{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
<footer class="global-footer border-top bg-body px-4 py-3 text-secondary small">{{ global_footer_text }}</footer>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
135
app/templates/dashboard/index.html
Normal file
135
app/templates/dashboard/index.html
Normal 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 %}
|
||||
1
app/templates/errors/403.html
Normal file
1
app/templates/errors/403.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}{% block title %}403{% endblock %}{% block content %}<div class="text-center py-5"><h1>403</h1><p>Brak uprawnień do tej operacji.</p></div>{% endblock %}
|
||||
1
app/templates/errors/404.html
Normal file
1
app/templates/errors/404.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}{% block title %}404{% endblock %}{% block content %}<div class="text-center py-5"><h1>404</h1><p>Nie znaleziono strony lub zasobu.</p></div>{% endblock %}
|
||||
1
app/templates/errors/500.html
Normal file
1
app/templates/errors/500.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}{% block title %}500{% endblock %}{% block content %}<div class="text-center py-5"><h1>500</h1><p>Wystąpił błąd serwera. Spróbuj ponownie.</p></div>{% endblock %}
|
||||
1
app/templates/errors/503.html
Normal file
1
app/templates/errors/503.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}{% block title %}503{% endblock %}{% block content %}<div class="container py-5"><div class="alert alert-warning shadow-sm"><h1 class="h3 mb-3">Usługa chwilowo niedostępna</h1><p class="mb-2">{{ message or 'Usługa pomocnicza jest chwilowo niedostępna.' }}</p><p class="mb-0 text-secondary small">Najczęściej oznacza to brak połączenia z Redisem. Błąd został przechwycony i nie powoduje już surowego błędu 500.</p></div></div>{% endblock %}
|
||||
62
app/templates/invoices/customers.html
Normal file
62
app/templates/invoices/customers.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-address-book me-2 text-primary"></i>Kontrahenci{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Kartoteka' %}{% set heading='Kontrahenci' %}{% set description='Lista kontrahentów i szybka edycja danych.' %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><i class="fa-solid fa-user-tie me-2"></i>{{ 'Edytuj kontrahenta' if editing else 'Nowy kontrahent' }}</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-2"><label class="form-label">Nazwa</label><input class="form-control" name="name" value="{{ editing.name if editing else '' }}" placeholder="Przy pobieraniu z CEIDG uzupełni się automatycznie" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-2"><label class="form-label">NIP</label><div class="input-group"><input class="form-control" name="tax_id" value="{{ editing.tax_id if editing else '' }}" placeholder="Wystarczy podać NIP" {% if read_only_mode %}disabled{% endif %}><button class="btn btn-outline-secondary" name="fetch_ceidg" value="1" {% if read_only_mode %}disabled{% endif %}>CEIDG</button></div><div class="form-text">Do pobrania danych z CEIDG wystarczy sam NIP.</div></div>
|
||||
<div class="mb-2"><label class="form-label">REGON</label><input class="form-control" name="regon" value="{{ editing.regon if editing else '' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-2"><label class="form-label">Adres</label><input class="form-control" name="address" value="{{ editing.address if editing else '' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-3"><label class="form-label">E-mail</label><input class="form-control" name="email" value="{{ editing.email if editing else '' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="d-grid gap-2"><button class="btn btn-primary" {% if read_only_mode %}disabled{% endif %}>{{ 'Zapisz kontrahenta' if editing else 'Dodaj kontrahenta' }}</button>{% if editing %}<a class="btn btn-outline-secondary" href="{{ url_for('invoices.customers') }}">Anuluj edycję</a>{% endif %}</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div><i class="fa-solid fa-users me-2"></i>Baza kontrahentów</div>
|
||||
<form method="get" class="row g-2 align-items-end w-100 ms-0">
|
||||
<div class="col-md-7"><label class="form-label form-label-sm mb-1">Szukaj</label><input class="form-control form-control-sm" type="search" name="q" value="{{ search or '' }}" placeholder="Szukaj po nazwie, NIP, REGON, mailu..."></div>
|
||||
<div class="col-md-4"><label class="form-label form-label-sm mb-1">Sortowanie</label><select class="form-select form-select-sm" name="sort">
|
||||
<option value="name_asc" {{ 'selected' if sort == 'name_asc' else '' }}>A-Z</option>
|
||||
<option value="name_desc" {{ 'selected' if sort == 'name_desc' else '' }}>Z-A</option>
|
||||
<option value="tax_id_asc" {{ 'selected' if sort == 'tax_id_asc' else '' }}>NIP rosnąco</option>
|
||||
<option value="tax_id_desc" {{ 'selected' if sort == 'tax_id_desc' else '' }}>NIP malejąco</option>
|
||||
</select></div>
|
||||
<div class="col-md-1 d-grid"><button class="btn btn-sm btn-outline-secondary"><i class="fa-solid fa-magnifying-glass"></i></button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead><tr><th>Nazwa</th><th>NIP</th><th>Adres</th><th>E-mail</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr><td><div class="fw-semibold">{{ item.name }}</div><div class="small text-secondary">{{ item.regon }}</div></td><td>{{ item.tax_id }}</td><td>{{ item.address }}</td><td>{{ item.email }}</td><td class="text-end"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('invoices.customers', customer_id=item.id) }}">Edytuj</a></td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-secondary text-center py-4">Brak kontrahentów.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body border-top py-2">
|
||||
<nav>
|
||||
<ul class="pagination justify-content-end mb-0">
|
||||
{% if pagination.has_prev %}<li class="page-item"><a class="page-link" href="{{ url_for('invoices.customers', page=pagination.prev_num, q=search, sort=sort) }}">Poprz.</a></li>{% endif %}
|
||||
<li class="page-item disabled"><span class="page-link">{{ pagination.page }} / {{ pagination.pages or 1 }}</span></li>
|
||||
{% if pagination.has_next %}<li class="page-item"><a class="page-link" href="{{ url_for('invoices.customers', page=pagination.next_num, q=search, sort=sort) }}">Dalej</a></li>{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
103
app/templates/invoices/detail.html
Normal file
103
app/templates/invoices/detail.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-file-lines me-2 text-primary"></i>Faktura{% endblock %}
|
||||
{% block content %}
|
||||
<div class="invoice-detail-layout align-items-start">
|
||||
<div class="invoice-detail-main">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header"><i class="fa-solid fa-circle-info me-2"></i>Szczegóły faktury</div>
|
||||
<div class="card-body">
|
||||
<div class="row small g-2">
|
||||
<div class="col-md-6"><strong>Numer:</strong> {{ invoice.invoice_number }}</div>
|
||||
<div class="col-md-6"><strong>Numer KSeF:</strong> {{ invoice.ksef_number }}</div>
|
||||
<div class="col-md-6"><strong>Kontrahent:</strong> {{ invoice.contractor_name }}</div>
|
||||
<div class="col-md-6"><strong>NIP:</strong> {{ invoice.contractor_nip }}</div>
|
||||
<div class="col-md-6"><strong>Adres:</strong> {{ invoice.contractor_address or '—' }}</div>
|
||||
<div class="col-md-6"><strong>Kartoteka klientów:</strong> {{ linked_customer.name if linked_customer else 'brak powiązania' }}</div>
|
||||
<div class="col-md-4"><strong>Netto:</strong> {{ invoice.net_amount|pln }}</div>
|
||||
<div class="col-md-4"><strong>VAT:</strong> {{ invoice.vat_amount|pln }}</div>
|
||||
<div class="col-md-4"><strong>Brutto:</strong> {{ invoice.gross_amount|pln }}</div>
|
||||
<div class="col-md-6"><strong>Split payment:</strong> <span class="badge text-bg-{{ 'warning' if invoice.split_payment else 'secondary' }}">{{ 'Tak' if invoice.split_payment else 'Nie' }}</span></div>
|
||||
<div class="col-md-6"><strong>Forma płatności:</strong> {{ payment_details.payment_form_label or '—' }}</div><div class="col-md-6"><strong>Rachunek bankowy:</strong> {{ payment_details.bank_account or (invoice.company.bank_account if invoice.company and invoice.source in ['issued', 'nfz'] else '') or '—' }}</div>{% if payment_details.bank_name %}<div class="col-md-6"><strong>Bank:</strong> {{ payment_details.bank_name }}</div>{% endif %}{% if payment_details.payment_due_date %}<div class="col-md-6"><strong>Termin płatności:</strong> {{ payment_details.payment_due_date }}</div>{% endif %}
|
||||
{% if invoice.source in ['issued', 'nfz'] %}
|
||||
<div class="col-md-6"><strong>Status wystawienia:</strong> {{ invoice.issued_status_label }}</div>
|
||||
<div class="col-md-6"><strong>KSeF:</strong> <span class="badge text-bg-{{ 'success' if invoice.issued_to_ksef_at else 'secondary' }}">{{ 'Przesłana do KSeF' if invoice.issued_to_ksef_at else 'Nieprzesłana do KSeF' }}</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if invoice.source in ['issued', 'nfz'] and not invoice.issued_to_ksef_at %}<div class="alert alert-warning mt-3 mb-0">Ta faktura nie została jeszcze wysłana do KSeF. Możesz ją edytować i wysłać później.</div>{% elif invoice.source in ['issued', 'nfz'] and invoice.issued_to_ksef_at %}<div class="alert alert-info mt-3 mb-0">Faktura została wysłana do KSeF {{ invoice.issued_to_ksef_at }}. Edycja jest zablokowana.</div>{% endif %}
|
||||
{% if invoice.external_metadata and invoice.external_metadata.get('nfz') %}<div class="alert alert-success mt-3 mb-0"><strong>Moduł NFZ:</strong> {{ invoice.external_metadata.get('nfz', {}).get('recipient_branch_name') }} · okres {{ invoice.external_metadata.get('nfz', {}).get('settlement_from') }} - {{ invoice.external_metadata.get('nfz', {}).get('settlement_to') }} · umowa {{ invoice.external_metadata.get('nfz', {}).get('contract_number') }}</div>{% endif %}
|
||||
<hr>
|
||||
<div class="mb-3">{% for tag in invoice.tags %}<span class="badge text-bg-{{ tag.color }} me-1">{{ tag.name }}</span>{% endfor %}</div>
|
||||
<div class="d-flex gap-2 flex-wrap mb-0">
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('invoices.pdf', invoice_id=invoice.id) }}"><i class="fa-solid fa-file-pdf me-1"></i>Pobierz PDF</a>
|
||||
{% if can_add_seller_customer %}
|
||||
<form method="post" action="{{ url_for('invoices.add_seller_customer', invoice_id=invoice.id) }}">
|
||||
{{ form.csrf_token }}
|
||||
<button class="btn btn-outline-success" {% if edit_locked %}disabled{% endif %}>
|
||||
<i class="fa-solid fa-user-plus me-1"></i>{{ 'Przejdź do kontrahenta' if linked_customer else 'Dodaj sprzedawcę do kontrahentów' }}
|
||||
</button>
|
||||
</form>
|
||||
{% elif linked_customer %}
|
||||
<a class="btn btn-outline-success" href="{{ url_for('invoices.customers', customer_id=linked_customer.id) }}"><i class="fa-solid fa-address-book me-1"></i>Otwórz kontrahenta</a>
|
||||
{% endif %}
|
||||
{% if invoice.source in ['issued', 'nfz'] %}
|
||||
<a class="btn btn-outline-secondary {% if edit_locked %}disabled{% endif %}" href="{{ url_for('invoices.duplicate', invoice_id=invoice.id) }}">
|
||||
<i class="fa-solid fa-copy me-1"></i>Duplikuj do wystawienia
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if invoice.source == 'issued' and not invoice.issued_to_ksef_at %}<a class="btn btn-outline-secondary" href="{{ url_for('invoices.issued_edit', invoice_id=invoice.id) }}"><i class="fa-solid fa-pen-to-square me-1"></i>Edytuj fakturę</a><form method="post" action="{{ url_for('invoices.send_to_ksef', invoice_id=invoice.id) }}">{{ form.csrf_token }}<button class="btn btn-primary"><i class="fa-solid fa-paper-plane me-1"></i>Wyślij do KSeF</button></form>{% elif invoice.source == 'nfz' and not invoice.issued_to_ksef_at %}<a class="btn btn-outline-secondary" href="{{ url_for('nfz.edit', invoice_id=invoice.id) }}"><i class="fa-solid fa-pen-to-square me-1"></i>Edytuj fakturę NFZ</a><form method="post" action="{{ url_for('nfz.send_to_ksef', invoice_id=invoice.id) }}">{{ form.csrf_token }}<button class="btn btn-primary"><i class="fa-solid fa-paper-plane me-1"></i>Wyślij NFZ do KSeF</button></form>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header"><i class="fa-solid fa-eye me-2"></i>Podgląd faktury</div>
|
||||
<div class="card-body">
|
||||
<div class="border rounded p-3 bg-white overflow-auto invoice-preview-surface">{{ invoice.html_preview|safe if invoice.html_preview else 'Brak podglądu HTML.' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="fa-solid fa-code me-2"></i>Surowy XML</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#invoiceRawXml" aria-expanded="false" aria-controls="invoiceRawXml">
|
||||
<i class="fa-solid fa-chevron-down me-1"></i>Pokaż XML
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="invoiceRawXml">
|
||||
<div class="card-body">
|
||||
<pre class="small bg-body-tertiary p-3 overflow-auto mb-0" style="max-height:26rem; white-space:pre-wrap;">{{ xml_content if xml_content else 'Brak XML.' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="invoice-detail-sidebar">
|
||||
<div class="invoice-detail-sticky">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header"><i class="fa-solid fa-pen-to-square me-2"></i>Metadane</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-2">{{ form.status.label(class='form-label') }}{{ form.status(class='form-select', disabled=edit_locked) }}</div>
|
||||
<div class="mb-2">{{ form.tags.label(class='form-label') }}{{ form.tags(class='form-control', disabled=edit_locked) }}</div>
|
||||
<div class="mb-2">{{ form.internal_note.label(class='form-label') }}{{ form.internal_note(class='form-control', rows=4, disabled=edit_locked) }}</div>
|
||||
<div class="form-check">{{ form.queue_accounting(class='form-check-input', disabled=edit_locked) }}{{ form.queue_accounting.label(class='form-check-label') }}</div>
|
||||
<div class="form-check mb-2">{{ form.pinned(class='form-check-input', disabled=edit_locked) }}{{ form.pinned.label(class='form-check-label') }}</div>
|
||||
{{ form.submit(class='btn btn-primary w-100', disabled=edit_locked) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><i class="fa-solid fa-paper-plane me-2"></i>Wyślij mailem</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ url_for('invoices.send', invoice_id=invoice.id) }}">
|
||||
{{ form.csrf_token }}
|
||||
<input class="form-control mb-2" type="email" name="recipient" placeholder="odbiorca@example.com" {% if edit_locked %}disabled{% endif %}>
|
||||
<button class="btn btn-outline-primary w-100" {% if edit_locked %}disabled{% endif %}><i class="fa-solid fa-envelope me-1"></i>Wyślij PDF</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
149
app/templates/invoices/index.html
Normal file
149
app/templates/invoices/index.html
Normal file
@@ -0,0 +1,149 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-file-import me-2 text-primary"></i>Faktury otrzymane{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set args = request.args.to_dict() %}
|
||||
{% if 'page' in args %}
|
||||
{% set _ = args.pop('page') %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1">Faktury otrzymane</h5>
|
||||
<div class="text-muted small">Tutaj widzisz tylko dokumenty kosztowe i otrzymane od kontrahentów.</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('invoices.issued_list') }}">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square me-1"></i>Przejdź do wystawionych
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="get" class="row g-2">
|
||||
<div class="col-md-1">{{ form.month(class='form-select') }}</div>
|
||||
<div class="col-md-1">{{ form.year(class='form-control', placeholder='Rok') }}</div>
|
||||
<div class="col-md-2">{{ form.contractor(class='form-control', placeholder='Kontrahent') }}</div>
|
||||
<div class="col-md-1">{{ form.nip(class='form-control', placeholder='NIP') }}</div>
|
||||
<div class="col-md-2">{{ form.invoice_type(class='form-select') }}</div>
|
||||
<div class="col-md-2">{{ form.status(class='form-select') }}</div>
|
||||
<div class="col-md-1">{{ form.quick_filter(class='form-select') }}</div>
|
||||
<div class="col-md-2">{{ form.search(class='form-control', placeholder='Szukaj') }}</div>
|
||||
<div class="col-md-2">{{ form.min_amount(class='form-control', placeholder='Min') }}</div>
|
||||
<div class="col-md-2">{{ form.max_amount(class='form-control', placeholder='Max') }}</div>
|
||||
<div class="col-md-2">{{ form.submit(class='btn btn-primary w-100') }}</div>
|
||||
<div class="col-md-2">
|
||||
<a class="btn btn-outline-secondary w-100" href="{{ url_for('invoices.export_csv', **request.args) }}">
|
||||
<i class="fa-solid fa-file-csv me-1"></i>CSV
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a class="btn btn-outline-secondary w-100" href="{{ url_for('invoices.export_zip', **request.args) }}">
|
||||
<i class="fa-solid fa-file-zipper me-1"></i>ZIP
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('invoices.bulk_action') }}">
|
||||
{{ form.csrf_token }}
|
||||
|
||||
<div class="d-flex gap-2 mb-2 flex-wrap">
|
||||
{% if read_only_mode %}
|
||||
<div class="small text-warning-emphasis align-self-center">Akcje masowe są zablokowane w trybie read only.</div>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary" name="action" value="mark_accounted" {% if read_only_mode %}disabled{% endif %}>
|
||||
<i class="fa-solid fa-book me-1"></i>Masowo zaksięguj
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-warning" name="action" value="queue_accounting" {% if read_only_mode %}disabled{% endif %}>
|
||||
<i class="fa-solid fa-inbox me-1"></i>Do księgowości
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"></th>
|
||||
<th>Numer</th>
|
||||
<th>KSeF</th>
|
||||
<th>Kontrahent</th>
|
||||
<th>NIP</th>
|
||||
<th>Data</th>
|
||||
<th>Netto</th>
|
||||
<th>VAT</th>
|
||||
<th>Brutto</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invoice in pagination.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="invoice_ids" value="{{ invoice.id }}" {% if read_only_mode %}disabled{% endif %}>
|
||||
</td>
|
||||
<td class="invoice-number-col">{{ invoice.invoice_number }}</td>
|
||||
<td class="invoice-ksef-col ksef-break small">{{ invoice.ksef_number }}</td>
|
||||
<td>{{ invoice.contractor_name }}</td>
|
||||
<td>{{ invoice.contractor_nip }}</td>
|
||||
<td>{{ invoice.issue_date }}</td>
|
||||
<td>{{ invoice.net_amount|pln }}</td>
|
||||
<td>{{ invoice.vat_amount|pln }}</td>
|
||||
<td>{{ invoice.gross_amount|pln }}</td>
|
||||
<td>{{ invoice.invoice_type_label }}</td>
|
||||
<td>
|
||||
<span class="badge text-bg-secondary">{{ invoice.status_label }}</span>
|
||||
</td>
|
||||
<td class="text-end invoice-actions-cell">
|
||||
<div class="invoice-actions-stack">
|
||||
<a class="btn btn-sm btn-outline-primary invoice-action-btn" href="{{ url_for('invoices.detail', invoice_id=invoice.id) }}">
|
||||
<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="#payModalReceived{{ 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 = 'payModalReceived' ~ invoice.id %}
|
||||
{% include 'partials/payment_modal.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="12" class="text-center text-muted py-4">Brak faktur dla wybranych filtrów.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if pagination.pages and pagination.pages > 1 %}
|
||||
<nav aria-label="Paginacja faktur">
|
||||
<ul class="pagination flex-wrap">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('invoices.index', page=pagination.prev_num, **args) }}">Poprz.</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for pg in range(1, (pagination.pages or 1) + 1) %}
|
||||
<li class="page-item {{ 'active' if pg == pagination.page else '' }}">
|
||||
<a class="page-link" href="{{ url_for('invoices.index', page=pg, **args) }}">{{ pg }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('invoices.index', page=pagination.next_num, **args) }}">Dalej</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
120
app/templates/invoices/issued_form.html
Normal file
120
app/templates/invoices/issued_form.html
Normal file
@@ -0,0 +1,120 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-square-plus me-2 text-primary"></i>{{ 'Edytuj fakturę' if editing_invoice else 'Wystaw fakturę' }}{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Sprzedaż' %}{% set heading=('Edytuj fakturę' if editing_invoice else 'Wystaw fakturę') %}{% set description='Widok uproszczony, dopasowany do stylu panelu administracyjnego.' %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if read_only_mode %}<div class="alert alert-warning">Tryb tylko do odczytu - wystawianie faktur jest zablokowane.</div>{% endif %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.customer_id.label(class='form-label mb-0') }}
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#customerQuickAddModal"><i class="fa-solid fa-plus me-1"></i>Dodaj</button>
|
||||
</div>
|
||||
{{ form.customer_id(class='form-select', disabled=read_only_mode) }}
|
||||
<div class="form-text"><a href="{{ url_for('invoices.customers') }}">Pełna kartoteka klientów</a></div>
|
||||
</div>
|
||||
<div class="col-md-6">{{ form.numbering_template.label(class='form-label') }}{{ form.numbering_template(class='form-select', disabled=read_only_mode) }}</div>
|
||||
<div class="col-md-6">{{ form.invoice_number.label(class='form-label') }}{{ form.invoice_number(class='form-control', disabled=read_only_mode, placeholder=preview_number) }}<div class="form-text">Puste pole = numer zostanie nadany automatycznie.</div></div>
|
||||
<div class="col-md-6 d-flex align-items-end"><div class="alert alert-secondary w-100 mb-0 small">Proponowany numer: <strong>{{ preview_number or '—' }}</strong></div></div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.product_id.label(class='form-label mb-0') }}
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#productQuickAddModal"><i class="fa-solid fa-plus me-1"></i>Dodaj</button>
|
||||
</div>
|
||||
<select class="form-select" id="productField" name="{{ form.product_id.name }}" {% if read_only_mode %}disabled{% endif %}>
|
||||
{% for product in products %}
|
||||
<option value="{{ product.id }}" data-net-price="{{ product.net_price }}" data-vat-rate="{{ product.vat_rate }}" data-split-payment="{{ 'true' if product.split_payment_default else 'false' }}" {{ 'selected' if form.product_id.data == product.id else '' }}>{{ product.name }} - {{ product.net_price }} PLN</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text"><a href="{{ url_for('invoices.products') }}">Pełna kartoteka towarów i usług</a></div>
|
||||
</div>
|
||||
<div class="col-md-3">{{ form.quantity.label(class='form-label') }}{{ form.quantity(class='form-control', disabled=read_only_mode, id='quantityField') }}</div>
|
||||
<div class="col-md-3">{{ form.unit_net.label(class='form-label') }}{{ form.unit_net(class='form-control', disabled=read_only_mode, id='unitNetField') }}</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check form-switch">
|
||||
{{ form.split_payment(class='form-check-input', disabled=read_only_mode, id='splitPaymentField') }}
|
||||
{{ form.split_payment.label(class='form-check-label') }}
|
||||
</div>
|
||||
<div class="form-text" id="splitPaymentHint">Domyślnie włączane dla usług oznaczonych w kartotece. Dla faktur powyżej 15 000 PLN brutto jest wymuszane.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2 flex-wrap">{% if editing_invoice %}<button class="btn btn-outline-primary" name="save_submit" type="submit" {% if read_only_mode %}disabled{% endif %}>Zapisz zmiany</button><button class="btn btn-primary" name="submit" type="submit" {% if read_only_mode %}disabled{% endif %}>Zapisz i wyślij do KSeF</button>{% else %}{{ form.save_submit(class='btn btn-outline-primary', disabled=read_only_mode) }}{{ form.submit(class='btn btn-primary', disabled=read_only_mode) }}{% endif %}</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><i class="fa-solid fa-circle-info me-2"></i>Podpowiedzi</div>
|
||||
<div class="card-body small text-secondary d-flex flex-column gap-3">
|
||||
<div>Dodawanie klientów działa teraz przez wspólne formularze dostępne także w formularzu NFZ. (jeśli moduł włączony)</div>
|
||||
<div>Po zapisaniu nowy klient lub towar zostanie automatycznie podstawiony do formularza.</div>
|
||||
<div>Pełne kartoteki nadal są dostępne z linków pod polami wyboru.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set quick_return_endpoint = 'invoices.issued_edit' if editing_invoice else 'invoices.issued_new' %}
|
||||
{% set quick_invoice_id = editing_invoice.id if editing_invoice else None %}
|
||||
{% include 'partials/invoice_quick_add_modals.html' %}
|
||||
<script>
|
||||
(() => {
|
||||
const productField = document.getElementById('productField');
|
||||
const quantityField = document.getElementById('quantityField');
|
||||
const unitNetField = document.getElementById('unitNetField');
|
||||
const splitPaymentField = document.getElementById('splitPaymentField');
|
||||
const splitPaymentHint = document.getElementById('splitPaymentHint');
|
||||
if (!productField || !quantityField || !unitNetField || !splitPaymentField) return;
|
||||
|
||||
let userModifiedSplit = false;
|
||||
|
||||
const parseNumber = (value) => {
|
||||
const normalized = String(value || '').replace(/\s+/g, '').replace(',', '.');
|
||||
const parsed = Number.parseFloat(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
};
|
||||
|
||||
const syncSplitPayment = (resetToProductDefault = false) => {
|
||||
const selected = productField.options[productField.selectedIndex];
|
||||
const vatRate = parseNumber(selected?.dataset?.vatRate || 0);
|
||||
const defaultSplit = (selected?.dataset?.splitPayment || 'false') === 'true';
|
||||
const gross = parseNumber(quantityField.value) * parseNumber(unitNetField.value) * (1 + vatRate / 100);
|
||||
const forced = gross > 15000;
|
||||
if (forced) {
|
||||
splitPaymentField.checked = true;
|
||||
splitPaymentField.disabled = true;
|
||||
if (splitPaymentHint) splitPaymentHint.textContent = 'Split payment jest wymagany, ponieważ wartość brutto faktury przekracza 15 000 PLN.';
|
||||
return;
|
||||
}
|
||||
splitPaymentField.disabled = {{ 'true' if read_only_mode else 'false' }};
|
||||
if (resetToProductDefault || !userModifiedSplit) {
|
||||
splitPaymentField.checked = defaultSplit;
|
||||
}
|
||||
if (splitPaymentHint) {
|
||||
splitPaymentHint.textContent = defaultSplit
|
||||
? 'Dla wybranej usługi split payment jest domyślnie włączony. Możesz go zmienić dla tej faktury.'
|
||||
: 'Split payment możesz włączyć ręcznie. Dla faktur powyżej 15 000 PLN brutto jest wymuszany.';
|
||||
}
|
||||
};
|
||||
|
||||
productField.addEventListener('change', () => {
|
||||
const selected = productField.options[productField.selectedIndex];
|
||||
if (selected?.dataset?.netPrice && !unitNetField.value) unitNetField.value = selected.dataset.netPrice;
|
||||
userModifiedSplit = false;
|
||||
syncSplitPayment(true);
|
||||
});
|
||||
quantityField.addEventListener('input', () => syncSplitPayment(false));
|
||||
unitNetField.addEventListener('input', () => syncSplitPayment(false));
|
||||
splitPaymentField.addEventListener('change', () => {
|
||||
userModifiedSplit = true;
|
||||
});
|
||||
syncSplitPayment(false);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
12
app/templates/invoices/issued_list.html
Normal file
12
app/templates/invoices/issued_list.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-paper-plane me-2 text-primary"></i>Faktury wystawione{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Sprzedaż' %}{% set heading='Faktury wystawione' %}{% set description='Dokumenty sprzedażowe przygotowane w systemie, z oznaczeniem statusu wysyłki do KSeF.' %}
|
||||
{% set actions %}<a class="btn btn-primary {% if read_only_mode %}disabled{% endif %}" href="{{ url_for('invoices.issued_new') }}"><i class="fa-solid fa-plus me-2"></i>Nowa faktura</a>{% endset %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="card mb-3"><div class="card-body"><form method="get" class="row g-2 align-items-end"><div class="col-md-10"><label class="form-label">Szukaj</label><input class="form-control" type="text" name="q" value="{{ search or '' }}" placeholder="Numer, KSeF, kontrahent, NIP"></div><div class="col-md-2 d-grid"><button class="btn btn-outline-primary"><i class="fa-solid fa-magnifying-glass me-2"></i>Szukaj</button></div></form></div></div>
|
||||
<div class="card">
|
||||
<div class="table-responsive"><table class="table table-hover align-middle mb-0"><thead><tr><th>Numer</th><th>Kontrahent</th><th>Brutto</th><th>KSeF</th><th>Status</th><th></th></tr></thead><tbody>{% for invoice in invoices %}<tr><td><div class="fw-semibold">{{ invoice.invoice_number }}</div><div class="small text-secondary">{{ invoice.issue_date }}</div>{% if invoice.source == 'nfz' %}<div><span class="badge text-bg-success-subtle border nfz-badge mt-1">NFZ</span></div>{% endif %}</td><td>{{ invoice.contractor_name }}</td><td>{{ invoice.gross_amount|pln }}</td><td>{% if invoice.issued_to_ksef_at %}<span class="badge text-bg-success">Przesłana do KSeF</span>{% else %}<span class="badge text-bg-secondary">Nieprzesłana do KSeF</span>{% endif %}<div class="small text-secondary mt-1">{{ invoice.ksef_number }}</div></td><td><span class="badge text-bg-{{ 'success' if invoice.issued_to_ksef_at else 'secondary' }}">{{ invoice.issued_status_label }}</span></td><td class="text-end"><div class="d-inline-flex gap-2 flex-wrap justify-content-end"><a class="btn btn-sm btn-outline-primary invoice-action-btn" href="{{ url_for('invoices.detail', invoice_id=invoice.id) }}"><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="#payModalIssued{{ 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 = 'payModalIssued' ~ invoice.id %}{% include 'partials/payment_modal.html' %}</td></tr>{% else %}<tr><td colspan="6" class="text-center text-secondary py-4">Brak faktur.</td></tr>{% endfor %}</tbody></table></div>
|
||||
<div class="card-body border-top py-2"><nav><ul class="pagination justify-content-end mb-0">{% if pagination.has_prev %}<li class="page-item"><a class="page-link" href="{{ url_for('invoices.issued_list', page=pagination.prev_num, q=search) }}">Poprz.</a></li>{% endif %}{% for pg in range(1, (pagination.pages or 1) + 1) %}<li class="page-item {{ 'active' if pg == pagination.page else '' }}"><a class="page-link" href="{{ url_for('invoices.issued_list', page=pg, q=search) }}">{{ pg }}</a></li>{% endfor %}{% if pagination.has_next %}<li class="page-item"><a class="page-link" href="{{ url_for('invoices.issued_list', page=pagination.next_num, q=search) }}">Dalej</a></li>{% endif %}</ul></nav></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1
app/templates/invoices/month_pdf.html
Normal file
1
app/templates/invoices/month_pdf.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:Helvetica,Arial,sans-serif;font-size:12px}table{width:100%;border-collapse:collapse}td,th{border:1px solid #ccc;padding:6px}</style></head><body><h1>{{ title }}</h1><table><thead><tr><th>Numer</th><th>Kontrahent</th><th>Netto</th><th>VAT</th><th>Brutto</th></tr></thead><tbody>{% for invoice in invoices %}<tr><td>{{ invoice.invoice_number }}</td><td>{{ invoice.contractor_name }}</td><td>{{ invoice.net_amount|pln }}</td><td>{{ invoice.vat_amount|pln }}</td><td>{{ invoice.gross_amount|pln }}</td></tr>{% endfor %}</tbody></table></body></html>
|
||||
9
app/templates/invoices/monthly.html
Normal file
9
app/templates/invoices/monthly.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-calendar-days me-2 text-primary"></i>Zestawienia {{ period_title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Analiza' %}{% set heading='Zestawienia ' ~ period_title %}{% set description='Widok roczny, kwartalny i miesięczny z porównaniem do innych lat.' %}
|
||||
{% set actions %}<div class="btn-group" role="group"><a class="btn {{ 'btn-primary' if period == 'year' else 'btn-outline-primary' }}" href="{{ url_for('invoices.monthly', period='year', q=search) }}">Roczne</a><a class="btn {{ 'btn-primary' if period == 'quarter' else 'btn-outline-primary' }}" href="{{ url_for('invoices.monthly', period='quarter', q=search) }}">Kwartalne</a><a class="btn {{ 'btn-primary' if period == 'month' else 'btn-outline-primary' }}" href="{{ url_for('invoices.monthly', period='month', q=search) }}">Miesięczne</a></div>{% endset %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="row g-3 mb-3"><div class="col-lg-8"><div class="card h-100"><div class="card-body"><form method="get" class="row g-2 align-items-end"><input type="hidden" name="period" value="{{ period }}"><div class="col-md-9"><label class="form-label">Szukaj w zestawieniach</label><input class="form-control" type="text" name="q" value="{{ search }}" placeholder="Numer, KSeF, kontrahent, NIP"></div><div class="col-md-3 d-grid"><button class="btn btn-primary" type="submit"><i class="fa-solid fa-magnifying-glass me-2"></i>Szukaj</button></div></form></div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-header">Statystyka innych lat</div><div class="card-body">{% for item in comparisons %}<div class="d-flex justify-content-between border-bottom py-2 small"><div><div class="fw-semibold">{{ item.year }}</div><div class="text-secondary">{{ item.count }} faktur · netto {{ item.net|pln }}</div></div><div class="text-end"><div>{{ item.gross|pln }}</div>{% if item.delta is not none %}<div class="text-{{ 'success' if item.delta >= 0 else 'danger' }}">{{ '+' if item.delta >= 0 else '' }}{{ item.delta|pln }}</div>{% else %}<div class="text-secondary">bazowy</div>{% endif %}</div></div>{% else %}<div class="text-secondary">Brak danych porównawczych.</div>{% endfor %}</div></div></div></div>
|
||||
{% for group in groups %}<div class="card mb-3"><div class="card-header d-flex justify-content-between flex-wrap gap-2"><span class="fw-semibold">{{ group.label }}</span><span>{{ group.gross|pln }}</span></div><div class="card-body"><div class="row small g-2 mb-3"><div class="col-md-3"><strong>Liczba faktur:</strong> {{ group.count }}</div><div class="col-md-3"><strong>Netto:</strong> {{ group.net|pln }}</div><div class="col-md-3"><strong>VAT:</strong> {{ group.vat|pln }}</div><div class="col-md-3"><strong>Brutto:</strong> {{ group.gross|pln }}</div></div>{% if period == 'month' %}<div class="mb-2"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('invoices.month_pdf', period=group.key) }}">PDF miesiąca</a></div>{% endif %}<div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Numer</th><th>Kontrahent</th><th>Data</th><th>Brutto</th><th></th></tr></thead><tbody>{% for invoice in group.entries %}<tr><td>{{ invoice.invoice_number }}</td><td>{{ invoice.contractor_name }}</td><td>{{ invoice.issue_date }}</td><td>{{ invoice.gross_amount|pln }}</td><td class="text-end"><a class="btn btn-sm btn-outline-primary invoice-action-btn" href="{{ url_for('invoices.detail', invoice_id=invoice.id) }}"><i class="fa-solid fa-folder-open me-1"></i>Otwórz</a></td></tr>{% else %}<tr><td colspan="5" class="text-center text-secondary py-3">Brak faktur w tym okresie.</td></tr>{% endfor %}</tbody></table></div></div></div>{% else %}<div class="alert alert-info">Brak danych.</div>{% endfor %}
|
||||
{% endblock %}
|
||||
1
app/templates/invoices/pdf.html
Normal file
1
app/templates/invoices/pdf.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:Helvetica,Arial,sans-serif;font-size:12px}table{width:100%;border-collapse:collapse}td,th{border:1px solid #ccc;padding:6px}</style></head><body><h1>Faktura {{ invoice.invoice_number }}</h1><table><tr><th>KSeF</th><td>{{ invoice.ksef_number }}</td></tr><tr><th>Kontrahent</th><td>{{ invoice.contractor_name }}</td></tr><tr><th>NIP</th><td>{{ invoice.contractor_nip }}</td></tr><tr><th>Data</th><td>{{ invoice.issue_date }}</td></tr><tr><th>Netto</th><td>{{ invoice.net_amount|pln }}</td></tr><tr><th>VAT</th><td>{{ invoice.vat_amount|pln }}</td></tr><tr><th>Brutto</th><td>{{ invoice.gross_amount|pln }}</td></tr><tr><th>Rachunek bankowy</th><td>{{ invoice.seller_bank_account or (invoice.company.bank_account if invoice.company else '') or '—' }}</td></tr></table></body></html>
|
||||
65
app/templates/invoices/products.html
Normal file
65
app/templates/invoices/products.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-boxes-stacked me-2 text-primary"></i>Towary i usługi{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Kartoteka' %}{% set heading='Towary i usługi' %}{% set description='Jednolity widok kartoteki z sortowaniem i paginacją.' %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><i class="fa-solid fa-box-open me-2"></i>{{ 'Edytuj pozycję' if editing else 'Nowa pozycja' }}</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-2"><label class="form-label">Nazwa</label><input class="form-control" name="name" value="{{ editing.name if editing else '' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-2"><label class="form-label">SKU</label><input class="form-control" name="sku" value="{{ editing.sku if editing else '' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-2"><label class="form-label">Jednostka</label><input class="form-control" name="unit" value="{{ editing.unit if editing else 'szt.' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-2"><label class="form-label">Cena netto</label><input class="form-control" name="net_price" value="{{ editing.net_price if editing else '' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="mb-2"><label class="form-label">VAT %</label><input class="form-control" name="vat_rate" value="{{ editing.vat_rate if editing else '23' }}" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="form-check mb-3"><input class="form-check-input" type="checkbox" id="split_payment_default" name="split_payment_default" value="1" {{ 'checked' if editing and editing.split_payment_default else '' }} {% if read_only_mode %}disabled{% endif %}><label class="form-check-label" for="split_payment_default">Domyślnie włączaj split payment dla tej usługi</label></div>
|
||||
<div class="d-grid gap-2"><button class="btn btn-primary" {% if read_only_mode %}disabled{% endif %}>{{ 'Zapisz pozycję' if editing else 'Dodaj pozycję' }}</button>{% if editing %}<a class="btn btn-outline-secondary" href="{{ url_for('invoices.products') }}">Anuluj edycję</a>{% endif %}</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div><i class="fa-solid fa-warehouse me-2"></i>Baza towarów i usług</div>
|
||||
<form method="get" class="row g-2 align-items-end w-100 ms-0">
|
||||
<div class="col-md-7"><label class="form-label form-label-sm mb-1">Szukaj</label><input class="form-control form-control-sm" type="search" name="q" value="{{ search or '' }}" placeholder="Szukaj po nazwie, SKU lub jednostce..."></div>
|
||||
<div class="col-md-4"><label class="form-label form-label-sm mb-1">Sortowanie</label><select class="form-select form-select-sm" name="sort">
|
||||
<option value="name_asc" {{ 'selected' if sort == 'name_asc' else '' }}>A-Z</option>
|
||||
<option value="name_desc" {{ 'selected' if sort == 'name_desc' else '' }}>Z-A</option>
|
||||
<option value="price_asc" {{ 'selected' if sort == 'price_asc' else '' }}>Cena rosnąco</option>
|
||||
<option value="price_desc" {{ 'selected' if sort == 'price_desc' else '' }}>Cena malejąco</option>
|
||||
<option value="sku_asc" {{ 'selected' if sort == 'sku_asc' else '' }}>SKU A-Z</option>
|
||||
<option value="sku_desc" {{ 'selected' if sort == 'sku_desc' else '' }}>SKU Z-A</option>
|
||||
</select></div>
|
||||
<div class="col-md-1 d-grid"><button class="btn btn-sm btn-outline-secondary"><i class="fa-solid fa-magnifying-glass"></i></button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead><tr><th>Nazwa</th><th>SKU</th><th>Cena netto</th><th>VAT</th><th>Split payment</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr><td><i class="fa-solid fa-cube text-primary me-2"></i>{{ item.name }}</td><td>{{ item.sku }}</td><td>{{ item.net_price|pln }}</td><td>{{ item.vat_rate }}%</td><td>{% if item.split_payment_default %}<span class="badge text-bg-warning">Domyślny</span>{% else %}<span class="text-secondary">Wyłączony</span>{% endif %}</td><td class="text-end"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('invoices.products', product_id=item.id) }}">Edytuj</a></td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="text-secondary text-center py-4">Brak pozycji.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body border-top py-2">
|
||||
<nav>
|
||||
<ul class="pagination justify-content-end mb-0">
|
||||
{% if pagination.has_prev %}<li class="page-item"><a class="page-link" href="{{ url_for('invoices.products', page=pagination.prev_num, q=search, sort=sort) }}">Poprz.</a></li>{% endif %}
|
||||
<li class="page-item disabled"><span class="page-link">{{ pagination.page }} / {{ pagination.pages or 1 }}</span></li>
|
||||
{% if pagination.has_next %}<li class="page-item"><a class="page-link" href="{{ url_for('invoices.products', page=pagination.next_num, q=search, sort=sort) }}">Dalej</a></li>{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
78
app/templates/nfz/index.html
Normal file
78
app/templates/nfz/index.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-hospital me-2 text-primary"></i>Faktury NFZ{% endblock %}
|
||||
{% block content %}
|
||||
{% set editing_invoice = editing_invoice|default(None) %}
|
||||
{% set eyebrow='Moduł dodatkowy' %}{% set heading='Wystawianie faktur NFZ' if not editing_invoice else 'Edycja faktury NFZ' %}{% set description='Formularz zawiera pola wymagane przez NFZ dla faktur ustrukturyzowanych FA(2)/FA(3) w KSeF.' %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="fa-solid fa-file-circle-plus me-2"></i>{{ 'Nowa faktura NFZ' if not editing_invoice else 'Edycja faktury NFZ ' ~ editing_invoice.invoice_number }}</div>
|
||||
<div class="card-body">
|
||||
{% if read_only_mode %}<div class="alert alert-warning">Tryb tylko do odczytu jest aktywny. Zapisy są zablokowane.</div>{% endif %}
|
||||
<form method="post" class="row g-3">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.customer_id.label(class='form-label mb-0') }}
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#customerQuickAddModal"><i class="fa-solid fa-plus me-1"></i>Dodaj</button>
|
||||
</div>
|
||||
{{ form.customer_id(class='form-select', disabled=read_only_mode) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.product_id.label(class='form-label mb-0') }}
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#productQuickAddModal"><i class="fa-solid fa-plus me-1"></i>Dodaj</button>
|
||||
</div>
|
||||
{{ form.product_id(class='form-select', disabled=read_only_mode) }}
|
||||
</div>
|
||||
<div class="col-md-4">{{ form.invoice_number.label(class='form-label') }}{{ form.invoice_number(class='form-control', disabled=read_only_mode) }}</div>
|
||||
<div class="col-md-4">{{ form.nfz_branch_id.label(class='form-label') }}{{ form.nfz_branch_id(class='form-select', disabled=read_only_mode) }}</div>
|
||||
<div class="col-md-4">{{ form.provider_identifier.label(class='form-label') }}{{ form.provider_identifier(class='form-control', disabled=read_only_mode, placeholder='id-swd') }}</div>
|
||||
<div class="col-md-3">{{ form.settlement_from.label(class='form-label') }}{{ form.settlement_from(class='form-control', disabled=read_only_mode) }}</div>
|
||||
<div class="col-md-3">{{ form.settlement_to.label(class='form-label') }}{{ form.settlement_to(class='form-control', disabled=read_only_mode) }}</div>
|
||||
<div class="col-md-6">{{ form.template_identifier.label(class='form-label') }}{{ form.template_identifier(class='form-control', disabled=read_only_mode, placeholder='id-szablonu z R_UMX') }}</div>
|
||||
<div class="col-md-8">{{ form.service_code.label(class='form-label') }}{{ form.service_code(class='form-control', disabled=read_only_mode, placeholder='02.1500.001.02/1 lub 01.0010.094.01/1/5.01.00.0000127') }}</div>
|
||||
<div class="col-md-4">{{ form.contract_number.label(class='form-label') }}{{ form.contract_number(class='form-control', disabled=read_only_mode, placeholder='120/999999/01/2025[23]') }}</div>
|
||||
<div class="col-md-3">{{ form.quantity.label(class='form-label') }}{{ form.quantity(class='form-control', disabled=read_only_mode) }}</div>
|
||||
<div class="col-md-3">{{ form.unit_net.label(class='form-label') }}{{ form.unit_net(class='form-control', disabled=read_only_mode) }}</div>
|
||||
<div class="col-12 d-flex gap-2 flex-wrap">{{ form.save_submit(class='btn btn-outline-primary', disabled=read_only_mode) }}{{ form.submit(class='btn btn-primary', disabled=read_only_mode) }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><i class="fa-solid fa-list-check me-2"></i>Pola wymagane</div>
|
||||
<div class="card-body small">
|
||||
{% for key, desc in spec_fields %}
|
||||
<div class="border-bottom py-2"><div class="fw-semibold">{{ key }}</div><div class="text-secondary">{{ desc }}</div></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="fa-solid fa-clock-rotate-left me-2"></i>Ostatnie faktury NFZ</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for invoice in drafts %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start gap-2">
|
||||
<a class="text-decoration-none" href="{{ url_for('invoices.detail', invoice_id=invoice.id) }}">
|
||||
<div class="fw-semibold">{{ invoice.invoice_number }}</div>
|
||||
<div class="small text-secondary">{{ invoice.contractor_name }}</div>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('invoices.duplicate', invoice_id=invoice.id) }}">Duplikuj</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3 text-secondary small">Brak faktur NFZ.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set quick_return_endpoint = 'nfz.edit' if editing_invoice else 'nfz.index' %}
|
||||
{% set quick_invoice_id = editing_invoice.id if editing_invoice else None %}
|
||||
{% include 'partials/invoice_quick_add_modals.html' %}
|
||||
{% endblock %}
|
||||
31
app/templates/notifications/index.html
Normal file
31
app/templates/notifications/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}<i class="fa-solid fa-bell me-2 text-primary"></i>Powiadomienia{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Log systemowy' %}{% set heading='Dziennik powiadomień' %}{% set description='Ostatnie wpisy z kanałów Pushover, maili i pozostałych integracji.' %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
|
||||
{% set tone_map = {'wysłano':'success','wysłane':'success','sukces':'success','błąd':'danger','blad':'danger','pominięto':'secondary','oczekuje':'warning'} %}
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr><th>Data</th><th>Kanał</th><th>Status</th><th>Treść</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
{% set status = status_pl(log.status) %}
|
||||
{% set badge = tone_map.get(status|lower, 'light') %}
|
||||
<tr>
|
||||
<td class="text-nowrap small">{{ log.created_at.strftime('%Y-%m-%d %H:%M') if log.created_at else '—' }}</td>
|
||||
<td><span class="badge rounded-pill text-bg-light border">{{ channel_pl(log.channel) }}</span></td>
|
||||
<td><span class="badge rounded-pill text-bg-{{ badge }}">{{ status }}</span></td>
|
||||
<td class="small">{{ log.message }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="text-center text-secondary py-5">Brak wpisów.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
app/templates/partials/invoice_quick_add_modals.html
Normal file
60
app/templates/partials/invoice_quick_add_modals.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% set quick_return_endpoint = quick_return_endpoint|default('invoices.issued_new') %}
|
||||
{% set quick_invoice_id = quick_invoice_id|default(None) %}
|
||||
|
||||
<div class="modal fade" id="customerQuickAddModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ url_for('invoices.quick_create_customer') }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-user-plus me-2"></i>Dodaj klienta</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="return_endpoint" value="{{ quick_return_endpoint }}">
|
||||
{% if quick_invoice_id %}<input type="hidden" name="invoice_id" value="{{ quick_invoice_id }}">{% endif %}
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label">Nazwa klienta</label><input class="form-control" name="name" required {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-md-6"><label class="form-label">NIP</label><input class="form-control" name="tax_id" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-md-6"><label class="form-label">REGON</label><input class="form-control" name="regon" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-12"><label class="form-label">Adres</label><input class="form-control" name="address" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-12"><label class="form-label">E-mail</label><input class="form-control" name="email" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button class="btn btn-primary" {% if read_only_mode %}disabled{% endif %}>Dodaj klienta</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="productQuickAddModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ url_for('invoices.quick_create_product') }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-box-open me-2"></i>Dodaj towar lub usługę</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="return_endpoint" value="{{ quick_return_endpoint }}">
|
||||
{% if quick_invoice_id %}<input type="hidden" name="invoice_id" value="{{ quick_invoice_id }}">{% endif %}
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label">Nazwa pozycji</label><input class="form-control" name="name" required {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-md-6"><label class="form-label">SKU</label><input class="form-control" name="sku" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-md-3"><label class="form-label">JM</label><input class="form-control" name="unit" value="szt." {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-md-3"><label class="form-label">VAT</label><input class="form-control" name="vat_rate" value="23" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
<div class="col-12"><label class="form-label">Cena netto</label><input class="form-control" name="net_price" {% if read_only_mode %}disabled{% endif %}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button class="btn btn-primary" {% if read_only_mode %}disabled{% endif %}>Dodaj pozycję</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
8
app/templates/partials/page_header.html
Normal file
8
app/templates/partials/page_header.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="page-section-header d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<div class="small text-secondary text-uppercase fw-semibold section-eyebrow">{{ eyebrow or 'Panel operacyjny' }}</div>
|
||||
<h4 class="mb-1">{{ heading }}</h4>
|
||||
{% if description %}<div class="text-secondary text-wrap-balanced">{{ description }}</div>{% endif %}
|
||||
</div>
|
||||
{% if actions %}<div class="d-flex gap-2 align-items-center flex-wrap">{{ actions|safe }}</div>{% endif %}
|
||||
</div>
|
||||
26
app/templates/partials/payment_modal.html
Normal file
26
app/templates/partials/payment_modal.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="modal fade" id="{{ modal_id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-wallet me-2"></i>Opłać</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="small text-secondary mb-3">Dane do płatności dla dokumentu <strong>{{ invoice.invoice_number }}</strong>.</div>
|
||||
<div class="vstack gap-2 small">
|
||||
<div><span class="text-secondary">Metoda płatności:</span> <strong>{{ payment_details.payment_form_label or 'brak danych' }}</strong>{% if payment_details.payment_form_code %} <span class="badge text-bg-light border">kod {{ payment_details.payment_form_code }}</span>{% endif %}</div>
|
||||
<div><span class="text-secondary">Numer faktury:</span> <strong>{{ invoice.invoice_number }}</strong></div>
|
||||
<div><span class="text-secondary">Kontrahent:</span> <strong>{{ invoice.contractor_name or '—' }}</strong></div>
|
||||
{% if invoice.contractor_address %}<div><span class="text-secondary">Adres:</span> {{ invoice.contractor_address }}</div>{% endif %}
|
||||
<div><span class="text-secondary">Kwota brutto:</span> <strong>{{ invoice.gross_amount|pln }}</strong></div>
|
||||
<div><span class="text-secondary">Numer konta:</span> <strong>{{ payment_details.bank_account or 'brak danych' }}</strong></div>
|
||||
{% if payment_details.bank_name %}<div><span class="text-secondary">Bank:</span> {{ payment_details.bank_name }}</div>{% endif %}
|
||||
{% if payment_details.payment_due_date %}<div><span class="text-secondary">Termin płatności:</span> {{ payment_details.payment_due_date }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
69
app/templates/settings/index.html
Normal file
69
app/templates/settings/index.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends 'base.html' %}
|
||||
{% macro source_switch(name, current, first_value, first_label, second_value, second_label) -%}
|
||||
<div class="source-switch" data-source-switch>
|
||||
<input class="btn-check" type="radio" name="{{ name }}" id="{{ name }}-{{ first_value }}" value="{{ first_value }}" autocomplete="off" {{ 'checked' if current == first_value else '' }}>
|
||||
<label class="btn btn-source" for="{{ name }}-{{ first_value }}">{{ first_label }}</label>
|
||||
<input class="btn-check" type="radio" name="{{ name }}" id="{{ name }}-{{ second_value }}" value="{{ second_value }}" autocomplete="off" {{ 'checked' if current == second_value else '' }}>
|
||||
<label class="btn btn-source" for="{{ name }}-{{ second_value }}">{{ second_label }}</label>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
{% block title %}<i class="fa-solid fa-gear me-2 text-primary"></i>Ustawienia{% endblock %}
|
||||
{% block content %}
|
||||
{% set eyebrow='Konfiguracja' %}{% set heading='Ustawienia użytkownika i firmy' %}{% set description='Wybierz, które moduły mają korzystać z profilu globalnego, a które z indywidualnych ustawień użytkownika.' %}
|
||||
{% include 'partials/page_header.html' with context %}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-4"><div class="card h-100"><div class="card-header">Aktywna firma</div><div class="card-body compact-card-body"><div class="fw-semibold">{{ company.name if company else 'Brak przypisanej firmy' }}</div><div class="small text-secondary mt-2">KSeF współdzielony dotyczy aktywnej firmy. SMTP, Pushover i NFZ mogą działać globalnie lub indywidualnie per użytkownik.</div><div class="mt-3 d-flex gap-2 flex-wrap"><span class="badge text-bg-light border">KSeF {{ ksef_environment|upper }}</span><span class="badge text-bg-light border">SMTP {{ 'global' if mail_mode == 'global' else 'indywidualny' }}</span><span class="badge text-bg-light border">Pushover {{ 'global' if notify_mode == 'global' else 'indywidualny' }}</span><span class="badge text-bg-light border">NFZ {{ 'globalny' if nfz_mode == 'global' else 'indywidualny' }}</span></div></div></div></div>
|
||||
<div class="col-lg-8"><div class="card h-100"><div class="card-header">Wygląd interfejsu</div><div class="card-body compact-card-body"><form method="post" class="row g-3 align-items-end">{{ appearance_form.hidden_tag() }}<div class="col-md-7">{{ appearance_form.theme_preference.label(class='form-label') }}{{ appearance_form.theme_preference(class='form-select') }}</div><div class="col-md-5 d-grid">{{ appearance_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<div class="nav flex-column nav-pills settings-tab gap-2">
|
||||
{% if can_manage_company_settings %}<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-company" type="button">Firma</button>{% endif %}
|
||||
<button class="nav-link {{ '' if can_manage_company_settings else 'active' }}" data-bs-toggle="pill" data-bs-target="#tab-ksef" type="button">KSeF</button>
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-mail" type="button">SMTP</button>
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-notify" type="button">Pushover</button>
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-nfz" type="button">NFZ</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="tab-content">
|
||||
{% if can_manage_company_settings %}
|
||||
<div class="tab-pane fade show active" id="tab-company"><div class="card"><div class="card-header">Ustawienia firmy</div><div class="card-body"><form method="post" class="row g-3 align-items-end">{{ company_form.hidden_tag() }}<div class="col-md-6">{{ company_form.name.label(class='form-label') }}{{ company_form.name(class='form-control') }}</div><div class="col-md-3">{{ company_form.tax_id.label(class='form-label') }}{{ company_form.tax_id(class='form-control') }}</div><div class="col-md-3">{{ company_form.sync_interval_minutes.label(class='form-label') }}{{ company_form.sync_interval_minutes(class='form-control') }}</div><div class="col-md-6">{{ company_form.bank_account.label(class='form-label') }}{{ company_form.bank_account(class='form-control', placeholder='np. 11 1111 1111 1111 1111 1111 1111') }}</div><div class="col-md-6 form-check ms-2">{{ company_form.sync_enabled(class='form-check-input') }}{{ company_form.sync_enabled.label(class='form-check-label') }}</div><div class="col-md-5"><div class="form-check form-switch fs-5">{{ company_form.read_only_mode(class='form-check-input') }}{{ company_form.read_only_mode.label(class='form-check-label') }}</div><div class="form-text">Rzeczywisty tryb może być dodatkowo ograniczony globalnie lub uprawnieniami użytkownika.</div></div><div class="col-12"><div class="alert alert-{{ 'warning' if effective_read_only else 'success' }} mb-0">Tryb efektywny: <strong>{{ 'R/O' if effective_read_only else 'R/W' }}</strong>{% if read_only_reasons %}<div class="small mt-2">Źródło: {{ read_only_reasons|join(', ') }}</div>{% endif %}</div></div><div class="col-12 d-grid d-md-flex">{{ company_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade {% if can_manage_company_settings %}{% else %}show active{% endif %}" id="tab-ksef"><div class="card"><div class="card-header">KSeF</div><div class="card-body">
|
||||
<div class="settings-module-intro"><div><h6 class="mb-1">Model biznesowy KSeF</h6><div class="text-secondary small">Domyślnie użytkownik pracuje na własnym profilu. W razie potrzeby może przełączyć się na współdzielony profil aktywnej firmy przygotowany przez administratora.</div></div><span class="badge text-bg-{{ 'primary' if ksef_mode == 'user' else 'secondary' }}">{{ 'profil indywidualny' if ksef_mode == 'user' else 'profil współdzielony firmy' }}</span></div>
|
||||
<form method="post" enctype="multipart/form-data" class="row g-3" data-mode-target="ksefFields">{{ ksef_form.hidden_tag() }}{{ source_switch(ksef_form.source_mode.name, ksef_mode, 'user', 'Moje ustawienia KSeF', 'global', 'Użyj profilu współdzielonego firmy') }}<div class="col-12"></div><div class="col-12 source-panel {{ '' if ksef_mode == 'user' else 'd-none' }}" id="ksefFields"><div class="row g-3"><div class="col-md-4">{{ ksef_form.environment.label(class='form-label') }}{{ ksef_form.environment(class='form-select') }}</div><div class="col-md-4">{{ ksef_form.auth_mode.label(class='form-label') }}{{ ksef_form.auth_mode(class='form-select') }}</div><div class="col-md-4">{{ ksef_form.client_id.label(class='form-label') }}{{ ksef_form.client_id(class='form-control') }}</div><div class="col-md-6">{{ ksef_form.token.label(class='form-label') }}{{ ksef_form.token(class='form-control', autocomplete='new-password', placeholder='Podaj nowy token tylko przy zmianie') }}<div class="form-text">{{ 'Token KSeF jest zapisany w konfiguracji tej firmy.' if company_token_configured else ('Token zapisany.' if token_configured else 'Brak zapisanego tokena.') }}</div></div><div class="col-md-6">{{ ksef_form.certificate_file.label(class='form-label') }}{{ ksef_form.certificate_file(class='form-control') }}<div class="form-text">{% if company_certificate_name %}Certyfikat KSeF jest zapisany w konfiguracji tej firmy. Wgrany plik: {{ company_certificate_name }}{% elif certificate_name %}Wgrany plik: {{ certificate_name }}{% elif company_certificate_configured %}Certyfikat KSeF jest zapisany w konfiguracji tej firmy.{% else %}Brak zapisanego certyfikatu.{% endif %}</div></div></div></div><div class="col-12 source-panel-note {{ '' if ksef_mode == 'global' else 'd-none' }}" id="ksefFieldsNote"><div class="alert alert-info mb-0">Po zapisaniu system będzie używał współdzielonego profilu KSeF aktywnej firmy. Parametry i certyfikat konfiguruje administrator w panelu Admin → Ustawienia globalne.</div></div><div class="col-12 d-flex gap-2 flex-wrap">{{ ksef_form.submit(class='btn btn-primary') }}</div></form>
|
||||
</div></div></div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-mail"><div class="card"><div class="card-header">SMTP</div><div class="card-body"><form method="post" class="row g-3" data-mode-target="mailFields">{{ mail_form.hidden_tag() }}{{ source_switch(mail_form.source_mode.name, mail_mode, 'global', 'Użyj ustawień globalnych', 'user', 'Podaj indywidualne ustawienia') }}<div class="col-12"></div><div class="col-12 source-panel {{ '' if mail_mode == 'user' else 'd-none' }}" id="mailFields"><div class="row g-3"><div class="col-md-4">{{ mail_form.server.label(class='form-label') }}{{ mail_form.server(class='form-control') }}</div><div class="col-md-2">{{ mail_form.port.label(class='form-label') }}{{ mail_form.port(class='form-control') }}</div><div class="col-md-3">{{ mail_form.username.label(class='form-label') }}{{ mail_form.username(class='form-control') }}</div><div class="col-md-3">{{ mail_form.password.label(class='form-label') }}{{ mail_form.password(class='form-control') }}</div><div class="col-md-6">{{ mail_form.sender.label(class='form-label') }}{{ mail_form.sender(class='form-control') }}</div><div class="col-md-6">{{ mail_form.test_recipient.label(class='form-label') }}{{ mail_form.test_recipient(class='form-control') }}</div><div class="col-md-4">{{ mail_form.security_mode.label(class='form-label') }}{{ mail_form.security_mode(class='form-select') }}</div></div></div><div class="col-12 source-panel-note {{ '' if mail_mode == 'global' else 'd-none' }}" id="mailFieldsNote"><div class="alert alert-info mb-0">Przy trybie globalnym wiadomości będą wysyłane przez konfigurację ustawioną przez administratora.</div></div><div class="col-12 d-flex gap-2 flex-wrap">{{ mail_form.submit(class='btn btn-primary') }}{{ mail_form.test_submit(class='btn btn-outline-secondary') }}</div></form></div></div></div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-notify"><div class="card"><div class="card-header">Pushover</div><div class="card-body"><form method="post" class="row g-3" data-mode-target="notifyFields">{{ notify_form.hidden_tag() }}{{ source_switch(notify_form.source_mode.name, notify_mode, 'global', 'Użyj ustawień globalnych', 'user', 'Podaj indywidualne ustawienia') }}<div class="col-12"></div><div class="col-12 source-panel {{ '' if notify_mode == 'user' else 'd-none' }}" id="notifyFields"><div class="row g-3"><div class="col-md-6">{{ notify_form.pushover_user_key.label(class='form-label') }}{{ notify_form.pushover_user_key(class='form-control') }}</div><div class="col-md-6">{{ notify_form.pushover_api_token.label(class='form-label') }}{{ notify_form.pushover_api_token(class='form-control') }}</div><div class="col-md-4">{{ notify_form.min_amount.label(class='form-label') }}{{ notify_form.min_amount(class='form-control') }}</div><div class="col-md-8">{{ notify_form.quiet_hours.label(class='form-label') }}{{ notify_form.quiet_hours(class='form-control') }}</div><div class="col-12 form-check">{{ notify_form.enabled(class='form-check-input') }}{{ notify_form.enabled.label(class='form-check-label') }}</div></div></div><div class="col-12 source-panel-note {{ '' if notify_mode == 'global' else 'd-none' }}" id="notifyFieldsNote"><div class="alert alert-info mb-0">Przy trybie globalnym powiadomienia trafią według konfiguracji wspólnej systemu.</div></div><div class="col-12 d-flex gap-2 flex-wrap">{{ notify_form.submit(class='btn btn-primary') }}{{ notify_form.test_submit(class='btn btn-outline-secondary') }}</div></form></div></div></div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-nfz"><div class="card"><div class="card-header">Moduł NFZ</div><div class="card-body"><form method="post" class="row g-3" data-mode-target="nfzFields">{{ nfz_form.hidden_tag() }}{{ source_switch(nfz_form.source_mode.name, nfz_mode, 'global', 'Użyj ustawień globalnych', 'user', 'Ustaw indywidualnie') }}<div class="col-12"></div><div class="col-12 source-panel {{ '' if nfz_mode == 'user' else 'd-none' }}" id="nfzFields"><div class="form-check form-switch fs-5">{{ nfz_form.enabled(class='form-check-input') }}{{ nfz_form.enabled.label(class='form-check-label') }}</div><div class="form-text">Własne ustawienie użytkownika nadpisze konfigurację globalną tylko dla Twojego konta.</div></div><div class="col-12 source-panel-note {{ '' if nfz_mode == 'global' else 'd-none' }}" id="nfzFieldsNote"><div class="alert alert-info mb-0">Moduł NFZ odziedziczy ustawienie globalne administratora.</div></div><div class="col-md-4 d-grid">{{ nfz_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('[data-source-switch]').forEach(function (group) {
|
||||
const form = group.closest('form');
|
||||
if (!form) return;
|
||||
const targetId = form.dataset.modeTarget;
|
||||
const fields = targetId ? document.getElementById(targetId) : null;
|
||||
const note = targetId ? document.getElementById(targetId + 'Note') : null;
|
||||
const update = function () {
|
||||
const checked = group.querySelector('input:checked');
|
||||
const isUser = checked && checked.value === 'user';
|
||||
if (fields) fields.classList.toggle('d-none', !isUser);
|
||||
if (note) note.classList.toggle('d-none', isUser);
|
||||
};
|
||||
group.querySelectorAll('input').forEach(function (input) { input.addEventListener('change', update); });
|
||||
update();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user