nordabiz/templates/admin/benefits_list.html
Maciej Pienczyn e718d96a7d
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
fix(security): Resolve 1 HIGH and 7 MEDIUM vulnerabilities from code review
- HIGH: Fix SQL injection in ZOPK knowledge service (3 functions) — replace f-strings with parameterized queries
- MEDIUM: Sanitize tsquery/LIKE input in SearchService to prevent injection
- MEDIUM: Add @login_required + @role_required(ADMIN) to /health/full endpoint
- MEDIUM: Add @role_required(ADMIN) to ZOPK knowledge search API
- MEDIUM: Add bleach HTML sanitization on write for announcements, events, board proceedings (stored XSS via |safe)
- MEDIUM: Remove partial API key from Gemini service logs
- MEDIUM: Remove @csrf.exempt from chat endpoints, add X-CSRFToken headers in JS
- MEDIUM: Add missing CSRF tokens to 3 POST forms (data_request, benefits_form, benefits_list)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 05:25:18 +01:00

330 lines
10 KiB
HTML

{% extends "base.html" %}
{% block title %}Korzyści Członkowskie - Admin{% 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-row {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.stat-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
min-width: 150px;
}
.stat-value {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-primary);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.benefits-table {
width: 100%;
border-collapse: collapse;
}
.benefits-table th,
.benefits-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.benefits-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
background: var(--background);
}
.benefits-table tr:hover {
background: var(--background);
}
.benefit-name-cell {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.benefit-logo-small {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
object-fit: contain;
background: #f8fafc;
}
.benefit-logo-placeholder-small {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: var(--font-size-sm);
}
.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;
}
.status-active { background: #dcfce7; color: #16a34a; }
.status-inactive { background: #f3f4f6; color: #6b7280; }
.status-featured { background: #dbeafe; color: #2563eb; }
.category-badge {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
background: var(--primary-bg);
color: var(--primary);
}
.actions-cell {
display: flex;
gap: var(--spacing-xs);
}
.btn-small {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-xs);
border-radius: var(--radius-sm);
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.btn-edit {
background: #f0f9ff;
color: #0284c7;
border: 1px solid #bae6fd;
}
.btn-edit:hover {
background: #e0f2fe;
}
.btn-toggle {
background: #fef3c7;
color: #d97706;
border: 1px solid #fde68a;
}
.btn-toggle:hover {
background: #fef08a;
}
.btn-delete {
background: #fee2e2;
color: #dc2626;
border: 1px solid #fecaca;
}
.btn-delete:hover {
background: #fecaca;
}
.btn-primary {
background: var(--primary);
color: white;
border: none;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
}
.btn-primary:hover {
background: var(--primary-dark);
color: white;
}
.empty-state {
text-align: center;
padding: var(--spacing-3xl);
color: var(--text-secondary);
}
.private-column {
background: #fef3c7;
}
.private-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--font-size-xs);
color: #92400e;
margin-left: 4px;
}
.private-indicator svg {
width: 12px;
height: 12px;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-header">
<h1>Korzyści Członkowskie</h1>
<a href="{{ url_for('admin.admin_benefits_new') }}" class="btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Dodaj korzyść
</a>
</div>
<div class="stats-row">
<div class="stat-card">
<div class="stat-value">{{ benefits|length }}</div>
<div class="stat-label">Wszystkich</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ active_count }}</div>
<div class="stat-label">Aktywnych</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ total_clicks }}</div>
<div class="stat-label">Kliknięć</div>
</div>
</div>
<div class="section">
{% if benefits %}
<table class="benefits-table">
<thead>
<tr>
<th>Nazwa</th>
<th>Kategoria</th>
{% if current_user.email == 'maciej.pienczyn@inpi.pl' %}
<th class="private-column">
Prowizja
<span class="private-indicator" title="Widoczne tylko dla Ciebie">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</span>
</th>
{% endif %}
<th>Kliknięcia</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for benefit in benefits %}
<tr>
<td>
<div class="benefit-name-cell">
{% if benefit.logo_url %}
<img src="{{ benefit.logo_url }}" alt="" class="benefit-logo-small">
{% else %}
<div class="benefit-logo-placeholder-small">{{ benefit.name[0] }}</div>
{% endif %}
<div>
<strong>{{ benefit.name }}</strong>
{% if benefit.is_featured %}
<span class="status-badge status-featured">Polecane</span>
{% endif %}
</div>
</div>
</td>
<td>
<span class="category-badge">{{ benefit.category_label }}</span>
</td>
{% if current_user.email == 'maciej.pienczyn@inpi.pl' %}
<td class="private-column">
{% if benefit.commission_rate %}
{{ benefit.commission_rate }}
{% if benefit.commission_duration %}<br><small>przez {{ benefit.commission_duration }}</small>{% endif %}
{% else %}
-
{% endif %}
</td>
{% endif %}
<td>{{ benefit.click_count or 0 }}</td>
<td>
{% if benefit.is_active %}
<span class="status-badge status-active">Aktywna</span>
{% else %}
<span class="status-badge status-inactive">Nieaktywna</span>
{% endif %}
</td>
<td>
<div class="actions-cell">
<a href="{{ url_for('admin.admin_benefits_edit', benefit_id=benefit.id) }}" class="btn-small btn-edit">Edytuj</a>
<form action="{{ url_for('admin.admin_benefits_toggle', benefit_id=benefit.id) }}" method="POST" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn-small btn-toggle">
{% if benefit.is_active %}Wyłącz{% else %}Włącz{% endif %}
</button>
</form>
<a href="{{ url_for('admin.admin_benefits_clicks', benefit_id=benefit.id) }}" class="btn-small btn-edit">Kliknięcia</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>Brak korzyści. <a href="{{ url_for('admin.admin_benefits_new') }}">Dodaj pierwszą</a></p>
</div>
{% endif %}
</div>
</div>
{% endblock %}