From 6e00291a883ca86ca02208d552983c57b7fa9ae9 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Sun, 11 Jan 2026 10:30:35 +0100 Subject: [PATCH] feat: AI usage user details + styled modals across app - Add /admin/ai-usage/user/ route for detailed AI usage per user - Add ai_usage_user.html template with stats, usage breakdown, logs - Make user names clickable in AI usage dashboard ranking - Replace all native browser dialogs (alert, confirm) with styled modals/toasts: - admin/fees.html, forum.html, recommendations.html, announcements.html, debug.html - calendar/admin.html, event.html - company_detail.html, company/recommend.html - forum/new_topic.html, topic.html - classifieds/view.html - auth/reset_password.html Co-Authored-By: Claude Opus 4.5 --- app.py | 115 +++++ templates/admin/ai_usage_dashboard.html | 27 +- templates/admin/ai_usage_user.html | 549 ++++++++++++++++++++++++ templates/admin/announcements.html | 97 ++++- templates/admin/debug.html | 50 ++- templates/admin/fees.html | 177 +++++--- templates/admin/forum.html | 89 +++- templates/admin/recommendations.html | 73 +++- templates/auth/reset_password.html | 20 +- templates/calendar/admin.html | 74 +++- templates/calendar/event.html | 28 +- templates/classifieds/view.html | 76 +++- templates/company/recommend.html | 24 +- templates/company_detail.html | 112 +++-- templates/forum/new_topic.html | 23 +- templates/forum/topic.html | 23 +- 16 files changed, 1421 insertions(+), 136 deletions(-) create mode 100644 templates/admin/ai_usage_user.html diff --git a/app.py b/app.py index 6ddb601..ffbedc8 100644 --- a/app.py +++ b/app.py @@ -5953,6 +5953,121 @@ def admin_ai_usage(): db.close() +@app.route('/admin/ai-usage/user/') +@login_required +def admin_ai_usage_user(user_id): + """Detailed AI usage for a specific user""" + if not current_user.is_admin: + flash('Brak uprawnień do tej strony.', 'error') + return redirect(url_for('dashboard')) + + from database import AIUsageLog, User, Company + from sqlalchemy import func, desc + + db = SessionLocal() + try: + # Get user info + user = db.query(User).filter_by(id=user_id).first() + if not user: + flash('Użytkownik nie istnieje.', 'error') + return redirect(url_for('admin_ai_usage')) + + company = None + if user.company_id: + company = db.query(Company).filter_by(id=user.company_id).first() + + # Get overall stats for this user + stats = db.query( + func.count(AIUsageLog.id).label('total_requests'), + func.coalesce(func.sum(AIUsageLog.tokens_input), 0).label('tokens_input'), + func.coalesce(func.sum(AIUsageLog.tokens_output), 0).label('tokens_output'), + func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents'), + func.count(func.nullif(AIUsageLog.success, True)).label('errors') + ).filter(AIUsageLog.user_id == user_id).first() + + # Usage by type + type_labels = { + 'ai_chat': 'Chat AI', + 'zopk_news_evaluation': 'Ocena newsów ZOPK', + 'ai_user_parse': 'Tworzenie user', + 'gbp_audit_ai': 'Audyt GBP', + 'general': 'Ogólne' + } + + type_stats = db.query( + AIUsageLog.request_type, + func.count(AIUsageLog.id).label('count'), + func.coalesce(func.sum(AIUsageLog.tokens_input + AIUsageLog.tokens_output), 0).label('tokens'), + func.coalesce(func.sum(AIUsageLog.cost_cents), 0).label('cost_cents') + ).filter( + AIUsageLog.user_id == user_id + ).group_by(AIUsageLog.request_type).order_by(desc('count')).all() + + # Calculate total for percentages + total_type_count = sum(t.count for t in type_stats) if type_stats else 1 + + type_classes = { + 'ai_chat': 'chat', + 'zopk_news_evaluation': 'news_evaluation', + 'ai_user_parse': 'user_creation', + 'gbp_audit_ai': 'image_analysis', + 'general': 'other' + } + + usage_by_type = [] + for t in type_stats: + usage_by_type.append({ + 'type': t.request_type, + 'type_label': type_labels.get(t.request_type, t.request_type), + 'type_class': type_classes.get(t.request_type, 'other'), + 'count': t.count, + 'tokens': int(t.tokens), + 'cost_usd': float(t.cost_cents) / 100, + 'percentage': round(t.count / total_type_count * 100, 1) if total_type_count > 0 else 0 + }) + + # Get all requests for this user (paginated) + page = request.args.get('page', 1, type=int) + per_page = 50 + + requests_query = db.query(AIUsageLog).filter( + AIUsageLog.user_id == user_id + ).order_by(desc(AIUsageLog.created_at)) + + total_requests = requests_query.count() + total_pages = (total_requests + per_page - 1) // per_page + + logs = requests_query.offset((page - 1) * per_page).limit(per_page).all() + + # Enrich logs with type labels and cost + for log in logs: + log.type_label = type_labels.get(log.request_type, log.request_type) + log.cost_usd = float(log.cost_cents or 0) / 100 + + user_stats = { + 'total_requests': stats.total_requests or 0, + 'tokens_total': int(stats.tokens_input or 0) + int(stats.tokens_output or 0), + 'tokens_input': int(stats.tokens_input or 0), + 'tokens_output': int(stats.tokens_output or 0), + 'cost_usd': float(stats.cost_cents or 0) / 100, + 'errors': stats.errors or 0 + } + + return render_template( + 'admin/ai_usage_user.html', + user=user, + company=company, + stats=user_stats, + usage_by_type=usage_by_type, + logs=logs, + page=page, + total_pages=total_pages, + total_requests=total_requests + ) + finally: + db.close() + + @app.route('/api/admin/chat-stats') @login_required def api_chat_stats(): diff --git a/templates/admin/ai_usage_dashboard.html b/templates/admin/ai_usage_dashboard.html index 1fae0a8..68d5898 100644 --- a/templates/admin/ai_usage_dashboard.html +++ b/templates/admin/ai_usage_dashboard.html @@ -240,6 +240,23 @@ color: var(--text-secondary); } + .user-info-link { + text-decoration: none; + display: block; + padding: var(--spacing-xs); + margin: calc(-1 * var(--spacing-xs)); + border-radius: var(--radius-sm); + transition: var(--transition); + } + + .user-info-link:hover { + background: var(--primary-light, #eff6ff); + } + + .user-info-link:hover .user-name { + color: var(--primary); + } + .cost-badge { display: inline-block; padding: 2px 8px; @@ -507,10 +524,12 @@ {{ loop.index }} - + {{ user.requests }} {{ "{:,}".format(user.tokens) }} diff --git a/templates/admin/ai_usage_user.html b/templates/admin/ai_usage_user.html new file mode 100644 index 0000000..455f891 --- /dev/null +++ b/templates/admin/ai_usage_user.html @@ -0,0 +1,549 @@ +{% extends "base.html" %} + +{% block title %}AI - {{ user.name or user.email }} - Panel Admina{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + + + +
+
+ {{ (user.name or user.email)[0].upper() }} +
+
+
{{ user.name or 'Brak nazwy' }}
+
{{ user.email }}
+ {% if company %} + + {{ company.name }} + + {% endif %} +
+
+ + +
+
+
{{ stats.total_requests }}
+
Zapytan
+
+
+
{{ "{:,}".format(stats.tokens_input) }}
+
Tokenow input
+
+
+
{{ "{:,}".format(stats.tokens_output) }}
+
Tokenow output
+
+
+
${{ "%.4f"|format(stats.cost_usd) }}
+
Calkowity koszt
+
+
+
{{ stats.errors }}
+
Bledow
+
+
+ + +
+
+

📊 Wykorzystanie wg typu

+ {% if usage_by_type %} +
+ {% for item in usage_by_type %} +
+
{{ item.type_label }}
+
+
+ {{ item.percentage }}% +
+
+
{{ item.count }}
+
${{ "%.4f"|format(item.cost_usd) }}
+
+ {% endfor %} +
+ {% else %} +
+
📭
+

Brak danych

+
+ {% endif %} +
+ +
+

💡 Podsumowanie

+
+

+ {{ user.name or user.email.split('@')[0] }} wykonal/a {{ stats.total_requests }} zapytan do AI, + wykorzystujac lacznie {{ "{:,}".format(stats.tokens_input + stats.tokens_output) }} tokenow. +

+ {% if usage_by_type %} +

+ Najczesciej uzywana funkcja: {{ usage_by_type[0].type_label }} ({{ usage_by_type[0].count }} zapytan). +

+ {% endif %} +

+ Calkowity koszt uzycia AI: ${{ "%.4f"|format(stats.cost_usd) }} +

+
+
+
+ + +
+

+ 📜 Historia zapytan + + {{ total_requests }} zapytan lacznie + +

+ + {% if logs %} + + + + + + + + + + + + + {% for log in logs %} + + + + + + + + + {% endfor %} + +
TypModelTokeny (in/out)KosztStatusData
+ {{ log.type_label }} + + {{ log.model or '-' }} + + {{ log.tokens_input or 0 }} / {{ log.tokens_output or 0 }} + + ${{ "%.5f"|format(log.cost_usd) }} + + {% if log.success %} + OK + {% else %} + Blad + {% if log.error_message %} +
{{ log.error_message[:50] }}...
+ {% endif %} + {% endif %} +
+ {{ log.created_at.strftime('%d.%m.%Y %H:%M') }} +
+ + + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+
📭
+

Brak zapytan AI dla tego uzytkownika

+
+ {% endif %} +
+{% endblock %} diff --git a/templates/admin/announcements.html b/templates/admin/announcements.html index 1d32586..2c1f6cd 100755 --- a/templates/admin/announcements.html +++ b/templates/admin/announcements.html @@ -165,30 +165,95 @@ {% endif %} + + + + +
+ + {% endblock %} {% block extra_js %} const now = new Date(); + let confirmResolve = null; - function deleteAnnouncement(id) { - if (!confirm('Czy na pewno chcesz usunac to ogloszenie?')) { - return; - } + function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); + } - fetch('/admin/announcements/' + id + '/delete', { - method: 'POST', - headers: { - 'X-CSRFToken': '{{ csrf_token() }}' - } - }) - .then(response => response.json()) - .then(data => { + function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } + } + + document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); + document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); + document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); + + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } + + async function deleteAnnouncement(id) { + const confirmed = await showConfirm('Czy na pewno chcesz usunąć to ogłoszenie?', { + icon: '🗑️', + title: 'Usuwanie ogłoszenia', + okText: 'Usuń', + okClass: 'btn-error' + }); + if (!confirmed) return; + + try { + const response = await fetch('/admin/announcements/' + id + '/delete', { + method: 'POST', + headers: { + 'X-CSRFToken': '{{ csrf_token() }}' + } + }); + const data = await response.json(); if (data.success) { - location.reload(); + showToast('Ogłoszenie zostało usunięte', 'success'); + setTimeout(() => location.reload(), 1500); } else { - alert('Blad: ' + data.error); + showToast('Błąd: ' + data.error, 'error'); } - }) - .catch(err => alert('Blad: ' + err)); + } catch (err) { + showToast('Błąd: ' + err, 'error'); + } } {% endblock %} diff --git a/templates/admin/debug.html b/templates/admin/debug.html index 39d5321..4559c0b 100755 --- a/templates/admin/debug.html +++ b/templates/admin/debug.html @@ -210,9 +210,51 @@ Oczekiwanie na logi... Wykonaj jakąś akcję na stronie. + + + + + {% endblock %} {% block extra_js %} + let confirmResolve = null; + + function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); + } + + function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } + } + + document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); + document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); + document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); const logContainer = document.getElementById('logContainer'); const emptyMessage = document.getElementById('emptyMessage'); const statusIndicator = document.getElementById('statusIndicator'); @@ -354,7 +396,13 @@ // Clear logs async function clearLogs() { - if (!confirm('Wyczyścić wszystkie logi?')) return; + const confirmed = await showConfirm('Czy na pewno chcesz wyczyścić wszystkie logi?', { + icon: '🗑️', + title: 'Czyszczenie logów', + okText: 'Wyczyść', + okClass: 'btn-danger' + }); + if (!confirmed) return; try { const response = await fetch('/api/admin/logs/clear', { diff --git a/templates/admin/fees.html b/templates/admin/fees.html index 536a276..54cb0a2 100755 --- a/templates/admin/fees.html +++ b/templates/admin/fees.html @@ -443,35 +443,101 @@ + + + + +
+ + {% endblock %} {% block extra_js %} - function generateFees() { - if (!confirm('Czy na pewno chcesz wygenerowac rekordy skladek dla wszystkich firm na wybrany miesiac?')) { - return; - } +// Modal system +let confirmResolve = null; +function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); +} +function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } +} +document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); +document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); +document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); + +function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); +} + +async function generateFees() { + const confirmed = await showConfirm('Czy na pewno chcesz wygenerować rekordy składek dla wszystkich firm na wybrany miesiąc?', { + icon: '📋', + title: 'Generowanie składek', + okText: 'Generuj', + okClass: 'btn-success' + }); + if (!confirmed) return; const formData = new FormData(); formData.append('year', {{ year }}); formData.append('month', {{ month or 'null' }}); - fetch('{{ url_for("admin_fees_generate") }}', { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': '{{ csrf_token() }}' - } - }) - .then(response => response.json()) - .then(data => { + try { + const response = await fetch('{{ url_for("admin_fees_generate") }}', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': '{{ csrf_token() }}' + } + }); + const data = await response.json(); if (data.success) { - alert(data.message); - location.reload(); + showToast(data.message, 'success'); + setTimeout(() => location.reload(), 1500); } else { - alert('Blad: ' + data.error); + showToast('Błąd: ' + data.error, 'error'); } - }) - .catch(err => alert('Blad: ' + err)); + } catch (err) { + showToast('Błąd: ' + err, 'error'); + } } function openPaymentModal(feeId, companyName, amount) { @@ -486,29 +552,31 @@ document.getElementById('paymentModal').classList.remove('active'); } - document.getElementById('paymentForm').addEventListener('submit', function(e) { + document.getElementById('paymentForm').addEventListener('submit', async function(e) { e.preventDefault(); const feeId = document.getElementById('modalFeeId').value; const formData = new FormData(this); - fetch('/admin/fees/' + feeId + '/mark-paid', { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': '{{ csrf_token() }}' - } - }) - .then(response => response.json()) - .then(data => { + try { + const response = await fetch('/admin/fees/' + feeId + '/mark-paid', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': '{{ csrf_token() }}' + } + }); + const data = await response.json(); if (data.success) { - alert(data.message); - location.reload(); + closePaymentModal(); + showToast(data.message, 'success'); + setTimeout(() => location.reload(), 1500); } else { - alert('Blad: ' + data.error); + showToast('Błąd: ' + data.error, 'error'); } - }) - .catch(err => alert('Blad: ' + err)); + } catch (err) { + showToast('Błąd: ' + err, 'error'); + } }); function toggleSelectAll() { @@ -517,37 +585,42 @@ checkboxes.forEach(cb => cb.checked = selectAll.checked); } - function bulkMarkPaid() { + async function bulkMarkPaid() { const checkboxes = document.querySelectorAll('.fee-checkbox:checked'); if (checkboxes.length === 0) { - alert('Zaznacz przynajmniej jedna skladke'); + showToast('Zaznacz przynajmniej jedną składkę', 'warning'); return; } - if (!confirm('Czy na pewno chcesz oznaczyc ' + checkboxes.length + ' skladek jako oplacone?')) { - return; - } + const confirmed = await showConfirm(`Czy na pewno chcesz oznaczyć ${checkboxes.length} składek jako opłacone?`, { + icon: '💰', + title: 'Oznaczanie płatności', + okText: 'Oznacz', + okClass: 'btn-success' + }); + if (!confirmed) return; const formData = new FormData(); checkboxes.forEach(cb => formData.append('fee_ids[]', cb.value)); - fetch('{{ url_for("admin_fees_bulk_mark_paid") }}', { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': '{{ csrf_token() }}' - } - }) - .then(response => response.json()) - .then(data => { + try { + const response = await fetch('{{ url_for("admin_fees_bulk_mark_paid") }}', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': '{{ csrf_token() }}' + } + }); + const data = await response.json(); if (data.success) { - alert(data.message); - location.reload(); + showToast(data.message, 'success'); + setTimeout(() => location.reload(), 1500); } else { - alert('Blad: ' + data.error); + showToast('Błąd: ' + data.error, 'error'); } - }) - .catch(err => alert('Blad: ' + err)); + } catch (err) { + showToast('Błąd: ' + err, 'error'); + } } // Close modal on outside click diff --git a/templates/admin/forum.html b/templates/admin/forum.html index 4061e97..3ac59f2 100755 --- a/templates/admin/forum.html +++ b/templates/admin/forum.html @@ -551,14 +551,71 @@ + + + +
+ + {% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; let currentTopicId = null; + let confirmResolve = null; + + function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); + } + + function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } + } + + document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); + document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); + document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); + + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } function showMessage(message, type) { - alert(message); + showToast(message, type === 'error' ? 'error' : 'success'); } // Status modal functions @@ -664,9 +721,13 @@ } async function deleteTopic(topicId, title) { - if (!confirm(`Czy na pewno chcesz usunac temat "${title}"?\n\nTa operacja usunie rowniez wszystkie odpowiedzi i jest nieodwracalna.`)) { - return; - } + const confirmed = await showConfirm(`Czy na pewno chcesz usunąć temat "${title}"?

Ta operacja usunie również wszystkie odpowiedzi i jest nieodwracalna.`, { + icon: '🗑️', + title: 'Usuwanie tematu', + okText: 'Usuń', + okClass: 'btn-danger' + }); + if (!confirmed) return; try { const response = await fetch(`/admin/forum/topic/${topicId}/delete`, { @@ -680,18 +741,23 @@ const data = await response.json(); if (data.success) { document.querySelector(`tr[data-topic-id="${topicId}"]`).remove(); + showToast('Temat został usunięty', 'success'); } else { - showMessage(data.error || 'Wystapil blad', 'error'); + showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { - showMessage('Blad polaczenia', 'error'); + showMessage('Błąd połączenia', 'error'); } } async function deleteReply(replyId) { - if (!confirm('Czy na pewno chcesz usunac te odpowiedz?')) { - return; - } + const confirmed = await showConfirm('Czy na pewno chcesz usunąć tę odpowiedź?', { + icon: '🗑️', + title: 'Usuwanie odpowiedzi', + okText: 'Usuń', + okClass: 'btn-danger' + }); + if (!confirmed) return; try { const response = await fetch(`/admin/forum/reply/${replyId}/delete`, { @@ -705,11 +771,12 @@ const data = await response.json(); if (data.success) { document.querySelector(`div[data-reply-id="${replyId}"]`).remove(); + showToast('Odpowiedź została usunięta', 'success'); } else { - showMessage(data.error || 'Wystapil blad', 'error'); + showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { - showMessage('Blad polaczenia', 'error'); + showMessage('Błąd połączenia', 'error'); } } {% endblock %} diff --git a/templates/admin/recommendations.html b/templates/admin/recommendations.html index abd4126..efb7f97 100755 --- a/templates/admin/recommendations.html +++ b/templates/admin/recommendations.html @@ -492,15 +492,72 @@ + + + + +
+ + {% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; let currentRecommendationId = null; + let confirmResolve = null; + + function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); + } + + function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } + } + + document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); + document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); + document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); + + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } function showMessage(message, type) { - // Simple alert for now - could be improved with toast notifications - alert(message); + showToast(message, type === 'error' ? 'error' : 'success'); } // Filter tabs functionality @@ -582,9 +639,13 @@ } async function deleteRecommendation(recommendationId, companyName) { - if (!confirm(`Czy na pewno chcesz usunąć rekomendację dla firmy "${companyName}"?\n\nTa operacja jest nieodwracalna.`)) { - return; - } + const confirmed = await showConfirm(`Czy na pewno chcesz usunąć rekomendację dla firmy "${companyName}"?

Ta operacja jest nieodwracalna.`, { + icon: '🗑️', + title: 'Usuwanie rekomendacji', + okText: 'Usuń', + okClass: 'btn-danger' + }); + if (!confirmed) return; try { const response = await fetch(`/api/recommendations/${recommendationId}/delete`, { @@ -598,7 +659,7 @@ const data = await response.json(); if (data.success) { document.querySelector(`tr[data-recommendation-id="${recommendationId}"]`).remove(); - showMessage('Rekomendacja została usunięta', 'success'); + showToast('Rekomendacja została usunięta', 'success'); } else { showMessage(data.error || 'Wystąpił błąd', 'error'); } diff --git a/templates/auth/reset_password.html b/templates/auth/reset_password.html index 3ba1bfc..b45835d 100755 --- a/templates/auth/reset_password.html +++ b/templates/auth/reset_password.html @@ -284,9 +284,27 @@ + +
+ {% endblock %} {% block extra_js %} + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { error: '✕', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } + const passwordInput = document.getElementById('password'); const passwordConfirm = document.getElementById('password_confirm'); const strengthBar = document.getElementById('strengthBar'); @@ -355,7 +373,7 @@ if (password !== confirm) { passwordConfirm.classList.add('error'); e.preventDefault(); - alert('Hasla nie sa identyczne'); + showToast('Hasła nie są identyczne', 'error'); } }); {% endblock %} diff --git a/templates/calendar/admin.html b/templates/calendar/admin.html index c593afc..c878de9 100755 --- a/templates/calendar/admin.html +++ b/templates/calendar/admin.html @@ -171,15 +171,78 @@ Dodaj wydarzenie {% endif %} + + + + +
+ + {% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; +let confirmResolve = null; + +function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); +} + +function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } +} + +document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); +document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); +document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); + +function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); +} async function deleteEvent(eventId, title) { - if (!confirm(`Czy na pewno chcesz usunac wydarzenie "${title}"?`)) { - return; - } + const confirmed = await showConfirm(`Czy na pewno chcesz usunąć wydarzenie "${title}"?`, { + icon: '🗑️', + title: 'Usuwanie wydarzenia', + okText: 'Usuń', + okClass: 'btn-danger' + }); + if (!confirmed) return; try { const response = await fetch(`/admin/kalendarz/${eventId}/delete`, { @@ -193,11 +256,12 @@ async function deleteEvent(eventId, title) { const data = await response.json(); if (data.success) { document.querySelector(`tr[data-event-id="${eventId}"]`).remove(); + showToast('Wydarzenie zostało usunięte', 'success'); } else { - alert(data.error || 'Wystapil blad'); + showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { - alert('Blad polaczenia'); + showToast('Błąd połączenia', 'error'); } } {% endblock %} diff --git a/templates/calendar/event.html b/templates/calendar/event.html index 20afdb4..37e1dc0 100755 --- a/templates/calendar/event.html +++ b/templates/calendar/event.html @@ -223,11 +223,31 @@ {% endif %} + +
+ + {% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; +function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); +} + async function toggleRSVP() { const btn = document.getElementById('rsvp-btn'); btn.disabled = true; @@ -247,18 +267,20 @@ async function toggleRSVP() { btn.textContent = 'Wypisz sie'; btn.classList.remove('btn-primary'); btn.classList.add('btn-secondary', 'attending'); + showToast('Zapisano na wydarzenie!', 'success'); } else { btn.textContent = 'Wezme udzial'; btn.classList.remove('btn-secondary', 'attending'); btn.classList.add('btn-primary'); + showToast('Wypisano z wydarzenia', 'info'); } // Refresh page to update attendees list - setTimeout(() => location.reload(), 500); + setTimeout(() => location.reload(), 1000); } else { - alert(data.error || 'Wystapil blad'); + showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { - alert('Blad polaczenia'); + showToast('Błąd połączenia', 'error'); } btn.disabled = false; diff --git a/templates/classifieds/view.html b/templates/classifieds/view.html index e1a6889..300ca04 100755 --- a/templates/classifieds/view.html +++ b/templates/classifieds/view.html @@ -282,15 +282,78 @@ + + + + +
+ + {% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; +let confirmResolve = null; + +function showConfirm(message, options = {}) { + return new Promise(resolve => { + confirmResolve = resolve; + document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; + document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; + document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; + document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); + document.getElementById('confirmModal').classList.add('active'); + }); +} + +function closeConfirm(result) { + document.getElementById('confirmModal').classList.remove('active'); + if (confirmResolve) { confirmResolve(result); confirmResolve = null; } +} + +document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); +document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); +document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); + +function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); +} async function closeClassified() { - if (!confirm('Czy na pewno chcesz zamknac to ogloszenie?')) { - return; - } + const confirmed = await showConfirm('Czy na pewno chcesz zamknąć to ogłoszenie?', { + icon: '🔒', + title: 'Zamykanie ogłoszenia', + okText: 'Zamknij', + okClass: 'btn-warning' + }); + if (!confirmed) return; try { const response = await fetch('{{ url_for("classifieds_close", classified_id=classified.id) }}', { @@ -303,12 +366,13 @@ async function closeClassified() { const data = await response.json(); if (data.success) { - window.location.href = '{{ url_for("classifieds_index") }}'; + showToast('Ogłoszenie zostało zamknięte', 'success'); + setTimeout(() => window.location.href = '{{ url_for("classifieds_index") }}', 1500); } else { - alert(data.error || 'Wystapil blad'); + showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { - alert('Blad polaczenia'); + showToast('Błąd połączenia', 'error'); } } {% endblock %} diff --git a/templates/company/recommend.html b/templates/company/recommend.html index 9ade853..4230903 100755 --- a/templates/company/recommend.html +++ b/templates/company/recommend.html @@ -280,9 +280,29 @@ + +
+ {% endblock %} {% block extra_js %} + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } + // Character counter const textarea = document.getElementById('recommendation_text'); const counter = document.getElementById('charCounter'); @@ -308,11 +328,11 @@ if (text.length < 50) { textarea.style.borderColor = 'var(--error)'; - alert('Rekomendacja musi mieć co najmniej 50 znaków.'); + showToast('Rekomendacja musi mieć co najmniej 50 znaków.', 'error'); valid = false; } else if (text.length > 2000) { textarea.style.borderColor = 'var(--error)'; - alert('Rekomendacja może mieć maksymalnie 2000 znaków.'); + showToast('Rekomendacja może mieć maksymalnie 2000 znaków.', 'error'); valid = false; } else { textarea.style.borderColor = ''; diff --git a/templates/company_detail.html b/templates/company_detail.html index c84ef6d..663ca33 100755 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -2248,39 +2248,101 @@ {% endif %} + + + +
+ + + diff --git a/templates/forum/new_topic.html b/templates/forum/new_topic.html index 4fc34a6..77fd578 100755 --- a/templates/forum/new_topic.html +++ b/templates/forum/new_topic.html @@ -339,9 +339,28 @@ + +
+ {% endblock %} {% block extra_js %} + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } // Client-side validation document.querySelector('form').addEventListener('submit', function(e) { const title = document.getElementById('title'); @@ -433,13 +452,13 @@ function handleFile(file) { // Validate file size (5MB) if (file.size > 5 * 1024 * 1024) { - alert('Plik jest za duzy (max 5MB)'); + showToast('Plik jest za duży (max 5MB)', 'error'); return; } // Validate file type if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) { - alert('Dozwolone formaty: JPG, PNG, GIF'); + showToast('Dozwolone formaty: JPG, PNG, GIF', 'warning'); return; } diff --git a/templates/forum/topic.html b/templates/forum/topic.html index 6d08487..8b54d68 100755 --- a/templates/forum/topic.html +++ b/templates/forum/topic.html @@ -689,9 +689,28 @@ + +
+ {% endblock %} {% block extra_js %} + function showToast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${icons[type]||'ℹ'}${message}`; + container.appendChild(toast); + setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); + } // Lightbox functions function openLightbox(src) { document.getElementById('lightboxImage').src = src; @@ -783,7 +802,7 @@ const availableSlots = MAX_FILES - currentCount; if (availableSlots <= 0) { - alert('Osiagnieto limit ' + MAX_FILES + ' plikow'); + showToast('Osiągnięto limit ' + MAX_FILES + ' plików', 'warning'); return; } @@ -809,7 +828,7 @@ }); if (errors.length > 0) { - alert('Bledy:\n' + errors.join('\n')); + showToast('Błędy: ' + errors.join(', '), 'error'); } updateCounter();