Compare commits
3 Commits
a0d99914e6
...
880c1bf9bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 880c1bf9bb | |||
| 94d6325693 | |||
| 7c27949cc2 |
+161
-1
@@ -146,7 +146,167 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Omzetten naar Roadmap!
|
||||
// 3c. Kaart bewerken (Inhoud, Categorie en/of Kleur)
|
||||
if ($action === 'update_card') {
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$cardId = (int)($data['card_id'] ?? 0);
|
||||
$content = trim($data['content'] ?? '');
|
||||
$category = trim($data['category'] ?? '');
|
||||
|
||||
if (!$cardId || empty($content)) {
|
||||
echo json_encode(['error' => 'Kaart ID en inhoud zijn verplicht']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$colorMap = [
|
||||
'Ideeën' => 'amber',
|
||||
'Knelpunten' => 'rose',
|
||||
'Oplossingen' => 'teal',
|
||||
'Quick Wins' => 'indigo'
|
||||
];
|
||||
$color = $colorMap[$category] ?? 'amber';
|
||||
|
||||
if (!empty($category)) {
|
||||
$stmt = $db->prepare("UPDATE brainstorm_cards SET content = ?, category = ?, color = ? WHERE id = ?");
|
||||
$stmt->execute([$content, $category, $color, $cardId]);
|
||||
} else {
|
||||
$stmt = $db->prepare("UPDATE brainstorm_cards SET content = ? WHERE id = ?");
|
||||
$stmt->execute([$content, $cardId]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'card_id' => $cardId, 'content' => $content, 'category' => $category, 'color' => $color]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3d. Kaart toewijzen aan collega en inplannen in agenda (Vrij blok)
|
||||
if ($action === 'assign_and_schedule') {
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$cardId = (int)($data['card_id'] ?? 0);
|
||||
$assigneeId = !empty($data['assignee_id']) ? (int)$data['assignee_id'] : null;
|
||||
$planDate = !empty($data['plan_date']) ? trim($data['plan_date']) : null;
|
||||
$startTime = !empty($data['start_time']) ? trim($data['start_time']) : null;
|
||||
$endTime = !empty($data['end_time']) ? trim($data['end_time']) : null;
|
||||
$durationMinutes = (int)($data['duration_minutes'] ?? 30);
|
||||
$syncOutlook = !empty($data['sync_outlook']);
|
||||
|
||||
if (!$cardId) {
|
||||
echo json_encode(['error' => 'Kaart ID ontbreekt']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmtCard = $db->prepare("SELECT * FROM brainstorm_cards WHERE id = ?");
|
||||
$stmtCard->execute([$cardId]);
|
||||
$card = $stmtCard->fetch();
|
||||
if (!$card) {
|
||||
echo json_encode(['error' => 'Kaart niet gevonden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Bereken eindtijd indien nodig
|
||||
if ($startTime && !$endTime && $durationMinutes > 0) {
|
||||
$tParts = explode(':', $startTime);
|
||||
$startMins = ((int)$tParts[0] * 60) + (int)($tParts[1] ?? 0);
|
||||
$endMins = $startMins + $durationMinutes;
|
||||
$endTime = sprintf('%02d:%02d', floor($endMins / 60), $endMins % 60);
|
||||
}
|
||||
|
||||
$createdTaskId = null;
|
||||
$outlookSyncMsg = null;
|
||||
|
||||
// Als er een datum & tijd is gekozen én een collega (of huidige gebruiker)
|
||||
if ($assigneeId && $planDate && $startTime) {
|
||||
// Zoek of maak daily plan
|
||||
$stmtPlan = $db->prepare("
|
||||
INSERT INTO daily_plans (user_id, plan_date)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)
|
||||
");
|
||||
$stmtPlan->execute([$assigneeId, $planDate]);
|
||||
$planId = (int)$db->lastInsertId();
|
||||
|
||||
$taskTitle = "Brainstorm taak: " . mb_substr($card['content'], 0, 75);
|
||||
$taskDesc = "Afkomstig van Brainstormsessie. Inhoud:\n" . $card['content'];
|
||||
$source = 'BRAINSTORM';
|
||||
$externalId = null;
|
||||
$externalUrl = null;
|
||||
|
||||
// Indien Outlook sync gewenst is
|
||||
if ($syncOutlook) {
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
try {
|
||||
$msResult = microsoft_create_event($assigneeId, [
|
||||
'subject' => $taskTitle,
|
||||
'description' => $taskDesc,
|
||||
'date' => $planDate,
|
||||
'start_time' => $startTime,
|
||||
'end_time' => $endTime,
|
||||
'duration_minutes' => $durationMinutes,
|
||||
]);
|
||||
if (!empty($msResult['success']) && !empty($msResult['event_id'])) {
|
||||
$source = 'OUTLOOK';
|
||||
$externalId = $msResult['event_id'];
|
||||
$externalUrl = $msResult['web_link'] ?? null;
|
||||
$outlookSyncMsg = 'Ingepland in Outlook 365 agenda.';
|
||||
}
|
||||
} catch (Throwable $t) {
|
||||
// stil doorgaan
|
||||
}
|
||||
}
|
||||
|
||||
// Maak of update taak
|
||||
if (!empty($card['scheduled_task_id'])) {
|
||||
$stmtUpdateTask = $db->prepare("
|
||||
UPDATE tasks
|
||||
SET user_id = ?, daily_plan_id = ?, title = ?, description = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmtUpdateTask->execute([$assigneeId, $planId, $taskTitle, $taskDesc, $startTime, $endTime, $durationMinutes, $card['scheduled_task_id']]);
|
||||
$createdTaskId = (int)$card['scheduled_task_id'];
|
||||
} else {
|
||||
$stmtInsertTask = $db->prepare("
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, description, priority, time_block_start, time_block_end, duration_minutes, status, source, external_id, external_url)
|
||||
VALUES (?, ?, ?, ?, 'P2', ?, ?, ?, 'TODO', ?, ?, ?)
|
||||
");
|
||||
$stmtInsertTask->execute([$assigneeId, $planId, $taskTitle, $taskDesc, $startTime, $endTime, $durationMinutes, $source, $externalId, $externalUrl]);
|
||||
$createdTaskId = (int)$db->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
// Update brainstorm kaart
|
||||
$stmtUpdateCard = $db->prepare("
|
||||
UPDATE brainstorm_cards
|
||||
SET assignee_id = ?, scheduled_date = ?, scheduled_start_time = ?, scheduled_end_time = ?, scheduled_task_id = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmtUpdateCard->execute([$assigneeId, $planDate, $startTime, $endTime, $createdTaskId, $cardId]);
|
||||
|
||||
// Haal assignee naam & avatar op
|
||||
$assigneeName = null;
|
||||
$assigneeAvatar = null;
|
||||
if ($assigneeId) {
|
||||
$stmtUser = $db->prepare("SELECT name, avatar_url FROM users WHERE id = ?");
|
||||
$stmtUser->execute([$assigneeId]);
|
||||
$usr = $stmtUser->fetch();
|
||||
if ($usr) {
|
||||
$assigneeName = $usr['name'];
|
||||
$assigneeAvatar = $usr['avatar_url'];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'card_id' => $cardId,
|
||||
'assignee_id' => $assigneeId,
|
||||
'assignee_name' => $assigneeName,
|
||||
'assignee_avatar' => $assigneeAvatar,
|
||||
'scheduled_date' => $planDate,
|
||||
'scheduled_start_time' => $startTime,
|
||||
'scheduled_end_time' => $endTime,
|
||||
'scheduled_task_id' => $createdTaskId,
|
||||
'outlook_message' => $outlookSyncMsg
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
if ($action === 'convert_to_roadmap') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$cardIds = $data['card_ids'] ?? [];
|
||||
|
||||
+7
-6
@@ -695,6 +695,7 @@ try {
|
||||
$duration = max(5, (int)($data['duration_minutes'] ?? 30));
|
||||
$excludeTaskId = (int)($data['task_id'] ?? 0);
|
||||
$startFromNextWorkday = !empty($data['start_from_next_workday']);
|
||||
$targetUserId = !empty($data['target_user_id']) ? (int)$data['target_user_id'] : (int)$user['id'];
|
||||
|
||||
$workdaySettings = get_workday_settings();
|
||||
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
|
||||
@@ -757,7 +758,7 @@ try {
|
||||
$rangeEndObj->modify('+21 days'); // ruim venster incl. weekenden
|
||||
$rangeEndDate = $rangeEndObj->format('Y-m-d');
|
||||
|
||||
// 1. Batch SQL: haal alle bestaande taken van de gebruiker in 1 geoptimaliseerde query op
|
||||
// 1. Batch SQL: haal alle bestaande taken van de doelcollega in 1 geoptimaliseerde query op
|
||||
$tasksByDate = [];
|
||||
$stmtTasks = $db->prepare("
|
||||
SELECT t.id, dp.plan_date, t.time_block_start, t.time_block_end, t.duration_minutes
|
||||
@@ -769,7 +770,7 @@ try {
|
||||
AND t.time_block_start != ''
|
||||
AND t.status != 'CANCELLED'
|
||||
");
|
||||
$stmtTasks->execute([$user['id'], $rangeStartDate, $rangeEndDate]);
|
||||
$stmtTasks->execute([$targetUserId, $rangeStartDate, $rangeEndDate]);
|
||||
while ($t = $stmtTasks->fetch()) {
|
||||
if ($excludeTaskId > 0 && (int)$t['id'] === $excludeTaskId) {
|
||||
continue;
|
||||
@@ -777,11 +778,11 @@ try {
|
||||
$tasksByDate[$t['plan_date']][] = $t;
|
||||
}
|
||||
|
||||
// 2. Batch Microsoft Graph: haal alle Outlook events in 1 API call op voor het gehele venster
|
||||
// 2. Batch Microsoft Graph: haal alle Outlook events in 1 API call op voor het gehele venster van de doelcollega
|
||||
$outlookByDate = [];
|
||||
if (function_exists('microsoft_fetch_events_range')) {
|
||||
try {
|
||||
$allOutlookEvts = microsoft_fetch_events_range($user['id'], $rangeStartDate, $rangeEndDate, false);
|
||||
$allOutlookEvts = microsoft_fetch_events_range($targetUserId, $rangeStartDate, $rangeEndDate, false);
|
||||
foreach ($allOutlookEvts as $oe) {
|
||||
if (!empty($oe['date'])) {
|
||||
$outlookByDate[$oe['date']][] = $oe;
|
||||
@@ -790,11 +791,11 @@ try {
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
// 3. Batch Calendar Blocks: haal verlof en geblokkeerde vakantiedagen op voor dit venster
|
||||
// 3. Batch Calendar Blocks: haal verlof en geblokkeerde vakantiedagen op voor dit venster van de doelcollega
|
||||
$blockedDates = [];
|
||||
try {
|
||||
$stmtBlk = $db->prepare("SELECT start_date, end_date FROM calendar_blocks WHERE user_id = ? AND start_date <= ? AND end_date >= ?");
|
||||
$stmtBlk->execute([$user['id'], $rangeEndDate, $rangeStartDate]);
|
||||
$stmtBlk->execute([$targetUserId, $rangeEndDate, $rangeStartDate]);
|
||||
while ($b = $stmtBlk->fetch()) {
|
||||
$bStart = strtotime($b['start_date']);
|
||||
$bEnd = strtotime($b['end_date']);
|
||||
|
||||
@@ -57,6 +57,26 @@ try {
|
||||
$fields[] = "quarter = ?";
|
||||
$params[] = $data['quarter'];
|
||||
}
|
||||
if (isset($data['title']) && trim($data['title']) !== '') {
|
||||
$fields[] = "title = ?";
|
||||
$params[] = trim($data['title']);
|
||||
}
|
||||
if (isset($data['description'])) {
|
||||
$fields[] = "description = ?";
|
||||
$params[] = trim($data['description']);
|
||||
}
|
||||
if (isset($data['type'])) {
|
||||
$fields[] = "type = ?";
|
||||
$params[] = $data['type'];
|
||||
}
|
||||
if (isset($data['priority'])) {
|
||||
$fields[] = "priority = ?";
|
||||
$params[] = $data['priority'];
|
||||
}
|
||||
if (array_key_exists('due_date', $data)) {
|
||||
$fields[] = "due_date = ?";
|
||||
$params[] = !empty($data['due_date']) ? $data['due_date'] : null;
|
||||
}
|
||||
if ($progress !== null) {
|
||||
$fields[] = "progress = ?";
|
||||
$params[] = max(0, min(100, $progress));
|
||||
|
||||
@@ -21,6 +21,47 @@ $isAdmin = ($userRole === 'ADMIN');
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* --- Realistische Post-it Styling met Opkrullende Rand Links Onder --- */
|
||||
.postit-note {
|
||||
position: relative;
|
||||
border-radius: 4px 18px 4px 18px;
|
||||
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.12), 0 2px 6px -1px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
.postit-note:hover {
|
||||
transform: translateY(-3px) scale(1.01);
|
||||
box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.16), 0 6px 10px -2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.postit-note::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(135deg, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.06) 45%, transparent 50%);
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: -2px 2px 4px rgba(0,0,0,0.12);
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
.postit-note::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 15%;
|
||||
right: 15%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dark .postit-note::before {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="boardPageContainer" class="space-y-4 w-full">
|
||||
@@ -277,6 +318,25 @@ $isAdmin = ($userRole === 'ADMIN');
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knop Vrij Blok Zoeken voor Assignee -->
|
||||
<div class="p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between gap-2">
|
||||
<div class="text-[11px]">
|
||||
<span class="font-bold text-amber-900 dark:text-amber-200 flex items-center space-x-1">
|
||||
<i data-lucide="sparkles" class="w-3 h-3 text-amber-500"></i>
|
||||
<span>Vrij agendablok zoeken</span>
|
||||
</span>
|
||||
<span class="text-[10px] text-amber-700 dark:text-amber-400">Zoekt binnen werktijden van de toegewezen collega</span>
|
||||
</div>
|
||||
<button type="button" id="btnBoardFindSlot" onclick="findFreeSlotForBoardAssignee()" class="px-2.5 py-1 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-bold text-[10px] shadow-xs transition flex items-center space-x-1 flex-shrink-0 cursor-pointer">
|
||||
<i data-lucide="search" class="w-3 h-3"></i>
|
||||
<span>Zoek vrij blok</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="boardFoundSlotNotice" class="hidden p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-300 dark:border-emerald-800 text-[11px] text-emerald-800 dark:text-emerald-300 font-medium flex items-center space-x-1.5">
|
||||
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
|
||||
<span id="boardFoundSlotText"></span>
|
||||
</div>
|
||||
|
||||
<div id="scheduleEndDateRow" class="hidden">
|
||||
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Einddatum herhaling</label>
|
||||
<input type="date" id="cardModalEndDate" value="<?= date('Y-m-d', strtotime('+7 days')) ?>" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none">
|
||||
@@ -425,7 +485,39 @@ $isAdmin = ($userRole === 'ADMIN');
|
||||
<button onclick="submitCreateBoard()" class="px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs rounded-xl transition">Bord Aanmaken</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Floating Rechtermuisknop Contextmenu voor Bord Kaarten -->
|
||||
<div id="boardCardContextMenu" class="hidden fixed z-[999] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1.5 w-64 text-xs font-medium text-slate-700 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-100">
|
||||
<div class="px-3.5 py-1.5 border-b border-slate-100 dark:border-slate-800/80 mb-1">
|
||||
<span id="boardCtxTitle" class="text-[11px] font-bold text-slate-900 dark:text-white truncate block"></span>
|
||||
<span id="boardCtxMeta" class="text-[10px] text-slate-400 truncate block"></span>
|
||||
</div>
|
||||
|
||||
<button onclick="triggerBoardCtxEdit()" 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>Taak bewerken</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerBoardCtxQuickSlot()" 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="zap" class="w-4 h-4 text-amber-500"></i>
|
||||
<span>⚡ Zoek direct vrij blok</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerBoardCtxQuickMove(-1)" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 text-slate-500"></i>
|
||||
<span>Verplaats naar vorige kolom</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerBoardCtxQuickMove(1)" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
<i data-lucide="arrow-right" class="w-4 h-4 text-slate-500"></i>
|
||||
<span>Verplaats naar volgende kolom</span>
|
||||
</button>
|
||||
|
||||
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
|
||||
|
||||
<button onclick="triggerBoardCtxDelete()" 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>Taak verwijderen</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -508,10 +600,11 @@ function onBoardSelectChange(bId) {
|
||||
}
|
||||
|
||||
// 2. Data van specifiek bord inladen
|
||||
async function loadBoardData(boardId) {
|
||||
async function loadBoardData(boardId, showLoader = true) {
|
||||
currentBoardId = boardId;
|
||||
document.getElementById('linkToTimeline')?.setAttribute('href', `/timeline.php?board_id=${boardId}`);
|
||||
const container = document.getElementById('boardLanesContainer');
|
||||
if (showLoader) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center w-full py-20 text-slate-400 text-sm">
|
||||
<i data-lucide="loader-2" class="w-5 h-5 animate-spin mr-2"></i>
|
||||
@@ -519,6 +612,7 @@ async function loadBoardData(boardId) {
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/board.php?action=get_board_data&board_id=${boardId}`);
|
||||
@@ -567,9 +661,13 @@ function onCardDragStart(e, cardId) {
|
||||
function onCardDragEnd(e) {
|
||||
if (draggedCardId) {
|
||||
const el = document.getElementById(`card-${draggedCardId}`);
|
||||
if (el) el.classList.remove('opacity-40');
|
||||
if (el) el.classList.remove('opacity-40', 'pointer-events-none');
|
||||
draggedCardId = null;
|
||||
}
|
||||
// Verwijder eventuele achtergebleven opacity op alle kaarten
|
||||
document.querySelectorAll('[id^="card-"].opacity-40').forEach(el => {
|
||||
el.classList.remove('opacity-40', 'pointer-events-none');
|
||||
});
|
||||
setTimeout(() => {
|
||||
isDraggingCard = false;
|
||||
}, 150);
|
||||
@@ -599,6 +697,23 @@ async function onLaneDrop(e, targetLaneId) {
|
||||
|
||||
const rawId = draggedCardId || e.dataTransfer.getData('text/plain');
|
||||
const cardId = parseInt(rawId, 10);
|
||||
|
||||
// Direct re-activeren van het gesleepte element
|
||||
const activeEl = cardId ? document.getElementById(`card-${cardId}`) : null;
|
||||
if (activeEl) {
|
||||
activeEl.classList.remove('opacity-40', 'pointer-events-none');
|
||||
// Optimistic DOM append als dropZone en element bestaan
|
||||
const emptyNotice = dropZone.querySelector('.pointer-events-none');
|
||||
if (emptyNotice && emptyNotice.innerText.includes('Geen taken')) {
|
||||
emptyNotice.remove();
|
||||
}
|
||||
dropZone.appendChild(activeEl);
|
||||
}
|
||||
draggedCardId = null;
|
||||
setTimeout(() => {
|
||||
isDraggingCard = false;
|
||||
}, 150);
|
||||
|
||||
if (!cardId) return;
|
||||
|
||||
try {
|
||||
@@ -611,7 +726,7 @@ async function onLaneDrop(e, targetLaneId) {
|
||||
target_position: 999
|
||||
})
|
||||
});
|
||||
await loadBoardData(currentBoardId);
|
||||
await loadBoardData(currentBoardId, false);
|
||||
} catch (err) {
|
||||
console.error('Fout bij verplaatsen taak:', err);
|
||||
}
|
||||
@@ -715,7 +830,8 @@ function renderCards() {
|
||||
ondragstart="onCardDragStart(event, ${card.id})"
|
||||
ondragend="onCardDragEnd(event)"
|
||||
onclick="onCardClick(event, ${card.id})"
|
||||
class="group/card relative p-3 rounded-xl border ${colorClass} shadow-2xs hover:shadow-md hover:-translate-y-0.5 transition cursor-grab active:cursor-grabbing mb-2.5"
|
||||
oncontextmenu="openBoardCardContextMenu(event, ${card.id}); return false;"
|
||||
class="group/card relative p-3 rounded-xl border ${colorClass} shadow-2xs hover:shadow-md hover:-translate-y-0.5 transition cursor-grab active:cursor-grabbing mb-2.5 postit-note"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-1.5 mb-1.5">
|
||||
<h4 class="text-xs font-bold leading-snug line-clamp-2 break-words">
|
||||
@@ -1030,6 +1146,131 @@ async function quickMoveCard(cardId, direction) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rechtermuisknop Context Menu Logica voor Bord
|
||||
let activeBoardCtxCardId = null;
|
||||
|
||||
function openBoardCardContextMenu(e, cardId) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
activeBoardCtxCardId = cardId;
|
||||
const menu = document.getElementById('boardCardContextMenu');
|
||||
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 closeBoardCardContextMenu() {
|
||||
const menu = document.getElementById('boardCardContextMenu');
|
||||
if (menu) menu.classList.add('hidden');
|
||||
activeBoardCtxCardId = null;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('#boardCardContextMenu')) {
|
||||
closeBoardCardContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
function triggerBoardCtxEdit() {
|
||||
if (!activeBoardCtxCardId) return;
|
||||
const cid = activeBoardCtxCardId;
|
||||
closeBoardCardContextMenu();
|
||||
openCardModal(cid);
|
||||
}
|
||||
|
||||
async function triggerBoardCtxQuickSlot() {
|
||||
if (!activeBoardCtxCardId) return;
|
||||
const cid = activeBoardCtxCardId;
|
||||
closeBoardCardContextMenu();
|
||||
openCardModal(cid);
|
||||
setTimeout(() => {
|
||||
findFreeSlotForBoardAssignee();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function triggerBoardCtxQuickMove(dir) {
|
||||
if (!activeBoardCtxCardId) return;
|
||||
const cid = activeBoardCtxCardId;
|
||||
closeBoardCardContextMenu();
|
||||
quickMoveCard(cid, dir);
|
||||
}
|
||||
|
||||
async function triggerBoardCtxDelete() {
|
||||
if (!activeBoardCtxCardId) return;
|
||||
const cid = activeBoardCtxCardId;
|
||||
closeBoardCardContextMenu();
|
||||
if (!confirm('Weet je zeker dat je deze taak wilt verwijderen?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/board.php?action=delete_card', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ card_id: cid })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await loadBoardData(currentBoardId);
|
||||
} else {
|
||||
alert(data.error || 'Kon taak niet verwijderen');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Netwerkfout bij verwijderen taak');
|
||||
}
|
||||
}
|
||||
|
||||
// Zoek vrij agendablok voor de in de modal geselecteerde assignee
|
||||
async function findFreeSlotForBoardAssignee() {
|
||||
const sel = document.getElementById('cardModalAssignee');
|
||||
const targetUserId = sel.value ? parseInt(sel.value, 10) : currentUserId;
|
||||
const schedCheckbox = document.getElementById('cardModalScheduleEnabled');
|
||||
if (!schedCheckbox.checked) {
|
||||
schedCheckbox.checked = true;
|
||||
toggleScheduleFields();
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnBoardFindSlot');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 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: 60,
|
||||
target_user_id: targetUserId
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success && data.slot) {
|
||||
const slot = data.slot;
|
||||
document.getElementById('cardModalStartDate').value = slot.date;
|
||||
document.getElementById('cardModalEndDate').value = slot.date;
|
||||
document.getElementById('cardModalStartTime').value = slot.start;
|
||||
document.getElementById('cardModalEndTime').value = slot.end;
|
||||
|
||||
const notice = document.getElementById('boardFoundSlotNotice');
|
||||
const noticeText = document.getElementById('boardFoundSlotText');
|
||||
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 voor deze collega.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Netwerkfout bij zoeken van vrij agendablok: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Beheer Modal: Lanes & Leden Autorisatie
|
||||
function openManageBoardModal() {
|
||||
if (!currentBoardData) return;
|
||||
|
||||
+559
-42
@@ -25,51 +25,112 @@ if (!$session) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Haal alle kaarten op van deze sessie
|
||||
// Haal alle kaarten op van deze sessie inclusief toewijzing
|
||||
$stmtCards = $db->prepare("
|
||||
SELECT * FROM brainstorm_cards
|
||||
WHERE session_id = ?
|
||||
ORDER BY votes DESC, created_at ASC
|
||||
SELECT c.*, u.name as assignee_name, u.avatar_url as assignee_avatar
|
||||
FROM brainstorm_cards c
|
||||
LEFT JOIN users u ON c.assignee_id = u.id
|
||||
WHERE c.session_id = ?
|
||||
ORDER BY c.votes DESC, c.created_at ASC
|
||||
");
|
||||
$stmtCards->execute([$sessionId]);
|
||||
$cards = $stmtCards->fetchAll();
|
||||
|
||||
// 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);
|
||||
|
||||
$categories = [
|
||||
'Ideeën' => [
|
||||
'title' => 'Ideeën & Concepten',
|
||||
'icon' => 'lightbulb',
|
||||
'color' => 'amber',
|
||||
'border' => 'border-amber-200/80 dark:border-amber-800/40',
|
||||
'bg' => 'bg-amber-50/60 dark:bg-amber-950/20',
|
||||
'badge' => 'bg-amber-500/15 text-amber-800 dark:text-amber-300'
|
||||
'border' => 'border-amber-300 dark:border-amber-600/40',
|
||||
'bg' => 'bg-[#fef9c3] dark:bg-[#713f12]/30 text-amber-950 dark:text-amber-100',
|
||||
'badge' => 'bg-amber-500/20 text-amber-900 dark:text-amber-200'
|
||||
],
|
||||
'Knelpunten' => [
|
||||
'title' => 'Knelpunten & Uitdagingen',
|
||||
'icon' => 'alert-triangle',
|
||||
'color' => 'rose',
|
||||
'border' => 'border-rose-200/80 dark:border-rose-800/40',
|
||||
'bg' => 'bg-rose-50/60 dark:bg-rose-950/20',
|
||||
'badge' => 'bg-rose-500/15 text-rose-800 dark:text-rose-300'
|
||||
'border' => 'border-rose-300 dark:border-rose-600/40',
|
||||
'bg' => 'bg-[#ffe4e6] dark:bg-[#881337]/30 text-rose-950 dark:text-rose-100',
|
||||
'badge' => 'bg-rose-500/20 text-rose-900 dark:text-rose-200'
|
||||
],
|
||||
'Oplossingen' => [
|
||||
'title' => 'Oplossingen & Features',
|
||||
'icon' => 'rocket',
|
||||
'color' => 'teal',
|
||||
'border' => 'border-teal-200/80 dark:border-teal-800/40',
|
||||
'bg' => 'bg-teal-50/60 dark:bg-teal-950/20',
|
||||
'badge' => 'bg-teal-500/15 text-teal-800 dark:text-teal-300'
|
||||
'border' => 'border-teal-300 dark:border-teal-600/40',
|
||||
'bg' => 'bg-[#ccfbf1] dark:bg-[#134e4a]/30 text-teal-950 dark:text-teal-100',
|
||||
'badge' => 'bg-teal-500/20 text-teal-900 dark:text-teal-200'
|
||||
],
|
||||
'Quick Wins' => [
|
||||
'title' => 'Quick Wins (Direct)',
|
||||
'icon' => 'zap',
|
||||
'color' => 'indigo',
|
||||
'border' => 'border-indigo-200/80 dark:border-indigo-800/40',
|
||||
'bg' => 'bg-indigo-50/60 dark:bg-indigo-950/20',
|
||||
'badge' => 'bg-indigo-500/15 text-indigo-800 dark:text-indigo-300'
|
||||
'border' => 'border-indigo-300 dark:border-indigo-600/40',
|
||||
'bg' => 'bg-[#e0e7ff] dark:bg-[#312e81]/30 text-indigo-950 dark:text-indigo-100',
|
||||
'badge' => 'bg-indigo-500/20 text-indigo-900 dark:text-indigo-200'
|
||||
],
|
||||
];
|
||||
?>
|
||||
|
||||
<style>
|
||||
/* --- Realistische Post-it Styling met Opkrullende Rand Links Onder --- */
|
||||
.postit-note {
|
||||
position: relative;
|
||||
border-radius: 4px 18px 4px 18px;
|
||||
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.12), 0 2px 6px -1px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
.postit-note:hover {
|
||||
transform: translateY(-3px) scale(1.01);
|
||||
box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.16), 0 6px 10px -2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
/* Opkrullend hoekje links onder */
|
||||
.postit-note::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: linear-gradient(135deg, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.06) 45%, transparent 50%);
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: -2px 2px 4px rgba(0,0,0,0.12);
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
/* Subtiele tape / plakrand bovenaan */
|
||||
.postit-note::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 15%;
|
||||
right: 15%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dark .postit-note::before {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="space-y-6 max-w-7xl mx-auto">
|
||||
<!-- Top Bar -->
|
||||
<div class="fociz-card p-5 flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
@@ -146,44 +207,89 @@ $categories = [
|
||||
Nog geen kaarten in deze categorie.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($catCards as $card): ?>
|
||||
<?php foreach ($catCards as $card):
|
||||
$hasAssignee = !empty($card['assignee_id']) && !empty($card['assignee_name']);
|
||||
$hasSchedule = !empty($card['scheduled_date']) && !empty($card['scheduled_start_time']);
|
||||
$cardJson = htmlspecialchars(json_encode([
|
||||
'id' => (int)$card['id'],
|
||||
'content' => $card['content'],
|
||||
'category' => $card['category'],
|
||||
'color' => $card['color'],
|
||||
'assignee_id' => $card['assignee_id'] ? (int)$card['assignee_id'] : null,
|
||||
'assignee_name' => $card['assignee_name'] ?? null,
|
||||
'scheduled_date' => $card['scheduled_date'] ?? null,
|
||||
'scheduled_start_time' => $card['scheduled_start_time'] ?? null,
|
||||
'scheduled_end_time' => $card['scheduled_end_time'] ?? null,
|
||||
]), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<div
|
||||
id="card-<?= $card['id'] ?>"
|
||||
draggable="true"
|
||||
ondragstart="handleCanvasCardDragStart(event, <?= $card['id'] ?>)"
|
||||
ondragend="handleCanvasCardDragEnd(event)"
|
||||
class="brainstorm-card group relative rounded-2xl p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> backdrop-blur-sm shadow-sm hover:shadow-md transition-all duration-150 cursor-grab active:cursor-grabbing hover:-translate-y-0.5 flex flex-col justify-between"
|
||||
oncontextmenu="openCardContextMenu(event, <?= $cardJson ?>); return false;"
|
||||
class="brainstorm-card postit-note group relative p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> transition-all duration-150 cursor-grab active:cursor-grabbing hover:-translate-y-1 flex flex-col justify-between"
|
||||
>
|
||||
|
||||
<!-- Top: Checkbox voor selectie + Roadmap badge -->
|
||||
<div class="flex items-center justify-between gap-2 mb-2.5">
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<label class="inline-flex items-center space-x-2 cursor-pointer select-none">
|
||||
<input type="checkbox" value="<?= $card['id'] ?>" data-content="<?= htmlspecialchars($card['content'], ENT_QUOTES) ?>" class="card-checkbox w-4 h-4 rounded bg-white dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-indigo-600 focus:ring-0">
|
||||
<span class="text-[11px] text-slate-500 dark:text-slate-400 font-medium">Selecteer</span>
|
||||
<input type="checkbox" value="<?= $card['id'] ?>" data-content="<?= htmlspecialchars($card['content'], ENT_QUOTES) ?>" class="card-checkbox w-4 h-4 rounded bg-white/80 dark:bg-slate-900/80 border-slate-400/50 text-indigo-600 focus:ring-0">
|
||||
<span class="text-[11px] opacity-70 font-medium">Selecteer</span>
|
||||
</label>
|
||||
|
||||
<div class="flex items-center space-x-1">
|
||||
<?php if (!empty($card['converted_to_roadmap_item_id'])): ?>
|
||||
<span class="px-2 py-0.5 rounded-md bg-teal-50 dark:bg-teal-500/15 text-teal-700 dark:text-teal-400 font-bold text-[10px] flex items-center space-x-1 border border-teal-200 dark:border-teal-500/20">
|
||||
<span class="px-2 py-0.5 rounded-md bg-teal-600/15 text-teal-800 dark:text-teal-300 font-bold text-[10px] flex items-center space-x-1 border border-teal-500/30">
|
||||
<i data-lucide="check" class="w-3 h-3"></i>
|
||||
<span>Op Roadmap</span>
|
||||
<span>Roadmap</span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<button type="button" onclick="openCardContextMenu(event, <?= $cardJson ?>)" class="p-1 rounded text-slate-500 hover:text-slate-800 dark:hover:text-white transition cursor-pointer" title="Opties & Inplannen (Rechtermuisknop)">
|
||||
<i data-lucide="more-horizontal" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inhoud Post-it met verfijnde typografie en uitlijning -->
|
||||
<div class="text-left py-1">
|
||||
<p class="text-xs text-slate-800 dark:text-slate-100 font-medium leading-relaxed whitespace-pre-wrap break-words text-left">
|
||||
<div class="text-left py-1" onclick="openEditCardModal(<?= $cardJson ?>)">
|
||||
<p class="text-xs font-semibold leading-relaxed whitespace-pre-wrap break-words text-left cursor-pointer" title="Klik om te bewerken">
|
||||
<?= htmlspecialchars($card['content']) ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Assignee & Agenda Tijdblok Badge (indien gekoppeld) -->
|
||||
<?php if ($hasAssignee || $hasSchedule): ?>
|
||||
<div class="mt-2.5 pt-2 border-t border-black/10 dark:border-white/10 flex items-center justify-between gap-1 text-[10px]">
|
||||
<?php if ($hasAssignee): ?>
|
||||
<div class="flex items-center space-x-1.5 truncate">
|
||||
<?php if (!empty($card['assignee_avatar'])): ?>
|
||||
<img src="<?= htmlspecialchars($card['assignee_avatar']) ?>" class="w-4 h-4 rounded-full object-cover border border-black/10">
|
||||
<?php else: ?>
|
||||
<div class="w-4 h-4 rounded-full bg-indigo-600 text-white font-bold text-[8px] flex items-center justify-center">
|
||||
<?= strtoupper(substr($card['assignee_name'], 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<span class="font-bold truncate max-w-[100px]"><?= htmlspecialchars($card['assignee_name']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasSchedule): ?>
|
||||
<span class="inline-flex items-center space-x-1 font-bold px-1.5 py-0.5 rounded bg-black/10 dark:bg-white/10 text-[9px]" title="Ingepland op <?= htmlspecialchars($card['scheduled_date']) ?>">
|
||||
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
|
||||
<span><?= htmlspecialchars(substr($card['scheduled_start_time'], 0, 5)) ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Footer van kaart -->
|
||||
<div class="mt-3 pt-2.5 border-t border-slate-200/60 dark:border-slate-800/60 flex items-center justify-between text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<div class="mt-2.5 pt-2 border-t border-black/10 dark:border-white/10 flex items-center justify-between text-[10px] opacity-80">
|
||||
<div class="flex items-center space-x-1.5 min-w-0">
|
||||
<span class="truncate font-semibold text-slate-700 dark:text-slate-300"><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
|
||||
<span class="truncate font-medium"><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
|
||||
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
|
||||
<button onclick="openAiBreakdownModal(<?= $card['id'] ?>, <?= htmlspecialchars(json_encode($card['content']), ENT_QUOTES) ?>)" class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-lg bg-violet-100/90 hover:bg-violet-200 dark:bg-violet-950/60 dark:hover:bg-violet-900/60 text-violet-700 dark:text-violet-300 font-bold text-[10px] transition cursor-pointer border border-violet-200/80 dark:border-violet-800/60 shadow-2xs flex-shrink-0" title="Splits dit idee uit naar behapbare taken via Google Gemini Pro">
|
||||
<i data-lucide="sparkles" class="w-3 h-3 text-violet-500"></i>
|
||||
<button onclick="openAiBreakdownModal(<?= $card['id'] ?>, <?= htmlspecialchars(json_encode($card['content']), ENT_QUOTES) ?>)" class="inline-flex items-center space-x-1 px-1.5 py-0.5 rounded bg-black/10 hover:bg-black/15 dark:bg-white/10 dark:hover:bg-white/15 font-bold text-[9px] transition cursor-pointer" title="Splits dit idee uit naar taken via AI">
|
||||
<i data-lucide="sparkles" class="w-2.5 h-2.5 text-violet-600 dark:text-violet-400"></i>
|
||||
<span>AI</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
@@ -191,13 +297,13 @@ $categories = [
|
||||
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
<!-- Upvote button -->
|
||||
<button onclick="voteCard(<?= $card['id'] ?>)" class="flex items-center space-x-1 px-2 py-1 rounded-lg bg-white/80 hover:bg-white dark:bg-slate-800/80 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-300 hover:text-amber-500 dark:hover:text-amber-400 border border-slate-200/80 dark:border-transparent transition shadow-xs cursor-pointer">
|
||||
<button onclick="voteCard(<?= $card['id'] ?>)" class="flex items-center space-x-1 px-2 py-0.5 rounded bg-black/10 hover:bg-black/15 dark:bg-white/10 dark:hover:bg-white/15 transition shadow-2xs cursor-pointer">
|
||||
<i data-lucide="thumbs-up" class="w-3 h-3"></i>
|
||||
<span id="votes-<?= $card['id'] ?>" class="font-bold"><?= $card['votes'] ?></span>
|
||||
</button>
|
||||
|
||||
<!-- Delete button -->
|
||||
<button onclick="deleteCard(<?= $card['id'] ?>)" class="p-1 rounded text-slate-400 hover:text-rose-600 dark:text-slate-500 dark:hover:text-rose-400 transition opacity-0 group-hover:opacity-100 cursor-pointer">
|
||||
<button onclick="deleteCard(<?= $card['id'] ?>)" class="p-1 rounded text-rose-600 hover:text-rose-700 opacity-0 group-hover:opacity-100 transition cursor-pointer" title="Verwijderen">
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -217,23 +323,66 @@ $categories = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Nieuwe Kaart Toevoegen -->
|
||||
<div id="addCardModal" onclick="if(event.target === this) closeAddCardModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<!-- Floating Rechtermuisknop Contextmenu -->
|
||||
<div id="cardContextMenu" class="hidden fixed z-[999] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1.5 w-64 text-xs font-medium text-slate-700 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-100">
|
||||
<div class="px-3.5 py-1.5 border-b border-slate-100 dark:border-slate-800/80 mb-1">
|
||||
<span id="ctxMenuCardTitle" class="text-[11px] font-bold text-slate-900 dark:text-white truncate block"></span>
|
||||
<span id="ctxMenuCardMeta" class="text-[10px] text-slate-400 truncate block"></span>
|
||||
</div>
|
||||
|
||||
<button onclick="triggerCtxEdit()" 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>Kaart bewerken</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerCtxAssign()" 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="user-check" class="w-4 h-4 text-emerald-500"></i>
|
||||
<span>Toewijzen & Agenda inplannen</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerCtxQuickSlot()" 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="zap" class="w-4 h-4 text-amber-500"></i>
|
||||
<span>⚡ Zoek direct vrij blok</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerCtxRoadmap()" 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="map" class="w-4 h-4 text-sky-500"></i>
|
||||
<span>Zet om naar Roadmap</span>
|
||||
</button>
|
||||
|
||||
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
|
||||
<button onclick="triggerCtxBreakdown()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-violet-50 dark:hover:bg-violet-950/50 hover:text-violet-600 dark:hover:text-violet-400 transition cursor-pointer">
|
||||
<i data-lucide="sparkles" class="w-4 h-4 text-violet-500"></i>
|
||||
<span>AI Splits uit in taken</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
|
||||
|
||||
<button onclick="triggerCtxDelete()" 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>Kaart verwijderen</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Kaart Bewerken -->
|
||||
<div id="editCardModal" onclick="if(event.target === this) closeEditCardModal()" 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 shadow-2xl max-w-md 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="sticky-note" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||||
<span>Brainstormkaart Toevoegen</span>
|
||||
<i data-lucide="edit-3" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||||
<span>Brainstormkaart Bewerken</span>
|
||||
</h3>
|
||||
<button onclick="closeAddCardModal()" 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">
|
||||
<button onclick="closeEditCardModal()" 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">
|
||||
<input type="hidden" id="editCardId" value="">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Categorie</label>
|
||||
<select id="modalCardCategory" 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">
|
||||
<select id="editCardCategory" 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="Ideeën">💡 Ideeën & Concepten</option>
|
||||
<option value="Knelpunten">⚠️ Knelpunten & Uitdagingen</option>
|
||||
<option value="Oplossingen">🚀 Oplossingen & Features</option>
|
||||
@@ -242,15 +391,125 @@ $categories = [
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Idee / Beschrijving</label>
|
||||
<textarea id="modalCardContent" rows="4" placeholder="Schrijf hier uw inzicht, idee of knelpunt..." 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>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Inhoud Post-it</label>
|
||||
<textarea id="editCardContent" rows="4" 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 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-none shadow-2xs"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||||
<button onclick="submitNewCard()" 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">
|
||||
Plaats op Canvas
|
||||
<button onclick="submitEditCard()" 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">
|
||||
Wijzigingen Opslaan
|
||||
</button>
|
||||
<button onclick="closeAddCardModal()" 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">
|
||||
<button onclick="closeEditCardModal()" 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: Toewijzen aan Collega & Inplannen in Agenda (Vrij Blok) -->
|
||||
<div id="assignScheduleModal" onclick="if(event.target === this) closeAssignScheduleModal()" 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 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-indigo-500/10 text-indigo-600 dark:text-indigo-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">Toewijzen & Agenda Inplanning</h3>
|
||||
<p class="text-[11px] text-slate-400">Plan deze brainstormkaart in als taak in het vrije blok van een collega</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeAssignScheduleModal()" 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">
|
||||
<input type="hidden" id="assignCardId" value="">
|
||||
|
||||
<div class="p-3 bg-slate-50 dark:bg-slate-900/60 rounded-xl border border-slate-200 dark:border-slate-800 text-xs">
|
||||
<span class="text-slate-400 text-[10px] uppercase font-bold tracking-wider block mb-1">Kaart</span>
|
||||
<p id="assignCardContentPreview" class="text-slate-800 dark:text-slate-200 font-medium line-clamp-2"></p>
|
||||
</div>
|
||||
|
||||
<!-- Collega selecteren -->
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">
|
||||
Toewijzen aan collega *
|
||||
</label>
|
||||
<select id="assignColleagueSelect" onchange="onAssignColleagueChange()" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer">
|
||||
<option value="">-- Kies een collega --</option>
|
||||
<?php foreach ($teamMembers as $tm): ?>
|
||||
<option value="<?= $tm['id'] ?>" data-has-outlook="<?= $tm['has_outlook'] ? '1' : '0' ?>" data-name="<?= htmlspecialchars($tm['name']) ?>">
|
||||
<?= htmlspecialchars($tm['name']) ?> <?= $tm['has_outlook'] ? '📅 (Outlook 365)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p id="assignOutlookNotice" class="hidden mt-1 text-[11px] text-sky-600 dark:text-sky-400 font-medium flex items-center space-x-1">
|
||||
<i data-lucide="calendar-check" class="w-3 h-3"></i>
|
||||
<span>Deze collega heeft Microsoft Outlook 365 actief. De afspraak kan direct in Outlook gezet worden!</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Slim Zoeken: Knop Eerstvolgend Vrij Blok -->
|
||||
<div class="p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between gap-3">
|
||||
<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-[11px] text-amber-700 dark:text-amber-400">Zoekt binnen de gezette werktijden en Outlook agenda</span>
|
||||
</div>
|
||||
<button type="button" id="btnFindSlot" onclick="findFreeSlotForColleague()" 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="foundSlotNotice" class="hidden p-2.5 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="foundSlotText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Datum & Tijdblok -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 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="assignPlanDate" value="<?= date('Y-m-d') ?>" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
|
||||
<input type="time" id="assignStartTime" value="09:00" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Duur</label>
|
||||
<select id="assignDurationMinutes" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none">
|
||||
<option value="15">15 minuten</option>
|
||||
<option value="30" selected>30 minuten</option>
|
||||
<option value="45">45 minuten</option>
|
||||
<option value="60">1 uur (60m)</option>
|
||||
<option value="90">1,5 uur (90m)</option>
|
||||
<option value="120">2 uur (120m)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vinkje Outlook -->
|
||||
<label class="flex items-center space-x-2 cursor-pointer pt-1">
|
||||
<input type="checkbox" id="assignSyncOutlook" checked class="rounded text-indigo-600 focus:ring-indigo-500">
|
||||
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200">
|
||||
Direct als agenda-afspraak plaatsen in Outlook 365 van deze collega
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||||
<button type="button" onclick="submitAssignAndSchedule()" id="btnSubmitAssign" 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 flex items-center justify-center space-x-1.5">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
<span>Inplannen & Toewijzen</span>
|
||||
</button>
|
||||
<button type="button" onclick="closeAssignScheduleModal()" 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>
|
||||
@@ -564,6 +823,260 @@ async function deleteCard(cardId) {
|
||||
document.getElementById(`card-${cardId}`)?.remove();
|
||||
}
|
||||
|
||||
// --- Rechtermuisknop Contextmenu Logica ---
|
||||
let _activeCtxCard = null;
|
||||
|
||||
function openCardContextMenu(e, card) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
_activeCtxCard = card;
|
||||
|
||||
const menu = document.getElementById('cardContextMenu');
|
||||
const titleEl = document.getElementById('ctxMenuCardTitle');
|
||||
const metaEl = document.getElementById('ctxMenuCardMeta');
|
||||
|
||||
const firstLine = card.content.split('\n')[0] || 'Brainstormkaart';
|
||||
titleEl.innerText = firstLine;
|
||||
metaEl.innerText = `${card.category} ${card.assignee_name ? '• ' + card.assignee_name : ''}`;
|
||||
|
||||
// Positioneer het menu op de muiscursor
|
||||
const mouseX = e ? e.clientX : window.innerWidth / 2;
|
||||
const mouseY = e ? e.clientY : window.innerHeight / 2;
|
||||
|
||||
menu.classList.remove('hidden');
|
||||
menu.style.left = `${Math.min(mouseX, window.innerWidth - 270)}px`;
|
||||
menu.style.top = `${Math.min(mouseY, window.innerHeight - 300)}px`;
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeCardContextMenu() {
|
||||
const menu = document.getElementById('cardContextMenu');
|
||||
if (menu) menu.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Contextmenu acties
|
||||
function triggerCtxEdit() {
|
||||
const card = _activeCtxCard;
|
||||
closeCardContextMenu();
|
||||
if (card) openEditCardModal(card);
|
||||
}
|
||||
|
||||
function triggerCtxAssign() {
|
||||
const card = _activeCtxCard;
|
||||
closeCardContextMenu();
|
||||
if (card) openAssignScheduleModal(card);
|
||||
}
|
||||
|
||||
function triggerCtxQuickSlot() {
|
||||
const card = _activeCtxCard;
|
||||
closeCardContextMenu();
|
||||
if (card) {
|
||||
openAssignScheduleModal(card);
|
||||
setTimeout(() => findFreeSlotForColleague(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCtxRoadmap() {
|
||||
const card = _activeCtxCard;
|
||||
closeCardContextMenu();
|
||||
if (card) {
|
||||
const chk = document.querySelector(`.card-checkbox[value="${card.id}"]`);
|
||||
if (chk) chk.checked = true;
|
||||
openConvertModal();
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCtxBreakdown() {
|
||||
const card = _activeCtxCard;
|
||||
closeCardContextMenu();
|
||||
if (card) openAiBreakdownModal(card.id, card.content);
|
||||
}
|
||||
|
||||
function triggerCtxDelete() {
|
||||
const card = _activeCtxCard;
|
||||
closeCardContextMenu();
|
||||
if (card) deleteCard(card.id);
|
||||
}
|
||||
|
||||
// Sluit contextmenu bij klik erbuiten
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('#cardContextMenu')) {
|
||||
closeCardContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Kaart Bewerken Modal Logica ---
|
||||
function openEditCardModal(card) {
|
||||
document.getElementById('editCardId').value = card.id;
|
||||
document.getElementById('editCardCategory').value = card.category || 'Ideeën';
|
||||
document.getElementById('editCardContent').value = card.content || '';
|
||||
document.getElementById('editCardModal').classList.remove('hidden');
|
||||
document.getElementById('editCardContent').focus();
|
||||
}
|
||||
|
||||
function closeEditCardModal() {
|
||||
document.getElementById('editCardModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitEditCard() {
|
||||
const cardId = parseInt(document.getElementById('editCardId').value, 10);
|
||||
const category = document.getElementById('editCardCategory').value;
|
||||
const content = document.getElementById('editCardContent').value.trim();
|
||||
|
||||
if (!cardId || !content) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/brainstorm.php?action=update_card', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ card_id: cardId, category, content })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeEditCardModal();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Kon kaart niet bijwerken');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij bijwerken: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Toewijzen & Agenda Inplannen Modal Logica ---
|
||||
function openAssignScheduleModal(card) {
|
||||
document.getElementById('assignCardId').value = card.id;
|
||||
document.getElementById('assignCardContentPreview').innerText = card.content;
|
||||
|
||||
const sel = document.getElementById('assignColleagueSelect');
|
||||
if (card.assignee_id) {
|
||||
sel.value = card.assignee_id;
|
||||
} else {
|
||||
sel.value = '';
|
||||
}
|
||||
onAssignColleagueChange();
|
||||
|
||||
if (card.scheduled_date) {
|
||||
document.getElementById('assignPlanDate').value = card.scheduled_date;
|
||||
} else {
|
||||
document.getElementById('assignPlanDate').value = new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
if (card.scheduled_start_time) {
|
||||
document.getElementById('assignStartTime').value = card.scheduled_start_time.slice(0, 5);
|
||||
} else {
|
||||
document.getElementById('assignStartTime').value = '09:00';
|
||||
}
|
||||
|
||||
document.getElementById('foundSlotNotice').classList.add('hidden');
|
||||
document.getElementById('assignScheduleModal').classList.remove('hidden');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeAssignScheduleModal() {
|
||||
document.getElementById('assignScheduleModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function onAssignColleagueChange() {
|
||||
const sel = document.getElementById('assignColleagueSelect');
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
const notice = document.getElementById('assignOutlookNotice');
|
||||
if (opt && opt.getAttribute('data-has-outlook') === '1') {
|
||||
notice.classList.remove('hidden');
|
||||
} else {
|
||||
notice.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Zoek eerstvolgend vrij blok van de geselecteerde collega
|
||||
async function findFreeSlotForColleague() {
|
||||
const sel = document.getElementById('assignColleagueSelect');
|
||||
const targetUserId = sel.value ? parseInt(sel.value, 10) : <?= $userId ?>;
|
||||
const duration = parseInt(document.getElementById('assignDurationMinutes').value, 10) || 30;
|
||||
|
||||
const btn = document.getElementById('btnFindSlot');
|
||||
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 (window.lucide) 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('assignPlanDate').value = slot.date;
|
||||
document.getElementById('assignStartTime').value = slot.start;
|
||||
|
||||
const notice = document.getElementById('foundSlotNotice');
|
||||
const noticeText = document.getElementById('foundSlotText');
|
||||
noticeText.innerHTML = `Vrij blok gevonden: <strong>${slot.formatted_date}</strong> om <strong>${slot.start} – ${slot.end}</strong> (${duration} min)`;
|
||||
notice.classList.remove('hidden');
|
||||
} else {
|
||||
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen werktijden.');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij zoeken van vrij blok: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAssignAndSchedule() {
|
||||
const cardId = parseInt(document.getElementById('assignCardId').value, 10);
|
||||
const assigneeId = document.getElementById('assignColleagueSelect').value ? parseInt(document.getElementById('assignColleagueSelect').value, 10) : null;
|
||||
const planDate = document.getElementById('assignPlanDate').value;
|
||||
const startTime = document.getElementById('assignStartTime').value;
|
||||
const durationMinutes = parseInt(document.getElementById('assignDurationMinutes').value, 10) || 30;
|
||||
const syncOutlook = document.getElementById('assignSyncOutlook').checked;
|
||||
|
||||
const btn = document.getElementById('btnSubmitAssign');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Opslaan...</span>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/brainstorm.php?action=assign_and_schedule', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
card_id: cardId,
|
||||
assignee_id: assigneeId,
|
||||
plan_date: planDate,
|
||||
start_time: startTime,
|
||||
duration_minutes: durationMinutes,
|
||||
sync_outlook: syncOutlook
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeAssignScheduleModal();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Kon toewijzing niet opslaan');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij opslaan: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Drag & Drop tussen Brainstorm Categorie Lanes ---
|
||||
let draggedCanvasCardId = null;
|
||||
|
||||
@@ -628,6 +1141,10 @@ async function handleCanvasLaneDrop(e, targetCategory) {
|
||||
const cardEl = document.getElementById(`card-${cardId}`);
|
||||
if (!cardEl) return;
|
||||
|
||||
// Directe reactivatie van de kaart
|
||||
cardEl.classList.remove('opacity-40', 'scale-[0.98]', 'pointer-events-none');
|
||||
document.querySelectorAll('.opacity-40.scale-\\[0\\.98\\]').forEach(el => el.classList.remove('opacity-40', 'scale-[0.98]', 'pointer-events-none'));
|
||||
|
||||
const sourceLane = cardEl.closest('.brainstorm-lane');
|
||||
const sourceCategory = sourceLane ? sourceLane.getAttribute('data-category') : null;
|
||||
|
||||
|
||||
@@ -175,6 +175,13 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
// Centrale Systeeminstellingen (AI, Werkdagen, Sessiebeveiliging)
|
||||
"CREATE TABLE IF NOT EXISTS `system_settings` (`setting_key` VARCHAR(64) PRIMARY KEY, `setting_value` LONGTEXT NULL, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
"ALTER TABLE `system_settings` MODIFY COLUMN `setting_value` LONGTEXT NULL",
|
||||
// Brainstorm kaarten toewijzen aan collega's en agenda-inplanning
|
||||
"ALTER TABLE brainstorm_cards ADD COLUMN assignee_id INT NULL AFTER color",
|
||||
"ALTER TABLE brainstorm_cards ADD COLUMN scheduled_date DATE NULL AFTER assignee_id",
|
||||
"ALTER TABLE brainstorm_cards ADD COLUMN scheduled_start_time VARCHAR(10) NULL AFTER scheduled_date",
|
||||
"ALTER TABLE brainstorm_cards ADD COLUMN scheduled_end_time VARCHAR(10) NULL AFTER scheduled_start_time",
|
||||
"ALTER TABLE brainstorm_cards ADD COLUMN scheduled_task_id INT NULL AFTER scheduled_end_time",
|
||||
"ALTER TABLE brainstorm_cards ADD INDEX idx_bc_assignee (assignee_id)",
|
||||
];
|
||||
foreach ($colUpgrades as $sql) {
|
||||
try {
|
||||
|
||||
@@ -101,12 +101,18 @@ CREATE TABLE IF NOT EXISTS `brainstorm_cards` (
|
||||
`content` TEXT NOT NULL,
|
||||
`category` VARCHAR(100) NOT NULL DEFAULT 'Ideeën',
|
||||
`color` VARCHAR(20) NOT NULL DEFAULT 'amber',
|
||||
`assignee_id` INT NULL,
|
||||
`scheduled_date` DATE NULL,
|
||||
`scheduled_start_time` VARCHAR(10) NULL,
|
||||
`scheduled_end_time` VARCHAR(10) NULL,
|
||||
`scheduled_task_id` INT NULL,
|
||||
`votes` INT NOT NULL DEFAULT 0,
|
||||
`author_name` VARCHAR(120) NULL,
|
||||
`converted_to_roadmap_item_id` INT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX (`session_id`),
|
||||
INDEX (`assignee_id`),
|
||||
CONSTRAINT `fk_cards_session` FOREIGN KEY (`session_id`) REFERENCES `brainstorm_sessions` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
+539
-2
@@ -38,6 +38,21 @@ try {
|
||||
}
|
||||
|
||||
$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">
|
||||
@@ -121,8 +136,17 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
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) bovenaan"
|
||||
title="Sleep naar een ander kwartaal (Q1, Q2, Q3, Q4) of klik met rechtermuisknop"
|
||||
>
|
||||
|
||||
<!-- Top header van item -->
|
||||
@@ -296,6 +320,237 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
</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) {
|
||||
@@ -405,6 +660,14 @@ async function handleRoadmapQuarterDrop(e, targetQuarter) {
|
||||
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 {
|
||||
@@ -425,10 +688,284 @@ async function handleRoadmapQuarterDrop(e, targetQuarter) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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.add('hidden');
|
||||
if (modal && !modal.classList.contains('hidden')) modal.classList.add('hidden');
|
||||
closeEditRoadmapModal();
|
||||
closeRoadmapScheduleModal();
|
||||
closeRoadmapContextMenu();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
+177
@@ -353,6 +353,25 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knop Vrij Blok Zoeken voor Assignee -->
|
||||
<div class="p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between gap-2">
|
||||
<div class="text-[11px]">
|
||||
<span class="font-bold text-amber-900 dark:text-amber-200 flex items-center space-x-1">
|
||||
<i data-lucide="sparkles" class="w-3 h-3 text-amber-500"></i>
|
||||
<span>Vrij agendablok zoeken</span>
|
||||
</span>
|
||||
<span class="text-[10px] text-amber-700 dark:text-amber-400">Zoekt binnen werktijden van de toegewezen collega</span>
|
||||
</div>
|
||||
<button type="button" id="btnTimelineFindSlot" onclick="findFreeSlotForTimelineAssignee()" class="px-2.5 py-1 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-bold text-[10px] shadow-xs transition flex items-center space-x-1 flex-shrink-0 cursor-pointer">
|
||||
<i data-lucide="search" class="w-3 h-3"></i>
|
||||
<span>Zoek vrij blok</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="timelineFoundSlotNotice" class="hidden p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-300 dark:border-emerald-800 text-[11px] text-emerald-800 dark:text-emerald-300 font-medium flex items-center space-x-1.5">
|
||||
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
|
||||
<span id="timelineFoundSlotText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Outlook Agenda Synchronisatie -->
|
||||
<div class="flex items-center justify-between pt-2 border-t border-slate-200/50 dark:border-slate-800/50">
|
||||
<div class="flex items-center space-x-2">
|
||||
@@ -393,6 +412,26 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechtermuisknop Contextmenu voor Tijdlijn Items -->
|
||||
<div id="timelineContextMenu" 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="triggerTimelineCtxEdit()" 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>Taak bewerken</span>
|
||||
</button>
|
||||
|
||||
<button onclick="triggerTimelineCtxQuickSlot()" 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="zap" class="w-4 h-4 text-amber-500"></i>
|
||||
<span>⚡ Zoek direct vrij blok</span>
|
||||
</button>
|
||||
|
||||
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
|
||||
|
||||
<button onclick="triggerTimelineCtxDelete()" 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>Taak verwijderen</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- Globale Status voor Tijdlijn ---
|
||||
let currentBoardId = null;
|
||||
@@ -573,6 +612,13 @@ function renderTimelineLaneDropBar() {
|
||||
dropBadge.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-100', 'dark:bg-indigo-900/60');
|
||||
const cardIdStr = e.dataTransfer.getData('text/plain');
|
||||
const cardId = parseInt(cardIdStr, 10);
|
||||
if (cardId) {
|
||||
const el = document.querySelector(`.timeline-row[data-card-id="${cardId}"]`);
|
||||
if (el) el.classList.remove('opacity-50', 'bg-indigo-50/50', 'dark:bg-indigo-950/30', 'pointer-events-none');
|
||||
}
|
||||
document.querySelectorAll('.timeline-row.opacity-50').forEach(el => {
|
||||
el.classList.remove('opacity-50', 'bg-indigo-50/50', 'dark:bg-indigo-950/30', 'pointer-events-none');
|
||||
});
|
||||
if (cardId && lane.id) {
|
||||
await moveTimelineCardToLane(cardId, lane.id);
|
||||
}
|
||||
@@ -726,6 +772,11 @@ function renderTimeline() {
|
||||
leftItem.setAttribute('draggable', 'true');
|
||||
leftItem.setAttribute('data-card-id', card.id);
|
||||
|
||||
leftItem.oncontextmenu = (e) => {
|
||||
openTimelineContextMenu(e, card.id);
|
||||
return false;
|
||||
};
|
||||
|
||||
leftItem.ondragstart = (e) => {
|
||||
e.dataTransfer.setData('text/plain', String(card.id));
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
@@ -772,6 +823,11 @@ function renderTimeline() {
|
||||
rightRow.className = 'timeline-task-row timeline-row h-12 relative flex items-center';
|
||||
rightRow.setAttribute('data-card-id', card.id);
|
||||
|
||||
rightRow.oncontextmenu = (e) => {
|
||||
openTimelineContextMenu(e, card.id);
|
||||
return false;
|
||||
};
|
||||
|
||||
// Klikken op een lege plek in de rij om direct in te plannen
|
||||
rightRow.onclick = (e) => {
|
||||
if (e.target.closest('.timeline-bar')) return;
|
||||
@@ -796,6 +852,10 @@ function renderTimeline() {
|
||||
barEl.style.left = `${leftPx}px`;
|
||||
barEl.style.width = `${widthPx}px`;
|
||||
barEl.title = `${escapeHtml(card.title)} (${card.start_date} t/m ${card.end_date})`;
|
||||
barEl.oncontextmenu = (ev) => {
|
||||
openTimelineContextMenu(ev, card.id);
|
||||
return false;
|
||||
};
|
||||
barEl.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
openCardModal(card.id);
|
||||
@@ -1165,9 +1225,126 @@ function escapeHtml(str) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// --- Rechtermuisknop Context Menu voor Tijdlijn ---
|
||||
let activeTimelineCtxCardId = null;
|
||||
|
||||
function openTimelineContextMenu(e, cardId) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
activeTimelineCtxCardId = cardId;
|
||||
const menu = document.getElementById('timelineContextMenu');
|
||||
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 closeTimelineContextMenu() {
|
||||
const menu = document.getElementById('timelineContextMenu');
|
||||
if (menu) menu.classList.add('hidden');
|
||||
activeTimelineCtxCardId = null;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('#timelineContextMenu')) {
|
||||
closeTimelineContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
function triggerTimelineCtxEdit() {
|
||||
if (!activeTimelineCtxCardId) return;
|
||||
const cid = activeTimelineCtxCardId;
|
||||
closeTimelineContextMenu();
|
||||
openCardModal(cid);
|
||||
}
|
||||
|
||||
function triggerTimelineCtxQuickSlot() {
|
||||
if (!activeTimelineCtxCardId) return;
|
||||
const cid = activeTimelineCtxCardId;
|
||||
closeTimelineContextMenu();
|
||||
openCardModal(cid);
|
||||
setTimeout(() => {
|
||||
findFreeSlotForTimelineAssignee();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
async function triggerTimelineCtxDelete() {
|
||||
if (!activeTimelineCtxCardId) return;
|
||||
const cid = activeTimelineCtxCardId;
|
||||
closeTimelineContextMenu();
|
||||
if (!confirm('Weet je zeker dat je deze taak wilt verwijderen?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/board.php?action=delete_card', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ card_id: cid })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await loadBoardData(currentBoardId);
|
||||
} else {
|
||||
alert(data.error || 'Kon taak niet verwijderen');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Netwerkfout bij verwijderen');
|
||||
}
|
||||
}
|
||||
|
||||
// Zoek vrij agendablok voor de in de tijdlijn-modal geselecteerde assignee
|
||||
async function findFreeSlotForTimelineAssignee() {
|
||||
const sel = document.getElementById('cardAssigneeSelect');
|
||||
const targetUserId = sel.value ? parseInt(sel.value, 10) : currentUserId;
|
||||
const schedToggle = document.getElementById('cardScheduleToggle');
|
||||
if (!schedToggle.checked) {
|
||||
schedToggle.checked = true;
|
||||
toggleScheduleFields(true);
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnTimelineFindSlot');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 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: 60,
|
||||
target_user_id: targetUserId
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success && data.slot) {
|
||||
const slot = data.slot;
|
||||
document.getElementById('cardStartDate').value = slot.date;
|
||||
document.getElementById('cardEndDate').value = slot.date;
|
||||
|
||||
const notice = document.getElementById('timelineFoundSlotNotice');
|
||||
const noticeText = document.getElementById('timelineFoundSlotText');
|
||||
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 voor deze collega.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Netwerkfout bij zoeken van vrij agendablok: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeCardModal();
|
||||
closeTimelineContextMenu();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user