Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ad186e95f | |||
| 8714038db2 | |||
| bc33c0bd1a |
@@ -894,23 +894,26 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="grid grid-cols-12 gap-3 px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800">
|
<div class="grid grid-cols-12 gap-3 px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800">
|
||||||
<div class="col-span-2 sm:col-span-1 text-center">Icoon</div>
|
<div class="col-span-3 sm:col-span-2 text-center">Icoon</div>
|
||||||
<div class="col-span-3 sm:col-span-2">Systeemcode</div>
|
<div class="col-span-3 sm:col-span-2">Systeemcode</div>
|
||||||
<div class="col-span-5 sm:col-span-6">Zichtbare Titel / Omschrijving</div>
|
<div class="col-span-4 sm:col-span-6">Zichtbare Titel / Omschrijving</div>
|
||||||
<div class="col-span-1 text-center">Actief</div>
|
<div class="col-span-1 text-center">Actief</div>
|
||||||
<div class="col-span-1 text-right">Verwijder</div>
|
<div class="col-span-1 text-right">Verwijder</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php foreach ($calendarBlockReasons as $idx => $reason): ?>
|
<?php foreach ($calendarBlockReasons as $idx => $reason): ?>
|
||||||
<div class="grid grid-cols-12 gap-3 items-center p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17] border border-slate-200/70 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
<div class="grid grid-cols-12 gap-3 items-center p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17] border border-slate-200/70 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
||||||
<div class="col-span-2 sm:col-span-1">
|
<div class="col-span-3 sm:col-span-2 flex items-center space-x-1.5">
|
||||||
<input type="text" name="reasons[<?= $idx ?>][icon]" value="<?= htmlspecialchars($reason['icon'] ?? '📅') ?>" class="w-full text-center text-lg py-1 px-1 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-1 focus:ring-amber-500">
|
<button type="button" onclick="openIconPickerFor('reason_icon_<?= $idx ?>', 'reason_icon_preview_<?= $idx ?>')" class="w-10 h-10 flex-shrink-0 text-xl flex items-center justify-center bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 hover:border-amber-400 dark:hover:border-amber-500 rounded-xl transition cursor-pointer shadow-2xs hover:scale-105" title="Klik om een icoon te kiezen uit de lijst">
|
||||||
|
<span id="reason_icon_preview_<?= $idx ?>"><?= htmlspecialchars($reason['icon'] ?? '📅') ?></span>
|
||||||
|
</button>
|
||||||
|
<input type="text" id="reason_icon_<?= $idx ?>" name="reasons[<?= $idx ?>][icon]" value="<?= htmlspecialchars($reason['icon'] ?? '📅') ?>" oninput="document.getElementById('reason_icon_preview_<?= $idx ?>').innerText = this.value || '📅'" class="w-12 text-center text-xs py-2 px-1 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-1 focus:ring-amber-500">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-3 sm:col-span-2">
|
<div class="col-span-3 sm:col-span-2">
|
||||||
<input type="hidden" name="reasons[<?= $idx ?>][code]" value="<?= htmlspecialchars($reason['code']) ?>">
|
<input type="hidden" name="reasons[<?= $idx ?>][code]" value="<?= htmlspecialchars($reason['code']) ?>">
|
||||||
<span class="font-mono text-xs font-bold text-slate-700 dark:text-slate-300 px-2.5 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 inline-block truncate w-full shadow-2xs"><?= htmlspecialchars($reason['code']) ?></span>
|
<span class="font-mono text-xs font-bold text-slate-700 dark:text-slate-300 px-2.5 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 inline-block truncate w-full shadow-2xs"><?= htmlspecialchars($reason['code']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-5 sm:col-span-6">
|
<div class="col-span-4 sm:col-span-6">
|
||||||
<input type="text" name="reasons[<?= $idx ?>][title]" value="<?= htmlspecialchars($reason['title']) ?>" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-amber-500" required>
|
<input type="text" name="reasons[<?= $idx ?>][title]" value="<?= htmlspecialchars($reason['title']) ?>" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-amber-500" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-1 text-center">
|
<div class="col-span-1 text-center">
|
||||||
@@ -929,20 +932,28 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
<!-- Nieuwe reden toevoegen regel -->
|
<!-- Nieuwe reden toevoegen regel -->
|
||||||
<div class="p-4 rounded-2xl bg-amber-50/40 dark:bg-amber-950/20 border border-dashed border-amber-300/80 dark:border-amber-800/60 space-y-3">
|
<div class="p-4 rounded-2xl bg-amber-50/40 dark:bg-amber-950/20 border border-dashed border-amber-300/80 dark:border-amber-800/60 space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center space-x-2 text-xs font-bold text-amber-900 dark:text-amber-200">
|
<div class="flex items-center space-x-2 text-xs font-bold text-amber-900 dark:text-amber-200">
|
||||||
<i data-lucide="plus-circle" class="w-4 h-4 text-amber-600 dark:text-amber-400"></i>
|
<i data-lucide="plus-circle" class="w-4 h-4 text-amber-600 dark:text-amber-400"></i>
|
||||||
<span>Nieuwe Blokkadereden Toevoegen</span>
|
<span>Nieuwe Blokkadereden Toevoegen</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="text-[10px] text-amber-700 dark:text-amber-400">Klik op de icoonknop om uit de standaard iconenset te kiezen</span>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-3 flex items-center space-x-1.5">
|
||||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Icoon (Emoji)</label>
|
<button type="button" onclick="openIconPickerFor('new_icon_input', 'new_icon_preview')" class="w-10 h-10 flex-shrink-0 text-xl flex items-center justify-center bg-white dark:bg-[#111624] border border-amber-300 dark:border-amber-700 hover:border-amber-500 rounded-xl transition cursor-pointer shadow-2xs hover:scale-105" title="Kies een icoon uit de lijst">
|
||||||
<input type="text" name="new_icon" placeholder="🎯" class="w-full text-center text-lg py-1 px-1 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-1 focus:ring-amber-500">
|
<span id="new_icon_preview">🎯</span>
|
||||||
|
</button>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Icoon (Kies of typ)</label>
|
||||||
|
<input type="text" id="new_icon_input" name="new_icon" value="🎯" placeholder="🎯" oninput="document.getElementById('new_icon_preview').innerText = this.value || '📅'" class="w-full text-center text-xs py-1.5 px-2 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-1 focus:ring-amber-500">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-3">
|
||||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Code (bv. STUDIEDAG)</label>
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Code (bv. STUDIEDAG)</label>
|
||||||
<input type="text" name="new_code" placeholder="BIJV_CURSUS" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-mono text-slate-900 dark:text-white uppercase focus:outline-none focus:ring-1 focus:ring-amber-500">
|
<input type="text" name="new_code" placeholder="BIJV_CURSUS" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-mono text-slate-900 dark:text-white uppercase focus:outline-none focus:ring-1 focus:ring-amber-500">
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-7">
|
<div class="sm:col-span-6">
|
||||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Zichtbare Titel</label>
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Zichtbare Titel</label>
|
||||||
<input type="text" name="new_title" placeholder="Bijv. Studiedag / Opleiding" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-amber-500">
|
<input type="text" name="new_title" placeholder="Bijv. Studiedag / Opleiding" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-amber-500">
|
||||||
</div>
|
</div>
|
||||||
@@ -1920,6 +1931,52 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Icoon Kiezer voor Agenda Blokkades -->
|
||||||
|
<div id="iconPickerModal" class="fixed inset-0 z-[99999] bg-slate-950/70 backdrop-blur-xs hidden items-center justify-center p-4">
|
||||||
|
<div class="fociz-card p-6 w-full max-w-lg shadow-2xl space-y-4 max-h-[85vh] flex flex-col animate-in fade-in zoom-in-95 duration-100">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||||||
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<div class="w-8 h-8 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center">
|
||||||
|
<i data-lucide="smile" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Kies een Icoon</h3>
|
||||||
|
<p class="text-[10px] text-slate-500 dark:text-slate-400">Selecteer een standaard icoon voor de dagblokkadereden</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="closeIconPicker()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||||
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zoekbalk & Categorie Tabs -->
|
||||||
|
<div class="space-y-2.5 flex-shrink-0">
|
||||||
|
<div class="relative">
|
||||||
|
<i data-lucide="search" class="w-3.5 h-3.5 absolute left-3 top-3 text-slate-400"></i>
|
||||||
|
<input type="text" id="iconPickerSearch" placeholder="Zoek op categorie of naam (bijv. vakantie, ziek, focus, kantoor, studie)..." oninput="filterIconPicker(this.value)" class="w-full pl-8 pr-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-amber-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-1.5 overflow-x-auto no-scrollbar py-0.5" id="iconPickerCategoryTabs">
|
||||||
|
<!-- Dynamische categorieknoppen -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid met Iconen -->
|
||||||
|
<div id="iconPickerGrid" class="flex-1 overflow-y-auto p-1.5 grid grid-cols-6 sm:grid-cols-8 gap-2 min-h-[220px] max-h-[320px] rounded-xl bg-slate-50/50 dark:bg-[#0c0f17]/50 border border-slate-200/50 dark:border-slate-800/50">
|
||||||
|
<!-- Wordt via JS gevuld met grote, klikbare iconen -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0 text-xs">
|
||||||
|
<span class="text-[11px] text-slate-400">Tip: Je kunt ook zelf een aangepaste emoji typen.</span>
|
||||||
|
<button type="button" onclick="closeIconPicker()" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold transition cursor-pointer">
|
||||||
|
Sluiten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ADMIN_TABS = ['users', 'calendar', 'microsoft', 'jira', 'ai', 'security', 'system'];
|
const ADMIN_TABS = ['users', 'calendar', 'microsoft', 'jira', 'ai', 'security', 'system'];
|
||||||
|
|
||||||
@@ -2128,9 +2185,154 @@ async function testAdminAiConnection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Agenda Blokkades Icoon Kiezer ---
|
||||||
|
let currentIconTargetInputId = null;
|
||||||
|
let currentIconTargetPreviewId = null;
|
||||||
|
let currentIconCategory = 'all';
|
||||||
|
|
||||||
|
const ICON_PICKER_DATA = {
|
||||||
|
'Vakantie & Vrij': ['🏖️', '✈️', '🌴', '⛺', '☀️', '⛵', '🎒', '🗺️', '🍹', '🏔️', '🚗', '🛳️', '🏕️', '🕶️', '🏄', '🏊', '⛷️', '🚴', '🎣', '🌅', '🌌', '🌍'],
|
||||||
|
'Werk & Focus': ['🎯', '📋', '💻', '📊', '📈', '🏢', '💼', '📁', '✍️', '💡', '⚡', '🔍', '📅', '🗓️', '⏰', '⏳', '🔒', '🛠️', '📞', '🤝', '📌', '📑', '🖊️', '🖥️', '⌨️', '📂'],
|
||||||
|
'Ziekte & Zorg': ['🤒', '🏥', '💊', '🩺', '🩹', '🛌', '🧑⚕️', '🦷', '🧘', '🍵', '💉', '😷', '🤧', '🚑', '🌡️', '🧑🦽', '🧴', '🍎', '❤️🩹'],
|
||||||
|
'Studie & Opleiding': ['🎓', '📚', '📖', '🏫', '🧑🏫', '📝', '🧠', '🔬', '📐', '📜', '🏆', '🎨', '🧪', '🔭', '💡', '🎒'],
|
||||||
|
'Persoonlijk & Verlof': ['👶', '💍', '🏠', '🚚', '🎂', '🎉', '🕊️', '⚖️', '👥', '☕', '🍴', '👨👩👧', '🎈', '🎁', '🐶', '🐱', '🧹', '🪴', '🛒', '🔑', '💤']
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.toString().replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openIconPickerFor(inputId, previewId) {
|
||||||
|
currentIconTargetInputId = inputId;
|
||||||
|
currentIconTargetPreviewId = previewId;
|
||||||
|
|
||||||
|
const modal = document.getElementById('iconPickerModal');
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
currentIconCategory = 'all';
|
||||||
|
renderIconCategoryTabs();
|
||||||
|
renderIconGrid();
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
|
||||||
|
if (window.lucide && typeof lucide.createIcons === 'function') {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const searchInput = document.getElementById('iconPickerSearch');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = '';
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeIconPicker() {
|
||||||
|
const modal = document.getElementById('iconPickerModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
currentIconTargetInputId = null;
|
||||||
|
currentIconTargetPreviewId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIcon(icon) {
|
||||||
|
if (currentIconTargetInputId) {
|
||||||
|
const input = document.getElementById(currentIconTargetInputId);
|
||||||
|
if (input) {
|
||||||
|
input.value = icon;
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentIconTargetPreviewId) {
|
||||||
|
const preview = document.getElementById(currentIconTargetPreviewId);
|
||||||
|
if (preview) preview.innerText = icon;
|
||||||
|
}
|
||||||
|
closeIconPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIconCategoryTabs() {
|
||||||
|
const container = document.getElementById('iconPickerCategoryTabs');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<button type="button" onclick="switchIconCategory('all')" class="px-2.5 py-1 rounded-lg text-xs font-semibold whitespace-nowrap transition cursor-pointer ${currentIconCategory === 'all' ? 'bg-amber-600 text-white shadow-xs' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'}">
|
||||||
|
Alle
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
Object.keys(ICON_PICKER_DATA).forEach(cat => {
|
||||||
|
const isActive = currentIconCategory === cat;
|
||||||
|
html += `
|
||||||
|
<button type="button" onclick="switchIconCategory('${escapeHtml(cat)}')" class="px-2.5 py-1 rounded-lg text-xs font-semibold whitespace-nowrap transition cursor-pointer ${isActive ? 'bg-amber-600 text-white shadow-xs' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'}">
|
||||||
|
${escapeHtml(cat)}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchIconCategory(category) {
|
||||||
|
currentIconCategory = category;
|
||||||
|
renderIconCategoryTabs();
|
||||||
|
const searchInput = document.getElementById('iconPickerSearch');
|
||||||
|
const query = searchInput ? searchInput.value : '';
|
||||||
|
renderIconGrid(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIconGrid(filterText = '') {
|
||||||
|
const grid = document.getElementById('iconPickerGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
let iconsToDisplay = [];
|
||||||
|
const query = filterText.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (currentIconCategory === 'all') {
|
||||||
|
Object.entries(ICON_PICKER_DATA).forEach(([cat, icons]) => {
|
||||||
|
if (!query || cat.toLowerCase().includes(query)) {
|
||||||
|
iconsToDisplay.push(...icons);
|
||||||
|
} else {
|
||||||
|
icons.forEach(ic => {
|
||||||
|
if (ic.includes(query)) iconsToDisplay.push(ic);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const catIcons = ICON_PICKER_DATA[currentIconCategory] || [];
|
||||||
|
if (!query) {
|
||||||
|
iconsToDisplay = catIcons;
|
||||||
|
} else {
|
||||||
|
iconsToDisplay = catIcons.filter(ic => ic.includes(query) || currentIconCategory.toLowerCase().includes(query));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iconsToDisplay = Array.from(new Set(iconsToDisplay));
|
||||||
|
|
||||||
|
if (iconsToDisplay.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="col-span-full py-8 text-center text-xs text-slate-400">Geen iconen gevonden voor deze zoekopdracht.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = iconsToDisplay.map(ic => `
|
||||||
|
<button type="button" onclick="selectIcon('${ic}')" class="h-11 flex items-center justify-center text-2xl rounded-xl bg-white hover:bg-amber-50 dark:bg-slate-900 dark:hover:bg-amber-950/40 border border-slate-200/80 hover:border-amber-400 dark:border-slate-800 dark:hover:border-amber-500 hover:scale-110 transition duration-150 cursor-pointer shadow-2xs group" title="${ic}">
|
||||||
|
<span class="group-hover:scale-110 transition-transform duration-150">${ic}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterIconPicker(query) {
|
||||||
|
renderIconGrid(query);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
closeJiraFilterModal();
|
closeJiraFilterModal();
|
||||||
|
closeIconPicker();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+2
-2
@@ -395,9 +395,9 @@ try {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) {
|
if ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) {
|
||||||
$aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings);
|
$aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 500, 'temperature' => 0.1], $testSettings);
|
||||||
} else {
|
} else {
|
||||||
$aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings);
|
$aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 500, 'temperature' => 0.1], $testSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
$latency = round((microtime(true) - $t0) * 1000);
|
$latency = round((microtime(true) - $t0) * 1000);
|
||||||
|
|||||||
+11
-7
@@ -121,14 +121,14 @@ try {
|
|||||||
// 6. E-mail direct inplannen in Fociz dagplanning
|
// 6. E-mail direct inplannen in Fociz dagplanning
|
||||||
if ($action === 'plan_email') {
|
if ($action === 'plan_email') {
|
||||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||||
$messageId = trim($input['message_id'] ?? '');
|
$messageId = trim($input['message_id'] ?? $input['mail_id'] ?? '');
|
||||||
$subject = trim($input['subject'] ?? 'E-mail actiepunt');
|
$subject = trim($input['title'] ?? $input['subject'] ?? 'E-mail actiepunt');
|
||||||
$planDate = trim($input['plan_date'] ?? date('Y-m-d'));
|
$planDate = trim($input['plan_date'] ?? $input['date'] ?? date('Y-m-d'));
|
||||||
$timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null;
|
$timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null;
|
||||||
$durationMinutes = (int)($input['duration_minutes'] ?? 30);
|
$durationMinutes = (int)($input['duration_minutes'] ?? 30);
|
||||||
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
|
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
|
||||||
$webLink = trim($input['web_link'] ?? 'https://outlook.office.com/mail/');
|
$webLink = trim($input['web_link'] ?? 'https://outlook.office.com/mail/');
|
||||||
$sender = trim($input['sender'] ?? '');
|
$sender = trim($input['sender'] ?? $input['sender_name'] ?? '');
|
||||||
|
|
||||||
if (empty($subject)) {
|
if (empty($subject)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Onderwerp is verplicht.']);
|
echo json_encode(['success' => false, 'error' => 'Onderwerp is verplicht.']);
|
||||||
@@ -137,8 +137,8 @@ try {
|
|||||||
|
|
||||||
if ($durationMinutes <= 0) $durationMinutes = 30;
|
if ($durationMinutes <= 0) $durationMinutes = 30;
|
||||||
|
|
||||||
$timeBlockEnd = null;
|
$timeBlockEnd = !empty($input['time_block_end']) ? trim($input['time_block_end']) : null;
|
||||||
if (!empty($timeBlockStart)) {
|
if (!empty($timeBlockStart) && empty($timeBlockEnd)) {
|
||||||
try {
|
try {
|
||||||
$dtEnd = new DateTime("{$planDate} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam'));
|
$dtEnd = new DateTime("{$planDate} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam'));
|
||||||
$dtEnd->modify("+{$durationMinutes} minutes");
|
$dtEnd->modify("+{$durationMinutes} minutes");
|
||||||
@@ -155,10 +155,14 @@ try {
|
|||||||
$stmtPlan->execute([$userId, $planDate]);
|
$stmtPlan->execute([$userId, $planDate]);
|
||||||
$planId = (int)$db->lastInsertId();
|
$planId = (int)$db->lastInsertId();
|
||||||
|
|
||||||
|
// Als er een custom titel is ingevoerd, gebruik die direct, anders maak een nette titel
|
||||||
|
$taskTitle = $subject;
|
||||||
|
if (isset($input['subject']) && !isset($input['title'])) {
|
||||||
$taskTitle = "✉️ " . $subject;
|
$taskTitle = "✉️ " . $subject;
|
||||||
if (!empty($sender)) {
|
if (!empty($sender) && strpos($taskTitle, $sender) === false) {
|
||||||
$taskTitle .= " (van " . $sender . ")";
|
$taskTitle .= " (van " . $sender . ")";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$stmtTask = $db->prepare("
|
$stmtTask = $db->prepare("
|
||||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, source, external_id, external_url)
|
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, source, external_id, external_url)
|
||||||
|
|||||||
+47
-4
@@ -271,20 +271,22 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$description = isset($data['description']) ? trim((string)$data['description']) : ($existingTask['description'] ?? null);
|
||||||
|
|
||||||
if ($newPlanId) {
|
if ($newPlanId) {
|
||||||
$stmtUpdate = $db->prepare("
|
$stmtUpdate = $db->prepare("
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?, daily_plan_id = ?
|
SET title = ?, description = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?, daily_plan_id = ?
|
||||||
WHERE id = ? AND user_id = ?
|
WHERE id = ? AND user_id = ?
|
||||||
");
|
");
|
||||||
$stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $newPlanId, $taskId, $user['id']]);
|
$stmtUpdate->execute([$title, $description, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $newPlanId, $taskId, $user['id']]);
|
||||||
} else {
|
} else {
|
||||||
$stmtUpdate = $db->prepare("
|
$stmtUpdate = $db->prepare("
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?
|
SET title = ?, description = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?
|
||||||
WHERE id = ? AND user_id = ?
|
WHERE id = ? AND user_id = ?
|
||||||
");
|
");
|
||||||
$stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]);
|
$stmtUpdate->execute([$title, $description, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -645,6 +647,47 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10b. Volledige taakdetails ophalen inclusief subtaken en dagplan informatie
|
||||||
|
if ($action === 'get_task_details') {
|
||||||
|
$taskId = (int)($_GET['task_id'] ?? ($_POST['task_id'] ?? 0));
|
||||||
|
|
||||||
|
if (!$taskId) {
|
||||||
|
echo json_encode(['error' => 'Taak-ID ontbreekt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, dp.plan_date, dp.focus_goal
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN daily_plans dp ON t.daily_plan_id = dp.id
|
||||||
|
WHERE t.id = ? AND t.user_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$taskId, $user['id']]);
|
||||||
|
$task = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$task) {
|
||||||
|
echo json_encode(['error' => 'Taak niet gevonden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtaken ophalen
|
||||||
|
$stmtSub = $db->prepare("
|
||||||
|
SELECT id, task_id, title, is_completed, position, created_at
|
||||||
|
FROM task_subtasks
|
||||||
|
WHERE task_id = ? AND user_id = ?
|
||||||
|
ORDER BY position ASC, created_at ASC
|
||||||
|
");
|
||||||
|
$stmtSub->execute([$taskId, $user['id']]);
|
||||||
|
$subtasks = $stmtSub->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'task' => $task,
|
||||||
|
'subtasks' => $subtasks,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// 11. Zoek eerstvolgende vrije open blok binnen werktijden (Release v2.1)
|
// 11. Zoek eerstvolgende vrije open blok binnen werktijden (Release v2.1)
|
||||||
if ($action === 'find_next_free_slot') {
|
if ($action === 'find_next_free_slot') {
|
||||||
$rawInput = json_decode(file_get_contents('php://input'), true);
|
$rawInput = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|||||||
+43
-12
@@ -83,9 +83,9 @@ 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-1.5-pro';
|
$model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-3.6-flash';
|
||||||
$temperature = $options['temperature'] ?? 0.3;
|
$temperature = $options['temperature'] ?? 0.3;
|
||||||
$maxTokens = $options['max_tokens'] ?? 2048;
|
$maxTokens = max(512, (int)($options['max_tokens'] ?? 2048));
|
||||||
$jsonMode = !empty($options['json_mode']);
|
$jsonMode = !empty($options['json_mode']);
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
@@ -140,34 +140,56 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
|
|
||||||
$cleanModel = preg_replace('#^models/#', '', $model);
|
$cleanModel = preg_replace('#^models/#', '', $model);
|
||||||
|
|
||||||
|
// Helper om tekst uit Gemini candidate te extraheren
|
||||||
|
$extractCandidateText = function(?array $candidate): string {
|
||||||
|
if (!$candidate || empty($candidate['content']['parts'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$txt = '';
|
||||||
|
foreach ($candidate['content']['parts'] as $part) {
|
||||||
|
if (!empty($part['text']) && empty($part['thought'])) {
|
||||||
|
$txt .= $part['text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($txt === '' && !empty($candidate['content']['parts'][0]['text'])) {
|
||||||
|
$txt = $candidate['content']['parts'][0]['text'];
|
||||||
|
}
|
||||||
|
return trim($txt);
|
||||||
|
};
|
||||||
|
|
||||||
// Bepaal fallback volgorde van endpoints/versies voor maximale compatibiliteit met alle Google AI Studio accounts
|
// Bepaal fallback volgorde van endpoints/versies voor maximale compatibiliteit met alle Google AI Studio accounts
|
||||||
$candidateUrls = [];
|
$candidateUrls = [];
|
||||||
if (str_contains($cleanModel, '3.6-flash')) {
|
if (str_contains($cleanModel, '3.6-flash')) {
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-3.6-flash:generateContent";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-3.6-flash:generateContent";
|
||||||
} elseif (str_contains($cleanModel, '2.0-flash')) {
|
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
|
||||||
} elseif (str_contains($cleanModel, '1.5-flash')) {
|
} elseif (str_contains($cleanModel, '2.0-flash')) {
|
||||||
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
||||||
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent";
|
||||||
|
$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-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";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent";
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-001:generateContent";
|
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent";
|
|
||||||
} elseif (str_contains($cleanModel, '1.5-pro')) {
|
} 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/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-1.5-pro-latest:generateContent";
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent";
|
||||||
} else {
|
} else {
|
||||||
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1beta/models/{$cleanModel}:generateContent";
|
$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-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";
|
$candidateUrls[] = "https://generativelanguage.googleapis.com/v1/models/{$cleanModel}:generateContent";
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = null;
|
$response = null;
|
||||||
$lastError = null;
|
$lastError = null;
|
||||||
$successfulModel = $cleanModel;
|
$successfulModel = $cleanModel;
|
||||||
|
$finalText = '';
|
||||||
|
|
||||||
for ($i = 0; $i < count($candidateUrls); $i++) {
|
for ($i = 0; $i < count($candidateUrls); $i++) {
|
||||||
$baseUrl = $candidateUrls[$i];
|
$baseUrl = $candidateUrls[$i];
|
||||||
@@ -182,13 +204,18 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
'timeout' => 20,
|
'timeout' => 20,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($res['status'] === 200 && !empty($res['json']['candidates'][0]['content']['parts'][0]['text'])) {
|
if ($res['status'] === 200 && !empty($res['json']['candidates'][0])) {
|
||||||
|
$extracted = $extractCandidateText($res['json']['candidates'][0]);
|
||||||
|
// Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS)
|
||||||
|
if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) {
|
||||||
$response = $res;
|
$response = $res;
|
||||||
|
$finalText = ($extracted !== '') ? $extracted : 'OK';
|
||||||
if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) {
|
if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) {
|
||||||
$successfulModel = $mName[1];
|
$successfulModel = $mName[1];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$msg = $res['json']['error']['message'] ?? ($res['error'] ?? '');
|
$msg = $res['json']['error']['message'] ?? ($res['error'] ?? '');
|
||||||
$lastError = $msg;
|
$lastError = $msg;
|
||||||
@@ -260,9 +287,13 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
'timeout' => 20,
|
'timeout' => 20,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($dynRes['status'] === 200 && !empty($dynRes['json']['candidates'][0]['content']['parts'][0]['text'])) {
|
if ($dynRes['status'] === 200 && !empty($dynRes['json']['candidates'][0])) {
|
||||||
|
$extracted = $extractCandidateText($dynRes['json']['candidates'][0]);
|
||||||
|
if ($extracted !== '' || !empty($dynRes['json']['candidates'][0]['finishReason'])) {
|
||||||
$response = $dynRes;
|
$response = $dynRes;
|
||||||
|
$finalText = ($extracted !== '') ? $extracted : 'OK';
|
||||||
$successfulModel = $fallbackModel;
|
$successfulModel = $fallbackModel;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$response = $dynRes;
|
$response = $dynRes;
|
||||||
}
|
}
|
||||||
@@ -270,7 +301,7 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$response || $response['status'] !== 200 || empty($response['json']['candidates'][0]['content']['parts'][0]['text'])) {
|
if (!$response || $response['status'] !== 200 || $finalText === '') {
|
||||||
$errMsg = null;
|
$errMsg = null;
|
||||||
if (!empty($response['json']['error']['message'])) {
|
if (!empty($response['json']['error']['message'])) {
|
||||||
$errMsg = $response['json']['error']['message'];
|
$errMsg = $response['json']['error']['message'];
|
||||||
@@ -288,7 +319,7 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$rawContent = trim($response['json']['candidates'][0]['content']['parts'][0]['text']);
|
$rawContent = $finalText;
|
||||||
|
|
||||||
// Indien Gemini het als markdown code block wrapped heeft (bv. ```json ... ```), schoon dit op
|
// Indien Gemini het als markdown code block wrapped heeft (bv. ```json ... ```), schoon dit op
|
||||||
if (preg_match('/^```(?:json)?\s*(.*?)\s*```$/s', $rawContent, $m)) {
|
if (preg_match('/^```(?:json)?\s*(.*?)\s*```$/s', $rawContent, $m)) {
|
||||||
|
|||||||
+1049
-96
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user