nordabiz/templates/admin/insights.html
Maciej Pienczyn 39da377065
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: UTC timezone correction for all JS date parsing across portal
Added global parseUTC() helper in base.html that appends 'Z' to
naive ISO dates from server. Applied to:
- Notification bell (base.html) — formatTimeAgo
- NordaGPT conversation sort (chat.html)
- B2B interest dates (classifieds/view.html)
- Admin forum moderation dates (admin/forum.html)
- Admin AI insights dates (admin/insights.html)

Same fix as conversations.js parseUTC, now available globally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:09:42 +02:00

416 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}Insights - Rozwój portalu - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.insights-header {
margin-bottom: var(--spacing-xl);
}
.insights-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
.stat-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
text-align: center;
}
.stat-value {
font-size: var(--font-size-3xl);
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 {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
align-items: center;
}
.filter-btn {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--surface);
cursor: pointer;
transition: all 0.2s;
}
.filter-btn:hover {
background: var(--bg-secondary);
}
.filter-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.insights-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.insight-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
border-left: 4px solid var(--border-color);
}
.insight-card.feature_request { border-left-color: var(--primary); }
.insight-card.bug_report { border-left-color: var(--error); }
.insight-card.improvement { border-left-color: var(--warning); }
.insight-card.question { border-left-color: var(--info); }
.insight-card.company_search { border-left-color: var(--success); }
.insight-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-md);
}
.insight-title {
font-weight: 600;
font-size: var(--font-size-lg);
color: var(--text-primary);
}
.insight-badges {
display: flex;
gap: var(--spacing-sm);
}
.badge {
padding: 2px 8px;
border-radius: var(--radius);
font-size: var(--font-size-xs);
font-weight: 500;
}
.badge-category {
background: var(--bg-secondary);
color: var(--text-secondary);
}
.badge-priority {
background: var(--primary-light);
color: var(--primary);
}
.badge-status {
background: var(--success-light);
color: var(--success);
}
.badge-status.reviewed { background: var(--info-light); color: var(--info); }
.badge-status.planned { background: var(--warning-light); color: var(--warning); }
.badge-status.implemented { background: var(--success-light); color: var(--success); }
.badge-status.rejected { background: var(--error-light); color: var(--error); }
.insight-content {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-md);
}
.insight-actions {
display: flex;
gap: var(--spacing-sm);
}
.insight-actions select {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-size: var(--font-size-sm);
}
.sync-btn {
margin-left: auto;
}
.loading {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
.category-icon {
margin-right: var(--spacing-xs);
}
</style>
{% endblock %}
{% block content %}
<div class="insights-header">
<h1>
<svg width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="color: var(--primary);">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
Insights - Rozwój portalu
</h1>
<p style="color: var(--text-secondary);">Zbieraj pomysły i feedback z interakcji użytkowników</p>
</div>
<!-- Stats -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-value" id="statTotal">-</div>
<div class="stat-label">Wszystkich insights</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statNew">-</div>
<div class="stat-label">Nowych</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statPlanned">-</div>
<div class="stat-label">Zaplanowanych</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statImplemented">-</div>
<div class="stat-label">Zrealizowanych</div>
</div>
</div>
<!-- Filters -->
<div class="filters-bar">
<button class="filter-btn active" data-status="">Wszystkie</button>
<button class="filter-btn" data-status="new">Nowe</button>
<button class="filter-btn" data-status="reviewed">Przejrzane</button>
<button class="filter-btn" data-status="planned">Zaplanowane</button>
<button class="filter-btn" data-status="implemented">Zrealizowane</button>
<button class="filter-btn" data-status="rejected">Odrzucone</button>
<button class="btn btn-primary sync-btn" onclick="syncInsights()">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="margin-right: 6px;">
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Synchronizuj
</button>
</div>
<!-- Insights List -->
<div class="insights-list" id="insightsList">
<div class="loading">Ładowanie insights...</div>
</div>
{% endblock %}
{% block extra_js %}
let currentStatus = '';
const categoryLabels = {
'feature_request': 'Propozycja funkcji',
'bug_report': 'Zgłoszenie błędu',
'improvement': 'Ulepszenie',
'question': 'Pytanie',
'pain_point': 'Problem',
'company_search': 'Wyszukiwanie firm',
'positive_feedback': 'Pozytywny feedback',
'other': 'Inne'
};
const categoryIcons = {
'feature_request': '💡',
'bug_report': '🐛',
'improvement': '⚡',
'question': '❓',
'pain_point': '😤',
'company_search': '🔍',
'positive_feedback': '👍',
'other': '📝'
};
const statusLabels = {
'new': 'Nowy',
'reviewed': 'Przejrzany',
'planned': 'Zaplanowany',
'implemented': 'Zrealizowany',
'rejected': 'Odrzucony'
};
async function loadInsights() {
const container = document.getElementById('insightsList');
container.innerHTML = '<div class="loading">Ładowanie insights...</div>';
try {
const url = currentStatus
? `/admin/insights-api?status=${currentStatus}`
: '/admin/insights-api';
const response = await fetch(url);
const data = await response.json();
if (!data.success) {
container.innerHTML = `<div class="empty-state">Błąd: ${data.error}</div>`;
return;
}
if (data.insights.length === 0) {
container.innerHTML = `
<div class="empty-state">
<p>Brak insights${currentStatus ? ' o statusie "' + statusLabels[currentStatus] + '"' : ''}.</p>
<p style="margin-top: var(--spacing-md);">Kliknij "Synchronizuj" aby pobrać dane z forum i czata.</p>
</div>
`;
return;
}
container.innerHTML = data.insights.map(insight => `
<div class="insight-card ${insight.category || ''}">
<div class="insight-header">
<div class="insight-title">
<span class="category-icon">${categoryIcons[insight.category] || '📝'}</span>
${insight.summary || 'Bez tytułu'}
</div>
<div class="insight-badges">
<span class="badge badge-category">${categoryLabels[insight.category] || insight.category}</span>
<span class="badge badge-priority">Priorytet: ${insight.priority || 0}</span>
<span class="badge badge-status ${insight.status}">${statusLabels[insight.status] || insight.status}</span>
</div>
</div>
<div class="insight-content">${insight.content || ''}</div>
<div class="insight-actions">
<select onchange="updateInsightStatus(${insight.id}, this.value)">
<option value="">Zmień status...</option>
<option value="reviewed">Przejrzany</option>
<option value="planned">Zaplanowany</option>
<option value="implemented">Zrealizowany</option>
<option value="rejected">Odrzucony</option>
</select>
<span style="font-size: var(--font-size-xs); color: var(--text-muted);">
${insight.source_type} | ${insight.created_at ? parseUTC(insight.created_at).toLocaleDateString('pl-PL') : ''}
</span>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading insights:', error);
container.innerHTML = '<div class="empty-state">Błąd ładowania danych</div>';
}
}
async function loadStats() {
try {
const response = await fetch('/admin/insights-api/stats');
const data = await response.json();
if (data.success && data.stats) {
document.getElementById('statTotal').textContent = data.stats.total_chunks || 0;
// Note: detailed stats by status would need additional backend support
}
} catch (error) {
console.error('Error loading stats:', error);
}
}
async function updateInsightStatus(insightId, status) {
if (!status) return;
try {
const response = await fetch(`/admin/insights-api/${insightId}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status })
});
const data = await response.json();
if (data.success) {
loadInsights();
} else {
alert('Błąd: ' + (data.error || 'Nieznany błąd'));
}
} catch (error) {
console.error('Error updating status:', error);
alert('Błąd połączenia');
}
}
async function syncInsights() {
const btn = document.querySelector('.sync-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Synchronizuję...';
try {
const response = await fetch('/admin/insights-api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ days_back: 30 })
});
const data = await response.json();
if (data.success) {
const r = data.results;
alert(`Synchronizacja zakończona!\n\n` +
`Forum: ${r.forum?.topics_added || 0} tematów, ${r.forum?.replies_added || 0} odpowiedzi\n` +
`Chat: ${r.chat?.responses_added || 0} odpowiedzi AI\n` +
`Insights: ${r.questions?.insights_added || 0} nowych wzorców`
);
loadInsights();
loadStats();
} else {
alert('Błąd: ' + (data.error || 'Nieznany błąd'));
}
} catch (error) {
console.error('Error syncing:', error);
alert('Błąd połączenia');
} finally {
btn.disabled = false;
btn.innerHTML = `
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="margin-right: 6px;">
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Synchronizuj
`;
}
}
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentStatus = btn.dataset.status;
loadInsights();
});
});
// Load on page load
loadInsights();
loadStats();
{% endblock %}