'Sessie ID ontbreekt']); exit; } $stmtSession = $db->prepare("SELECT * FROM brainstorm_sessions WHERE id = ?"); $stmtSession->execute([$sessionId]); $session = $stmtSession->fetch(); if (!$session) { echo json_encode(['error' => 'Sessie niet gevonden']); exit; } $stmtCards = $db->prepare("SELECT id, content, category FROM brainstorm_cards WHERE session_id = ?"); $stmtCards->execute([$sessionId]); $cards = $stmtCards->fetchAll(); if (empty($cards)) { echo json_encode(['error' => 'Er zijn nog geen kaartjes op het canvas om te categoriseren']); exit; } $aiRes = fociz_ai_categorize_cards($session['title'], $session['theme'] ?? 'Algemeen', $cards, $user['id']); if (!$aiRes['success']) { echo json_encode(['error' => $aiRes['error']]); exit; } $validCategories = ['Ideeën', 'Knelpunten', 'Oplossingen', 'Quick Wins']; $updateStmt = $db->prepare("UPDATE brainstorm_cards SET category = ? WHERE id = ? AND session_id = ?"); $updatedCount = 0; foreach ($aiRes['categorized'] as $item) { $cardId = (int)($item['id'] ?? 0); $newCat = trim($item['category'] ?? ''); if ($cardId > 0 && in_array($newCat, $validCategories)) { $updateStmt->execute([$newCat, $cardId, $sessionId]); $updatedCount++; } } echo json_encode(['success' => true, 'updated' => $updatedCount]); exit; } // 2. Automatisch ideeën genereren voor een sessie if ($action === 'generate_ideas') { $data = json_decode(file_get_contents('php://input'), true); $sessionId = (int)($data['session_id'] ?? 0); $count = min(6, max(2, (int)($data['count'] ?? 4))); if (!$sessionId) { echo json_encode(['error' => 'Sessie ID ontbreekt']); exit; } $stmtSession = $db->prepare("SELECT * FROM brainstorm_sessions WHERE id = ?"); $stmtSession->execute([$sessionId]); $session = $stmtSession->fetch(); if (!$session) { echo json_encode(['error' => 'Sessie niet gevonden']); exit; } $aiRes = fociz_ai_generate_ideas($session['title'], $session['theme'] ?? 'Algemeen', $count, $user['id']); if (!$aiRes['success']) { echo json_encode(['error' => $aiRes['error']]); exit; } $insertStmt = $db->prepare(" INSERT INTO brainstorm_cards (session_id, content, category, color, author_name, votes) VALUES (?, ?, 'Ideeën', 'amber', ?, 1) "); $createdCards = []; $authorName = 'AI Co-Pilot'; foreach ($aiRes['ideas'] as $ideaText) { $ideaText = trim($ideaText); if (!empty($ideaText)) { $insertStmt->execute([$sessionId, $ideaText, $authorName]); $createdCards[] = [ 'id' => (int)$db->lastInsertId(), 'content' => $ideaText, 'category' => 'Ideeën', 'color' => 'amber', 'author_name' => $authorName, 'votes' => 1, ]; } } echo json_encode(['success' => true, 'cards' => $createdCards]); exit; } // 3. Brainstorm notitie omzetten naar Roadmap Item if ($action === 'summarize_to_roadmap') { $data = json_decode(file_get_contents('php://input'), true); $content = trim($data['content'] ?? ''); $sessionTitle = trim($data['session_title'] ?? ''); if (empty($content)) { echo json_encode(['error' => 'Geen inhoud opgegeven om samen te vatten']); exit; } $aiRes = fociz_ai_summarize_to_roadmap($content, $sessionTitle, $user['id']); if (!$aiRes['success']) { echo json_encode(['error' => $aiRes['error']]); exit; } echo json_encode(['success' => true, 'item' => $aiRes['item']]); exit; } // 3b. AI Brainstorm Idee Uitwerken tot concrete behapbare items if ($action === 'breakdown_idea') { $data = json_decode(file_get_contents('php://input'), true) ?: $_POST; $content = trim($data['content'] ?? ''); $sessionId = (int)($data['session_id'] ?? 0); $sessionTitle = trim($data['session_title'] ?? ''); $theme = trim($data['theme'] ?? ''); if (empty($content)) { echo json_encode(['error' => 'Geen inhoud opgegeven om uit te werken.']); exit; } if ($sessionId > 0 && (empty($sessionTitle) || empty($theme))) { $stmtS = $db->prepare("SELECT title, theme FROM brainstorm_sessions WHERE id = ?"); $stmtS->execute([$sessionId]); $sRow = $stmtS->fetch(); if ($sRow) { if (empty($sessionTitle)) $sessionTitle = $sRow['title'] ?? ''; if (empty($theme)) $theme = $sRow['theme'] ?? ''; } } $aiRes = fociz_ai_breakdown_idea($content, $sessionTitle, $theme, $user['id']); if (!$aiRes['success']) { echo json_encode(['error' => $aiRes['error']]); exit; } echo json_encode([ 'success' => true, 'summary' => $aiRes['summary'], 'items' => $aiRes['items'], 'model' => $aiRes['model'] ?? 'Gemini Pro', ]); exit; } // 3c. Opgesplitste AI items exporteren naar Kanban Bord, Tijdlijn of Dagplanner if ($action === 'save_breakdown_items') { $data = json_decode(file_get_contents('php://input'), true) ?: $_POST; $destination = trim($data['destination'] ?? 'kanban'); // 'kanban', 'timeline', 'planner' $items = $data['items'] ?? []; if (empty($items) || !is_array($items)) { echo json_encode(['error' => 'Geen items geselecteerd om op te slaan.']); exit; } $isAdmin = ($user['role'] === 'ADMIN'); $createdCount = 0; if ($destination === 'kanban' || $destination === 'timeline') { $boardId = (int)($data['board_id'] ?? 0); $laneId = (int)($data['lane_id'] ?? 0); if ($boardId <= 0) { // Pak het eerste actieve bord van de gebruiker if ($isAdmin) { $boardId = (int)$db->query("SELECT id FROM boards WHERE is_archived = 0 ORDER BY id ASC LIMIT 1")->fetchColumn(); } else { $stmtB = $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"); $stmtB->execute([$user['id']]); $boardId = (int)$stmtB->fetchColumn(); } } if ($boardId <= 0) { echo json_encode(['error' => 'Geen actief Kanban-bord gevonden om taken aan toe te voegen.']); exit; } // Toegangscontrole if (!$isAdmin) { $stmtCheck = $db->prepare("SELECT COUNT(*) FROM board_members WHERE board_id = ? AND user_id = ?"); $stmtCheck->execute([$boardId, $user['id']]); if ((int)$stmtCheck->fetchColumn() === 0) { http_response_code(403); echo json_encode(['error' => 'Geen toegang tot dit bord.']); exit; } } // Als lane_id niet geldig is, pak de eerste kolom (bijv. 'To Do' of 'Backlog') 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(); } if ($laneId <= 0) { echo json_encode(['error' => 'Het geselecteerde bord heeft geen kolommen.']); exit; } // Datumbereik voor Tijdlijn $startDate = !empty($data['start_date']) ? trim($data['start_date']) : date('Y-m-d'); $endDate = !empty($data['end_date']) ? trim($data['end_date']) : date('Y-m-d', strtotime('+3 days')); $stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = ?"); $stmtInsertCard = $db->prepare(" INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "); $stmtSchedule = $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') "); $priorityMap = ['P1' => 'HIGH', 'P2' => 'MEDIUM', 'P3' => 'LOW']; foreach ($items as $item) { $title = trim($item['title'] ?? ''); if (empty($title)) continue; $desc = trim($item['description'] ?? ''); $rawPri = strtoupper(trim($item['priority'] ?? 'P2')); $priority = $priorityMap[$rawPri] ?? ($rawPri === 'HIGH' || $rawPri === 'LOW' ? $rawPri : 'MEDIUM'); $color = trim($item['color'] ?? 'indigo'); $stmtMax->execute([$laneId]); $maxPos = (int)$stmtMax->fetchColumn(); $stmtInsertCard->execute([ $boardId, $laneId, $title, $desc, $user['id'], $priority, $color, $maxPos + 1, $user['id'], ]); $cardId = (int)$db->lastInsertId(); if ($destination === 'timeline' && $cardId > 0) { $stmtSchedule->execute([$cardId, $startDate, $endDate]); } $createdCount++; } echo json_encode([ 'success' => true, 'destination' => $destination, 'created_count' => $createdCount, 'board_id' => $boardId, ]); exit; } if ($destination === 'planner') { $planDate = !empty($data['plan_date']) ? trim($data['plan_date']) : date('Y-m-d'); // Zoek of maak daily plan $stmtPlan = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?"); $stmtPlan->execute([$user['id'], $planDate]); $planId = (int)$stmtPlan->fetchColumn(); if ($planId <= 0) { $stmtNewPlan = $db->prepare("INSERT INTO daily_plans (user_id, plan_date, focus_goal) VALUES (?, ?, 'Brainstorm actiepunten oppakken')"); $stmtNewPlan->execute([$user['id'], $planDate]); $planId = (int)$db->lastInsertId(); } $stmtInsertTask = $db->prepare(" INSERT INTO tasks (user_id, daily_plan_id, title, priority, duration_minutes, status, source) VALUES (?, ?, ?, ?, ?, 'TODO', 'BRAINSTORM') "); foreach ($items as $item) { $title = trim($item['title'] ?? ''); if (empty($title)) continue; $rawPri = strtoupper(trim($item['priority'] ?? 'P2')); $priority = in_array($rawPri, ['P1', 'P2', 'P3']) ? $rawPri : 'P2'; $duration = max(15, min(240, (int)($item['estimated_minutes'] ?? 30))); $stmtInsertTask->execute([ $user['id'], $planId, $title, $priority, $duration, ]); $createdCount++; } echo json_encode([ 'success' => true, 'destination' => 'planner', 'created_count' => $createdCount, 'plan_date' => $planDate, ]); exit; } echo json_encode(['error' => 'Onbekende bestemming: ' . htmlspecialchars($destination)]); exit; } // 3d. Test AI Verbinding (Gemini Pro / OpenAI) if ($action === 'test_connection') { $data = json_decode(file_get_contents('php://input'), true) ?: $_POST; $tempProvider = !empty($data['provider']) ? trim($data['provider']) : null; $tempKey = !empty($data['api_key']) ? trim($data['api_key']) : null; $tempModel = !empty($data['model']) ? trim($data['model']) : null; // Als er test credentials zijn meegegeven, test direct met die credentials if (!empty($tempKey)) { $testSettings = [ 'ai_provider' => $tempProvider ?: 'gemini', 'ai_api_key' => $tempKey, 'ai_model' => $tempModel ?: ($tempProvider === 'openai' ? 'gpt-4o-mini' : 'gemini-1.5-pro'), ]; } else { // Anders haal de geconfigureerde instellingen op van user of org $testSettings = get_ai_settings($user['id']); } if (empty($testSettings['ai_api_key'])) { echo json_encode([ 'success' => false, 'error' => 'Geen API-sleutel ingevuld of geconfigureerd om te testen.', ]); exit; } $provider = strtolower($testSettings['ai_provider'] ?? 'gemini'); $model = $testSettings['ai_model'] ?? ($provider === 'openai' ? 'gpt-4o-mini' : 'gemini-1.5-pro'); $t0 = microtime(true); $testMessages = [ ['role' => 'user', 'content' => 'Fociz connectiviteitstest. Reageer enkel met het woord OK.'], ]; if ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) { $aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings); } else { $aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings); } $latency = round((microtime(true) - $t0) * 1000); if (!$aiRes['success']) { echo json_encode([ 'success' => false, 'error' => $aiRes['error'] ?? 'Verbindingstest mislukt.', 'latency_ms' => $latency, 'provider' => $provider, 'model' => $model, ]); exit; } $providerName = ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) ? 'Google Gemini Pro' : 'OpenAI'; echo json_encode([ 'success' => true, 'message' => "Verbinding met {$providerName} is succesvol tot stand gebracht! (Response tijd: {$latency}ms)", 'latency_ms' => $latency, 'provider' => $provider, 'model' => $aiRes['model'] ?? $model, 'zero_leak' => true, ]); exit; } // 4. Slimme dagindeling berekenen voor taken rondom bezette tijdblokken // ZERO-DATA LEAKAGE: Geen meetingsdetails of externe tickets worden verzonden! if ($action === 'smart_schedule') { $data = json_decode(file_get_contents('php://input'), true); $date = !empty($data['date']) ? trim($data['date']) : date('Y-m-d'); // Haal daily plan op $stmtPlan = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?"); $stmtPlan->execute([$user['id'], $date]); $plan = $stmtPlan->fetch(); $planId = $plan['id'] ?? 0; // ZERO-DATA LEAKAGE: Sluit expliciet alle taken uit die afkomstig zijn van Jira, Mail of Teams! $isToday = ($date === date('Y-m-d')); $stmtTasks = $db->prepare(" SELECT t.id, t.title, t.priority, t.duration_minutes FROM tasks t WHERE t.user_id = ? AND (t.daily_plan_id = ? " . ($isToday ? "OR t.daily_plan_id IS NULL" : "") . ") AND t.is_completed = 0 AND (t.source IS NULL OR t.source NOT IN ('JIRA', 'MAIL', 'EMAIL', 'OUTLOOK', 'TEAMS')) AND t.external_id IS NULL ORDER BY t.priority ASC, t.created_at ASC "); $stmtTasks->execute([$user['id'], $planId]); $tasks = $stmtTasks->fetchAll(); if (empty($tasks)) { echo json_encode(['error' => 'Geen openstaande Fociz taken gevonden om in te delen']); exit; } // ZERO-DATA LEAKAGE: Haal meetings op maar geef UITSLUITEND start/eindtijd door (geen titel/deelnemers) $busyBlocks = []; try { require_once __DIR__ . '/../includes/integrations/microsoft.php'; $rawEvents = microsoft_fetch_today_events($user['id'], $date); foreach ($rawEvents as $ev) { if (!empty($ev['start']) && !empty($ev['end'])) { $busyBlocks[] = [ 'start' => substr($ev['start'], 0, 5), 'end' => substr($ev['end'], 0, 5), ]; } } } catch (Throwable $e) { // Microsoft niet gekoppeld } $workdaySettings = get_workday_settings(); $aiRes = fociz_ai_smart_schedule($tasks, $busyBlocks, $workdaySettings, $user['id']); if (!$aiRes['success']) { echo json_encode(['error' => $aiRes['error']]); exit; } // Werk de tijdsblokken bij in MariaDB $updateStmt = $db->prepare(" UPDATE tasks SET time_block_start = ?, duration_minutes = COALESCE(?, duration_minutes) WHERE id = ? AND user_id = ? "); $scheduledCount = 0; foreach ($aiRes['scheduled'] as $s) { $taskId = (int)($s['id'] ?? 0); $startTime = trim($s['time_block_start'] ?? ''); $duration = !empty($s['duration_minutes']) ? (int)$s['duration_minutes'] : null; if ($taskId > 0 && preg_match('/^\d{1,2}:\d{2}$/', $startTime)) { $updateStmt->execute([$startTime, $duration, $taskId, $user['id']]); $scheduledCount++; } } echo json_encode(['success' => true, 'scheduled_count' => $scheduledCount]); exit; } // 5. Persoonlijke AI Dagbriefing genereren // ZERO-DATA LEAKAGE: Absoluut GEEN Jira, Mail of Teams data! if ($action === 'daily_briefing') { $data = json_decode(file_get_contents('php://input'), true); $date = !empty($data['date']) ? trim($data['date']) : date('Y-m-d'); $today = date('Y-m-d'); // Haal openstaande taken van vandaag op (ZERO-DATA LEAKAGE: GEEN externe Jira/Mail taken) $stmtPlan = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?"); $stmtPlan->execute([$user['id'], $date]); $planId = $stmtPlan->fetchColumn(); $isToday = ($date === $today); $taskFilter = $planId ? "t.daily_plan_id = " . (int)$planId . ($isToday ? " OR t.daily_plan_id IS NULL" : "") : ($isToday ? "t.daily_plan_id IS NULL" : "1=0"); $stmtTasks = $db->prepare(" SELECT id, title, priority, time_block_start, duration_minutes FROM tasks t WHERE t.user_id = ? AND t.is_completed = 0 AND ({$taskFilter}) AND (t.source IS NULL OR t.source NOT IN ('JIRA', 'MAIL', 'EMAIL', 'OUTLOOK', 'TEAMS')) AND t.external_id IS NULL ORDER BY t.priority ASC, t.time_block_start ASC LIMIT 15 "); $stmtTasks->execute([$user['id']]); $tasks = $stmtTasks->fetchAll(); // Haal gisteren niet-voltooide interne taken op $yesterday = date('Y-m-d', strtotime('-1 day', strtotime($date))); $stmtYest = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?"); $stmtYest->execute([$user['id'], $yesterday]); $yesterdayPlanId = $stmtYest->fetchColumn(); $unfinishedYest = []; if ($yesterdayPlanId) { $stmtUnf = $db->prepare(" SELECT title FROM tasks WHERE user_id = ? AND daily_plan_id = ? AND is_completed = 0 AND (source IS NULL OR source NOT IN ('JIRA', 'MAIL', 'EMAIL', 'OUTLOOK', 'TEAMS')) AND external_id IS NULL LIMIT 5 "); $stmtUnf->execute([$user['id'], $yesterdayPlanId]); $unfinishedYest = $stmtUnf->fetchAll(); } // ZERO-DATA LEAKAGE: Alleen geanonimiseerde bezette tijdblokken (geen subject/afzender) $busyBlocks = []; try { require_once __DIR__ . '/../includes/integrations/microsoft.php'; $rawEvents = microsoft_fetch_today_events($user['id'], $date); foreach ($rawEvents as $ev) { if (!empty($ev['start']) && !empty($ev['end'])) { $busyBlocks[] = [ 'start' => substr($ev['start'], 0, 5), 'end' => substr($ev['end'], 0, 5), ]; } } } catch (Throwable $e) { // Geen agenda beschikbaar } $aiSettings = get_ai_settings($user['id']); if (empty($aiSettings['ai_enabled']) || empty($aiSettings['has_key'])) { echo json_encode(['error' => 'AI Co-Pilot is niet geconfigureerd']); exit; } $aiRes = fociz_ai_daily_briefing( $user['name'] ?? 'Jij', $date, $tasks, $busyBlocks, $unfinishedYest, $user['id'] ); if (!$aiRes['success']) { echo json_encode(['error' => $aiRes['error']]); exit; } echo json_encode([ 'success' => true, 'greeting' => $aiRes['greeting'], 'focus_task' => $aiRes['focus_task'], 'tip' => $aiRes['tip'], 'warnings' => $aiRes['warnings'], 'date' => $date, 'model' => $aiRes['model'] ?? 'Gemini Pro', ]); exit; } echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); }