nordabiz/templates/admin/social_publisher_form.html
Maciej Pienczyn af745cfb60
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: replace unpublish with withdraw (FB API doesn't support unpublishing)
Facebook Graph API returns error 100 when setting is_published=false on
already-published posts. Replaced "Zmień na debug" with "Usuń z Facebooka"
which deletes the post from FB and resets status to draft for re-publishing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 10:50:10 +01:00

762 lines
30 KiB
HTML

{% extends "base.html" %}
{% block title %}{% if post %}Edycja Posta #{{ post.id }}{% else %}Nowy Post{% endif %} - Social Publisher{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.form-section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
max-width: 900px;
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-xs);
font-weight: 500;
color: var(--text-primary);
}
.form-group input[type="text"],
.form-group input[type="datetime-local"],
.form-group select,
.form-group textarea {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: var(--font-size-base);
font-family: inherit;
}
.form-group textarea {
min-height: 120px;
resize: vertical;
}
.form-group textarea.content-editor {
min-height: 250px;
}
.form-hint {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-xs);
border-bottom: 1px solid var(--border);
}
.btn-group {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-xl);
flex-wrap: wrap;
}
.btn-generate {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
cursor: pointer;
}
.btn-generate:hover {
opacity: 0.9;
}
.btn-generate:disabled {
opacity: 0.6;
cursor: wait;
}
.btn-generate-sm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
cursor: pointer;
font-size: var(--font-size-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
}
.btn-generate-sm:hover {
opacity: 0.9;
}
.btn-generate-sm:disabled {
opacity: 0.6;
cursor: wait;
}
.ai-engine-info {
font-size: 11px;
color: var(--text-tertiary, #9ca3af);
margin-top: var(--spacing-xs);
line-height: 1.4;
}
.hashtag-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
}
.status-info {
background: var(--background);
padding: var(--spacing-md);
border-radius: var(--radius);
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-sm);
}
.status-info strong {
color: var(--primary);
}
.engagement-box {
background: var(--background);
padding: var(--spacing-lg);
border-radius: var(--radius);
margin-top: var(--spacing-lg);
}
.engagement-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.engagement-item {
text-align: center;
}
.engagement-item .value {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--primary);
}
.engagement-item .label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.d-none {
display: none !important;
}
.char-counter {
font-size: var(--font-size-xs);
color: var(--text-secondary);
text-align: right;
margin-top: var(--spacing-xs);
}
.char-counter.warning { color: var(--warning); }
.char-counter.over { color: var(--error); }
.fb-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
color: #1877f2;
font-weight: 500;
text-decoration: none;
}
.fb-link:hover {
text-decoration: underline;
}
.visibility-status {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 600;
}
.visibility-status.live {
background: #dcfce7;
color: #166534;
}
.visibility-status.debug {
background: #fef9c3;
color: #854d0e;
}
.confirm-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1050;
align-items: center;
justify-content: center;
}
.confirm-overlay.active {
display: flex;
}
.confirm-box {
max-width: 440px;
width: 90%;
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
}
.confirm-icon {
font-size: 2.5em;
margin-bottom: var(--spacing-md);
}
.confirm-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.confirm-message {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
margin-bottom: var(--spacing-lg);
}
.confirm-actions {
display: flex;
gap: var(--spacing-sm);
justify-content: center;
}
.confirm-actions .btn {
min-width: 120px;
}
.btn-danger {
background: #dc2626;
color: white;
border: none;
}
.btn-danger:hover {
background: #b91c1c;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-header">
<h1>{% if post %}Edycja Posta #{{ post.id }}{% else %}Nowy Post{% endif %}</h1>
<a href="{{ url_for('admin.social_publisher_list') }}" class="btn btn-secondary">Powrot do listy</a>
</div>
{% if post %}
<div class="status-info">
<strong>Status:</strong>
{% if post.status == 'draft' %}Szkic
{% elif post.status == 'approved' %}Zatwierdzony
{% elif post.status == 'scheduled' %}Zaplanowany
{% elif post.status == 'published' %}Opublikowany
{% elif post.status == 'failed' %}Błąd
{% else %}{{ post.status }}{% endif %}
{% if post.creator %}
| <strong>Autor:</strong> {{ post.creator.name }}
{% endif %}
{% if post.ai_model %}
| <strong>Model AI:</strong> {{ post.ai_model }}
{% endif %}
{% if post.published_at %}
| <strong>Opublikowano:</strong> {{ post.published_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
{% if post.status == 'published' and post.meta_post_id %}
|
{% if post.is_live %}
<span class="visibility-status live">Publiczny</span>
{% else %}
<span class="visibility-status debug">Debug (tylko admin)</span>
{% endif %}
{% endif %}
</div>
{% endif %}
<div class="form-section">
<form method="POST" id="postForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- Publikuj jako (firma z konfiguracją FB) -->
{% if configured_companies %}
<div class="form-group">
<label for="publishing_company_id">Publikuj jako (strona FB)</label>
<select id="publishing_company_id" name="publishing_company_id">
<option value="">-- Bez publikacji na FB --</option>
{% for cc in configured_companies %}
<option value="{{ cc.company_id }}" {% if post and post.publishing_company_id == cc.company_id %}selected{% endif %}>
{{ cc.company_name }} ({{ cc.page_name }}){% if cc.debug_mode %} [DEBUG]{% endif %}
</option>
{% endfor %}
</select>
<p class="form-hint">Strona Facebook, na ktorej zostanie opublikowany post</p>
</div>
{% endif %}
<!-- Typ posta -->
<div class="form-group">
<label for="post_type">Typ posta</label>
<select id="post_type" name="post_type" required>
<option value="">-- Wybierz typ --</option>
{% for key, label in post_types.items() %}
<option value="{{ key }}" {% if post and post.post_type == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<!-- Firma (widoczna dla member_spotlight) -->
<div class="form-group" id="company-field">
<label for="company_id">Firma</label>
<select id="company_id" name="company_id">
<option value="">-- Wybierz firmę --</option>
{% for company in companies %}
<option value="{{ company.id }}" {% if post and post.company_id == company.id %}selected{% endif %}>{{ company.name }}</option>
{% endfor %}
</select>
<p class="form-hint">Firma, którą chcesz zaprezentować w poście</p>
</div>
<!-- Wydarzenie (widoczne dla event_*) -->
<div class="form-group" id="event-field">
<label for="event_id">Wydarzenie</label>
<select id="event_id" name="event_id">
<option value="">-- Wybierz wydarzenie --</option>
{% for event in events %}
<option value="{{ event.id }}" {% if post and post.event_id == event.id %}selected{% endif %}>{{ event.title }}</option>
{% endfor %}
</select>
<p class="form-hint">Wydarzenie powiązane z postem</p>
</div>
<!-- Kontekst dodatkowy (dla regional_news/chamber_news) -->
<div class="form-group" id="custom-context-field">
<h3 class="section-title">Kontekst dla AI</h3>
<div class="form-group">
<label for="custom_topic">Temat</label>
<input type="text" id="custom_topic" name="custom_topic" placeholder="np. Nowa inwestycja w porcie Gdynia">
</div>
<div class="form-group">
<label for="custom_details">Szczegoly</label>
<textarea id="custom_details" name="custom_details" rows="3" placeholder="Dodatkowe informacje, które AI powinno uwzględnić..."></textarea>
</div>
<div class="form-group">
<label for="custom_facts">Fakty/dane</label>
<input type="text" id="custom_facts" name="custom_facts" placeholder="np. Wartosc: 50 mln PLN, termin: Q3 2026">
</div>
<div class="form-group">
<label for="custom_source">Zrodlo</label>
<input type="text" id="custom_source" name="custom_source" placeholder="np. Portal Morski, komunikat prasowy">
</div>
</div>
<!-- Tonacja + Model + Przycisk generowania AI -->
<div class="form-group" style="display: flex; align-items: center; justify-content: flex-end; gap: var(--spacing-sm); flex-wrap: wrap;">
<label for="tone" style="margin: 0; white-space: nowrap;">Tonacja:</label>
<select id="tone" name="tone" style="width: auto; min-width: 160px; padding: var(--spacing-xs) var(--spacing-sm); border: 1px solid var(--border); border-radius: var(--radius-md); font-size: var(--font-size-sm);">
{% for key, tone in post_tones.items() %}
<option value="{{ key }}" {% if key == default_tone %}selected{% endif %}>{{ tone.label }}</option>
{% endfor %}
</select>
<label for="ai_model" style="margin: 0; white-space: nowrap;">Model:</label>
<select id="ai_model" name="ai_model" style="width: auto; min-width: 170px; padding: var(--spacing-xs) var(--spacing-sm); border: 1px solid var(--border); border-radius: var(--radius-md); font-size: var(--font-size-sm);">
{% for key, model in ai_models.items() %}
<option value="{{ key }}" {% if key == default_ai_model %}selected{% endif %} title="{{ model.description }}">{{ model.label }}</option>
{% endfor %}
</select>
<button type="button" id="btn-generate-ai" class="btn btn-generate">
Generuj AI
</button>
</div>
<div id="ai-error-msg" style="display:none; padding: var(--spacing-sm) var(--spacing-md); margin-bottom: var(--spacing-md); border-radius: var(--radius); background: var(--error-bg, #fef2f2); color: var(--error, #dc2626); border: 1px solid var(--error, #dc2626); font-size: var(--font-size-sm);"></div>
<!-- Tresc -->
<div class="form-group">
<label for="content">Tresc posta</label>
<textarea id="content" name="content" class="content-editor" required
placeholder="Wpisz tresc posta lub wygeneruj za pomoca AI...">{{ post.content if post else '' }}</textarea>
<div class="char-counter" id="content-counter">0 znaków</div>
</div>
<!-- Hashtagi -->
<div class="form-group">
<div class="hashtag-header">
<label for="hashtags">Hashtagi</label>
<button type="button" id="btn-generate-hashtags" class="btn-generate-sm">
Generuj hashtagi AI
</button>
</div>
<input type="text" id="hashtags" name="hashtags"
value="{{ post.hashtags if post else '' }}"
placeholder="#NordaBiznes #Wejherowo #Pomorze">
<p class="form-hint">Hashtagi oddzielone spacjami</p>
<div id="hashtag-error-msg" style="display:none; padding: var(--spacing-xs) var(--spacing-sm); margin-top: var(--spacing-xs); border-radius: var(--radius); background: var(--error-bg, #fef2f2); color: var(--error, #dc2626); border: 1px solid var(--error, #dc2626); font-size: var(--font-size-xs);"></div>
</div>
<!-- Info o silniku AI -->
<div class="ai-engine-info">
Silnik AI: <strong>Google Gemini</strong>. Domyslny model (Gemini 3 Flash) jest szybki i tani.
Dla lepszej jakosci wybierz Gemini 3 Pro — lepsza kreatywnosc i styl, ale ~4x wyzszy koszt.
<span id="ai-model-used" style="display:none;"> Ostatnio uzyty: <strong id="ai-model-used-name"></strong></span>
</div>
<!-- Akcje -->
<div class="btn-group">
<a href="{{ url_for('admin.social_publisher_list') }}" class="btn btn-secondary">Anuluj</a>
{% set is_debug = configured_companies and configured_companies|selectattr('debug_mode')|list|length > 0 %}
{% if post %}
<button type="submit" name="action" value="save" class="btn btn-primary">Zapisz zmiany</button>
{% if post.status == 'draft' %}
<button type="submit" name="action" value="approve" class="btn btn-info" style="background: #0ea5e9; color: white; border: none;">Zatwierdz</button>
{% endif %}
{% if post.status in ['draft', 'approved'] %}
<button type="submit" name="action" value="publish" class="btn btn-success">Publikuj teraz{% if is_debug %} (debug){% endif %}</button>
{% if is_debug %}
<button type="button" class="btn btn-danger"
onclick="showConfirm({icon: '🌐', title: 'Publikacja na żywo', message: 'Post zostanie opublikowany publicznie i będzie widoczny dla wszystkich użytkowników Facebooka.', okText: 'Publikuj na żywo', okClass: 'btn btn-danger', submitAction: 'publish_live'});">
Publikuj na żywo
</button>
{% endif %}
{% endif %}
{% if post.status != 'published' %}
<button type="button" class="btn btn-error"
onclick="showConfirm({icon: '🗑️', title: 'Usuń post', message: 'Post zostanie trwale usunięty. Tej operacji nie można cofnąć.', okText: 'Usuń', okClass: 'btn btn-error', submitAction: 'delete'});">Usuń</button>
{% endif %}
{% else %}
<button type="submit" name="action" value="draft" class="btn btn-secondary">Zapisz szkic</button>
<button type="submit" name="action" value="publish" class="btn btn-success">Publikuj teraz{% if is_debug %} (debug){% endif %}</button>
{% if is_debug %}
<button type="button" class="btn btn-danger"
onclick="showConfirm({icon: '🌐', title: 'Publikacja na żywo', message: 'Post zostanie opublikowany publicznie i będzie widoczny dla wszystkich użytkowników Facebooka.', okText: 'Publikuj na żywo', okClass: 'btn btn-danger', submitAction: 'publish_live'});">
Publikuj na żywo
</button>
{% endif %}
{% endif %}
</div>
</form>
{% if post and post.status == 'published' and post.meta_post_id %}
<!-- Visibility controls (outside postForm to avoid nested forms) -->
<div class="btn-group" style="margin-top: var(--spacing-md);">
<strong style="align-self: center;">Widoczność na FB:</strong>
{% if post.is_live %}
<form method="POST" id="withdrawForm" action="{{ url_for('admin.social_publisher_withdraw', post_id=post.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="button" class="btn btn-error"
onclick="showConfirm({icon: '🗑️', title: 'Usuń z Facebooka', message: 'Post zostanie usunięty z Facebooka i przywrócony do szkicu. Będziesz mógł go ponownie opublikować.', okText: 'Usuń z FB', okClass: 'btn btn-error', form: 'withdrawForm'});">
Usuń z Facebooka
</button>
</form>
{% else %}
<form method="POST" id="toggleVisibilityForm" action="{{ url_for('admin.social_publisher_toggle_visibility', post_id=post.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="button" class="btn btn-danger"
onclick="showConfirm({icon: '🌐', title: 'Opublikuj publicznie', message: 'Post zostanie upubliczniony i będzie widoczny dla wszystkich użytkowników Facebooka.', okText: 'Opublikuj', okClass: 'btn btn-danger', form: 'toggleVisibilityForm'});">
Opublikuj publicznie
</button>
</form>
{% endif %}
</div>
{% endif %}
{% if post and post.status == 'published' %}
<!-- Engagement metrics -->
<div class="engagement-box">
<h3 class="section-title" style="margin-top: 0;">Engagement</h3>
<div class="engagement-grid">
<div class="engagement-item">
<div class="value">{{ post.engagement_likes or 0 }}</div>
<div class="label">Polubienia</div>
</div>
<div class="engagement-item">
<div class="value">{{ post.engagement_comments or 0 }}</div>
<div class="label">Komentarze</div>
</div>
<div class="engagement-item">
<div class="value">{{ post.engagement_shares or 0 }}</div>
<div class="label">Udostepnienia</div>
</div>
<div class="engagement-item">
<div class="value">{{ post.engagement_reach or 0 }}</div>
<div class="label">Zasieg</div>
</div>
</div>
<div style="display: flex; gap: var(--spacing-md); align-items: center; flex-wrap: wrap;">
<form method="POST" action="{{ url_for('admin.social_publisher_refresh_engagement', post_id=post.id) }}" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-secondary btn-small">Odswiez engagement</button>
</form>
{% if post.meta_post_id %}
<a href="https://www.facebook.com/{{ post.meta_post_id }}" target="_blank" class="fb-link">
Zobacz post na Facebook &#8599;
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Confirm modal -->
<div class="confirm-overlay" id="confirmOverlay">
<div class="confirm-box">
<div class="confirm-icon" id="confirmIcon"></div>
<div class="confirm-title" id="confirmTitle"></div>
<div class="confirm-message" id="confirmMessage"></div>
<div class="confirm-actions">
<button type="button" class="btn btn-secondary" id="confirmCancel">Anuluj</button>
<button type="button" class="btn" id="confirmOk">OK</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
function showAiError(msg) {
const el = document.getElementById('ai-error-msg');
el.textContent = msg;
el.style.display = 'block';
}
function hideAiError() {
document.getElementById('ai-error-msg').style.display = 'none';
}
// AI generation
document.getElementById('btn-generate-ai')?.addEventListener('click', async function() {
const postType = document.getElementById('post_type').value;
const companyId = document.getElementById('company_id')?.value;
const eventId = document.getElementById('event_id')?.value;
const tone = document.getElementById('tone')?.value || '';
const aiModel = document.getElementById('ai_model')?.value || '';
if (!postType) {
showAiError('Wybierz typ posta przed generowaniem.');
return;
}
const customContext = {};
const topicEl = document.getElementById('custom_topic');
if (topicEl) customContext.topic = topicEl.value;
const detailsEl = document.getElementById('custom_details');
if (detailsEl) customContext.details = detailsEl.value;
const factsEl = document.getElementById('custom_facts');
if (factsEl) customContext.facts = factsEl.value;
const sourceEl = document.getElementById('custom_source');
if (sourceEl) customContext.source = sourceEl.value;
this.disabled = true;
this.textContent = 'Generowanie...';
try {
const resp = await fetch("{{ url_for('admin.social_publisher_generate') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrf_token]')?.value || ''
},
body: JSON.stringify({
post_type: postType,
company_id: companyId || null,
event_id: eventId || null,
publishing_company_id: document.getElementById('publishing_company_id')?.value || null,
tone: tone,
ai_model: aiModel,
custom_context: customContext
})
});
const data = await resp.json();
if (data.success) {
document.getElementById('content').value = data.content;
if (data.hashtags) {
document.getElementById('hashtags').value = data.hashtags;
}
updateContentCounter();
hideAiError();
if (data.model) {
document.getElementById('ai-model-used-name').textContent = data.model;
document.getElementById('ai-model-used').style.display = 'inline';
}
} else {
showAiError(data.error || 'Nie udalo sie wygenerowac tresci. Sprobuj ponownie.');
}
} catch (err) {
showAiError('Blad polaczenia z serwerem. Sprawdz polaczenie internetowe i sprobuj ponownie.');
} finally {
this.disabled = false;
this.textContent = 'Generuj AI';
}
});
// Hashtag generation
document.getElementById('btn-generate-hashtags')?.addEventListener('click', async function() {
const content = document.getElementById('content').value.trim();
const errEl = document.getElementById('hashtag-error-msg');
if (!content) {
errEl.textContent = 'Wpisz najpierw tresc posta, aby wygenerowac hashtagi.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
this.disabled = true;
this.textContent = 'Generowanie...';
try {
const resp = await fetch("{{ url_for('admin.social_publisher_generate_hashtags') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrf_token]')?.value || ''
},
body: JSON.stringify({
content: content,
post_type: document.getElementById('post_type')?.value || '',
ai_model: document.getElementById('ai_model')?.value || ''
})
});
const data = await resp.json();
if (data.success) {
document.getElementById('hashtags').value = data.hashtags;
errEl.style.display = 'none';
} else {
errEl.textContent = data.error || 'Nie udalo sie wygenerowac hashtagow.';
errEl.style.display = 'block';
}
} catch (err) {
errEl.textContent = 'Blad polaczenia z serwerem.';
errEl.style.display = 'block';
} finally {
this.disabled = false;
this.textContent = 'Generuj hashtagi AI';
}
});
// Show/hide context fields based on post type
document.getElementById('post_type')?.addEventListener('change', function() {
const type = this.value;
document.getElementById('company-field')?.classList.toggle('d-none', type !== 'member_spotlight');
document.getElementById('event-field')?.classList.toggle('d-none', !type.startsWith('event_'));
document.getElementById('custom-context-field')?.classList.toggle('d-none',
!['regional_news', 'chamber_news'].includes(type));
});
// Trigger on load
document.getElementById('post_type')?.dispatchEvent(new Event('change'));
// Character counter
function updateContentCounter() {
const content = document.getElementById('content');
const counter = document.getElementById('content-counter');
if (content && counter) {
const len = content.value.length;
counter.textContent = len + ' znaków';
counter.classList.remove('warning', 'over');
if (len > 2000) counter.classList.add('over');
else if (len > 1500) counter.classList.add('warning');
}
}
document.getElementById('content')?.addEventListener('input', updateContentCounter);
updateContentCounter();
// Custom confirm modal
let confirmCallback = null;
function showConfirm(opts) {
document.getElementById('confirmIcon').textContent = opts.icon || '❓';
document.getElementById('confirmTitle').textContent = opts.title || 'Potwierdzenie';
document.getElementById('confirmMessage').textContent = opts.message || '';
const okBtn = document.getElementById('confirmOk');
okBtn.textContent = opts.okText || 'OK';
okBtn.className = opts.okClass || 'btn btn-primary';
if (opts.form) {
confirmCallback = function() {
document.getElementById(opts.form).submit();
};
} else if (opts.submitAction) {
confirmCallback = function() {
const form = document.getElementById('postForm');
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'action';
hidden.value = opts.submitAction;
form.appendChild(hidden);
form.submit();
};
}
document.getElementById('confirmOverlay').classList.add('active');
}
function hideConfirm() {
document.getElementById('confirmOverlay').classList.remove('active');
confirmCallback = null;
}
document.getElementById('confirmOk')?.addEventListener('click', function() {
if (confirmCallback) confirmCallback();
hideConfirm();
});
document.getElementById('confirmCancel')?.addEventListener('click', hideConfirm);
document.getElementById('confirmOverlay')?.addEventListener('click', function(e) {
if (e.target === this) hideConfirm();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') hideConfirm();
});
{% endblock %}