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
- 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>
354 lines
13 KiB
HTML
354 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{% if benefit %}Edytuj{% else %}Dodaj{% endif %} Korzyść - Admin{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.form-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.admin-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.admin-header h1 {
|
|
font-size: var(--font-size-2xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-section {
|
|
background: var(--surface);
|
|
padding: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-section-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-lg);
|
|
padding-bottom: var(--spacing-sm);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-row.single {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.form-group label .required {
|
|
color: var(--error);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select,
|
|
.form-group textarea {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--font-size-md);
|
|
background: var(--surface);
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus,
|
|
.form-group textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px var(--primary-bg);
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 100px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.form-group .hint {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.checkbox-group input[type="checkbox"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
justify-content: flex-end;
|
|
padding-top: var(--spacing-lg);
|
|
border-top: 1px solid var(--border);
|
|
margin-top: var(--spacing-lg);
|
|
}
|
|
|
|
.btn {
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
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 {
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--primary-dark);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--surface);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-lg);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.back-link svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<div class="form-container">
|
|
<a href="{{ url_for('admin.admin_benefits') }}" class="back-link">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
|
<polyline points="12 19 5 12 12 5"></polyline>
|
|
</svg>
|
|
Powrót do listy
|
|
</a>
|
|
|
|
<div class="admin-header">
|
|
<h1>{% if benefit %}Edytuj korzyść: {{ benefit.name }}{% else %}Dodaj nową korzyść{% endif %}</h1>
|
|
</div>
|
|
|
|
<form method="POST">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<!-- Podstawowe info -->
|
|
<div class="form-section">
|
|
<div class="form-section-title">Podstawowe informacje</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Nazwa <span class="required">*</span></label>
|
|
<input type="text" name="name" value="{{ benefit.name if benefit else '' }}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Slug (URL) <span class="required">*</span></label>
|
|
<input type="text" name="slug" value="{{ benefit.slug if benefit else '' }}" required pattern="[a-z0-9-]+">
|
|
<span class="hint">Tylko małe litery, cyfry i myślniki, np. wispr-flow</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Kategoria</label>
|
|
<select name="category">
|
|
<option value="">-- Wybierz --</option>
|
|
{% for value, label in categories %}
|
|
<option value="{{ value }}" {% if benefit and benefit.category == value %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Kolejność wyświetlania</label>
|
|
<input type="number" name="display_order" value="{{ benefit.display_order if benefit else 0 }}" min="0">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row single">
|
|
<div class="form-group">
|
|
<label>Krótki opis (na kartę)</label>
|
|
<input type="text" name="short_description" value="{{ benefit.short_description if benefit else '' }}" maxlength="200">
|
|
<span class="hint">Max 200 znaków</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row single">
|
|
<div class="form-group">
|
|
<label>Pełny opis</label>
|
|
<textarea name="description">{{ benefit.description if benefit else '' }}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ceny i oferta -->
|
|
<div class="form-section">
|
|
<div class="form-section-title">Ceny i oferta</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Cena regularna</label>
|
|
<input type="text" name="regular_price" value="{{ benefit.regular_price if benefit else '' }}" placeholder="np. $10/mies">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Cena dla członków</label>
|
|
<input type="text" name="member_price" value="{{ benefit.member_price if benefit else '' }}" placeholder="np. $8/mies">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row single">
|
|
<div class="form-group">
|
|
<label>Opis zniżki</label>
|
|
<input type="text" name="discount_description" value="{{ benefit.discount_description if benefit else '' }}" placeholder="np. 10% zniżki dla członków">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Kod promocyjny</label>
|
|
<input type="text" name="promo_code" value="{{ benefit.promo_code if benefit else '' }}" placeholder="np. NORDA10">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Instrukcja użycia kodu</label>
|
|
<input type="text" name="promo_code_instructions" value="{{ benefit.promo_code_instructions if benefit else '' }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Linki -->
|
|
<div class="form-section">
|
|
<div class="form-section-title">Linki</div>
|
|
|
|
<div class="form-row single">
|
|
<div class="form-group">
|
|
<label>Link afiliacyjny <span class="required">*</span></label>
|
|
<input type="url" name="affiliate_url" value="{{ benefit.affiliate_url if benefit else '' }}" required>
|
|
<span class="hint">Link przez który śledzimy polecenia</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Link do strony produktu</label>
|
|
<input type="url" name="product_url" value="{{ benefit.product_url if benefit else '' }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>URL logo</label>
|
|
<input type="url" name="logo_url" value="{{ benefit.logo_url if benefit else '' }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if current_user.email == 'maciej.pienczyn@inpi.pl' %}
|
|
<!-- Prowizja (tylko dla właściciela) -->
|
|
<div class="form-section" style="background: #fffbeb; border: 1px solid #fde68a;">
|
|
<div class="form-section-title" style="color: #92400e;">
|
|
Dane prowizji (prywatne)
|
|
<span style="font-size: var(--font-size-xs); font-weight: normal; margin-left: 8px;">
|
|
<svg style="width: 14px; height: 14px; vertical-align: middle;" 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>
|
|
Widoczne tylko dla Ciebie
|
|
</span>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Wysokość prowizji</label>
|
|
<input type="text" name="commission_rate" value="{{ benefit.commission_rate if benefit else '' }}" placeholder="np. 25%">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Okres prowizji</label>
|
|
<input type="text" name="commission_duration" value="{{ benefit.commission_duration if benefit else '' }}" placeholder="np. 12 miesięcy">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>Platforma partnerska</label>
|
|
<input type="text" name="partner_platform" value="{{ benefit.partner_platform if benefit else '' }}" placeholder="np. Dub Partners">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Partner od</label>
|
|
<input type="date" name="partner_since" value="{{ benefit.partner_since.strftime('%Y-%m-%d') if benefit and benefit.partner_since else '' }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Status -->
|
|
<div class="form-section">
|
|
<div class="form-section-title">Status</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" name="is_active" id="is_active" {% if not benefit or benefit.is_active %}checked{% endif %}>
|
|
<label for="is_active">Aktywna (widoczna na stronie)</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" name="is_featured" id="is_featured" {% if benefit and benefit.is_featured %}checked{% endif %}>
|
|
<label for="is_featured">Polecana (wyróżniona)</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<a href="{{ url_for('admin.admin_benefits') }}" class="btn btn-secondary">Anuluj</a>
|
|
<button type="submit" class="btn btn-primary">
|
|
{% if benefit %}Zapisz zmiany{% else %}Dodaj korzyść{% endif %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|