prepare("SELECT COUNT(*) FROM board_members WHERE board_id = ? AND user_id = ?"); $stmt->execute([$boardId, $userId]); return ((int)$stmt->fetchColumn()) > 0; } /** * Controleert of de gebruiker beheerder is van het bord */ function check_board_admin(PDO $db, int $boardId, int $userId, bool $isAdmin): bool { if ($isAdmin) return true; $stmt = $db->prepare("SELECT role FROM board_members WHERE board_id = ? AND user_id = ?"); $stmt->execute([$boardId, $userId]); $role = $stmt->fetchColumn(); return ($role === 'ADMIN'); } try { // 1. Alle beschikbare borden ophalen voor deze gebruiker if ($action === 'get_boards') { if ($isAdmin) { $stmt = $db->query(" SELECT b.*, u.name as creator_name, (SELECT COUNT(*) FROM board_cards WHERE board_id = b.id) as card_count FROM boards b LEFT JOIN users u ON b.created_by_id = u.id WHERE b.is_archived = 0 ORDER BY b.id ASC "); $boards = $stmt->fetchAll(); } else { $stmt = $db->prepare(" SELECT b.*, u.name as creator_name, (SELECT COUNT(*) FROM board_cards WHERE board_id = b.id) as card_count FROM boards b JOIN board_members bm ON b.id = bm.board_id LEFT JOIN users u ON b.created_by_id = u.id WHERE bm.user_id = ? AND b.is_archived = 0 ORDER BY b.id ASC "); $stmt->execute([$userId]); $boards = $stmt->fetchAll(); } echo json_encode([ 'success' => true, 'boards' => $boards, 'is_admin' => $isAdmin ]); exit; } // 2. Volledige bord-data ophalen (lanes, kaarten, toewijzingen, autorisaties) if ($action === 'get_board_data') { $boardId = (int)($input['board_id'] ?? ($_GET['board_id'] ?? 0)); // Als geen board_id is meegegeven, pak het eerste beschikbare bord if ($boardId <= 0) { if ($isAdmin) { $boardId = (int)$db->query("SELECT id FROM boards WHERE is_archived = 0 ORDER BY id ASC LIMIT 1")->fetchColumn(); } else { $stmtFirst = $db->prepare("SELECT b.id FROM boards b JOIN board_members bm ON b.id = bm.board_id WHERE bm.user_id = ? AND b.is_archived = 0 ORDER BY b.id ASC LIMIT 1"); $stmtFirst->execute([$userId]); $boardId = (int)$stmtFirst->fetchColumn(); } } if ($boardId <= 0) { echo json_encode([ 'success' => true, 'has_board' => false, 'message' => 'Geen actieve borden gevonden.' ]); exit; } if (!check_board_access($db, $boardId, $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen toegang tot dit bord. Vraag de beheerder om autorisatie.']); exit; } // Haal bord-informatie op $stmtB = $db->prepare("SELECT b.*, u.name as creator_name FROM boards b LEFT JOIN users u ON b.created_by_id = u.id WHERE b.id = ?"); $stmtB->execute([$boardId]); $board = $stmtB->fetch(); // Haal lanes op $stmtL = $db->prepare("SELECT * FROM board_lanes WHERE board_id = ? ORDER BY position ASC, id ASC"); $stmtL->execute([$boardId]); $lanes = $stmtL->fetchAll(); // Haal kaarten op met assignee en schedule $stmtC = $db->prepare(" SELECT c.*, u.name as assignee_name, u.email as assignee_email, u.avatar_url as assignee_avatar, s.id as schedule_id, s.schedule_type, s.start_date, s.end_date, s.start_time, s.end_time, s.days_of_week, s.outlook_event_id, s.outlook_synced_at FROM board_cards c LEFT JOIN users u ON c.assignee_id = u.id LEFT JOIN board_card_schedules s ON c.id = s.card_id WHERE c.board_id = ? ORDER BY c.position ASC, c.id DESC "); $stmtC->execute([$boardId]); $cards = $stmtC->fetchAll(); // Groepeer kaarten per lane $cardsByLane = []; foreach ($lanes as $lane) { $cardsByLane[$lane['id']] = []; } foreach ($cards as $card) { $lId = $card['lane_id']; if (!isset($cardsByLane[$lId])) { $cardsByLane[$lId] = []; } $cardsByLane[$lId][] = $card; } // Haal teamleden en Outlook integratie status op (voor toewijzing) $usersStmt = $db->query("SELECT id, name, email, role, avatar_url FROM users ORDER BY name ASC"); $teamMembers = $usersStmt->fetchAll(); // Check welke collega's een actieve Outlook koppeling hebben $outlookActiveUsers = []; $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) { $outlookActiveUsers[(int)$row['user_id']] = true; } foreach ($teamMembers as &$tm) { $tm['has_outlook'] = !empty($outlookActiveUsers[(int)$tm['id']]); } unset($tm); // Haal geautoriseerde leden van dit bord op $membersStmt = $db->prepare("SELECT user_id, role FROM board_members WHERE board_id = ?"); $membersStmt->execute([$boardId]); $authorizedMembers = []; foreach ($membersStmt->fetchAll() as $m) { $authorizedMembers[(int)$m['user_id']] = $m['role']; } echo json_encode([ 'success' => true, 'has_board' => true, 'board' => $board, 'lanes' => $lanes, 'cards_by_lane' => $cardsByLane, 'team_members' => $teamMembers, 'authorized_members' => $authorizedMembers, 'is_board_admin' => check_board_admin($db, $boardId, $userId, $isAdmin), 'current_user_id' => $userId, ]); exit; } // 3. Nieuw bord aanmaken (Beheerder) if ($action === 'create_board') { if (!$isAdmin) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Alleen beheerders kunnen nieuwe borden aanmaken.']); exit; } $title = trim($input['title'] ?? ''); $description = trim($input['description'] ?? ''); if (empty($title)) { echo json_encode(['success' => false, 'error' => 'Vul een bordtitel in.']); exit; } $stmt = $db->prepare("INSERT INTO boards (title, description, created_by_id) VALUES (?, ?, ?)"); $stmt->execute([$title, $description, $userId]); $boardId = (int)$db->lastInsertId(); // Voeg maker toe als beheerder van het bord $db->prepare("INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, 'ADMIN')") ->execute([$boardId, $userId]); // Standaard lanes aanmaken $defaultLanes = [ ['title' => 'Open', 'color' => 'slate', 'pos' => 0], ['title' => 'In uitvoering', 'color' => 'amber', 'pos' => 1], ['title' => 'Testen', 'color' => 'purple', 'pos' => 2], ['title' => 'Klaar', 'color' => 'emerald', 'pos' => 3], ]; $laneStmt = $db->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)"); foreach ($defaultLanes as $l) { $laneStmt->execute([$boardId, $l['title'], $l['color'], $l['pos']]); } echo json_encode([ 'success' => true, 'board_id' => $boardId, 'message' => 'Bord succesvol aangemaakt!' ]); exit; } // 4. Bord autorisaties / leden beheren (Beheerder) if ($action === 'manage_members') { $boardId = (int)($input['board_id'] ?? 0); if (!check_board_admin($db, $boardId, $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen beheerrechten voor dit bord.']); exit; } $memberIds = $input['member_ids'] ?? []; if (!is_array($memberIds)) $memberIds = []; // Huidige leden bijwerken $db->prepare("DELETE FROM board_members WHERE board_id = ?")->execute([$boardId]); $insertStmt = $db->prepare("INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, 'MEMBER')"); foreach ($memberIds as $mId) { $mId = (int)$mId; if ($mId > 0) { $insertStmt->execute([$boardId, $mId]); } } // Zorg dat beheerder altijd lid blijft $db->prepare("INSERT IGNORE INTO board_members (board_id, user_id, role) VALUES (?, ?, 'ADMIN')")->execute([$boardId, $userId]); echo json_encode(['success' => true, 'message' => 'Toegangsrechten voor het bord succesvol bijgewerkt.']); exit; } // 5. Kolom / Lane toevoegen of bewerken if ($action === 'save_lane') { $boardId = (int)($input['board_id'] ?? 0); $laneId = (int)($input['lane_id'] ?? 0); $title = trim($input['title'] ?? ''); $color = trim($input['color'] ?? 'slate'); if (!check_board_admin($db, $boardId, $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Alleen bordbeheerders kunnen kolommen beheren.']); exit; } if (empty($title)) { echo json_encode(['success' => false, 'error' => 'Vul een kolomnaam in.']); exit; } if ($laneId > 0) { $stmt = $db->prepare("UPDATE board_lanes SET title = ?, color = ? WHERE id = ? AND board_id = ?"); $stmt->execute([$title, $color, $laneId, $boardId]); } else { $stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_lanes WHERE board_id = ?"); $stmtMax->execute([$boardId]); $maxPos = (int)$stmtMax->fetchColumn(); $stmt = $db->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)"); $stmt->execute([$boardId, $title, $color, $maxPos + 1]); $laneId = (int)$db->lastInsertId(); } echo json_encode(['success' => true, 'lane_id' => $laneId, 'message' => 'Kolom opgeslagen.']); exit; } // 6. Kolom / Lane verwijderen if ($action === 'delete_lane') { $boardId = (int)($input['board_id'] ?? 0); $laneId = (int)($input['lane_id'] ?? 0); if (!check_board_admin($db, $boardId, $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen beheerrechten.']); exit; } // Controleer of er kaarten in staan $stmtCardCnt = $db->prepare("SELECT COUNT(*) FROM board_cards WHERE lane_id = ?"); $stmtCardCnt->execute([$laneId]); $cardCount = (int)$stmtCardCnt->fetchColumn(); if ($cardCount > 0) { echo json_encode(['success' => false, 'error' => 'Verplaats of verwijder eerst de taken in deze kolom alvorens de kolom te verwijderen.']); exit; } $db->prepare("DELETE FROM board_lanes WHERE id = ? AND board_id = ?")->execute([$laneId, $boardId]); echo json_encode(['success' => true, 'message' => 'Kolom verwijderd.']); exit; } // 7. Kaart (Post-it / Opdracht) aanmaken of bewerken if ($action === 'save_card') { $boardId = (int)($input['board_id'] ?? 0); $cardId = (int)($input['card_id'] ?? 0); $laneId = (int)($input['lane_id'] ?? 0); $title = trim($input['title'] ?? ''); $description = trim($input['description'] ?? ''); $assigneeId = !empty($input['assignee_id']) ? (int)$input['assignee_id'] : null; $priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2'; $color = in_array($input['color'] ?? '', ['yellow', 'blue', 'green', 'purple', 'pink', 'orange', 'slate']) ? $input['color'] : 'yellow'; if (!check_board_access($db, $boardId, $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen toegang tot dit bord.']); exit; } if (empty($title)) { echo json_encode(['success' => false, 'error' => 'Vul een titel in voor de taak.']); exit; } if ($cardId > 0) { $stmt = $db->prepare(" UPDATE board_cards SET title = ?, description = ?, assignee_id = ?, priority = ?, color = ?, lane_id = ? WHERE id = ? AND board_id = ? "); $stmt->execute([$title, $description, $assigneeId, $priority, $color, $laneId, $cardId, $boardId]); } else { $stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = ?"); $stmtMax->execute([$laneId]); $maxPos = (int)$stmtMax->fetchColumn(); $stmt = $db->prepare(" INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([$boardId, $laneId, $title, $description, $assigneeId, $priority, $color, $maxPos + 1, $userId]); $cardId = (int)$db->lastInsertId(); } // Tijdspad opslaan en optioneel Outlook agenda synchroniseren indien meegegeven $scheduleData = $input['schedule'] ?? null; $outlookSyncMsg = null; if (!empty($scheduleData) && is_array($scheduleData) && !empty($scheduleData['enabled'])) { $scheduleType = in_array($scheduleData['type'] ?? '', ['SINGLE', 'DAILY_RANGE']) ? $scheduleData['type'] : 'SINGLE'; $startDate = trim($scheduleData['start_date'] ?? date('Y-m-d')); $endDate = ($scheduleType === 'DAILY_RANGE' && !empty($scheduleData['end_date'])) ? trim($scheduleData['end_date']) : $startDate; $startTime = trim($scheduleData['start_time'] ?? '14:00'); $endTime = trim($scheduleData['end_time'] ?? '15:00'); $daysOfWeek = trim($scheduleData['days_of_week'] ?? '1,2,3,4,5'); $syncOutlook = !empty($scheduleData['sync_outlook']); $outlookEventId = null; $outlookSyncedAt = null; // Als Outlook sync gevraagd is én er een assignee is: if ($syncOutlook && $assigneeId > 0) { // Controleer of de assignee Outlook gekoppeld heeft $stmtCheckInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1"); $stmtCheckInt->execute([$assigneeId]); $outlookInt = $stmtCheckInt->fetch(); if (!empty($outlookInt['access_token'])) { $startTs = strtotime("{$startDate} {$startTime}"); $endTs = strtotime("{$startDate} {$endTime}"); $durationMins = max(15, (int)(($endTs - $startTs) / 60)); // Maak agenda-afspraak aan in Outlook $eventPayload = [ 'subject' => "[Fociz Bord] {$title}", 'description' => "Opdracht toegewezen via Fociz Bord:\n\n{$description}\n\nTijdspad: {$startTime} - {$endTime}", 'date' => $startDate, 'start_time' => $startTime, 'end_time' => $endTime, 'duration_minutes' => $durationMins, ]; $resCreate = microsoft_create_event($assigneeId, $eventPayload); if (!empty($resCreate['success'])) { $outlookEventId = $resCreate['event_id'] ?? ($resCreate['id'] ?? null); $outlookSyncedAt = date('Y-m-d H:i:s'); $outlookSyncMsg = 'Taak succesvol ingepland in de Outlook agenda van de collega!'; } else { $outlookSyncMsg = 'Taak opgeslagen, maar Outlook-sync mislukt: ' . ($resCreate['error'] ?? 'onbekende fout'); } } else { $outlookSyncMsg = 'Let op: De toegewezen collega heeft Microsoft Outlook 365 nog niet gekoppeld.'; } } // Opslaan in board_card_schedules $stmtSched = $db->prepare(" INSERT INTO board_card_schedules (card_id, schedule_type, start_date, end_date, start_time, end_time, days_of_week, outlook_event_id, outlook_synced_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE schedule_type = VALUES(schedule_type), start_date = VALUES(start_date), end_date = VALUES(end_date), start_time = VALUES(start_time), end_time = VALUES(end_time), days_of_week = VALUES(days_of_week), outlook_event_id = COALESCE(VALUES(outlook_event_id), outlook_event_id), outlook_synced_at = COALESCE(VALUES(outlook_synced_at), outlook_synced_at) "); $stmtSched->execute([$cardId, $scheduleType, $startDate, $endDate, $startTime, $endTime, $daysOfWeek, $outlookEventId, $outlookSyncedAt]); } elseif (isset($scheduleData['enabled']) && !$scheduleData['enabled']) { // Tijdspad uitschakelen indien expliciet uitgezet $db->prepare("DELETE FROM board_card_schedules WHERE card_id = ?")->execute([$cardId]); } echo json_encode([ 'success' => true, 'card_id' => $cardId, 'outlook_message' => $outlookSyncMsg, 'message' => 'Taak succesvol opgeslagen!' ]); exit; } // 8. Kaart verplaatsen naar een andere lane of positie (Drag & Drop / Quick move) if ($action === 'move_card') { $cardId = (int)($input['card_id'] ?? 0); $targetLaneId = (int)($input['target_lane_id'] ?? 0); $targetPosition = (int)($input['target_position'] ?? 0); $stmtCard = $db->prepare("SELECT * FROM board_cards WHERE id = ?"); $stmtCard->execute([$cardId]); $card = $stmtCard->fetch(); if (!$card) { echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']); exit; } if (!check_board_access($db, (int)$card['board_id'], $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen toegang.']); exit; } // Valideer of de doellane daadwerkelijk bij hetzelfde bord hoort (voorkom IDOR/lane hijacking) $stmtLane = $db->prepare("SELECT id FROM board_lanes WHERE id = ? AND board_id = ?"); $stmtLane->execute([$targetLaneId, (int)$card['board_id']]); if (!$stmtLane->fetch()) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'Ongeldige doellane voor dit bord.']); exit; } $stmt = $db->prepare("UPDATE board_cards SET lane_id = ?, position = ? WHERE id = ?"); $stmt->execute([$targetLaneId, $targetPosition, $cardId]); echo json_encode(['success' => true]); exit; } // 9. Kaart verwijderen if ($action === 'delete_card') { $cardId = (int)($input['card_id'] ?? 0); $stmtCard = $db->prepare(" SELECT c.*, s.outlook_event_id FROM board_cards c LEFT JOIN board_card_schedules s ON c.id = s.card_id WHERE c.id = ? "); $stmtCard->execute([$cardId]); $card = $stmtCard->fetch(); if (!$card) { echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']); exit; } if (!check_board_access($db, (int)$card['board_id'], $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen rechten.']); exit; } // Indien gekoppeld aan Outlook van de assignee, verwijder ook daar de afspraak if (!empty($card['outlook_event_id']) && !empty($card['assignee_id'])) { try { microsoft_delete_event((int)$card['assignee_id'], $card['outlook_event_id']); } catch (Throwable $t) {} } $db->prepare("DELETE FROM board_cards WHERE id = ?")->execute([$cardId]); echo json_encode(['success' => true, 'message' => 'Taak verwijderd.']); exit; } // 10. Tijdlijn Datums Snel Bijwerken (Startdatum & Einddatum via Gantt drag) if ($action === 'update_card_dates') { $cardId = (int)($input['card_id'] ?? 0); $startDate = trim($input['start_date'] ?? ''); $endDate = trim($input['end_date'] ?? $startDate); if (!$cardId || !$startDate) { echo json_encode(['success' => false, 'error' => 'Ongeldige parameters.']); exit; } $stmtCard = $db->prepare("SELECT * FROM board_cards WHERE id = ?"); $stmtCard->execute([$cardId]); $card = $stmtCard->fetch(); if (!$card) { echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']); exit; } if (!check_board_access($db, (int)$card['board_id'], $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen toegang tot dit bord.']); exit; } $stmtSched = $db->prepare(" INSERT INTO board_card_schedules (card_id, schedule_type, start_date, end_date, start_time, end_time, days_of_week) VALUES (?, 'DAILY_RANGE', ?, ?, '09:00', '17:00', '1,2,3,4,5') ON DUPLICATE KEY UPDATE start_date = VALUES(start_date), end_date = VALUES(end_date), schedule_type = 'DAILY_RANGE' "); $stmtSched->execute([$cardId, $startDate, $endDate]); echo json_encode([ 'success' => true, 'card_id' => $cardId, 'start_date' => $startDate, 'end_date' => $endDate, 'message' => 'Tijdlijn bijgewerkt!' ]); exit; } // 11. Snelle Inline Taak Aanmaken (⚡ Wat moet er gedaan worden?) if ($action === 'quick_create_card') { $boardId = (int)($input['board_id'] ?? 0); $title = trim($input['title'] ?? ''); $laneId = (int)($input['lane_id'] ?? 0); $startDate = !empty($input['start_date']) ? trim($input['start_date']) : null; $endDate = !empty($input['end_date']) ? trim($input['end_date']) : $startDate; if (!$boardId || empty($title)) { echo json_encode(['success' => false, 'error' => 'Vul een titel in voor de taak.']); exit; } if (!check_board_access($db, $boardId, $userId, $isAdmin)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Geen toegang tot dit bord.']); exit; } // Als geen lane_id is meegegeven, pak de 1e lane van het bord if ($laneId <= 0) { $stmtLane = $db->prepare("SELECT id FROM board_lanes WHERE board_id = ? ORDER BY position ASC, id ASC LIMIT 1"); $stmtLane->execute([$boardId]); $laneId = (int)$stmtLane->fetchColumn(); } $stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = ?"); $stmtMax->execute([$laneId]); $maxPos = (int)$stmtMax->fetchColumn(); $stmt = $db->prepare(" INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id) VALUES (?, ?, ?, '', NULL, 'P2', 'yellow', ?, ?) "); $stmt->execute([$boardId, $laneId, $title, $maxPos + 1, $userId]); $newCardId = (int)$db->lastInsertId(); // Optioneel datums toekennen if ($startDate) { $stmtSched = $db->prepare(" INSERT INTO board_card_schedules (card_id, schedule_type, start_date, end_date, start_time, end_time, days_of_week) VALUES (?, 'DAILY_RANGE', ?, ?, '09:00', '17:00', '1,2,3,4,5') "); $stmtSched->execute([$newCardId, $startDate, $endDate]); } echo json_encode([ 'success' => true, 'card_id' => $newCardId, 'lane_id' => $laneId, 'message' => 'Taak direct toegevoegd!' ]); exit; } echo json_encode(['success' => false, 'error' => 'Onbekende actie.']); } catch (Throwable $e) { error_log("Fociz Board API Fout: " . $e->getMessage()); http_response_code(500); echo json_encode(['success' => false, 'error' => 'Serverfout: ' . $e->getMessage()]); }