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
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>
416 lines
13 KiB
HTML
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 %}
|