1891 lines
106 KiB
PHP
1891 lines
106 KiB
PHP
<?php
|
||
/**
|
||
* Fociz Dagplanning, Focus Modus & Time-blocking
|
||
* Gekoppeld aan Microsoft Outlook 365 (Agenda) en Atlassian Jira (Tickets)
|
||
*/
|
||
|
||
// Controleer of dit een AJAX verzoek is voor een geruisloze datumwisseling
|
||
$isAjaxRequest = (!empty($_GET['ajax']) && $_GET['ajax'] === '1');
|
||
|
||
if ($isAjaxRequest) {
|
||
require_once __DIR__ . '/includes/auth.php';
|
||
$currentUser = require_auth(true);
|
||
} else {
|
||
require_once __DIR__ . '/includes/header.php';
|
||
}
|
||
|
||
require_once __DIR__ . '/includes/settings.php';
|
||
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||
require_once __DIR__ . '/includes/integrations/jira.php';
|
||
|
||
$db = get_db_connection();
|
||
$userId = (int)($currentUser['id'] ?? 0);
|
||
$today = date('Y-m-d');
|
||
$selectedDate = (!empty($_GET['date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date'])) ? $_GET['date'] : $today;
|
||
$isTodaySelected = ($selectedDate === $today);
|
||
$selectedTs = strtotime($selectedDate);
|
||
|
||
// Haal organisatie werkdag- en AI-standaarden op (Release v1.4 & v1.5)
|
||
$workdaySettings = get_workday_settings();
|
||
$aiSettings = get_ai_settings();
|
||
|
||
// Bereken weekgrenzen (Maandag t/m Zondag) rondom de geselecteerde datum
|
||
$dayOfWeek = (int)date('N', $selectedTs); // 1 = Maandag, 7 = Zondag
|
||
$mondayTs = strtotime('-' . ($dayOfWeek - 1) . ' days', $selectedTs);
|
||
$sundayTs = strtotime('+' . (7 - $dayOfWeek) . ' days', $selectedTs);
|
||
$weekStart = date('Y-m-d', $mondayTs);
|
||
$weekEnd = date('Y-m-d', $sundayTs);
|
||
$weekNumber = (int)date('W', $selectedTs);
|
||
$yearNumber = (int)date('o', $selectedTs);
|
||
|
||
// URLs voor weeknavigatie
|
||
$prevWeekDate = date('Y-m-d', strtotime('-7 days', $mondayTs));
|
||
$nextWeekDate = date('Y-m-d', strtotime('+7 days', $mondayTs));
|
||
|
||
// Nederlandse vertalingen voor datums
|
||
$dutchShortDays = [1 => 'Ma', 2 => 'Di', 3 => 'Wo', 4 => 'Do', 5 => 'Vr', 6 => 'Za', 7 => 'Zo'];
|
||
$dutchFullDays = [1 => 'Maandag', 2 => 'Dinsdag', 3 => 'Woensdag', 4 => 'Donderdag', 5 => 'Vrijdag', 6 => 'Zaterdag', 7 => 'Zondag'];
|
||
$dutchMonths = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'];
|
||
$dutchShortMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
|
||
|
||
$formattedSelectedDate = $dutchFullDays[(int)date('N', $selectedTs)] . ' ' . date('j', $selectedTs) . ' ' . $dutchMonths[(int)date('n', $selectedTs)] . ' ' . date('Y', $selectedTs);
|
||
$formattedWeekRange = date('j', $mondayTs) . ' ' . $dutchShortMonths[(int)date('n', $mondayTs)] . ' – ' . date('j', $sundayTs) . ' ' . $dutchShortMonths[(int)date('n', $sundayTs)] . ' ' . date('Y', $sundayTs);
|
||
|
||
// 1. Haal integratiestatussen op
|
||
$userIntegrations = [];
|
||
try {
|
||
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ?");
|
||
$stmtInt->execute([$userId]);
|
||
foreach ($stmtInt->fetchAll() as $row) {
|
||
$userIntegrations[$row['provider']] = $row;
|
||
}
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen integraties: " . $e->getMessage());
|
||
ensure_tables_exist($db);
|
||
}
|
||
|
||
$isOutlookConnected = !empty($userIntegrations['OUTLOOK']['access_token']) && empty($userIntegrations['OUTLOOK']['is_demo_mode']);
|
||
$outlookIntegrationSettings = json_decode($userIntegrations['OUTLOOK']['settings'] ?? '{}', true) ?: [];
|
||
$syncIntervalSeconds = isset($outlookIntegrationSettings['sync_interval_seconds']) ? (int)$outlookIntegrationSettings['sync_interval_seconds'] : 30;
|
||
|
||
$weekOutlookEvents = [];
|
||
$isLiveOutlook = false;
|
||
|
||
if ($isOutlookConnected) {
|
||
try {
|
||
// Haal afspraken op voor het volledige datumbereik van de week
|
||
$weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd);
|
||
$isLiveOutlook = true;
|
||
|
||
// Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data
|
||
microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents);
|
||
} catch (Throwable $e) {
|
||
error_log("Outlook synchronisatiefout in planner: " . $e->getMessage());
|
||
$isLiveOutlook = false;
|
||
$weekOutlookEvents = [];
|
||
}
|
||
} else {
|
||
// Demo data indien Microsoft Outlook nog niet gekoppeld is
|
||
$weekOutlookEvents = [
|
||
[
|
||
'id' => 'evt-1',
|
||
'subject' => 'Voorbeeld: Team Standup & Sync',
|
||
'date' => $today,
|
||
'start' => '09:00',
|
||
'end' => '09:30',
|
||
'location' => 'Microsoft Teams',
|
||
'meeting_url' => 'https://teams.microsoft.com',
|
||
'source' => 'SANDBOX',
|
||
],
|
||
[
|
||
'id' => 'evt-2',
|
||
'subject' => 'Voorbeeld: Roadmap Review met Stakeholders',
|
||
'date' => $today,
|
||
'start' => '11:00',
|
||
'end' => '12:00',
|
||
'location' => 'Vergaderruimte A & Teams',
|
||
'meeting_url' => 'https://teams.microsoft.com',
|
||
'source' => 'SANDBOX',
|
||
],
|
||
[
|
||
'id' => 'evt-3',
|
||
'subject' => 'Voorbeeld: Architectuur & DB Evaluatie',
|
||
'date' => $today,
|
||
'start' => '14:30',
|
||
'end' => '15:30',
|
||
'location' => 'Online Teams Call',
|
||
'meeting_url' => 'https://teams.microsoft.com',
|
||
'source' => 'SANDBOX',
|
||
]
|
||
];
|
||
}
|
||
|
||
// 2. Haal dagplanning op of maak aan voor $selectedDate
|
||
$dailyPlan = null;
|
||
try {
|
||
$stmtPlan = $db->prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?");
|
||
$stmtPlan->execute([$userId, $selectedDate]);
|
||
$dailyPlan = $stmtPlan->fetch();
|
||
|
||
if (!$dailyPlan) {
|
||
$defaultGoal = ($selectedDate === $today) ? 'Mijn belangrijkste doel van vandaag behalen' : 'Focusdoel voor deze dag bepalen';
|
||
$stmtCreatePlan = $db->prepare("
|
||
INSERT INTO daily_plans (user_id, plan_date, focus_goal)
|
||
VALUES (?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)
|
||
");
|
||
$stmtCreatePlan->execute([$userId, $selectedDate, $defaultGoal]);
|
||
$planId = (int)$db->lastInsertId();
|
||
$dailyPlan = ['id' => $planId, 'plan_date' => $selectedDate, 'focus_goal' => $defaultGoal, 'notes' => ''];
|
||
}
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij dagplanning ophalen/aanmaken: " . $e->getMessage());
|
||
ensure_tables_exist($db);
|
||
$dailyPlan = ['id' => 0, 'plan_date' => $selectedDate, 'focus_goal' => 'Mijn belangrijkste doel van vandaag behalen', 'notes' => ''];
|
||
}
|
||
|
||
$planId = !empty($dailyPlan['id']) ? (int)$dailyPlan['id'] : 0;
|
||
|
||
// 3. Haal taken op voor de geselecteerde dag (vers bijgewerkt na eventuele reconciliatie)
|
||
$tasks = [];
|
||
try {
|
||
$stmtTasks = $db->prepare("
|
||
SELECT * FROM tasks
|
||
WHERE user_id = ? AND (daily_plan_id = ? " . ($selectedDate === $today ? "OR daily_plan_id IS NULL" : "") . ")
|
||
ORDER BY is_completed ASC, priority ASC, time_block_start ASC, created_at DESC
|
||
");
|
||
$stmtTasks->execute([$userId, $planId]);
|
||
$tasks = $stmtTasks->fetchAll() ?: [];
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen taken: " . $e->getMessage());
|
||
}
|
||
|
||
// 4. Haal weekstatistieken voor taken op (aantal per dag)
|
||
$weekTaskStats = [];
|
||
try {
|
||
$stmtWeekTasks = $db->prepare("
|
||
SELECT dp.plan_date, COUNT(t.id) as total_tasks, SUM(CASE WHEN t.is_completed = 1 THEN 1 ELSE 0 END) as completed_tasks
|
||
FROM daily_plans dp
|
||
LEFT JOIN tasks t ON t.daily_plan_id = dp.id AND t.user_id = ?
|
||
WHERE dp.user_id = ? AND dp.plan_date BETWEEN ? AND ?
|
||
GROUP BY dp.plan_date
|
||
");
|
||
$stmtWeekTasks->execute([$userId, $userId, $weekStart, $weekEnd]);
|
||
foreach ($stmtWeekTasks->fetchAll() as $row) {
|
||
$weekTaskStats[$row['plan_date']] = [
|
||
'total' => (int)$row['total_tasks'],
|
||
'completed' => (int)$row['completed_tasks'],
|
||
];
|
||
}
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij weekstatistieken ophalen: " . $e->getMessage());
|
||
}
|
||
|
||
// Groepeer Outlook afspraken per datum
|
||
$outlookEventsByDate = [];
|
||
foreach ($weekOutlookEvents as $evt) {
|
||
$evtDate = $evt['date'] ?? $selectedDate;
|
||
$outlookEventsByDate[$evtDate][] = $evt;
|
||
}
|
||
|
||
// 7 Dagen van de week samenstellen voor de weekselector
|
||
$weekDays = [];
|
||
for ($i = 0; $i < 7; $i++) {
|
||
$dayTs = strtotime("+{$i} days", $mondayTs);
|
||
$dayDate = date('Y-m-d', $dayTs);
|
||
$dayN = (int)date('N', $dayTs);
|
||
$dayM = (int)date('n', $dayTs);
|
||
$tStat = $weekTaskStats[$dayDate] ?? ['total' => 0, 'completed' => 0];
|
||
$mCount = count($outlookEventsByDate[$dayDate] ?? []);
|
||
|
||
$weekDays[] = [
|
||
'date' => $dayDate,
|
||
'day_num' => date('j', $dayTs),
|
||
'short_name' => $dutchShortDays[$dayN],
|
||
'full_name' => $dutchFullDays[$dayN],
|
||
'month_short' => $dutchShortMonths[$dayM],
|
||
'is_today' => ($dayDate === $today),
|
||
'is_selected' => ($dayDate === $selectedDate),
|
||
'task_count' => $tStat['total'],
|
||
'completed_count' => $tStat['completed'],
|
||
'meeting_count' => $mCount,
|
||
];
|
||
}
|
||
|
||
// 5. Haal Jira issues op voor de snelle import widget
|
||
$jiraIssues = [];
|
||
$isJiraLive = false;
|
||
try {
|
||
$jiraData = jira_get_user_issues($userId);
|
||
$jiraIssues = $jiraData['issues'] ?? [];
|
||
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen Jira issues: " . $e->getMessage());
|
||
}
|
||
|
||
// Filter Outlook events die afkomstig zijn van Fociz taken voor de geselecteerde dag om duplicaten op de tijdlijn te voorkomen
|
||
$dayOutlookEvents = $outlookEventsByDate[$selectedDate] ?? [];
|
||
$syncedTaskExternalIds = [];
|
||
foreach ($tasks as $t) {
|
||
if (!empty($t['external_id'])) {
|
||
$syncedTaskExternalIds[$t['external_id']] = true;
|
||
}
|
||
}
|
||
$displayedOutlookEvents = [];
|
||
foreach ($dayOutlookEvents as $evt) {
|
||
if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) {
|
||
continue;
|
||
}
|
||
$displayedOutlookEvents[] = $evt;
|
||
}
|
||
|
||
// Bereken initiële status-hash voor achtergrondsynchronisatie
|
||
$dayEventsForHash = [];
|
||
foreach ($displayedOutlookEvents as $e) {
|
||
$dayEventsForHash[] = [
|
||
'id' => $e['id'] ?? '',
|
||
'start' => $e['start'] ?? '',
|
||
'end' => $e['end'] ?? '',
|
||
'subject' => $e['subject'] ?? '',
|
||
];
|
||
}
|
||
$tasksForHash = array_map(function($t) {
|
||
return [
|
||
'id' => $t['id'],
|
||
'title' => $t['title'],
|
||
'time_block_start' => $t['time_block_start'],
|
||
'time_block_end' => $t['time_block_end'],
|
||
'is_completed' => $t['is_completed'],
|
||
];
|
||
}, $tasks);
|
||
$initialSyncHash = md5(json_encode([
|
||
'events' => $dayEventsForHash,
|
||
'tasks' => $tasksForHash
|
||
]));
|
||
|
||
// 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4)
|
||
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
|
||
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:30';
|
||
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
|
||
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
|
||
$lunchHour = (int)explode(':', $lunchStart)[0];
|
||
|
||
$startHour = (int)explode(':', $workdayStartStr)[0];
|
||
$endHourParts = explode(':', $workdayEndStr);
|
||
$endHour = (int)$endHourParts[0];
|
||
if (!empty($endHourParts[1]) && (int)$endHourParts[1] > 0) {
|
||
$endHour++;
|
||
}
|
||
if ($startHour < 6) $startHour = 6;
|
||
if ($endHour > 23) $endHour = 23;
|
||
|
||
foreach ($displayedOutlookEvents as $e) {
|
||
if (!empty($e['start'])) {
|
||
$h = (int)explode(':', $e['start'])[0];
|
||
if ($h < $startHour && $h >= 6) $startHour = $h;
|
||
if ($h > $endHour && $h <= 23) $endHour = $h + 1;
|
||
}
|
||
}
|
||
foreach ($tasks as $t) {
|
||
if (!empty($t['time_block_start'])) {
|
||
$h = (int)explode(':', $t['time_block_start'])[0];
|
||
if ($h < $startHour && $h >= 6) $startHour = $h;
|
||
if ($h > $endHour && $h <= 23) $endHour = $h + 1;
|
||
}
|
||
}
|
||
$hours = [];
|
||
for ($h = $startHour; $h <= $endHour; $h++) {
|
||
$hours[] = sprintf('%02d:00', $h);
|
||
}
|
||
?>
|
||
<style>
|
||
.line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
</style>
|
||
|
||
<div id="plannerContentArea" class="space-y-6 max-w-7xl mx-auto transition-opacity duration-200">
|
||
<?php if ($isAjaxRequest) ob_start(); ?>
|
||
<!-- Top Banner: Datum, Hoofddoel & Integraties Status -->
|
||
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden">
|
||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||
<div>
|
||
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
|
||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||
<span><?= htmlspecialchars($formattedSelectedDate) ?></span>
|
||
<?php if ($isTodaySelected): ?>
|
||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300 ring-1 ring-indigo-500/30">VANDAAG</span>
|
||
<?php else: ?>
|
||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-800/80 text-slate-600 dark:text-slate-400">GEKOZEN DAG</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
|
||
Dagplanning & Structuur
|
||
</h1>
|
||
</div>
|
||
|
||
<!-- Integratie Status Badges -->
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<?php if ($isOutlookConnected): ?>
|
||
<div id="outlookSyncBadge" class="flex items-center space-x-2 px-3.5 py-1.5 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200/80 dark:border-sky-500/20 text-sky-800 dark:text-sky-300 text-xs font-semibold shadow-2xs transition">
|
||
<span id="outlookSyncDot" class="w-2 h-2 rounded-full bg-sky-500"></span>
|
||
<span id="outlookSyncStatusText">Outlook 365 Live</span>
|
||
<span id="outlookLastSyncText" class="text-[10px] text-sky-600/80 dark:text-sky-400/70 font-mono hidden sm:inline">(synced <span id="syncTimeDisplay"><?= date('H:i') ?></span>)</span>
|
||
<button id="manualSyncBtn" onclick="triggerManualSync()" title="Nu handmatig synchroniseren met Outlook" class="p-0.5 hover:bg-sky-100 dark:hover:bg-sky-500/20 text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-white rounded transition cursor-pointer">
|
||
<i id="manualSyncIcon" data-lucide="refresh-cw" class="w-3 h-3"></i>
|
||
</button>
|
||
</div>
|
||
<?php else: ?>
|
||
<a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition shadow-2xs">
|
||
<i data-lucide="link-2" class="w-3.5 h-3.5 text-sky-500"></i>
|
||
<span>Koppel Outlook</span>
|
||
</a>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($isJiraLive): ?>
|
||
<a href="#jiraPanel" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition shadow-2xs">
|
||
<span class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></span>
|
||
<span>Jira: <?= count($jiraIssues) ?> tickets</span>
|
||
</a>
|
||
<?php else: ?>
|
||
<a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition shadow-2xs">
|
||
<i data-lucide="trello" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||
<span>Koppel Jira</span>
|
||
</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dagdoel Focus Spotlight Blok -->
|
||
<div class="mt-6 p-4 sm:p-5 rounded-2xl bg-gradient-to-r from-indigo-500/[0.08] via-sky-500/[0.04] to-transparent dark:from-indigo-500/[0.14] dark:via-sky-500/[0.06] dark:to-transparent border border-indigo-500/20 dark:border-indigo-500/30 shadow-xs relative overflow-hidden">
|
||
<div class="flex items-center space-x-2 mb-2">
|
||
<div class="w-6 h-6 rounded-lg bg-indigo-600 text-white flex items-center justify-center shadow-xs">
|
||
<i data-lucide="target" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<label for="focusGoalInput" class="text-xs font-bold text-indigo-950 dark:text-indigo-200 uppercase tracking-wider">
|
||
Focus van de Dag • <?= htmlspecialchars($dutchFullDays[(int)date('N', $selectedTs)]) ?>
|
||
</label>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<div class="relative flex-1">
|
||
<input
|
||
type="text"
|
||
id="focusGoalInput"
|
||
value="<?= htmlspecialchars($dailyPlan['focus_goal'] ?? '') ?>"
|
||
placeholder="Wat moet er deze dag absoluut gebeuren voor succes?"
|
||
class="w-full bg-white/95 dark:bg-[#0c0f17]/95 border border-slate-200/90 dark:border-slate-800/90 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500 font-medium transition shadow-2xs"
|
||
>
|
||
</div>
|
||
<button
|
||
onclick="saveGoal()"
|
||
id="saveGoalBtn"
|
||
class="px-5 py-2.5 bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 text-white font-semibold text-xs rounded-xl transition flex items-center space-x-1.5 shadow-sm shadow-indigo-600/30 flex-shrink-0 cursor-pointer"
|
||
>
|
||
<i data-lucide="check" class="w-4 h-4 stroke-[2.5]"></i>
|
||
<span>Opslaan</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Week Navigatie & Dagselector -->
|
||
<div class="fociz-card p-5 sm:p-6">
|
||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 mb-4 border-b border-slate-100 dark:border-slate-800/60">
|
||
<!-- Navigatie controls -->
|
||
<div class="flex items-center space-x-2">
|
||
<a href="planner.php?date=<?= $prevWeekDate ?>" onclick="switchPlannerDate('<?= $prevWeekDate ?>', event)" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Vorige week (Maandag <?= date('j M', strtotime($prevWeekDate)) ?>)">
|
||
<i data-lucide="chevron-left" class="w-4 h-4"></i>
|
||
<span class="hidden sm:inline">Vorige week</span>
|
||
</a>
|
||
|
||
<a href="planner.php?date=<?= $today ?>" onclick="switchPlannerDate('<?= $today ?>', event)" class="px-3.5 py-1.5 rounded-xl <?= $isTodaySelected ? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-bold shadow-xs' : 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 font-semibold shadow-2xs' ?> text-xs transition" title="Spring direct naar vandaag">
|
||
Vandaag
|
||
</a>
|
||
|
||
<a href="planner.php?date=<?= $nextWeekDate ?>" onclick="switchPlannerDate('<?= $nextWeekDate ?>', event)" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Volgende week (Maandag <?= date('j M', strtotime($nextWeekDate)) ?>)">
|
||
<span class="hidden sm:inline">Volgende week</span>
|
||
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Week Titel & Datumrange -->
|
||
<div class="text-center md:text-left">
|
||
<div class="text-xs font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
|
||
Week <?= $weekNumber ?> • <?= $yearNumber ?>
|
||
</div>
|
||
<div class="text-base font-black text-slate-900 dark:text-white">
|
||
<?= htmlspecialchars($formattedWeekRange) ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Directe datumselector -->
|
||
<div class="flex items-center space-x-2 justify-end">
|
||
<label for="jumpDateInput" class="text-xs text-slate-500 dark:text-slate-400 font-medium hidden sm:inline">Datum:</label>
|
||
<input
|
||
type="date"
|
||
id="jumpDateInput"
|
||
value="<?= htmlspecialchars($selectedDate) ?>"
|
||
onchange="if(this.value) switchPlannerDate(this.value, event);"
|
||
class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer shadow-2xs"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 7 Dagen Weekstrook -->
|
||
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-3">
|
||
<?php foreach ($weekDays as $wd): ?>
|
||
<a
|
||
href="planner.php?date=<?= $wd['date'] ?>"
|
||
onclick="switchPlannerDate('<?= $wd['date'] ?>', event)"
|
||
class="fociz-card-interactive group relative flex flex-col p-3.5 rounded-2xl border transition-all text-left <?= $wd['is_selected']
|
||
? 'bg-gradient-to-b from-indigo-500/15 via-indigo-500/5 to-transparent dark:from-indigo-500/25 dark:via-indigo-500/10 dark:to-transparent border-indigo-500 ring-2 ring-indigo-500/40 shadow-sm'
|
||
: 'bg-slate-50/70 dark:bg-[#0c0f17]/50 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 hover:bg-white dark:hover:bg-[#111624] shadow-2xs' ?>"
|
||
>
|
||
<div class="flex items-center justify-between mb-1.5">
|
||
<span class="text-[11px] font-bold uppercase tracking-wider <?= $wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-500 dark:text-slate-400' ?>">
|
||
<?= $wd['short_name'] ?>
|
||
</span>
|
||
<?php if ($wd['is_today']): ?>
|
||
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-indigo-600 text-white shadow-xs">
|
||
Nu
|
||
</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="flex items-baseline space-x-1.5 mb-3">
|
||
<span class="text-2xl font-black <?= $wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-900 dark:text-white' ?>">
|
||
<?= $wd['day_num'] ?>
|
||
</span>
|
||
<span class="text-[11px] font-semibold text-slate-400 dark:text-slate-500 uppercase">
|
||
<?= $wd['month_short'] ?>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Mini chips voor taken en meetings op deze dag -->
|
||
<div class="flex flex-col gap-1.5 mt-auto pt-2 border-t border-slate-200/60 dark:border-slate-800/60 text-[10px]">
|
||
<div class="flex items-center justify-between px-2 py-1 rounded-lg bg-white/70 dark:bg-slate-900/60 border border-slate-200/50 dark:border-slate-800/50 text-slate-600 dark:text-slate-400">
|
||
<span class="flex items-center space-x-1">
|
||
<i data-lucide="check-square" class="w-3 h-3 text-indigo-500"></i>
|
||
<span>Taken</span>
|
||
</span>
|
||
<span class="font-bold <?= $wd['task_count'] > 0 ? ($wd['completed_count'] === $wd['task_count'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-800 dark:text-slate-200') : 'text-slate-400' ?>">
|
||
<?= $wd['completed_count'] ?>/<?= $wd['task_count'] ?>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between px-2 py-1 rounded-lg bg-sky-50/70 dark:bg-sky-500/10 border border-sky-200/50 dark:border-sky-500/20 text-sky-800 dark:text-sky-300">
|
||
<span class="flex items-center space-x-1">
|
||
<i data-lucide="calendar" class="w-3 h-3 text-sky-500"></i>
|
||
<span>Agenda</span>
|
||
</span>
|
||
<span class="font-bold">
|
||
<?= $wd['meeting_count'] ?>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Grid: Timer + Takenlijst + Jira (Links) & Timeblocking (Rechts) -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||
|
||
<!-- Linker kolom: Focus Timer & Taken -->
|
||
<div class="lg:col-span-7 space-y-6 min-w-0">
|
||
|
||
<!-- Focus Modus Timer Workstation -->
|
||
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center space-x-3">
|
||
<div class="w-10 h-10 rounded-2xl bg-amber-500/10 border border-amber-500/20 text-amber-500 flex items-center justify-center font-bold shadow-xs">
|
||
<i data-lucide="flame" class="w-5 h-5"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider">Focus Modus</h3>
|
||
<p class="text-[11px] text-slate-500 dark:text-slate-400">Pomodoro werkstation & concentratie</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex bg-slate-100/90 dark:bg-[#0c0f17]/90 p-1 rounded-xl border border-slate-200/80 dark:border-slate-800/80 text-xs">
|
||
<button onclick="setTimerPreset(<?= (int)$workdaySettings['pomodoro_focus'] ?>)" id="btnPresetFocus" class="px-3 py-1.5 rounded-lg font-bold bg-indigo-600 text-white shadow-xs transition"><?= (int)$workdaySettings['pomodoro_focus'] ?>m Focus</button>
|
||
<button onclick="setTimerPreset(<?= (int)$workdaySettings['pomodoro_deep'] ?>)" id="btnPresetDeep" class="px-3 py-1.5 rounded-lg font-semibold text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition"><?= (int)$workdaySettings['pomodoro_deep'] ?>m Deep</button>
|
||
<button onclick="setTimerPreset(<?= (int)$workdaySettings['pomodoro_break'] ?>)" id="btnPresetBreak" class="px-3 py-1.5 rounded-lg font-semibold text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition"><?= (int)$workdaySettings['pomodoro_break'] ?>m Pauze</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-center my-6 relative">
|
||
<div id="timerDisplay" class="text-6xl sm:text-7xl font-black font-mono tracking-tight text-slate-900 dark:text-white mb-2"><?= sprintf('%02d:00', (int)$workdaySettings['pomodoro_focus']) ?></div>
|
||
<div class="min-h-[26px] text-xs text-slate-500 dark:text-slate-400 truncate max-w-md mx-auto flex items-center justify-center space-x-1.5 px-3.5 py-1 rounded-full bg-slate-100/80 dark:bg-slate-950/80 border border-slate-200/70 dark:border-slate-800/70 w-fit">
|
||
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||
<span class="text-indigo-600 dark:text-indigo-400 font-semibold">Doeltaak:</span>
|
||
<span id="activeTaskDisplay" class="text-slate-800 dark:text-slate-200 font-bold truncate max-w-[260px]">Kies een taak hieronder</span>
|
||
</div>
|
||
|
||
<div class="w-full bg-slate-100 dark:bg-slate-950 h-2 rounded-full overflow-hidden mt-5 border border-slate-200/80 dark:border-slate-800">
|
||
<div id="timerProgress" class="h-full bg-gradient-to-r from-indigo-500 via-sky-500 to-indigo-600 transition-all duration-300 rounded-full shadow-[0_0_12px_rgba(99,102,241,0.35)]" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button id="timerToggleBtn" onclick="toggleTimer()" class="flex items-center space-x-2 px-7 py-3 rounded-xl font-bold text-sm bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 text-white transition shadow-md shadow-indigo-600/30 hover:scale-[1.02] active:scale-[0.98] cursor-pointer">
|
||
<i data-lucide="play" class="w-4 h-4 fill-current"></i>
|
||
<span id="timerToggleText">Start Focus</span>
|
||
</button>
|
||
<button onclick="resetTimer()" class="p-3 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 transition border border-slate-200 dark:border-slate-700/80 cursor-pointer shadow-2xs" title="Reset">
|
||
<i data-lucide="rotate-ccw" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Takenlijst Blok -->
|
||
<div class="fociz-card p-6 sm:p-7">
|
||
<div class="flex items-center justify-between mb-5 pb-3 border-b border-slate-100 dark:border-slate-800/60">
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-2">
|
||
<span>Dagtaken & Prioriteiten</span>
|
||
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300 ring-1 ring-indigo-500/20"><?= count($tasks) ?></span>
|
||
</h3>
|
||
</div>
|
||
|
||
<button onclick="toggleAddTaskForm()" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-sm shadow-indigo-600/25 cursor-pointer">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
<span>Nieuwe Taak</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Nieuwe taak formulier (verborgen by default) -->
|
||
<div id="addTaskForm" class="hidden mb-5 p-5 rounded-2xl bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-indigo-500/30 shadow-xs space-y-3.5 transition">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
|
||
<input type="text" id="newTaskTitle" placeholder="Wat moet er vandaag gedaan worden?" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
</div>
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||
<div>
|
||
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
|
||
<select id="newTaskPriority" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
<option value="P1">P1 - Brandend / Focus</option>
|
||
<option value="P2" selected>P2 - Belangrijk (Standaard)</option>
|
||
<option value="P3">P3 - Regulier / Lage druk</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
|
||
<div class="relative">
|
||
<input type="text" id="newTaskTime" placeholder="Bijv. 09:30" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-7 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="onNewTaskTimeOrDurationChange('start')">
|
||
<i data-lucide="clock" class="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300">Eindtijd</label>
|
||
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Vrij invulbaar</span>
|
||
</div>
|
||
<div class="relative">
|
||
<input type="text" id="newTaskEndTime" placeholder="Bijv. 09:40" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-7 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="onNewTaskTimeOrDurationChange('end')">
|
||
<i data-lucide="clock-arrow-down" class="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300">Tijdsduur</label>
|
||
<span id="newTaskDurationDisplay" class="text-[10px] font-mono font-bold text-indigo-600 dark:text-indigo-400">30m</span>
|
||
</div>
|
||
<div class="flex items-center space-x-1.5">
|
||
<?php
|
||
$defDuration = (int)($workdaySettings['default_task_duration'] ?? 30);
|
||
?>
|
||
<select id="newTaskDuration" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="onNewTaskTimeOrDurationChange('duration')">
|
||
<option value="5" <?= $defDuration === 5 ? 'selected' : '' ?>>5 minuten</option>
|
||
<option value="10" <?= $defDuration === 10 ? 'selected' : '' ?>>10 minuten</option>
|
||
<option value="15" <?= $defDuration === 15 ? 'selected' : '' ?>>15 minuten</option>
|
||
<option value="20" <?= $defDuration === 20 ? 'selected' : '' ?>>20 minuten</option>
|
||
<option value="25" <?= $defDuration === 25 ? 'selected' : '' ?>>25 minuten</option>
|
||
<option value="30" <?= $defDuration === 30 ? 'selected' : '' ?>>30 minuten</option>
|
||
<option value="45" <?= $defDuration === 45 ? 'selected' : '' ?>>45 minuten</option>
|
||
<option value="60" <?= $defDuration === 60 ? 'selected' : '' ?>>60 minuten (1 uur)</option>
|
||
<option value="90" <?= $defDuration === 90 ? 'selected' : '' ?>>90 minuten (1,5 uur)</option>
|
||
<option value="120" <?= $defDuration === 120 ? 'selected' : '' ?>>120 minuten (2 uur)</option>
|
||
<option value="custom">Aangepast...</option>
|
||
</select>
|
||
<input type="number" id="newTaskCustomDuration" min="1" max="720" placeholder="Min" class="hidden w-20 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-2 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="onNewTaskTimeOrDurationChange('custom_duration')">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($isOutlookConnected): ?>
|
||
<div class="p-3 rounded-xl bg-sky-50/80 dark:bg-sky-950/40 border border-sky-200/70 dark:border-sky-800/50">
|
||
<label class="flex items-center space-x-2 text-xs text-sky-900 dark:text-sky-200 cursor-pointer select-none">
|
||
<input type="checkbox" id="newTaskSyncOutlook" checked class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
||
<span class="flex items-center space-x-1.5 font-semibold">
|
||
<i data-lucide="calendar" class="w-3.5 h-3.5 text-sky-500"></i>
|
||
<span>Direct inplannen in Microsoft Outlook agenda</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="flex space-x-2 pt-1">
|
||
<button onclick="submitNewTask()" class="px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl text-xs transition cursor-pointer shadow-xs">Opslaan</button>
|
||
<button onclick="toggleAddTaskForm()" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-400 rounded-xl text-xs transition cursor-pointer">Annuleren</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Taken weergave -->
|
||
<div class="space-y-2.5" id="tasksContainer">
|
||
<?php if (empty($tasks)): ?>
|
||
<div class="text-center py-10 text-slate-400 dark:text-slate-500 text-xs">
|
||
<i data-lucide="check-circle" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||
<p>Nog geen taken gepland voor <?= htmlspecialchars($formattedSelectedDate) ?>.</p>
|
||
<p class="text-[11px] mt-0.5">Klik op 'Nieuwe Taak' of importeer een Jira ticket hieronder.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<?php foreach ($tasks as $task): ?>
|
||
<div class="fociz-card-interactive group flex items-center justify-between p-3.5 rounded-2xl border <?= $task['is_completed'] ? 'bg-slate-50/50 dark:bg-slate-950/25 border-slate-200/60 dark:border-slate-800/40 opacity-60' : 'bg-white/95 dark:bg-[#111624]/90 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 shadow-2xs hover:shadow-xs' ?> <?= $task['priority'] === 'P1' ? 'border-l-4 border-l-rose-500' : ($task['priority'] === 'P2' ? 'border-l-4 border-l-amber-500' : 'border-l-4 border-l-indigo-400 dark:border-l-indigo-500') ?> transition min-w-0" id="task-row-<?= $task['id'] ?>">
|
||
<div class="flex items-center space-x-3 truncate mr-2">
|
||
<button onclick="toggleTask(<?= $task['id'] ?>, <?= $task['is_completed'] ? 0 : 1 ?>)" class="w-5 h-5 rounded-lg border flex items-center justify-center transition <?= $task['is_completed'] ? 'bg-indigo-600 border-indigo-600 text-white font-bold' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900' ?>">
|
||
<?php if ($task['is_completed']): ?>
|
||
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
|
||
<?php endif; ?>
|
||
</button>
|
||
|
||
<div class="truncate">
|
||
<div class="flex items-center space-x-1.5 truncate">
|
||
<?php if ($task['source'] === 'JIRA'): ?>
|
||
<span class="px-1.5 py-0.2 rounded bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-400 font-mono text-[9px] font-bold">
|
||
JIRA
|
||
</span>
|
||
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
|
||
<span class="px-1.5 py-0.2 rounded bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
|
||
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
|
||
<span>OUTLOOK</span>
|
||
</span>
|
||
<?php endif; ?>
|
||
<span class="text-xs font-semibold truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
|
||
<?= htmlspecialchars($task['title']) ?>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-2 mt-0.5">
|
||
<?php if (!empty($task['time_block_start'])): ?>
|
||
<span class="text-[10px] text-slate-500 dark:text-slate-400 font-mono flex items-center space-x-1">
|
||
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
|
||
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?><?= !empty($task['duration_minutes']) ? ' (' . (int)$task['duration_minutes'] . 'm)' : '' ?></span>
|
||
</span>
|
||
<?php endif; ?>
|
||
<?php if (!empty($task['external_url'])): ?>
|
||
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-sky-600 dark:text-sky-400' : 'text-indigo-600 dark:text-indigo-400' ?> hover:underline flex items-center space-x-0.5">
|
||
<span><?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'Open in Outlook' : 'Open in Jira' ?></span>
|
||
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
|
||
</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-2 flex-shrink-0">
|
||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $task['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($task['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
|
||
<?= htmlspecialchars($task['priority']) ?>
|
||
</span>
|
||
|
||
<?php if ($isOutlookConnected && empty($task['external_id']) && !$task['is_completed']): ?>
|
||
<button onclick="syncTaskToOutlook(<?= $task['id'] ?>, this)" title="Synchroniseer deze taak direct naar Outlook Agenda" class="flex items-center space-x-1 text-[10px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 transition cursor-pointer">
|
||
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
|
||
<span class="hidden sm:inline">Naar Outlook</span>
|
||
</button>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$task['is_completed']): ?>
|
||
<button onclick="selectFocusTask('<?= htmlspecialchars(addslashes($task['title'])) ?>')" title="Koppel aan Focus Timer" class="p-1.5 rounded-lg text-slate-400 hover:text-amber-500 hover:bg-amber-500/10 transition cursor-pointer">
|
||
<i data-lucide="flame" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
<?php endif; ?>
|
||
|
||
<button onclick="openEditTaskModal(<?= htmlspecialchars(json_encode([
|
||
'id' => (int)$task['id'],
|
||
'title' => $task['title'],
|
||
'priority' => $task['priority'],
|
||
'start' => $task['time_block_start'] ?? '',
|
||
'end' => $task['time_block_end'] ?? '',
|
||
'duration' => (int)($task['duration_minutes'] ?: 30),
|
||
'source' => $task['source'],
|
||
'external_id' => $task['external_id'] ?? ''
|
||
]), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijd aanpassen" class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer">
|
||
<i data-lucide="edit-3" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
|
||
<button onclick="deleteTask(<?= $task['id'] ?>)" title="Verwijderen" class="p-1.5 rounded-lg text-slate-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer">
|
||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Jira Snelle Import Widget -->
|
||
<div id="jiraPanel" class="fociz-card p-6">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-3">
|
||
<div class="flex items-center space-x-2">
|
||
<i data-lucide="trello" class="w-4 h-4 text-sky-500"></i>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Mijn Jira Tickets
|
||
</h3>
|
||
<span 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' ?>">
|
||
<?= count($jiraIssues) ?>
|
||
</span>
|
||
</div>
|
||
|
||
<a href="/profile.php" class="text-xs text-slate-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition flex items-center space-x-1 font-medium">
|
||
<span>Beheren</span>
|
||
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||
</a>
|
||
</div>
|
||
|
||
<?php if (empty($jiraIssues)): ?>
|
||
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||
Geen openstaande tickets gevonden in Jira.
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="space-y-2 max-h-64 overflow-y-auto pr-1">
|
||
<?php foreach (array_slice($jiraIssues, 0, 6) as $issue): ?>
|
||
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
|
||
<div class="truncate mr-2">
|
||
<div class="flex items-center space-x-2">
|
||
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
|
||
<?= htmlspecialchars($issue['key']) ?>
|
||
</span>
|
||
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
|
||
<?= htmlspecialchars($issue['summary']) ?>
|
||
</span>
|
||
</div>
|
||
<span class="text-[10px] text-slate-500 mt-0.5 block">
|
||
<?= htmlspecialchars($issue['status']) ?> • <?= htmlspecialchars($issue['priority']) ?>
|
||
</span>
|
||
</div>
|
||
|
||
<button
|
||
onclick="quickImportJira(this, '<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>', '<?= htmlspecialchars(addslashes($issue['url'] ?? '')) ?>')"
|
||
class="flex items-center space-x-1 text-[11px] 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="Plan in als dagtaak"
|
||
>
|
||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||
<span>Plan</span>
|
||
</button>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Dagnotities & Reflectie -->
|
||
<div class="fociz-card p-6">
|
||
<div class="flex items-center space-x-2 mb-3">
|
||
<i data-lucide="book-open" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Dagnotities & Kladblok</h3>
|
||
</div>
|
||
<textarea
|
||
id="dailyNotesInput"
|
||
rows="3"
|
||
placeholder="Belangrijke aantekeningen, reflecties of snelle memo's voor vandaag..."
|
||
onblur="saveGoal()"
|
||
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl p-3 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-none transition shadow-2xs"
|
||
><?= htmlspecialchars($dailyPlan['notes'] ?? '') ?></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rechter kolom: Timeblocking & Outlook Tijdlijn -->
|
||
<div class="lg:col-span-5 space-y-6 min-w-0">
|
||
<div class="fociz-card p-5 sm:p-6 overflow-hidden">
|
||
<div class="flex items-center justify-between mb-4 pb-3 border-b border-slate-100 dark:border-slate-800">
|
||
<div class="flex items-center space-x-2">
|
||
<i data-lucide="clock" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Tijdlijn & Time-blocking</h3>
|
||
</div>
|
||
<div class="flex items-center space-x-2">
|
||
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
|
||
<button onclick="aiSmartSchedule(this)" id="aiScheduleBtn" title="Laat AI Co-Pilot openstaande taken optimaal inplannen om meetings en lunch heen" class="flex items-center space-x-1.5 px-3 py-1 rounded-lg bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-semibold shadow-xs transition cursor-pointer">
|
||
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
|
||
<span class="hidden sm:inline">AI Slim Indelen</span>
|
||
</button>
|
||
<?php endif; ?>
|
||
<?php if ($isOutlookConnected): ?>
|
||
<span class="px-2 py-0.5 rounded-lg bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 text-[10px] font-semibold">Outlook 365</span>
|
||
<?php endif; ?>
|
||
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono"><?= $hours[0] ?> – <?= end($hours) ?></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-2.5">
|
||
<?php foreach ($hours as $hour):
|
||
$hourInt = (int)explode(':', $hour)[0];
|
||
$isLunchSlot = ($hourInt === $lunchHour && !empty($lunchStart) && !empty($lunchEnd));
|
||
|
||
// Zoek Outlook meetings die binnen dit uur vallen (gefilterd op Fociz duplicaten)
|
||
$slotEvents = array_filter($displayedOutlookEvents, function($e) use ($hourInt) {
|
||
if (empty($e['start'])) return false;
|
||
return (int)explode(':', $e['start'])[0] === $hourInt;
|
||
});
|
||
|
||
// Zoek taken gepland in dit uur
|
||
$slotTasks = array_filter($tasks, function($t) use ($hourInt) {
|
||
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $hourInt;
|
||
});
|
||
?>
|
||
<div class="flex items-start group min-h-[48px] border-b border-slate-100 dark:border-slate-800/40 pb-2.5 last:border-0 min-w-0">
|
||
<div class="w-12 sm:w-14 text-xs font-mono font-semibold text-slate-500 dark:text-slate-400 pt-1.5 flex-shrink-0">
|
||
<?= $hour ?>
|
||
</div>
|
||
|
||
<div class="flex-1 space-y-1.5 min-w-0">
|
||
<?php if ($isLunchSlot): ?>
|
||
<!-- Automatische Lunchpauze Reservering (Release v1.4) -->
|
||
<div class="p-3 rounded-xl bg-amber-500/10 dark:bg-amber-500/15 border-l-4 border-l-amber-500 border border-amber-500/20 text-amber-900 dark:text-amber-200 text-xs shadow-2xs flex items-center justify-between min-w-0 transition-all">
|
||
<div class="flex items-center space-x-2.5 min-w-0">
|
||
<span class="px-2 py-0.5 rounded-md bg-amber-500/20 text-amber-900 dark:text-amber-200 font-mono text-[10px] font-bold flex-shrink-0">
|
||
<?= htmlspecialchars($lunchStart) ?> - <?= htmlspecialchars($lunchEnd) ?>
|
||
</span>
|
||
<span class="font-semibold text-xs flex items-center space-x-1.5 truncate">
|
||
<i data-lucide="coffee" class="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0"></i>
|
||
<span class="truncate">Lunchpauze (Gereserveerd)</span>
|
||
</span>
|
||
</div>
|
||
<span class="text-[10px] font-semibold text-amber-700 dark:text-amber-400 flex-shrink-0 ml-2">Pauze</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Outlook meetings -->
|
||
<?php foreach ($slotEvents as $evt): ?>
|
||
<div class="p-3 rounded-xl bg-sky-500/[0.07] dark:bg-sky-500/[0.12] border-l-4 border-l-sky-500 border border-sky-500/20 text-sky-950 dark:text-sky-100 text-xs shadow-2xs min-w-0 overflow-hidden transition-all hover:bg-sky-500/[0.10] dark:hover:bg-sky-500/[0.16]">
|
||
<div class="flex items-start justify-between gap-2.5 min-w-0">
|
||
<div class="flex items-start space-x-2 min-w-0 flex-1">
|
||
<span class="px-2 py-0.5 rounded-md bg-sky-500/15 text-sky-800 dark:text-sky-300 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5">
|
||
<?= htmlspecialchars($evt['start']) ?> - <?= htmlspecialchars($evt['end']) ?>
|
||
</span>
|
||
<span class="font-semibold text-xs leading-snug break-words line-clamp-2 min-w-0 flex-1" title="<?= htmlspecialchars($evt['subject']) ?>">
|
||
<?= htmlspecialchars($evt['subject']) ?>
|
||
</span>
|
||
</div>
|
||
<div class="flex items-center space-x-1 flex-shrink-0">
|
||
<?php if (!empty($evt['meeting_url'])): ?>
|
||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-white bg-indigo-600 hover:bg-indigo-700 px-2.5 py-1 rounded-lg flex-shrink-0 transition font-semibold shadow-xs">
|
||
<i data-lucide="video" class="w-3 h-3"></i>
|
||
<span class="hidden sm:inline">Deelnemen</span>
|
||
</a>
|
||
<?php elseif (!empty($evt['web_link'])): ?>
|
||
<a href="<?= htmlspecialchars($evt['web_link']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-sky-700 dark:text-sky-300 hover:underline px-1 py-0.5 transition font-medium">
|
||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||
<span class="hidden sm:inline">Outlook</span>
|
||
</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- Geplande taken -->
|
||
<?php foreach ($slotTasks as $t): ?>
|
||
<div class="p-3 rounded-xl border-l-4 <?= $t['priority'] === 'P1' ? 'border-l-rose-500' : ($t['priority'] === 'P2' ? 'border-l-amber-500' : 'border-l-indigo-400') ?> <?= $t['is_completed'] ? 'bg-slate-100/60 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-white/80 dark:bg-[#0c0f17]/70 border border-slate-200/90 dark:border-slate-800 text-slate-900 dark:text-slate-100 shadow-2xs hover:shadow-xs' ?> text-xs min-w-0 overflow-hidden transition-all">
|
||
<div class="flex items-start justify-between gap-2.5 min-w-0">
|
||
<div class="flex items-start space-x-2 min-w-0 flex-1">
|
||
<span class="px-2 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/20 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5">
|
||
<?= htmlspecialchars($t['time_block_start']) ?><?= !empty($t['time_block_end']) ? ' - ' . htmlspecialchars($t['time_block_end']) : '' ?>
|
||
</span>
|
||
<span class="font-semibold text-xs leading-snug break-words line-clamp-2 min-w-0 flex-1" title="<?= htmlspecialchars($t['title']) ?>">
|
||
<?= htmlspecialchars($t['title']) ?>
|
||
</span>
|
||
</div>
|
||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||
<?php if (!empty($t['external_id']) && $t['source'] === 'OUTLOOK'): ?>
|
||
<span class="text-[9px] font-semibold px-2 py-0.5 rounded bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-500/25 flex items-center space-x-1 whitespace-nowrap" title="In Outlook agenda">
|
||
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
|
||
<span class="hidden sm:inline">In Outlook</span>
|
||
</span>
|
||
<?php elseif ($isOutlookConnected && !$t['is_completed']): ?>
|
||
<button onclick="syncTaskToOutlook(<?= $t['id'] ?>, this)" title="Synchroniseer naar Outlook Agenda" class="text-[9px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 flex items-center space-x-1 transition cursor-pointer whitespace-nowrap">
|
||
<i data-lucide="calendar-plus" class="w-2.5 h-2.5"></i>
|
||
<span class="hidden sm:inline">Naar Outlook</span>
|
||
</button>
|
||
<?php endif; ?>
|
||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $t['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($t['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
|
||
<?= $t['priority'] ?>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<?php if (empty($slotEvents) && empty($slotTasks) && !$isLunchSlot): ?>
|
||
<button onclick="quickSchedule('<?= $hour ?>')" class="w-full text-left py-2 px-3 rounded-xl border border-dashed border-slate-200 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 hover:bg-indigo-500/[0.04] dark:hover:bg-indigo-500/[0.08] text-slate-400 dark:text-slate-500 hover:text-indigo-600 dark:hover:text-indigo-400 text-xs font-medium transition flex items-center justify-between group/slot cursor-pointer">
|
||
<span>+ Plan taak om <?= $hour ?></span>
|
||
<i data-lucide="plus" class="w-3.5 h-3.5 opacity-0 group-hover/slot:opacity-100 transition-opacity"></i>
|
||
</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
if ($isAjaxRequest) {
|
||
$contentHtml = ob_get_clean();
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode([
|
||
'success' => true,
|
||
'selectedDate' => $selectedDate,
|
||
'formattedDate' => $formattedSelectedDate,
|
||
'weekStart' => $weekStart,
|
||
'weekEnd' => $weekEnd,
|
||
'weekNumber' => $weekNumber,
|
||
'yearNumber' => $yearNumber,
|
||
'formattedWeek' => $formattedWeekRange,
|
||
'syncHash' => $initialSyncHash,
|
||
'focusGoal' => $dailyPlan['focus_goal'] ?? '',
|
||
'dailyNotes' => $dailyPlan['notes'] ?? '',
|
||
'title' => 'Fociz — Dagplanning (' . $formattedSelectedDate . ')',
|
||
'html' => $contentHtml,
|
||
]);
|
||
exit;
|
||
}
|
||
?>
|
||
</div>
|
||
|
||
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
|
||
<div id="editTaskModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
|
||
<div class="fociz-card p-6 w-full max-w-lg shadow-2xl space-y-4">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center space-x-2.5">
|
||
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||
<i data-lucide="edit-3" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Taak & Tijdblok Bewerken</h3>
|
||
<p class="text-[10px] text-slate-500 dark:text-slate-400">Pas titel, start-/eindtijd en tijdsduur vrij aan</p>
|
||
</div>
|
||
</div>
|
||
<button onclick="closeEditTaskModal()" 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>
|
||
|
||
<input type="hidden" id="editTaskId">
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
|
||
<input type="text" id="editTaskTitle" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
|
||
<select id="editTaskPriority" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
|
||
<option value="P1">P1 - Brandend / Focus</option>
|
||
<option value="P2">P2 - Belangrijk (Standaard)</option>
|
||
<option value="P3">P3 - Regulier / Lage druk</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Tijdsduur</label>
|
||
<span id="editTaskDurationDisplay" class="text-[11px] font-mono font-bold text-indigo-600 dark:text-indigo-400">30m</span>
|
||
</div>
|
||
<div class="flex items-center space-x-1.5">
|
||
<select id="editTaskDuration" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" onchange="onEditTaskTimeOrDurationChange('duration')">
|
||
<option value="5">5 minuten</option>
|
||
<option value="10">10 minuten</option>
|
||
<option value="15">15 minuten</option>
|
||
<option value="20">20 minuten</option>
|
||
<option value="25">25 minuten</option>
|
||
<option value="30">30 minuten</option>
|
||
<option value="45">45 minuten</option>
|
||
<option value="60">60 minuten (1 uur)</option>
|
||
<option value="90">90 minuten (1,5 uur)</option>
|
||
<option value="120">120 minuten (2 uur)</option>
|
||
<option value="custom">Aangepast...</option>
|
||
</select>
|
||
<input type="number" id="editTaskCustomDuration" min="1" max="720" placeholder="Min" class="hidden w-20 bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('custom_duration')">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
|
||
<div class="relative">
|
||
<input type="text" id="editTaskStartTime" placeholder="Bijv. 09:30" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('start')">
|
||
<i data-lucide="clock" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Eindtijd</label>
|
||
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Vrij invulbaar</span>
|
||
</div>
|
||
<div class="relative">
|
||
<input type="text" id="editTaskEndTime" placeholder="Bijv. 09:40" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('end')">
|
||
<i data-lucide="clock-arrow-down" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="editTaskOutlookNotice" class="hidden p-3 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-800 dark:text-sky-300 text-[11px] flex items-center space-x-2">
|
||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
||
<span>Deze taak is gekoppeld aan Outlook 365. Wijzigingen worden automatisch in uw Outlook agenda gesynchroniseerd.</span>
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800/80">
|
||
<button type="button" onclick="closeEditTaskModal()" 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 text-xs font-semibold transition cursor-pointer">
|
||
Annuleren
|
||
</button>
|
||
<button type="button" onclick="submitEditTask()" id="btnSaveEditTask" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer">
|
||
Wijzigingen Opslaan
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- Focus Timer Logica (Release v1.4) ---
|
||
const pomodoroFocusTime = <?= (int)$workdaySettings['pomodoro_focus'] ?>;
|
||
const pomodoroDeepTime = <?= (int)$workdaySettings['pomodoro_deep'] ?>;
|
||
const pomodoroBreakTime = <?= (int)$workdaySettings['pomodoro_break'] ?>;
|
||
|
||
let timerInterval = null;
|
||
let timerSeconds = pomodoroFocusTime * 60;
|
||
let totalDuration = pomodoroFocusTime * 60;
|
||
let isRunning = false;
|
||
|
||
function updateTimerDisplay() {
|
||
const min = Math.floor(timerSeconds / 60);
|
||
const sec = timerSeconds % 60;
|
||
document.getElementById('timerDisplay').innerText =
|
||
`${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||
|
||
const progress = ((totalDuration - timerSeconds) / totalDuration) * 100;
|
||
document.getElementById('timerProgress').style.width = `${progress}%`;
|
||
}
|
||
|
||
function setTimerPreset(minutes) {
|
||
clearInterval(timerInterval);
|
||
isRunning = false;
|
||
timerSeconds = minutes * 60;
|
||
totalDuration = minutes * 60;
|
||
updateTimerDisplay();
|
||
|
||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
||
const inactiveClass = 'px-2.5 py-1 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition';
|
||
const activeClass = 'px-2.5 py-1 rounded-lg font-semibold bg-indigo-600 text-white shadow-xs transition';
|
||
if (document.getElementById('btnPresetFocus')) document.getElementById('btnPresetFocus').className = minutes === pomodoroFocusTime ? activeClass : inactiveClass;
|
||
if (document.getElementById('btnPresetDeep')) document.getElementById('btnPresetDeep').className = minutes === pomodoroDeepTime ? activeClass : inactiveClass;
|
||
if (document.getElementById('btnPresetBreak')) document.getElementById('btnPresetBreak').className = minutes === pomodoroBreakTime ? activeClass : inactiveClass;
|
||
}
|
||
|
||
function toggleTimer() {
|
||
if (isRunning) {
|
||
clearInterval(timerInterval);
|
||
isRunning = false;
|
||
document.getElementById('timerToggleText').innerText = 'Hervatten';
|
||
saveTimerStateBeforeReload();
|
||
} else {
|
||
isRunning = true;
|
||
document.getElementById('timerToggleText').innerText = 'Pauzeren';
|
||
timerInterval = setInterval(() => {
|
||
if (timerSeconds > 0) {
|
||
timerSeconds--;
|
||
updateTimerDisplay();
|
||
} else {
|
||
clearInterval(timerInterval);
|
||
isRunning = false;
|
||
sessionStorage.removeItem('fociz_timer_state');
|
||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
||
alert('Focus sessie voltooid! Neem even een korte pauze.');
|
||
}
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
function resetTimer() {
|
||
clearInterval(timerInterval);
|
||
isRunning = false;
|
||
timerSeconds = totalDuration;
|
||
sessionStorage.removeItem('fociz_timer_state');
|
||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
||
updateTimerDisplay();
|
||
}
|
||
|
||
function saveTimerStateBeforeReload() {
|
||
try {
|
||
if (isRunning || timerSeconds < totalDuration) {
|
||
const state = {
|
||
isRunning: isRunning,
|
||
timerSeconds: timerSeconds,
|
||
totalDuration: totalDuration,
|
||
timestamp: Date.now()
|
||
};
|
||
sessionStorage.setItem('fociz_timer_state', JSON.stringify(state));
|
||
}
|
||
} catch (e) {
|
||
console.error('Kon timer status niet opslaan:', e);
|
||
}
|
||
}
|
||
|
||
function restoreTimerStateIfAny() {
|
||
try {
|
||
const raw = sessionStorage.getItem('fociz_timer_state');
|
||
if (!raw) return;
|
||
sessionStorage.removeItem('fociz_timer_state');
|
||
const state = JSON.parse(raw);
|
||
if (state && typeof state.timerSeconds === 'number') {
|
||
totalDuration = state.totalDuration || (pomodoroFocusTime * 60);
|
||
let remaining = state.timerSeconds;
|
||
if (state.isRunning && state.timestamp) {
|
||
const elapsed = Math.floor((Date.now() - state.timestamp) / 1000);
|
||
remaining = Math.max(0, remaining - elapsed);
|
||
}
|
||
timerSeconds = remaining;
|
||
updateTimerDisplay();
|
||
|
||
if (state.isRunning && timerSeconds > 0) {
|
||
toggleTimer(); // Hervat timer
|
||
} else if (!state.isRunning && timerSeconds < totalDuration) {
|
||
const toggleBtnText = document.getElementById('timerToggleText');
|
||
if (toggleBtnText) toggleBtnText.innerText = 'Hervatten';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Kon timer status niet herstellen:', e);
|
||
}
|
||
}
|
||
|
||
window.addEventListener('beforeunload', () => {
|
||
saveTimerStateBeforeReload();
|
||
});
|
||
|
||
function selectFocusTask(title) {
|
||
document.getElementById('activeTaskDisplay').innerText = title;
|
||
}
|
||
|
||
// --- Globale Planner Status & Naadloos AJAX Datumbeheer ---
|
||
let activePlannerDate = '<?= $selectedDate ?>';
|
||
let weekStartDate = '<?= $weekStart ?>';
|
||
let weekEndDate = '<?= $weekEnd ?>';
|
||
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
|
||
let isDateSwitching = false;
|
||
|
||
/**
|
||
* Wisselt de actieve plannerdatum geruisloos via AJAX
|
||
* Behoudt de huidige schermhoogte (scrollpositie) en voorkomt flitsen/herladen
|
||
*/
|
||
async function switchPlannerDate(newDate, event = null, pushState = true) {
|
||
if (event && event.preventDefault) {
|
||
event.preventDefault();
|
||
}
|
||
if (!newDate || isDateSwitching) return;
|
||
|
||
const container = document.getElementById('plannerContentArea');
|
||
if (!container) {
|
||
window.location.href = 'planner.php?date=' + encodeURIComponent(newDate);
|
||
return;
|
||
}
|
||
|
||
// Onthoud exact de huidige scrollpositie
|
||
const currentScrollY = window.scrollY;
|
||
isDateSwitching = true;
|
||
|
||
// Subtiele visuele feedback zonder layout-shift
|
||
container.classList.add('opacity-60', 'pointer-events-none');
|
||
|
||
try {
|
||
const response = await fetch(`planner.php?date=${encodeURIComponent(newDate)}&ajax=1`, {
|
||
headers: {
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP fout ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (!data || !data.success || !data.html) {
|
||
throw new Error(data?.error || 'Ongeldige serverrespons');
|
||
}
|
||
|
||
// Vervang de dynamische plannerinhoud geruisloos
|
||
container.innerHTML = data.html;
|
||
|
||
// Werk globale variabelen bij
|
||
activePlannerDate = data.selectedDate;
|
||
if (data.weekStart) weekStartDate = data.weekStart;
|
||
if (data.weekEnd) weekEndDate = data.weekEnd;
|
||
if (data.syncHash) currentSyncHash = data.syncHash;
|
||
|
||
// Werk paginatitel bij
|
||
if (data.title) {
|
||
document.title = data.title;
|
||
}
|
||
|
||
// Werk URL bij in adresbalk zonder paginaherlaad
|
||
if (pushState && window.history && window.history.pushState) {
|
||
const newUrl = `planner.php?date=${encodeURIComponent(newDate)}`;
|
||
window.history.pushState({ date: newDate }, data.title || '', newUrl);
|
||
}
|
||
|
||
// Render Lucide SVG-iconen in de nieuwe inhoud
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
|
||
// Garandeer dat het scherm exact op dezelfde scrollhoogte blijft
|
||
window.scrollTo({
|
||
top: currentScrollY,
|
||
behavior: 'instant'
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Fout bij wisselen van datum via AJAX:', err);
|
||
// Veilige fallback bij netwerkonderbreking
|
||
window.location.href = 'planner.php?date=' + encodeURIComponent(newDate);
|
||
} finally {
|
||
isDateSwitching = false;
|
||
container.classList.remove('opacity-60', 'pointer-events-none');
|
||
}
|
||
}
|
||
|
||
// Browser Terug/Vooruit knoppen opvangen (popstate)
|
||
window.addEventListener('popstate', (event) => {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const dateFromUrl = urlParams.get('date');
|
||
if (dateFromUrl && dateFromUrl !== activePlannerDate) {
|
||
switchPlannerDate(dateFromUrl, null, false);
|
||
}
|
||
});
|
||
|
||
// --- Dagdoel & Taken AJAX ---
|
||
|
||
async function saveGoal() {
|
||
const focusGoal = document.getElementById('focusGoalInput').value;
|
||
const notes = document.getElementById('dailyNotesInput').value;
|
||
const btn = document.getElementById('saveGoalBtn');
|
||
|
||
btn.innerText = 'Opslaan...';
|
||
try {
|
||
await fetch('/api/planner.php?action=save_goal', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ focus_goal: focusGoal, notes: notes, date: activePlannerDate })
|
||
});
|
||
btn.innerHTML = '<i data-lucide="check" class="w-4 h-4"></i><span>Opgeslagen</span>';
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
setTimeout(() => {
|
||
btn.innerHTML = '<i data-lucide="check-circle-2" class="w-4 h-4"></i><span>Opslaan</span>';
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}, 1500);
|
||
} catch(err) {
|
||
btn.innerText = 'Fout';
|
||
}
|
||
}
|
||
|
||
// --- Tijdblok Hulpfuncties & Vrije Duur Berekening ---
|
||
function parseTimeToMinutes(str) {
|
||
if (!str || typeof str !== 'string') return null;
|
||
str = str.trim();
|
||
if (!str) return null;
|
||
|
||
let m = str.match(/^(\d{1,2})[:.](\d{2})$/);
|
||
if (m) {
|
||
const hours = parseInt(m[1], 10);
|
||
const mins = parseInt(m[2], 10);
|
||
if (hours >= 0 && hours < 24 && mins >= 0 && mins < 60) {
|
||
return (hours * 60) + mins;
|
||
}
|
||
}
|
||
|
||
m = str.match(/^(\d{1,2})$/);
|
||
if (m) {
|
||
const hours = parseInt(m[1], 10);
|
||
if (hours >= 0 && hours < 24) {
|
||
return hours * 60;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function formatMinutesToTime(totalMins) {
|
||
totalMins = (totalMins + (24 * 60)) % (24 * 60);
|
||
const h = Math.floor(totalMins / 60);
|
||
const m = totalMins % 60;
|
||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||
}
|
||
|
||
// Tweeweg synchronisatie bij Nieuwe Taak (Starttijd, Eindtijd & Tijdsduur)
|
||
function onNewTaskTimeOrDurationChange(source) {
|
||
const startInput = document.getElementById('newTaskTime');
|
||
const endInput = document.getElementById('newTaskEndTime');
|
||
const durationSelect = document.getElementById('newTaskDuration');
|
||
const customDurationInput = document.getElementById('newTaskCustomDuration');
|
||
const durationDisplay = document.getElementById('newTaskDurationDisplay');
|
||
|
||
if (!startInput || !endInput || !durationSelect) return;
|
||
const startMins = parseTimeToMinutes(startInput.value);
|
||
|
||
if (source === 'end') {
|
||
const endMins = parseTimeToMinutes(endInput.value);
|
||
if (startMins !== null && endMins !== null && endMins > startMins) {
|
||
const diffMins = endMins - startMins;
|
||
setSelectedNewTaskDuration(diffMins);
|
||
}
|
||
} else if (source === 'duration') {
|
||
const val = durationSelect.value;
|
||
if (val === 'custom') {
|
||
customDurationInput.classList.remove('hidden');
|
||
customDurationInput.focus();
|
||
const customVal = parseInt(customDurationInput.value, 10);
|
||
if (!isNaN(customVal) && customVal > 0 && startMins !== null) {
|
||
endInput.value = formatMinutesToTime(startMins + customVal);
|
||
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
|
||
}
|
||
} else {
|
||
customDurationInput.classList.add('hidden');
|
||
const dur = parseInt(val, 10);
|
||
if (!isNaN(dur) && dur > 0) {
|
||
if (durationDisplay) durationDisplay.innerText = `${dur}m`;
|
||
if (startMins !== null) {
|
||
endInput.value = formatMinutesToTime(startMins + dur);
|
||
}
|
||
}
|
||
}
|
||
} else if (source === 'custom_duration') {
|
||
const customVal = parseInt(customDurationInput.value, 10);
|
||
if (!isNaN(customVal) && customVal > 0) {
|
||
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
|
||
if (startMins !== null) {
|
||
endInput.value = formatMinutesToTime(startMins + customVal);
|
||
}
|
||
}
|
||
} else if (source === 'start') {
|
||
let dur = getSelectedNewTaskDurationMinutes();
|
||
if (startMins !== null) {
|
||
if (endInput.value && parseTimeToMinutes(endInput.value) !== null) {
|
||
const currentEndMins = parseTimeToMinutes(endInput.value);
|
||
if (currentEndMins > startMins) {
|
||
setSelectedNewTaskDuration(currentEndMins - startMins);
|
||
} else if (dur > 0) {
|
||
endInput.value = formatMinutesToTime(startMins + dur);
|
||
}
|
||
} else if (dur > 0) {
|
||
endInput.value = formatMinutesToTime(startMins + dur);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function getSelectedNewTaskDurationMinutes() {
|
||
const durationSelect = document.getElementById('newTaskDuration');
|
||
const customDurationInput = document.getElementById('newTaskCustomDuration');
|
||
if (!durationSelect) return 30;
|
||
if (durationSelect.value === 'custom') {
|
||
const val = parseInt(customDurationInput?.value, 10);
|
||
return (!isNaN(val) && val > 0) ? val : 30;
|
||
}
|
||
const val = parseInt(durationSelect.value, 10);
|
||
return (!isNaN(val) && val > 0) ? val : 30;
|
||
}
|
||
|
||
function setSelectedNewTaskDuration(mins) {
|
||
const durationSelect = document.getElementById('newTaskDuration');
|
||
const customDurationInput = document.getElementById('newTaskCustomDuration');
|
||
const durationDisplay = document.getElementById('newTaskDurationDisplay');
|
||
if (!durationSelect) return;
|
||
|
||
let found = false;
|
||
for (let i = 0; i < durationSelect.options.length; i++) {
|
||
if (durationSelect.options[i].value === String(mins)) {
|
||
durationSelect.selectedIndex = i;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
durationSelect.value = 'custom';
|
||
if (customDurationInput) {
|
||
customDurationInput.value = mins;
|
||
customDurationInput.classList.remove('hidden');
|
||
}
|
||
} else {
|
||
if (customDurationInput) customDurationInput.classList.add('hidden');
|
||
}
|
||
if (durationDisplay) durationDisplay.innerText = `${mins}m`;
|
||
}
|
||
|
||
function toggleAddTaskForm() {
|
||
const form = document.getElementById('addTaskForm');
|
||
form.classList.toggle('hidden');
|
||
if (!form.classList.contains('hidden')) {
|
||
document.getElementById('newTaskTitle').focus();
|
||
}
|
||
}
|
||
|
||
function quickSchedule(hour) {
|
||
const form = document.getElementById('addTaskForm');
|
||
if (form && form.classList.contains('hidden')) {
|
||
form.classList.remove('hidden');
|
||
}
|
||
const timeInput = document.getElementById('newTaskTime');
|
||
if (timeInput) {
|
||
timeInput.value = hour;
|
||
onNewTaskTimeOrDurationChange('start');
|
||
}
|
||
const outlookCheckbox = document.getElementById('newTaskSyncOutlook');
|
||
if (outlookCheckbox) outlookCheckbox.checked = true;
|
||
document.getElementById('newTaskTitle')?.focus();
|
||
}
|
||
|
||
async function submitNewTask() {
|
||
const title = document.getElementById('newTaskTitle').value.trim();
|
||
const priority = document.getElementById('newTaskPriority').value;
|
||
const time = document.getElementById('newTaskTime').value.trim();
|
||
const endTime = document.getElementById('newTaskEndTime') ? document.getElementById('newTaskEndTime').value.trim() : '';
|
||
const duration = getSelectedNewTaskDurationMinutes();
|
||
const syncOutlook = document.getElementById('newTaskSyncOutlook') ? document.getElementById('newTaskSyncOutlook').checked : false;
|
||
|
||
if (!title) {
|
||
alert('Vul een taaktitel in.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=add_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
title,
|
||
priority,
|
||
time_block_start: time,
|
||
time_block_end: endTime,
|
||
duration_minutes: duration,
|
||
sync_to_outlook: syncOutlook,
|
||
date: activePlannerDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
if (syncOutlook && !data.outlook_synced) {
|
||
alert('Let op: De taak is opgeslagen in Fociz, maar kon niet direct naar Microsoft Outlook worden verzonden:\n\n' + (data.outlook_error || 'Controleer uw verbinding in Mijn Profiel.'));
|
||
}
|
||
toggleAddTaskForm();
|
||
const titleInput = document.getElementById('newTaskTitle');
|
||
if (titleInput) titleInput.value = '';
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
} else {
|
||
alert(data.error || 'Fout bij opslaan van de taak');
|
||
}
|
||
} catch(err) {
|
||
alert('Netwerkfout bij opslaan van de taak');
|
||
}
|
||
}
|
||
|
||
// --- Taak & Tijdblok Bewerken Modal Logica ---
|
||
function openEditTaskModal(task) {
|
||
if (!task) return;
|
||
document.getElementById('editTaskId').value = task.id;
|
||
document.getElementById('editTaskTitle').value = task.title || '';
|
||
document.getElementById('editTaskPriority').value = task.priority || 'P2';
|
||
document.getElementById('editTaskStartTime').value = task.start || '';
|
||
document.getElementById('editTaskEndTime').value = task.end || '';
|
||
|
||
const dur = task.duration || 30;
|
||
setSelectedEditTaskDuration(dur);
|
||
|
||
const outlookNotice = document.getElementById('editTaskOutlookNotice');
|
||
if (outlookNotice) {
|
||
if (task.source === 'OUTLOOK' || task.external_id) {
|
||
outlookNotice.classList.remove('hidden');
|
||
} else {
|
||
outlookNotice.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
const modal = document.getElementById('editTaskModal');
|
||
if (modal) {
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
document.getElementById('editTaskTitle')?.focus();
|
||
}
|
||
}
|
||
|
||
function closeEditTaskModal() {
|
||
const modal = document.getElementById('editTaskModal');
|
||
if (modal) {
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
}
|
||
|
||
function onEditTaskTimeOrDurationChange(source) {
|
||
const startInput = document.getElementById('editTaskStartTime');
|
||
const endInput = document.getElementById('editTaskEndTime');
|
||
const durationSelect = document.getElementById('editTaskDuration');
|
||
const customDurationInput = document.getElementById('editTaskCustomDuration');
|
||
const durationDisplay = document.getElementById('editTaskDurationDisplay');
|
||
|
||
if (!startInput || !endInput || !durationSelect) return;
|
||
const startMins = parseTimeToMinutes(startInput.value);
|
||
|
||
if (source === 'end') {
|
||
const endMins = parseTimeToMinutes(endInput.value);
|
||
if (startMins !== null && endMins !== null && endMins > startMins) {
|
||
const diffMins = endMins - startMins;
|
||
setSelectedEditTaskDuration(diffMins);
|
||
}
|
||
} else if (source === 'duration') {
|
||
const val = durationSelect.value;
|
||
if (val === 'custom') {
|
||
customDurationInput.classList.remove('hidden');
|
||
customDurationInput.focus();
|
||
const customVal = parseInt(customDurationInput.value, 10);
|
||
if (!isNaN(customVal) && customVal > 0 && startMins !== null) {
|
||
endInput.value = formatMinutesToTime(startMins + customVal);
|
||
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
|
||
}
|
||
} else {
|
||
customDurationInput.classList.add('hidden');
|
||
const dur = parseInt(val, 10);
|
||
if (!isNaN(dur) && dur > 0) {
|
||
if (durationDisplay) durationDisplay.innerText = `${dur}m`;
|
||
if (startMins !== null) {
|
||
endInput.value = formatMinutesToTime(startMins + dur);
|
||
}
|
||
}
|
||
}
|
||
} else if (source === 'custom_duration') {
|
||
const customVal = parseInt(customDurationInput.value, 10);
|
||
if (!isNaN(customVal) && customVal > 0) {
|
||
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
|
||
if (startMins !== null) {
|
||
endInput.value = formatMinutesToTime(startMins + customVal);
|
||
}
|
||
}
|
||
} else if (source === 'start') {
|
||
let dur = getSelectedEditTaskDurationMinutes();
|
||
if (startMins !== null) {
|
||
if (endInput.value && parseTimeToMinutes(endInput.value) !== null) {
|
||
const currentEndMins = parseTimeToMinutes(endInput.value);
|
||
if (currentEndMins > startMins) {
|
||
setSelectedEditTaskDuration(currentEndMins - startMins);
|
||
} else if (dur > 0) {
|
||
endInput.value = formatMinutesToTime(startMins + dur);
|
||
}
|
||
} else if (dur > 0) {
|
||
endInput.value = formatMinutesToTime(startMins + dur);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function getSelectedEditTaskDurationMinutes() {
|
||
const durationSelect = document.getElementById('editTaskDuration');
|
||
const customDurationInput = document.getElementById('editTaskCustomDuration');
|
||
if (!durationSelect) return 30;
|
||
if (durationSelect.value === 'custom') {
|
||
const val = parseInt(customDurationInput?.value, 10);
|
||
return (!isNaN(val) && val > 0) ? val : 30;
|
||
}
|
||
const val = parseInt(durationSelect.value, 10);
|
||
return (!isNaN(val) && val > 0) ? val : 30;
|
||
}
|
||
|
||
function setSelectedEditTaskDuration(mins) {
|
||
const durationSelect = document.getElementById('editTaskDuration');
|
||
const customDurationInput = document.getElementById('editTaskCustomDuration');
|
||
const durationDisplay = document.getElementById('editTaskDurationDisplay');
|
||
if (!durationSelect) return;
|
||
|
||
let found = false;
|
||
for (let i = 0; i < durationSelect.options.length; i++) {
|
||
if (durationSelect.options[i].value === String(mins)) {
|
||
durationSelect.selectedIndex = i;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
durationSelect.value = 'custom';
|
||
if (customDurationInput) {
|
||
customDurationInput.value = mins;
|
||
customDurationInput.classList.remove('hidden');
|
||
}
|
||
} else {
|
||
if (customDurationInput) customDurationInput.classList.add('hidden');
|
||
}
|
||
if (durationDisplay) durationDisplay.innerText = `${mins}m`;
|
||
}
|
||
|
||
async function submitEditTask() {
|
||
const taskId = parseInt(document.getElementById('editTaskId').value, 10);
|
||
const title = document.getElementById('editTaskTitle').value.trim();
|
||
const priority = document.getElementById('editTaskPriority').value;
|
||
const startTime = document.getElementById('editTaskStartTime').value.trim();
|
||
const endTime = document.getElementById('editTaskEndTime').value.trim();
|
||
const duration = getSelectedEditTaskDurationMinutes();
|
||
const saveBtn = document.getElementById('btnSaveEditTask');
|
||
|
||
if (!title) {
|
||
alert('Vul een taaktitel in.');
|
||
return;
|
||
}
|
||
|
||
if (saveBtn) {
|
||
saveBtn.innerText = 'Opslaan...';
|
||
saveBtn.disabled = true;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=update_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
task_id: taskId,
|
||
title: title,
|
||
priority: priority,
|
||
time_block_start: startTime,
|
||
time_block_end: endTime,
|
||
duration_minutes: duration
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
closeEditTaskModal();
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
} else {
|
||
alert(data.error || 'Kon taak niet bijwerken');
|
||
if (saveBtn) {
|
||
saveBtn.innerText = 'Wijzigingen Opslaan';
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
alert('Netwerkfout bij bijwerken van de taak');
|
||
if (saveBtn) {
|
||
saveBtn.innerText = 'Wijzigingen Opslaan';
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function syncTaskToOutlook(taskId, btn) {
|
||
if (!btn) return;
|
||
const originalHtml = btn.innerHTML;
|
||
btn.innerHTML = '<span class="text-[9px] animate-pulse font-medium">Syncen...</span>';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=sync_task_to_outlook', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ task_id: taskId })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
btn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 text-teal-500"></i>';
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 600);
|
||
} else {
|
||
alert(data.error || 'Kon taak niet synchroniseren naar Outlook');
|
||
btn.innerHTML = originalHtml;
|
||
btn.disabled = false;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
} catch(err) {
|
||
alert('Netwerkfout bij synchroniseren naar Outlook');
|
||
btn.innerHTML = originalHtml;
|
||
btn.disabled = false;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
async function quickImportJira(btn, title, jiraKey, url) {
|
||
const originalHtml = btn.innerHTML;
|
||
btn.innerText = '...';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=add_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
title: `[${jiraKey}] ${title}`,
|
||
priority: 'P1',
|
||
source: 'JIRA',
|
||
external_id: jiraKey,
|
||
external_url: url,
|
||
date: activePlannerDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i>';
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
} else {
|
||
btn.innerHTML = originalHtml;
|
||
btn.disabled = false;
|
||
alert(data.error || 'Kon taak niet toevoegen');
|
||
}
|
||
} catch(err) {
|
||
btn.innerHTML = originalHtml;
|
||
btn.disabled = false;
|
||
alert('Netwerkfout bij toevoegen');
|
||
}
|
||
}
|
||
|
||
async function toggleTask(taskId, isCompleted) {
|
||
await fetch('/api/planner.php?action=toggle_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ task_id: taskId, is_completed: isCompleted })
|
||
});
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
}
|
||
|
||
async function deleteTask(taskId) {
|
||
if (!confirm('Taak verwijderen? (Indien gekoppeld aan Outlook wordt ook de agenda-afspraak verwijderd)')) return;
|
||
await fetch('/api/planner.php?action=delete_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ task_id: taskId })
|
||
});
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
}
|
||
|
||
// --- AI Co-Pilot Slimme Dagplanning (Release v1.5) ---
|
||
async function aiSmartSchedule(btn) {
|
||
if (!confirm('Wil je AI Co-Pilot jouw openstaande taken voor deze dag slim laten inplannen rondom je Outlook meetings en lunch?')) return;
|
||
|
||
const origHtml = btn ? btn.innerHTML : '';
|
||
if (btn) {
|
||
btn.innerHTML = '<span class="text-xs font-bold animate-pulse flex items-center space-x-1"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Indelen...</span></span>';
|
||
btn.disabled = true;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/ai.php?action=smart_schedule', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ date: activePlannerDate })
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
alert(`Succes! AI Co-Pilot heeft ${data.scheduled_count} ta(a)k(en) optimaal ingepland op je tijdlijn.`);
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
} else {
|
||
alert(data.error || 'Kon dagplanning niet genereren via AI');
|
||
if (btn) {
|
||
btn.innerHTML = origHtml;
|
||
btn.disabled = false;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
} catch(err) {
|
||
alert('Netwerkfout bij aanroepen van AI Co-Pilot');
|
||
if (btn) {
|
||
btn.innerHTML = origHtml;
|
||
btn.disabled = false;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
}
|
||
// --- Outlook 365 Auto-Sync & Achtergrond Controle ---
|
||
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
|
||
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
|
||
let isSyncCheckActive = false;
|
||
|
||
async function checkOutlookChanges(isManual = false) {
|
||
if (!isOutlookConnected || isSyncCheckActive) return;
|
||
isSyncCheckActive = true;
|
||
|
||
const icon = document.getElementById('manualSyncIcon');
|
||
const dot = document.getElementById('outlookSyncDot');
|
||
const statusText = document.getElementById('outlookSyncStatusText');
|
||
|
||
if (icon) icon.classList.add('animate-spin');
|
||
if (dot) {
|
||
dot.classList.remove('bg-sky-500');
|
||
dot.classList.add('bg-amber-500', 'animate-ping');
|
||
}
|
||
if (isManual && statusText) {
|
||
statusText.innerText = 'Synchroniseren...';
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`/api/planner.php?action=check_outlook_sync&plan_date=${encodeURIComponent(activePlannerDate)}&week_start=${encodeURIComponent(weekStartDate)}&week_end=${encodeURIComponent(weekEndDate)}&last_hash=${encodeURIComponent(currentSyncHash)}`);
|
||
if (!res.ok) throw new Error('Netwerkfout bij synchronisatie controle');
|
||
const data = await res.json();
|
||
|
||
if (data.success && data.connected) {
|
||
if (data.last_sync) {
|
||
const timeEl = document.getElementById('syncTimeDisplay');
|
||
if (timeEl) timeEl.innerText = data.last_sync.substring(0, 5);
|
||
}
|
||
|
||
if (data.has_changes) {
|
||
if (statusText) statusText.innerText = 'Wijzigingen bijwerken...';
|
||
await switchPlannerDate(activePlannerDate, null, false);
|
||
if (statusText) statusText.innerText = 'Outlook 365 Live';
|
||
return;
|
||
} else {
|
||
currentSyncHash = data.hash;
|
||
if (statusText) statusText.innerText = 'Outlook 365 Live';
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Outlook achtergrond controle:', err);
|
||
if (statusText) statusText.innerText = 'Outlook 365 Live';
|
||
} finally {
|
||
isSyncCheckActive = false;
|
||
if (icon) icon.classList.remove('animate-spin');
|
||
if (dot) {
|
||
dot.classList.remove('bg-amber-500', 'animate-ping');
|
||
dot.classList.add('bg-sky-500');
|
||
}
|
||
}
|
||
}
|
||
|
||
function triggerManualSync() {
|
||
checkOutlookChanges(true);
|
||
}
|
||
|
||
// Initialisatie bij pagina-inladen
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
restoreTimerStateIfAny();
|
||
|
||
if (isOutlookConnected) {
|
||
if (syncIntervalSeconds > 0) {
|
||
setInterval(() => {
|
||
checkOutlookChanges(false);
|
||
}, syncIntervalSeconds * 1000);
|
||
}
|
||
|
||
// Direct controleren wanneer gebruiker terugkeert naar het tabblad / venster
|
||
window.addEventListener('focus', () => {
|
||
checkOutlookChanges(false);
|
||
});
|
||
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.visibilityState === 'visible') {
|
||
checkOutlookChanges(false);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|