Files
Fociz/roadmap.php
T

329 lines
21 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;
}
$sql .= " ORDER BY r.quarter ASC, r.priority DESC, r.due_date ASC";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$roadmapItems = $stmt->fetchAll();
$quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
?>
<div class="space-y-6 max-w-7xl mx-auto">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 rounded-3xl p-6 sm:p-7 shadow-sm dark:shadow-xl transition">
<div>
<div class="flex items-center space-x-2 text-emerald-600 dark:text-emerald-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">
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-emerald-500 hover:bg-emerald-600 text-slate-950 text-xs font-bold transition shadow-lg shadow-emerald-500/20 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="flex flex-wrap items-center justify-between gap-3 bg-white dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800/80 rounded-2xl p-4 shadow-sm transition">
<!-- Quarter Tabs -->
<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-xl transition font-medium <?= $selectedQuarter === 'ALL' ? 'bg-emerald-500 text-slate-950 font-bold shadow-sm' : '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' ?>">
Alle Quarters
</a>
<?php foreach ($quarters as $q): ?>
<a href="/roadmap.php?quarter=<?= $q ?>&status=<?= htmlspecialchars($selectedStatus) ?>" class="px-3 py-1.5 rounded-xl transition font-medium <?= $selectedQuarter === $q ? 'bg-emerald-500 text-slate-950 font-bold shadow-sm' : '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' ?>">
<?= $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-medium">Status:</span>
<select onchange="window.location.href='/roadmap.php?quarter=<?= htmlspecialchars($selectedQuarter) ?>&status='+this.value" class="bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1 text-slate-900 dark:text-white text-xs focus:outline-none">
<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="bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800/80 rounded-3xl p-12 text-center shadow-sm">
<i data-lucide="map-pin" class="w-12 h-12 text-emerald-500/40 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-amber-600 dark:text-amber-400 underline font-semibold">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): ?>
<div class="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 rounded-3xl p-5 shadow-sm hover:shadow-md dark:shadow-lg transition space-y-3" id="roadmap-item-<?= $item['id'] ?>">
<!-- 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-full bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 font-mono text-xs font-bold border border-emerald-500/30">
<?= htmlspecialchars($item['quarter']) ?>
</span>
<!-- Type badge -->
<span class="px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-[10px] font-semibold">
<?= htmlspecialchars($item['type']) ?>
</span>
<!-- Priority badge -->
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $item['priority'] === 'CRITICAL' ? 'bg-red-500/15 text-red-700 dark:text-red-400 border border-red-500/30' : ($item['priority'] === 'HIGH' ? 'bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400') ?>">
<?= 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 py-0.5 rounded-full bg-blue-50 dark:bg-blue-950/60 border border-blue-200 dark:border-blue-800/60 text-blue-700 dark:text-blue-300 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 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1 text-xs text-slate-800 dark:text-slate-200 font-semibold focus:outline-none">
<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-red-600 dark:text-slate-600 dark:hover:text-red-400 hover:bg-red-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 dark:bg-slate-950/50 rounded-xl p-2.5 text-[11px] text-slate-500 dark:text-slate-400 flex items-center space-x-2 border border-slate-200 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 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-medium w-16">Voortgang:</span>
<div class="flex-1 bg-slate-100 dark:bg-slate-950 h-2 rounded-full overflow-hidden border border-slate-200 dark:border-slate-800">
<div class="bg-gradient-to-r from-emerald-500 to-teal-400 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-1.5 py-0.5 rounded 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">25%</button>
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 50)" class="px-1.5 py-0.5 rounded 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">50%</button>
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 100)" class="px-1.5 py-0.5 rounded bg-emerald-500/20 text-[10px] text-emerald-700 dark:text-emerald-400 hover:text-emerald-800 dark:hover:text-emerald-300 font-bold cursor-pointer">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-medium">
<i data-lucide="calendar" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-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" class="hidden fixed inset-0 z-50 bg-black/60 dark:bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-3xl p-6 shadow-2xl max-w-lg w-full transition">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-base font-bold text-slate-900 dark:text-white flex items-center space-x-2">
<i data-lucide="map" class="w-4 h-4 text-emerald-600 dark:text-emerald-400"></i>
<span>Nieuw Roadmap Item Toevoegen</span>
</h3>
<button onclick="document.getElementById('newItemModal').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 dark:hover:text-white cursor-pointer">
<i data-lucide="x" class="w-5 h-5"></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 dark:bg-slate-950 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-emerald-500">
</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 dark:bg-slate-950 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 resize-none"></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 dark:bg-slate-950 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">
<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 dark:bg-slate-950 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">
<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 dark:bg-slate-950 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">
<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 dark:bg-slate-950 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">
</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 dark:bg-slate-950 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">
</div>
<div class="flex space-x-2 pt-2">
<button onclick="submitManualItem()" class="flex-1 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer">
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 transition cursor-pointer">
Annuleren
</button>
</div>
</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();
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>