feat(board): widok read-only składek dla Zarządu i Rady Izby
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
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
Nowy route /rada/skladki dostępny dla członków Rady Izby (chamber_role). Pokazuje te same dane co panel admin, ale bez edycji — tylko podgląd statusów płatności, filtrowanie i zaległości z lat poprzednich. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a612c45526
commit
0e5f76dea9
@ -22,6 +22,7 @@ Endpoints - Documents:
|
||||
"""
|
||||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from flask import (
|
||||
render_template, request, redirect, url_for, flash,
|
||||
@ -31,7 +32,7 @@ from flask_login import login_required, current_user
|
||||
from sqlalchemy import desc
|
||||
|
||||
from . import bp
|
||||
from database import SessionLocal, BoardMeeting, BoardDocument, SystemRole, User
|
||||
from database import SessionLocal, BoardMeeting, BoardDocument, SystemRole, User, Company, MembershipFee, MembershipFeeConfig
|
||||
from utils.decorators import rada_member_required, office_manager_required
|
||||
from utils.helpers import sanitize_html
|
||||
from services.document_upload_service import DocumentUploadService
|
||||
@ -755,3 +756,89 @@ def _handle_meeting_form(db, meeting=None):
|
||||
current_app.logger.error(f"Failed to save meeting: {e}")
|
||||
flash('Błąd podczas zapisywania posiedzenia.', 'error')
|
||||
return redirect(request.url)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MEMBERSHIP FEES - READ ONLY VIEW FOR BOARD MEMBERS
|
||||
# =============================================================================
|
||||
|
||||
MONTHS_PL_BOARD = [
|
||||
(1, 'Styczeń'), (2, 'Luty'), (3, 'Marzec'), (4, 'Kwiecień'),
|
||||
(5, 'Maj'), (6, 'Czerwiec'), (7, 'Lipiec'), (8, 'Sierpień'),
|
||||
(9, 'Wrzesień'), (10, 'Październik'), (11, 'Listopad'), (12, 'Grudzień')
|
||||
]
|
||||
|
||||
|
||||
@bp.route('/skladki')
|
||||
@login_required
|
||||
@rada_member_required
|
||||
def board_fees():
|
||||
"""Read-only view of membership fees for board members."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from sqlalchemy import func
|
||||
|
||||
year = request.args.get('year', datetime.now().year, type=int)
|
||||
status_filter = request.args.get('status', '')
|
||||
|
||||
companies = db.query(Company).filter(Company.status == 'active').order_by(Company.name).all()
|
||||
|
||||
fee_query = db.query(MembershipFee).filter(MembershipFee.fee_year == year)
|
||||
fees = {(f.company_id, f.fee_month): f for f in fee_query.all()}
|
||||
|
||||
companies_fees = []
|
||||
for company in companies:
|
||||
company_data = {'company': company, 'months': {}, 'monthly_rate': 0, 'has_data': False}
|
||||
for m in range(1, 13):
|
||||
fee = fees.get((company.id, m))
|
||||
company_data['months'][m] = fee
|
||||
if fee and fee.amount:
|
||||
company_data['has_data'] = True
|
||||
if not company_data['monthly_rate']:
|
||||
company_data['monthly_rate'] = int(fee.amount)
|
||||
companies_fees.append(company_data)
|
||||
|
||||
# Sort: companies with fee data first
|
||||
companies_fees.sort(key=lambda cf: (0 if cf.get('has_data') else 1, cf['company'].name))
|
||||
|
||||
# Filters
|
||||
if status_filter:
|
||||
if status_filter == 'paid':
|
||||
companies_fees = [cf for cf in companies_fees if all(
|
||||
cf['months'].get(m) and cf['months'][m].status == 'paid' for m in range(1, 13)
|
||||
if cf['months'].get(m)
|
||||
) and any(cf['months'].get(m) for m in range(1, 13))]
|
||||
elif status_filter == 'partial':
|
||||
companies_fees = [cf for cf in companies_fees if (
|
||||
(any(cf['months'].get(m) and cf['months'][m].status == 'paid' for m in range(1, 13)) and
|
||||
any(cf['months'].get(m) and cf['months'][m].status in ('pending', 'overdue') for m in range(1, 13))) or
|
||||
any(cf['months'].get(m) and cf['months'][m].status == 'partial' for m in range(1, 13))
|
||||
)]
|
||||
elif status_filter == 'none':
|
||||
companies_fees = [cf for cf in companies_fees if
|
||||
any(cf['months'].get(m) for m in range(1, 13)) and
|
||||
not any(cf['months'].get(m) and cf['months'][m].status in ('paid', 'partial') for m in range(1, 13))
|
||||
]
|
||||
|
||||
total_companies = len(companies)
|
||||
all_fees = list(fees.values())
|
||||
paid_count = sum(1 for f in all_fees if f.status == 'paid')
|
||||
pending_count = len(all_fees) - paid_count
|
||||
total_due = sum(float(f.amount) for f in all_fees) if all_fees else Decimal(0)
|
||||
total_paid = sum(float(f.amount_paid or 0) for f in all_fees) if all_fees else Decimal(0)
|
||||
|
||||
return render_template(
|
||||
'board/fees_readonly.html',
|
||||
companies_fees=companies_fees,
|
||||
year=year,
|
||||
status_filter=status_filter,
|
||||
total_companies=total_companies,
|
||||
paid_count=paid_count,
|
||||
pending_count=pending_count,
|
||||
total_due=total_due,
|
||||
total_paid=total_paid,
|
||||
years=list(range(2022, datetime.now().year + 2)),
|
||||
months=MONTHS_PL_BOARD,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
274
templates/board/fees_readonly.html
Normal file
274
templates/board/fees_readonly.html
Normal file
@ -0,0 +1,274 @@
|
||||
{% 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);
|
||||
}
|
||||
|
||||
.fees-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.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.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>Składki Członkowskie <span class="readonly-badge">tylko podgląd</span></h1>
|
||||
</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;"><span class="month-cell overdue" style="width: 24px; height: 24px; 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; font-size: 11px;">-</span> Brak danych</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;">500 zł</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 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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set ns = namespace(separator_shown=false) %}
|
||||
{% for cf in companies_fees %}
|
||||
{% if not cf.has_data and not ns.separator_shown %}
|
||||
{% set ns.separator_shown = true %}
|
||||
<tr><td colspan="14" 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 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.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 class="col-month">
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user