Brainstorm huge refactor #78

Merged
NiekRabelink merged 1 commits from dev into main 2026-09-14 09:11:26 +00:00
9 changed files with 747 additions and 41 deletions
Showing only changes of commit 24fbfb9d82 - Show all commits
+55 -5
View File
@@ -72,6 +72,7 @@ try {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$sessionId = (int)($data['session_id'] ?? 0); $sessionId = (int)($data['session_id'] ?? 0);
$count = min(6, max(2, (int)($data['count'] ?? 4))); $count = min(6, max(2, (int)($data['count'] ?? 4)));
$fullBoard = !empty($data['full_board']); // Genereren over alle 4 categorieën o.b.v. briefing
if (!$sessionId) { if (!$sessionId) {
echo json_encode(['error' => 'Sessie ID ontbreekt']); echo json_encode(['error' => 'Sessie ID ontbreekt']);
@@ -87,7 +88,59 @@ try {
exit; exit;
} }
$aiRes = fociz_ai_generate_ideas($session['title'], $session['theme'] ?? 'Algemeen', $count, $user['id']); $authorName = 'AI Co-Pilot';
$createdCards = [];
$colorMap = [
'Ideeën' => 'amber',
'Knelpunten' => 'rose',
'Oplossingen' => 'teal',
'Quick Wins' => 'indigo',
];
if ($fullBoard || !empty($session['description'])) {
// Gebruik de volledige board generator die alle 4 categorieën vult aan de hand van de briefing
$aiRes = fociz_ai_generate_session_board(
$session['title'],
$session['theme'] ?? 'Algemeen',
$session['description'] ?? '',
$user['id']
);
if ($aiRes['success'] && !empty($aiRes['cards'])) {
$insertStmt = $db->prepare("
INSERT INTO brainstorm_cards (session_id, content, category, color, author_name, votes)
VALUES (?, ?, ?, ?, ?, 1)
");
foreach ($aiRes['cards'] as $cardItem) {
$cText = trim($cardItem['content'] ?? '');
$cCat = trim($cardItem['category'] ?? 'Ideeën');
if (!isset($colorMap[$cCat])) {
$cCat = 'Ideeën';
}
$cColor = $colorMap[$cCat];
if (!empty($cText)) {
$insertStmt->execute([$sessionId, $cText, $cCat, $cColor, $authorName]);
$createdCards[] = [
'id' => (int)$db->lastInsertId(),
'content' => $cText,
'category' => $cCat,
'color' => $cColor,
'author_name' => $authorName,
'votes' => 1,
];
}
}
echo json_encode(['success' => true, 'cards' => $createdCards, 'mode' => 'full_board']);
exit;
}
}
// Fallback: reguliere generatie
$aiRes = fociz_ai_generate_ideas($session['title'], $session['theme'] ?? 'Algemeen', $count, $user['id'], $session['description'] ?? '');
if (!$aiRes['success']) { if (!$aiRes['success']) {
echo json_encode(['error' => $aiRes['error']]); echo json_encode(['error' => $aiRes['error']]);
@@ -99,9 +152,6 @@ try {
VALUES (?, ?, 'Ideeën', 'amber', ?, 1) VALUES (?, ?, 'Ideeën', 'amber', ?, 1)
"); ");
$createdCards = [];
$authorName = 'AI Co-Pilot';
foreach ($aiRes['ideas'] as $ideaText) { foreach ($aiRes['ideas'] as $ideaText) {
$ideaText = trim($ideaText); $ideaText = trim($ideaText);
if (!empty($ideaText)) { if (!empty($ideaText)) {
@@ -117,7 +167,7 @@ try {
} }
} }
echo json_encode(['success' => true, 'cards' => $createdCards]); echo json_encode(['success' => true, 'cards' => $createdCards, 'mode' => 'ideas']);
exit; exit;
} }
+32
View File
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../includes/auth.php'; require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/integrations/microsoft.php'; require_once __DIR__ . '/../includes/integrations/microsoft.php';
require_once __DIR__ . '/../includes/boards.php';
$user = require_auth(); $user = require_auth();
$userId = (int)$user['id']; $userId = (int)$user['id'];
@@ -213,11 +214,42 @@ try {
]); ]);
$taskId = (int)$db->lastInsertId(); $taskId = (int)$db->lastInsertId();
// Optioneel toevoegen aan Bord en Tijdlijn
$boardSyncResult = null;
$addToBoard = !empty($input['add_to_board']) && !empty($input['board_id']);
if ($addToBoard) {
$boardId = (int)$input['board_id'];
$laneId = (int)($input['lane_id'] ?? 0);
$scheduleOnTimeline = !empty($input['schedule_on_timeline']);
$timelineStartDate = !empty($input['timeline_start_date']) ? trim($input['timeline_start_date']) : $planDate;
$timelineEndDate = !empty($input['timeline_end_date']) ? trim($input['timeline_end_date']) : $timelineStartDate;
$mailDesc = "Afzender: " . ($sender ?: 'Onbekend');
if (!empty($webLink)) {
$mailDesc .= "\nWeblink: " . $webLink;
}
$boardSyncResult = fociz_add_item_to_board_and_timeline($db, $userId, (($user['role'] ?? '') === 'ADMIN'), [
'board_id' => $boardId,
'lane_id' => $laneId,
'title' => $taskTitle,
'description' => $mailDesc,
'priority' => $priority,
'color' => !empty($input['board_color']) ? $input['board_color'] : 'blue',
'schedule_on_timeline' => $scheduleOnTimeline,
'start_date' => $timelineStartDate,
'end_date' => $timelineEndDate,
'start_time' => $timeBlockStart ?: '09:00',
'end_time' => $timeBlockEnd ?: '10:00',
]);
}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'task_id' => $taskId, 'task_id' => $taskId,
'plan_date' => $planDate, 'plan_date' => $planDate,
'outlook_synced' => ($source === 'OUTLOOK'), 'outlook_synced' => ($source === 'OUTLOOK'),
'board_sync' => $boardSyncResult,
'message' => "E-mail succesvol ingepland voor {$planDate}." . ($outlookSyncMsg ? " ({$outlookSyncMsg})" : "") 'message' => "E-mail succesvol ingepland voor {$planDate}." . ($outlookSyncMsg ? " ({$outlookSyncMsg})" : "")
]); ]);
exit; exit;
+54 -1
View File
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../includes/auth.php'; require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/settings.php'; require_once __DIR__ . '/../includes/settings.php';
require_once __DIR__ . '/../includes/integrations/microsoft.php'; require_once __DIR__ . '/../includes/integrations/microsoft.php';
require_once __DIR__ . '/../includes/boards.php';
$user = require_auth(true); $user = require_auth(true);
$db = get_db_connection(); $db = get_db_connection();
@@ -182,6 +183,31 @@ try {
$stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl]); $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl]);
$taskId = (int)$db->lastInsertId(); $taskId = (int)$db->lastInsertId();
// Optioneel toevoegen aan Bord en Tijdlijn
$boardSyncResult = null;
$addToBoard = !empty($data['add_to_board']) && !empty($data['board_id']);
if ($addToBoard) {
$boardId = (int)$data['board_id'];
$laneId = (int)($data['lane_id'] ?? 0);
$scheduleOnTimeline = !empty($data['schedule_on_timeline']);
$timelineStartDate = !empty($data['timeline_start_date']) ? trim($data['timeline_start_date']) : $date;
$timelineEndDate = !empty($data['timeline_end_date']) ? trim($data['timeline_end_date']) : $timelineStartDate;
$boardSyncResult = fociz_add_item_to_board_and_timeline($db, (int)$user['id'], ($user['role'] === 'ADMIN'), [
'board_id' => $boardId,
'lane_id' => $laneId,
'title' => $title,
'description' => (!empty($data['description']) ? trim((string)$data['description']) : "") . (!empty($externalUrl) ? "\n\nReferentie: " . $externalUrl : ""),
'priority' => $priority,
'color' => !empty($data['board_color']) ? $data['board_color'] : 'blue',
'schedule_on_timeline' => $scheduleOnTimeline,
'start_date' => $timelineStartDate,
'end_date' => $timelineEndDate,
'start_time' => $timeBlockStart ?: '09:00',
'end_time' => $timeBlockEnd ?: '10:00',
]);
}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'task' => [ 'task' => [
@@ -199,6 +225,7 @@ try {
], ],
'outlook_synced' => ($source === 'OUTLOOK' && !empty($externalId)), 'outlook_synced' => ($source === 'OUTLOOK' && !empty($externalId)),
'outlook_error' => $outlookSyncError, 'outlook_error' => $outlookSyncError,
'board_sync' => $boardSyncResult,
]); ]);
exit; exit;
} }
@@ -337,12 +364,38 @@ try {
$stmtUpdate->execute([$title, $description, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl, $taskId, $user['id']]); $stmtUpdate->execute([$title, $description, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl, $taskId, $user['id']]);
} }
// Optioneel toevoegen aan Bord en Tijdlijn bij herplannen
$boardSyncResult = null;
$addToBoard = !empty($data['add_to_board']) && !empty($data['board_id']);
if ($addToBoard) {
$boardId = (int)$data['board_id'];
$laneId = (int)($data['lane_id'] ?? 0);
$scheduleOnTimeline = !empty($data['schedule_on_timeline']);
$timelineStartDate = !empty($data['timeline_start_date']) ? trim($data['timeline_start_date']) : $planDate;
$timelineEndDate = !empty($data['timeline_end_date']) ? trim($data['timeline_end_date']) : $timelineStartDate;
$boardSyncResult = fociz_add_item_to_board_and_timeline($db, (int)$user['id'], ($user['role'] === 'ADMIN'), [
'board_id' => $boardId,
'lane_id' => $laneId,
'title' => $title,
'description' => (!empty($description) ? trim((string)$description) : "") . (!empty($externalUrl) ? "\n\nReferentie: " . $externalUrl : ""),
'priority' => $priority,
'color' => !empty($data['board_color']) ? $data['board_color'] : 'blue',
'schedule_on_timeline' => $scheduleOnTimeline,
'start_date' => $timelineStartDate,
'end_date' => $timelineEndDate,
'start_time' => $timeBlockStart ?: '09:00',
'end_time' => $timeBlockEnd ?: '10:00',
]);
}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'time_block_start' => $timeBlockStart, 'time_block_start' => $timeBlockStart,
'time_block_end' => $timeBlockEnd, 'time_block_end' => $timeBlockEnd,
'duration_minutes' => $durationMinutes, 'duration_minutes' => $durationMinutes,
'date' => $planDate 'date' => $planDate,
'board_sync' => $boardSyncResult,
]); ]);
exit; exit;
} }
+63 -20
View File
@@ -19,7 +19,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$theme = $firstActive ? $firstActive['name'] : 'Product & Strategie'; $theme = $firstActive ? $firstActive['name'] : 'Product & Strategie';
} }
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
$autoAi = !empty($_POST['auto_ai']) ? 1 : 0;
if (!empty($title)) { if (!empty($title)) {
$stmt = $db->prepare(" $stmt = $db->prepare("
@@ -28,7 +28,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
"); ");
$stmt->execute([$title, $theme, $description, $currentUser['id']]); $stmt->execute([$title, $theme, $description, $currentUser['id']]);
$newId = $db->lastInsertId(); $newId = $db->lastInsertId();
header("Location: /brainstorm_canvas.php?id={$newId}"); $redirectUrl = "/brainstorm_canvas.php?id={$newId}" . ($autoAi ? "&auto_ai=1" : "");
header("Location: {$redirectUrl}");
exit; exit;
} else { } else {
$error = "Titel van de brainstormsessie is verplicht."; $error = "Titel van de brainstormsessie is verplicht.";
@@ -147,13 +148,18 @@ require_once __DIR__ . '/includes/header.php';
</div> </div>
<!-- Modal: Nieuwe sessie --> <!-- Modal: Nieuwe sessie -->
<div id="newSessionModal" onclick="if(event.target === this) this.classList.add('hidden')" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"> <div id="newSessionModal" onclick="if(event.target === this) this.classList.add('hidden')" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4 overflow-y-auto">
<div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-lg w-full space-y-4"> <div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-xl w-full my-6 space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800"> <div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2"> <div class="flex items-center space-x-2.5">
<i data-lucide="sparkles" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i> <div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center border border-indigo-100 dark:border-indigo-800/60">
<span>Nieuwe Brainstormsessie</span> <i data-lucide="sparkles" class="w-4 h-4"></i>
</h3> </div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Nieuwe Brainstormsessie</h3>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Creëer een canvas en voed de AI Co-Pilot met achtergrondinformatie</p>
</div>
</div>
<button onclick="document.getElementById('newSessionModal').classList.add('hidden')" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer"> <button onclick="document.getElementById('newSessionModal').classList.add('hidden')" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i> <i data-lucide="x" class="w-4 h-4"></i>
</button> </button>
@@ -162,14 +168,15 @@ require_once __DIR__ . '/includes/header.php';
<form method="POST" action="/brainstorm.php" class="space-y-4"> <form method="POST" action="/brainstorm.php" class="space-y-4">
<input type="hidden" name="action" value="create_session"> <input type="hidden" name="action" value="create_session">
<div> <div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Onderwerp / Titel</label> <div class="sm:col-span-2">
<input type="text" name="title" required placeholder="Bijv. Q4 Automatisering & Klantreis" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs"> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
</div> Onderwerp / Titel <span class="text-rose-500">*</span>
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3"> <input type="text" name="title" required placeholder="Bijv. Modernisering Klantportaal & Self-service" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs font-semibold">
</div>
<div> <div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Categorie</label> <label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Thema / Categorie</label>
<select name="category" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs"> <select name="category" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<?php <?php
$hasActiveCategory = false; $hasActiveCategory = false;
@@ -188,15 +195,51 @@ require_once __DIR__ . '/includes/header.php';
<?php endif; ?> <?php endif; ?>
</select> </select>
</div> </div>
<div> </div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Doel / Beschrijving</label>
<input type="text" name="description" placeholder="Doel van deze sessie..." class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs"> <div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">
Uitgebreide Briefing, Context & Doelstellingen
</label>
<span class="text-[10px] text-indigo-600 dark:text-indigo-400 font-medium">Voeding voor AI post-its</span>
</div> </div>
<textarea
name="description"
rows="6"
placeholder="Voer hier alle achtergrondinformatie, context en doelen in waar de AI post-its van gaat maken. Bijvoorbeeld:&#10;&#10;• Achtergrond: Klanten klagen over lange wachttijden bij onboarding.&#10;• Doel: Binnen 3 maanden de doorlooptijd met 50% verkorten.&#10;• Bekende knelpunten: Handmatige validatie, ontbrekende API-koppeling met CRM.&#10;• Gewenste richting: Self-service portaal, automatische validatie, duidelijke statusupdates."
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl p-3.5 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs leading-relaxed resize-y font-normal"
></textarea>
<p class="text-[11px] text-slate-500 dark:text-slate-400 mt-1">
Tip: Hoe gedetailleerder de informatie, hoe gerichter en waardevoller de ideeën, knelpunten en oplossingen die AI genereert.
</p>
</div>
<!-- AI Direct Genereren Optie -->
<div class="p-3.5 rounded-xl bg-violet-50/70 dark:bg-violet-950/25 border border-violet-200/80 dark:border-violet-800/40 flex items-start space-x-3">
<input
type="checkbox"
id="auto_ai_checkbox"
name="auto_ai"
value="1"
checked
class="w-4 h-4 mt-0.5 rounded text-violet-600 focus:ring-0 border-violet-300 dark:border-violet-700 cursor-pointer"
>
<label for="auto_ai_checkbox" class="text-xs cursor-pointer select-none">
<span class="font-bold text-violet-900 dark:text-violet-200 block flex items-center space-x-1.5">
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-violet-600 dark:text-violet-400"></i>
<span>Direct AI post-its genereren op basis van deze sessiebeschrijving</span>
</span>
<span class="text-[11px] text-violet-700/80 dark:text-violet-300/80 block mt-0.5 leading-snug">
AI Co-Pilot analyseert de ingevoerde context direct bij het openen en maakt automatisch gerichte post-its aan voor Ideeën, Knelpunten, Oplossingen en Quick Wins.
</span>
</label>
</div> </div>
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80"> <div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<button type="submit" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs"> <button type="submit" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs flex items-center justify-center space-x-2">
Sessie Aanmaken & Canvas Openen <i data-lucide="layout-grid" class="w-4 h-4"></i>
<span>Sessie Aanmaken & Canvas Openen</span>
</button> </button>
<button type="button" onclick="document.getElementById('newSessionModal').classList.add('hidden')" class="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-xs font-semibold transition cursor-pointer"> <button type="button" onclick="document.getElementById('newSessionModal').classList.add('hidden')" class="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-xs font-semibold transition cursor-pointer">
Annuleren Annuleren
+61 -9
View File
@@ -168,9 +168,9 @@ $categories = [
<div class="flex flex-wrap items-center gap-2.5"> <div class="flex flex-wrap items-center gap-2.5">
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?> <?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="aiGenerateIdeas(this)" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl bg-violet-50 hover:bg-violet-100 dark:bg-violet-950/50 dark:hover:bg-violet-900/50 text-violet-700 dark:text-violet-300 border border-violet-200 dark:border-violet-800/60 text-xs font-bold transition cursor-pointer shadow-xs" title="Genereer 4 innovatieve ideeën via AI Co-Pilot"> <button onclick="aiGenerateSessionBoard(this)" class="flex items-center space-x-1.5 px-3.5 py-2 rounded-xl bg-violet-600 hover:bg-violet-500 text-white text-xs font-bold transition cursor-pointer shadow-xs" title="Genereer een compleet whiteboard (Ideeën, Knelpunten, Oplossingen en Quick Wins) op basis van de sessie briefing">
<i data-lucide="sparkles" class="w-4 h-4 text-violet-500"></i> <i data-lucide="sparkles" class="w-4 h-4 text-violet-200"></i>
<span>AI Ideeën</span> <span>AI Post-its Genereren</span>
</button> </button>
<button onclick="aiCategorizeCards(this)" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60 text-xs font-bold transition cursor-pointer shadow-xs" title="Sorteer alle kaartjes automatisch in Ideeën, Knelpunten, Oplossingen of Quick Wins"> <button onclick="aiCategorizeCards(this)" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60 text-xs font-bold transition cursor-pointer shadow-xs" title="Sorteer alle kaartjes automatisch in Ideeën, Knelpunten, Oplossingen of Quick Wins">
@@ -198,6 +198,32 @@ $categories = [
</div> </div>
</div> </div>
<!-- Sessie Context & Briefing Banner -->
<?php if (!empty($session['description'])): ?>
<div class="fociz-card p-4 sm:p-5 border-l-4 border-l-indigo-500 relative overflow-hidden transition">
<div class="flex items-start justify-between gap-4">
<div class="space-y-1.5 flex-1">
<div class="flex items-center space-x-2">
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 flex items-center space-x-1.5">
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
<span>Sessie Briefing & Doelstellingen</span>
</span>
<span class="text-[11px] text-slate-400 dark:text-slate-500">• Context voor AI & Team</span>
</div>
<div class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-line leading-relaxed font-normal">
<?= htmlspecialchars($session['description']) ?>
</div>
</div>
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="aiGenerateSessionBoard(this)" class="flex-shrink-0 px-3 py-2 rounded-xl bg-violet-50 hover:bg-violet-100 dark:bg-violet-950/40 dark:hover:bg-violet-900/40 text-violet-700 dark:text-violet-300 border border-violet-200 dark:border-violet-800/60 text-xs font-bold transition cursor-pointer flex items-center space-x-1.5 shadow-2xs">
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-violet-500"></i>
<span>AI Post-its Maken</span>
</button>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Whiteboard Categorie Kolommen --> <!-- Whiteboard Categorie Kolommen -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5"> <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5">
<?php foreach ($categories as $catKey => $catConfig): <?php foreach ($categories as $catKey => $catConfig):
@@ -789,6 +815,9 @@ $categories = [
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
<!-- Modal: Sessie Verwijderen Bevestiging --> <!-- Modal: Sessie Verwijderen Bevestiging -->
<div id="deleteCanvasSessionModal" onclick="if(event.target === this) closeDeleteCanvasSessionModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"> <div id="deleteCanvasSessionModal" onclick="if(event.target === this) closeDeleteCanvasSessionModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
<div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-md w-full space-y-4"> <div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-md w-full space-y-4">
@@ -1297,12 +1326,18 @@ async function submitConvertToRoadmap() {
} }
// --- AI Co-Pilot Canvas Assistentie (Release v1.5) --- // --- AI Co-Pilot Canvas Assistentie (Release v1.5) ---
async function aiGenerateIdeas(btn) { async function aiGenerateSessionBoard(btn, silent = false) {
if (!confirm('Wil je AI Co-Pilot 4 innovatieve ideeën laten genereren en direct op het canvas plaatsen?')) return; if (!silent) {
const hasBriefing = <?= !empty($session['description']) ? 'true' : 'false' ?>;
const confirmMsg = hasBriefing
? 'Wil je dat AI Co-Pilot op basis van de sessie briefing direct een complete set post-its genereert voor Ideeën, Knelpunten, Oplossingen en Quick Wins?'
: 'Wil je dat AI Co-Pilot innovatieve post-its genereert voor het whiteboard?';
if (!confirm(confirmMsg)) return;
}
const origHtml = btn ? btn.innerHTML : ''; const origHtml = btn ? btn.innerHTML : '';
if (btn) { if (btn) {
btn.innerHTML = '<span class="animate-pulse flex items-center space-x-1"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Genereren...</span></span>'; btn.innerHTML = '<span class="animate-pulse flex items-center space-x-1.5"><i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>AI genereert post-its...</span></span>';
btn.disabled = true; btn.disabled = true;
if (typeof lucide !== 'undefined') lucide.createIcons(); if (typeof lucide !== 'undefined') lucide.createIcons();
} }
@@ -1311,13 +1346,16 @@ async function aiGenerateIdeas(btn) {
const res = await fetch('/api/ai.php?action=generate_ideas', { const res = await fetch('/api/ai.php?action=generate_ideas', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ session_id: sessionId, count: 4 }) body: JSON.stringify({ session_id: sessionId, count: 4, full_board: 1 })
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
// Verwijder auto_ai parameter uit URL bij reload
const cleanUrl = window.location.pathname + '?id=' + sessionId;
window.history.replaceState({}, document.title, cleanUrl);
location.reload(); location.reload();
} else { } else {
alert(data.error || 'Kon geen ideeën genereren via AI'); alert(data.error || 'Kon geen post-its genereren via AI');
if (btn) { if (btn) {
btn.innerHTML = origHtml; btn.innerHTML = origHtml;
btn.disabled = false; btn.disabled = false;
@@ -1325,7 +1363,7 @@ async function aiGenerateIdeas(btn) {
} }
} }
} catch(err) { } catch(err) {
alert('Netwerkfout bij aanroepen van AI'); alert('Netwerkfout bij aanroepen van AI: ' + err.message);
if (btn) { if (btn) {
btn.innerHTML = origHtml; btn.innerHTML = origHtml;
btn.disabled = false; btn.disabled = false;
@@ -1334,6 +1372,10 @@ async function aiGenerateIdeas(btn) {
} }
} }
async function aiGenerateIdeas(btn) {
return aiGenerateSessionBoard(btn);
}
async function aiCategorizeCards(btn) { async function aiCategorizeCards(btn) {
if (!confirm('Wil je dat AI Co-Pilot alle kaartjes automatisch classificeert in de juiste kolom (Ideeën, Knelpunten, Oplossingen of Quick Wins)?')) return; if (!confirm('Wil je dat AI Co-Pilot alle kaartjes automatisch classificeert in de juiste kolom (Ideeën, Knelpunten, Oplossingen of Quick Wins)?')) return;
@@ -1838,6 +1880,16 @@ document.addEventListener('keydown', (e) => {
closeDeleteCanvasSessionModal(); closeDeleteCanvasSessionModal();
} }
}); });
// Automatische AI-generatie bij doorgestuurde auto_ai=1 parameter (bijv. direct na creatie)
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('auto_ai') === '1') {
// Toon subtiele toast of indicator en start generatie
const aiBtn = document.querySelector('button[onclick*="aiGenerateSessionBoard"]');
aiGenerateSessionBoard(aiBtn, true);
}
});
</script> </script>
<?php require_once __DIR__ . '/includes/footer.php'; ?> <?php require_once __DIR__ . '/includes/footer.php'; ?>
+46 -3
View File
@@ -647,15 +647,18 @@ function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $c
/** /**
* Genereert nieuwe relevante brainstorm ideeën voor een sessie * Genereert nieuwe relevante brainstorm ideeën voor een sessie
*/ */
function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count = 4, ?int $userId = null): array { function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count = 4, ?int $userId = null, string $description = ''): array {
$descContext = !empty($description) ? "\nContext en briefing van de sessie:\n" . trim($description) . "\n" : "";
$systemPrompt = "Je bent een innovatieve productstrateeg en brainstorm co-pilot.\n" $systemPrompt = "Je bent een innovatieve productstrateeg en brainstorm co-pilot.\n"
. "Genereer {$count} creatieve, scherpe en direct toepasbare ideeën voor een brainstormsessie getiteld: '{$sessionTitle}' (Thema: '{$theme}').\n" . "Genereer {$count} creatieve, scherpe en direct toepasbare ideeën voor een brainstormsessie getiteld: '{$sessionTitle}' (Thema: '{$theme}').\n"
. "Houd de teksten beknopt en krachtig (1-2 zinnen per idee, alsof ze op een gele post-it staan).\n" . $descContext
. "Houd de teksten beknopt en krachtig (1-2 zinnen per idee, alsof ze op een post-it staan).\n"
. "Retourneer uitsluitend een JSON-object met het veld 'ideas' dat een array van strings is."; . "Retourneer uitsluitend een JSON-object met het veld 'ideas' dat een array van strings is.";
$aiRes = fociz_ai_chat([ $aiRes = fociz_ai_chat([
['role' => 'system', 'content' => $systemPrompt], ['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => "Genereer {$count} vernieuwende ideeën."], ['role' => 'user', 'content' => "Genereer {$count} vernieuwende ideeën op basis van de sessie en context."],
], ['json_mode' => true, 'user_id' => $userId]); ], ['json_mode' => true, 'user_id' => $userId]);
if (!$aiRes['success'] || empty($aiRes['json']['ideas'])) { if (!$aiRes['success'] || empty($aiRes['json']['ideas'])) {
@@ -665,6 +668,46 @@ function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count
return ['success' => true, 'ideas' => $aiRes['json']['ideas']]; return ['success' => true, 'ideas' => $aiRes['json']['ideas']];
} }
/**
* Analyseert de volledige sessie briefing en genereert een complete set post-its verdeeld over:
* - Ideeën & Concepten (amber)
* - Knelpunten & Uitdagingen (rose)
* - Oplossingen & Features (teal)
* - Quick Wins (indigo)
*/
function fociz_ai_generate_session_board(string $sessionTitle, string $theme, string $description = '', ?int $userId = null): array {
$descContext = !empty($description)
? "Uitgebreide sessiebeschrijving en briefing:\n" . trim($description) . "\n\n"
: "Geen aanvullende briefing opgegeven; baseer je op de titel en het thema.\n\n";
$systemPrompt = "Je bent een ervaren agile facilitator en strategisch adviseur.\n"
. "Je analyseert de briefing van een brainstormsessie getiteld '{$sessionTitle}' (Thema: '{$theme}').\n"
. $descContext
. "Jouw taak is om de sessie direct uit te werken tot een rijk, concreet whiteboard met post-its over exact 4 categorieën:\n"
. "1. 'Ideeën': Vernieuwende concepten, functionaliteiten en strategische kansen (2 tot 4 post-its).\n"
. "2. 'Knelpunten': Concreet benoemde knelpunten, risico's, blokkades of gebruikersfrustraties (2 tot 4 post-its).\n"
. "3. 'Oplossingen': Structurele oplossingen, architectuurkeuzes of procesaanpassingen voor de knelpunten (2 tot 4 post-its).\n"
. "4. 'Quick Wins': Laagdrempelige acties die met minimale inspanning direct resultaat opleveren (2 tot 3 post-its).\n\n"
. "Regels per post-it:\n"
. "- Formuleer scherp, helder en direct to-the-point (1-2 zinnen per kaartje alsof het op een fysieke post-it staat).\n"
. "- Sluit naadloos aan op de verstrekte briefing en achtergrondinformatie.\n\n"
. "Retourneer UITSLUITEND een geldig JSON-object met het veld 'cards', waarbij elk item bestaat uit:\n"
. "- 'content': string (de tekst op de post-it)\n"
. "- 'category': exact één van ['Ideeën', 'Knelpunten', 'Oplossingen', 'Quick Wins']";
$aiRes = fociz_ai_chat([
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => "Genereer het complete brainstorm whiteboard op basis van de sessiebeschrijving."],
], ['json_mode' => true, 'user_id' => $userId]);
if (!$aiRes['success'] || empty($aiRes['json']['cards']) || !is_array($aiRes['json']['cards'])) {
// Fallback als Gemini geen gestructureerde cards array teruggaf
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen gestructureerde post-its genereren'];
}
return ['success' => true, 'cards' => $aiRes['json']['cards']];
}
/** /**
* Zet brainstorm kaartjes om in een scherp Roadmap Item * Zet brainstorm kaartjes om in een scherp Roadmap Item
*/ */
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* Fociz Board & Timeline Integratie Helper
* Biedt functies om taken / e-mails / Jira tickets direct als kaarten toe te voegen aan Kanban-borden en Project Tijdlijnen.
*/
require_once __DIR__ . '/auth.php';
/**
* Haalt alle actieve borden op waar de gebruiker toegang toe heeft, inclusief kolommen (lanes).
*/
function fociz_get_user_boards_with_lanes(PDO $db, int $userId, bool $isAdmin): array {
if ($isAdmin) {
$stmt = $db->query("
SELECT b.id, b.title, b.description, b.created_by_id
FROM boards b
WHERE b.is_archived = 0
ORDER BY b.id ASC
");
$boards = $stmt->fetchAll();
} else {
$stmt = $db->prepare("
SELECT b.id, b.title, b.description, b.created_by_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
");
$stmt->execute([$userId]);
$boards = $stmt->fetchAll();
}
if (empty($boards)) {
return [];
}
$boardIds = array_map(fn($b) => (int)$b['id'], $boards);
$placeholders = implode(',', array_fill(0, count($boardIds), '?'));
$laneStmt = $db->prepare("
SELECT id, board_id, title, color, position
FROM board_lanes
WHERE board_id IN ({$placeholders})
ORDER BY position ASC, id ASC
");
$laneStmt->execute($boardIds);
$lanes = $laneStmt->fetchAll();
$lanesByBoard = [];
foreach ($lanes as $lane) {
$bId = (int)$lane['board_id'];
$lanesByBoard[$bId][] = $lane;
}
foreach ($boards as &$b) {
$bId = (int)$b['id'];
$b['lanes'] = $lanesByBoard[$bId] ?? [];
}
unset($b);
return $boards;
}
/**
* Voegt een item toe aan een Kanban bord en optioneel aan de Project Tijdlijn.
*
* @param PDO $db Database verbinding
* @param int $userId ID van de gebruiker
* @param bool $isAdmin Is gebruiker admin
* @param array $data Data array met:
* - board_id (int)
* - lane_id (int, optioneel - defaults naar 1e lane)
* - title (string)
* - description (string, optioneel)
* - priority ('P1'|'P2'|'P3', optioneel)
* - color (string, optioneel)
* - schedule_on_timeline (bool, optioneel)
* - start_date (string 'YYYY-MM-DD', optioneel)
* - end_date (string 'YYYY-MM-DD', optioneel)
* - start_time (string 'HH:MM', optioneel)
* - end_time (string 'HH:MM', optioneel)
* - sync_outlook (bool, optioneel)
* @return array ['success' => bool, 'card_id' => int, 'message' => string]
*/
function fociz_add_item_to_board_and_timeline(PDO $db, int $userId, bool $isAdmin, array $data): array {
$boardId = (int)($data['board_id'] ?? 0);
$title = trim($data['title'] ?? '');
if ($boardId <= 0 || empty($title)) {
return ['success' => false, 'error' => 'Geldig bord en titel zijn verplicht.'];
}
// Autorisatiecheck
if (!$isAdmin) {
$stmtCheck = $db->prepare("SELECT COUNT(*) FROM board_members WHERE board_id = ? AND user_id = ?");
$stmtCheck->execute([$boardId, $userId]);
if ((int)$stmtCheck->fetchColumn() === 0) {
return ['success' => false, 'error' => 'Geen toegang tot dit bord.'];
}
}
$laneId = (int)($data['lane_id'] ?? 0);
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) {
return ['success' => false, 'error' => 'Het geselecteerde bord heeft geen kolommen.'];
}
$description = trim($data['description'] ?? '');
$priority = in_array($data['priority'] ?? '', ['P1', 'P2', 'P3']) ? $data['priority'] : 'P2';
$color = in_array($data['color'] ?? '', ['yellow', 'blue', 'green', 'purple', 'pink', 'orange', 'slate', 'indigo']) ? $data['color'] : 'blue';
$stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = ?");
$stmtMax->execute([$laneId]);
$maxPos = (int)$stmtMax->fetchColumn();
$stmtInsert = $db->prepare("
INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmtInsert->execute([$boardId, $laneId, $title, $description, $userId, $priority, $color, $maxPos + 1, $userId]);
$cardId = (int)$db->lastInsertId();
// Optioneel inplannen op Tijdlijn / Gantt
$scheduleOnTimeline = !empty($data['schedule_on_timeline']);
if ($scheduleOnTimeline && $cardId > 0) {
$startDate = !empty($data['start_date']) ? trim($data['start_date']) : date('Y-m-d');
$endDate = !empty($data['end_date']) ? trim($data['end_date']) : $startDate;
$startTime = !empty($data['start_time']) ? trim($data['start_time']) : '09:00';
$endTime = !empty($data['end_time']) ? trim($data['end_time']) : '17:00';
$daysOfWeek = trim($data['days_of_week'] ?? '1,2,3,4,5');
$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', ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
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)
");
$stmtSched->execute([$cardId, $startDate, $endDate, $startTime, $endTime, $daysOfWeek]);
}
return [
'success' => true,
'card_id' => $cardId,
'board_id' => $boardId,
'lane_id' => $laneId,
'scheduled_on_timeline' => $scheduleOnTimeline,
'message' => $scheduleOnTimeline
? 'Item succesvol toegevoegd aan het Bord en ingepland op de Tijdlijn!'
: 'Item succesvol toegevoegd aan het Bord!'
];
}
+134 -1
View File
@@ -7,9 +7,12 @@
$pageTitle = 'Fociz — Microsoft Outlook Mail'; $pageTitle = 'Fociz — Microsoft Outlook Mail';
require_once __DIR__ . '/includes/header.php'; require_once __DIR__ . '/includes/header.php';
require_once __DIR__ . '/includes/integrations/microsoft.php'; require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/boards.php';
$userId = (int)($currentUser['id'] ?? 0); $userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$db = get_db_connection(); $db = get_db_connection();
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1) // Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1'; $forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
@@ -325,6 +328,55 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
</div> </div>
</div> </div>
<!-- Optie: Tevens toevoegen aan Bord & Tijdlijn -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 space-y-2.5">
<label class="flex items-center space-x-2.5 cursor-pointer select-none">
<input type="checkbox" id="mailPlanAddToBoard" onchange="toggleMailBoardOptions(this.checked)" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="layout-grid" class="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400"></i>
<span>Tevens toevoegen aan Bord &amp; Tijdlijn</span>
</span>
</label>
<div id="mailBoardOptionsWrapper" class="hidden space-y-2.5 pt-2.5 border-t border-slate-200/70 dark:border-slate-800">
<?php if (empty($userBoards)): ?>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Er zijn geen actieve borden gevonden.</p>
<?php else: ?>
<div class="grid grid-cols-2 gap-2">
<div>
<label for="mailPlanBoardSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Bord</label>
<select id="mailPlanBoardSelect" onchange="onMailBoardSelectChange(this.value)" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<?php foreach ($userBoards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= htmlspecialchars($b['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label for="mailPlanLaneSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Kolom</label>
<select id="mailPlanLaneSelect" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<!-- Wordt gevuld via JS -->
</select>
</div>
</div>
<div class="pt-1">
<label class="flex items-center space-x-2 cursor-pointer select-none">
<input type="checkbox" id="mailPlanScheduleOnTimeline" checked onchange="toggleMailTimelineDates(this.checked)" class="w-3.5 h-3.5 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">Inplannen op de Project Tijdlijn</span>
</label>
<div id="mailTimelineDatesWrapper" class="grid grid-cols-2 gap-2 mt-2">
<div>
<label for="mailTimelineStartDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Startdatum</label>
<input type="date" id="mailTimelineStartDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label for="mailTimelineEndDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Einddatum</label>
<input type="date" id="mailTimelineEndDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800"> <div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closePlanMailModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer"> <button type="button" onclick="closePlanMailModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren Annuleren
@@ -833,6 +885,58 @@ function closeOutsideWorkhoursModalMail(choice) {
} }
} }
const availableUserBoards = <?= json_encode($userBoards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?> || [];
function toggleMailBoardOptions(show) {
const wrapper = document.getElementById('mailBoardOptionsWrapper');
if (wrapper) {
if (show) {
wrapper.classList.remove('hidden');
const boardSelect = document.getElementById('mailPlanBoardSelect');
if (boardSelect && boardSelect.value) {
onMailBoardSelectChange(boardSelect.value);
}
const planDate = document.getElementById('planMailDate')?.value || '';
const startInput = document.getElementById('mailTimelineStartDate');
const endInput = document.getElementById('mailTimelineEndDate');
if (startInput && !startInput.value) startInput.value = planDate;
if (endInput && !endInput.value) endInput.value = planDate;
} else {
wrapper.classList.add('hidden');
}
}
}
function onMailBoardSelectChange(boardId) {
const laneSelect = document.getElementById('mailPlanLaneSelect');
if (!laneSelect) return;
laneSelect.innerHTML = '';
const board = availableUserBoards.find(b => String(b.id) === String(boardId));
if (board && board.lanes && board.lanes.length > 0) {
board.lanes.forEach((lane, idx) => {
const opt = document.createElement('option');
opt.value = lane.id;
opt.textContent = lane.title;
if (idx === 0) opt.selected = true;
laneSelect.appendChild(opt);
});
} else {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Geen kolommen beschikbaar';
laneSelect.appendChild(opt);
}
}
function toggleMailTimelineDates(show) {
const wrapper = document.getElementById('mailTimelineDatesWrapper');
if (wrapper) {
if (show) wrapper.classList.remove('hidden');
else wrapper.classList.add('hidden');
}
}
function openPlanMailModal() { function openPlanMailModal() {
if (!selectedMessage) return; if (!selectedMessage) return;
document.getElementById('planMailId').value = selectedMessage.id; document.getElementById('planMailId').value = selectedMessage.id;
@@ -843,6 +947,22 @@ function openPlanMailModal() {
const notice = document.getElementById('mailPlanOpenBlockNotice'); const notice = document.getElementById('mailPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden'); if (notice) notice.classList.add('hidden');
const planDate = document.getElementById('planMailDate')?.value || '';
const addToBoardCb = document.getElementById('mailPlanAddToBoard');
if (addToBoardCb) {
addToBoardCb.checked = false;
toggleMailBoardOptions(false);
}
const schedTimelineCb = document.getElementById('mailPlanScheduleOnTimeline');
if (schedTimelineCb) {
schedTimelineCb.checked = true;
toggleMailTimelineDates(true);
}
const tStart = document.getElementById('mailTimelineStartDate');
const tEnd = document.getElementById('mailTimelineEndDate');
if (tStart) tStart.value = planDate;
if (tEnd) tEnd.value = planDate;
updatePlanMailEndTimeBadge(); updatePlanMailEndTimeBadge();
const modal = document.getElementById('planMailModal'); const modal = document.getElementById('planMailModal');
@@ -910,6 +1030,13 @@ async function submitPlanEmail(e, options = {}) {
if (typeof lucide !== 'undefined') lucide.createIcons(); if (typeof lucide !== 'undefined') lucide.createIcons();
} }
const addToBoard = document.getElementById('mailPlanAddToBoard')?.checked || false;
const boardId = parseInt(document.getElementById('mailPlanBoardSelect')?.value || '0', 10);
const laneId = parseInt(document.getElementById('mailPlanLaneSelect')?.value || '0', 10);
const scheduleOnTimeline = document.getElementById('mailPlanScheduleOnTimeline')?.checked || false;
const timelineStartDate = document.getElementById('mailTimelineStartDate')?.value || planDate;
const timelineEndDate = document.getElementById('mailTimelineEndDate')?.value || timelineStartDate;
try { try {
const res = await fetch('/api/mail.php?action=plan_email', { const res = await fetch('/api/mail.php?action=plan_email', {
method: 'POST', method: 'POST',
@@ -922,7 +1049,13 @@ async function submitPlanEmail(e, options = {}) {
duration_minutes: durationMinutes, duration_minutes: durationMinutes,
priority: priority, priority: priority,
web_link: webLink, web_link: webLink,
sender: sender sender: sender,
add_to_board: addToBoard,
board_id: boardId,
lane_id: laneId,
schedule_on_timeline: scheduleOnTimeline,
timeline_start_date: timelineStartDate,
timeline_end_date: timelineEndDate
}) })
}); });
const data = await res.json(); const data = await res.json();
+142 -2
View File
@@ -18,10 +18,13 @@ if ($isAjaxRequest) {
require_once __DIR__ . '/includes/settings.php'; require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/integrations/microsoft.php'; require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php'; require_once __DIR__ . '/includes/integrations/jira.php';
require_once __DIR__ . '/includes/boards.php';
$db = get_db_connection(); $db = get_db_connection();
$userId = (int)($currentUser['id'] ?? 0); $userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$today = date('Y-m-d'); $today = date('Y-m-d');
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
// Indien de datum van vandaag via de URL is meegegeven bij een normale pagina-aanroep, schoon de URL op naar zuiver 'planner.php' // Indien de datum van vandaag via de URL is meegegeven bij een normale pagina-aanroep, schoon de URL op naar zuiver 'planner.php'
if (!$isAjaxRequest && isset($_GET['date']) && $_GET['date'] === $today) { if (!$isAjaxRequest && isset($_GET['date']) && $_GET['date'] === $today) {
@@ -2126,6 +2129,55 @@ if ($isAjaxRequest) {
<option value="P3" selected>P3 — Normaal</option> <option value="P3" selected>P3 — Normaal</option>
</select> </select>
</div> </div>
<!-- Optie: Tevens toevoegen aan Bord & Tijdlijn -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 space-y-2.5">
<label class="flex items-center space-x-2.5 cursor-pointer select-none">
<input type="checkbox" id="jiraPlanAddToBoard" onchange="toggleJiraBoardOptions(this.checked)" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="layout-grid" class="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400"></i>
<span>Tevens toevoegen aan Bord &amp; Tijdlijn</span>
</span>
</label>
<div id="jiraBoardOptionsWrapper" class="hidden space-y-2.5 pt-2.5 border-t border-slate-200/70 dark:border-slate-800">
<?php if (empty($userBoards)): ?>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Er zijn geen actieve borden gevonden.</p>
<?php else: ?>
<div class="grid grid-cols-2 gap-2">
<div>
<label for="jiraPlanBoardSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Bord</label>
<select id="jiraPlanBoardSelect" onchange="onJiraBoardSelectChange(this.value)" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<?php foreach ($userBoards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= htmlspecialchars($b['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label for="jiraPlanLaneSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Kolom</label>
<select id="jiraPlanLaneSelect" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<!-- Wordt gevuld via JS -->
</select>
</div>
</div>
<div class="pt-1">
<label class="flex items-center space-x-2 cursor-pointer select-none">
<input type="checkbox" id="jiraPlanScheduleOnTimeline" checked onchange="toggleJiraTimelineDates(this.checked)" class="w-3.5 h-3.5 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">Inplannen op de Project Tijdlijn</span>
</label>
<div id="jiraTimelineDatesWrapper" class="grid grid-cols-2 gap-2 mt-2">
<div>
<label for="jiraTimelineStartDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Startdatum</label>
<input type="date" id="jiraTimelineStartDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label for="jiraTimelineEndDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Einddatum</label>
<input type="date" id="jiraTimelineEndDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div> </div>
<div class="flex space-x-3 pt-2 border-t border-slate-100 dark:border-slate-800/80"> <div class="flex space-x-3 pt-2 border-t border-slate-100 dark:border-slate-800/80">
@@ -4976,6 +5028,59 @@ function toggleJiraPlanDescription() {
if (typeof lucide !== 'undefined') lucide.createIcons(); if (typeof lucide !== 'undefined') lucide.createIcons();
} }
const availableUserBoards = <?= json_encode($userBoards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?> || [];
function toggleJiraBoardOptions(show) {
const wrapper = document.getElementById('jiraBoardOptionsWrapper');
if (wrapper) {
if (show) {
wrapper.classList.remove('hidden');
const boardSelect = document.getElementById('jiraPlanBoardSelect');
if (boardSelect && boardSelect.value) {
onJiraBoardSelectChange(boardSelect.value);
}
// Sync start en einddatum met de gekozen plandatum
const planDate = document.getElementById('jiraPlanDate')?.value || '';
const startInput = document.getElementById('jiraTimelineStartDate');
const endInput = document.getElementById('jiraTimelineEndDate');
if (startInput && !startInput.value) startInput.value = planDate;
if (endInput && !endInput.value) endInput.value = planDate;
} else {
wrapper.classList.add('hidden');
}
}
}
function onJiraBoardSelectChange(boardId) {
const laneSelect = document.getElementById('jiraPlanLaneSelect');
if (!laneSelect) return;
laneSelect.innerHTML = '';
const board = availableUserBoards.find(b => String(b.id) === String(boardId));
if (board && board.lanes && board.lanes.length > 0) {
board.lanes.forEach((lane, idx) => {
const opt = document.createElement('option');
opt.value = lane.id;
opt.textContent = lane.title;
if (idx === 0) opt.selected = true;
laneSelect.appendChild(opt);
});
} else {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Geen kolommen beschikbaar';
laneSelect.appendChild(opt);
}
}
function toggleJiraTimelineDates(show) {
const wrapper = document.getElementById('jiraTimelineDatesWrapper');
if (wrapper) {
if (show) wrapper.classList.remove('hidden');
else wrapper.classList.add('hidden');
}
}
function openJiraPlanModal(issueKey, fallbackData = null) { function openJiraPlanModal(issueKey, fallbackData = null) {
const rawKey = String(issueKey || '').trim(); const rawKey = String(issueKey || '').trim();
const normKey = rawKey.toUpperCase(); const normKey = rawKey.toUpperCase();
@@ -5111,6 +5216,22 @@ function openJiraPlanModal(issueKey, fallbackData = null) {
if (notice) notice.classList.add('hidden'); if (notice) notice.classList.add('hidden');
updateJiraPlanEndTimeDisplay(); updateJiraPlanEndTimeDisplay();
// Reset board & timeline velden
const addToBoardCb = document.getElementById('jiraPlanAddToBoard');
if (addToBoardCb) {
addToBoardCb.checked = false;
toggleJiraBoardOptions(false);
}
const schedTimelineCb = document.getElementById('jiraPlanScheduleOnTimeline');
if (schedTimelineCb) {
schedTimelineCb.checked = true;
toggleJiraTimelineDates(true);
}
const tStart = document.getElementById('jiraTimelineStartDate');
const tEnd = document.getElementById('jiraTimelineEndDate');
if (tStart) tStart.value = targetDate;
if (tEnd) tEnd.value = targetDate;
// Reset submit button state // Reset submit button state
const submitBtn = document.getElementById('btnSubmitJiraPlan'); const submitBtn = document.getElementById('btnSubmitJiraPlan');
if (submitBtn) { if (submitBtn) {
@@ -5328,6 +5449,23 @@ async function submitJiraPlanModal() {
finalEndTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`; finalEndTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
} }
const addToBoard = document.getElementById('jiraPlanAddToBoard')?.checked || false;
const boardId = parseInt(document.getElementById('jiraPlanBoardSelect')?.value || '0', 10);
const laneId = parseInt(document.getElementById('jiraPlanLaneSelect')?.value || '0', 10);
const scheduleOnTimeline = document.getElementById('jiraPlanScheduleOnTimeline')?.checked || false;
const timelineStartDate = document.getElementById('jiraTimelineStartDate')?.value || planDate;
const timelineEndDate = document.getElementById('jiraTimelineEndDate')?.value || timelineStartDate;
const boardPayload = {
add_to_board: addToBoard,
board_id: boardId,
lane_id: laneId,
schedule_on_timeline: scheduleOnTimeline,
timeline_start_date: timelineStartDate,
timeline_end_date: timelineEndDate,
description: issue.summary ? `${issue.key}: ${issue.summary}` : ''
};
if (isUpdate) { if (isUpdate) {
// Bestaande taak bijwerken / verplaatsen naar eventueel andere datum // Bestaande taak bijwerken / verplaatsen naar eventueel andere datum
res = await fetch('/api/planner.php?action=update_task', { res = await fetch('/api/planner.php?action=update_task', {
@@ -5340,7 +5478,8 @@ async function submitJiraPlanModal() {
duration_minutes: duration, duration_minutes: duration,
time_block_start: finalStartTime, time_block_start: finalStartTime,
time_block_end: finalEndTime, time_block_end: finalEndTime,
date: planDate date: planDate,
...boardPayload
}) })
}); });
data = await res.json(); data = await res.json();
@@ -5353,7 +5492,8 @@ async function submitJiraPlanModal() {
external_id: issue.key, external_id: issue.key,
external_url: issue.url || '', external_url: issue.url || '',
duration_minutes: duration, duration_minutes: duration,
date: planDate date: planDate,
...boardPayload
}; };
if (finalStartTime) { if (finalStartTime) {
payload.time_block_start = finalStartTime; payload.time_block_start = finalStartTime;