9da056b480
- Implemented roadmap.php for displaying and managing roadmap items with filters for quarter and status. - Created schema.sql to define the database structure for users, invites, daily plans, tasks, brainstorm sessions, brainstorm cards, roadmap items, and integrations. - Developed users.php for admin panel to manage team members and invitations, including functionality to create and revoke invites.
330 lines
18 KiB
PHP
330 lines
18 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">
|
|
<!-- Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-900/80 border border-slate-800 rounded-2xl p-6 shadow-xl">
|
|
<div>
|
|
<div class="flex items-center space-x-2 text-emerald-400 text-xs font-semibold 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-white tracking-tight">
|
|
Roadmap & Tijdlijn
|
|
</h1>
|
|
<p class="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">
|
|
<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-slate-900/40 border border-slate-800/80 rounded-2xl p-4">
|
|
<!-- 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' : 'text-slate-400 hover:text-slate-200 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' : 'text-slate-400 hover:text-slate-200 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">Status:</span>
|
|
<select onchange="window.location.href='/roadmap.php?quarter=<?= htmlspecialchars($selectedQuarter) ?>&status='+this.value" class="bg-slate-800 border border-slate-700 rounded-xl px-2.5 py-1 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-slate-900/60 border border-slate-800/80 rounded-2xl p-12 text-center">
|
|
<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-white">Geen items gevonden op de roadmap</h3>
|
|
<p class="text-xs text-slate-400 mt-1 max-w-sm mx-auto">
|
|
Ga naar een <a href="/brainstorm.php" class="text-amber-400 underline">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-slate-900/80 border border-slate-800 hover:border-slate-700 rounded-2xl p-5 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-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-800 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/20 text-red-400' : ($item['priority'] === 'HIGH' ? 'bg-amber-500/20 text-amber-400' : 'bg-slate-800 text-slate-400') ?>">
|
|
<?= htmlspecialchars($item['priority']) ?>
|
|
</span>
|
|
|
|
<!-- Jira Issue Key link -->
|
|
<?php if (!empty($item['jira_issue_key'])): ?>
|
|
<a href="/integrations.php" class="flex items-center space-x-1 px-2 py-0.5 rounded-full bg-blue-950/60 border border-blue-800/60 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-950 border border-slate-700 rounded-lg px-2.5 py-1 text-xs 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-600 hover:text-red-400 hover:bg-red-500/10 transition">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Titel & Beschrijving -->
|
|
<div>
|
|
<h3 class="text-base font-bold text-white leading-snug">
|
|
<?= htmlspecialchars($item['title']) ?>
|
|
</h3>
|
|
<?php if (!empty($item['description'])): ?>
|
|
<p class="text-slate-400 text-xs mt-1 whitespace-pre-wrap">
|
|
<?= htmlspecialchars($item['description']) ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Origin Brainstorm snippet -->
|
|
<?php if (!empty($item['origin_brainstorm_card_id'])): ?>
|
|
<div class="bg-slate-950/50 rounded-xl p-2.5 text-[11px] text-slate-400 flex items-center space-x-2 border border-slate-800">
|
|
<i data-lucide="lightbulb" class="w-3.5 h-3.5 text-amber-400 flex-shrink-0"></i>
|
|
<span class="truncate">Ontstaan uit brainstormsessie</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Voortgangsbalk & Deadline -->
|
|
<div class="pt-2 border-t 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-400 text-[11px] font-medium w-16">Voortgang:</span>
|
|
<div class="flex-1 bg-slate-950 h-2 rounded-full overflow-hidden border 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-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-800 text-[10px] text-slate-400 hover:text-white">25%</button>
|
|
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 50)" class="px-1.5 py-0.5 rounded bg-slate-800 text-[10px] text-slate-400 hover:text-white">50%</button>
|
|
<button onclick="updateRoadmapProgress(<?= $item['id'] ?>, 100)" class="px-1.5 py-0.5 rounded bg-emerald-500/20 text-[10px] text-emerald-400 hover:text-emerald-300 font-bold">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-300">
|
|
<i data-lucide="calendar" class="w-3.5 h-3.5 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/70 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-2xl max-w-lg w-full">
|
|
<div class="flex items-center justify-between mb-4 pb-3 border-b border-slate-800">
|
|
<h3 class="text-base font-bold text-white flex items-center space-x-2">
|
|
<i data-lucide="map" class="w-4 h-4 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-white">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-300 mb-1">Titel / Doel</label>
|
|
<input type="text" id="manualTitle" placeholder="Bijv. 2FA Authenticatie afronden" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-white text-xs placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-emerald-500">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium 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-950 border border-slate-800 rounded-xl p-3 text-white text-xs 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-medium text-slate-300 mb-1">Kwartaal</label>
|
|
<select id="manualQuarter" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 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-medium text-slate-300 mb-1">Type</label>
|
|
<select id="manualType" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 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-medium text-slate-300 mb-1">Prioriteit</label>
|
|
<select id="manualPriority" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 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-medium text-slate-300 mb-1">Jira Issue Key (optioneel)</label>
|
|
<input type="text" id="manualJiraKey" placeholder="Bijv. FOC-101" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-white text-xs font-mono focus:outline-none">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-300 mb-1">Streefdatum</label>
|
|
<input type="date" id="manualDueDate" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 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">
|
|
Toevoegen aan Roadmap
|
|
</button>
|
|
<button onclick="document.getElementById('newItemModal').classList.add('hidden')" class="px-4 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl text-xs transition">
|
|
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'; ?>
|
|
|