- Add MembershipFee and MembershipFeeConfig models - Add /health endpoint for monitoring - Add Microsoft Fluent Design CSS - Update templates with new CSS structure - Add Announcement model - Update .gitignore to exclude analysis files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
560 lines
18 KiB
HTML
560 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Skladki Czlonkowskie - Norda Biznes Hub{% 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-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card.success { border-left: 4px solid var(--success); }
|
|
.stat-card.warning { border-left: 4px solid var(--warning); }
|
|
.stat-card.primary { border-left: 4px solid var(--primary); }
|
|
|
|
.stat-value {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.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.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); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<div class="admin-header">
|
|
<h1>Skladki Czlonkowskie</h1>
|
|
<div class="header-actions">
|
|
<a href="{{ url_for('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">{{ "%.2f"|format(total_paid) }} zl</div>
|
|
<div class="stat-label">Zebrano</div>
|
|
</div>
|
|
<div class="stat-card warning">
|
|
<div class="stat-value">{{ "%.2f"|format(total_due - total_paid) }} zl</div>
|
|
<div class="stat-label">Do zebrania</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filters-bar">
|
|
<form method="GET" action="{{ url_for('admin_fees') }}" style="display: flex; gap: var(--spacing-md); flex-wrap: wrap; align-items: center;">
|
|
<select name="year">
|
|
{% for y in years %}
|
|
<option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
|
|
<select name="month">
|
|
<option value="">-- Caly rok --</option>
|
|
{% for m, name in months %}
|
|
<option value="{{ m }}" {% if m == month %}selected{% endif %}>{{ name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
|
|
{% if month %}
|
|
<select name="status">
|
|
<option value="">-- Wszystkie --</option>
|
|
<option value="paid" {% if status_filter == 'paid' %}selected{% endif %}>Oplacone</option>
|
|
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Oczekujace</option>
|
|
<option value="overdue" {% if status_filter == 'overdue' %}selected{% endif %}>Zaległe</option>
|
|
</select>
|
|
{% endif %}
|
|
|
|
<button type="submit" class="btn btn-primary">Filtruj</button>
|
|
</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>
|
|
{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for cf in companies_fees %}
|
|
<tr>
|
|
{% 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">
|
|
{{ cf.company.name }}
|
|
</a>
|
|
</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 }}: {{ fee.amount }} zl">
|
|
{{ m }}
|
|
</span>
|
|
{% else %}
|
|
<span class="month-cell empty" title="Brak rekordu">-</span>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
{% 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()">×</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>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
function generateFees() {
|
|
if (!confirm('Czy na pewno chcesz wygenerowac rekordy skladek dla wszystkich firm na wybrany miesiac?')) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('year', {{ year }});
|
|
formData.append('month', {{ month or 'null' }});
|
|
|
|
fetch('{{ url_for("admin_fees_generate") }}', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(data.message);
|
|
location.reload();
|
|
} else {
|
|
alert('Blad: ' + data.error);
|
|
}
|
|
})
|
|
.catch(err => alert('Blad: ' + err));
|
|
}
|
|
|
|
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', function(e) {
|
|
e.preventDefault();
|
|
|
|
const feeId = document.getElementById('modalFeeId').value;
|
|
const formData = new FormData(this);
|
|
|
|
fetch('/admin/fees/' + feeId + '/mark-paid', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(data.message);
|
|
location.reload();
|
|
} else {
|
|
alert('Blad: ' + data.error);
|
|
}
|
|
})
|
|
.catch(err => alert('Blad: ' + err));
|
|
});
|
|
|
|
function toggleSelectAll() {
|
|
const selectAll = document.getElementById('selectAll');
|
|
const checkboxes = document.querySelectorAll('.fee-checkbox:not(:disabled)');
|
|
checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
|
}
|
|
|
|
function bulkMarkPaid() {
|
|
const checkboxes = document.querySelectorAll('.fee-checkbox:checked');
|
|
if (checkboxes.length === 0) {
|
|
alert('Zaznacz przynajmniej jedna skladke');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('Czy na pewno chcesz oznaczyc ' + checkboxes.length + ' skladek jako oplacone?')) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
checkboxes.forEach(cb => formData.append('fee_ids[]', cb.value));
|
|
|
|
fetch('{{ url_for("admin_fees_bulk_mark_paid") }}', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(data.message);
|
|
location.reload();
|
|
} else {
|
|
alert('Blad: ' + data.error);
|
|
}
|
|
})
|
|
.catch(err => alert('Blad: ' + err));
|
|
}
|
|
|
|
// Close modal on outside click
|
|
document.getElementById('paymentModal').addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closePaymentModal();
|
|
}
|
|
});
|
|
{% endblock %}
|