Fix consistentie benamingen.
This commit is contained in:
+534
-10
@@ -6,7 +6,8 @@
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/settings.php';
|
||||
$db = get_db_connection();
|
||||
$aiSettings = get_ai_settings();
|
||||
$userId = (int)($currentUser['id'] ?? 0);
|
||||
$aiSettings = get_ai_settings($userId);
|
||||
|
||||
$sessionId = (int)($_GET['id'] ?? 0);
|
||||
$stmtSession = $db->prepare("
|
||||
@@ -164,7 +165,15 @@ $categories = [
|
||||
|
||||
<!-- Footer van kaart -->
|
||||
<div class="mt-3 pt-2.5 border-t border-slate-200/60 dark:border-slate-800/60 flex items-center justify-between text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<span><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<span><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
|
||||
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
|
||||
<button onclick="openAiBreakdownModal(<?= $card['id'] ?>, <?= htmlspecialchars(json_encode($card['content']), ENT_QUOTES) ?>)" class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-lg bg-violet-100/90 hover:bg-violet-200 dark:bg-violet-950/60 dark:hover:bg-violet-900/60 text-violet-700 dark:text-violet-300 font-bold text-[10px] transition cursor-pointer border border-violet-200/80 dark:border-violet-800/60 shadow-2xs" title="Splits dit idee uit naar behapbare taken via Google Gemini Pro">
|
||||
<i data-lucide="sparkles" class="w-3 h-3 text-violet-500"></i>
|
||||
<span>Werk uit met AI</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Upvote button -->
|
||||
@@ -317,6 +326,175 @@ $categories = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: AI Idee Uitwerken & Exporteren (Release Google Gemini Pro) -->
|
||||
<div id="aiBreakdownModal" onclick="if(event.target === this) closeAiBreakdownModal()" class="hidden fixed inset-0 z-50 bg-slate-950/75 backdrop-blur-sm flex items-center justify-center p-3 sm:p-4 overflow-y-auto">
|
||||
<div class="fociz-card p-5 sm:p-6 shadow-2xl max-w-2xl w-full my-8 space-y-4 max-h-[92vh] flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 flex-shrink-0">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-9 h-9 rounded-xl bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-300 flex items-center justify-center flex-shrink-0">
|
||||
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm sm:text-base font-bold text-slate-900 dark:text-white flex items-center space-x-2">
|
||||
<span>Idee Uitwerken naar Taken via AI</span>
|
||||
</h3>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400">Google Gemini Pro ontleedt het idee in behapbare items</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="hidden sm:inline-flex px-2 py-0.5 rounded-full bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800/60 text-[10px] font-bold items-center space-x-1">
|
||||
<i data-lucide="shield-check" class="w-3 h-3"></i>
|
||||
<span>Zero-Data Leakage</span>
|
||||
</span>
|
||||
<button onclick="closeAiBreakdownModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable content area -->
|
||||
<div class="space-y-4 overflow-y-auto pr-1 flex-1">
|
||||
<!-- Geselecteerd Idee Preview -->
|
||||
<div class="p-3 rounded-xl bg-violet-50/70 dark:bg-violet-950/20 border border-violet-200/70 dark:border-violet-800/40">
|
||||
<div class="flex items-center justify-between text-[10px] text-violet-700 dark:text-violet-300 font-bold uppercase tracking-wider mb-1">
|
||||
<span>Oorspronkelijk Brainstorm Idee</span>
|
||||
<span id="breakdownSessionLabel" class="font-normal normal-case text-slate-500 dark:text-slate-400"></span>
|
||||
</div>
|
||||
<p id="breakdownIdeaText" class="text-xs text-slate-800 dark:text-slate-200 italic font-medium leading-relaxed"></p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State met Spinner -->
|
||||
<div id="breakdownLoading" class="py-12 flex flex-col items-center justify-center space-y-3 text-center">
|
||||
<div class="w-10 h-10 rounded-full border-3 border-violet-500/20 border-t-violet-600 animate-spin"></div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-bold text-slate-900 dark:text-white">Google Gemini Pro aan het werk...</p>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400">Idee wordt ontleed in 3-5 concrete user stories en acceptatiecriteria</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="breakdownError" class="hidden p-4 rounded-xl bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 text-xs">
|
||||
<div class="flex items-center space-x-2 font-bold mb-1">
|
||||
<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400"></i>
|
||||
<span>Kon taken niet genereren</span>
|
||||
</div>
|
||||
<p id="breakdownErrorMsg" class="text-[11px] text-rose-700 dark:text-rose-300"></p>
|
||||
</div>
|
||||
|
||||
<!-- Resultaat Container -->
|
||||
<div id="breakdownResult" class="hidden space-y-4">
|
||||
<!-- AI Aanpak Samenvatting -->
|
||||
<div id="breakdownSummaryBox" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-xs flex items-start space-x-2.5">
|
||||
<i data-lucide="sparkles" class="w-4 h-4 text-violet-500 flex-shrink-0 mt-0.5"></i>
|
||||
<div>
|
||||
<strong class="text-slate-900 dark:text-white font-bold block text-xs">Geadviseerde Aanpak:</strong>
|
||||
<p id="breakdownSummaryText" class="text-slate-600 dark:text-slate-300 text-[11px] mt-0.5 leading-relaxed"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bestemming Kiezer Tabs -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-bold text-slate-900 dark:text-white">Kies Export Bestemming:</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button type="button" onclick="setBreakdownDestination('kanban')" id="destBtn-kanban" class="dest-btn p-2.5 rounded-xl border text-xs font-bold transition flex flex-col items-center justify-center space-y-1 cursor-pointer bg-indigo-50 dark:bg-indigo-950/40 border-indigo-500 text-indigo-700 dark:text-indigo-300">
|
||||
<i data-lucide="trello" class="w-4 h-4"></i>
|
||||
<span>Kanban Bord</span>
|
||||
</button>
|
||||
<button type="button" onclick="setBreakdownDestination('timeline')" id="destBtn-timeline" class="dest-btn p-2.5 rounded-xl border text-xs font-semibold transition flex flex-col items-center justify-center space-y-1 cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400 hover:border-slate-300">
|
||||
<i data-lucide="clock" class="w-4 h-4"></i>
|
||||
<span>Tijdlijn / Gantt</span>
|
||||
</button>
|
||||
<button type="button" onclick="setBreakdownDestination('planner')" id="destBtn-planner" class="dest-btn p-2.5 rounded-xl border text-xs font-semibold transition flex flex-col items-center justify-center space-y-1 cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400 hover:border-slate-300">
|
||||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||||
<span>Dagplanner</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bestemmings-specifieke opties -->
|
||||
<!-- Opties voor Kanban Bord -->
|
||||
<div id="destOptions-kanban" class="grid grid-cols-1 sm:grid-cols-2 gap-3 p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-xs">
|
||||
<div>
|
||||
<label class="block font-semibold text-slate-700 dark:text-slate-300 mb-1">Kanban Bord</label>
|
||||
<select id="breakdownBoardSelect" onchange="onBreakdownBoardChange(this.value)" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<option value="">Borden laden...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-slate-700 dark:text-slate-300 mb-1">Kolom / Status</label>
|
||||
<select id="breakdownLaneSelect" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<option value="0">Standaard (eerste kolom)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opties voor Tijdlijn -->
|
||||
<div id="destOptions-timeline" class="hidden grid grid-cols-1 sm:grid-cols-3 gap-3 p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-xs">
|
||||
<div class="sm:col-span-1">
|
||||
<label class="block font-semibold text-slate-700 dark:text-slate-300 mb-1">Project Bord</label>
|
||||
<select id="breakdownTimelineBoardSelect" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<option value="">Borden laden...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-slate-700 dark:text-slate-300 mb-1">Startdatum</label>
|
||||
<input type="date" id="breakdownStartDate" value="<?= date('Y-m-d') ?>" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-slate-700 dark:text-slate-300 mb-1">Einddatum</label>
|
||||
<input type="date" id="breakdownEndDate" value="<?= date('Y-m-d', strtotime('+3 days')) ?>" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opties voor Dagplanner -->
|
||||
<div id="destOptions-planner" class="hidden p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-xs">
|
||||
<div class="max-w-xs">
|
||||
<label class="block font-semibold text-slate-700 dark:text-slate-300 mb-1">Plandatum</label>
|
||||
<input type="date" id="breakdownPlanDate" value="<?= date('Y-m-d') ?>" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Taken Lijst -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
||||
<span>Gegenereerde Taken</span>
|
||||
<span id="breakdownTasksBadge" class="px-2 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-950 text-indigo-700 dark:text-indigo-400 font-mono text-[10px]">0</span>
|
||||
</span>
|
||||
<button type="button" onclick="addManualBreakdownItem()" class="text-xs text-indigo-600 dark:text-indigo-400 font-bold hover:underline flex items-center space-x-1 cursor-pointer">
|
||||
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
|
||||
<span>Taak toevoegen</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="breakdownTasksContainer" class="space-y-2.5">
|
||||
<!-- Wordt dynamisch gevuld met renderBreakdownTasks() -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="pt-3 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between flex-shrink-0">
|
||||
<button type="button" onclick="regenerateBreakdown()" id="breakdownRegenBtn" class="px-3.5 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-xs font-semibold transition cursor-pointer flex items-center space-x-1.5">
|
||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
|
||||
<span>Opnieuw Genereren</span>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="button" onclick="closeAiBreakdownModal()" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
|
||||
Annuleren
|
||||
</button>
|
||||
<button type="button" onclick="submitBreakdownExport()" id="breakdownExportBtn" class="px-5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-2">
|
||||
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||||
<span id="breakdownExportBtnText">Exporteer Taken</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sessionId = <?= $sessionId ?>;
|
||||
|
||||
@@ -544,20 +722,366 @@ async function aiSuggestRoadmapMeta(btn) {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij ophalen van AI advies');
|
||||
if (btn) {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// AI IDEE UITWERKEN & EXPORTEREN LOGICA
|
||||
// ==========================================
|
||||
let currentBreakdownIdea = '';
|
||||
let currentBreakdownCardId = 0;
|
||||
let currentBreakdownItems = [];
|
||||
let currentDestination = 'kanban';
|
||||
let cachedBoards = [];
|
||||
|
||||
async function openAiBreakdownModal(cardId, content) {
|
||||
currentBreakdownCardId = cardId;
|
||||
currentBreakdownIdea = content;
|
||||
currentBreakdownItems = [];
|
||||
|
||||
document.getElementById('breakdownIdeaText').innerText = `"${content}"`;
|
||||
document.getElementById('breakdownSessionLabel').innerText = `Sessie: <?= htmlspecialchars(addslashes($session['title'])) ?> (<?= htmlspecialchars(addslashes($session['theme'])) ?>)`;
|
||||
|
||||
const modal = document.getElementById('aiBreakdownModal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Reset weergave
|
||||
document.getElementById('breakdownLoading').classList.remove('hidden');
|
||||
document.getElementById('breakdownResult').classList.add('hidden');
|
||||
document.getElementById('breakdownError').classList.add('hidden');
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Haal borden op
|
||||
await loadBoardsForBreakdown();
|
||||
|
||||
// Haal AI taken op
|
||||
await fetchAiBreakdown(content);
|
||||
}
|
||||
|
||||
function closeAiBreakdownModal() {
|
||||
const modal = document.getElementById('aiBreakdownModal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function regenerateBreakdown() {
|
||||
if (!currentBreakdownIdea) return;
|
||||
const regenBtn = document.getElementById('breakdownRegenBtn');
|
||||
if (regenBtn) {
|
||||
regenBtn.disabled = true;
|
||||
regenBtn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Bezig...</span>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
document.getElementById('breakdownLoading').classList.remove('hidden');
|
||||
document.getElementById('breakdownResult').classList.add('hidden');
|
||||
document.getElementById('breakdownError').classList.add('hidden');
|
||||
|
||||
await fetchAiBreakdown(currentBreakdownIdea);
|
||||
|
||||
if (regenBtn) {
|
||||
regenBtn.disabled = false;
|
||||
regenBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i><span>Opnieuw Genereren</span>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAiBreakdown(content) {
|
||||
try {
|
||||
const res = await fetch('/api/ai.php?action=breakdown_idea', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
content: content,
|
||||
session_title: '<?= htmlspecialchars(addslashes($session['title'])) ?>',
|
||||
theme: '<?= htmlspecialchars(addslashes($session['theme'])) ?>',
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success || !data.items || data.items.length === 0) {
|
||||
document.getElementById('breakdownLoading').classList.add('hidden');
|
||||
document.getElementById('breakdownError').classList.remove('hidden');
|
||||
document.getElementById('breakdownErrorMsg').innerText = data.error || 'Geen taken gegenereerd door AI.';
|
||||
return;
|
||||
}
|
||||
|
||||
currentBreakdownItems = data.items.map((it, idx) => ({
|
||||
id: idx + 1,
|
||||
selected: true,
|
||||
title: it.title || 'Nieuwe taak',
|
||||
description: it.description || '',
|
||||
priority: it.priority || 'P2',
|
||||
estimated_minutes: parseInt(it.estimated_minutes) || 45,
|
||||
color: it.color || 'indigo',
|
||||
category: it.category || 'Feature',
|
||||
}));
|
||||
|
||||
document.getElementById('breakdownSummaryText').innerText = data.summary || 'Dit idee is opgesplitst in behapbare deeltaken voor snelle executie.';
|
||||
|
||||
renderBreakdownTasks();
|
||||
|
||||
document.getElementById('breakdownLoading').classList.add('hidden');
|
||||
document.getElementById('breakdownResult').classList.remove('hidden');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} catch (err) {
|
||||
document.getElementById('breakdownLoading').classList.add('hidden');
|
||||
document.getElementById('breakdownError').classList.remove('hidden');
|
||||
document.getElementById('breakdownErrorMsg').innerText = 'Netwerkfout bij communicatie met AI: ' + err.message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderBreakdownTasks() {
|
||||
const container = document.getElementById('breakdownTasksContainer');
|
||||
const badge = document.getElementById('breakdownTasksBadge');
|
||||
if (!container) return;
|
||||
|
||||
badge.innerText = currentBreakdownItems.length;
|
||||
container.innerHTML = '';
|
||||
|
||||
currentBreakdownItems.forEach((item, idx) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'p-3 rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#0c0f17] space-y-2 text-xs transition hover:border-indigo-300 dark:hover:border-indigo-700/60 shadow-2xs';
|
||||
div.innerHTML = `
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center space-x-2 flex-1">
|
||||
<input type="checkbox" onchange="toggleBreakdownItemSelect(${idx}, this.checked)" ${item.selected ? 'checked' : ''} class="w-4 h-4 rounded text-indigo-600 focus:ring-0 border-slate-300 dark:border-slate-700 cursor-pointer">
|
||||
<input type="text" value="${escapeHtml(item.title)}" onchange="updateBreakdownItemField(${idx}, 'title', this.value)" placeholder="Taaktitel" class="w-full font-bold text-slate-900 dark:text-white bg-transparent border-b border-transparent hover:border-slate-300 focus:border-indigo-500 focus:outline-none px-1 py-0.5 text-xs">
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
<select onchange="updateBreakdownItemField(${idx}, 'priority', this.value)" class="bg-slate-100 dark:bg-slate-800 text-[10px] font-bold rounded-lg px-2 py-1 text-slate-700 dark:text-slate-300 border-none focus:ring-0 cursor-pointer">
|
||||
<option value="P1" ${item.priority === 'P1' ? 'selected' : ''}>P1 (Brandend)</option>
|
||||
<option value="P2" ${item.priority === 'P2' ? 'selected' : ''}>P2 (Normaal)</option>
|
||||
<option value="P3" ${item.priority === 'P3' ? 'selected' : ''}>P3 (Laag)</option>
|
||||
</select>
|
||||
<select onchange="updateBreakdownItemField(${idx}, 'estimated_minutes', parseInt(this.value))" class="bg-slate-100 dark:bg-slate-800 text-[10px] font-semibold rounded-lg px-2 py-1 text-slate-700 dark:text-slate-300 border-none focus:ring-0 cursor-pointer">
|
||||
<option value="15" ${item.estimated_minutes === 15 ? 'selected' : ''}>15m</option>
|
||||
<option value="30" ${item.estimated_minutes === 30 ? 'selected' : ''}>30m</option>
|
||||
<option value="45" ${item.estimated_minutes === 45 ? 'selected' : ''}>45m</option>
|
||||
<option value="60" ${item.estimated_minutes === 60 ? 'selected' : ''}>60m</option>
|
||||
<option value="90" ${item.estimated_minutes === 90 ? 'selected' : ''}>90m</option>
|
||||
<option value="120" ${item.estimated_minutes === 120 ? 'selected' : ''}>120m</option>
|
||||
</select>
|
||||
<button type="button" onclick="removeBreakdownItem(${idx})" class="p-1 text-slate-400 hover:text-rose-500 rounded transition cursor-pointer" title="Verwijder taak">
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea onchange="updateBreakdownItemField(${idx}, 'description', this.value)" placeholder="Toelichting & acceptatiecriteria..." rows="2" class="w-full bg-slate-50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg p-2 text-[11px] text-slate-700 dark:text-slate-300 focus:outline-none focus:border-indigo-500 leading-relaxed resize-none">${escapeHtml(item.description)}</textarea>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
|
||||
updateExportButtonText();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function toggleBreakdownItemSelect(idx, isChecked) {
|
||||
if (currentBreakdownItems[idx]) {
|
||||
currentBreakdownItems[idx].selected = isChecked;
|
||||
updateExportButtonText();
|
||||
}
|
||||
}
|
||||
|
||||
function updateBreakdownItemField(idx, field, value) {
|
||||
if (currentBreakdownItems[idx]) {
|
||||
currentBreakdownItems[idx][field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function addManualBreakdownItem() {
|
||||
currentBreakdownItems.push({
|
||||
id: Date.now(),
|
||||
selected: true,
|
||||
title: 'Nieuwe deel-taak',
|
||||
description: 'Context en criteria toevoegen...',
|
||||
priority: 'P2',
|
||||
estimated_minutes: 30,
|
||||
color: 'indigo',
|
||||
category: 'Feature'
|
||||
});
|
||||
renderBreakdownTasks();
|
||||
}
|
||||
|
||||
function removeBreakdownItem(idx) {
|
||||
currentBreakdownItems.splice(idx, 1);
|
||||
renderBreakdownTasks();
|
||||
}
|
||||
|
||||
function setBreakdownDestination(dest) {
|
||||
currentDestination = dest;
|
||||
const dests = ['kanban', 'timeline', 'planner'];
|
||||
|
||||
dests.forEach(d => {
|
||||
const btn = document.getElementById('destBtn-' + d);
|
||||
const opt = document.getElementById('destOptions-' + d);
|
||||
if (d === dest) {
|
||||
btn.className = 'dest-btn p-2.5 rounded-xl border text-xs font-bold transition flex flex-col items-center justify-center space-y-1 cursor-pointer bg-indigo-50 dark:bg-indigo-950/40 border-indigo-500 text-indigo-700 dark:text-indigo-300';
|
||||
if (opt) opt.classList.remove('hidden');
|
||||
} else {
|
||||
btn.className = 'dest-btn p-2.5 rounded-xl border text-xs font-semibold transition flex flex-col items-center justify-center space-y-1 cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400 hover:border-slate-300';
|
||||
if (opt) opt.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
updateExportButtonText();
|
||||
}
|
||||
|
||||
function updateExportButtonText() {
|
||||
const selectedCount = currentBreakdownItems.filter(i => i.selected).length;
|
||||
const btnText = document.getElementById('breakdownExportBtnText');
|
||||
if (!btnText) return;
|
||||
|
||||
let targetLabel = 'Kanban Bord';
|
||||
if (currentDestination === 'timeline') targetLabel = 'Tijdlijn';
|
||||
if (currentDestination === 'planner') targetLabel = 'Dagplanner';
|
||||
|
||||
btnText.innerText = `Exporteer ${selectedCount} ${selectedCount === 1 ? 'Taak' : 'Taken'} naar ${targetLabel}`;
|
||||
}
|
||||
|
||||
async function loadBoardsForBreakdown() {
|
||||
try {
|
||||
const res = await fetch('/api/board.php?action=get_boards');
|
||||
const data = await res.json();
|
||||
if (data.success && data.boards) {
|
||||
cachedBoards = data.boards;
|
||||
const selectKanban = document.getElementById('breakdownBoardSelect');
|
||||
const selectTimeline = document.getElementById('breakdownTimelineBoardSelect');
|
||||
|
||||
selectKanban.innerHTML = '';
|
||||
selectTimeline.innerHTML = '';
|
||||
|
||||
cachedBoards.forEach(b => {
|
||||
const opt1 = document.createElement('option');
|
||||
opt1.value = b.id;
|
||||
opt1.textContent = b.title;
|
||||
selectKanban.appendChild(opt1);
|
||||
|
||||
const opt2 = document.createElement('option');
|
||||
opt2.value = b.id;
|
||||
opt2.textContent = b.title;
|
||||
selectTimeline.appendChild(opt2);
|
||||
});
|
||||
|
||||
if (cachedBoards.length > 0) {
|
||||
await onBreakdownBoardChange(cachedBoards[0].id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fout bij ophalen van borden:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onBreakdownBoardChange(boardId) {
|
||||
if (!boardId) return;
|
||||
try {
|
||||
const res = await fetch(`/api/board.php?action=get_board_data&board_id=${boardId}`);
|
||||
const data = await res.json();
|
||||
const laneSelect = document.getElementById('breakdownLaneSelect');
|
||||
laneSelect.innerHTML = '';
|
||||
|
||||
if (data.success && data.lanes && data.lanes.length > 0) {
|
||||
data.lanes.forEach(lane => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = lane.id;
|
||||
opt.textContent = lane.name;
|
||||
laneSelect.appendChild(opt);
|
||||
});
|
||||
} else {
|
||||
laneSelect.innerHTML = '<option value="0">Standaard kolom</option>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fout bij ophalen van lanes:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBreakdownExport() {
|
||||
const selectedItems = currentBreakdownItems.filter(i => i.selected);
|
||||
if (selectedItems.length === 0) {
|
||||
alert('Selecteer minimaal één taak om te exporteren.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('breakdownExportBtn');
|
||||
const btnText = document.getElementById('breakdownExportBtnText');
|
||||
const origHtml = btn.innerHTML;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="flex items-center space-x-2"><i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Exporteren...</span></span>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
const payload = {
|
||||
destination: currentDestination,
|
||||
items: selectedItems,
|
||||
};
|
||||
|
||||
if (currentDestination === 'kanban') {
|
||||
payload.board_id = document.getElementById('breakdownBoardSelect').value;
|
||||
payload.lane_id = document.getElementById('breakdownLaneSelect').value;
|
||||
} else if (currentDestination === 'timeline') {
|
||||
payload.board_id = document.getElementById('breakdownTimelineBoardSelect').value;
|
||||
payload.start_date = document.getElementById('breakdownStartDate').value;
|
||||
payload.end_date = document.getElementById('breakdownEndDate').value;
|
||||
} else if (currentDestination === 'planner') {
|
||||
payload.plan_date = document.getElementById('breakdownPlanDate').value;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/ai.php?action=save_breakdown_items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
closeAiBreakdownModal();
|
||||
let destMsg = '';
|
||||
let targetUrl = '';
|
||||
if (currentDestination === 'kanban') {
|
||||
destMsg = `Succesvol ${data.created_count} taken toegevoegd aan het Kanban Bord!`;
|
||||
targetUrl = `/board.php?id=${payload.board_id}`;
|
||||
} else if (currentDestination === 'timeline') {
|
||||
destMsg = `Succesvol ${data.created_count} taken gepland op de Project Tijdlijn!`;
|
||||
targetUrl = `/timeline.php?board_id=${payload.board_id}`;
|
||||
} else {
|
||||
destMsg = `Succesvol ${data.created_count} taken toegevoegd aan uw Dagplanner!`;
|
||||
targetUrl = `/planner.php?date=${payload.plan_date}`;
|
||||
}
|
||||
|
||||
if (confirm(`${destMsg}\n\nWilt u nu direct naar deze pagina gaan om ze te bekijken?`)) {
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
} else {
|
||||
alert('Fout bij exporteren van taken: ' + (data.error || 'Onbekende fout'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Netwerkfout bij exporteren: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeAddCardModal();
|
||||
closeConvertModal();
|
||||
closeAiBreakdownModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user