nordabiz/templates/admin/fees.html
Maciej Pienczyn 49fcdbdfb5
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
improve(fees): multi-email support in payment reminders
- Checkboxes for each available email (company, contacts, users)
- Multiple emails can be selected simultaneously
- Additional manual email input field
- Backend sends to all selected addresses in one email

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 08:44:17 +01:00

823 lines
34 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Skladki Czlonkowskie - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.admin-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
.stat-card {
background: var(--surface);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
box-shadow: var(--shadow);
text-align: center;
}
.stat-card.success { border-left: 3px solid var(--success); }
.stat-card.warning { border-left: 3px solid var(--warning); }
.stat-card.primary { border-left: 3px solid var(--primary); }
.stat-value {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--primary);
}
.stat-label {
font-size: var(--font-size-xs);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.filters-bar {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
display: flex;
gap: var(--spacing-md);
flex-wrap: wrap;
align-items: center;
}
.filters-bar select, .filters-bar input {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
}
.filters-bar .btn {
padding: var(--spacing-sm) var(--spacing-lg);
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.section h2 {
font-size: var(--font-size-xl);
color: var(--text-primary);
}
.fees-table {
width: 100%;
border-collapse: collapse;
}
.fees-table th,
.fees-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.fees-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--background);
}
.fees-table tr:hover {
background: var(--background);
}
.status-badge {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 600;
text-transform: uppercase;
}
.status-paid { background: var(--success-bg); color: var(--success); }
.status-pending { background: var(--warning-bg); color: var(--warning); }
.status-overdue { background: var(--error-bg); color: var(--error); }
.status-partial { background: var(--info-bg); color: var(--info); }
.status-brak { background: var(--surface-secondary); color: var(--text-secondary); }
.btn-small {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-xs);
}
.actions-cell {
white-space: nowrap;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
max-width: 500px;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.modal-header h3 {
font-size: var(--font-size-xl);
}
.modal-close {
background: none;
border: none;
font-size: var(--font-size-xl);
cursor: pointer;
color: var(--text-secondary);
}
.form-group {
margin-bottom: var(--spacing-md);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-xs);
font-weight: 500;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.btn-group {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-lg);
}
/* Month grid for year view */
.month-cell {
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 600;
cursor: pointer;
}
.month-cell.paid { background: var(--success); color: white; }
.month-cell.partial { background: #60a5fa; color: white; }
.month-cell.pending { background: var(--warning); color: white; }
.month-cell.overdue { background: var(--error); color: white; }
.month-cell.empty { background: var(--surface-secondary); color: var(--text-secondary); }
.partial-badge {
position: absolute;
top: -6px;
right: -6px;
background: #ef4444;
color: white;
font-size: 8px;
font-weight: 700;
padding: 1px 3px;
border-radius: 6px;
line-height: 1;
transform: rotate(12deg);
min-width: 14px;
text-align: center;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-header">
<h1>Skladki Czlonkowskie</h1>
<div class="header-actions">
<a href="{{ url_for('admin.admin_fees_export', year=year, month=month) }}" class="btn btn-secondary">
Eksportuj CSV
</a>
</div>
</div>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-value">{{ total_companies }}</div>
<div class="stat-label">Firm czlonkowskich</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ paid_count }}</div>
<div class="stat-label">Oplaconych</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ pending_count }}</div>
<div class="stat-label">Oczekujacych</div>
</div>
<div class="stat-card primary">
<div class="stat-value">{{ total_paid|int }} zł</div>
<div class="stat-label">Zebrano</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ (total_due - total_paid)|int }} zł</div>
<div class="stat-label">Do zebrania</div>
</div>
</div>
<!-- Legenda -->
<div style="display: flex; gap: var(--spacing-lg); flex-wrap: wrap; margin-bottom: var(--spacing-md); font-size: var(--font-size-sm); color: var(--text-secondary); align-items: center;">
<span style="display: flex; align-items: center; gap: 4px;"><span class="month-cell paid" style="width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px;">1</span> Opłacone</span>
<span style="display: flex; align-items: center; gap: 4px;"><span class="month-cell partial" style="width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px;">1</span> Niepełna wpłata</span>
<span style="display: flex; align-items: center; gap: 4px;"><span class="month-cell pending" style="width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px;">1</span> Oczekujące</span>
<span style="display: flex; align-items: center; gap: 4px;"><span class="month-cell overdue" style="width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px;">1</span> Zaległe</span>
<span style="display: flex; align-items: center; gap: 4px;"><span class="month-cell empty" style="width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px;">-</span> Brak danych</span>
</div>
<!-- Filters -->
<div class="filters-bar">
<form id="feesFilterForm" method="GET" action="{{ url_for('admin.admin_fees') }}" style="display: flex; gap: var(--spacing-md); flex-wrap: wrap; align-items: center;">
<select name="year" onchange="this.form.submit()">
{% for y in years %}
<option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option>
{% endfor %}
</select>
<select name="month" onchange="this.form.submit()">
<option value="">-- Cały rok --</option>
{% for m, name in months %}
<option value="{{ m }}" {% if m == month %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
<select name="status" onchange="this.form.submit()">
<option value="">-- Wszystkie firmy --</option>
<option value="paid" {% if status_filter == 'paid' %}selected{% endif %}>{% if month %}Opłacone{% else %}Uregulowane cały rok{% endif %}</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Oczekujące</option>
{% if month %}
<option value="overdue" {% if status_filter == 'overdue' %}selected{% endif %}>Zaległe</option>
{% else %}
<option value="partial" {% if status_filter == 'partial' %}selected{% endif %}>Częściowo opłacone</option>
{% endif %}
</select>
</form>
{% if month %}
<button class="btn btn-success" onclick="generateFees()">
Generuj skladki na {{ dict(months).get(month, month) }}
</button>
{% endif %}
</div>
<!-- Companies Table -->
<div class="section">
<div class="section-header">
<h2>Lista firm {% if month %}({{ dict(months).get(month, month) }} {{ year }}){% else %}({{ year }}){% endif %}</h2>
{% if month %}
<button class="btn btn-success btn-small" onclick="bulkMarkPaid()">
Oznacz zaznaczone jako oplacone
</button>
{% endif %}
</div>
<table class="fees-table">
<thead>
<tr>
{% if month %}<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>{% endif %}
<th>Firma</th>
{% if month %}
<th>Status</th>
<th>Kwota</th>
<th>Zaplacono</th>
<th>Data platnosci</th>
<th>Akcje</th>
{% else %}
<th>Sty</th><th>Lut</th><th>Mar</th><th>Kwi</th><th>Maj</th><th>Cze</th>
<th>Lip</th><th>Sie</th><th>Wrz</th><th>Paz</th><th>Lis</th><th>Gru</th>
<th></th>
{% endif %}
</tr>
</thead>
<tbody>
{% set ns = namespace(separator_shown=false) %}
{% for cf in companies_fees %}
{% if not month and not cf.has_data and not ns.separator_shown %}
{% set ns.separator_shown = true %}
<tr><td colspan="13" style="background: var(--border); padding: var(--spacing-xs); text-align: center; font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 600;">Firmy bez danych o składkach</td></tr>
{% endif %}
<tr{% if not month and not cf.has_data %} style="opacity: 0.5;"{% endif %}>
{% if month %}
<td>
{% if cf.fee %}
<input type="checkbox" class="fee-checkbox" value="{{ cf.fee.id }}" {% if cf.status == 'paid' %}disabled{% endif %}>
{% endif %}
</td>
<td>
<a href="{{ url_for('company_detail_by_slug', slug=cf.company.slug) }}" target="_blank">
{{ cf.company.name }}
</a>
</td>
<td>
<span class="status-badge status-{{ cf.status }}">
{% if cf.status == 'paid' %}Oplacone
{% elif cf.status == 'pending' %}Oczekuje
{% elif cf.status == 'overdue' %}Zalegle
{% elif cf.status == 'partial' %}Czesciowe
{% else %}Brak
{% endif %}
</span>
</td>
<td>{% if cf.fee %}{{ cf.fee.amount }} zl{% else %}-{% endif %}</td>
<td>{% if cf.fee and cf.fee.amount_paid %}{{ cf.fee.amount_paid }} zl{% else %}-{% endif %}</td>
<td>{% if cf.fee and cf.fee.payment_date %}{{ cf.fee.payment_date }}{% else %}-{% endif %}</td>
<td class="actions-cell">
{% if cf.fee and cf.status != 'paid' %}
<button class="btn btn-success btn-small" onclick="openPaymentModal({{ cf.fee.id }}, '{{ cf.company.name }}', {{ cf.fee.amount }})">
Oplac
</button>
{% elif not cf.fee %}
<span class="text-secondary">Brak rekordu</span>
{% endif %}
</td>
{% else %}
<td>
<a href="{{ url_for('company_detail_by_slug', slug=cf.company.slug) }}" target="_blank" {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}>
{{ cf.company.name }}
</a>
{% if cf.monthly_rate and cf.monthly_rate > 200 %}
<span style="display:inline-block;background:#dbeafe;color:#1e40af;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;margin-left:4px;">{{ cf.monthly_rate }} zł</span>
{% endif %}
</td>
{% for m in range(1, 13) %}
<td>
{% set fee = cf.months.get(m) %}
{% if fee %}
<span class="month-cell {{ fee.status }}" title="{{ fee.status }}: wpłacono {{ fee.amount_paid|int }} z {{ fee.amount|int }} zł" style="position:relative;">
{{ m }}{% if fee.status == 'partial' %}<span class="partial-badge">{{ fee.amount_paid|int }}</span>{% endif %}
</span>
{% else %}
<span class="month-cell empty" title="Brak rekordu">-</span>
{% endif %}
</td>
{% endfor %}
<td>
{% if cf.has_data and cf.months.values()|selectattr('status', 'in', ['pending', 'partial', 'overdue'])|list %}
<button class="btn btn-small" style="font-size:11px;padding:2px 8px;background:var(--warning);color:white;border:none;" onclick="openReminderModal({{ cf.company.id }}, '{{ cf.company.name|e }}', {{ year }})">Przypomnij</button>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Payment Modal -->
<div class="modal" id="paymentModal">
<div class="modal-content">
<div class="modal-header">
<h3>Rejestracja platnosci</h3>
<button class="modal-close" onclick="closePaymentModal()">&times;</button>
</div>
<form id="paymentForm">
<input type="hidden" name="fee_id" id="modalFeeId">
<div class="form-group">
<label>Firma</label>
<input type="text" id="modalCompanyName" disabled>
</div>
<div class="form-group">
<label>Kwota do zaplaty</label>
<input type="number" name="amount_paid" id="modalAmount" step="0.01" required>
</div>
<div class="form-group">
<label>Data platnosci</label>
<input type="date" name="payment_date" id="modalDate" value="{{ now.strftime('%Y-%m-%d') if now else '' }}">
</div>
<div class="form-group">
<label>Metoda platnosci</label>
<select name="payment_method">
<option value="transfer">Przelew bankowy</option>
<option value="cash">Gotowka</option>
<option value="card">Karta</option>
<option value="other">Inna</option>
</select>
</div>
<div class="form-group">
<label>Numer referencyjny</label>
<input type="text" name="payment_reference" placeholder="np. numer przelewu">
</div>
<div class="form-group">
<label>Notatki</label>
<textarea name="notes" rows="2"></textarea>
</div>
<div class="btn-group">
<button type="button" class="btn btn-secondary" onclick="closePaymentModal()">Anuluj</button>
<button type="submit" class="btn btn-success">Zarejestruj platnosc</button>
</div>
</form>
</div>
</div>
<!-- Universal Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width: 420px;">
<div style="text-align: center; margin-bottom: var(--spacing-lg);">
<div class="modal-icon" id="confirmModalIcon"></div>
<h3 id="confirmModalTitle" style="margin-bottom: var(--spacing-sm);">Potwierdzenie</h3>
<p class="modal-description" id="confirmModalMessage"></p>
</div>
<div class="modal-actions" style="justify-content: center;">
<button type="button" class="btn btn-secondary" id="confirmModalCancel">Anuluj</button>
<button type="button" class="btn btn-primary" id="confirmModalOk">OK</button>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.modal-overlay#confirmModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#confirmModal.active { display: flex; }
#confirmModal .modal { background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl); }
#confirmModal .modal-icon { font-size: 3em; margin-bottom: var(--spacing-md); }
#confirmModal .modal-actions { display: flex; gap: var(--spacing-sm); margin-top: var(--spacing-lg); }
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
<!-- Reminder Modal -->
<div class="modal" id="reminderModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1050;align-items:center;justify-content:center;">
<div style="background:var(--surface);border-radius:var(--radius-lg);padding:var(--spacing-xl);max-width:700px;width:90%;max-height:80vh;overflow-y:auto;">
<h3 style="margin-bottom:var(--spacing-md);">Przypomnienie o składce — <span id="reminderCompanyName"></span></h3>
<div style="margin-bottom:var(--spacing-md);padding:var(--spacing-md);background:var(--background);border-radius:var(--radius);font-size:var(--font-size-sm);">
<strong>Kwota:</strong> <span id="reminderAmount"></span>&middot;
<strong>Okres:</strong> <span id="reminderPeriod"></span>
</div>
<div style="margin-bottom:var(--spacing-sm);font-weight:600;">Podgląd wiadomości:</div>
<div id="reminderMessagePreview" style="border:1px solid var(--border);border-radius:var(--radius);padding:var(--spacing-md);margin-bottom:var(--spacing-md);background:white;font-size:var(--font-size-sm);line-height:1.6;max-height:300px;overflow-y:auto;"></div>
<div style="margin-bottom:var(--spacing-md);display:flex;flex-direction:column;gap:var(--spacing-sm);">
<div>
<label style="font-size:var(--font-size-sm);font-weight:600;display:block;margin-bottom:4px;">Wiadomość na portalu do:</label>
<select id="reminderPortalRecipient" style="width:100%;padding:6px;border:1px solid var(--border);border-radius:var(--radius);font-size:var(--font-size-sm);">
<option value="">— nie wysyłaj na portal —</option>
</select>
</div>
<div>
<label style="font-size:var(--font-size-sm);font-weight:600;display:block;margin-bottom:4px;">Wyślij email do:</label>
<div id="reminderEmailCheckboxes" style="display:flex;flex-direction:column;gap:4px;margin-bottom:6px;"></div>
<input type="text" id="reminderEmailCustom" placeholder="+ dodatkowy adres email (wpisz ręcznie)" style="width:100%;padding:6px;border:1px solid var(--border);border-radius:var(--radius);font-size:var(--font-size-sm);">
</div>
</div>
<div style="display:flex;gap:var(--spacing-sm);justify-content:flex-end;">
<button class="btn btn-secondary" onclick="closeReminderModal()">Anuluj</button>
<button class="btn btn-primary" id="reminderSendBtn" onclick="sendReminder()">Wyślij przypomnienie</button>
</div>
<input type="hidden" id="reminderCompanyId">
<input type="hidden" id="reminderSubject">
<input type="hidden" id="reminderMessage">
</div>
</div>
{% endblock %}
{% block extra_js %}
// Modal system
let confirmResolve = null;
function showConfirm(message, options = {}) {
return new Promise(resolve => {
confirmResolve = resolve;
document.getElementById('confirmModalIcon').textContent = options.icon || '❓';
document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie';
document.getElementById('confirmModalMessage').innerHTML = message;
document.getElementById('confirmModalOk').textContent = options.okText || 'OK';
document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary');
document.getElementById('confirmModal').classList.add('active');
});
}
function closeConfirm(result) {
document.getElementById('confirmModal').classList.remove('active');
if (confirmResolve) { confirmResolve(result); confirmResolve = null; }
}
document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true));
document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false));
document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); });
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
async function generateFees() {
const confirmed = await showConfirm('Czy na pewno chcesz wygenerować rekordy składek dla wszystkich firm na wybrany miesiąc?', {
icon: '📋',
title: 'Generowanie składek',
okText: 'Generuj',
okClass: 'btn-success'
});
if (!confirmed) return;
const formData = new FormData();
formData.append('year', {{ year }});
formData.append('month', {{ month or 'null' }});
try {
const response = await fetch('{{ url_for("admin.admin_fees_generate") }}', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast('Błąd: ' + data.error, 'error');
}
} catch (err) {
showToast('Błąd: ' + err, 'error');
}
}
function openPaymentModal(feeId, companyName, amount) {
document.getElementById('modalFeeId').value = feeId;
document.getElementById('modalCompanyName').value = companyName;
document.getElementById('modalAmount').value = amount;
document.getElementById('modalDate').value = new Date().toISOString().split('T')[0];
document.getElementById('paymentModal').classList.add('active');
}
function closePaymentModal() {
document.getElementById('paymentModal').classList.remove('active');
}
document.getElementById('paymentForm').addEventListener('submit', async function(e) {
e.preventDefault();
const feeId = document.getElementById('modalFeeId').value;
const formData = new FormData(this);
try {
const response = await fetch('/admin/fees/' + feeId + '/mark-paid', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
});
const data = await response.json();
if (data.success) {
closePaymentModal();
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast('Błąd: ' + data.error, 'error');
}
} catch (err) {
showToast('Błąd: ' + err, 'error');
}
});
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.fee-checkbox:not(:disabled)');
checkboxes.forEach(cb => cb.checked = selectAll.checked);
}
async function bulkMarkPaid() {
const checkboxes = document.querySelectorAll('.fee-checkbox:checked');
if (checkboxes.length === 0) {
showToast('Zaznacz przynajmniej jedną składkę', 'warning');
return;
}
const confirmed = await showConfirm(`Czy na pewno chcesz oznaczyć <strong>${checkboxes.length}</strong> składek jako opłacone?`, {
icon: '💰',
title: 'Oznaczanie płatności',
okText: 'Oznacz',
okClass: 'btn-success'
});
if (!confirmed) return;
const formData = new FormData();
checkboxes.forEach(cb => formData.append('fee_ids[]', cb.value));
try {
const response = await fetch('{{ url_for("admin.admin_fees_bulk_mark_paid") }}', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast('Błąd: ' + data.error, 'error');
}
} catch (err) {
showToast('Błąd: ' + err, 'error');
}
}
// Close modal on outside click
document.getElementById('paymentModal').addEventListener('click', function(e) {
if (e.target === this) {
closePaymentModal();
}
});
// Reminder functions
async function openReminderModal(companyId, companyName, year) {
var fd = new FormData();
fd.append('company_id', companyId);
fd.append('year', year);
try {
var resp = await fetch('/admin/fees/reminder-preview', {
method: 'POST',
body: fd,
headers: {'X-CSRFToken': '{{ csrf_token() }}'}
});
var data = await resp.json();
if (!data.success) {
showToast(data.error, 'error');
return;
}
document.getElementById('reminderCompanyName').textContent = data.company_name;
document.getElementById('reminderAmount').textContent = data.total_due;
document.getElementById('reminderPeriod').textContent = data.period;
document.getElementById('reminderMessagePreview').innerHTML = data.message;
// Populate portal recipient dropdown
var portalSel = document.getElementById('reminderPortalRecipient');
portalSel.innerHTML = '<option value="">— nie wysyłaj na portal —</option>';
(data.linked_users || []).forEach(function(u) {
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.name + (u.role ? ' (' + u.role + ')' : '');
portalSel.appendChild(opt);
});
if (data.manager_user_id) portalSel.value = data.manager_user_id;
// Populate email checkboxes
var emailBox = document.getElementById('reminderEmailCheckboxes');
emailBox.innerHTML = '';
var emails = data.available_emails || [];
emails.forEach(function(e, i) {
var label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'reminder-email-cb';
cb.value = e.email;
cb.checked = (i === 0);
label.appendChild(cb);
label.appendChild(document.createTextNode(e.label));
emailBox.appendChild(label);
});
if (emails.length === 0) {
emailBox.innerHTML = '<span style="font-size:12px;color:var(--text-secondary);">Brak adresów email w systemie</span>';
}
document.getElementById('reminderEmailCustom').value = '';
document.getElementById('reminderCompanyId').value = companyId;
document.getElementById('reminderSubject').value = data.subject;
document.getElementById('reminderMessage').value = data.message;
document.getElementById('reminderModal').style.display = 'flex';
} catch(e) {
showToast('Błąd: ' + e, 'error');
}
}
function closeReminderModal() {
document.getElementById('reminderModal').style.display = 'none';
}
async function sendReminder() {
var btn = document.getElementById('reminderSendBtn');
btn.disabled = true;
btn.textContent = 'Wysyłanie...';
var fd = new FormData();
fd.append('company_id', document.getElementById('reminderCompanyId').value);
fd.append('manager_user_id', document.getElementById('reminderPortalRecipient').value);
fd.append('subject', document.getElementById('reminderSubject').value);
fd.append('message', document.getElementById('reminderMessage').value);
// Collect all checked emails + custom
var selectedEmails = [];
document.querySelectorAll('.reminder-email-cb:checked').forEach(function(cb) {
selectedEmails.push(cb.value);
});
var custom = document.getElementById('reminderEmailCustom').value.trim();
if (custom && selectedEmails.indexOf(custom) === -1) {
selectedEmails.push(custom);
}
if (selectedEmails.length > 0) {
fd.append('send_email', 'on');
fd.append('company_email', selectedEmails.join(','));
}
try {
var resp = await fetch('/admin/fees/send-reminder', {
method: 'POST',
body: fd,
headers: {'X-CSRFToken': '{{ csrf_token() }}'}
});
var data = await resp.json();
if (data.success) {
closeReminderModal();
showToast(data.message, 'success');
} else {
showToast(data.error, 'error');
}
} catch(e) {
showToast('Błąd: ' + e, 'error');
}
btn.disabled = false;
btn.textContent = 'Wyślij przypomnienie';
}
document.getElementById('reminderModal').addEventListener('click', function(e) {
if (e.target === this) closeReminderModal();
});
{% endblock %}