nordabiz/templates/board/fees_readonly.html
Maciej Pienczyn ea8b622903
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
feat: fee underpayment indicator + premium color on month tiles
- paid 200 zł (stawka 200): green (standard)
- paid 300 zł (stawka 300): teal (paid-premium)
- paid 200 zł (stawka 300): red outline + ! badge (underpaid)
Applied to both board/skladki and admin/fees views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:46:37 +02:00

342 lines
16 KiB
HTML

{% extends "base.html" %}
{% block title %}Składki Członkowskie - Rada Izby{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
}
.admin-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 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 {
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 {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.container {
max-width: 1600px;
}
.fees-table {
width: 100%;
border-collapse: collapse;
min-width: 900px;
}
.fees-table th,
.fees-table td {
padding: var(--spacing-xs) var(--spacing-sm);
text-align: center;
border-bottom: 1px solid var(--border);
}
.fees-table th:first-child,
.fees-table td:first-child {
text-align: left;
width: 160px;
}
.fees-table th.col-month,
.fees-table td.col-month {
width: 36px;
padding: var(--spacing-xs) 2px;
}
.fees-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0;
background: var(--background);
}
.fees-table tr:hover {
background: var(--background);
}
.month-cell {
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 10px;
font-weight: 600;
}
.month-cell.paid { background: var(--success); color: white; }
.month-cell.paid-premium { background: #0d9488; 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;
}
.readonly-badge {
display: inline-block;
background: var(--info-bg);
color: var(--info);
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius-full);
font-weight: 600;
margin-left: var(--spacing-sm);
vertical-align: middle;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-header">
<h1>
<svg width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Strefa RADA <span class="readonly-badge">tylko podgląd</span>
</h1>
</div>
<!-- Board sub-navigation -->
<div style="display:flex;gap:0;margin-bottom:var(--spacing-xl);border-bottom:2px solid var(--border);">
<a href="{{ url_for('board.index') }}" style="padding:10px 20px;font-weight:500;font-size:var(--font-size-sm);text-decoration:none;border-bottom:2px solid transparent;color:var(--text-secondary);">Posiedzenia</a>
<a href="{{ url_for('board.board_fees') }}" style="padding:10px 20px;font-weight:500;font-size:var(--font-size-sm);text-decoration:none;border-bottom:2px solid var(--primary);color:var(--primary);margin-bottom:-2px;">Składki członkowskie</a>
</div>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-value">{{ total_companies }}</div>
<div class="stat-label">Firm członkowskich</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ paid_count }}</div>
<div class="stat-label">Opłaconych składek (łącznie)</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ pending_count }}</div>
<div class="stat-label">Oczekujących składek (łącznie)</div>
</div>
<div class="stat-card success">
<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; 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; 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; font-size: 11px;">1</span> Oczekujące</span>
<span style="display: flex; align-items: center; gap: 4px; border-left: 1px solid var(--border); padding-left: var(--spacing-lg);"><span style="color:var(--error);font-weight:700;font-size:12px;"></span> Zaległości z lat poprzednich</span>
</div>
<!-- Filters -->
<div class="filters-bar">
<form method="GET" action="{{ url_for('board.board_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="status" onchange="this.form.submit()">
<option value="">-- Wszystkie firmy --</option>
<option value="paid" {% if status_filter == 'paid' %}selected{% endif %}>Opłacone za cały rok</option>
<option value="partial" {% if status_filter == 'partial' %}selected{% endif %}>Częściowo opłacone</option>
<option value="none" {% if status_filter == 'none' %}selected{% endif %}>Brak wpłat</option>
</select>
</form>
</div>
<!-- Companies Table -->
<div class="section">
<div class="section-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--spacing-lg);">
<h2>Lista firm ({{ year }}) <span style="display:inline-flex;align-items:center;justify-content:center;background:var(--error);color:white;font-size:var(--font-size-sm);font-weight:700;min-width:28px;height:28px;border-radius:var(--radius-full);padding:0 8px;vertical-align:middle;margin-left:8px;">{{ companies_fees|length }}</span></h2>
</div>
<table class="fees-table">
<thead>
<tr>
<th>Firma</th>
<th style="width:90px;font-size:10px;">Stawka</th>
<th class="col-month">I</th><th class="col-month">II</th><th class="col-month">III</th><th class="col-month">IV</th><th class="col-month">V</th><th class="col-month">VI</th>
<th class="col-month">VII</th><th class="col-month">VIII</th><th class="col-month">IX</th><th class="col-month">X</th><th class="col-month">XI</th><th class="col-month">XII</th>
<th style="width:70px;font-size:9px;line-height:1.2;">Zaległ.<br><span style="font-weight:400;text-transform:none;">z lat poprz.</span></th>
<th style="width:50px;font-size:9px;">Przyp.</th>
<th style="width:60px;font-size:9px;">Wezwanie</th>
</tr>
</thead>
<tbody>
{% set ns = namespace(separator_shown=false) %}
{% for cf in companies_fees %}
{% if not cf.has_data and not ns.separator_shown and not cf.is_child %}
{% set ns.separator_shown = true %}
<tr><td colspan="15" 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 %}
{% if cf.is_child %}
{# Firma córka — wiersz z przekreślonymi kafelkami #}
<tr style="opacity:0.55;">
<td style="padding-left:24px;">
<span style="color:var(--text-secondary);font-size:12px;">↳ {{ cf.company.name }}</span>
<span style="display:inline-block;background:#e0e7ff;color:#3730a3;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:600;margin-left:4px;">firma córka</span>
</td>
<td style="text-align:center;">
<span style="font-size:11px;color:var(--text-muted);">0 zł</span>
</td>
{% for m in range(1, 13) %}
<td class="col-month">
{% set parent_fee = cf.parent_months.get(m) %}
<span class="month-cell {% if parent_fee %}{{ parent_fee.status }}{% else %}empty{% endif %}" style="position:relative;opacity:0.4;background-image:repeating-linear-gradient(135deg,transparent,transparent 3px,rgba(0,0,0,0.12) 3px,rgba(0,0,0,0.12) 4px);" title="Nie dotyczy — składka w firmie matce">
-
</span>
</td>
{% endfor %}
<td><span style="color:var(--text-secondary);font-size:11px;"></span></td>
</tr>
{% else %}
{# Firma normalna lub matka #}
<tr{% if not cf.has_data %} style="opacity: 0.5;"{% endif %}>
<td>
<span {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}>
{{ cf.company.name }}
</span>
{% if cf.child_count > 0 %}
<details style="margin-top:2px;">
<summary style="font-size:10px;color:var(--primary);cursor:pointer;">{{ cf.child_count }} {{ 'marka' if cf.child_count == 1 else ('marki' if cf.child_count <= 4 else 'marek') }} zależnych</summary>
<div style="font-size:10px;color:var(--text-secondary);padding:2px 0 0 8px;">
{% for ch in cf.child_brands|sort(attribute='name') %}
<div>{{ ch.name }} <span style="color:var(--text-muted);font-size:9px;">(od {{ ch.created_at.strftime('%m/%Y') if ch.created_at else '?' }})</span></div>
{% endfor %}
</div>
</details>
{% endif %}
</td>
<td style="text-align:center;">
{% if cf.child_count > 0 %}
{% set rate_change_month = cf.rate_change_month %}
{% if rate_change_month and rate_change_month > 1 %}
<div style="font-size:10px;line-height:1.3;">
<span style="color:var(--text-secondary);">I-{{ ['','I','II','III','IV','V','VI','VII','VIII','IX','X','XI','XII'][rate_change_month - 1] }}: 200 zł</span><br>
<span style="display:inline-block;background:#fef3c7;color:#92400e;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:700;">od {{ ['','I','II','III','IV','V','VI','VII','VIII','IX','X','XI','XII'][rate_change_month] }}: 300 zł</span>
</div>
{% else %}
<span style="display:inline-block;background:#fef3c7;color:#92400e;font-size:11px;padding:2px 6px;border-radius:4px;font-weight:700;">300 zł</span>
{% endif %}
{% else %}
<span style="font-size:11px;color:var(--text-secondary);">200 zł</span>
{% endif %}
</td>
{% for m in range(1, 13) %}
<td class="col-month">
{% set fee = cf.months.get(m) %}
{% set expected = cf.expected_fees.get(m, 200) %}
{% set underpaid = fee and fee.amount and fee.amount|int < expected %}
{% set is_premium = fee and fee.status == 'paid' and expected >= 300 and fee.amount|int >= 300 %}
{% if fee %}
<span class="month-cell {% if is_premium %}paid-premium{% else %}{{ fee.status }}{% endif %}" title="{{ fee.status }}: wpłacono {{ fee.amount_paid|int }} z {{ fee.amount|int }} zł{% if underpaid %} ⚠ stawka powinna wynosić {{ expected }} zł{% endif %}{% if is_premium %} (stawka pełna 300 zł){% endif %}" style="position:relative;{% if underpaid %}outline:2px solid var(--error);outline-offset:-2px;{% endif %}">
{{ m }}{% if fee.status == 'partial' %}<span class="partial-badge">{{ fee.amount_paid|int }}</span>{% endif %}{% if underpaid %}<span style="position:absolute;top:-6px;right:-6px;background:var(--error);color:white;font-size:8px;font-weight:700;width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1;">!</span>{% endif %}
</span>
{% else %}
<span class="month-cell empty" title="Brak rekordu (stawka: {{ expected }} zł)">-</span>
{% endif %}
</td>
{% endfor %}
<td>
{% set debt = cf.company.previous_years_debt|default(0)|float %}
{% if debt > 0 %}
<span style="color:var(--error);font-weight:700;font-size:13px;">{{ debt|int }} zł</span>
{% else %}
<span style="color:var(--text-secondary);font-size:11px;"></span>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}