Compare commits
2 Commits
0bafe0afe9
...
29874f184b
| Author | SHA1 | Date | |
|---|---|---|---|
| 29874f184b | |||
| f63ea740bf |
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user