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
New analyze_roadmap_with_ai() function sends existing milestones and recent knowledge facts to Gemini for comprehensive analysis. Returns new milestone suggestions, status update recommendations, and identified roadmap gaps. Adds PATCH endpoint for milestone status updates and tabbed UI modal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
832 lines
39 KiB
HTML
832 lines
39 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Timeline ZOPK - Roadmapa{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg); }
|
||
.page-header h1 { font-size: var(--font-size-2xl); color: var(--text-primary); }
|
||
.breadcrumb { display: flex; gap: var(--spacing-xs); color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-lg); }
|
||
.breadcrumb a { color: var(--primary); text-decoration: none; }
|
||
|
||
.timeline-container { position: relative; padding: var(--spacing-lg) 0; }
|
||
.timeline-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 4px; background: var(--border); transform: translateX(-50%); }
|
||
|
||
.timeline-item { display: flex; margin-bottom: var(--spacing-xl); position: relative; }
|
||
.timeline-item:nth-child(odd) { flex-direction: row-reverse; }
|
||
.timeline-item:nth-child(odd) .timeline-content { text-align: right; padding-right: var(--spacing-xl); }
|
||
.timeline-item:nth-child(even) .timeline-content { padding-left: var(--spacing-xl); }
|
||
|
||
.timeline-content { width: 45%; }
|
||
.timeline-dot { position: absolute; left: 50%; transform: translateX(-50%); width: 20px; height: 20px; border-radius: 50%; border: 4px solid var(--surface); z-index: 1; }
|
||
|
||
.timeline-card { background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); padding: var(--spacing-md); }
|
||
.timeline-date { font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-xs); }
|
||
.timeline-title { font-size: var(--font-size-lg); font-weight: 600; margin-bottom: var(--spacing-xs); }
|
||
.timeline-desc { font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-sm); }
|
||
.timeline-meta { display: flex; gap: var(--spacing-sm); flex-wrap: wrap; }
|
||
.timeline-badge { padding: 2px 8px; border-radius: 12px; font-size: var(--font-size-xs); }
|
||
|
||
.status-planned { background: #e5e7eb; color: #374151; }
|
||
.status-in_progress { background: #dbeafe; color: #1d4ed8; }
|
||
.status-completed { background: #d1fae5; color: #047857; }
|
||
.status-delayed { background: #fef3c7; color: #b45309; }
|
||
|
||
.category-nuclear { --cat-color: #ef4444; }
|
||
.category-offshore { --cat-color: #3b82f6; }
|
||
.category-infrastructure { --cat-color: #8b5cf6; }
|
||
.category-defense { --cat-color: #059669; }
|
||
.category-other { --cat-color: #6b7280; }
|
||
|
||
.timeline-dot.category-nuclear { background: #ef4444; }
|
||
.timeline-dot.category-offshore { background: #3b82f6; }
|
||
.timeline-dot.category-infrastructure { background: #8b5cf6; }
|
||
.timeline-dot.category-defense { background: #059669; }
|
||
.timeline-dot.category-other { background: #6b7280; }
|
||
|
||
.btn { padding: 8px 16px; border-radius: var(--radius); font-weight: 500; cursor: pointer; border: none; }
|
||
.btn-primary { background: var(--primary); color: white; }
|
||
.btn-sm { padding: 4px 8px; font-size: var(--font-size-xs); }
|
||
|
||
.legend { display: flex; gap: var(--spacing-lg); margin-bottom: var(--spacing-lg); flex-wrap: wrap; }
|
||
.legend-item { display: flex; align-items: center; gap: var(--spacing-xs); font-size: var(--font-size-sm); }
|
||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; }
|
||
|
||
.modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }
|
||
.modal.active { display: flex; }
|
||
.modal-content { background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-lg); width: 500px; max-width: 90%; }
|
||
.modal-content.modal-large { width: 900px; max-height: 80vh; overflow-y: auto; }
|
||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-md); }
|
||
.form-group { margin-bottom: var(--spacing-md); }
|
||
.form-group label { display: block; font-size: var(--font-size-sm); margin-bottom: 4px; }
|
||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius); }
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); }
|
||
.empty-state { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); }
|
||
|
||
/* Suggestions styles */
|
||
.btn-secondary { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); }
|
||
.btn-secondary:hover { background: var(--background); }
|
||
.btn-success { background: var(--success); color: white; }
|
||
.btn-group { display: flex; gap: var(--spacing-sm); }
|
||
|
||
.suggestions-stats { display: flex; gap: var(--spacing-lg); margin-bottom: var(--spacing-md); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius); }
|
||
.stat-item { text-align: center; }
|
||
.stat-value { font-size: var(--font-size-xl); font-weight: 700; color: var(--primary); }
|
||
.stat-label { font-size: var(--font-size-xs); color: var(--text-secondary); }
|
||
|
||
.suggestion-card { background: var(--background); border-radius: var(--radius); padding: var(--spacing-md); margin-bottom: var(--spacing-sm); border-left: 4px solid var(--primary); }
|
||
.suggestion-card.approved { opacity: 0.5; border-left-color: var(--success); }
|
||
.suggestion-text { font-size: var(--font-size-sm); margin-bottom: var(--spacing-sm); line-height: 1.5; }
|
||
.suggestion-meta { display: flex; gap: var(--spacing-md); font-size: var(--font-size-xs); color: var(--text-secondary); flex-wrap: wrap; margin-bottom: var(--spacing-sm); }
|
||
.suggestion-actions { display: flex; gap: var(--spacing-sm); align-items: center; }
|
||
.suggestion-input { flex: 1; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius); font-size: var(--font-size-sm); }
|
||
|
||
/* AI Analysis tabs */
|
||
.ai-tabs { display: flex; gap: 0; border-bottom: 2px solid var(--border); margin-bottom: var(--spacing-md); }
|
||
.ai-tab { padding: 10px 16px; cursor: pointer; font-size: var(--font-size-sm); font-weight: 500; color: var(--text-secondary); border-bottom: 2px solid transparent; margin-bottom: -2px; display: flex; align-items: center; gap: 6px; }
|
||
.ai-tab:hover { color: var(--text-primary); background: var(--background); }
|
||
.ai-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
||
.ai-tab-badge { background: var(--primary); color: white; font-size: 11px; padding: 1px 7px; border-radius: 10px; min-width: 18px; text-align: center; }
|
||
.ai-tab-badge.zero { background: var(--border); color: var(--text-secondary); }
|
||
.ai-tab-content { display: none; }
|
||
.ai-tab-content.active { display: block; }
|
||
.update-card { background: var(--background); border-radius: var(--radius); padding: var(--spacing-md); margin-bottom: var(--spacing-sm); border-left: 4px solid #f59e0b; }
|
||
.update-card .status-arrow { font-weight: 600; color: var(--primary); }
|
||
.gap-card { background: var(--background); border-radius: var(--radius); padding: var(--spacing-md); margin-bottom: var(--spacing-sm); border-left: 4px solid #8b5cf6; }
|
||
|
||
/* Toast notifications */
|
||
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 2000; display: flex; flex-direction: column; gap: 10px; }
|
||
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-lg); display: flex; align-items: center; gap: 10px; animation: slideIn 0.3s ease; }
|
||
.toast.success { border-left: 4px solid var(--success); }
|
||
.toast.error { border-left: 4px solid var(--error); }
|
||
.toast.info { border-left: 4px solid var(--primary); }
|
||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||
@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container">
|
||
<div class="breadcrumb">
|
||
<a href="{{ url_for('admin.admin_zopk') }}">Panel ZOPK</a>
|
||
<span>›</span>
|
||
<span>Timeline / Roadmapa</span>
|
||
</div>
|
||
|
||
<div class="page-header">
|
||
<h1>🗺️ Timeline ZOPK</h1>
|
||
<div class="btn-group">
|
||
<button class="btn btn-secondary" onclick="loadSuggestions()">🧠 Sugestie z bazy wiedzy</button>
|
||
<button class="btn btn-secondary" onclick="loadAIAnalysis()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;">🤖 AI Analiza roadmapy</button>
|
||
<button class="btn btn-primary" onclick="openAddModal()">➕ Dodaj kamień milowy</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="legend">
|
||
<div class="legend-item"><div class="legend-dot" style="background: #ef4444;"></div> Energia jądrowa</div>
|
||
<div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Offshore wind</div>
|
||
<div class="legend-item"><div class="legend-dot" style="background: #8b5cf6;"></div> Infrastruktura</div>
|
||
<div class="legend-item"><div class="legend-dot" style="background: #059669;"></div> Obronność</div>
|
||
<div class="legend-item"><div class="legend-dot" style="background: #6b7280;"></div> Inne</div>
|
||
</div>
|
||
|
||
<div class="timeline-container">
|
||
<div class="timeline-line"></div>
|
||
<div id="timelineItems">
|
||
<div class="empty-state">Ładowanie...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal dodawania -->
|
||
<div class="modal" id="addModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3 id="modalTitle">Dodaj kamień milowy</h3>
|
||
<button onclick="closeModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
|
||
</div>
|
||
<form id="milestoneForm" onsubmit="saveMilestone(event)">
|
||
<input type="hidden" id="milestoneId">
|
||
<div class="form-group">
|
||
<label>Tytuł *</label>
|
||
<input type="text" id="title" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Opis</label>
|
||
<textarea id="description" rows="3"></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Kategoria</label>
|
||
<select id="category">
|
||
<option value="nuclear">Energia jądrowa</option>
|
||
<option value="offshore">Offshore wind</option>
|
||
<option value="infrastructure">Infrastruktura</option>
|
||
<option value="defense">Obronność</option>
|
||
<option value="other">Inne</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Status</label>
|
||
<select id="status">
|
||
<option value="planned">Planowane</option>
|
||
<option value="in_progress">W trakcie</option>
|
||
<option value="completed">Zakończone</option>
|
||
<option value="delayed">Opóźnione</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Data planowana</label>
|
||
<input type="date" id="targetDate">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Data rzeczywista</label>
|
||
<input type="date" id="actualDate">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Źródło (URL)</label>
|
||
<input type="url" id="sourceUrl">
|
||
</div>
|
||
<div style="display: flex; justify-content: flex-end; gap: var(--spacing-sm);">
|
||
<button type="button" class="btn" onclick="closeModal()">Anuluj</button>
|
||
<button type="submit" class="btn btn-primary">Zapisz</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal sugestii z bazy wiedzy -->
|
||
<div class="modal" id="suggestionsModal">
|
||
<div class="modal-content modal-large">
|
||
<div class="modal-header">
|
||
<h3>🧠 Sugestie kamieni milowych z bazy wiedzy</h3>
|
||
<button onclick="closeSuggestionsModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
|
||
</div>
|
||
|
||
<div class="suggestions-stats" id="suggestionsStats">
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="statTotalFacts">-</div>
|
||
<div class="stat-label">Faktów typu milestone</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="statInTimeline">-</div>
|
||
<div class="stat-label">Już w Timeline</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="statSuggestions">-</div>
|
||
<div class="stat-label">Sugestii do dodania</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="suggestionsList">
|
||
<div class="empty-state">Kliknij przycisk aby załadować sugestie...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal AI Analysis -->
|
||
<div class="modal" id="aiAnalysisModal">
|
||
<div class="modal-content modal-large">
|
||
<div class="modal-header">
|
||
<h3>🤖 AI Analiza roadmapy</h3>
|
||
<button onclick="closeAIModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
|
||
</div>
|
||
|
||
<div class="suggestions-stats" id="aiStats">
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="aiStatMilestones">-</div>
|
||
<div class="stat-label">Kamieni milowych</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="aiStatFacts">-</div>
|
||
<div class="stat-label">Faktów przeanalizowanych</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="aiStatNew">-</div>
|
||
<div class="stat-label">Nowych sugestii</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="aiStatUpdates">-</div>
|
||
<div class="stat-label">Aktualizacji statusu</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="aiStatGaps">-</div>
|
||
<div class="stat-label">Braków w roadmapie</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ai-tabs">
|
||
<div class="ai-tab active" onclick="switchAITab('new')">Nowe kamienie milowe <span class="ai-tab-badge" id="badgeNew">0</span></div>
|
||
<div class="ai-tab" onclick="switchAITab('updates')">Aktualizacje statusu <span class="ai-tab-badge" id="badgeUpdates">0</span></div>
|
||
<div class="ai-tab" onclick="switchAITab('gaps')">Braki w roadmapie <span class="ai-tab-badge" id="badgeGaps">0</span></div>
|
||
</div>
|
||
|
||
<div class="ai-tab-content active" id="tabNew">
|
||
<div class="empty-state">Ładowanie...</div>
|
||
</div>
|
||
<div class="ai-tab-content" id="tabUpdates">
|
||
<div class="empty-state">Ładowanie...</div>
|
||
</div>
|
||
<div class="ai-tab-content" id="tabGaps">
|
||
<div class="empty-state">Ładowanie...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast container -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
let milestones = [];
|
||
let editingId = null;
|
||
|
||
async function loadMilestones() {
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/milestones');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
milestones = data.milestones;
|
||
renderTimeline();
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('timelineItems').innerHTML = '<div class="empty-state">Błąd ładowania: ' + error + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderTimeline() {
|
||
if (milestones.length === 0) {
|
||
document.getElementById('timelineItems').innerHTML = '<div class="empty-state">Brak kamieni milowych. Dodaj pierwszy!</div>';
|
||
return;
|
||
}
|
||
|
||
const statusLabels = {planned: 'Planowane', in_progress: 'W trakcie', completed: 'Zakończone', delayed: 'Opóźnione'};
|
||
const categoryLabels = {nuclear: 'Energia jądrowa', offshore: 'Offshore', infrastructure: 'Infrastruktura', defense: 'Obronność', other: 'Inne'};
|
||
|
||
const html = milestones.map(m => `
|
||
<div class="timeline-item">
|
||
<div class="timeline-content">
|
||
<div class="timeline-card">
|
||
<div class="timeline-date">${formatDate(m.target_date)}</div>
|
||
<div class="timeline-title">${escapeHtml(m.title)}</div>
|
||
<div class="timeline-desc">${escapeHtml(m.description || '')}</div>
|
||
<div class="timeline-meta">
|
||
<span class="timeline-badge status-${m.status}">${statusLabels[m.status] || m.status}</span>
|
||
<span class="timeline-badge" style="background: var(--cat-color);">${categoryLabels[m.category] || m.category}</span>
|
||
<button class="btn btn-sm" onclick="editMilestone(${m.id})">✏️</button>
|
||
<button class="btn btn-sm" onclick="deleteMilestone(${m.id})">🗑️</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="timeline-dot category-${m.category}"></div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('timelineItems').innerHTML = html;
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return 'Brak daty';
|
||
const d = new Date(dateStr);
|
||
return d.toLocaleDateString('pl-PL', {year: 'numeric', month: 'long'});
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text || '';
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function openAddModal() {
|
||
editingId = null;
|
||
document.getElementById('modalTitle').textContent = 'Dodaj kamień milowy';
|
||
document.getElementById('milestoneForm').reset();
|
||
document.getElementById('addModal').classList.add('active');
|
||
}
|
||
|
||
function editMilestone(id) {
|
||
const m = milestones.find(x => x.id === id);
|
||
if (!m) return;
|
||
|
||
editingId = id;
|
||
document.getElementById('modalTitle').textContent = 'Edytuj kamień milowy';
|
||
document.getElementById('title').value = m.title;
|
||
document.getElementById('description').value = m.description || '';
|
||
document.getElementById('category').value = m.category;
|
||
document.getElementById('status').value = m.status;
|
||
document.getElementById('targetDate').value = m.target_date || '';
|
||
document.getElementById('actualDate').value = m.actual_date || '';
|
||
document.getElementById('sourceUrl').value = m.source_url || '';
|
||
document.getElementById('addModal').classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('addModal').classList.remove('active');
|
||
}
|
||
|
||
async function saveMilestone(e) {
|
||
e.preventDefault();
|
||
|
||
const data = {
|
||
title: document.getElementById('title').value,
|
||
description: document.getElementById('description').value,
|
||
category: document.getElementById('category').value,
|
||
status: document.getElementById('status').value,
|
||
target_date: document.getElementById('targetDate').value || null,
|
||
actual_date: document.getElementById('actualDate').value || null,
|
||
source_url: document.getElementById('sourceUrl').value || null
|
||
};
|
||
|
||
try {
|
||
const url = editingId ? `/admin/zopk-api/milestones/${editingId}` : '/admin/zopk-api/milestones';
|
||
const method = editingId ? 'PUT' : 'POST';
|
||
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
closeModal();
|
||
loadMilestones();
|
||
} else {
|
||
alert('Błąd: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
alert('Błąd: ' + error);
|
||
}
|
||
}
|
||
|
||
async function deleteMilestone(id) {
|
||
if (!confirm('Usunąć ten kamień milowy?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/admin/zopk-api/milestones/${id}`, {
|
||
method: 'DELETE',
|
||
headers: {'X-CSRFToken': '{{ csrf_token() }}'}
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
loadMilestones();
|
||
} else {
|
||
alert('Błąd: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
alert('Błąd: ' + error);
|
||
}
|
||
}
|
||
|
||
// ========== SUGGESTIONS ==========
|
||
let suggestionsData = [];
|
||
|
||
async function loadSuggestions() {
|
||
document.getElementById('suggestionsModal').classList.add('active');
|
||
document.getElementById('suggestionsList').innerHTML = '<div class="empty-state">⏳ Ładowanie sugestii...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/timeline/suggestions?limit=30');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
suggestionsData = data.suggestions;
|
||
|
||
// Update stats
|
||
document.getElementById('statTotalFacts').textContent = data.total_milestone_facts || 0;
|
||
document.getElementById('statInTimeline').textContent = data.already_in_timeline || 0;
|
||
document.getElementById('statSuggestions').textContent = data.suggestions_count || 0;
|
||
|
||
renderSuggestions();
|
||
} else {
|
||
document.getElementById('suggestionsList').innerHTML = '<div class="empty-state">❌ Błąd: ' + (data.error || 'Nieznany błąd') + '</div>';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('suggestionsList').innerHTML = '<div class="empty-state">❌ Błąd połączenia: ' + error + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderSuggestions() {
|
||
if (suggestionsData.length === 0) {
|
||
document.getElementById('suggestionsList').innerHTML = '<div class="empty-state">✅ Brak nowych sugestii - wszystkie fakty typu milestone są już w Timeline!</div>';
|
||
return;
|
||
}
|
||
|
||
const categoryLabels = {nuclear: '⚛️ Energia jądrowa', offshore: '🌊 Offshore', infrastructure: '🛣️ Infrastruktura', defense: '🛡️ Obronność', other: '📌 Inne'};
|
||
const statusLabels = {completed: '✅ Zakończone', planned: '📅 Planowane', in_progress: '🔄 W trakcie'};
|
||
|
||
const html = suggestionsData.map((s, idx) => `
|
||
<div class="suggestion-card" id="suggestion-${idx}">
|
||
<div class="suggestion-text">${escapeHtml(s.full_text)}</div>
|
||
<div class="suggestion-meta">
|
||
<span>${categoryLabels[s.suggested_category] || s.suggested_category}</span>
|
||
<span>${statusLabels[s.suggested_status] || s.suggested_status}</span>
|
||
${s.suggested_date ? `<span>📅 ${s.suggested_date}</span>` : ''}
|
||
${s.source_name ? `<span>📰 ${s.source_name}</span>` : ''}
|
||
<span>🎯 ${Math.round(s.confidence_score * 100)}%</span>
|
||
</div>
|
||
<div class="suggestion-actions">
|
||
<input type="text" class="suggestion-input" id="title-${idx}" value="${escapeHtml(s.suggested_title)}" placeholder="Tytuł">
|
||
<select id="category-${idx}" style="padding: 6px; border-radius: var(--radius);">
|
||
<option value="nuclear" ${s.suggested_category === 'nuclear' ? 'selected' : ''}>⚛️ Energia jądrowa</option>
|
||
<option value="offshore" ${s.suggested_category === 'offshore' ? 'selected' : ''}>🌊 Offshore</option>
|
||
<option value="infrastructure" ${s.suggested_category === 'infrastructure' ? 'selected' : ''}>🛣️ Infrastruktura</option>
|
||
<option value="defense" ${s.suggested_category === 'defense' ? 'selected' : ''}>🛡️ Obronność</option>
|
||
<option value="other" ${s.suggested_category === 'other' ? 'selected' : ''}>📌 Inne</option>
|
||
</select>
|
||
<button class="btn btn-success btn-sm" onclick="approveSuggestion(${idx})">✅ Dodaj</button>
|
||
<button class="btn btn-sm" onclick="skipSuggestion(${idx})">⏭️</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('suggestionsList').innerHTML = html;
|
||
}
|
||
|
||
async function approveSuggestion(idx) {
|
||
const s = suggestionsData[idx];
|
||
if (!s) return;
|
||
|
||
const title = document.getElementById(`title-${idx}`).value;
|
||
const category = document.getElementById(`category-${idx}`).value;
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/timeline/suggestions/approve', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||
body: JSON.stringify({
|
||
fact_id: s.fact_id,
|
||
title: title,
|
||
description: s.full_text,
|
||
category: category,
|
||
target_date: s.suggested_date,
|
||
status: s.suggested_status,
|
||
source_url: s.news_url
|
||
})
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast('success', `✅ Dodano: ${title}`);
|
||
document.getElementById(`suggestion-${idx}`).classList.add('approved');
|
||
suggestionsData.splice(idx, 1);
|
||
|
||
// Refresh timeline
|
||
loadMilestones();
|
||
|
||
// Update stats
|
||
const current = parseInt(document.getElementById('statInTimeline').textContent) || 0;
|
||
document.getElementById('statInTimeline').textContent = current + 1;
|
||
document.getElementById('statSuggestions').textContent = suggestionsData.length;
|
||
} else {
|
||
showToast('error', `❌ Błąd: ${result.error}`);
|
||
}
|
||
} catch (error) {
|
||
showToast('error', `❌ Błąd: ${error}`);
|
||
}
|
||
}
|
||
|
||
function skipSuggestion(idx) {
|
||
document.getElementById(`suggestion-${idx}`).remove();
|
||
suggestionsData.splice(idx, 1);
|
||
document.getElementById('statSuggestions').textContent = suggestionsData.length;
|
||
|
||
if (suggestionsData.length === 0) {
|
||
document.getElementById('suggestionsList').innerHTML = '<div class="empty-state">✅ Przejrzano wszystkie sugestie!</div>';
|
||
}
|
||
}
|
||
|
||
function closeSuggestionsModal() {
|
||
document.getElementById('suggestionsModal').classList.remove('active');
|
||
}
|
||
|
||
// ========== TOASTS ==========
|
||
function showToast(type, message, duration = 4000) {
|
||
const container = document.getElementById('toastContainer');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.innerHTML = message;
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.animation = 'slideOut 0.3s ease forwards';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, duration);
|
||
}
|
||
|
||
// ========== AI ANALYSIS ==========
|
||
let aiData = null;
|
||
|
||
async function loadAIAnalysis() {
|
||
document.getElementById('aiAnalysisModal').classList.add('active');
|
||
document.getElementById('tabNew').innerHTML = '<div class="empty-state">🤖 AI analizuje roadmapę... (może potrwać 10-15 sekund)</div>';
|
||
document.getElementById('tabUpdates').innerHTML = '<div class="empty-state">Oczekiwanie na wyniki...</div>';
|
||
document.getElementById('tabGaps').innerHTML = '<div class="empty-state">Oczekiwanie na wyniki...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/timeline/ai-analyze');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
aiData = data;
|
||
|
||
// Update stats
|
||
document.getElementById('aiStatMilestones').textContent = data.total_milestones || 0;
|
||
document.getElementById('aiStatFacts').textContent = data.total_facts_analyzed || 0;
|
||
document.getElementById('aiStatNew').textContent = (data.new_milestones || []).length;
|
||
document.getElementById('aiStatUpdates').textContent = (data.status_updates || []).length;
|
||
document.getElementById('aiStatGaps').textContent = (data.gaps || []).length;
|
||
|
||
// Update badges
|
||
updateBadge('badgeNew', (data.new_milestones || []).length);
|
||
updateBadge('badgeUpdates', (data.status_updates || []).length);
|
||
updateBadge('badgeGaps', (data.gaps || []).length);
|
||
|
||
renderAINewMilestones(data.new_milestones || []);
|
||
renderAIStatusUpdates(data.status_updates || []);
|
||
renderAIGaps(data.gaps || []);
|
||
} else {
|
||
document.getElementById('tabNew').innerHTML = '<div class="empty-state">Błąd: ' + (data.error || 'Nieznany') + '</div>';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('tabNew').innerHTML = '<div class="empty-state">Błąd połączenia: ' + error + '</div>';
|
||
}
|
||
}
|
||
|
||
function updateBadge(id, count) {
|
||
const badge = document.getElementById(id);
|
||
badge.textContent = count;
|
||
badge.className = count > 0 ? 'ai-tab-badge' : 'ai-tab-badge zero';
|
||
}
|
||
|
||
function switchAITab(tab) {
|
||
document.querySelectorAll('.ai-tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.ai-tab-content').forEach(t => t.classList.remove('active'));
|
||
|
||
const tabs = {new: 0, updates: 1, gaps: 2};
|
||
document.querySelectorAll('.ai-tab')[tabs[tab]].classList.add('active');
|
||
document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1)).classList.add('active');
|
||
}
|
||
|
||
const statusLabelsAI = {planned: 'Planowane', in_progress: 'W trakcie', completed: 'Zakończone', delayed: 'Opóźnione'};
|
||
const categoryLabelsAI = {nuclear: 'Energia jądrowa', offshore: 'Offshore', infrastructure: 'Infrastruktura', defense: 'Obronność', other: 'Inne'};
|
||
|
||
function renderAINewMilestones(items) {
|
||
if (items.length === 0) {
|
||
document.getElementById('tabNew').innerHTML = '<div class="empty-state">AI nie znalazło nowych kandydatów na kamienie milowe</div>';
|
||
return;
|
||
}
|
||
|
||
const html = items.map((item, idx) => `
|
||
<div class="suggestion-card" id="ai-new-${idx}">
|
||
<div class="suggestion-text">${escapeHtml(item.full_text || item.reason || '')}</div>
|
||
<div class="suggestion-meta">
|
||
<span>Kategoria: ${categoryLabelsAI[item.category] || item.category}</span>
|
||
<span>Status: ${statusLabelsAI[item.status] || item.status}</span>
|
||
${item.target_date ? '<span>Data: ' + item.target_date + '</span>' : ''}
|
||
${item.news_title ? '<span>Źródło: ' + escapeHtml(item.news_title) + '</span>' : ''}
|
||
</div>
|
||
<div class="suggestion-meta" style="margin-bottom: var(--spacing-xs);"><em>AI: ${escapeHtml(item.reason || '')}</em></div>
|
||
<div class="suggestion-actions">
|
||
<input type="text" class="suggestion-input" id="ai-new-title-${idx}" value="${escapeHtml(item.title)}" placeholder="Tytuł">
|
||
<select id="ai-new-cat-${idx}" style="padding: 6px; border-radius: var(--radius);">
|
||
<option value="nuclear" ${item.category === 'nuclear' ? 'selected' : ''}>Energia jądrowa</option>
|
||
<option value="offshore" ${item.category === 'offshore' ? 'selected' : ''}>Offshore</option>
|
||
<option value="infrastructure" ${item.category === 'infrastructure' ? 'selected' : ''}>Infrastruktura</option>
|
||
<option value="defense" ${item.category === 'defense' ? 'selected' : ''}>Obronność</option>
|
||
<option value="other" ${item.category === 'other' ? 'selected' : ''}>Inne</option>
|
||
</select>
|
||
<button class="btn btn-success btn-sm" onclick="approveAINew(${idx})">Dodaj</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('tabNew').innerHTML = html;
|
||
}
|
||
|
||
function renderAIStatusUpdates(items) {
|
||
if (items.length === 0) {
|
||
document.getElementById('tabUpdates').innerHTML = '<div class="empty-state">AI nie znalazło potrzebnych aktualizacji statusu</div>';
|
||
return;
|
||
}
|
||
|
||
const html = items.map((item, idx) => `
|
||
<div class="update-card" id="ai-update-${idx}">
|
||
<div style="font-weight: 600; margin-bottom: var(--spacing-xs);">${escapeHtml(item.milestone_title || 'Milestone #' + item.milestone_id)}</div>
|
||
<div class="suggestion-meta">
|
||
<span class="status-arrow">
|
||
<span class="timeline-badge status-${item.current_status}">${statusLabelsAI[item.current_status]}</span>
|
||
→
|
||
<span class="timeline-badge status-${item.suggested_status}">${statusLabelsAI[item.suggested_status]}</span>
|
||
</span>
|
||
${item.milestone_category ? '<span>Kategoria: ' + (categoryLabelsAI[item.milestone_category] || item.milestone_category) + '</span>' : ''}
|
||
</div>
|
||
<div class="suggestion-meta"><em>AI: ${escapeHtml(item.reason || '')}</em></div>
|
||
<div class="suggestion-actions" style="margin-top: var(--spacing-sm);">
|
||
<button class="btn btn-success btn-sm" onclick="applyStatusUpdate(${idx})">Zastosuj zmianę</button>
|
||
<button class="btn btn-sm" onclick="skipUpdate(${idx})">Pomiń</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('tabUpdates').innerHTML = html;
|
||
}
|
||
|
||
function renderAIGaps(items) {
|
||
if (items.length === 0) {
|
||
document.getElementById('tabGaps').innerHTML = '<div class="empty-state">AI nie zidentyfikowało braków w roadmapie</div>';
|
||
return;
|
||
}
|
||
|
||
const html = items.map((item, idx) => `
|
||
<div class="gap-card" id="ai-gap-${idx}">
|
||
<div style="font-weight: 600; margin-bottom: var(--spacing-xs);">${escapeHtml(item.suggested_title)}</div>
|
||
<div class="suggestion-text">${escapeHtml(item.description)}</div>
|
||
<div class="suggestion-meta">
|
||
<span>Kategoria: ${categoryLabelsAI[item.category] || item.category}</span>
|
||
</div>
|
||
<div class="suggestion-meta"><em>AI: ${escapeHtml(item.reason || '')}</em></div>
|
||
<div class="suggestion-actions" style="margin-top: var(--spacing-sm);">
|
||
<input type="text" class="suggestion-input" id="ai-gap-title-${idx}" value="${escapeHtml(item.suggested_title)}" placeholder="Tytuł">
|
||
<button class="btn btn-success btn-sm" onclick="createFromGap(${idx})">Utwórz</button>
|
||
<button class="btn btn-sm" onclick="skipGap(${idx})">Pomiń</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('tabGaps').innerHTML = html;
|
||
}
|
||
|
||
async function approveAINew(idx) {
|
||
const item = aiData.new_milestones[idx];
|
||
if (!item) return;
|
||
|
||
const title = document.getElementById('ai-new-title-' + idx).value;
|
||
const category = document.getElementById('ai-new-cat-' + idx).value;
|
||
|
||
const body = {
|
||
title: title,
|
||
description: item.full_text || item.reason,
|
||
category: category,
|
||
target_date: item.target_date || null,
|
||
status: item.status || 'planned',
|
||
source_url: item.news_url || null,
|
||
source_news_id: item.source_news_id || null
|
||
};
|
||
|
||
// If fact_id exists, use suggestion approve endpoint
|
||
if (item.fact_id) {
|
||
body.fact_id = item.fact_id;
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/timeline/suggestions/approve', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||
body: JSON.stringify(body)
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showToast('success', 'Dodano: ' + title);
|
||
document.getElementById('ai-new-' + idx).classList.add('approved');
|
||
loadMilestones();
|
||
} else {
|
||
showToast('error', 'Błąd: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
showToast('error', 'Błąd: ' + error);
|
||
}
|
||
} else {
|
||
// Create directly
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/milestones', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||
body: JSON.stringify(body)
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showToast('success', 'Dodano: ' + title);
|
||
document.getElementById('ai-new-' + idx).classList.add('approved');
|
||
loadMilestones();
|
||
} else {
|
||
showToast('error', 'Błąd: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
showToast('error', 'Błąd: ' + error);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function applyStatusUpdate(idx) {
|
||
const item = aiData.status_updates[idx];
|
||
if (!item) return;
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/milestones/' + item.milestone_id + '/status', {
|
||
method: 'PATCH',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||
body: JSON.stringify({status: item.suggested_status})
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showToast('success', 'Status zmieniony: ' + (item.milestone_title || '#' + item.milestone_id));
|
||
document.getElementById('ai-update-' + idx).classList.add('approved');
|
||
loadMilestones();
|
||
} else {
|
||
showToast('error', 'Błąd: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
showToast('error', 'Błąd: ' + error);
|
||
}
|
||
}
|
||
|
||
function skipUpdate(idx) {
|
||
document.getElementById('ai-update-' + idx).remove();
|
||
}
|
||
|
||
async function createFromGap(idx) {
|
||
const item = aiData.gaps[idx];
|
||
if (!item) return;
|
||
|
||
const title = document.getElementById('ai-gap-title-' + idx).value;
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk-api/milestones', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
|
||
body: JSON.stringify({
|
||
title: title,
|
||
description: item.description,
|
||
category: item.category || 'other',
|
||
status: 'planned'
|
||
})
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showToast('success', 'Utworzono: ' + title);
|
||
document.getElementById('ai-gap-' + idx).classList.add('approved');
|
||
loadMilestones();
|
||
} else {
|
||
showToast('error', 'Błąd: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
showToast('error', 'Błąd: ' + error);
|
||
}
|
||
}
|
||
|
||
function skipGap(idx) {
|
||
document.getElementById('ai-gap-' + idx).remove();
|
||
}
|
||
|
||
function closeAIModal() {
|
||
document.getElementById('aiAnalysisModal').classList.remove('active');
|
||
}
|
||
|
||
// Init
|
||
loadMilestones();
|
||
{% endblock %}
|