Fix #73

Merged
NiekRabelink merged 1 commits from dev into main 2026-09-12 16:13:16 +00:00
4 changed files with 426 additions and 164 deletions
Showing only changes of commit f63ea740bf - Show all commits
+69 -31
View File
@@ -22,55 +22,62 @@ $isAdmin = ($userRole === 'ADMIN');
width: 100% !important; width: 100% !important;
} }
/* --- Realistische Echte Post-it Sticky Note Styling --- */ /* --- Realistische Echte Post-it Sticky Note Styling (Conform referentie linksboven) --- */
.postit-note { .postit-note {
position: relative; position: relative;
border-top-left-radius: 2px; border-radius: 2px;
border-top-right-radius: 3px; /* Uitgesproken, zachte schaduw zoals in referentie */
border-bottom-right-radius: 4px;
border-bottom-left-radius: 14px;
box-shadow: box-shadow:
0 1px 1px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06),
0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 8px 16px -2px rgba(0, 0, 0, 0.12),
0 10px 18px -4px rgba(0, 0, 0, 0.12), 0 18px 28px -6px rgba(0, 0, 0, 0.14),
-2px 6px 8px -2px rgba(0, 0, 0, 0.08); 8px 16px 20px -4px rgba(0, 0, 0, 0.18);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease; transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
} }
.postit-note:hover { .postit-note:hover {
transform: translateY(-3px) rotate(-0.5deg); transform: translateY(-3px) rotate(-0.5deg);
box-shadow: box-shadow:
0 2px 4px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.08),
0 8px 12px -2px rgba(0, 0, 0, 0.12), 0 12px 22px -3px rgba(0, 0, 0, 0.16),
0 16px 24px -4px rgba(0, 0, 0, 0.16), 0 24px 34px -6px rgba(0, 0, 0, 0.18),
-4px 8px 12px -2px rgba(0, 0, 0, 0.12); 10px 20px 24px -4px rgba(0, 0, 0, 0.22);
} }
/* Echte sticky note curl & schaduw links onder */
/* De opkrullende flap rechtsonder met realistische 3D vouw en diepe slagschaduw */
.postit-note::after { .postit-note::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -1px; bottom: -1px;
left: -1px; right: -1px;
width: 24px; width: 34px;
height: 24px; height: 34px;
background: background:
linear-gradient(135deg, rgba(0, 0, 0, 0.22) 0%, rgba(0, 0, 0, 0.08) 35%, transparent 45%), linear-gradient(135deg, transparent 48%, rgba(0, 0, 0, 0.08) 50%, rgba(0, 0, 0, 0.28) 60%, transparent 68%),
linear-gradient(315deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.2) 40%, transparent 48%); linear-gradient(225deg, #fef08a 0%, #facc15 48%, transparent 52%);
border-bottom-left-radius: 4px; border-bottom-right-radius: 2px;
pointer-events: none; pointer-events: none;
z-index: 5; z-index: 10;
box-shadow: 5px 5px 10px -2px rgba(0, 0, 0, 0.3);
clip-path: polygon(100% 0, 0 100%, 100% 100%);
} }
/* Subtiele klevende plakrand aan de bovenkant */
.postit-note::before { .dark .postit-note::after {
content: ''; background:
linear-gradient(135deg, transparent 48%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0.45) 60%, transparent 68%),
linear-gradient(225deg, #eab308 0%, #ca8a04 48%, transparent 52%);
box-shadow: 5px 5px 12px -2px rgba(0, 0, 0, 0.6);
}
/* 3D Rode Punaise (Pushpin) linksboven met naald en schaduw conform de afbeelding */
.postit-pin {
position: absolute; position: absolute;
top: 0; top: -10px;
left: 0; left: 20px;
right: 0; width: 22px;
height: 6px; height: 22px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.04), transparent);
border-top-left-radius: 2px;
border-top-right-radius: 3px;
pointer-events: none; pointer-events: none;
z-index: 25;
filter: drop-shadow(3px 5px 3px rgba(0, 0, 0, 0.35));
} }
</style> </style>
@@ -843,6 +850,37 @@ function renderCards() {
oncontextmenu="openBoardCardContextMenu(event, ${card.id}); return false;" oncontextmenu="openBoardCardContextMenu(event, ${card.id}); return false;"
class="group/card relative p-3 rounded-xl border ${colorClass} shadow-2xs hover:shadow-md hover:-translate-y-0.5 transition cursor-grab active:cursor-grabbing mb-2.5 postit-note" class="group/card relative p-3 rounded-xl border ${colorClass} shadow-2xs hover:shadow-md hover:-translate-y-0.5 transition cursor-grab active:cursor-grabbing mb-2.5 postit-note"
> >
<!-- Realistische 3D Rode Punaise conform referentie linksboven -->
<div class="postit-pin" aria-hidden="true">
<svg viewBox="0 0 28 32" class="w-full h-full overflow-visible">
<defs>
<radialGradient id="boardPinHeadGrad" cx="35%" cy="30%" r="65%">
<stop offset="0%" stop-color="#ff8585"/>
<stop offset="35%" stop-color="#dc2626"/>
<stop offset="85%" stop-color="#991b1b"/>
<stop offset="100%" stop-color="#5a0b0b"/>
</radialGradient>
<linearGradient id="boardPinBodyGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#b91c1c"/>
<stop offset="40%" stop-color="#ef4444"/>
<stop offset="80%" stop-color="#991b1b"/>
<stop offset="100%" stop-color="#7f1d1d"/>
</linearGradient>
<linearGradient id="boardPinHighlight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
</defs>
<path d="M14 20 L21 30" stroke="#71717a" stroke-width="2.5" stroke-linecap="round"/>
<path d="M14 20 L20.5 29.5" stroke="#e4e4e7" stroke-width="1.2" stroke-linecap="round"/>
<ellipse cx="14" cy="19" rx="7" ry="2.8" fill="#7f1d1d"/>
<ellipse cx="14" cy="18" rx="6.5" ry="2.5" fill="url(#boardPinBodyGrad)"/>
<path d="M9 14 C9 17, 10 18.5, 14 18.5 C18 18.5, 19 17, 19 14 Z" fill="url(#boardPinBodyGrad)"/>
<circle cx="14" cy="11" r="7.5" fill="url(#boardPinHeadGrad)"/>
<ellipse cx="11.5" cy="8.5" rx="3" ry="1.8" fill="url(#boardPinHighlight)" transform="rotate(-30 11.5 8.5)"/>
</svg>
</div>
<div class="flex items-start justify-between gap-1.5 mb-1.5"> <div class="flex items-start justify-between gap-1.5 mb-1.5">
<h4 class="text-xs font-bold leading-snug line-clamp-2 break-words"> <h4 class="text-xs font-bold leading-snug line-clamp-2 break-words">
${escapeHtml(card.title)} ${escapeHtml(card.title)}
+97 -37
View File
@@ -87,55 +87,64 @@ $categories = [
?> ?>
<style> <style>
/* --- Realistische Echte Post-it Sticky Note Styling --- */ /* --- Realistische Echte Post-it Sticky Note Styling (Conform referentie linksboven) --- */
.postit-note { .postit-note {
position: relative; position: relative;
border-top-left-radius: 2px; border-radius: 2px 2px 2px 2px;
border-top-right-radius: 3px; /* Uitgesproken, zachte schaduw zoals in referentie */
border-bottom-right-radius: 4px;
border-bottom-left-radius: 14px;
box-shadow: box-shadow:
0 1px 1px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06),
0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 8px 16px -2px rgba(0, 0, 0, 0.12),
0 10px 18px -4px rgba(0, 0, 0, 0.12), 0 18px 28px -6px rgba(0, 0, 0, 0.14),
-2px 6px 8px -2px rgba(0, 0, 0, 0.08); 8px 16px 20px -4px rgba(0, 0, 0, 0.18);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease; transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
} }
.postit-note:hover { .postit-note:hover {
transform: translateY(-3px) rotate(-0.5deg); transform: translateY(-3px) rotate(-0.5deg);
box-shadow: box-shadow:
0 2px 4px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.08),
0 8px 12px -2px rgba(0, 0, 0, 0.12), 0 12px 22px -3px rgba(0, 0, 0, 0.16),
0 16px 24px -4px rgba(0, 0, 0, 0.16), 0 24px 34px -6px rgba(0, 0, 0, 0.18),
-4px 8px 12px -2px rgba(0, 0, 0, 0.12); 10px 20px 24px -4px rgba(0, 0, 0, 0.22);
} }
/* Echte sticky note curl & schaduw links onder */
/* De opkrullende flap rechtsonder met realistische 3D vouw en diepe slagschaduw */
.postit-note::after { .postit-note::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -1px; bottom: -1px;
left: -1px; right: -1px;
width: 38px;
height: 38px;
/* Subtiel opkrullend effect met highlight en slagschaduw */
background:
linear-gradient(135deg, transparent 48%, rgba(0, 0, 0, 0.08) 50%, rgba(0, 0, 0, 0.28) 60%, transparent 68%),
linear-gradient(225deg, #fef08a 0%, #facc15 48%, transparent 52%);
border-bottom-right-radius: 2px;
pointer-events: none;
z-index: 10;
box-shadow: 6px 6px 12px -2px rgba(0, 0, 0, 0.3);
clip-path: polygon(100% 0, 0 100%, 100% 100%);
}
/* Donkere modus aanpassing voor de opkrulflap */
.dark .postit-note::after {
background:
linear-gradient(135deg, transparent 48%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0.45) 60%, transparent 68%),
linear-gradient(225deg, #eab308 0%, #ca8a04 48%, transparent 52%);
box-shadow: 6px 6px 14px -2px rgba(0, 0, 0, 0.6);
}
/* 3D Rode Punaise (Pushpin) linksboven met naald en schaduw conform de afbeelding */
.postit-pin {
position: absolute;
top: -12px;
left: 28px;
width: 24px; width: 24px;
height: 24px; height: 24px;
background:
linear-gradient(135deg, rgba(0, 0, 0, 0.22) 0%, rgba(0, 0, 0, 0.08) 35%, transparent 45%),
linear-gradient(315deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.2) 40%, transparent 48%);
border-bottom-left-radius: 4px;
pointer-events: none;
z-index: 5;
}
/* Subtiele klevende plakrand aan de bovenkant */
.postit-note::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.04), transparent);
border-top-left-radius: 2px;
border-top-right-radius: 3px;
pointer-events: none; pointer-events: none;
z-index: 25;
filter: drop-shadow(4px 6px 4px rgba(0, 0, 0, 0.35));
} }
</style> </style>
@@ -238,6 +247,51 @@ $categories = [
oncontextmenu="openCardContextMenu(event, <?= $cardJson ?>); return false;" oncontextmenu="openCardContextMenu(event, <?= $cardJson ?>); return false;"
class="brainstorm-card postit-note group relative p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> transition-all duration-150 cursor-grab active:cursor-grabbing hover:-translate-y-1 flex flex-col justify-between" class="brainstorm-card postit-note group relative p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> transition-all duration-150 cursor-grab active:cursor-grabbing hover:-translate-y-1 flex flex-col justify-between"
> >
<!-- Realistische 3D Rode Punaise (Pushpin) met metalen pin & schaduw conform referentie linksboven -->
<div class="postit-pin" aria-hidden="true">
<svg viewBox="0 0 28 32" class="w-full h-full overflow-visible">
<defs>
<!-- Schaduw van de naald op het papier -->
<filter id="pinNeedleShadow" x="-20%" y="-20%" width="160%" height="160%">
<feDropShadow dx="3" dy="4" stdDeviation="1.5" flood-opacity="0.35"/>
</filter>
<!-- Radiaal verloop voor de bolle kop van de punaise -->
<radialGradient id="pinHeadGrad" cx="35%" cy="30%" r="65%">
<stop offset="0%" stop-color="#ff8585"/>
<stop offset="35%" stop-color="#dc2626"/>
<stop offset="85%" stop-color="#991b1b"/>
<stop offset="100%" stop-color="#5a0b0b"/>
</radialGradient>
<!-- Verloop voor de hals en basis ring van de punaise -->
<linearGradient id="pinBodyGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#b91c1c"/>
<stop offset="40%" stop-color="#ef4444"/>
<stop offset="80%" stop-color="#991b1b"/>
<stop offset="100%" stop-color="#7f1d1d"/>
</linearGradient>
<!-- Glans highlight op de kop -->
<linearGradient id="pinHighlight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Metalen naald die schuin in het papier prikt -->
<path d="M14 20 L21 30" stroke="#71717a" stroke-width="2.5" stroke-linecap="round" filter="url(#pinNeedleShadow)"/>
<path d="M14 20 L20.5 29.5" stroke="#e4e4e7" stroke-width="1.2" stroke-linecap="round"/>
<!-- Onderste rand / ring van de punaisekop -->
<ellipse cx="14" cy="19" rx="7" ry="2.8" fill="#7f1d1d"/>
<ellipse cx="14" cy="18" rx="6.5" ry="2.5" fill="url(#pinBodyGrad)"/>
<!-- Getailleerde taille / hals van de punaise -->
<path d="M9 14 C9 17, 10 18.5, 14 18.5 C18 18.5, 19 17, 19 14 Z" fill="url(#pinBodyGrad)"/>
<!-- Bolle kop van de punaise (rood met 3D glans) -->
<circle cx="14" cy="11" r="7.5" fill="url(#pinHeadGrad)"/>
<!-- Reflectie highlight linksboven op de ronde kop -->
<ellipse cx="11.5" cy="8.5" rx="3" ry="1.8" fill="url(#pinHighlight)" transform="rotate(-30 11.5 8.5)"/>
</svg>
</div>
<!-- Top: Checkbox voor selectie + Roadmap badge --> <!-- Top: Checkbox voor selectie + Roadmap badge -->
<div class="flex items-center justify-between gap-2 mb-2"> <div class="flex items-center justify-between gap-2 mb-2">
@@ -653,12 +707,18 @@ $categories = [
</div> </div>
<!-- Error State --> <!-- Error State -->
<div id="breakdownError" class="hidden p-4 rounded-xl bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 text-xs"> <div id="breakdownError" class="hidden p-4 rounded-xl bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 text-xs space-y-2">
<div class="flex items-center space-x-2 font-bold mb-1"> <div class="flex items-center space-x-2 font-bold">
<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400"></i> <i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400 flex-shrink-0"></i>
<span>Kon taken niet genereren</span> <span>Kon taken niet genereren</span>
</div> </div>
<p id="breakdownErrorMsg" class="text-[11px] text-rose-700 dark:text-rose-300"></p> <p id="breakdownErrorMsg" class="text-[11px] text-rose-700 dark:text-rose-300 leading-relaxed"></p>
<div class="pt-1">
<button type="button" onclick="regenerateBreakdown()" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-rose-600 hover:bg-rose-700 text-white text-xs font-semibold transition cursor-pointer">
<i data-lucide="refresh-cw" class="w-3 h-3"></i>
<span>Opnieuw proberen</span>
</button>
</div>
</div> </div>
<!-- Resultaat Container --> <!-- Resultaat Container -->
+75 -30
View File
@@ -83,7 +83,7 @@ function fociz_ai_enforce_privacy_firewall(array &$messages): array {
*/ */
function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings): array { function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings): array {
$apiKey = trim((string)($aiSettings['ai_api_key'] ?? '')); $apiKey = trim((string)($aiSettings['ai_api_key'] ?? ''));
$model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-3.6-flash'; $model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-2.0-flash';
$temperature = $options['temperature'] ?? 0.3; $temperature = $options['temperature'] ?? 0.3;
$maxTokens = max(512, (int)($options['max_tokens'] ?? 2048)); $maxTokens = max(512, (int)($options['max_tokens'] ?? 2048));
$jsonMode = !empty($options['json_mode']); $jsonMode = !empty($options['json_mode']);
@@ -157,34 +157,33 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
return trim($txt); return trim($txt);
}; };
// Bepaal fallback volgorde van endpoints/versies voor maximale compatibiliteit met alle Google AI Studio accounts // Normaliseer modelnaam en stel geldige kandidatenlijst op
$candidateUrls = []; $candidateUrls = [];
if (str_contains($cleanModel, '3.6-flash')) {
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; // Helper om endpoints op te bouwen voor v1beta en v1
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-3.6-flash:generateContent"; $addCandidatesFor = function(string $m) use (&$candidateUrls) {
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; $m = preg_replace('#^models/#', '', trim($m));
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"; if ($m === '') return;
} elseif (str_contains($cleanModel, '2.0-flash')) { $v1beta = "https://generativelanguage.googleapis.com/v1beta/models/{$m}:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; $v1 = "https://generativelanguage.googleapis.com/v1/models/{$m}:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent"; if (!in_array($v1beta, $candidateUrls, true)) {
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"; $candidateUrls[] = $v1beta;
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
} elseif (str_contains($cleanModel, '1.5-flash')) {
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent";
} elseif (str_contains($cleanModel, '1.5-pro')) {
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-pro:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
} else {
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/{$cleanModel}:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/{$cleanModel}:generateContent";
} }
if (!in_array($v1, $candidateUrls, true)) {
$candidateUrls[] = $v1;
}
};
// Voeg het geconfigureerde model toe als eerste kandidaat (tenzij het een niet-bestaand/fictief model is)
if (!empty($cleanModel) && !str_contains($cleanModel, '3.6-flash')) {
$addCandidatesFor($cleanModel);
}
// Voeg altijd de stabiele, snelle Google AI modellen toe als betrouwbare fallbacks/standaarden
$addCandidatesFor('gemini-2.0-flash');
$addCandidatesFor('gemini-1.5-flash');
$addCandidatesFor('gemini-1.5-pro');
$addCandidatesFor('gemini-2.5-flash');
$response = null; $response = null;
$lastError = null; $lastError = null;
@@ -335,6 +334,14 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
// Poging 2: json decode met raw // Poging 2: json decode met raw
$parsedJson = json_decode($rawContent, true); $parsedJson = json_decode($rawContent, true);
} }
// Poging 3: extraheer JSON substring {...} of [...] indien AI conversatietekst heeft toegevoegd
if ($parsedJson === null) {
if (preg_match('/\{(?:[^{}]|(?R))*\}/s', $cleanContent, $matchJson)) {
$parsedJson = json_decode($matchJson[0], true);
} elseif (preg_match('/\[(?:[^\[\]]|(?R))*\]/s', $cleanContent, $matchJson)) {
$parsedJson = json_decode($matchJson[0], true);
}
}
} }
$promptTokens = (int)($response['json']['usageMetadata']['promptTokenCount'] ?? 0); $promptTokens = (int)($response['json']['usageMetadata']['promptTokenCount'] ?? 0);
@@ -540,17 +547,55 @@ function fociz_ai_breakdown_idea(string $ideaContent, string $sessionTitle = '',
'user_id' => $userId, 'user_id' => $userId,
]); ]);
if (!$aiRes['success'] || empty($aiRes['json']['items'])) { if (!$aiRes['success']) {
error_log("[fociz_ai_breakdown_idea] AI request failed: " . ($aiRes['error'] ?? 'Unknown error'));
return [ return [
'success' => false, 'success' => false,
'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.', 'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.',
]; ];
} }
$parsed = $aiRes['json'] ?? null;
$items = [];
$summary = '';
if (is_array($parsed)) {
if (!empty($parsed['items']) && is_array($parsed['items'])) {
$items = $parsed['items'];
} elseif (!empty($parsed['tasks']) && is_array($parsed['tasks'])) {
$items = $parsed['tasks'];
} elseif (!empty($parsed['taken']) && is_array($parsed['taken'])) {
$items = $parsed['taken'];
} elseif (!empty($parsed['cards']) && is_array($parsed['cards'])) {
$items = $parsed['cards'];
} elseif (array_is_list($parsed)) {
$items = $parsed;
}
$summary = (string)($parsed['summary'] ?? '');
}
// Als JSON parsing toch leeg was, probeer regex fallback op ruwe content
if (empty($items) && !empty($aiRes['content'])) {
if (preg_match('/\[\s*\{.*\}\s*\]/s', $aiRes['content'], $mArr)) {
$decodedArr = json_decode($mArr[0], true);
if (is_array($decodedArr)) {
$items = $decodedArr;
}
}
}
if (empty($items)) {
error_log("[fociz_ai_breakdown_idea] Geen geldige items gevonden in response: " . substr((string)($aiRes['content'] ?? ''), 0, 500));
return [
'success' => false,
'error' => 'De AI gaf een antwoord maar kon de taken niet structureren. Probeer het nogmaals.',
];
}
return [ return [
'success' => true, 'success' => true,
'summary' => $aiRes['json']['summary'] ?? '', 'summary' => $summary ?: 'Dit idee is opgesplitst in behapbare deeltaken.',
'items' => $aiRes['json']['items'], 'items' => $items,
'model' => $aiRes['model'] ?? 'Gemini Pro', 'model' => $aiRes['model'] ?? 'Gemini Pro',
]; ];
} }
+148 -29
View File
@@ -186,16 +186,31 @@ try {
error_log("Fout bij ophalen taken: " . $e->getMessage()); error_log("Fout bij ophalen taken: " . $e->getMessage());
} }
// 3b. Haal alle reeds ingeplande externe items op (Jira keys & Outlook Mail IDs) // 3b. Haal alle reeds ingeplande externe items op (Jira keys & Outlook Mail IDs) inclusief taakgegevens voor herplannen
$plannedExternalIds = []; $plannedExternalIds = [];
try { try {
$stmtPlannedExt = $db->prepare(" $stmtPlannedExt = $db->prepare("
SELECT external_id FROM tasks SELECT t.id as task_id, t.external_id, t.title, t.priority, t.time_block_start, t.time_block_end,
WHERE user_id = ? AND external_id IS NOT NULL AND external_id != '' t.duration_minutes, dp.plan_date
FROM tasks t
LEFT JOIN daily_plans dp ON dp.id = t.daily_plan_id
WHERE t.user_id = ? AND t.external_id IS NOT NULL AND t.external_id != ''
ORDER BY t.id DESC
"); ");
$stmtPlannedExt->execute([$userId]); $stmtPlannedExt->execute([$userId]);
foreach ($stmtPlannedExt->fetchAll() as $row) { foreach ($stmtPlannedExt->fetchAll() as $row) {
$plannedExternalIds[$row['external_id']] = true; $extId = $row['external_id'];
if (!isset($plannedExternalIds[$extId])) {
$plannedExternalIds[$extId] = [
'task_id' => (int)$row['task_id'],
'title' => $row['title'],
'priority' => $row['priority'],
'time_block_start' => $row['time_block_start'] ? substr($row['time_block_start'], 0, 5) : '',
'time_block_end' => $row['time_block_end'] ? substr($row['time_block_end'], 0, 5) : '',
'duration_minutes' => (int)($row['duration_minutes'] ?: 30),
'date' => $row['plan_date'] ?: date('Y-m-d')
];
}
} }
} catch (Throwable $e) { } catch (Throwable $e) {
error_log("Fout bij ophalen planned external IDs: " . $e->getMessage()); error_log("Fout bij ophalen planned external IDs: " . $e->getMessage());
@@ -330,7 +345,7 @@ $availableWidgets = [
'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Dag/Weekoverzicht', 'category' => 'Agenda', 'icon' => 'calendar', 'enabled' => true, 'col_span' => 12, 'order' => 2, 'desc' => 'Gecombineerd weekoverzicht, dagselectie en uren-tijdlijn met time-blocking en AI'], 'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Dag/Weekoverzicht', 'category' => 'Agenda', 'icon' => 'calendar', 'enabled' => true, 'col_span' => 12, 'order' => 2, 'desc' => 'Gecombineerd weekoverzicht, dagselectie en uren-tijdlijn met time-blocking en AI'],
'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 3, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'], 'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 3, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'],
'outlook_mail' => ['id' => 'outlook_mail', 'title' => 'Outlook E-mail Widget', 'category' => 'Communicatie', 'icon' => 'mail', 'enabled' => true, 'col_span' => 6, 'order' => 4, 'desc' => 'Inkomende e-mails direct inzien en met 1 klik inplannen als taak'], 'outlook_mail' => ['id' => 'outlook_mail', 'title' => 'Outlook E-mail Widget', 'category' => 'Communicatie', 'icon' => 'mail', 'enabled' => true, 'col_span' => 6, 'order' => 4, 'desc' => 'Inkomende e-mails direct inzien en met 1 klik inplannen als taak'],
'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets & Wachtrijen', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets (DX/SD wachtrijen) met directe importknop'], 'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets met directe importknop'],
'calendar_meetings' => ['id' => 'calendar_meetings', 'title' => 'Agenda & Teams Meetings', 'category' => 'Agenda', 'icon' => 'video', 'enabled' => true, 'col_span' => 6, 'order' => 6, 'desc' => 'Vandaag geplande afspraken met 1-klik Teams deelnemen knop'], 'calendar_meetings' => ['id' => 'calendar_meetings', 'title' => 'Agenda & Teams Meetings', 'category' => 'Agenda', 'icon' => 'video', 'enabled' => true, 'col_span' => 6, 'order' => 6, 'desc' => 'Vandaag geplande afspraken met 1-klik Teams deelnemen knop'],
'scratchpad' => ['id' => 'scratchpad', 'title' => 'Dagnotities & Kladblok', 'category' => 'Productiviteit', 'icon' => 'edit-3', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Vrij notitieblok met automatische achtergrond-opslag'], 'scratchpad' => ['id' => 'scratchpad', 'title' => 'Dagnotities & Kladblok', 'category' => 'Productiviteit', 'icon' => 'edit-3', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Vrij notitieblok met automatische achtergrond-opslag'],
'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'], 'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'],
@@ -1478,7 +1493,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div> </div>
<?php elseif ($wId === 'jira_queue'): ?> <?php elseif ($wId === 'jira_queue'): ?>
<!-- Widget 5: Jira Tickets & Wachtrijen (Interactive v2.0) --> <!-- Widget 5: Jira Tickets (Interactive v2.0) -->
<div id="jiraPanel" class="fociz-card p-5 sm:p-6"> <div id="jiraPanel" class="fociz-card p-5 sm:p-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 mb-3 gap-2"> <div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 mb-3 gap-2">
<div class="flex items-center space-x-2.5"> <div class="flex items-center space-x-2.5">
@@ -1487,7 +1502,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div> </div>
<div> <div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider"> <h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Jira Wachtrijen & Tickets Jira Tickets
</h3> </h3>
</div> </div>
<span id="plannerJiraCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>"> <span id="plannerJiraCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>">
@@ -1559,7 +1574,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>')" onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>')"
data-jira-key="<?= htmlspecialchars($issue['key'] ?? '') ?>" data-jira-key="<?= htmlspecialchars($issue['key'] ?? '') ?>"
class="plan-btn-jira flex items-center space-x-1 text-[11px] <?= $isJiraPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : 'font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20' ?> px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer" class="plan-btn-jira flex items-center space-x-1 text-[11px] <?= $isJiraPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : 'font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20' ?> px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="<?= $isJiraPlanned ? 'Dit ticket is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)' : 'Plan in als dagtaak' ?>" title="<?= $isJiraPlanned ? 'Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)' : 'Plan in als dagtaak' ?>"
> >
<?php if ($isJiraPlanned): ?> <?php if ($isJiraPlanned): ?>
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i> <i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
@@ -1988,7 +2003,7 @@ if ($isAjaxRequest) {
<i data-lucide="calendar-plus" class="w-4 h-4"></i> <i data-lucide="calendar-plus" class="w-4 h-4"></i>
</div> </div>
<div> <div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Jira Ticket Inplannen</h3> <h3 class="text-sm font-bold text-slate-900 dark:text-white" id="jiraPlanModalTitle">Jira Ticket Inplannen</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="jiraPlanModalKey">—</p> <p class="text-[10px] text-slate-500 dark:text-slate-400" id="jiraPlanModalKey">—</p>
</div> </div>
</div> </div>
@@ -1997,6 +2012,22 @@ if ($isAjaxRequest) {
</button> </button>
</div> </div>
<!-- Hidden veld voor bestaande gekoppelde taak (bij herplannen) -->
<input type="hidden" id="jiraPlanTaskId" value="">
<!-- Banner: Al Ingepland (zichtbaar wanneer ticket reeds gepland is) -->
<div id="jiraPlanAlreadyScheduledBanner" class="hidden p-3 rounded-xl bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-300 dark:border-emerald-700 text-emerald-900 dark:text-emerald-200 text-xs">
<div class="flex items-start space-x-2">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0 mt-0.5"></i>
<div class="flex-1 min-w-0">
<p class="font-bold">Reeds ingepland in je dagplanning</p>
<p class="text-[11px] text-emerald-800 dark:text-emerald-300 mt-0.5" id="jiraPlanAlreadyScheduledDetails">
Ingepland op ...
</p>
</div>
</div>
</div>
<!-- Ticket samenvatting & Uitklapbare beschrijving --> <!-- Ticket samenvatting & Uitklapbare beschrijving -->
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5"> <div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5">
<div> <div>
@@ -2028,7 +2059,7 @@ if ($isAjaxRequest) {
<div class="space-y-3.5"> <div class="space-y-3.5">
<!-- Titel voor de agenda / dagtaak --> <!-- Titel voor de agenda / dagtaak -->
<div> <div>
<label for="jiraPlanCustomTitle" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5"> <label for="jiraPlanCustomTitle" id="jiraPlanCustomTitleLabel" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
Hoe het in de agenda komt (Titel) Hoe het in de agenda komt (Titel)
</label> </label>
<input type="text" id="jiraPlanCustomTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="Oppakken Jira item ..."> <input type="text" id="jiraPlanCustomTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="Oppakken Jira item ...">
@@ -4612,7 +4643,8 @@ plannerJiraAssigned.forEach(iss => {
if (iss && iss.key && !plannerTicketStore.has(iss.key)) plannerTicketStore.set(iss.key, iss); if (iss && iss.key && !plannerTicketStore.has(iss.key)) plannerTicketStore.set(iss.key, iss);
}); });
const plannedExternalIdsSet = new Set(<?= json_encode(array_keys($plannedExternalIds)) ?>); const plannedExternalTasksMap = <?= json_encode($plannedExternalIds, JSON_UNESCAPED_UNICODE) ?> || {};
const plannedExternalIdsSet = new Set(Object.keys(plannedExternalTasksMap));
let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue
function isIssueInQueue(iss, qCode) { function isIssueInQueue(iss, qCode) {
@@ -4720,7 +4752,7 @@ function filterPlannerJira() {
onclick="quickImportJiraByKey(this, '${keyEsc}')" onclick="quickImportJiraByKey(this, '${keyEsc}')"
data-jira-key="${keyEsc}" data-jira-key="${keyEsc}"
class="plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer" class="plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="Dit ticket is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)" title="Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)"
> >
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i> <i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
<span>Ingepland</span> <span>Ingepland</span>
@@ -4926,15 +4958,50 @@ function openJiraPlanModal(issueKey) {
if (descChevron) descChevron.classList.remove('rotate-180'); if (descChevron) descChevron.classList.remove('rotate-180');
if (descLabel) descLabel.textContent = 'Inhoud van het item bekijken'; if (descLabel) descLabel.textContent = 'Inhoud van het item bekijken';
// Titel hoe het in de agenda komt: standaard "Oppakken Jira item <issue.key>" // Controleer of dit ticket al is ingepland
const existingTask = (typeof plannedExternalTasksMap !== 'undefined' && plannedExternalTasksMap[issue.key]) ? plannedExternalTasksMap[issue.key] : null;
const isAlreadyPlanned = Boolean(existingTask);
const taskIdInput = document.getElementById('jiraPlanTaskId');
if (taskIdInput) {
taskIdInput.value = (existingTask && existingTask.task_id) ? existingTask.task_id : '';
}
const modalTitle = document.getElementById('jiraPlanModalTitle');
const scheduledBanner = document.getElementById('jiraPlanAlreadyScheduledBanner');
const scheduledDetails = document.getElementById('jiraPlanAlreadyScheduledDetails');
const titleInput = document.getElementById('jiraPlanCustomTitle'); const titleInput = document.getElementById('jiraPlanCustomTitle');
const dateInput = document.getElementById('jiraPlanDate');
const startTimeInput = document.getElementById('jiraPlanStartTime');
if (isAlreadyPlanned && existingTask) {
if (modalTitle) modalTitle.textContent = 'Jira Ticket Herplannen';
if (scheduledBanner) scheduledBanner.classList.remove('hidden');
if (scheduledDetails) {
const timeInfo = existingTask.time_block_start
? `om ${escapeHtml(existingTask.time_block_start)}${existingTask.time_block_end ? ' - ' + escapeHtml(existingTask.time_block_end) : ''} (${existingTask.duration_minutes}m)`
: `(zonder specifiek tijdblok, ${existingTask.duration_minutes}m)`;
scheduledDetails.innerHTML = `Momenteel ingepland op <strong>${escapeHtml(existingTask.date)}</strong> ${timeInfo}. Pas hieronder de datum, tijd of duur aan om te herplannen.`;
}
if (titleInput) {
titleInput.value = existingTask.title || `Oppakken Jira item ${issue.key}`;
}
if (dateInput) {
dateInput.value = existingTask.date || targetDate;
}
setJiraPlanDuration(existingTask.duration_minutes || 30);
if (existingTask.priority) {
document.getElementById('jiraPlanPriority').value = existingTask.priority;
}
if (startTimeInput) {
startTimeInput.value = existingTask.time_block_start || '';
}
} else {
if (modalTitle) modalTitle.textContent = 'Jira Ticket Inplannen';
if (scheduledBanner) scheduledBanner.classList.add('hidden');
if (titleInput) { if (titleInput) {
titleInput.value = `Oppakken Jira item ${issue.key}`; titleInput.value = `Oppakken Jira item ${issue.key}`;
} }
// Datum: standaard de actieve planner datum of vandaag
const dateInput = document.getElementById('jiraPlanDate');
const targetDate = (typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10);
if (dateInput) { if (dateInput) {
dateInput.value = targetDate; dateInput.value = targetDate;
} }
@@ -4960,7 +5027,10 @@ function openJiraPlanModal(issueKey) {
const now = new Date(); const now = new Date();
const todayStr = now.toISOString().slice(0, 10); const todayStr = now.toISOString().slice(0, 10);
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00'; const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
document.getElementById('jiraPlanStartTime').value = suggestTime; if (startTimeInput) {
startTimeInput.value = suggestTime;
}
}
const notice = document.getElementById('jiraPlanOpenBlockNotice'); const notice = document.getElementById('jiraPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden'); if (notice) notice.classList.add('hidden');
@@ -4970,8 +5040,14 @@ function openJiraPlanModal(issueKey) {
const submitBtn = document.getElementById('btnSubmitJiraPlan'); const submitBtn = document.getElementById('btnSubmitJiraPlan');
if (submitBtn) { if (submitBtn) {
submitBtn.disabled = false; submitBtn.disabled = false;
if (isAlreadyPlanned) {
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
submitBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i><span>Herplannen</span>';
} else {
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>'; submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
} }
}
const modal = document.getElementById('jiraPlanModal'); const modal = document.getElementById('jiraPlanModal');
modal.classList.remove('hidden'); modal.classList.remove('hidden');
@@ -5164,7 +5240,37 @@ async function submitJiraPlanModal() {
btn.disabled = true; btn.disabled = true;
} }
const existingTaskId = parseInt(document.getElementById('jiraPlanTaskId')?.value || '0', 10);
const isUpdate = existingTaskId > 0;
try { try {
let res, data;
let finalStartTime = startTime;
let finalEndTime = '';
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
finalEndTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
}
if (isUpdate) {
// Bestaande taak bijwerken / verplaatsen naar eventueel andere datum
res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
task_id: existingTaskId,
title: finalTitle,
priority: priority,
duration_minutes: duration,
time_block_start: finalStartTime,
time_block_end: finalEndTime,
date: planDate
})
});
data = await res.json();
} else {
// Nieuwe taak toevoegen
const payload = { const payload = {
title: finalTitle, title: finalTitle,
priority: priority, priority: priority,
@@ -5174,32 +5280,45 @@ async function submitJiraPlanModal() {
duration_minutes: duration, duration_minutes: duration,
date: planDate date: planDate
}; };
if (startTime) { if (finalStartTime) {
payload.time_block_start = startTime; payload.time_block_start = finalStartTime;
// Bereken eindtijd payload.time_block_end = finalEndTime;
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
payload.time_block_end = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
} }
const res = await fetch('/api/planner.php?action=add_task', { res = await fetch('/api/planner.php?action=add_task', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
const data = await res.json(); data = await res.json();
}
if (data.success) { if (data.success) {
closeJiraPlanModal(); closeJiraPlanModal();
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> ingepland als dagtaak`, 'success'); const actionVerb = isUpdate ? 'geherpland' : 'ingepland';
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> succesvol ${actionVerb}`, 'success');
// Live update: voeg toe aan plannedExternalIdsSet en transformeer knoppen naar groen 'Ingepland' // Live update van cache en sets
const savedTaskId = isUpdate ? existingTaskId : (data.task ? data.task.id : existingTaskId);
if (typeof plannedExternalTasksMap !== 'undefined') {
plannedExternalTasksMap[issue.key] = {
task_id: savedTaskId,
title: finalTitle,
priority: priority,
time_block_start: finalStartTime || '',
time_block_end: finalEndTime || '',
duration_minutes: duration,
date: planDate
};
}
if (typeof plannedExternalIdsSet !== 'undefined') { if (typeof plannedExternalIdsSet !== 'undefined') {
plannedExternalIdsSet.add(issue.key); plannedExternalIdsSet.add(issue.key);
} }
// Knoppen bijwerken
document.querySelectorAll(`.plan-btn-jira[data-jira-key="${issue.key}"]`).forEach(btn => { document.querySelectorAll(`.plan-btn-jira[data-jira-key="${issue.key}"]`).forEach(btn => {
btn.className = "plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"; btn.className = "plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer";
btn.title = "Dit ticket is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)"; btn.title = "Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)";
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>'; btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>';
}); });
const detailPlanBtn = document.getElementById('btnJiraDetailPlan'); const detailPlanBtn = document.getElementById('btnJiraDetailPlan');