From 94d6325693a918217d43efd512abd649750d530c Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Sat, 12 Sep 2026 17:14:47 +0200 Subject: [PATCH] Postits en editable items. --- api/brainstorm.php | 162 ++++++++++- api/planner.php | 13 +- api/roadmap.php | 20 ++ board.php | 220 ++++++++++++++- brainstorm_canvas.php | 605 ++++++++++++++++++++++++++++++++++++++---- config/database.php | 7 + config/schema.sql | 6 + roadmap.php | 533 ++++++++++++++++++++++++++++++++++++- timeline.php | 170 ++++++++++++ 9 files changed, 1680 insertions(+), 56 deletions(-) diff --git a/api/brainstorm.php b/api/brainstorm.php index f40447f..94f2d88 100644 --- a/api/brainstorm.php +++ b/api/brainstorm.php @@ -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'] ?? []; diff --git a/api/planner.php b/api/planner.php index 0003fd7..d5988fe 100644 --- a/api/planner.php +++ b/api/planner.php @@ -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']); diff --git a/api/roadmap.php b/api/roadmap.php index 9260d59..a3aa865 100644 --- a/api/roadmap.php +++ b/api/roadmap.php @@ -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)); diff --git a/board.php b/board.php index 59e5d1a..b8a6d09 100644 --- a/board.php +++ b/board.php @@ -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); +}
@@ -277,6 +318,25 @@ $isAdmin = ($userRole === 'ADMIN');
+ +
+
+ + + Vrij agendablok zoeken + + Zoekt binnen werktijden van de toegewezen collega +
+ +
+ + + + diff --git a/timeline.php b/timeline.php index 78814dc..3c866a0 100644 --- a/timeline.php +++ b/timeline.php @@ -353,6 +353,25 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb { + +
+
+ + + Vrij agendablok zoeken + + Zoekt binnen werktijden van de toegewezen collega +
+ +
+ +
@@ -393,6 +412,26 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
+ + +