diff --git a/api/ai.php b/api/ai.php index 3bd5237..3cafde4 100644 --- a/api/ai.php +++ b/api/ai.php @@ -205,6 +205,99 @@ try { exit; } + // 5. Persoonlijke AI Dagbriefing genereren + 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 + $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"); + + $tasks = $db->query(" + SELECT id, title, priority, time_block_start, duration_minutes + FROM tasks + WHERE user_id = {$user['id']} AND is_completed = 0 AND ({$taskFilter}) + ORDER BY priority ASC, time_block_start ASC + LIMIT 15 + ")->fetchAll(); + + // Haal gisteren niet-voltooide 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 + LIMIT 5 + "); + $stmtUnf->execute([$user['id'], $yesterdayPlanId]); + $unfinishedYest = $stmtUnf->fetchAll(); + } + + // Haal Outlook meetings op als beschikbaar + $meetings = []; + try { + require_once __DIR__ . '/../includes/integrations/microsoft.php'; + $meetings = microsoft_fetch_today_events($user['id'], $date); + } catch (Throwable $e) { + // meetings niet beschikbaar, geen probleem + } + + // Haal urgente Jira tickets op als beschikbaar + $jiraIssues = []; + try { + require_once __DIR__ . '/../includes/integrations/jira.php'; + $jiraData = jira_get_dashboard_data($user['id'], false); + $jiraIssues = array_merge( + $jiraData['assigned_issues'] ?? [], + $jiraData['team_issues'] ?? [] + ); + } catch (Throwable $e) { + // Jira niet beschikbaar, geen probleem + } + + $aiSettings = get_ai_settings(); + 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, + $meetings, + $jiraIssues, + $unfinishedYest + ); + + 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, + ]); + exit; + } + echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); diff --git a/api/planner.php b/api/planner.php index 61f5bc1..30accc8 100644 --- a/api/planner.php +++ b/api/planner.php @@ -486,6 +486,120 @@ try { exit; } + // --- V2.0: Subtaken & Checklists --- + + // 7. Subtaak toevoegen aan een taak + if ($action === 'add_subtask') { + $data = json_decode(file_get_contents('php://input'), true); + $taskId = (int)($data['task_id'] ?? 0); + $title = trim($data['title'] ?? ''); + + if (!$taskId || empty($title)) { + echo json_encode(['error' => 'Taak-ID en titel zijn verplicht']); + exit; + } + + // Controleer of de taak van deze gebruiker is + $stmtCheck = $db->prepare("SELECT id FROM tasks WHERE id = ? AND user_id = ?"); + $stmtCheck->execute([$taskId, $user['id']]); + if (!$stmtCheck->fetchColumn()) { + echo json_encode(['error' => 'Taak niet gevonden']); + exit; + } + + // Bereken positie (achteraan) + $stmtPos = $db->prepare("SELECT COALESCE(MAX(position), -1) + 1 FROM task_subtasks WHERE task_id = ?"); + $stmtPos->execute([$taskId]); + $position = (int)$stmtPos->fetchColumn(); + + $stmt = $db->prepare(" + INSERT INTO task_subtasks (task_id, user_id, title, is_completed, position) + VALUES (?, ?, ?, 0, ?) + "); + $stmt->execute([$taskId, $user['id'], substr($title, 0, 255), $position]); + $subtaskId = (int)$db->lastInsertId(); + + echo json_encode([ + 'success' => true, + 'subtask' => [ + 'id' => $subtaskId, + 'task_id' => $taskId, + 'title' => $title, + 'is_completed' => 0, + 'position' => $position, + ] + ]); + exit; + } + + // 8. Subtaak afvinken / openzetten + if ($action === 'toggle_subtask') { + $data = json_decode(file_get_contents('php://input'), true); + $subtaskId = (int)($data['subtask_id'] ?? 0); + $isCompleted = !empty($data['is_completed']) ? 1 : 0; + + if (!$subtaskId) { + echo json_encode(['error' => 'Subtaak-ID ontbreekt']); + exit; + } + + $stmt = $db->prepare(" + UPDATE task_subtasks SET is_completed = ? + WHERE id = ? AND user_id = ? + "); + $stmt->execute([$isCompleted, $subtaskId, $user['id']]); + + echo json_encode(['success' => true, 'is_completed' => $isCompleted]); + exit; + } + + // 9. Subtaak verwijderen + if ($action === 'delete_subtask') { + $data = json_decode(file_get_contents('php://input'), true); + $subtaskId = (int)($data['subtask_id'] ?? 0); + + if (!$subtaskId) { + echo json_encode(['error' => 'Subtaak-ID ontbreekt']); + exit; + } + + $stmt = $db->prepare("DELETE FROM task_subtasks WHERE id = ? AND user_id = ?"); + $stmt->execute([$subtaskId, $user['id']]); + + echo json_encode(['success' => true]); + exit; + } + + // 10. Subtaken ophalen voor een taak + if ($action === 'get_subtasks') { + $taskId = (int)($_GET['task_id'] ?? 0); + + if (!$taskId) { + echo json_encode(['error' => 'Taak-ID ontbreekt']); + exit; + } + + // Controleer eigenaarschap + $stmtCheck = $db->prepare("SELECT id FROM tasks WHERE id = ? AND user_id = ?"); + $stmtCheck->execute([$taskId, $user['id']]); + if (!$stmtCheck->fetchColumn()) { + echo json_encode(['error' => 'Taak niet gevonden']); + exit; + } + + $stmt = $db->prepare(" + SELECT id, task_id, title, is_completed, position + FROM task_subtasks + WHERE task_id = ? AND user_id = ? + ORDER BY position ASC, created_at ASC + "); + $stmt->execute([$taskId, $user['id']]); + $subtasks = $stmt->fetchAll(); + + echo json_encode(['success' => true, 'subtasks' => $subtasks]); + exit; + } + echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); diff --git a/config/database.php b/config/database.php index 3c72702..7b62a46 100644 --- a/config/database.php +++ b/config/database.php @@ -154,6 +154,8 @@ function ensure_tables_exist(PDO $pdo): void { "ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)", "CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", "CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + // V2.0: Subtaken & Checklists + "CREATE TABLE IF NOT EXISTS `task_subtasks` (`id` INT AUTO_INCREMENT PRIMARY KEY, `task_id` INT NOT NULL, `user_id` INT NOT NULL, `title` VARCHAR(255) NOT NULL, `is_completed` TINYINT(1) NOT NULL DEFAULT 0, `position` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (`task_id`, `position`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", ]; foreach ($colUpgrades as $sql) { try { diff --git a/config/schema.sql b/config/schema.sql index dd49ea9..d16c1e3 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -242,3 +242,18 @@ CREATE TABLE IF NOT EXISTS `app_cache` ( `expires_at` INT UNSIGNED NOT NULL, INDEX `idx_app_cache_expires` (`expires_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Fociz V2.0: Subtaken & Checklists per taak +CREATE TABLE IF NOT EXISTS `task_subtasks` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `task_id` INT NOT NULL, + `user_id` INT NOT NULL, + `title` VARCHAR(255) NOT NULL, + `is_completed` TINYINT(1) NOT NULL DEFAULT 0, + `position` INT NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX (`task_id`, `position`), + CONSTRAINT `fk_subtasks_task` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_subtasks_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/ai.php b/includes/ai.php index 46c0cb1..b26d148 100644 --- a/includes/ai.php +++ b/includes/ai.php @@ -195,3 +195,86 @@ function fociz_ai_smart_schedule(array $tasks, array $events, array $workdaySett return ['success' => true, 'scheduled' => $aiRes['json']['scheduled']]; } +/** + * Genereert een persoonlijke AI dagbriefing op basis van taken, afspraken en Jira tickets + * + * @param string $userName Naam van de gebruiker + * @param string $date Datum (Y-m-d) + * @param array $tasks Openstaande taken van vandaag + * @param array $meetings Outlook meetings van vandaag + * @param array $jiraIssues Jira tickets (P1/blokkades) + * @param array $unfinishedYest Gisteren niet voltooide taken + * @return array ['success' => bool, 'briefing' => string, 'focus_tip' => string, 'warnings' => array] + */ +function fociz_ai_daily_briefing( + string $userName, + string $date, + array $tasks, + array $meetings, + array $jiraIssues = [], + array $unfinishedYest = [] +): array { + $dutchDays = ['', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag']; + $dayName = $dutchDays[(int)date('N', strtotime($date))] ?? 'vandaag'; + $dateLabel = $dayName . ' ' . date('j', strtotime($date)) . ' ' . date('F', strtotime($date)); + + $taskSummary = array_map(fn($t) => [ + 'title' => $t['title'], + 'priority' => $t['priority'] ?? 'P2', + 'time' => $t['time_block_start'] ?? null, + 'duration' => (int)($t['duration_minutes'] ?? 30), + ], array_slice($tasks, 0, 10)); + + $meetingSummary = array_map(fn($e) => [ + 'subject' => $e['subject'] ?? 'Afspraak', + 'start' => $e['start'] ?? null, + 'end' => $e['end'] ?? null, + ], array_slice($meetings, 0, 8)); + + $jiraSummary = array_map(fn($j) => [ + 'key' => $j['key'] ?? '', + 'title' => $j['summary'] ?? '', + 'priority' => $j['priority'] ?? 'Medium', + 'status' => $j['status'] ?? 'Open', + ], array_slice(array_filter($jiraIssues, fn($j) => in_array($j['priority'] ?? '', ['Highest', 'High'])), 0, 5)); + + $yesterdaySummary = array_map(fn($t) => $t['title'], array_slice($unfinishedYest, 0, 5)); + + $systemPrompt = "Je bent de persoonlijke AI Co-Pilot van {$userName} in Fociz. " + . "Schrijf een korte, energieke en persoonlijke dagbriefing in het Nederlands voor {$dateLabel}. " + . "Wees direct, concreet en vriendelijk. Gebruik geen markdown headers, geen bullet symbolen zoals *, -, •. " + . "Geef 1-2 zinnen dagopening + 1 zin over de grootste prioriteit + 1 praktisch focustip. " + . "Maximaal 60 woorden totaal. " + . "Retourneer een JSON-object met:\n" + . "- 'greeting': openingszin (max 20 woorden)\n" + . "- 'focus_task': de meest urgente taak of meeting vandaag (max 15 woorden)\n" + . "- 'tip': één concrete focustip voor vandaag (max 20 woorden)\n" + . "- 'warnings': array van strings, max 2 items — alleen als er iets opvalt (gisteren niet afgerond, urgent Jira, overvolle dag)"; + + $userPrompt = json_encode([ + 'date' => $dateLabel, + 'tasks_today' => $taskSummary, + 'meetings_today' => $meetingSummary, + 'urgent_jira' => $jiraSummary, + 'unfinished_yesterday' => $yesterdaySummary, + 'total_tasks' => count($tasks), + 'total_meetings' => count($meetings), + ], JSON_UNESCAPED_UNICODE); + + $aiRes = fociz_ai_chat([ + ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $userPrompt], + ], ['json_mode' => true, 'temperature' => 0.6, 'max_tokens' => 300]); + + if (!$aiRes['success'] || empty($aiRes['json'])) { + return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen dagbriefing genereren']; + } + + return [ + 'success' => true, + 'greeting' => $aiRes['json']['greeting'] ?? "Goedemorgen {$userName}, een nieuwe dag!", + 'focus_task'=> $aiRes['json']['focus_task'] ?? '', + 'tip' => $aiRes['json']['tip'] ?? '', + 'warnings' => $aiRes['json']['warnings'] ?? [], + ]; +} diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index 4b93189..1739bce 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -236,6 +236,9 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri 'description', 'labels', 'components', + 'story_points', + 'customfield_10016', // Story Points (Jira Cloud standaard) + 'customfield_10028', // Story Points (alternatief veld sommige instanties) ]; // 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST @@ -389,6 +392,16 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri elseif (str_contains($sumLower, 'koppeling')) $onderwerpLogging = 'Koppelingen'; } + // Detectie van story points (customfield_10016 is Jira Cloud standaard, met fallback naar alternatieven) + $storyPoints = null; + if (isset($fields['customfield_10016']) && is_numeric($fields['customfield_10016'])) { + $storyPoints = (float)$fields['customfield_10016']; + } elseif (isset($fields['customfield_10028']) && is_numeric($fields['customfield_10028'])) { + $storyPoints = (float)$fields['customfield_10028']; + } elseif (isset($fields['story_points']) && is_numeric($fields['story_points'])) { + $storyPoints = (float)$fields['story_points']; + } + $issues[] = [ 'key' => $key, 'summary' => $summary, @@ -409,6 +422,7 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri 'created' => $created, 'updated' => $updated, 'url' => "{$cleanDomain}/browse/{$key}", + 'story_points' => $storyPoints, 'source' => 'JIRA_LIVE', ]; } diff --git a/jira.php b/jira.php index 9eb8bc0..6d735ff 100644 --- a/jira.php +++ b/jira.php @@ -494,10 +494,15 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT