Compare commits
2 Commits
6a4981744f
...
bf0d2f3bca
| Author | SHA1 | Date | |
|---|---|---|---|
| bf0d2f3bca | |||
| 24fbfb9d82 |
+55
-5
@@ -72,6 +72,7 @@ try {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$sessionId = (int)($data['session_id'] ?? 0);
|
||||
$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) {
|
||||
echo json_encode(['error' => 'Sessie ID ontbreekt']);
|
||||
@@ -87,7 +88,59 @@ try {
|
||||
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']) {
|
||||
echo json_encode(['error' => $aiRes['error']]);
|
||||
@@ -99,9 +152,6 @@ try {
|
||||
VALUES (?, ?, 'Ideeën', 'amber', ?, 1)
|
||||
");
|
||||
|
||||
$createdCards = [];
|
||||
$authorName = 'AI Co-Pilot';
|
||||
|
||||
foreach ($aiRes['ideas'] as $ideaText) {
|
||||
$ideaText = trim($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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/../includes/boards.php';
|
||||
|
||||
$user = require_auth();
|
||||
$userId = (int)$user['id'];
|
||||
@@ -213,11 +214,42 @@ try {
|
||||
]);
|
||||
$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([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'plan_date' => $planDate,
|
||||
'outlook_synced' => ($source === 'OUTLOOK'),
|
||||
'board_sync' => $boardSyncResult,
|
||||
'message' => "E-mail succesvol ingepland voor {$planDate}." . ($outlookSyncMsg ? " ({$outlookSyncMsg})" : "")
|
||||
]);
|
||||
exit;
|
||||
|
||||
+54
-1
@@ -6,6 +6,7 @@
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/settings.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/../includes/boards.php';
|
||||
$user = require_auth(true);
|
||||
$db = get_db_connection();
|
||||
|
||||
@@ -182,6 +183,31 @@ try {
|
||||
$stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl]);
|
||||
$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([
|
||||
'success' => true,
|
||||
'task' => [
|
||||
@@ -199,6 +225,7 @@ try {
|
||||
],
|
||||
'outlook_synced' => ($source === 'OUTLOOK' && !empty($externalId)),
|
||||
'outlook_error' => $outlookSyncError,
|
||||
'board_sync' => $boardSyncResult,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
@@ -337,12 +364,38 @@ try {
|
||||
$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([
|
||||
'success' => true,
|
||||
'time_block_start' => $timeBlockStart,
|
||||
'time_block_end' => $timeBlockEnd,
|
||||
'duration_minutes' => $durationMinutes,
|
||||
'date' => $planDate
|
||||
'date' => $planDate,
|
||||
'board_sync' => $boardSyncResult,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
+63
-20
@@ -19,7 +19,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
$theme = $firstActive ? $firstActive['name'] : 'Product & Strategie';
|
||||
}
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
$autoAi = !empty($_POST['auto_ai']) ? 1 : 0;
|
||||
|
||||
if (!empty($title)) {
|
||||
$stmt = $db->prepare("
|
||||
@@ -28,7 +28,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
");
|
||||
$stmt->execute([$title, $theme, $description, $currentUser['id']]);
|
||||
$newId = $db->lastInsertId();
|
||||
header("Location: /brainstorm_canvas.php?id={$newId}");
|
||||
$redirectUrl = "/brainstorm_canvas.php?id={$newId}" . ($autoAi ? "&auto_ai=1" : "");
|
||||
header("Location: {$redirectUrl}");
|
||||
exit;
|
||||
} else {
|
||||
$error = "Titel van de brainstormsessie is verplicht.";
|
||||
@@ -147,13 +148,18 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<!-- 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 class="fociz-card p-6 sm:p-7 shadow-2xl max-w-lg w-full space-y-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-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">
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
|
||||
<i data-lucide="sparkles" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||||
<span>Nieuwe Brainstormsessie</span>
|
||||
</h3>
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<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">
|
||||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||
</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">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
@@ -162,14 +168,15 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<form method="POST" action="/brainstorm.php" class="space-y-4">
|
||||
<input type="hidden" name="action" value="create_session">
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Onderwerp / Titel</label>
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
||||
Onderwerp / Titel <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<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>
|
||||
<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">
|
||||
<?php
|
||||
$hasActiveCategory = false;
|
||||
@@ -188,15 +195,51 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</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>
|
||||
<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>
|
||||
<textarea
|
||||
name="description"
|
||||
rows="6"
|
||||
placeholder="Voer hier alle achtergrondinformatie, context en doelen in waar de AI post-its van gaat maken. Bijvoorbeeld: • Achtergrond: Klanten klagen over lange wachttijden bij onboarding. • Doel: Binnen 3 maanden de doorlooptijd met 50% verkorten. • Bekende knelpunten: Handmatige validatie, ontbrekende API-koppeling met CRM. • 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 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">
|
||||
Sessie Aanmaken & Canvas Openen
|
||||
<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">
|
||||
<i data-lucide="layout-grid" class="w-4 h-4"></i>
|
||||
<span>Sessie Aanmaken & Canvas Openen</span>
|
||||
</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">
|
||||
Annuleren
|
||||
|
||||
+61
-9
@@ -168,9 +168,9 @@ $categories = [
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2.5">
|
||||
<?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">
|
||||
<i data-lucide="sparkles" class="w-4 h-4 text-violet-500"></i>
|
||||
<span>AI Ideeën</span>
|
||||
<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-200"></i>
|
||||
<span>AI Post-its Genereren</span>
|
||||
</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">
|
||||
@@ -198,6 +198,32 @@ $categories = [
|
||||
</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 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5">
|
||||
<?php foreach ($categories as $catKey => $catConfig):
|
||||
@@ -789,6 +815,9 @@ $categories = [
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 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) ---
|
||||
async function aiGenerateIdeas(btn) {
|
||||
if (!confirm('Wil je AI Co-Pilot 4 innovatieve ideeën laten genereren en direct op het canvas plaatsen?')) return;
|
||||
async function aiGenerateSessionBoard(btn, silent = false) {
|
||||
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 : '';
|
||||
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;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
@@ -1311,13 +1346,16 @@ async function aiGenerateIdeas(btn) {
|
||||
const res = await fetch('/api/ai.php?action=generate_ideas', {
|
||||
method: 'POST',
|
||||
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();
|
||||
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();
|
||||
} else {
|
||||
alert(data.error || 'Kon geen ideeën genereren via AI');
|
||||
alert(data.error || 'Kon geen post-its genereren via AI');
|
||||
if (btn) {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
@@ -1325,7 +1363,7 @@ async function aiGenerateIdeas(btn) {
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij aanroepen van AI');
|
||||
alert('Netwerkfout bij aanroepen van AI: ' + err.message);
|
||||
if (btn) {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
@@ -1334,6 +1372,10 @@ async function aiGenerateIdeas(btn) {
|
||||
}
|
||||
}
|
||||
|
||||
async function aiGenerateIdeas(btn) {
|
||||
return aiGenerateSessionBoard(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;
|
||||
|
||||
@@ -1838,6 +1880,16 @@ document.addEventListener('keydown', (e) => {
|
||||
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>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
+46
-3
@@ -647,15 +647,18 @@ function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $c
|
||||
/**
|
||||
* 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"
|
||||
. "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.";
|
||||
|
||||
$aiRes = fociz_ai_chat([
|
||||
['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]);
|
||||
|
||||
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']];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -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!'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
$pageTitle = 'Fociz — Microsoft Outlook Mail';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/includes/boards.php';
|
||||
|
||||
$userId = (int)($currentUser['id'] ?? 0);
|
||||
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
|
||||
$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)
|
||||
$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>
|
||||
|
||||
<!-- 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 & 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">
|
||||
<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
|
||||
@@ -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() {
|
||||
if (!selectedMessage) return;
|
||||
document.getElementById('planMailId').value = selectedMessage.id;
|
||||
@@ -843,6 +947,22 @@ function openPlanMailModal() {
|
||||
const notice = document.getElementById('mailPlanOpenBlockNotice');
|
||||
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();
|
||||
|
||||
const modal = document.getElementById('planMailModal');
|
||||
@@ -910,6 +1030,13 @@ async function submitPlanEmail(e, options = {}) {
|
||||
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 {
|
||||
const res = await fetch('/api/mail.php?action=plan_email', {
|
||||
method: 'POST',
|
||||
@@ -922,7 +1049,13 @@ async function submitPlanEmail(e, options = {}) {
|
||||
duration_minutes: durationMinutes,
|
||||
priority: priority,
|
||||
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();
|
||||
|
||||
+142
-2
@@ -18,10 +18,13 @@ if ($isAjaxRequest) {
|
||||
require_once __DIR__ . '/includes/settings.php';
|
||||
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/includes/integrations/jira.php';
|
||||
require_once __DIR__ . '/includes/boards.php';
|
||||
|
||||
$db = get_db_connection();
|
||||
$userId = (int)($currentUser['id'] ?? 0);
|
||||
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
|
||||
$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'
|
||||
if (!$isAjaxRequest && isset($_GET['date']) && $_GET['date'] === $today) {
|
||||
@@ -2126,6 +2129,55 @@ if ($isAjaxRequest) {
|
||||
<option value="P3" selected>P3 — Normaal</option>
|
||||
</select>
|
||||
</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 & 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 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();
|
||||
}
|
||||
|
||||
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) {
|
||||
const rawKey = String(issueKey || '').trim();
|
||||
const normKey = rawKey.toUpperCase();
|
||||
@@ -5111,6 +5216,22 @@ function openJiraPlanModal(issueKey, fallbackData = null) {
|
||||
if (notice) notice.classList.add('hidden');
|
||||
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
|
||||
const submitBtn = document.getElementById('btnSubmitJiraPlan');
|
||||
if (submitBtn) {
|
||||
@@ -5328,6 +5449,23 @@ async function submitJiraPlanModal() {
|
||||
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) {
|
||||
// Bestaande taak bijwerken / verplaatsen naar eventueel andere datum
|
||||
res = await fetch('/api/planner.php?action=update_task', {
|
||||
@@ -5340,7 +5478,8 @@ async function submitJiraPlanModal() {
|
||||
duration_minutes: duration,
|
||||
time_block_start: finalStartTime,
|
||||
time_block_end: finalEndTime,
|
||||
date: planDate
|
||||
date: planDate,
|
||||
...boardPayload
|
||||
})
|
||||
});
|
||||
data = await res.json();
|
||||
@@ -5353,7 +5492,8 @@ async function submitJiraPlanModal() {
|
||||
external_id: issue.key,
|
||||
external_url: issue.url || '',
|
||||
duration_minutes: duration,
|
||||
date: planDate
|
||||
date: planDate,
|
||||
...boardPayload
|
||||
};
|
||||
if (finalStartTime) {
|
||||
payload.time_block_start = finalStartTime;
|
||||
|
||||
Reference in New Issue
Block a user