Files
Fociz/roadmap.php
T

437 lines
25 KiB
PHP

<?php
/**
* Fociz Roadmap & Tijdlijn
*/
require_once __DIR__ . '/includes/header.php';
$db = get_db_connection();
$selectedQuarter = $_GET['quarter'] ?? 'ALL';
$selectedStatus = $_GET['status'] ?? 'ALL';
$sql = "
SELECT r.*, u.name as creator_name, c.content as brainstorm_snippet
FROM roadmap_items r
JOIN users u ON r.created_by_id = u.id
LEFT JOIN brainstorm_cards c ON r.origin_brainstorm_card_id = c.id
WHERE 1=1
";
$params = [];
if ($selectedQuarter !== 'ALL') {
$sql .= " AND r.quarter = ?";
$params[] = $selectedQuarter;
}
if ($selectedStatus !== 'ALL') {
$sql .= " AND r.status = ?";
$params[] = $selectedStatus;
}
$roadmapItems = [];
try {
$stmt = $db->prepare($sql);
$stmt->execute($params);
$roadmapItems = $stmt->fetchAll() ?: [];
} catch (Throwable $e) {
error_log("Fout bij ophalen roadmap items: " . $e->getMessage());
ensure_tables_exist($db);
}
$quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
?>
<div class="space-y-6 max-w-7xl mx-auto">
<!-- Header -->
<div class="fociz-card p-6 sm:p-7 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
<i data-lucide="map" class="w-4 h-4"></i>
<span>Planning & Strategie</span>
</div>
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 dark:text-white tracking-tight">
Roadmap & Tijdlijn
</h1>
<p class="text-slate-500 dark:text-slate-400 text-xs sm:text-sm mt-1 max-w-2xl">
Centraal overzicht van alle mijlpalen, features en doelen, direct voortgekomen uit brainstorms en gesynchroniseerd met Jira.
</p>
</div>
<button onclick="document.getElementById('newItemModal').classList.remove('hidden')" class="flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs flex-shrink-0 cursor-pointer">
<i data-lucide="plus" class="w-4 h-4"></i>
<span>Nieuw Item</span>
</button>
</div>
<!-- Filters: Quarters & Status -->
<div class="fociz-card p-4 flex flex-wrap items-center justify-between gap-3">
<!-- Quarter Tabs (tevens drop targets) -->
<div class="flex items-center space-x-1 text-xs">
<a href="/roadmap.php?quarter=ALL&status=<?= htmlspecialchars($selectedStatus) ?>" class="px-3 py-1.5 rounded-lg transition font-semibold <?= $selectedQuarter === 'ALL' ? 'bg-indigo-600 text-white shadow-xs' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80' ?>">
Alle Quarters
</a>
<?php foreach ($quarters as $q): ?>
<a
href="/roadmap.php?quarter=<?= $q ?>&status=<?= htmlspecialchars($selectedStatus) ?>"
class="roadmap-quarter-tab px-3.5 py-1.5 rounded-lg transition-all font-semibold <?= $selectedQuarter === $q ? 'bg-indigo-600 text-white shadow-xs' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80' ?>"
data-quarter="<?= $q ?>"
ondragover="handleRoadmapQuarterDragOver(event)"
ondragleave="handleRoadmapQuarterDragLeave(event)"
ondrop="handleRoadmapQuarterDrop(event, '<?= $q ?>')"
title="Sleep een roadmap item hierheen om het kwartaal direct te wijzigen"
>
<?= $q ?>
</a>
<?php endforeach; ?>
</div>
<!-- Status Filter -->
<div class="flex items-center space-x-2 text-xs">
<span class="text-slate-500 dark:text-slate-400 font-semibold">Status:</span>
<select onchange="window.location.href='/roadmap.php?quarter=<?= htmlspecialchars($selectedQuarter) ?>&status='+this.value" class="bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-slate-900 dark:text-white text-xs font-semibold focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="ALL" <?= $selectedStatus === 'ALL' ? 'selected' : '' ?>>Alle Statussen</option>
<option value="PLANNED" <?= $selectedStatus === 'PLANNED' ? 'selected' : '' ?>>Gepland</option>
<option value="IN_PROGRESS" <?= $selectedStatus === 'IN_PROGRESS' ? 'selected' : '' ?>>In Uitvoering</option>
<option value="REVIEW" <?= $selectedStatus === 'REVIEW' ? 'selected' : '' ?>>In Review</option>
<option value="COMPLETED" <?= $selectedStatus === 'COMPLETED' ? 'selected' : '' ?>>Voltooid</option>
</select>
</div>
</div>
<!-- Roadmap Items Lijst -->
<div class="space-y-4">
<?php if (empty($roadmapItems)): ?>
<div class="fociz-card p-12 text-center">
<i data-lucide="map-pin" class="w-12 h-12 text-slate-300 dark:text-slate-700 mx-auto mb-3"></i>
<h3 class="text-base font-bold text-slate-900 dark:text-white">Geen items gevonden op de roadmap</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1 max-w-sm mx-auto">
Ga naar een <a href="/brainstorm.php" class="text-indigo-600 dark:text-indigo-400 underline font-medium">Brainstorm sessie</a> en zet ideeën met één klik om naar de roadmap, of voeg handmatig een item toe.
</p>
</div>
<?php else: ?>
<?php foreach ($roadmapItems as $item):
$statusBorder = match($item['status']) {
'COMPLETED' => 'border-l-4 border-l-emerald-500',
'IN_PROGRESS' => 'border-l-4 border-l-indigo-500',
'REVIEW' => 'border-l-4 border-l-amber-500',
default => 'border-l-4 border-l-slate-300 dark:border-l-slate-700',
};
?>
<div
id="roadmap-item-<?= $item['id'] ?>"
draggable="true"
ondragstart="handleRoadmapItemDragStart(event, <?= $item['id'] ?>)"
ondragend="handleRoadmapItemDragEnd(event)"
class="fociz-card-interactive p-5 space-y-3.5 <?= $statusBorder ?> cursor-grab active:cursor-grabbing transition-all duration-150"
title="Sleep naar een ander kwartaal (Q1, Q2, Q3, Q4) bovenaan"
>
<!-- Top header van item -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div class="flex items-center space-x-2.5 flex-wrap">
<!-- Quarter badge -->
<span class="px-2.5 py-0.5 rounded-lg bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 font-mono text-xs font-bold border border-indigo-200 dark:border-indigo-500/20">
<?= htmlspecialchars($item['quarter']) ?>
</span>
<!-- Type badge -->
<span class="px-2 py-0.5 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-[10px] font-semibold border border-slate-200 dark:border-slate-700">
<?= htmlspecialchars($item['type']) ?>
</span>
<!-- Priority badge -->
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $item['priority'] === 'CRITICAL' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($item['priority'] === 'HIGH' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= htmlspecialchars($item['priority']) ?>
</span>
<!-- Jira Issue Key link -->
<?php if (!empty($item['jira_issue_key'])): ?>
<a href="/profile.php" class="flex items-center space-x-1 px-2.5 py-0.5 rounded-lg bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 text-[10px] font-mono hover:underline">
<i data-lucide="layers" class="w-3 h-3"></i>
<span><?= htmlspecialchars($item['jira_issue_key']) ?></span>
</a>
<?php endif; ?>
</div>
<!-- Status Selector -->
<div class="flex items-center space-x-2">
<select onchange="updateRoadmapStatus(<?= $item['id'] ?>, this.value)" class="bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1 text-xs text-slate-800 dark:text-slate-200 font-bold focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="PLANNED" <?= $item['status'] === 'PLANNED' ? 'selected' : '' ?>>⏳ Gepland</option>
<option value="IN_PROGRESS" <?= $item['status'] === 'IN_PROGRESS' ? 'selected' : '' ?>>⚡ In Uitvoering</option>
<option value="REVIEW" <?= $item['status'] === 'REVIEW' ? 'selected' : '' ?>>🔍 In Review</option>
<option value="COMPLETED" <?= $item['status'] === 'COMPLETED' ? 'selected' : '' ?>>✅ Voltooid</option>
</select>
<button onclick="deleteRoadmapItem(<?= $item['id'] ?>)" title="Verwijderen" class="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 dark:text-slate-500 dark:hover:text-rose-400 hover:bg-rose-500/10 transition cursor-pointer">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
</div>
<!-- Titel & Beschrijving -->
<div>
<h3 class="text-base font-bold text-slate-900 dark:text-white leading-snug">
<?= htmlspecialchars($item['title']) ?>
</h3>
<?php if (!empty($item['description'])): ?>
<p class="text-slate-600 dark:text-slate-400 text-xs mt-1 whitespace-pre-wrap leading-relaxed">
<?= htmlspecialchars($item['description']) ?>
</p>
<?php endif; ?>
</div>
<!-- Origin Brainstorm snippet -->
<?php if (!empty($item['origin_brainstorm_card_id'])): ?>
<div class="bg-slate-50/70 dark:bg-[#0c0f17]/60 rounded-xl p-2.5 text-[11px] text-slate-600 dark:text-slate-400 flex items-center space-x-2 border border-slate-200/80 dark:border-slate-800">
<i data-lucide="lightbulb" class="w-3.5 h-3.5 text-amber-500 flex-shrink-0"></i>
<span class="truncate">Ontstaan uit brainstormsessie</span>
</div>
<?php endif; ?>
<!-- Voortgangsbalk & Deadline -->
<div class="pt-2.5 border-t border-slate-100 dark:border-slate-800/60 flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
<!-- Progress Slider -->
<div class="flex items-center space-x-3 flex-1 max-w-md">
<span class="text-slate-500 dark:text-slate-400 text-[11px] font-semibold w-16">Voortgang:</span>
<div class="flex-1 bg-slate-100 dark:bg-slate-900 h-2.5 rounded-full overflow-hidden border border-slate-200 dark:border-slate-800">
<div class="bg-gradient-to-r from-indigo-600 to-sky-500 h-full transition-all" style="width: <?= $item['progress'] ?>%"></div>
</div>
<span class="font-mono text-slate-800 dark:text-slate-200 font-bold w-10 text-right"><?= $item['progress'] ?>%</span>
<div class="flex space-x-1">
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 25)" class="px-2 py-0.5 rounded-lg bg-slate-100 dark:bg-slate-800 text-[10px] text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white cursor-pointer font-semibold transition">25%</button>
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 50)" class="px-2 py-0.5 rounded-lg bg-slate-100 dark:bg-slate-800 text-[10px] text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white cursor-pointer font-semibold transition">50%</button>
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 100)" class="px-2 py-0.5 rounded-lg bg-indigo-50 dark:bg-indigo-500/15 text-[10px] text-indigo-700 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 font-bold cursor-pointer transition">100%</button>
</div>
</div>
<!-- Deadline & Creator -->
<div class="flex items-center space-x-3 text-slate-500 text-[11px]">
<?php if (!empty($item['due_date'])): ?>
<span class="flex items-center space-x-1 text-slate-700 dark:text-slate-300 font-semibold">
<i data-lucide="calendar" class="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400"></i>
<span>Deadline: <?= date('d-m-Y', strtotime($item['due_date'])) ?></span>
</span>
<?php endif; ?>
<span>Toegevoegd door <?= htmlspecialchars($item['creator_name']) ?></span>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- Modal: Nieuw Roadmap Item Handmatig Toevoegen -->
<div id="newItemModal" onclick="if(event.target === this) this.classList.add('hidden')" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
<div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-lg w-full space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
<i data-lucide="map" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span>Nieuw Roadmap Item Toevoegen</span>
</h3>
<button onclick="document.getElementById('newItemModal').classList.add('hidden')" 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 class="space-y-4">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Titel / Doel</label>
<input type="text" id="manualTitle" placeholder="Bijv. 2FA Authenticatie afronden" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Beschrijving / Specificatie</label>
<textarea id="manualDescription" rows="3" placeholder="Wat houdt deze milestone of feature in?" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl p-3 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-none shadow-2xs"></textarea>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Kwartaal</label>
<select id="manualQuarter" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="Q1">Q1</option>
<option value="Q2">Q2</option>
<option value="Q3" selected>Q3</option>
<option value="Q4">Q4</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Type</label>
<select id="manualType" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="FEATURE" selected>Feature</option>
<option value="EPIC">Epic</option>
<option value="MILESTONE">Mijlpaal</option>
<option value="TASK">Taak</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="manualPriority" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="CRITICAL">Kritiek</option>
<option value="HIGH" selected>Hoog</option>
<option value="MEDIUM">Normaal</option>
<option value="LOW">Laag</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Jira Issue Key (optioneel)</label>
<input type="text" id="manualJiraKey" placeholder="Bijv. FOC-101" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-1.5 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Streefdatum</label>
<input type="date" id="manualDueDate" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-1.5 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<button onclick="submitManualItem()" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs">
Toevoegen aan Roadmap
</button>
<button onclick="document.getElementById('newItemModal').classList.add('hidden')" class="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
</div>
</div>
</div>
<script>
async function updateRoadmapStatus(id, status) {
await fetch('/api/roadmap.php?action=update_item', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id, status })
});
}
async function updateRoadmapProgress(id, progress) {
await fetch('/api/roadmap.php?action=update_item', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id, progress })
});
location.reload();
}
async function deleteRoadmapItem(id) {
if (!confirm('Dit roadmap item verwijderen?')) return;
await fetch('/api/roadmap.php?action=delete_item', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id })
});
document.getElementById(`roadmap-item-${id}`)?.remove();
}
async function submitManualItem() {
const title = document.getElementById('manualTitle').value.trim();
const description = document.getElementById('manualDescription').value.trim();
const quarter = document.getElementById('manualQuarter').value;
const type = document.getElementById('manualType').value;
const priority = document.getElementById('manualPriority').value;
const jira_key = document.getElementById('manualJiraKey').value.trim();
const due_date = document.getElementById('manualDueDate').value;
if (!title) {
alert('Titel is verplicht');
return;
}
const res = await fetch('/api/roadmap.php?action=add_item', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ title, description, quarter, type, priority, jira_key, due_date })
});
const data = await res.json();
if (data.success) {
location.reload();
}
}
// --- Drag & Drop van Roadmap Items naar ander Kwartaal (Q1, Q2, Q3, Q4) ---
let draggedRoadmapItemId = null;
function handleRoadmapItemDragStart(e, itemId) {
draggedRoadmapItemId = itemId;
try {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(itemId));
} catch (err) {}
setTimeout(() => {
const el = document.getElementById(`roadmap-item-${itemId}`);
if (el) el.classList.add('opacity-40', 'scale-[0.99]');
}, 0);
}
function handleRoadmapItemDragEnd(e) {
if (draggedRoadmapItemId) {
const el = document.getElementById(`roadmap-item-${draggedRoadmapItemId}`);
if (el) el.classList.remove('opacity-40', 'scale-[0.99]');
draggedRoadmapItemId = null;
}
document.querySelectorAll('.roadmap-quarter-tab').forEach(tab => {
tab.classList.remove('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
});
}
function handleRoadmapQuarterDragOver(e) {
if (!draggedRoadmapItemId) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const tab = e.currentTarget;
if (tab && !tab.classList.contains('ring-2')) {
tab.classList.add('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
}
}
function handleRoadmapQuarterDragLeave(e) {
const tab = e.currentTarget;
if (tab && !tab.contains(e.relatedTarget)) {
tab.classList.remove('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
}
}
async function handleRoadmapQuarterDrop(e, targetQuarter) {
e.preventDefault();
const tab = e.currentTarget;
if (tab) {
tab.classList.remove('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
}
const itemId = draggedRoadmapItemId || parseInt(e.dataTransfer.getData('text/plain'), 10);
draggedRoadmapItemId = null;
if (!itemId || !targetQuarter) return;
try {
const res = await fetch('/api/roadmap.php?action=update_item', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: itemId, quarter: targetQuarter })
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.error || 'Kon item niet verplaatsen');
}
} catch (err) {
console.error('Fout bij verplaatsen roadmap item:', err);
alert('Netwerkfout bij verplaatsen');
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const modal = document.getElementById('newItemModal');
if (modal) modal.classList.add('hidden');
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>