Brainstorm huge refactor
This commit is contained in:
+55
-5
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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: • 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>
|
||||||
|
|
||||||
<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
@@ -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
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
$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 & 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
@@ -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 & 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user