From 8714038db2bedd8e8cd10f5f25741b42d552f688 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Sat, 12 Sep 2026 13:24:00 +0200 Subject: [PATCH] Aanpassingen planner en ai --- api/ai.php | 4 +- api/mail.php | 22 +- api/planner.php | 51 ++- includes/ai.php | 67 ++- planner.php | 1171 ++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 1173 insertions(+), 142 deletions(-) diff --git a/api/ai.php b/api/ai.php index 264dd83..27b3a8e 100644 --- a/api/ai.php +++ b/api/ai.php @@ -395,9 +395,9 @@ try { ]; if ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) { - $aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings); + $aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 500, 'temperature' => 0.1], $testSettings); } else { - $aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings); + $aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 500, 'temperature' => 0.1], $testSettings); } $latency = round((microtime(true) - $t0) * 1000); diff --git a/api/mail.php b/api/mail.php index 4ae95cd..ada9fc6 100644 --- a/api/mail.php +++ b/api/mail.php @@ -121,14 +121,14 @@ try { // 6. E-mail direct inplannen in Fociz dagplanning if ($action === 'plan_email') { $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; - $messageId = trim($input['message_id'] ?? ''); - $subject = trim($input['subject'] ?? 'E-mail actiepunt'); - $planDate = trim($input['plan_date'] ?? date('Y-m-d')); + $messageId = trim($input['message_id'] ?? $input['mail_id'] ?? ''); + $subject = trim($input['title'] ?? $input['subject'] ?? 'E-mail actiepunt'); + $planDate = trim($input['plan_date'] ?? $input['date'] ?? date('Y-m-d')); $timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null; $durationMinutes = (int)($input['duration_minutes'] ?? 30); $priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2'; $webLink = trim($input['web_link'] ?? 'https://outlook.office.com/mail/'); - $sender = trim($input['sender'] ?? ''); + $sender = trim($input['sender'] ?? $input['sender_name'] ?? ''); if (empty($subject)) { echo json_encode(['success' => false, 'error' => 'Onderwerp is verplicht.']); @@ -137,8 +137,8 @@ try { if ($durationMinutes <= 0) $durationMinutes = 30; - $timeBlockEnd = null; - if (!empty($timeBlockStart)) { + $timeBlockEnd = !empty($input['time_block_end']) ? trim($input['time_block_end']) : null; + if (!empty($timeBlockStart) && empty($timeBlockEnd)) { try { $dtEnd = new DateTime("{$planDate} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam')); $dtEnd->modify("+{$durationMinutes} minutes"); @@ -155,9 +155,13 @@ try { $stmtPlan->execute([$userId, $planDate]); $planId = (int)$db->lastInsertId(); - $taskTitle = "✉️ " . $subject; - if (!empty($sender)) { - $taskTitle .= " (van " . $sender . ")"; + // Als er een custom titel is ingevoerd, gebruik die direct, anders maak een nette titel + $taskTitle = $subject; + if (isset($input['subject']) && !isset($input['title'])) { + $taskTitle = "✉️ " . $subject; + if (!empty($sender) && strpos($taskTitle, $sender) === false) { + $taskTitle .= " (van " . $sender . ")"; + } } $stmtTask = $db->prepare(" diff --git a/api/planner.php b/api/planner.php index 05c1afa..c867601 100644 --- a/api/planner.php +++ b/api/planner.php @@ -271,20 +271,22 @@ try { } } + $description = isset($data['description']) ? trim((string)$data['description']) : ($existingTask['description'] ?? null); + if ($newPlanId) { $stmtUpdate = $db->prepare(" UPDATE tasks - SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?, daily_plan_id = ? + SET title = ?, description = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?, daily_plan_id = ? WHERE id = ? AND user_id = ? "); - $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $newPlanId, $taskId, $user['id']]); + $stmtUpdate->execute([$title, $description, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $newPlanId, $taskId, $user['id']]); } else { $stmtUpdate = $db->prepare(" UPDATE tasks - SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? + SET title = ?, description = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? WHERE id = ? AND user_id = ? "); - $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]); + $stmtUpdate->execute([$title, $description, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]); } echo json_encode([ @@ -645,6 +647,47 @@ try { exit; } + // 10b. Volledige taakdetails ophalen inclusief subtaken en dagplan informatie + if ($action === 'get_task_details') { + $taskId = (int)($_GET['task_id'] ?? ($_POST['task_id'] ?? 0)); + + if (!$taskId) { + echo json_encode(['error' => 'Taak-ID ontbreekt']); + exit; + } + + $stmt = $db->prepare(" + SELECT t.*, dp.plan_date, dp.focus_goal + FROM tasks t + LEFT JOIN daily_plans dp ON t.daily_plan_id = dp.id + WHERE t.id = ? AND t.user_id = ? + "); + $stmt->execute([$taskId, $user['id']]); + $task = $stmt->fetch(); + + if (!$task) { + echo json_encode(['error' => 'Taak niet gevonden']); + exit; + } + + // Subtaken ophalen + $stmtSub = $db->prepare(" + SELECT id, task_id, title, is_completed, position, created_at + FROM task_subtasks + WHERE task_id = ? AND user_id = ? + ORDER BY position ASC, created_at ASC + "); + $stmtSub->execute([$taskId, $user['id']]); + $subtasks = $stmtSub->fetchAll(); + + echo json_encode([ + 'success' => true, + 'task' => $task, + 'subtasks' => $subtasks, + ]); + exit; + } + // 11. Zoek eerstvolgende vrije open blok binnen werktijden (Release v2.1) if ($action === 'find_next_free_slot') { $rawInput = json_decode(file_get_contents('php://input'), true); diff --git a/includes/ai.php b/includes/ai.php index 315dc8c..240812a 100644 --- a/includes/ai.php +++ b/includes/ai.php @@ -83,9 +83,9 @@ function fociz_ai_enforce_privacy_firewall(array &$messages): array { */ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings): array { $apiKey = trim((string)($aiSettings['ai_api_key'] ?? '')); - $model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-1.5-pro'; + $model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-3.6-flash'; $temperature = $options['temperature'] ?? 0.3; - $maxTokens = $options['max_tokens'] ?? 2048; + $maxTokens = max(512, (int)($options['max_tokens'] ?? 2048)); $jsonMode = !empty($options['json_mode']); if (empty($apiKey)) { @@ -140,34 +140,56 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings $cleanModel = preg_replace('#^models/#', '', $model); + // Helper om tekst uit Gemini candidate te extraheren + $extractCandidateText = function(?array $candidate): string { + if (!$candidate || empty($candidate['content']['parts'])) { + return ''; + } + $txt = ''; + foreach ($candidate['content']['parts'] as $part) { + if (!empty($part['text']) && empty($part['thought'])) { + $txt .= $part['text']; + } + } + if ($txt === '' && !empty($candidate['content']['parts'][0]['text'])) { + $txt = $candidate['content']['parts'][0]['text']; + } + return trim($txt); + }; + // Bepaal fallback volgorde van endpoints/versies voor maximale compatibiliteit met alle Google AI Studio accounts $candidateUrls = []; if (str_contains($cleanModel, '3.6-flash')) { $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-3.6-flash:generateContent"; - } elseif (str_contains($cleanModel, '2.0-flash')) { - $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; - $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"; - } elseif (str_contains($cleanModel, '1.5-flash')) { + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"; + } elseif (str_contains($cleanModel, '2.0-flash')) { + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"; + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"; + } elseif (str_contains($cleanModel, '1.5-flash')) { + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"; + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent"; - $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-001:generateContent"; - $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent"; } elseif (str_contains($cleanModel, '1.5-pro')) { + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-pro:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; } else { $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/{$cleanModel}:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; + $candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"; $candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/{$cleanModel}:generateContent"; } $response = null; $lastError = null; $successfulModel = $cleanModel; + $finalText = ''; for ($i = 0; $i < count($candidateUrls); $i++) { $baseUrl = $candidateUrls[$i]; @@ -182,12 +204,17 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings 'timeout' => 20, ]); - if ($res['status'] === 200 && !empty($res['json']['candidates'][0]['content']['parts'][0]['text'])) { - $response = $res; - if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) { - $successfulModel = $mName[1]; + if ($res['status'] === 200 && !empty($res['json']['candidates'][0])) { + $extracted = $extractCandidateText($res['json']['candidates'][0]); + // Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS) + if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) { + $response = $res; + $finalText = ($extracted !== '') ? $extracted : 'OK'; + if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) { + $successfulModel = $mName[1]; + } + break; } - break; } $msg = $res['json']['error']['message'] ?? ($res['error'] ?? ''); @@ -260,9 +287,13 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings 'timeout' => 20, ]); - if ($dynRes['status'] === 200 && !empty($dynRes['json']['candidates'][0]['content']['parts'][0]['text'])) { - $response = $dynRes; - $successfulModel = $fallbackModel; + if ($dynRes['status'] === 200 && !empty($dynRes['json']['candidates'][0])) { + $extracted = $extractCandidateText($dynRes['json']['candidates'][0]); + if ($extracted !== '' || !empty($dynRes['json']['candidates'][0]['finishReason'])) { + $response = $dynRes; + $finalText = ($extracted !== '') ? $extracted : 'OK'; + $successfulModel = $fallbackModel; + } } else { $response = $dynRes; } @@ -270,7 +301,7 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings } } - if (!$response || $response['status'] !== 200 || empty($response['json']['candidates'][0]['content']['parts'][0]['text'])) { + if (!$response || $response['status'] !== 200 || $finalText === '') { $errMsg = null; if (!empty($response['json']['error']['message'])) { $errMsg = $response['json']['error']['message']; @@ -288,7 +319,7 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings ]; } - $rawContent = trim($response['json']['candidates'][0]['content']['parts'][0]['text']); + $rawContent = $finalText; // Indien Gemini het als markdown code block wrapped heeft (bv. ```json ... ```), schoon dit op if (preg_match('/^```(?:json)?\s*(.*?)\s*```$/s', $rawContent, $m)) { diff --git a/planner.php b/planner.php index 1a9cc2f..67940d2 100644 --- a/planner.php +++ b/planner.php @@ -850,12 +850,27 @@ for ($h = $startHour; $h <= $endHour; $h++) {

Klik op 'Nieuwe Taak' of importeer uit Jira / Outlook Mail hieronder.

- -
+ (int)$task['id'], + 'title' => $task['title'], + 'description' => $task['description'] ?? '', + 'priority' => $task['priority'], + 'start' => $task['time_block_start'] ?? '', + 'end' => $task['time_block_end'] ?? '', + 'duration' => (int)($task['duration_minutes'] ?: 30), + 'is_completed' => (bool)$task['is_completed'], + 'source' => $task['source'], + 'external_id' => $task['external_id'] ?? '', + 'external_url' => $task['external_url'] ?? '', + 'date' => $selectedDate + ]; + ?> +
-
-
-
-
+
- + + + - - -
@@ -1015,11 +1025,16 @@ for ($h = $startHour; $h <= $endHour; $h++) { onchange="if(this.value) { switchPlannerDate(this.value, event); }" class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer shadow-2xs" > + +
- @@ -1260,10 +1268,25 @@ for ($h = $startHour; $h <= $endHour; $h++) { - -
+ (int)$t['id'], + 'title' => $t['title'], + 'description' => $t['description'] ?? '', + 'priority' => $t['priority'], + 'start' => $t['time_block_start'] ?? '', + 'end' => $t['time_block_end'] ?? '', + 'duration' => (int)($t['duration_minutes'] ?: 30), + 'is_completed' => (bool)$t['is_completed'], + 'source' => $t['source'], + 'external_id' => $t['external_id'] ?? '', + 'external_url' => $t['external_url'] ?? '', + 'date' => $selectedDate + ]; + ?> +
-
+
@@ -1278,7 +1301,7 @@ for ($h = $startHour; $h <= $endHour; $h++) { - @@ -1286,19 +1309,15 @@ for ($h = $startHour; $h <= $endHour; $h++) { - + +
@@ -1376,7 +1395,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
+
+ + +
+
@@ -2264,75 +2288,155 @@ if ($isAjaxRequest) {