974 lines
54 KiB
PHP
974 lines
54 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'];
|
||
$currentUserId = (int)($currentUser['id'] ?? 0);
|
||
|
||
// Haal alle actieve teamleden op voor toewijzing & agendablokkades
|
||
$teamMembersStmt = $db->query("SELECT id, name, email, role, avatar_url FROM users ORDER BY name ASC");
|
||
$teamMembers = $teamMembersStmt->fetchAll();
|
||
|
||
$outlookUsers = [];
|
||
$intStmt = $db->query("SELECT user_id FROM integrations WHERE provider = 'OUTLOOK' AND is_enabled = 1 AND access_token IS NOT NULL");
|
||
foreach ($intStmt->fetchAll() as $row) {
|
||
$outlookUsers[(int)$row['user_id']] = true;
|
||
}
|
||
foreach ($teamMembers as &$tm) {
|
||
$tm['has_outlook'] = !empty($outlookUsers[(int)$tm['id']]);
|
||
}
|
||
unset($tm);
|
||
?>
|
||
|
||
<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)"
|
||
oncontextmenu="openRoadmapContextMenu(event, <?= $item['id'] ?>); return false;"
|
||
data-title="<?= htmlspecialchars($item['title'], ENT_QUOTES) ?>"
|
||
data-description="<?= htmlspecialchars($item['description'] ?? '', ENT_QUOTES) ?>"
|
||
data-quarter="<?= htmlspecialchars($item['quarter']) ?>"
|
||
data-type="<?= htmlspecialchars($item['type']) ?>"
|
||
data-priority="<?= htmlspecialchars($item['priority']) ?>"
|
||
data-status="<?= htmlspecialchars($item['status']) ?>"
|
||
data-due-date="<?= htmlspecialchars($item['due_date'] ?? '') ?>"
|
||
data-jira-key="<?= htmlspecialchars($item['jira_issue_key'] ?? '') ?>"
|
||
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) of klik met rechtermuisknop"
|
||
>
|
||
|
||
<!-- 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>
|
||
</div>
|
||
|
||
<!-- Modal: Roadmap Item Bewerken -->
|
||
<div id="editRoadmapModal" onclick="if(event.target === this) closeEditRoadmapModal()" 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="edit-3" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||
<span>Roadmap Item Bewerken</span>
|
||
</h3>
|
||
<button onclick="closeEditRoadmapModal()" 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>
|
||
|
||
<input type="hidden" id="editRoadmapId">
|
||
|
||
<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="editRoadmapTitle" 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</label>
|
||
<textarea id="editRoadmapDescription" rows="3" 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="editRoadmapQuarter" 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">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="editRoadmapType" 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">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="editRoadmapPriority" 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">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">Status</label>
|
||
<select id="editRoadmapStatus" 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="PLANNED">Gepland</option>
|
||
<option value="IN_PROGRESS">In Uitvoering</option>
|
||
<option value="REVIEW">In Review</option>
|
||
<option value="COMPLETED">Voltooid</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">Jira Issue Key</label>
|
||
<input type="text" id="editRoadmapJiraKey" 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>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Streefdatum</label>
|
||
<input type="date" id="editRoadmapDueDate" 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>
|
||
|
||
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||
<button id="btnSaveEditRoadmap" onclick="submitEditRoadmapItem()" 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">
|
||
Opslaan
|
||
</button>
|
||
<button onclick="closeEditRoadmapModal()" 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>
|
||
</div>
|
||
|
||
<!-- Modal: Roadmap Item Toewijzen & Inplannen in Vrij Blok -->
|
||
<div id="roadmapScheduleModal" onclick="if(event.target === this) closeRoadmapScheduleModal()" 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">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-8 h-8 rounded-xl bg-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center">
|
||
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Inplannen in Agenda van Collega</h3>
|
||
<p class="text-[11px] text-slate-500 dark:text-slate-400">Plant dit item direct in een vrij tijdblok van de collega</p>
|
||
</div>
|
||
</div>
|
||
<button onclick="closeRoadmapScheduleModal()" 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>
|
||
|
||
<input type="hidden" id="roadmapScheduleItemId">
|
||
|
||
<div class="space-y-4">
|
||
<!-- Item titel preview -->
|
||
<div class="p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-xs">
|
||
<span class="text-[10px] text-slate-400 font-bold uppercase tracking-wider block mb-0.5">Roadmap Item</span>
|
||
<span id="roadmapScheduleTitlePreview" class="font-bold text-slate-800 dark:text-slate-200"></span>
|
||
</div>
|
||
|
||
<!-- Collega Selector -->
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Toewijzen aan collega</label>
|
||
<select id="roadmapAssigneeSelect" onchange="onRoadmapAssigneeChange()" 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">
|
||
<?php foreach ($teamMembers as $tm): ?>
|
||
<option value="<?= $tm['id'] ?>" data-has-outlook="<?= $tm['has_outlook'] ? '1' : '0' ?>" <?= $tm['id'] === $currentUserId ? 'selected' : '' ?>>
|
||
<?= htmlspecialchars($tm['name']) ?> (<?= $tm['role'] === 'ADMIN' ? 'Beheerder' : 'Collega' ?>) <?= $tm['has_outlook'] ? '— 📅 Outlook Actief' : '' ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Outlook melding -->
|
||
<div id="roadmapAssignOutlookNotice" class="p-2 rounded-xl bg-sky-50 dark:bg-sky-950/40 border border-sky-200 dark:border-sky-800/60 flex items-center space-x-2 text-[11px] text-sky-700 dark:text-sky-300">
|
||
<i data-lucide="calendar-check" class="w-4 h-4 text-sky-500 flex-shrink-0"></i>
|
||
<span>Deze collega heeft Microsoft Outlook gekoppeld. Er wordt gekeken naar echte vrije blokken.</span>
|
||
</div>
|
||
|
||
<!-- Vrij Blok Zoeker -->
|
||
<div class="p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 space-y-2">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xs">
|
||
<span class="font-bold text-amber-900 dark:text-amber-200 flex items-center space-x-1">
|
||
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-amber-500"></i>
|
||
<span>Automatisch vrij blok vinden</span>
|
||
</span>
|
||
<span class="text-[10px] text-amber-700 dark:text-amber-400">Zoekt binnen werktijden (09:00 - 17:00)</span>
|
||
</div>
|
||
<button type="button" id="btnRoadmapFindSlot" onclick="findFreeSlotForRoadmapAssignee()" class="px-3 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-bold text-xs shadow-xs transition flex items-center space-x-1.5 flex-shrink-0 cursor-pointer">
|
||
<i data-lucide="search" class="w-3.5 h-3.5"></i>
|
||
<span>Zoek vrij blok</span>
|
||
</button>
|
||
</div>
|
||
<div id="roadmapFoundSlotNotice" class="hidden p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-300 dark:border-emerald-800 text-xs text-emerald-800 dark:text-emerald-300 font-medium flex items-center space-x-1.5">
|
||
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-500 flex-shrink-0"></i>
|
||
<span id="roadmapFoundSlotText"></span>
|
||
</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">Datum</label>
|
||
<input type="date" id="roadmapPlanDate" value="<?= date('Y-m-d') ?>" 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>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
|
||
<input type="time" id="roadmapStartTime" value="10:00" 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>
|
||
|
||
<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">Duur</label>
|
||
<select id="roadmapDurationMinutes" 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">
|
||
<option value="30">30 minuten</option>
|
||
<option value="45">45 minuten</option>
|
||
<option value="60" selected>1 uur</option>
|
||
<option value="90">1.5 uur</option>
|
||
<option value="120">2 uur</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex items-center pt-5">
|
||
<label class="flex items-center space-x-2 cursor-pointer">
|
||
<input type="checkbox" id="roadmapSyncOutlook" checked class="rounded text-indigo-600 focus:ring-indigo-500">
|
||
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200">Sync met Outlook 365</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||
<button id="btnSubmitRoadmapSchedule" onclick="submitRoadmapSchedule()" class="flex-1 bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs flex items-center justify-center space-x-1.5">
|
||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||
<span>Inplannen in Agenda</span>
|
||
</button>
|
||
<button onclick="closeRoadmapScheduleModal()" 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>
|
||
</div>
|
||
|
||
<!-- Rechtermuisknop Contextmenu voor Roadmap Items -->
|
||
<div id="roadmapContextMenu" class="hidden fixed z-50 w-56 bg-white dark:bg-slate-900 rounded-xl shadow-2xl border border-slate-200 dark:border-slate-800 py-1.5 text-xs text-slate-700 dark:text-slate-200">
|
||
<button onclick="triggerRoadmapCtxEdit()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-indigo-50 dark:hover:bg-indigo-950/50 hover:text-indigo-600 dark:hover:text-indigo-400 transition cursor-pointer">
|
||
<i data-lucide="edit-3" class="w-4 h-4 text-indigo-500"></i>
|
||
<span>Item bewerken</span>
|
||
</button>
|
||
|
||
<button onclick="triggerRoadmapCtxSchedule()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-amber-50 dark:hover:bg-amber-950/40 hover:text-amber-600 dark:hover:text-amber-400 transition cursor-pointer">
|
||
<i data-lucide="calendar-plus" class="w-4 h-4 text-amber-500"></i>
|
||
<span>👤 Toewijzen & vrij blok inplannen</span>
|
||
</button>
|
||
|
||
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
|
||
|
||
<div class="px-3.5 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider">Verplaats naar Kwartaal</div>
|
||
<div class="grid grid-cols-4 gap-1 px-3 py-1">
|
||
<button onclick="triggerRoadmapCtxMoveQuarter('Q1')" class="py-1 text-center font-mono font-bold rounded hover:bg-indigo-50 dark:hover:bg-indigo-950/50 text-indigo-600 cursor-pointer">Q1</button>
|
||
<button onclick="triggerRoadmapCtxMoveQuarter('Q2')" class="py-1 text-center font-mono font-bold rounded hover:bg-indigo-50 dark:hover:bg-indigo-950/50 text-indigo-600 cursor-pointer">Q2</button>
|
||
<button onclick="triggerRoadmapCtxMoveQuarter('Q3')" class="py-1 text-center font-mono font-bold rounded hover:bg-indigo-50 dark:hover:bg-indigo-950/50 text-indigo-600 cursor-pointer">Q3</button>
|
||
<button onclick="triggerRoadmapCtxMoveQuarter('Q4')" class="py-1 text-center font-mono font-bold rounded hover:bg-indigo-50 dark:hover:bg-indigo-950/50 text-indigo-600 cursor-pointer">Q4</button>
|
||
</div>
|
||
|
||
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
|
||
|
||
<button onclick="triggerRoadmapCtxDelete()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer">
|
||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||
<span>Item verwijderen</span>
|
||
</button>
|
||
</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) {
|
||
const el = document.getElementById(`roadmap-item-${itemId}`);
|
||
if (el) el.classList.remove('opacity-40', 'scale-[0.99]', 'pointer-events-none');
|
||
}
|
||
document.querySelectorAll('[id^="roadmap-item-"].opacity-40').forEach(el => {
|
||
el.classList.remove('opacity-40', 'scale-[0.99]', 'pointer-events-none');
|
||
});
|
||
|
||
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');
|
||
}
|
||
}
|
||
|
||
// --- Rechtermuisknop Contextmenu & Acties voor Roadmap Items ---
|
||
let activeRoadmapCtxItemId = null;
|
||
|
||
function openRoadmapContextMenu(e, itemId) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
activeRoadmapCtxItemId = itemId;
|
||
const menu = document.getElementById('roadmapContextMenu');
|
||
if (!menu) return;
|
||
|
||
menu.classList.remove('hidden');
|
||
const x = Math.min(e.clientX, window.innerWidth - 240);
|
||
const y = Math.min(e.clientY, window.innerHeight - 240);
|
||
menu.style.left = `${x}px`;
|
||
menu.style.top = `${y}px`;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function closeRoadmapContextMenu() {
|
||
const menu = document.getElementById('roadmapContextMenu');
|
||
if (menu) menu.classList.add('hidden');
|
||
activeRoadmapCtxItemId = null;
|
||
}
|
||
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('#roadmapContextMenu')) {
|
||
closeRoadmapContextMenu();
|
||
}
|
||
});
|
||
|
||
function triggerRoadmapCtxEdit() {
|
||
if (!activeRoadmapCtxItemId) return;
|
||
const itemId = activeRoadmapCtxItemId;
|
||
closeRoadmapContextMenu();
|
||
|
||
const el = document.getElementById(`roadmap-item-${itemId}`);
|
||
if (!el) return;
|
||
|
||
document.getElementById('editRoadmapId').value = itemId;
|
||
document.getElementById('editRoadmapTitle').value = el.getAttribute('data-title') || '';
|
||
document.getElementById('editRoadmapDescription').value = el.getAttribute('data-description') || '';
|
||
document.getElementById('editRoadmapQuarter').value = el.getAttribute('data-quarter') || 'Q3';
|
||
document.getElementById('editRoadmapType').value = el.getAttribute('data-type') || 'FEATURE';
|
||
document.getElementById('editRoadmapPriority').value = el.getAttribute('data-priority') || 'HIGH';
|
||
document.getElementById('editRoadmapStatus').value = el.getAttribute('data-status') || 'PLANNED';
|
||
document.getElementById('editRoadmapDueDate').value = el.getAttribute('data-due-date') || '';
|
||
document.getElementById('editRoadmapJiraKey').value = el.getAttribute('data-jira-key') || '';
|
||
|
||
const modal = document.getElementById('editRoadmapModal');
|
||
modal.classList.remove('hidden');
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
document.getElementById('editRoadmapTitle')?.focus();
|
||
}
|
||
|
||
function closeEditRoadmapModal() {
|
||
document.getElementById('editRoadmapModal')?.classList.add('hidden');
|
||
}
|
||
|
||
async function submitEditRoadmapItem() {
|
||
const id = parseInt(document.getElementById('editRoadmapId').value, 10);
|
||
const title = document.getElementById('editRoadmapTitle').value.trim();
|
||
const description = document.getElementById('editRoadmapDescription').value.trim();
|
||
const quarter = document.getElementById('editRoadmapQuarter').value;
|
||
const type = document.getElementById('editRoadmapType').value;
|
||
const priority = document.getElementById('editRoadmapPriority').value;
|
||
const status = document.getElementById('editRoadmapStatus').value;
|
||
const due_date = document.getElementById('editRoadmapDueDate').value;
|
||
const jira_key = document.getElementById('editRoadmapJiraKey').value.trim();
|
||
|
||
if (!title) {
|
||
alert('Titel is verplicht');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('btnSaveEditRoadmap');
|
||
btn.disabled = true;
|
||
btn.innerText = 'Opslaan...';
|
||
|
||
try {
|
||
const res = await fetch('/api/roadmap.php?action=update_item', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
id,
|
||
title,
|
||
description,
|
||
quarter,
|
||
type,
|
||
priority,
|
||
status,
|
||
due_date,
|
||
jira_key
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
closeEditRoadmapModal();
|
||
location.reload();
|
||
} else {
|
||
alert(data.error || 'Kon item niet opslaan');
|
||
}
|
||
} catch(e) {
|
||
alert('Netwerkfout bij opslaan');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerText = 'Opslaan';
|
||
}
|
||
}
|
||
|
||
function triggerRoadmapCtxSchedule() {
|
||
if (!activeRoadmapCtxItemId) return;
|
||
const itemId = activeRoadmapCtxItemId;
|
||
closeRoadmapContextMenu();
|
||
|
||
const el = document.getElementById(`roadmap-item-${itemId}`);
|
||
if (!el) return;
|
||
|
||
document.getElementById('roadmapScheduleItemId').value = itemId;
|
||
document.getElementById('roadmapScheduleTitlePreview').innerText = el.getAttribute('data-title') || 'Roadmap item';
|
||
document.getElementById('roadmapFoundSlotNotice').classList.add('hidden');
|
||
|
||
onRoadmapAssigneeChange();
|
||
|
||
const modal = document.getElementById('roadmapScheduleModal');
|
||
modal.classList.remove('hidden');
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
|
||
setTimeout(() => {
|
||
findFreeSlotForRoadmapAssignee();
|
||
}, 150);
|
||
}
|
||
|
||
function closeRoadmapScheduleModal() {
|
||
document.getElementById('roadmapScheduleModal')?.classList.add('hidden');
|
||
}
|
||
|
||
function onRoadmapAssigneeChange() {
|
||
const sel = document.getElementById('roadmapAssigneeSelect');
|
||
const opt = sel.options[sel.selectedIndex];
|
||
const notice = document.getElementById('roadmapAssignOutlookNotice');
|
||
if (opt && opt.getAttribute('data-has-outlook') === '1') {
|
||
notice.classList.remove('hidden');
|
||
} else {
|
||
notice.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
async function findFreeSlotForRoadmapAssignee() {
|
||
const sel = document.getElementById('roadmapAssigneeSelect');
|
||
const targetUserId = sel.value ? parseInt(sel.value, 10) : <?= $currentUserId ?>;
|
||
const duration = parseInt(document.getElementById('roadmapDurationMinutes').value, 10) || 60;
|
||
|
||
const btn = document.getElementById('btnRoadmapFindSlot');
|
||
const origHtml = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Zoeken...</span>';
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
duration_minutes: duration,
|
||
target_user_id: targetUserId
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success && data.slot) {
|
||
const slot = data.slot;
|
||
document.getElementById('roadmapPlanDate').value = slot.date;
|
||
document.getElementById('roadmapStartTime').value = slot.start;
|
||
|
||
const notice = document.getElementById('roadmapFoundSlotNotice');
|
||
const noticeText = document.getElementById('roadmapFoundSlotText');
|
||
noticeText.innerHTML = `Vrij agendablok gevonden: <strong>${slot.formatted_date}</strong> om <strong>${slot.start} – ${slot.end}</strong>`;
|
||
notice.classList.remove('hidden');
|
||
} else {
|
||
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen werktijden.');
|
||
}
|
||
} catch(err) {
|
||
alert('Netwerkfout bij zoeken van vrij agendablok: ' + err.message);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = origHtml;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
async function submitRoadmapSchedule() {
|
||
const itemId = parseInt(document.getElementById('roadmapScheduleItemId').value, 10);
|
||
const assigneeId = document.getElementById('roadmapAssigneeSelect').value ? parseInt(document.getElementById('roadmapAssigneeSelect').value, 10) : null;
|
||
const planDate = document.getElementById('roadmapPlanDate').value;
|
||
const startTime = document.getElementById('roadmapStartTime').value;
|
||
const durationMinutes = parseInt(document.getElementById('roadmapDurationMinutes').value, 10) || 60;
|
||
const syncOutlook = document.getElementById('roadmapSyncOutlook').checked;
|
||
|
||
const el = document.getElementById(`roadmap-item-${itemId}`);
|
||
const title = el ? el.getAttribute('data-title') : 'Roadmap Item';
|
||
|
||
const btn = document.getElementById('btnSubmitRoadmapSchedule');
|
||
const origHtml = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Inplannen...</span>';
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
|
||
try {
|
||
// Maak agenda taak aan via api/planner.php
|
||
const res = await fetch('/api/planner.php?action=add_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
date: planDate,
|
||
title: title,
|
||
start_time: startTime,
|
||
duration: durationMinutes,
|
||
type: 'PROJECT',
|
||
priority: 'MEDIUM',
|
||
sync_to_microsoft: syncOutlook,
|
||
target_user_id: assigneeId
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
closeRoadmapScheduleModal();
|
||
let msg = `Roadmap item is succesvol ingepland op ${planDate} van ${startTime} tot ${data.task?.end_time || ''}!`;
|
||
if (data.outlook_message) {
|
||
msg += `\n${data.outlook_message}`;
|
||
}
|
||
alert(msg);
|
||
} else {
|
||
alert(data.error || 'Kon afspraak niet inplannen');
|
||
}
|
||
} catch(err) {
|
||
alert('Netwerkfout bij inplannen: ' + err.message);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = origHtml;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
async function triggerRoadmapCtxMoveQuarter(targetQuarter) {
|
||
if (!activeRoadmapCtxItemId) return;
|
||
const itemId = activeRoadmapCtxItemId;
|
||
closeRoadmapContextMenu();
|
||
|
||
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) {
|
||
alert('Netwerkfout bij verplaatsen item');
|
||
}
|
||
}
|
||
|
||
function triggerRoadmapCtxDelete() {
|
||
if (!activeRoadmapCtxItemId) return;
|
||
const itemId = activeRoadmapCtxItemId;
|
||
closeRoadmapContextMenu();
|
||
deleteRoadmapItem(itemId);
|
||
}
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
const modal = document.getElementById('newItemModal');
|
||
if (modal && !modal.classList.contains('hidden')) modal.classList.add('hidden');
|
||
closeEditRoadmapModal();
|
||
closeRoadmapScheduleModal();
|
||
closeRoadmapContextMenu();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|