7260 lines
403 KiB
PHP
7260 lines
403 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 {
|
||
$pageTitle = 'Fociz';
|
||
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');
|
||
|
||
// Indien de datum van vandaag via de URL is meegegeven bij een normale pagina-aanroep, schoon de URL op naar zuiver 'planner.php'
|
||
if (!$isAjaxRequest && isset($_GET['date']) && $_GET['date'] === $today) {
|
||
header('Location: planner.php', true, 302);
|
||
exit;
|
||
}
|
||
|
||
$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();
|
||
$calendarBlockReasons = get_calendar_block_reasons();
|
||
$activeWorkdays = $workdaySettings['workdays'] ?? [1, 2, 3, 4, 5];
|
||
if (!is_array($activeWorkdays) || empty($activeWorkdays)) {
|
||
$activeWorkdays = [1, 2, 3, 4, 5];
|
||
}
|
||
$selectedDayN = (int)date('N', $selectedTs);
|
||
$isSelectedDateWorkday = in_array($selectedDayN, $activeWorkdays, true);
|
||
$isSelectedDateWeekend = !$isSelectedDateWorkday;
|
||
|
||
// 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;
|
||
|
||
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
|
||
|
||
if ($isOutlookConnected) {
|
||
try {
|
||
// Haal afspraken op voor het volledige datumbereik van de week (met 60s cache)
|
||
$weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd, $forceRefresh);
|
||
$isLiveOutlook = true;
|
||
|
||
// Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data
|
||
// Alleen uitvoeren indien events gewijzigd zijn of bij expliciete refresh om overtollige DB load te voorkomen
|
||
$eventsHash = md5(json_encode($weekOutlookEvents));
|
||
$lastReconciledHash = $_SESSION["last_reconciled_{$userId}"] ?? '';
|
||
if ($eventsHash !== $lastReconciledHash || $forceRefresh) {
|
||
microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents);
|
||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||
$_SESSION["last_reconciled_{$userId}"] = $eventsHash;
|
||
}
|
||
}
|
||
} 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());
|
||
}
|
||
|
||
// 3b. Haal alle reeds ingeplande externe items op (Jira keys & Outlook Mail IDs) inclusief taakgegevens voor herplannen
|
||
$plannedExternalIds = [];
|
||
try {
|
||
$stmtPlannedExt = $db->prepare("
|
||
SELECT t.id as task_id, t.external_id, t.title, t.priority, t.time_block_start, t.time_block_end,
|
||
t.duration_minutes, dp.plan_date
|
||
FROM tasks t
|
||
LEFT JOIN daily_plans dp ON dp.id = t.daily_plan_id
|
||
WHERE t.user_id = ? AND t.external_id IS NOT NULL AND t.external_id != ''
|
||
ORDER BY t.id DESC
|
||
");
|
||
$stmtPlannedExt->execute([$userId]);
|
||
foreach ($stmtPlannedExt->fetchAll() as $row) {
|
||
$extId = $row['external_id'];
|
||
if (!isset($plannedExternalIds[$extId])) {
|
||
$plannedExternalIds[$extId] = [
|
||
'task_id' => (int)$row['task_id'],
|
||
'title' => $row['title'],
|
||
'priority' => $row['priority'],
|
||
'time_block_start' => $row['time_block_start'] ? substr($row['time_block_start'], 0, 5) : '',
|
||
'time_block_end' => $row['time_block_end'] ? substr($row['time_block_end'], 0, 5) : '',
|
||
'duration_minutes' => (int)($row['duration_minutes'] ?: 30),
|
||
'date' => $row['plan_date'] ?: date('Y-m-d')
|
||
];
|
||
}
|
||
}
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen planned external IDs: " . $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;
|
||
}
|
||
|
||
// 4b. Haal actieve kalenderblokkades (vakanties / verlof / afwezigheid) op
|
||
$allCalendarBlocks = [];
|
||
$calendarBlocksByDate = [];
|
||
try {
|
||
$stmtBlocks = $db->prepare("
|
||
SELECT * FROM calendar_blocks
|
||
WHERE user_id = ? AND start_date <= ? AND end_date >= ?
|
||
ORDER BY start_date ASC
|
||
");
|
||
$stmtBlocks->execute([$userId, date('Y-m-d', strtotime('+35 days', $sundayTs)), date('Y-m-d', strtotime('-14 days', $mondayTs))]);
|
||
$allCalendarBlocks = $stmtBlocks->fetchAll() ?: [];
|
||
foreach ($allCalendarBlocks as $cb) {
|
||
$cbStart = strtotime($cb['start_date']);
|
||
$cbEnd = strtotime($cb['end_date']);
|
||
for ($d = $cbStart; $d <= $cbEnd; $d += 86400) {
|
||
$dStr = date('Y-m-d', $d);
|
||
$calendarBlocksByDate[$dStr][] = $cb;
|
||
}
|
||
}
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen calendar_blocks in planner: " . $e->getMessage());
|
||
}
|
||
|
||
$selectedDayBlock = $calendarBlocksByDate[$selectedDate][0] ?? null;
|
||
$isSelectedDateBlocked = !empty($selectedDayBlock);
|
||
|
||
// 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] ?? []);
|
||
$isWorkday = in_array($dayN, $activeWorkdays, true);
|
||
$dayBlock = $calendarBlocksByDate[$dayDate][0] ?? null;
|
||
$isBlocked = !empty($dayBlock);
|
||
|
||
$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),
|
||
'is_workday' => $isWorkday,
|
||
'is_weekend' => !$isWorkday,
|
||
'is_blocked' => $isBlocked,
|
||
'block_info' => $dayBlock,
|
||
'task_count' => $tStat['total'],
|
||
'completed_count' => $tStat['completed'],
|
||
'meeting_count' => $mCount,
|
||
];
|
||
}
|
||
|
||
// 5. Haal Jira dashboard data op voor de snelle import widget en wachtrijfiltering
|
||
$jiraAssignedIssues = [];
|
||
$jiraTeamIssues = [];
|
||
$jiraAllIssues = [];
|
||
$jiraQueueFilters = [];
|
||
$isJiraLive = false;
|
||
try {
|
||
$jiraDashboardData = jira_get_dashboard_data($userId, $forceRefresh);
|
||
$jiraAssignedIssues = $jiraDashboardData['assigned_issues'] ?? [];
|
||
$jiraTeamIssues = $jiraDashboardData['team_issues'] ?? [];
|
||
$jiraQueueFilters = $jiraDashboardData['queue_filters'] ?? [];
|
||
$isJiraLive = !empty($jiraDashboardData['success']) && empty($jiraDashboardData['is_demo']);
|
||
|
||
$seenJiraKeys = [];
|
||
foreach (array_merge($jiraAssignedIssues, $jiraTeamIssues) as $iss) {
|
||
if (!empty($iss['key']) && !isset($seenJiraKeys[$iss['key']])) {
|
||
$seenJiraKeys[$iss['key']] = true;
|
||
$jiraAllIssues[] = $iss;
|
||
}
|
||
}
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen Jira issues in planner: " . $e->getMessage());
|
||
}
|
||
$jiraIssues = $jiraAllIssues;
|
||
|
||
// 5b. Haal recente e-mails op voor de Outlook E-mail Widget
|
||
$recentEmails = [];
|
||
$unreadEmailCount = 0;
|
||
$mailNeedsReconsent = false;
|
||
try {
|
||
$mailData = microsoft_fetch_inbox_messages($userId, 5, false, $forceRefresh);
|
||
$recentEmails = $mailData['messages'] ?? [];
|
||
$unreadEmailCount = (int)($mailData['unread_count'] ?? 0);
|
||
$mailNeedsReconsent = !empty($mailData['needs_reconsent']);
|
||
} catch (Throwable $e) {
|
||
error_log("Fout bij ophalen recente e-mails in planner: " . $e->getMessage());
|
||
}
|
||
|
||
// 5c. Widget Catalogus & Raster Indeling (Fociz v2.0)
|
||
$availableWidgets = [
|
||
'daily_goal' => ['id' => 'daily_goal', 'title' => 'Dagfocus & Hoofddoel', 'category' => 'Planning', 'icon' => 'target', 'enabled' => true, 'col_span' => 12, 'order' => 1, 'desc' => 'Centraal focusdoel voor de dag met snelle tellers'],
|
||
'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Dag/Weekoverzicht', 'category' => 'Agenda', 'icon' => 'calendar', 'enabled' => true, 'col_span' => 12, 'order' => 2, 'desc' => 'Gecombineerd weekoverzicht, dagselectie en uren-tijdlijn met time-blocking en AI'],
|
||
'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 3, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'],
|
||
'outlook_mail' => ['id' => 'outlook_mail', 'title' => 'Outlook E-mail Widget', 'category' => 'Communicatie', 'icon' => 'mail', 'enabled' => true, 'col_span' => 6, 'order' => 4, 'desc' => 'Inkomende e-mails direct inzien en met 1 klik inplannen als taak'],
|
||
'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets met directe importknop'],
|
||
'calendar_meetings' => ['id' => 'calendar_meetings', 'title' => 'Agenda & Teams Meetings', 'category' => 'Agenda', 'icon' => 'video', 'enabled' => true, 'col_span' => 6, 'order' => 6, 'desc' => 'Vandaag geplande afspraken met 1-klik Teams deelnemen knop'],
|
||
'scratchpad' => ['id' => 'scratchpad', 'title' => 'Dagnotities & Kladblok', 'category' => 'Productiviteit', 'icon' => 'edit-3', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Vrij notitieblok met automatische achtergrond-opslag'],
|
||
'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'],
|
||
'quick_links' => ['id' => 'quick_links', 'title' => 'Snelle Koppelingen & Tools', 'category' => 'Productiviteit', 'icon' => 'external-link', 'enabled' => false, 'col_span' => 6, 'order' => 9, 'desc' => '1-klik snelkoppelingen naar Jira, Teams, Outlook, Azure en documentatie'],
|
||
'habit_tracker' => ['id' => 'habit_tracker', 'title' => 'Dagelijkse Focus Gewoontes', 'category' => 'Productiviteit', 'icon' => 'sparkles', 'enabled' => false, 'col_span' => 6, 'order' => 10, 'desc' => 'Houd gezonde werkgewoontes bij (water drinken, 15m bewegen, inbox zero)'],
|
||
'focus_sound' => ['id' => 'focus_sound', 'title' => 'Focus Sounds & Omgeving', 'category' => 'Productiviteit', 'icon' => 'headphones', 'enabled' => false, 'col_span' => 6, 'order' => 11, 'desc' => 'Rustgevende focus-geluiden voor concentratie en diep werk'],
|
||
'team_status' => ['id' => 'team_status', 'title' => 'Team & Beschikbaarheid', 'category' => 'Team', 'icon' => 'users', 'enabled' => false, 'col_span' => 6, 'order' => 12, 'desc' => 'Overzicht van collega\'s en wie vandaag bereikbaar of in overleg is']
|
||
];
|
||
|
||
$userWidgetLayout = $availableWidgets;
|
||
$savedLayoutJson = $currentUser['fociz_widget_layout'] ?? '';
|
||
if (!empty($savedLayoutJson)) {
|
||
$decoded = json_decode($savedLayoutJson, true);
|
||
if (is_array($decoded)) {
|
||
$savedOrder = 1;
|
||
$seen = [];
|
||
foreach ($decoded as $item) {
|
||
$wId = $item['id'] ?? '';
|
||
if (isset($userWidgetLayout[$wId])) {
|
||
$userWidgetLayout[$wId]['enabled'] = !empty($item['enabled']);
|
||
if (!empty($item['col_span'])) $userWidgetLayout[$wId]['col_span'] = (int)$item['col_span'];
|
||
$userWidgetLayout[$wId]['order'] = isset($item['order']) ? (int)$item['order'] : $savedOrder;
|
||
if (!empty($item['custom_title'])) {
|
||
$userWidgetLayout[$wId]['custom_title'] = trim($item['custom_title']);
|
||
$userWidgetLayout[$wId]['title'] = trim($item['custom_title']);
|
||
}
|
||
$seen[$wId] = true;
|
||
$savedOrder++;
|
||
}
|
||
}
|
||
foreach ($userWidgetLayout as $wId => &$wData) {
|
||
if (!isset($seen[$wId])) {
|
||
$wData['order'] = $savedOrder++;
|
||
}
|
||
}
|
||
unset($wData);
|
||
}
|
||
}
|
||
uasort($userWidgetLayout, function($a, $b) {
|
||
$diff = ($a['order'] ?? 0) <=> ($b['order'] ?? 0);
|
||
if ($diff !== 0) return $diff;
|
||
return strcmp($a['id'] ?? '', $b['id'] ?? '');
|
||
});
|
||
|
||
// Bereken dagstatistieken
|
||
$completedTasksCount = 0;
|
||
$pendingTasksCount = 0;
|
||
$highPriorityCount = 0;
|
||
$totalPlannedMinutes = 0;
|
||
foreach ($tasks as $t) {
|
||
if (!empty($t['is_completed'])) {
|
||
$completedTasksCount++;
|
||
} else {
|
||
$pendingTasksCount++;
|
||
if (($t['priority'] ?? '') === 'P1') $highPriorityCount++;
|
||
}
|
||
$totalPlannedMinutes += (int)($t['duration_minutes'] ?: 30);
|
||
}
|
||
$totalTaskCount = count($tasks);
|
||
$taskProgressPercent = $totalTaskCount > 0 ? round(($completedTasksCount / $totalTaskCount) * 100) : 0;
|
||
|
||
// 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:00';
|
||
$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);
|
||
}
|
||
?>
|
||
<?php if (!$isAjaxRequest): ?>
|
||
<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 else: ?>
|
||
<?php ob_start(); ?>
|
||
<?php endif; ?>
|
||
<!-- 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 flex-wrap items-center gap-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
|
||
<div class="flex items-center space-x-1.5">
|
||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||
<span><?= htmlspecialchars($formattedSelectedDate) ?></span>
|
||
</div>
|
||
<?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; ?>
|
||
<?php if ($isSelectedDateWorkday): ?>
|
||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 ring-1 ring-emerald-500/30 flex items-center gap-1" title="Reguliere werkdag conform beheerinstellingen">
|
||
<i data-lucide="briefcase" class="w-3 h-3"></i>
|
||
<span>Werkdag</span>
|
||
</span>
|
||
<?php else: ?>
|
||
<span class="px-2 py-0.5 rounded-full text-[10px] font-extrabold bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-400 ring-1 ring-amber-500/30 flex items-center gap-1" title="Weekend / Vrije dag conform beheerinstellingen">
|
||
<i data-lucide="coffee" class="w-3 h-3"></i>
|
||
<span>Weekend / Vrij</span>
|
||
</span>
|
||
<?php endif; ?>
|
||
<?php if ($isSelectedDateBlocked): ?>
|
||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-amber-100 text-amber-900 dark:bg-amber-950/80 dark:text-amber-300 ring-1 ring-amber-500/30 flex items-center gap-1 cursor-pointer" onclick="openCalendarBlockModal(<?= htmlspecialchars(json_encode($selectedDayBlock), ENT_QUOTES) ?>)" title="<?= htmlspecialchars($selectedDayBlock['title']) ?> (klik om te bewerken)">
|
||
<i data-lucide="palmtree" class="w-3 h-3 text-amber-600 dark:text-amber-400"></i>
|
||
<span><?= htmlspecialchars($selectedDayBlock['title']) ?></span>
|
||
</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
|
||
Fociz
|
||
</h1>
|
||
</div>
|
||
|
||
<!-- Instellingen Dropdown (Widgets, Indeling & Dagen Blokkeren) -->
|
||
<div class="relative">
|
||
<button id="plannerSettingsDropdownBtn" onclick="togglePlannerSettingsMenu(event)" class="p-2 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80 transition cursor-pointer border border-transparent hover:border-slate-200 dark:hover:border-slate-700" title="Dashboard Opties & Instellingen">
|
||
<i data-lucide="settings" class="w-5 h-5"></i>
|
||
</button>
|
||
<div id="plannerSettingsDropdownMenu" class="hidden absolute right-0 mt-2 w-56 rounded-2xl bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 shadow-xl py-1.5 z-50 text-xs text-slate-700 dark:text-slate-300">
|
||
<button onclick="closePlannerSettingsMenu(); openWidgetManagerModal();" class="w-full text-left px-3.5 py-2 hover:bg-slate-50 dark:hover:bg-slate-800/70 flex items-center space-x-2.5 transition cursor-pointer">
|
||
<i data-lucide="layout-grid" class="w-4 h-4 text-indigo-500"></i>
|
||
<span class="font-medium">Widgets & Indeling</span>
|
||
</button>
|
||
<button onclick="closePlannerSettingsMenu(); openCalendarBlockModal();" class="w-full text-left px-3.5 py-2 hover:bg-slate-50 dark:hover:bg-slate-800/70 flex items-center space-x-2.5 transition cursor-pointer">
|
||
<i data-lucide="palmtree" class="w-4 h-4 text-amber-500"></i>
|
||
<span class="font-medium">Vakantie / Dagen Blokkeren</span>
|
||
</button>
|
||
<?php if ($isOutlookConnected): ?>
|
||
<button onclick="closePlannerSettingsMenu(); triggerManualSync();" class="w-full text-left px-3.5 py-2 hover:bg-slate-50 dark:hover:bg-slate-800/70 flex items-center space-x-2.5 transition cursor-pointer border-t border-slate-100 dark:border-slate-800/80">
|
||
<i data-lucide="refresh-cw" class="w-4 h-4 text-sky-500"></i>
|
||
<span class="font-medium">Outlook Synchroniseren</span>
|
||
</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI Dagbriefing Banner (async geladen, alleen vandaag) -->
|
||
<?php if ($isTodaySelected): ?>
|
||
<div id="aiBriefingBanner" class="fociz-card p-5 relative overflow-hidden border-l-4 border-l-indigo-500 hidden">
|
||
<!-- Skeleton laadindicator -->
|
||
<div id="aiBriefingLoading" class="flex items-center space-x-3">
|
||
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 animate-pulse">
|
||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||
</div>
|
||
<div class="flex-1 space-y-1.5">
|
||
<div class="h-3 bg-slate-200 dark:bg-slate-800 rounded-lg animate-pulse w-3/4"></div>
|
||
<div class="h-2.5 bg-slate-200 dark:bg-slate-800 rounded-lg animate-pulse w-1/2"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Inhoud (gevuld via JS) -->
|
||
<div id="aiBriefingContent" class="hidden">
|
||
<div class="flex items-start space-x-3">
|
||
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="flex items-center space-x-2 mb-1">
|
||
<span class="text-[10px] font-black uppercase tracking-wider text-indigo-600 dark:text-indigo-400">AI Co-Pilot · Dagbriefing</span>
|
||
<span class="text-[10px] text-slate-400 font-mono"><?= date('H:i') ?></span>
|
||
</div>
|
||
<p id="aiBriefingGreeting" class="text-sm font-semibold text-slate-900 dark:text-white"></p>
|
||
<div id="aiBriefingDetails" class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-600 dark:text-slate-400">
|
||
<span id="aiBriefingFocusTask" class="hidden flex items-center space-x-1">
|
||
<i data-lucide="zap" class="w-3 h-3 text-amber-500 flex-shrink-0"></i>
|
||
<span id="aiBriefingFocusText"></span>
|
||
</span>
|
||
<span id="aiBriefingTip" class="hidden flex items-center space-x-1">
|
||
<i data-lucide="lightbulb" class="w-3 h-3 text-emerald-500 flex-shrink-0"></i>
|
||
<span id="aiBriefingTipText"></span>
|
||
</span>
|
||
</div>
|
||
<div id="aiBriefingWarnings" class="mt-2 space-y-1 hidden"></div>
|
||
</div>
|
||
<button onclick="document.getElementById('aiBriefingBanner').classList.add('hidden')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0" title="Sluiten">
|
||
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Modular Customizable Widget Grid Container (Fociz v2.0) -->
|
||
<div class="relative w-full" id="focizWidgetGridContainer">
|
||
<!-- Blueprint Achtergrond Raster (zichtbaar zodra een widget wordt opgepakt) -->
|
||
<div id="widgetGridBlueprint" class="absolute inset-0 hidden pointer-events-none z-0 rounded-3xl border-2 border-dashed border-indigo-400/30 dark:border-indigo-500/25 bg-indigo-500/[0.02] p-3 transition-all duration-300">
|
||
<div class="grid grid-cols-1 lg:grid-cols-12 h-full gap-6">
|
||
<?php for ($col = 1; $col <= 12; $col++): ?>
|
||
<div class="h-full min-h-[360px] rounded-2xl border border-dashed border-indigo-400/25 dark:border-indigo-400/15 bg-indigo-50/20 dark:bg-indigo-950/20 flex flex-col items-center justify-between py-4 select-none">
|
||
<span class="text-[10px] font-mono font-black text-indigo-500/60 dark:text-indigo-400/50 tracking-wider">KOLOM <?= $col ?></span>
|
||
<div class="w-2 h-2 rounded-full bg-indigo-400/30 dark:bg-indigo-500/30"></div>
|
||
<span class="text-[10px] font-mono font-black text-indigo-500/40 dark:text-indigo-400/30">1/12</span>
|
||
</div>
|
||
<?php endfor; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Widget Raster (Fociz v2.0) -->
|
||
<div id="focizWidgetGrid" class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start relative z-10 min-h-[300px]">
|
||
<?php foreach ($userWidgetLayout as $wId => $widget):
|
||
if (empty($widget['enabled'])) continue;
|
||
$span = (int)($widget['col_span'] ?? 6);
|
||
$colClass = match($span) {
|
||
12 => 'lg:col-span-12',
|
||
8 => 'lg:col-span-8',
|
||
7 => 'lg:col-span-7',
|
||
6 => 'lg:col-span-6',
|
||
5 => 'lg:col-span-5',
|
||
4 => 'lg:col-span-4',
|
||
default => 'lg:col-span-6'
|
||
};
|
||
?>
|
||
<div class="<?= $colClass ?> col-span-1 min-w-0 transition-all duration-300 fociz-widget-wrapper group/widget" data-widget-id="<?= htmlspecialchars($wId) ?>" data-col-span="<?= $span ?>" id="widgetWrapper_<?= htmlspecialchars($wId) ?>">
|
||
<!-- Bovenste rand van de widget: Prachtig gestyled en direct geïntegreerd in de widget -->
|
||
<div class="widget-header-bar flex items-center justify-between px-3.5 py-1.5 bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-b-0 border-slate-200/80 dark:border-slate-800/80 rounded-t-2xl select-none backdrop-blur-xs transition-colors group-hover/widget:border-indigo-400/50 dark:group-hover/widget:border-indigo-500/30">
|
||
<!-- Links: Drag Handle Grip + Widget Titel -->
|
||
<div class="widget-drag-handle flex items-center space-x-1.5 cursor-grab active:cursor-grabbing text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 py-0.5 px-1 -ml-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800/50 transition group/drag touch-none" data-widget-id="<?= htmlspecialchars($wId) ?>" title="Sleep om te verplaatsen">
|
||
<i data-lucide="grip-vertical" class="w-3.5 h-3.5 text-slate-400 group-hover/drag:text-indigo-500 transition-colors flex-shrink-0"></i>
|
||
<span id="widgetTitleText_<?= htmlspecialchars($wId) ?>" class="text-xs font-bold text-slate-700 dark:text-slate-300 tracking-wide truncate max-w-[160px] sm:max-w-[240px]"><?= htmlspecialchars($widget['title']) ?></span>
|
||
</div>
|
||
|
||
<!-- Rechts: Tandwieltje (Formaat, Naam & Verbergen) & Inklappen -->
|
||
<div class="flex items-center space-x-1 relative">
|
||
<!-- Tandwieltje Knop met Dropdown -->
|
||
<div class="relative">
|
||
<button type="button" onclick="toggleWidgetSettingsMenu('<?= $wId ?>', event)" class="widget-settings-btn p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800/60 transition cursor-pointer" title="Widget instellingen (naam, formaat en verbergen)">
|
||
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
|
||
<!-- Dropdown Popover Menu -->
|
||
<div id="widgetSettingsMenu_<?= htmlspecialchars($wId) ?>" class="widget-settings-dropdown hidden absolute right-0 top-full mt-1.5 z-50 w-64 p-3 rounded-2xl bg-white/95 dark:bg-[#111624]/95 backdrop-blur-md border border-slate-200 dark:border-slate-800 shadow-2xl space-y-3">
|
||
<!-- 1. Naam / Titel Aanpassen -->
|
||
<div class="space-y-1.5 pb-2.5 border-b border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center justify-between">
|
||
<label for="widgetTitleInput_<?= htmlspecialchars($wId) ?>" class="text-[10px] font-bold uppercase tracking-wider text-slate-400">Naam aanpassen</label>
|
||
<button type="button" onclick="resetWidgetTitle('<?= $wId ?>', '<?= htmlspecialchars(addslashes($availableWidgets[$wId]['title'] ?? $widget['title']), ENT_QUOTES) ?>')" class="text-[10px] text-indigo-500 hover:text-indigo-600 dark:hover:text-indigo-400 font-medium cursor-pointer" title="Standaardnaam herstellen">Herstellen</button>
|
||
</div>
|
||
<div class="flex items-center space-x-1.5">
|
||
<input type="text"
|
||
id="widgetTitleInput_<?= htmlspecialchars($wId) ?>"
|
||
value="<?= htmlspecialchars($widget['title']) ?>"
|
||
data-default-title="<?= htmlspecialchars($availableWidgets[$wId]['title'] ?? $widget['title']) ?>"
|
||
placeholder="<?= htmlspecialchars($availableWidgets[$wId]['title'] ?? 'Widget titel') ?>"
|
||
maxlength="50"
|
||
onclick="event.stopPropagation();"
|
||
onkeydown="if(event.key==='Enter'){event.preventDefault();saveWidgetTitle('<?= $wId ?>');}"
|
||
class="flex-1 min-w-0 px-2.5 py-1.5 text-xs rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition">
|
||
<button type="button"
|
||
onclick="saveWidgetTitle('<?= $wId ?>')"
|
||
class="px-2.5 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition flex-shrink-0 cursor-pointer shadow-2xs flex items-center justify-center"
|
||
title="Naam opslaan">
|
||
<i data-lucide="check" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2. Breedte instellen -->
|
||
<div>
|
||
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 block mb-1.5">Breedte instellen</span>
|
||
<div class="grid grid-cols-4 gap-1">
|
||
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 4)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 4 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="4" title="1/3 breedte (4 kolommen)">1/3</button>
|
||
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 6)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 6 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="6" title="1/2 breedte (6 kolommen)">1/2</button>
|
||
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 8)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 8 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="8" title="2/3 breedte (8 kolommen)">2/3</button>
|
||
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 12)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 12 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="12" title="Volledige breedte (12 kolommen)">Vol</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 3. Widget verbergen -->
|
||
<div class="pt-1.5 border-t border-slate-100 dark:border-slate-800/80">
|
||
<button type="button" onclick="hideWidgetOnScreen('<?= $wId ?>')" class="w-full flex items-center space-x-2 px-2 py-1.5 rounded-xl text-slate-600 dark:text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition cursor-pointer text-left text-xs font-semibold">
|
||
<i data-lucide="eye-off" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
||
<span>Widget verbergen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Inklappen / Uitklappen Knop -->
|
||
<button type="button" onclick="toggleWidgetCollapse('<?= $wId ?>')" class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800/60 transition cursor-pointer" title="Inklappen / Uitklappen">
|
||
<i data-lucide="chevron-up" class="w-3.5 h-3.5 widget-collapse-icon transition-transform duration-200" id="collapse_icon_<?= htmlspecialchars($wId) ?>"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Widget Content Body: sluit naadloos aan op de bovenste rand -->
|
||
<div class="widget-content-body transition-all duration-200 [&>.fociz-card]:rounded-t-none [&>.fociz-card]:border-t-0" id="widget_body_<?= htmlspecialchars($wId) ?>">
|
||
|
||
<?php if ($wId === 'daily_goal'): ?>
|
||
<!-- Widget 1: Dagfocus & Hoofddoel -->
|
||
<div class="fociz-card p-5 sm:p-6 border-l-4 border-l-indigo-500 relative overflow-hidden">
|
||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||
<div class="flex items-center space-x-3 min-w-0 flex-1">
|
||
<div class="w-10 h-10 rounded-2xl bg-indigo-500/10 border border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold shadow-2xs flex-shrink-0">
|
||
<i data-lucide="target" class="w-5 h-5"></i>
|
||
</div>
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex items-center space-x-2">
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Dagfocus & Hoofddoel</h3>
|
||
<span class="text-[10px] text-slate-400 font-mono"><?= htmlspecialchars($formattedSelectedDate) ?></span>
|
||
</div>
|
||
<div class="mt-1 flex items-center space-x-2">
|
||
<input
|
||
type="text"
|
||
id="focusGoalInput"
|
||
value="<?= htmlspecialchars($dailyPlan['focus_goal'] ?? '') ?>"
|
||
placeholder="Wat is je absolute topprioriteit voor vandaag?"
|
||
onkeydown="if(event.key==='Enter'){saveGoal();}"
|
||
class="w-full bg-slate-50/80 dark:bg-[#0c0f17]/80 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs sm:text-sm font-semibold text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 transition shadow-2xs"
|
||
>
|
||
<button
|
||
id="saveGoalBtn"
|
||
onclick="saveGoal()"
|
||
class="px-3.5 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition flex items-center space-x-1.5 flex-shrink-0 cursor-pointer shadow-xs"
|
||
>
|
||
<i data-lucide="check-circle-2" class="w-3.5 h-3.5"></i>
|
||
<span class="hidden sm:inline">Opslaan</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-2 sm:border-l border-slate-100 dark:border-slate-800/80 sm:pl-4 flex-shrink-0">
|
||
<div class="text-center px-3 py-1 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/50 border border-slate-200/60 dark:border-slate-800/60">
|
||
<span class="text-[10px] uppercase font-bold text-slate-400 block">Taken</span>
|
||
<span class="text-xs font-black text-slate-800 dark:text-slate-200"><?= $completedTasksCount ?>/<?= $totalTaskCount ?></span>
|
||
</div>
|
||
<div class="text-center px-3 py-1 rounded-xl bg-rose-50/70 dark:bg-rose-500/10 border border-rose-200/60 dark:border-rose-500/20">
|
||
<span class="text-[10px] uppercase font-bold text-rose-500 block">P1 Focus</span>
|
||
<span class="text-xs font-black text-rose-600 dark:text-rose-400"><?= $highPriorityCount ?></span>
|
||
</div>
|
||
<div class="text-center px-3 py-1 rounded-xl bg-sky-50/70 dark:bg-sky-500/10 border border-sky-200/60 dark:border-sky-500/20">
|
||
<span class="text-[10px] uppercase font-bold text-sky-500 block">Meetings</span>
|
||
<span class="text-xs font-black text-sky-600 dark:text-sky-400"><?= count($displayedOutlookEvents) ?></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'tasks'): ?>
|
||
<!-- Widget 2: Dagtaken & Prioriteiten -->
|
||
<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">
|
||
<i data-lucide="check-square" class="w-4 h-4 text-indigo-500"></i>
|
||
<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 uit Jira / Outlook Mail hieronder.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<?php foreach ($tasks as $task):
|
||
$taskJson = [
|
||
'id' => (int)$task['id'],
|
||
'title' => $task['title'],
|
||
'description' => $task['description'] ?? '',
|
||
'priority' => $task['priority'],
|
||
'start' => $task['time_block_start'] ?? '',
|
||
'end' => $task['time_block_end'] ?? '',
|
||
'duration' => (int)($task['duration_minutes'] ?: 30),
|
||
'is_completed' => (bool)$task['is_completed'],
|
||
'source' => $task['source'],
|
||
'external_id' => $task['external_id'] ?? '',
|
||
'external_url' => $task['external_url'] ?? '',
|
||
'date' => $selectedDate
|
||
];
|
||
?>
|
||
<div class="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 shadow-2xs' ?> <?= $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'] ?>" oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($taskJson), ENT_QUOTES, 'UTF-8') ?>)">
|
||
<!-- Hoofd taakrij -->
|
||
<div class="fociz-card-interactive group flex items-center justify-between p-3.5 <?= $task['is_completed'] ? '' : 'hover:border-indigo-400 dark:hover:border-indigo-500/50' ?>" title="Klik met de rechtermuisknop voor meer opties (openen, bewerken, verwijderen)">
|
||
<div class="flex items-center space-x-3 truncate mr-2 flex-1 cursor-pointer" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($taskJson), ENT_QUOTES, 'UTF-8') ?>)">
|
||
<button type="button" onclick="event.stopPropagation(); 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.5 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_MAIL'): ?>
|
||
<span class="px-1.5 py-0.5 rounded bg-violet-50 dark:bg-violet-500/10 border border-violet-200 dark:border-violet-500/20 text-violet-700 dark:text-violet-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
|
||
<i data-lucide="mail" class="w-2.5 h-2.5"></i>
|
||
<span>MAIL</span>
|
||
</span>
|
||
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
|
||
<span class="px-1.5 py-0.5 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" onclick="event.stopPropagation()" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-sky-600 dark:text-sky-400' : (($task['source'] === 'OUTLOOK_MAIL') ? 'text-violet-600 dark:text-violet-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' : (($task['source'] === 'OUTLOOK_MAIL') ? 'Open Mail' : '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-1.5 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="event.stopPropagation(); 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; ?>
|
||
|
||
<!-- Volledige inhoud / Details knop -->
|
||
<button onclick="event.stopPropagation(); openTaskDetailModal(<?= htmlspecialchars(json_encode($taskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Volledige inhoud openen" 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="eye" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
|
||
<!-- Subtaak toggle knop -->
|
||
<button onclick="event.stopPropagation(); toggleSubtaskPanel(<?= $task['id'] ?>)" title="Subtaken / Checklist" 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" id="subtask-btn-<?= $task['id'] ?>">
|
||
<i data-lucide="list-checks" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
|
||
<button onclick="event.stopPropagation(); openEditTaskModal(<?= htmlspecialchars(json_encode($taskJson), 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="event.stopPropagation(); 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>
|
||
|
||
<!-- Subtaken panel (verborgen by default, uitklapbaar) -->
|
||
<div id="subtask-panel-<?= $task['id'] ?>" class="hidden border-t border-slate-100 dark:border-slate-800/60 px-4 pb-3 pt-2.5 space-y-2 bg-slate-50/50 dark:bg-slate-950/20 rounded-b-2xl">
|
||
<!-- Subtaak lijst (dynamisch gevuld) -->
|
||
<div id="subtask-list-<?= $task['id'] ?>" class="space-y-1.5"></div>
|
||
<!-- Nieuw subtaak invoer -->
|
||
<div class="flex items-center space-x-2 mt-1.5">
|
||
<input
|
||
type="text"
|
||
id="subtask-input-<?= $task['id'] ?>"
|
||
placeholder="Subtaak toevoegen…"
|
||
class="flex-1 bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||
onkeydown="if(event.key==='Enter'){addSubtask(<?= $task['id'] ?>);}"
|
||
>
|
||
<button onclick="addSubtask(<?= $task['id'] ?>)" class="flex items-center space-x-1 px-2.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-[10px] font-bold rounded-xl transition cursor-pointer">
|
||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||
<span>Toevoegen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'timeline'): ?>
|
||
<!-- Widget: Tijdlijn, Weekoverzicht & Dagselector (1 Geïntegreerde Widget) -->
|
||
<div class="fociz-card p-5 sm:p-6 overflow-hidden space-y-5">
|
||
|
||
<!-- 1. Week Navigatie & Directe Datumkeuze -->
|
||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||
<!-- Navigatie controls -->
|
||
<div class="flex items-center space-x-2">
|
||
<a href="planner.php?date=<?= $prevWeekDate ?>" onclick="switchPlannerDate('<?= $prevWeekDate ?>', event); return false;" 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 cursor-pointer" 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" onclick="switchPlannerDate('<?= $today ?>', event); return false;" 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 cursor-pointer" title="Spring direct naar vandaag">
|
||
Vandaag
|
||
</a>
|
||
|
||
<a href="planner.php?date=<?= $nextWeekDate ?>" onclick="switchPlannerDate('<?= $nextWeekDate ?>', event); return false;" 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 cursor-pointer" 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-[11px] font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
|
||
Week <?= $weekNumber ?> • <?= $yearNumber ?>
|
||
</div>
|
||
<div class="text-sm sm:text-base font-black text-slate-900 dark:text-white">
|
||
<?= htmlspecialchars($formattedWeekRange) ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rechterkant: AI Slim Indelen & Directe datumselector -->
|
||
<div class="flex flex-wrap items-center gap-2 justify-end">
|
||
<?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.5 rounded-xl 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="hidden sm:inline-flex px-2 py-1 rounded-xl 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; ?>
|
||
|
||
<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-2.5 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>
|
||
|
||
<!-- Inklap / Uitklap Pijltje rechts direct boven de laatste dag van de weekstrook -->
|
||
<div class="flex justify-end -mb-2">
|
||
<button
|
||
type="button"
|
||
onclick="toggleDaySelectionCollapse()"
|
||
id="btnToggleDaySelection"
|
||
class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer"
|
||
title="Dagen inklappen / tonen"
|
||
>
|
||
<i data-lucide="chevron-up" id="iconToggleDaySelection" class="w-4 h-4 transition-transform duration-200"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 2. 7 Dagen Weekstrook (Dagselector met Rechtermuisknop Blokkade Opties) -->
|
||
<div id="weekDaysStripContainer" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-2.5 transition-all duration-200">
|
||
<?php foreach ($weekDays as $wd): ?>
|
||
<a
|
||
href="<?= ($wd['date'] === $today) ? 'planner.php' : ('planner.php?date=' . $wd['date']) ?>"
|
||
onclick="switchPlannerDate('<?= $wd['date'] ?>', event); return false;"
|
||
oncontextmenu="openDayContextMenu(event, '<?= $wd['date'] ?>', <?= $wd['is_blocked'] ? (int)$wd['block_info']['id'] : 0 ?>, '<?= $wd['is_blocked'] ? addslashes(htmlspecialchars($wd['block_info']['title'], ENT_QUOTES)) : '' ?>'); return false;"
|
||
data-date="<?= $wd['date'] ?>"
|
||
data-blocked="<?= $wd['is_blocked'] ? '1' : '0' ?>"
|
||
class="fociz-card-interactive group relative flex flex-col p-3 rounded-xl border transition-all text-left <?= $wd['is_blocked']
|
||
? ($wd['is_selected']
|
||
? 'bg-amber-100/90 dark:bg-amber-950/60 border-amber-400 dark:border-amber-600 ring-2 ring-amber-400/50 shadow-sm'
|
||
: 'bg-amber-50/80 dark:bg-amber-950/30 border-amber-300/80 dark:border-amber-800/70 hover:border-amber-400 dark:hover:border-amber-600 shadow-2xs')
|
||
: ($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'
|
||
: ($wd['is_workday']
|
||
? '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'
|
||
: 'bg-slate-100/60 dark:bg-[#080b11]/60 border-dashed border-slate-200/80 dark:border-slate-800/80 hover:border-slate-300 dark:hover:border-slate-700 hover:bg-white dark:hover:bg-[#0f141f] shadow-2xs')) ?>"
|
||
title="<?= $wd['is_blocked'] ? ('Geblokkeerd: ' . htmlspecialchars($wd['block_info']['title']) . ' • Rechtermuisknop voor opties') : 'Klik om te selecteren • Rechtermuisknop om dag te blokkeren' ?>"
|
||
>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<span class="text-[10px] font-bold uppercase tracking-wider <?= $wd['is_blocked'] ? 'text-amber-800 dark:text-amber-300' : ($wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : ($wd['is_workday'] ? 'text-slate-500 dark:text-slate-400' : 'text-slate-400 dark:text-slate-500')) ?>">
|
||
<?= $wd['short_name'] ?>
|
||
</span>
|
||
<div class="flex items-center gap-1">
|
||
<?php if ($wd['is_blocked']): ?>
|
||
<span class="px-2 py-0.5 rounded-md text-[9px] font-bold uppercase bg-amber-200/90 dark:bg-amber-900/80 text-amber-950 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80 inline-flex items-center gap-1 leading-none shadow-2xs" title="<?= htmlspecialchars($wd['block_info']['title']) ?>">
|
||
<i data-lucide="palmtree" class="w-2.5 h-2.5 flex-shrink-0"></i>
|
||
<span class="max-w-[80px] truncate"><?= htmlspecialchars($wd['block_info']['title']) ?></span>
|
||
</span>
|
||
<?php elseif ($wd['is_weekend']): ?>
|
||
<span class="px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase bg-slate-200/60 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-300/60 dark:border-slate-700/60">
|
||
Weekend
|
||
</span>
|
||
<?php endif; ?>
|
||
<?php if ($wd['is_today']): ?>
|
||
<span class="px-1.5 py-0.5 rounded-full text-[8px] font-extrabold uppercase bg-indigo-600 text-white shadow-xs">
|
||
Nu
|
||
</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-baseline space-x-1 mb-2">
|
||
<span class="text-xl 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-[10px] 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 mt-auto pt-1.5 border-t border-slate-200/60 dark:border-slate-800/60 text-[9px]">
|
||
<div class="flex items-center justify-between px-1.5 py-0.5 rounded-md 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-2.5 h-2.5 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-1.5 py-0.5 rounded-md 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-2.5 h-2.5 text-sky-500"></i>
|
||
<span>Agenda</span>
|
||
</span>
|
||
<span class="font-bold">
|
||
<?= $wd['meeting_count'] ?>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- 3. Sub-header van de Tijdlijn voor de geselecteerde dag -->
|
||
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg <?= ($isSelectedDateBlocked || $isSelectedDateWeekend) ? 'bg-amber-50 dark:bg-amber-950/50 text-amber-600 dark:text-amber-400' : 'bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400' ?> flex items-center justify-center font-bold">
|
||
<i data-lucide="<?= $isSelectedDateBlocked ? 'palmtree' : ($isSelectedDateWeekend ? 'coffee' : 'clock') ?>" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center space-x-2">
|
||
<h4 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Agenda voor <?= htmlspecialchars($formattedSelectedDate) ?>
|
||
</h4>
|
||
<?php if ($isSelectedDateBlocked): ?>
|
||
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-amber-100 text-amber-900 dark:bg-amber-950/80 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80">
|
||
<?= htmlspecialchars($selectedDayBlock['title']) ?> (Geblokkeerd)
|
||
</span>
|
||
<?php elseif ($isSelectedDateWeekend): ?>
|
||
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200/80 dark:border-amber-800/60">
|
||
Weekend / Vrije Dag
|
||
</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono font-semibold bg-slate-100 dark:bg-slate-800/70 px-2.5 py-1 rounded-lg">
|
||
<?= $hours[0] ?> – <?= end($hours) ?>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 4. Capaciteitsbar: Geplande minuten vs. werkdagcapaciteit -->
|
||
<?php
|
||
$workdayCapacityMinutes = max(1, ($endHour - $startHour) * 60);
|
||
$lunchMinutes = 0;
|
||
if (!empty($lunchStart) && !empty($lunchEnd)) {
|
||
$lunchMinutes = (int)((strtotime("2000-01-01 {$lunchEnd}") - strtotime("2000-01-01 {$lunchStart}")) / 60);
|
||
}
|
||
$effectiveCapacity = max(1, $workdayCapacityMinutes - $lunchMinutes);
|
||
$meetingMinutes = 0;
|
||
foreach ($displayedOutlookEvents as $ev) {
|
||
if (!empty($ev['start']) && !empty($ev['end'])) {
|
||
$meetingMinutes += max(0, (int)((strtotime("2000-01-01 {$ev['end']}") - strtotime("2000-01-01 {$ev['start']}")) / 60));
|
||
}
|
||
}
|
||
$usedMinutes = $totalPlannedMinutes + $meetingMinutes;
|
||
$capPercent = min(100, round(($usedMinutes / $effectiveCapacity) * 100));
|
||
$taskOnlyPercent = min(100, round(($totalPlannedMinutes / $effectiveCapacity) * 100));
|
||
$meetingPercent = min(100 - $taskOnlyPercent, round(($meetingMinutes / $effectiveCapacity) * 100));
|
||
$capColor = $capPercent > 100 ? 'bg-rose-500' : ($capPercent >= 80 ? 'bg-amber-500' : 'bg-indigo-500');
|
||
$capLabel = $capPercent > 100 ? 'Overbelast' : ($capPercent >= 80 ? 'Bijna vol' : 'Goed ingepland');
|
||
$capLabelColor = $capPercent > 100 ? 'text-rose-600 dark:text-rose-400' : ($capPercent >= 80 ? 'text-amber-600 dark:text-amber-400' : 'text-indigo-600 dark:text-indigo-400');
|
||
$freeMinutes = max(0, $effectiveCapacity - $usedMinutes);
|
||
$freeH = intdiv($freeMinutes, 60);
|
||
$freeM = $freeMinutes % 60;
|
||
$freeLabel = $freeH > 0 ? "{$freeH}u " : '';
|
||
$freeLabel .= $freeM > 0 ? "{$freeM}m" : '';
|
||
$freeLabel = $freeLabel ?: '0m';
|
||
?>
|
||
<div class="space-y-1.5">
|
||
<div class="flex items-center justify-between text-[10px]">
|
||
<div class="flex items-center space-x-2">
|
||
<span class="font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">Dagcapaciteit</span>
|
||
<?php if ($isSelectedDateWeekend): ?>
|
||
<span class="px-1.5 py-0.5 rounded text-[9px] font-extrabold uppercase bg-amber-100 text-amber-700 dark:bg-amber-950/70 dark:text-amber-400">Weekend</span>
|
||
<?php endif; ?>
|
||
<span class="font-mono font-black <?= $capLabelColor ?>"><?= $capPercent ?>% · <?= $capLabel ?></span>
|
||
</div>
|
||
<div class="flex items-center space-x-3 font-mono text-slate-500 dark:text-slate-400">
|
||
<?php if ($meetingMinutes > 0): ?>
|
||
<span class="flex items-center space-x-1">
|
||
<span class="w-2 h-2 rounded-sm bg-sky-400 dark:bg-sky-500 inline-block"></span>
|
||
<span><?= $meetingMinutes ?>m meetings</span>
|
||
</span>
|
||
<?php endif; ?>
|
||
<?php if ($totalPlannedMinutes > 0): ?>
|
||
<span class="flex items-center space-x-1">
|
||
<span class="w-2 h-2 rounded-sm bg-indigo-500 inline-block"></span>
|
||
<span><?= $totalPlannedMinutes ?>m taken</span>
|
||
</span>
|
||
<?php endif; ?>
|
||
<span class="text-emerald-600 dark:text-emerald-400 font-semibold"><?= $freeLabel ?> vrij</span>
|
||
</div>
|
||
</div>
|
||
<div class="h-2.5 w-full bg-slate-100 dark:bg-slate-900/60 rounded-full overflow-hidden flex">
|
||
<?php if ($meetingPercent > 0): ?>
|
||
<div class="h-full bg-sky-400 dark:bg-sky-500 transition-all duration-500 rounded-l-full" style="width: <?= $meetingPercent ?>%"></div>
|
||
<?php endif; ?>
|
||
<?php if ($taskOnlyPercent > 0): ?>
|
||
<div class="h-full <?= $capColor ?> transition-all duration-500 <?= $meetingPercent === 0 ? 'rounded-l-full' : '' ?> <?= ($meetingPercent + $taskOnlyPercent) >= 100 ? 'rounded-r-full' : '' ?>" style="width: <?= $taskOnlyPercent ?>%"></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 5. De urentijdlijn rijen -->
|
||
<div class="space-y-2.5">
|
||
<?php foreach ($hours as $hour):
|
||
$hourInt = (int)explode(':', $hour)[0];
|
||
$isLunchSlot = ($hourInt === $lunchHour && !empty($lunchStart) && !empty($lunchEnd));
|
||
|
||
$slotEvents = array_filter($displayedOutlookEvents, function($e) use ($hourInt) {
|
||
if (empty($e['start'])) return false;
|
||
return (int)explode(':', $e['start'])[0] === $hourInt;
|
||
});
|
||
|
||
$slotTasks = array_filter($tasks, function($t) use ($hourInt) {
|
||
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $hourInt;
|
||
});
|
||
?>
|
||
<div class="timeline-hour-slot 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 transition-all duration-150 rounded-xl p-1"
|
||
data-hour="<?= $hour ?>"
|
||
ondragover="handleTimelineSlotDragOver(event)"
|
||
ondragleave="handleTimelineSlotDragLeave(event)"
|
||
ondrop="handleTimelineSlotDrop(event, '<?= $hour ?>')">
|
||
<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 select-none">
|
||
<?= $hour ?>
|
||
</div>
|
||
|
||
<div class="flex-1 space-y-1.5 min-w-0">
|
||
<?php if ($isLunchSlot): ?>
|
||
<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):
|
||
$evtJson = [
|
||
'id' => $evt['id'] ?? '',
|
||
'title' => $evt['subject'] ?? 'Afspraak',
|
||
'subject' => $evt['subject'] ?? 'Afspraak',
|
||
'start' => $evt['start'] ?? '',
|
||
'end' => $evt['end'] ?? '',
|
||
'date' => $evt['date'] ?? $selectedDate,
|
||
'location' => $evt['location'] ?? '',
|
||
'meeting_url' => $evt['meeting_url'] ?? '',
|
||
'web_link' => $evt['web_link'] ?? '',
|
||
'is_all_day' => !empty($evt['is_all_day']),
|
||
'source' => 'OUTLOOK_LIVE',
|
||
'is_outlook_event' => true,
|
||
];
|
||
?>
|
||
<div id="timeline-evt-<?= htmlspecialchars(preg_replace('/[^a-zA-Z0-9_-]/', '', $evt['id'] ?? '')) ?>"
|
||
draggable="true"
|
||
ondragstart="handleTimelineItemDragStart(event, 'outlook', <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"
|
||
ondragend="handleTimelineItemDragEnd(event)"
|
||
oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"
|
||
title="Sleep naar ander tijdblok, of rechtermuisklik voor opties"
|
||
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] cursor-grab active:cursor-grabbing group/evt">
|
||
<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" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)">
|
||
<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" onclick="event.stopPropagation()">
|
||
<?php if (!empty($evt['meeting_url'])): ?>
|
||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[11px] 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):
|
||
$slotTaskJson = [
|
||
'id' => (int)$t['id'],
|
||
'title' => $t['title'],
|
||
'description' => $t['description'] ?? '',
|
||
'priority' => $t['priority'],
|
||
'start' => $t['time_block_start'] ?? '',
|
||
'end' => $t['time_block_end'] ?? '',
|
||
'duration' => (int)($t['duration_minutes'] ?: 30),
|
||
'is_completed' => (bool)$t['is_completed'],
|
||
'source' => $t['source'],
|
||
'external_id' => $t['external_id'] ?? '',
|
||
'external_url' => $t['external_url'] ?? '',
|
||
'date' => $selectedDate
|
||
];
|
||
?>
|
||
<div id="timeline-task-<?= $t['id'] ?>"
|
||
draggable="true"
|
||
ondragstart="handleTimelineItemDragStart(event, 'task', <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"
|
||
ondragend="handleTimelineItemDragEnd(event)"
|
||
oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"
|
||
title="Sleep naar ander tijdblok, of rechtermuisklik voor opties"
|
||
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 cursor-grab active:cursor-grabbing group/task">
|
||
<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 cursor-pointer" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)">
|
||
<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="event.stopPropagation(); 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>
|
||
<button onclick="event.stopPropagation(); openTaskDetailModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Volledige inhoud openen" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||
<i data-lucide="eye" class="w-3 h-3"></i>
|
||
</button>
|
||
<button onclick="event.stopPropagation(); openEditTaskModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijdblok bewerken" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||
<i data-lucide="edit-3" class="w-3 h-3"></i>
|
||
</button>
|
||
<button onclick="event.stopPropagation(); deleteTask(<?= $t['id'] ?>)" title="Verwijderen" class="p-1 rounded-lg text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer">
|
||
<i data-lucide="trash-2" class="w-3 h-3"></i>
|
||
</button>
|
||
</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>
|
||
|
||
<?php elseif ($wId === 'outlook_mail'): ?>
|
||
<!-- Widget 4: Outlook E-mail & Inbox -->
|
||
<div class="fociz-card p-6" id="plannerMailWidget">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="mail" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Outlook E-mail
|
||
</h3>
|
||
<?php if ($mailNeedsReconsent): ?>
|
||
<a href="/auth/oauth_microsoft.php?reconsent=1&return_to=/planner.php" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/30 hover:underline flex items-center space-x-1" title="Klik om e-mail toestemming te verlenen bij Microsoft">
|
||
<i data-lucide="shield-alert" class="w-3 h-3 text-amber-600"></i>
|
||
<span>Machtiging vereist →</span>
|
||
</a>
|
||
<?php elseif ($unreadEmailCount > 0): ?>
|
||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-rose-50 dark:bg-rose-500/15 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/30">
|
||
<?= $unreadEmailCount ?> ongelezen
|
||
</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<a href="/mail.php" class="text-xs text-sky-600 dark:text-sky-400 hover:text-sky-700 dark:hover:text-sky-300 transition flex items-center space-x-1 font-semibold">
|
||
<span>Naar Mailbox</span>
|
||
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||
</a>
|
||
</div>
|
||
|
||
<?php if (empty($recentEmails)): ?>
|
||
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||
<i data-lucide="inbox" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||
<p>Geen recente e-mails gevonden in Postvak IN.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="space-y-2 max-h-72 overflow-y-auto pr-1">
|
||
<?php foreach ($recentEmails as $mail): ?>
|
||
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl <?= !empty($mail['is_read']) ? 'bg-slate-50/70 dark:bg-[#0c0f17]/60' : 'bg-sky-50/40 dark:bg-sky-500/10 border-sky-300/60 dark:border-sky-500/30 font-medium' ?> 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 min-w-0 flex-1">
|
||
<div class="flex items-center space-x-2">
|
||
<?php if (empty($mail['is_read'])): ?>
|
||
<span class="w-2 h-2 rounded-full bg-sky-500 flex-shrink-0"></span>
|
||
<?php endif; ?>
|
||
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
|
||
<?= htmlspecialchars($mail['sender_name'] ?? 'Onbekend') ?>
|
||
</span>
|
||
<span class="text-[10px] text-slate-400 font-mono flex-shrink-0">
|
||
<?= !empty($mail['received_at']) ? date('H:i', strtotime($mail['received_at'])) : '' ?>
|
||
</span>
|
||
</div>
|
||
<p class="text-xs text-slate-700 dark:text-slate-300 font-medium truncate mt-0.5">
|
||
<?= htmlspecialchars($mail['subject'] ?: '(Geen onderwerp)') ?>
|
||
</p>
|
||
<p class="text-[11px] text-slate-400 truncate">
|
||
<?= htmlspecialchars($mail['body_preview'] ?? '') ?>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-1 flex-shrink-0">
|
||
<?php $isMailPlanned = !empty($plannedExternalIds[$mail['id']]); ?>
|
||
<button
|
||
onclick="openPlanEmailDirect('<?= htmlspecialchars(addslashes($mail['id'])) ?>', '<?= htmlspecialchars(addslashes($mail['subject'])) ?>', '<?= htmlspecialchars(addslashes($mail['sender_name'])) ?>', '<?= htmlspecialchars(addslashes($mail['web_link'] ?? '')) ?>', '<?= htmlspecialchars(addslashes($mail['body_preview'] ?? '')) ?>')"
|
||
data-mail-id="<?= htmlspecialchars($mail['id']) ?>"
|
||
class="plan-btn-mail flex items-center space-x-1 text-[11px] <?= $isMailPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : 'font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20' ?> px-2.5 py-1 rounded-lg transition cursor-pointer"
|
||
title="<?= $isMailPlanned ? 'Deze e-mail is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)' : 'Plan deze e-mail direct in als dagtaak' ?>"
|
||
>
|
||
<?php if ($isMailPlanned): ?>
|
||
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
|
||
<span>Ingepland</span>
|
||
<?php else: ?>
|
||
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
|
||
<span>Plan</span>
|
||
<?php endif; ?>
|
||
</button>
|
||
<a
|
||
href="/mail.php?id=<?= urlencode($mail['id']) ?>"
|
||
class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition"
|
||
title="Open in Mailbox"
|
||
>
|
||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'jira_queue'): ?>
|
||
<!-- Widget 5: Jira Tickets (Interactive v2.0) -->
|
||
<div id="jiraPanel" class="fociz-card p-5 sm:p-6">
|
||
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 mb-3 gap-2">
|
||
<div class="flex items-center space-x-2.5">
|
||
<div class="w-7 h-7 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="layers" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Jira Tickets
|
||
</h3>
|
||
</div>
|
||
<span id="plannerJiraCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>">
|
||
<?= count($jiraAllIssues) ?>
|
||
</span>
|
||
</div>
|
||
|
||
<a href="/jira.php" class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition flex items-center space-x-1 font-semibold flex-shrink-0">
|
||
<span>Jira Hub</span>
|
||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Filter Queue Pills & Quick Search -->
|
||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2.5 mb-3">
|
||
<div class="flex items-center space-x-1 overflow-x-auto pb-1 sm:pb-0 scrollbar-none" id="plannerJiraFilterTabs">
|
||
<button type="button" onclick="setPlannerJiraQueue('')" id="pjPill_all" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-indigo-600 text-white shadow-2xs flex-shrink-0 cursor-pointer">
|
||
Alle (<span id="pjCount_all"><?= count($jiraAllIssues) ?></span>)
|
||
</button>
|
||
<button type="button" onclick="setPlannerJiraQueue('my')" id="pjPill_my" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
|
||
Mijn (<span id="pjCount_my"><?= count($jiraAssignedIssues) ?></span>)
|
||
</button>
|
||
<button type="button" onclick="setPlannerJiraQueue('DX')" id="pjPill_DX" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
|
||
DX Queue (<span id="pjCount_DX">0</span>)
|
||
</button>
|
||
<button type="button" onclick="setPlannerJiraQueue('SD')" id="pjPill_SD" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
|
||
SD Queue (<span id="pjCount_SD">0</span>)
|
||
</button>
|
||
</div>
|
||
|
||
<div class="relative min-w-[140px] sm:w-44 flex-shrink-0">
|
||
<i data-lucide="search" class="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||
<input
|
||
type="text"
|
||
id="plannerJiraSearch"
|
||
oninput="filterPlannerJira()"
|
||
placeholder="Filter tickets..."
|
||
class="w-full pl-8 pr-2 py-1 text-xs bg-slate-50 dark:bg-[#0c0f17] border border-slate-200/80 dark:border-slate-800/80 rounded-xl text-slate-800 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ticket List Container -->
|
||
<div id="plannerJiraListContainer" class="space-y-2 max-h-72 overflow-y-auto pr-1">
|
||
<?php if (empty($jiraAllIssues)): ?>
|
||
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||
<p>Geen openstaande tickets gevonden in Jira.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<?php foreach (array_slice($jiraAllIssues, 0, 8) 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 min-w-0 flex-1 cursor-pointer" onclick="openJiraTicketDetail('<?= htmlspecialchars($issue['key'] ?? '') ?>')" title="Klik voor ticketdetails">
|
||
<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>
|
||
|
||
<?php $isJiraPlanned = !empty($plannedExternalIds[$issue['key']]); ?>
|
||
<button
|
||
onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>')"
|
||
data-jira-key="<?= htmlspecialchars($issue['key'] ?? '') ?>"
|
||
class="plan-btn-jira flex items-center space-x-1 text-[11px] <?= $isJiraPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : 'font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20' ?> px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
|
||
title="<?= $isJiraPlanned ? 'Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)' : 'Plan in als dagtaak' ?>"
|
||
>
|
||
<?php if ($isJiraPlanned): ?>
|
||
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
|
||
<span>Ingepland</span>
|
||
<?php else: ?>
|
||
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
|
||
<span>Plan</span>
|
||
<?php endif; ?>
|
||
</button>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Lege Toestand (verborgen by default) -->
|
||
<div id="plannerJiraEmptyState" class="hidden text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||
<p>Geen tickets gevonden voor deze filterselectie.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'calendar_meetings'): ?>
|
||
<!-- Widget 6: Agenda & Teams Meetings -->
|
||
<div class="fociz-card p-6" id="plannerMeetingsWidget">
|
||
<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">
|
||
<div class="w-7 h-7 rounded-lg bg-teal-500/10 text-teal-600 dark:text-teal-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="video" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Agenda & Teams Meetings
|
||
</h3>
|
||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-teal-50 dark:bg-teal-500/15 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-500/30">
|
||
<?= count($displayedOutlookEvents) ?> afspraken
|
||
</span>
|
||
</div>
|
||
|
||
<?php if ($isOutlookConnected): ?>
|
||
<button onclick="triggerManualSync()" title="Synchroniseer agenda nu" class="text-xs text-teal-600 dark:text-teal-400 hover:underline flex items-center space-x-1 font-semibold cursor-pointer">
|
||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
|
||
<span>Verversen</span>
|
||
</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if (empty($displayedOutlookEvents)): ?>
|
||
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||
<i data-lucide="calendar-check" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||
<p>Geen vergaderingen of afspraken gepland voor <?= htmlspecialchars($formattedSelectedDate) ?>.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="space-y-2 max-h-72 overflow-y-auto pr-1">
|
||
<?php foreach ($displayedOutlookEvents as $evt):
|
||
$widgetEvtJson = [
|
||
'id' => $evt['id'] ?? '',
|
||
'title' => $evt['subject'] ?? 'Afspraak',
|
||
'subject' => $evt['subject'] ?? 'Afspraak',
|
||
'start' => $evt['start'] ?? '',
|
||
'end' => $evt['end'] ?? '',
|
||
'date' => $evt['date'] ?? $selectedDate,
|
||
'location' => $evt['location'] ?? '',
|
||
'meeting_url' => $evt['meeting_url'] ?? '',
|
||
'web_link' => $evt['web_link'] ?? '',
|
||
'is_all_day' => !empty($evt['is_all_day']),
|
||
'source' => 'OUTLOOK_LIVE',
|
||
'is_outlook_event' => true,
|
||
];
|
||
?>
|
||
<div oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($widgetEvtJson), ENT_QUOTES, 'UTF-8') ?>)" title="Rechtermuisklik voor opties (openen, bewerken, verwijderen)" class="fociz-card-interactive flex items-center justify-between p-3 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-teal-300 dark:hover:border-teal-600/50 transition shadow-2xs cursor-pointer group/evt">
|
||
<div class="truncate mr-2 min-w-0 flex-1" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($widgetEvtJson), ENT_QUOTES, 'UTF-8') ?>)">
|
||
<div class="flex items-center space-x-2">
|
||
<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">
|
||
<?= htmlspecialchars($evt['start'] ?? '') ?> - <?= htmlspecialchars($evt['end'] ?? '') ?>
|
||
</span>
|
||
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate" title="<?= htmlspecialchars($evt['subject'] ?? 'Afspraak') ?>">
|
||
<?= htmlspecialchars($evt['subject'] ?? 'Afspraak') ?>
|
||
</span>
|
||
</div>
|
||
<?php if (!empty($evt['location'])): ?>
|
||
<span class="text-[10px] text-slate-500 mt-0.5 flex items-center space-x-1">
|
||
<i data-lucide="map-pin" class="w-3 h-3 text-slate-400"></i>
|
||
<span class="truncate"><?= htmlspecialchars($evt['location']) ?></span>
|
||
</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-1 flex-shrink-0" onclick="event.stopPropagation()">
|
||
<?php if (!empty($evt['meeting_url'])): ?>
|
||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[11px] text-white bg-indigo-600 hover:bg-indigo-700 px-2.5 py-1 rounded-lg transition font-semibold shadow-xs">
|
||
<i data-lucide="video" class="w-3 h-3"></i>
|
||
<span>Deelnemen</span>
|
||
</a>
|
||
<?php elseif (!empty($evt['web_link'])): ?>
|
||
<a href="<?= htmlspecialchars($evt['web_link']) ?>" target="_blank" class="p-1.5 text-sky-600 dark:text-sky-400 hover:text-sky-700 rounded-lg hover:bg-sky-50 dark:hover:bg-sky-500/10 transition" title="Open in Outlook">
|
||
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
|
||
</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'scratchpad'): ?>
|
||
<!-- Widget 7: Dagnotities & Kladblok -->
|
||
<div 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">
|
||
<div class="w-7 h-7 rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="edit-3" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Dagnotities & Kladblok
|
||
</h3>
|
||
</div>
|
||
<span id="scratchpadSaveStatus" class="text-[10px] text-slate-400 font-mono">Automatisch opgeslagen</span>
|
||
</div>
|
||
<textarea
|
||
id="dailyNotesInput"
|
||
rows="5"
|
||
placeholder="Belangrijke aantekeningen, reflecties of snelle memo's voor vandaag..."
|
||
oninput="autoSaveScratchpad()"
|
||
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 leading-relaxed"
|
||
><?= htmlspecialchars($dailyPlan['notes'] ?? '') ?></textarea>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'stats'): ?>
|
||
<!-- Widget 8: Dagstatistieken & Voortgang -->
|
||
<div class="fociz-card p-6">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="bar-chart-2" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Dagstatistieken & Voortgang
|
||
</h3>
|
||
</div>
|
||
<span class="text-xs font-mono font-bold text-indigo-600 dark:text-indigo-400"><?= $taskProgressPercent ?>% voltooid</span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
|
||
<span class="text-[10px] font-bold uppercase text-slate-400 block mb-1">Voltooide Taken</span>
|
||
<div class="flex items-baseline space-x-1.5">
|
||
<span class="text-2xl font-black text-slate-900 dark:text-white"><?= $completedTasksCount ?></span>
|
||
<span class="text-xs text-slate-400 font-semibold">/ <?= $totalTaskCount ?></span>
|
||
</div>
|
||
<div class="w-full bg-slate-200 dark:bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
|
||
<div class="h-full bg-emerald-500 rounded-full" style="width: <?= $taskProgressPercent ?>%"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
|
||
<span class="text-[10px] font-bold uppercase text-rose-500 block mb-1">P1 Focus Taken</span>
|
||
<div class="flex items-baseline space-x-1.5">
|
||
<span class="text-2xl font-black text-rose-600 dark:text-rose-400"><?= $highPriorityCount ?></span>
|
||
<span class="text-xs text-slate-400 font-semibold">openstaand</span>
|
||
</div>
|
||
<span class="text-[10px] text-slate-400 mt-1 block">Hoogste urgentie</span>
|
||
</div>
|
||
|
||
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
|
||
<span class="text-[10px] font-bold uppercase text-sky-500 block mb-1">Geplande Tijd</span>
|
||
<?php
|
||
$hoursPlanned = floor($totalPlannedMinutes / 60);
|
||
$minsPlanned = $totalPlannedMinutes % 60;
|
||
?>
|
||
<div class="flex items-baseline space-x-1.5">
|
||
<span class="text-2xl font-black text-sky-600 dark:text-sky-400"><?= $hoursPlanned ?>u <?= $minsPlanned ?>m</span>
|
||
</div>
|
||
<span class="text-[10px] text-slate-400 mt-1 block">In timeblocks</span>
|
||
</div>
|
||
|
||
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
|
||
<span class="text-[10px] font-bold uppercase text-teal-500 block mb-1">Agenda Meetings</span>
|
||
<div class="flex items-baseline space-x-1.5">
|
||
<span class="text-2xl font-black text-teal-600 dark:text-teal-400"><?= count($displayedOutlookEvents) ?></span>
|
||
<span class="text-xs text-slate-400 font-semibold">afspraken</span>
|
||
</div>
|
||
<span class="text-[10px] text-slate-400 mt-1 block">In Outlook 365</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'quick_links'): ?>
|
||
<!-- Widget 9: Snelle Koppelingen & Tools -->
|
||
<div class="fociz-card p-6">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="external-link" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Snelle Koppelingen & Tools
|
||
</h3>
|
||
</div>
|
||
<span class="text-[10px] text-slate-400 font-mono">Werktools</span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2.5">
|
||
<a href="/jira.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
|
||
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold flex-shrink-0">
|
||
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="truncate">
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-indigo-600">Jira Cloud</span>
|
||
<span class="text-[10px] text-slate-400">Tickets</span>
|
||
</div>
|
||
</a>
|
||
|
||
<a href="/mail.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-sky-400 dark:hover:border-sky-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
|
||
<div class="w-7 h-7 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold flex-shrink-0">
|
||
<i data-lucide="mail" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="truncate">
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-sky-600">Outlook Mail</span>
|
||
<span class="text-[10px] text-slate-400">Postvak IN</span>
|
||
</div>
|
||
</a>
|
||
|
||
<a href="https://teams.microsoft.com" target="_blank" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-violet-400 dark:hover:border-violet-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
|
||
<div class="w-7 h-7 rounded-lg bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center font-bold flex-shrink-0">
|
||
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="truncate">
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-violet-600">MS Teams</span>
|
||
<span class="text-[10px] text-slate-400">Chat & Calls</span>
|
||
</div>
|
||
</a>
|
||
|
||
<a href="/board.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-amber-400 dark:hover:border-amber-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
|
||
<div class="w-7 h-7 rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center font-bold flex-shrink-0">
|
||
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="truncate">
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-amber-600">Kanban Bord</span>
|
||
<span class="text-[10px] text-slate-400">Sprint & Work</span>
|
||
</div>
|
||
</a>
|
||
|
||
<a href="https://portal.azure.com" target="_blank" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-blue-400 dark:hover:border-blue-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
|
||
<div class="w-7 h-7 rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400 flex items-center justify-center font-bold flex-shrink-0">
|
||
<i data-lucide="cloud" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="truncate">
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-blue-600">Azure Portal</span>
|
||
<span class="text-[10px] text-slate-400">Cloud & DevOps</span>
|
||
</div>
|
||
</a>
|
||
|
||
<a href="/profile.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-teal-400 dark:hover:border-teal-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
|
||
<div class="w-7 h-7 rounded-lg bg-teal-500/10 text-teal-600 dark:text-teal-400 flex items-center justify-center font-bold flex-shrink-0">
|
||
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="truncate">
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-teal-600">Instellingen</span>
|
||
<span class="text-[10px] text-slate-400">Profiel & Koppel</span>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'habit_tracker'): ?>
|
||
<!-- Widget 10: Dagelijkse Focus Gewoontes -->
|
||
<div class="fociz-card p-6" id="habitTrackerWidget">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg bg-rose-500/10 text-rose-600 dark:text-rose-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Dagelijkse Focus Gewoontes
|
||
</h3>
|
||
</div>
|
||
<span id="habitProgressBadge" class="text-xs font-mono font-bold text-rose-600 dark:text-rose-400">0/5 gedaan</span>
|
||
</div>
|
||
|
||
<div class="space-y-2.5">
|
||
<?php
|
||
$habits = [
|
||
['id' => 'water', 'label' => '💧 Voldoende water gedronken (2L)'],
|
||
['id' => 'walk', 'label' => '🚶♂️ Minimaal 15 minuten bewogen / gewandeld'],
|
||
['id' => 'inbox', 'label' => '📭 Postvak IN opgeruimd & acties ingepland'],
|
||
['id' => 'standup', 'label' => '👥 Standup & overleg voorbereid'],
|
||
['id' => 'reflection', 'label' => '🧘 Dag afgesloten met reflectie & planning morgen'],
|
||
];
|
||
foreach ($habits as $h): ?>
|
||
<label class="flex items-center justify-between p-3 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-rose-300 dark:hover:border-rose-500/30 transition cursor-pointer select-none">
|
||
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200"><?= $h['label'] ?></span>
|
||
<input type="checkbox" data-habit-id="<?= $h['id'] ?>" onchange="updateHabitCount()" class="w-4 h-4 rounded text-rose-600 focus:ring-rose-500 border-slate-300 dark:border-slate-700 cursor-pointer">
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'focus_sound'): ?>
|
||
<!-- Widget 11: Focus Sounds & Omgeving -->
|
||
<div class="fociz-card p-6" id="focusSoundWidget">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="headphones" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Focus Sounds & Rust
|
||
</h3>
|
||
</div>
|
||
<span id="soundStatusText" class="text-[10px] text-slate-400 font-mono">Gepauzeerd</span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
|
||
<button onclick="toggleSound('rain', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 transition text-center cursor-pointer">
|
||
<span class="text-xl block mb-1">🌧️</span>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Zachte Regen</span>
|
||
</button>
|
||
|
||
<button onclick="toggleSound('forest', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-emerald-400 transition text-center cursor-pointer">
|
||
<span class="text-xl block mb-1">🌲</span>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Bos & Vogels</span>
|
||
</button>
|
||
|
||
<button onclick="toggleSound('cafe', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-amber-400 transition text-center cursor-pointer">
|
||
<span class="text-xl block mb-1">☕</span>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Koffiebar</span>
|
||
</button>
|
||
|
||
<button onclick="toggleSound('whitenoise', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-sky-400 transition text-center cursor-pointer">
|
||
<span class="text-xl block mb-1">🌊</span>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Witte Ruis</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<?php elseif ($wId === 'team_status'): ?>
|
||
<!-- Widget 12: Team & Beschikbaarheid -->
|
||
<div class="fociz-card p-6">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-7 h-7 rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400 flex items-center justify-center font-bold">
|
||
<i data-lucide="users" class="w-4 h-4"></i>
|
||
</div>
|
||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||
Team & Beschikbaarheid
|
||
</h3>
|
||
</div>
|
||
<span class="text-[10px] text-slate-400 font-mono">Vandaag</span>
|
||
</div>
|
||
|
||
<div class="space-y-2.5">
|
||
<div class="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">
|
||
<div class="flex items-center space-x-2.5">
|
||
<div class="w-7 h-7 rounded-full bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 font-bold text-xs flex items-center justify-center">
|
||
<?= strtoupper(substr($currentUser['name'] ?? 'Ik', 0, 2)) ?>
|
||
</div>
|
||
<div>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block"><?= htmlspecialchars($currentUser['name'] ?? 'Jij') ?> (Jij)</span>
|
||
<span class="text-[10px] text-slate-400">Actief in Fociz</span>
|
||
</div>
|
||
</div>
|
||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-500/30">
|
||
Beschikbaar
|
||
</span>
|
||
</div>
|
||
|
||
<div class="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">
|
||
<div class="flex items-center space-x-2.5">
|
||
<div class="w-7 h-7 rounded-full bg-amber-500/20 text-amber-700 dark:text-amber-300 font-bold text-xs flex items-center justify-center">
|
||
SD
|
||
</div>
|
||
<div>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Servicedesk Team</span>
|
||
<span class="text-[10px] text-slate-400">DX & SD Wachtrij</span>
|
||
</div>
|
||
</div>
|
||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300 border border-amber-200 dark:border-amber-500/30">
|
||
Tickets opvolgen
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
</div><!-- /.widget-content-body -->
|
||
</div><!-- /.fociz-widget-wrapper -->
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div><!-- /#focizWidgetGridContainer -->
|
||
<?php
|
||
if ($isAjaxRequest) {
|
||
$contentHtml = ob_get_clean();
|
||
while (ob_get_level() > 0) {
|
||
ob_end_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 (' . $formattedSelectedDate . ')',
|
||
'html' => $contentHtml,
|
||
]);
|
||
exit;
|
||
}
|
||
?>
|
||
<?php if (!$isAjaxRequest): ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Modal: Jira Ticket Inplannen (met datum, tijdsuggestie & aanpasbare titel) -->
|
||
<div id="jiraPlanModal" onclick="if(event.target === this) closeJiraPlanModal()" 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 max-h-[90vh] overflow-y-auto 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-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center">
|
||
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white" id="jiraPlanModalTitle">Jira Ticket Inplannen</h3>
|
||
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="jiraPlanModalKey">—</p>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="closeJiraPlanModal()" 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>
|
||
|
||
<!-- Hidden veld voor bestaande gekoppelde taak (bij herplannen) -->
|
||
<input type="hidden" id="jiraPlanTaskId" value="">
|
||
|
||
<!-- Banner: Al Ingepland (zichtbaar wanneer ticket reeds gepland is) -->
|
||
<div id="jiraPlanAlreadyScheduledBanner" class="hidden p-3 rounded-xl bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-300 dark:border-emerald-700 text-emerald-900 dark:text-emerald-200 text-xs">
|
||
<div class="flex items-start space-x-2">
|
||
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0 mt-0.5"></i>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="font-bold">Reeds ingepland in je dagplanning</p>
|
||
<p class="text-[11px] text-emerald-800 dark:text-emerald-300 mt-0.5" id="jiraPlanAlreadyScheduledDetails">
|
||
Ingepland op ...
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ticket samenvatting & Uitklapbare beschrijving -->
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5">
|
||
<div>
|
||
<p class="text-xs font-semibold text-slate-800 dark:text-slate-200 leading-snug" id="jiraPlanModalSummary">—</p>
|
||
<div class="flex items-center space-x-2 mt-1.5">
|
||
<span class="text-[10px] text-slate-500" id="jiraPlanModalStatus">—</span>
|
||
<span class="text-slate-300 dark:text-slate-600">•</span>
|
||
<span class="text-[10px] text-slate-500" id="jiraPlanModalPriority">—</span>
|
||
<span id="jiraPlanModalSpBadge" class="hidden px-1.5 py-0.5 rounded bg-violet-50 dark:bg-violet-500/10 border border-violet-200 dark:border-violet-500/20 text-violet-700 dark:text-violet-400 font-mono text-[9px] font-bold"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Uitklapknop voor inhoud ticket (10 regels verticaal scrollbaar) -->
|
||
<div class="pt-2 border-t border-slate-200/70 dark:border-slate-800/70">
|
||
<button type="button" onclick="toggleJiraPlanDescription()" class="w-full flex items-center justify-between text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition cursor-pointer group">
|
||
<span class="flex items-center space-x-1.5">
|
||
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
|
||
<span id="jiraPlanDescToggleLabel">Inhoud van het item bekijken</span>
|
||
</span>
|
||
<i data-lucide="chevron-down" id="jiraPlanDescChevron" class="w-3.5 h-3.5 transition-transform duration-200 group-hover:translate-y-0.5"></i>
|
||
</button>
|
||
<div id="jiraPlanDescWrapper" class="hidden mt-2.5">
|
||
<div id="jiraPlanModalDescription" class="max-h-48 overflow-y-auto p-3 bg-white dark:bg-[#0c0f17] rounded-xl border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans leading-relaxed"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Formulier velden -->
|
||
<div class="space-y-3.5">
|
||
<!-- Titel voor de agenda / dagtaak -->
|
||
<div>
|
||
<label for="jiraPlanCustomTitle" id="jiraPlanCustomTitleLabel" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||
Hoe het in de agenda komt (Titel)
|
||
</label>
|
||
<input type="text" id="jiraPlanCustomTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="Oppakken Jira item ...">
|
||
</div>
|
||
|
||
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
|
||
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-[11px] font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
|
||
<button type="button" onclick="planJiraInNextOpenBlock()" id="btnJiraPlanOpenBlock" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
|
||
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
|
||
<span>Plan in open blok</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
|
||
<div id="jiraPlanOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
|
||
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
|
||
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
|
||
<span id="jiraPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
|
||
</div>
|
||
<button type="button" onclick="document.getElementById('jiraPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
|
||
<i data-lucide="x" class="w-3 h-3"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Datum & Starttijd -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<div>
|
||
<label for="jiraPlanDate" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||
Datum
|
||
</label>
|
||
<input type="date" id="jiraPlanDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="updateJiraPlanEndTimeDisplay()">
|
||
</div>
|
||
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1.5">
|
||
<label for="jiraPlanStartTime" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300">
|
||
Starttijd
|
||
</label>
|
||
<span id="jiraPlanEndTimeBadge" class="text-[10px] font-mono text-indigo-600 dark:text-indigo-400 font-semibold"></span>
|
||
</div>
|
||
<div class="relative flex items-center">
|
||
<input type="time" id="jiraPlanStartTime" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="updateJiraPlanEndTimeDisplay()">
|
||
<button type="button" onclick="clearJiraPlanStartTime()" title="Starttijd wissen" class="absolute right-2 p-1 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 rounded transition cursor-pointer">
|
||
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
<p class="text-[10px] text-slate-400 mt-1 flex items-center justify-between">
|
||
<span>Laat leeg voor handmatig inplannen</span>
|
||
<button type="button" onclick="clearJiraPlanStartTime()" class="text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer flex items-center space-x-0.5" title="Starttijd wissen">
|
||
<i data-lucide="x" class="w-2.5 h-2.5"></i>
|
||
<span>Wissen</span>
|
||
</button>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Duur -->
|
||
<div>
|
||
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Duur</label>
|
||
<div class="grid grid-cols-4 gap-2">
|
||
<button type="button" onclick="setJiraPlanDuration(15)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="15">15m</button>
|
||
<button type="button" onclick="setJiraPlanDuration(30)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="30">30m</button>
|
||
<button type="button" onclick="setJiraPlanDuration(60)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="60">1u</button>
|
||
<button type="button" onclick="setJiraPlanDuration(90)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="90">1,5u</button>
|
||
</div>
|
||
<input type="hidden" id="jiraPlanDuration" value="15">
|
||
</div>
|
||
|
||
<!-- Prioriteit -->
|
||
<div>
|
||
<label for="jiraPlanPriority" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Prioriteit</label>
|
||
<select id="jiraPlanPriority" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
<option value="P1">P1 — Urgent</option>
|
||
<option value="P2">P2 — Hoog</option>
|
||
<option value="P3" selected>P3 — Normaal</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex space-x-3 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||
<button type="button" onclick="closeJiraPlanModal()" class="flex-1 px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold text-sm transition cursor-pointer">
|
||
Annuleren
|
||
</button>
|
||
<button type="button" id="btnSubmitJiraPlan" onclick="submitJiraPlanModal()" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
|
||
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
|
||
<span>Inplannen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Jira Ticket Detail (popup bij klikken op ticket) -->
|
||
<div id="jiraTicketDetailModal" 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-0 w-full max-w-lg shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
|
||
<!-- Header -->
|
||
<div class="flex items-start justify-between p-5 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<div class="space-y-1 min-w-0 flex-1 mr-3">
|
||
<div class="flex items-center space-x-2">
|
||
<span class="font-mono text-[11px] 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-2 py-0.5 rounded flex-shrink-0" id="jiraDetailKey">—</span>
|
||
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold" id="jiraDetailStatusBadge">—</span>
|
||
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold" id="jiraDetailPriorityBadge">—</span>
|
||
</div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white leading-snug" id="jiraDetailSummary">—</h3>
|
||
</div>
|
||
<button onclick="closeJiraTicketDetailModal()" 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 flex-shrink-0">
|
||
<i data-lucide="x" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body: scrollable -->
|
||
<div class="overflow-y-auto flex-1 p-5 space-y-4">
|
||
<!-- Meta info -->
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Aangemeld bij</p>
|
||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailAssignee">—</p>
|
||
</div>
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Story Points</p>
|
||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailSp">—</p>
|
||
</div>
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Project</p>
|
||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailProject">—</p>
|
||
</div>
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Type</p>
|
||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailType">—</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Beschrijving -->
|
||
<div id="jiraDetailDescSection" class="hidden">
|
||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-1.5">Beschrijving</p>
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-wrap max-h-40 overflow-y-auto" id="jiraDetailDesc"></div>
|
||
</div>
|
||
|
||
<!-- Labels -->
|
||
<div id="jiraDetailLabelsSection" class="hidden">
|
||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-1.5">Labels</p>
|
||
<div class="flex flex-wrap gap-1.5" id="jiraDetailLabels"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="flex items-center justify-between p-4 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0 gap-3">
|
||
<a href="#" target="_blank" id="jiraDetailOpenLink" class="flex items-center space-x-1.5 text-xs font-semibold text-sky-600 dark:text-sky-400 hover:underline cursor-pointer">
|
||
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
|
||
<span>Openen in Jira</span>
|
||
</a>
|
||
<div class="flex items-center space-x-2">
|
||
<button type="button" onclick="closeJiraTicketDetailModal()" class="px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold text-xs transition cursor-pointer">
|
||
Sluiten
|
||
</button>
|
||
<button type="button" onclick="openJiraPlanFromDetail()" id="btnJiraDetailPlan" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition cursor-pointer">
|
||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
|
||
<span>Inplannen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
|
||
<div id="editTaskModal" onclick="if(event.target === this) closeEditTaskModal()" 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 id="editTaskModalHeaderTitle" class="text-sm font-bold text-slate-900 dark:text-white">Taak & Tijdblok Bewerken</h3>
|
||
<p id="editTaskModalHeaderSubtitle" 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">
|
||
<input type="hidden" id="editTaskIsOutlook" value="0">
|
||
|
||
<div>
|
||
<label id="editTaskTitleLabel" 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>
|
||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Beschrijving / Notities</label>
|
||
<textarea id="editTaskDescription" rows="2" placeholder="Optionele toelichting..." 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"></textarea>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<div id="editTaskPriorityContainer">
|
||
<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 id="editTaskDurationContainer">
|
||
<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>
|
||
|
||
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
|
||
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
|
||
<button type="button" onclick="planInNextOpenBlock()" id="btnPlanOpenBlock" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
|
||
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
|
||
<span>Plan in open blok</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
|
||
<div id="editTaskOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
|
||
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
|
||
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
|
||
<span id="editTaskOpenBlockNoticeText" class="truncate font-semibold"></span>
|
||
</div>
|
||
<button type="button" onclick="document.getElementById('editTaskOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
|
||
<i data-lucide="x" class="w-3 h-3"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||
<div>
|
||
<label for="editTaskDate" class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
|
||
<input type="date" id="editTaskDate" 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">
|
||
</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">Starttijd</label>
|
||
<button type="button" onclick="clearEditTaskTimes()" title="Tijd wissen" class="text-[10px] text-slate-400 hover:text-rose-500 cursor-pointer">Wissen</button>
|
||
</div>
|
||
<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">Auto/Vrij</span>
|
||
</div>
|
||
<div class="relative">
|
||
<input type="text" id="editTaskEndTime" placeholder="Bijv. 10:00" 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>
|
||
|
||
<!-- Modal: Waarschuwing Buiten Werktijden -->
|
||
<div id="outsideWorkhoursModal" onclick="if(event.target === this) closeOutsideWorkhoursModal('CANCEL')" 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-md shadow-2xl space-y-4 border border-amber-500/30">
|
||
<div class="flex items-start 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 flex-shrink-0">
|
||
<i data-lucide="alert-triangle" class="w-5 h-5"></i>
|
||
</div>
|
||
<div class="min-w-0 flex-1">
|
||
<h3 class="text-base font-bold text-slate-900 dark:text-white leading-tight">
|
||
Let op! Je plant buiten werktijden
|
||
</h3>
|
||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||
Het ingeplande tijdblok valt buiten de gezette werktijden.
|
||
</p>
|
||
</div>
|
||
<button type="button" onclick="closeOutsideWorkhoursModal('CANCEL')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg transition cursor-pointer flex-shrink-0">
|
||
<i data-lucide="x" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800/40 rounded-xl p-3.5 text-xs text-amber-800 dark:text-amber-300 space-y-1.5">
|
||
<div class="flex items-center justify-between text-[11px] font-semibold text-amber-900 dark:text-amber-200">
|
||
<span>Gekozen tijdblok:</span>
|
||
<span id="outsideWorkhoursSelectedSlot" class="font-mono font-bold"></span>
|
||
</div>
|
||
<div class="flex items-center justify-between text-[11px] text-amber-700 dark:text-amber-400">
|
||
<span>Gezette werktijden:</span>
|
||
<span id="outsideWorkhoursBounds" class="font-mono font-semibold"></span>
|
||
</div>
|
||
<p class="text-[11px] text-amber-700/80 dark:text-amber-400/80 pt-1 border-t border-amber-200/60 dark:border-amber-800/30">
|
||
Kies <strong>Nee</strong> om het item automatisch in te plannen in het eerstvolgende open blok op de volgende werkdag, of kies <strong>Ja</strong> om toch nu buiten werktijd te plannen.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-2.5 pt-2">
|
||
<button type="button" id="btnOutsideWorkhoursNo" onclick="closeOutsideWorkhoursModal('NEE')" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition shadow-xs flex items-center justify-center space-x-1.5 cursor-pointer">
|
||
<i data-lucide="calendar-arrow-up" class="w-4 h-4"></i>
|
||
<span>Nee (Volgende werkdag)</span>
|
||
</button>
|
||
<button type="button" id="btnOutsideWorkhoursYes" onclick="closeOutsideWorkhoursModal('JA')" class="px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold text-xs transition cursor-pointer">
|
||
<span>Ja, toch inplannen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Direct E-mail Inplannen als Dagtaak -->
|
||
<div id="planEmailDirectModal" onclick="if(event.target === this) closePlanEmailDirectModal()" 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 max-h-[92vh] overflow-y-auto">
|
||
<!-- Header -->
|
||
<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-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center">
|
||
<i data-lucide="mail" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">E-mail Inplannen</h3>
|
||
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="planEmailModalSenderBadge">—</p>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="closePlanEmailDirectModal()" 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="planEmailId">
|
||
<input type="hidden" id="planEmailWebLink">
|
||
<input type="hidden" id="planEmailSender">
|
||
|
||
<!-- E-mail samenvatting & Uitklapbare inhoud -->
|
||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5">
|
||
<div>
|
||
<p class="text-xs font-semibold text-slate-800 dark:text-slate-200 leading-snug" id="planEmailModalSubject">—</p>
|
||
<div class="flex items-center space-x-2 mt-1.5">
|
||
<span class="text-[10px] text-slate-500" id="planEmailModalSenderName">—</span>
|
||
<span class="text-slate-300 dark:text-slate-600">•</span>
|
||
<span class="text-[10px] text-sky-600 dark:text-sky-400 font-medium">Outlook Mail</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Uitklapknop voor inhoud e-mail -->
|
||
<div class="pt-2 border-t border-slate-200/70 dark:border-slate-800/70">
|
||
<button type="button" onclick="toggleEmailPlanPreview()" class="w-full flex items-center justify-between text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition cursor-pointer group">
|
||
<span class="flex items-center space-x-1.5">
|
||
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
|
||
<span id="emailPlanDescToggleLabel">Inhoud van het bericht bekijken</span>
|
||
</span>
|
||
<i data-lucide="chevron-down" id="emailPlanDescChevron" class="w-3.5 h-3.5 transition-transform duration-200 group-hover:translate-y-0.5"></i>
|
||
</button>
|
||
<div id="emailPlanDescWrapper" class="hidden mt-2.5">
|
||
<div id="emailPlanModalDescription" class="max-h-48 overflow-y-auto p-3 bg-white dark:bg-[#0c0f17] rounded-xl border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans leading-relaxed"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Formulier velden -->
|
||
<div class="space-y-3.5">
|
||
<!-- Titel voor de agenda / dagtaak -->
|
||
<div>
|
||
<label for="planEmailTitle" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||
Hoe het in de agenda komt (Titel)
|
||
</label>
|
||
<input type="text" id="planEmailTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="E-mail opvolgen...">
|
||
</div>
|
||
|
||
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
|
||
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-[11px] font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
|
||
<button type="button" onclick="planEmailInNextOpenBlock()" id="btnEmailPlanOpenBlock" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
|
||
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
|
||
<span>Plan in open blok</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
|
||
<div id="emailPlanOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
|
||
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
|
||
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
|
||
<span id="emailPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
|
||
</div>
|
||
<button type="button" onclick="document.getElementById('emailPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
|
||
<i data-lucide="x" class="w-3 h-3"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Datum & Starttijd -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<div>
|
||
<label for="planEmailDate" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||
Datum
|
||
</label>
|
||
<input type="date" id="planEmailDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="updatePlanEmailEndTimeDisplay()">
|
||
</div>
|
||
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1.5">
|
||
<label for="planEmailStartTime" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300">
|
||
Starttijd
|
||
</label>
|
||
<span id="planEmailEndTimeBadge" class="text-[10px] font-mono text-indigo-600 dark:text-indigo-400 font-semibold"></span>
|
||
</div>
|
||
<div class="relative flex items-center">
|
||
<input type="time" id="planEmailStartTime" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="updatePlanEmailEndTimeDisplay()">
|
||
<button type="button" onclick="clearPlanEmailStartTime()" title="Starttijd wissen" class="absolute right-2 p-1 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 rounded transition cursor-pointer">
|
||
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
<p class="text-[10px] text-slate-400 mt-1 flex items-center justify-between">
|
||
<span>Laat leeg voor handmatig inplannen</span>
|
||
<button type="button" onclick="clearPlanEmailStartTime()" class="text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer flex items-center space-x-0.5" title="Starttijd wissen">
|
||
<i data-lucide="x" class="w-2.5 h-2.5"></i>
|
||
<span>Wissen</span>
|
||
</button>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Duur -->
|
||
<div>
|
||
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Duur</label>
|
||
<div class="grid grid-cols-4 gap-2">
|
||
<button type="button" onclick="setEmailPlanDuration(15)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="15">15m</button>
|
||
<button type="button" onclick="setEmailPlanDuration(30)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="30">30m</button>
|
||
<button type="button" onclick="setEmailPlanDuration(60)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="60">1u</button>
|
||
<button type="button" onclick="setEmailPlanDuration(90)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="90">1,5u</button>
|
||
</div>
|
||
<input type="hidden" id="planEmailDuration" value="30">
|
||
</div>
|
||
|
||
<!-- Prioriteit & Outlook Synchronisatie -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-center">
|
||
<div>
|
||
<label for="planEmailPriority" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Prioriteit</label>
|
||
<select id="planEmailPriority" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
<option value="P1">P1 — Urgent</option>
|
||
<option value="P2" selected>P2 — Belangrijk</option>
|
||
<option value="P3">P3 — Normaal</option>
|
||
</select>
|
||
</div>
|
||
<div class="pt-5">
|
||
<label class="flex items-center space-x-2 text-xs text-slate-700 dark:text-slate-300 cursor-pointer select-none">
|
||
<input type="checkbox" id="planEmailSyncOutlook" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700" <?= $isOutlookConnected ? 'checked' : 'disabled' ?>>
|
||
<span>Blokkeer in Outlook Agenda</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex space-x-3 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||
<button type="button" onclick="closePlanEmailDirectModal()" class="flex-1 px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold text-sm transition cursor-pointer">
|
||
Annuleren
|
||
</button>
|
||
<button type="button" id="btnSubmitPlanEmailDirect" onclick="submitPlanEmailDirect()" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
|
||
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
|
||
<span>Inplannen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Widgets & Raster Indeling Beheren (Fociz v2.0) -->
|
||
<div id="widgetManagerModal" 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-2xl shadow-2xl space-y-4 max-h-[90vh] flex flex-col">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<div class="flex items-center space-x-2.5">
|
||
<div class="w-8 h-8 rounded-xl bg-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="layout-grid" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Fociz Widgets & Indeling Beheren</h3>
|
||
<p class="text-[10px] text-slate-500 dark:text-slate-400">Activeer, versleep en bepaal de breedte van je werkstation-widgets</p>
|
||
</div>
|
||
</div>
|
||
<button onclick="closeWidgetManagerModal()" 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>
|
||
|
||
<div class="flex-1 overflow-y-auto pr-1 space-y-2.5" id="widgetManagerList">
|
||
<!-- Dynamisch gevuld via renderWidgetManagerList() -->
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<button type="button" onclick="resetWidgetLayoutToDefault()" class="text-xs text-slate-500 hover:text-rose-600 dark:text-slate-400 dark:hover:text-rose-400 font-semibold transition cursor-pointer">
|
||
Herstel Standaard
|
||
</button>
|
||
<div class="flex items-center space-x-2">
|
||
<button type="button" onclick="closeWidgetManagerModal()" 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="saveWidgetLayoutConfig()" id="btnSaveWidgetLayout" 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 flex items-center space-x-1.5">
|
||
<i data-lucide="save" class="w-3.5 h-3.5"></i>
|
||
<span>Indeling Opslaan</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contextmenu voor Rechtermuisklik op een dag (geplaatst op body-niveau om absolute viewport containment te waarborgen) -->
|
||
<div id="dayContextMenu" class="fixed z-[99999] hidden min-w-[220px] max-w-[280px] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1 text-xs text-slate-800 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-75">
|
||
<div class="px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800/80 flex items-center justify-between">
|
||
<span id="dayContextMenuDateLabel">Dag</span>
|
||
<span id="dayContextMenuStatusBadge"></span>
|
||
</div>
|
||
<div id="dayContextMenuActions" class="p-1 space-y-0.5">
|
||
<!-- Acties worden via JS ingevuld -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contextmenu voor Rechtermuisklik op een Taak (geplaatst op body-niveau) -->
|
||
<div id="taskContextMenu" class="fixed z-[99999] hidden min-w-[240px] max-w-[300px] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1.5 text-xs text-slate-800 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-75">
|
||
<div class="px-3 py-1.5 border-b border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center justify-between gap-1.5">
|
||
<span id="taskContextMenuTitle" class="font-bold text-slate-900 dark:text-white truncate max-w-[170px] text-xs">Taak</span>
|
||
<span id="taskContextMenuPriorityBadge" class="text-[9px] font-extrabold px-1.5 py-0.5 rounded-md"></span>
|
||
</div>
|
||
<div id="taskContextMenuMeta" class="text-[10px] text-slate-400 dark:text-slate-500 font-mono mt-0.5 truncate"></div>
|
||
</div>
|
||
<div id="taskContextMenuActions" class="p-1 space-y-0.5">
|
||
<button type="button" id="taskCtxBtnOpen" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-indigo-50 dark:hover:bg-indigo-950/40 text-slate-700 dark:text-slate-200 hover:text-indigo-600 dark:hover:text-indigo-400 text-xs font-semibold transition cursor-pointer text-left">
|
||
<i data-lucide="eye" class="w-3.5 h-3.5 text-indigo-500 flex-shrink-0"></i>
|
||
<span>Volledige inhoud openen</span>
|
||
</button>
|
||
<button type="button" id="taskCtxBtnEdit" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition cursor-pointer text-left">
|
||
<i data-lucide="edit-3" class="w-3.5 h-3.5 text-slate-500 flex-shrink-0"></i>
|
||
<span id="taskCtxBtnEditLabel">Taak bewerken</span>
|
||
</button>
|
||
<button type="button" id="taskCtxBtnToggle" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition cursor-pointer text-left">
|
||
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
|
||
<span id="taskCtxBtnToggleLabel">Voltooien</span>
|
||
</button>
|
||
<button type="button" id="taskCtxBtnSubtasks" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition cursor-pointer text-left">
|
||
<i data-lucide="list-checks" class="w-3.5 h-3.5 text-sky-500 flex-shrink-0"></i>
|
||
<span>Subtaken / Checklist</span>
|
||
</button>
|
||
<button type="button" id="taskCtxBtnOutlook" class="hidden w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-sky-50 dark:hover:bg-sky-950/40 text-slate-700 dark:text-slate-200 hover:text-sky-600 dark:hover:text-sky-400 text-xs font-semibold transition cursor-pointer text-left">
|
||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5 text-sky-500 flex-shrink-0"></i>
|
||
<span>Naar Outlook synchroniseren</span>
|
||
</button>
|
||
<div id="taskCtxDivider" class="border-t border-slate-100 dark:border-slate-800/80 my-1"></div>
|
||
<button type="button" id="taskCtxBtnDelete" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-950/40 text-rose-600 dark:text-rose-400 text-xs font-semibold transition cursor-pointer text-left">
|
||
<i data-lucide="trash-2" class="w-3.5 h-3.5 text-rose-500 flex-shrink-0"></i>
|
||
<span id="taskCtxBtnDeleteLabel">Taak verwijderen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Volledige Taak Inhoud & Details Bekijken (Fociz Release v2.2) -->
|
||
<div id="taskDetailModal" onclick="if(event.target === this) closeTaskDetailModal()" 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-xl shadow-2xl space-y-4 max-h-[90vh] flex flex-col">
|
||
<!-- Header -->
|
||
<div class="flex items-start justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<div class="flex items-start space-x-3 min-w-0 flex-1 mr-3">
|
||
<div id="taskDetailPriorityIcon" class="w-10 h-10 rounded-2xl flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<i data-lucide="check-square" class="w-5 h-5"></i>
|
||
</div>
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex flex-wrap items-center gap-1.5 mb-1">
|
||
<span id="taskDetailPriorityBadge" class="px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide"></span>
|
||
<span id="taskDetailSourceBadge" class="px-2 py-0.5 rounded-full text-[10px] font-mono font-bold"></span>
|
||
<span id="taskDetailStatusBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold"></span>
|
||
</div>
|
||
<h3 id="taskDetailTitle" class="text-base font-bold text-slate-900 dark:text-white leading-snug break-words"></h3>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="closeTaskDetailModal()" 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 flex-shrink-0">
|
||
<i data-lucide="x" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Scrollable Content Body -->
|
||
<div class="flex-1 overflow-y-auto pr-1 space-y-4 text-xs">
|
||
<!-- Tijd & Planning blok -->
|
||
<div class="p-3.5 rounded-2xl bg-slate-50/80 dark:bg-[#0c0f17]/70 border border-slate-200/80 dark:border-slate-800/80 grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||
<div>
|
||
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 block mb-0.5">Datum</span>
|
||
<span id="taskDetailDate" class="font-semibold text-slate-800 dark:text-slate-200"></span>
|
||
</div>
|
||
<div>
|
||
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 block mb-0.5">Tijdblok</span>
|
||
<span id="taskDetailTimeSlot" class="font-mono font-semibold text-slate-800 dark:text-slate-200"></span>
|
||
</div>
|
||
<div>
|
||
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 block mb-0.5">Geschatte Duur</span>
|
||
<span id="taskDetailDuration" class="font-mono font-bold text-indigo-600 dark:text-indigo-400"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Volledige Beschrijving / Inhoud -->
|
||
<div class="space-y-1.5">
|
||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300">
|
||
Volledige Beschrijving & Inhoud
|
||
</label>
|
||
<div id="taskDetailDescription" class="p-3.5 rounded-2xl bg-slate-50/50 dark:bg-[#0c0f17]/50 border border-slate-200/60 dark:border-slate-800/60 text-slate-700 dark:text-slate-300 whitespace-pre-wrap leading-relaxed min-h-[60px]">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Subtaken & Checklist -->
|
||
<div id="taskDetailSubtasksSection" class="space-y-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center space-x-1.5">
|
||
<i data-lucide="list-checks" class="w-4 h-4 text-indigo-500"></i>
|
||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200">Subtaken / Checklist</span>
|
||
</div>
|
||
<span id="taskDetailSubtasksCount" class="text-[10px] font-mono text-slate-400"></span>
|
||
</div>
|
||
|
||
<div id="taskDetailSubtasksList" class="space-y-1.5">
|
||
<!-- Dynamisch geladen -->
|
||
</div>
|
||
|
||
<!-- Snel subtaak toevoegen binnen detail modal -->
|
||
<div class="flex items-center space-x-2 pt-1">
|
||
<input
|
||
type="text"
|
||
id="taskDetailNewSubtaskInput"
|
||
placeholder="Nieuwe checklist item toevoegen..."
|
||
class="flex-1 bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||
onkeydown="if(event.key==='Enter'){event.preventDefault(); addSubtaskFromDetailModal();}"
|
||
>
|
||
<button type="button" onclick="addSubtaskFromDetailModal()" class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-xl transition cursor-pointer flex items-center space-x-1">
|
||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||
<span>Toevoegen</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Externe Koppeling / Broninfo (indien Jira, Outlook of Mail) -->
|
||
<div id="taskDetailExternalBlock" class="hidden p-3 rounded-xl bg-indigo-50/50 dark:bg-indigo-950/20 border border-indigo-200/50 dark:border-indigo-800/40 flex items-center justify-between">
|
||
<div class="flex items-center space-x-2 text-xs text-indigo-900 dark:text-indigo-200">
|
||
<i data-lucide="link" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||
<span id="taskDetailExternalLabel">Gekoppeld item</span>
|
||
</div>
|
||
<a id="taskDetailExternalLink" href="#" target="_blank" class="px-2.5 py-1 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-[11px] font-bold transition flex items-center space-x-1 shadow-2xs">
|
||
<span>Openen</span>
|
||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer Actions -->
|
||
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<button type="button" id="taskDetailBtnDelete" class="px-3.5 py-2 rounded-xl text-xs font-bold text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer flex items-center space-x-1.5">
|
||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||
<span id="taskDetailBtnDeleteLabel">Verwijderen</span>
|
||
</button>
|
||
|
||
<div class="flex items-center space-x-2">
|
||
<button type="button" onclick="closeTaskDetailModal()" 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">
|
||
Sluiten
|
||
</button>
|
||
<button type="button" id="taskDetailBtnEdit" 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 flex items-center space-x-1.5">
|
||
<i data-lucide="edit-3" class="w-3.5 h-3.5"></i>
|
||
<span id="taskDetailBtnEditLabel">Taak Bewerken</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Agenda Dagen Blokkeren (Vakantie / Verlof / Focus) -->
|
||
<div id="calendarBlockModal" 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 max-h-[90vh] flex flex-col">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<div class="flex items-center space-x-2.5">
|
||
<div class="w-8 h-8 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center">
|
||
<i data-lucide="palmtree" class="w-4 h-4"></i>
|
||
</div>
|
||
<div>
|
||
<h3 id="calendarBlockModalTitle" class="text-sm font-bold text-slate-900 dark:text-white">Dagen Blokkeren / Vakantie</h3>
|
||
<p class="text-[10px] text-slate-500 dark:text-slate-400">Blokkeer een hele dag of periode in je agenda & planner</p>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="closeCalendarBlockModal()" 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>
|
||
|
||
<!-- Formulier / Content -->
|
||
<form id="calendarBlockForm" onsubmit="submitCalendarBlock(event)" class="flex-1 overflow-y-auto pr-1 space-y-3.5">
|
||
<input type="hidden" id="calBlockId" value="0">
|
||
|
||
<div>
|
||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Type Blokkade</label>
|
||
<select id="calBlockType" onchange="updateCalBlockDefaultTitle(this.value)" class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
|
||
<?php foreach ($calendarBlockReasons as $reason): ?>
|
||
<?php if (!empty($reason['is_active'])): ?>
|
||
<option value="<?= htmlspecialchars($reason['code']) ?>"><?= htmlspecialchars((!empty($reason['icon']) ? $reason['icon'] . ' ' : '') . $reason['title']) ?></option>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Titel / Omschrijving <span class="text-rose-500">*</span></label>
|
||
<input type="text" id="calBlockTitle" required placeholder="Bijv. Vakantie, Vrije dag, Verlof..." class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white placeholder-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500">
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Begindatum <span class="text-rose-500">*</span></label>
|
||
<input type="date" id="calBlockStartDate" required class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Einddatum (t/m) <span class="text-rose-500">*</span></label>
|
||
<input type="date" id="calBlockEndDate" required class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Notities (optioneel)</label>
|
||
<textarea id="calBlockNotes" rows="2" placeholder="Eventuele overdracht of bijzonderheden..." class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white placeholder-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500"></textarea>
|
||
</div>
|
||
|
||
<?php if ($isOutlookConnected): ?>
|
||
<div class="p-3 rounded-xl bg-sky-50/60 dark:bg-sky-950/20 border border-sky-200/60 dark:border-sky-800/40 flex items-center justify-between">
|
||
<div class="flex items-center space-x-2.5">
|
||
<i data-lucide="refresh-cw" class="w-4 h-4 text-sky-600 dark:text-sky-400 flex-shrink-0"></i>
|
||
<div>
|
||
<p class="text-xs font-bold text-sky-900 dark:text-sky-200">Synchroniseren naar Outlook 365</p>
|
||
<p class="text-[10px] text-sky-700/80 dark:text-sky-400/80">Maakt een 'Afwezig' (OOF) dagblokkade aan in je Microsoft agenda</p>
|
||
</div>
|
||
</div>
|
||
<label class="relative inline-flex items-center cursor-pointer flex-shrink-0 ml-2">
|
||
<input type="checkbox" id="calBlockSyncOutlook" checked class="sr-only peer">
|
||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-slate-600 peer-checked:bg-sky-600"></div>
|
||
</label>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Knoppen balk -->
|
||
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||
<button type="button" id="btnDeleteCurrentCalBlock" onclick="deleteCurrentCalendarBlock()" class="hidden px-3 py-2 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-950/50 dark:hover:bg-rose-900/60 text-rose-700 dark:text-rose-300 text-xs font-bold border border-rose-200 dark:border-rose-800/60 transition cursor-pointer items-center space-x-1">
|
||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||
<span>Opheffen</span>
|
||
</button>
|
||
<div class="flex items-center space-x-2 ml-auto">
|
||
<button type="button" onclick="closeCalendarBlockModal()" 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="submit" id="btnSaveCalBlock" class="px-4 py-2 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-1.5">
|
||
<i data-lucide="check" class="w-3.5 h-3.5"></i>
|
||
<span>Opslaan</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- Dashboard Banner Instellingen Dropdown Menu ---
|
||
function togglePlannerSettingsMenu(e) {
|
||
if (e) e.stopPropagation();
|
||
const menu = document.getElementById('plannerSettingsDropdownMenu');
|
||
if (menu) {
|
||
menu.classList.toggle('hidden');
|
||
}
|
||
}
|
||
|
||
function closePlannerSettingsMenu() {
|
||
const menu = document.getElementById('plannerSettingsDropdownMenu');
|
||
if (menu) {
|
||
menu.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
document.addEventListener('click', function(e) {
|
||
const menu = document.getElementById('plannerSettingsDropdownMenu');
|
||
const btn = document.getElementById('plannerSettingsDropdownBtn');
|
||
if (menu && !menu.classList.contains('hidden')) {
|
||
if (!menu.contains(e.target) && !btn.contains(e.target)) {
|
||
closePlannerSettingsMenu();
|
||
}
|
||
}
|
||
});
|
||
|
||
// --- Fociz Widget Beheer & Configuratielogica (v2.0) ---
|
||
let focizWidgets = <?= json_encode(array_values($userWidgetLayout), JSON_UNESCAPED_UNICODE) ?>;
|
||
const defaultWidgetLayout = <?= json_encode(array_values($availableWidgets), JSON_UNESCAPED_UNICODE) ?>;
|
||
|
||
// --- AI Dagbriefing (laden bij pagina-open, alleen voor vandaag) ---
|
||
<?php if ($isTodaySelected): ?>
|
||
const _aiBriefingDate = '<?= $selectedDate ?>';
|
||
let _aiBriefingLoaded = false;
|
||
|
||
async function loadDailyBriefing() {
|
||
if (_aiBriefingLoaded) return;
|
||
const banner = document.getElementById('aiBriefingBanner');
|
||
if (!banner) return;
|
||
|
||
// Toon banner met skeleton
|
||
banner.classList.remove('hidden');
|
||
|
||
try {
|
||
const resp = await fetch('/api/ai.php?action=daily_briefing', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ date: _aiBriefingDate })
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (!data.success || data.error) {
|
||
// Geen AI key of fout: banner verbergen, stil falen
|
||
banner.classList.add('hidden');
|
||
return;
|
||
}
|
||
|
||
// Vul content
|
||
document.getElementById('aiBriefingGreeting').textContent = data.greeting || '';
|
||
|
||
const focusEl = document.getElementById('aiBriefingFocusTask');
|
||
const focusTextEl = document.getElementById('aiBriefingFocusText');
|
||
if (data.focus_task) {
|
||
focusTextEl.textContent = data.focus_task;
|
||
focusEl.classList.remove('hidden');
|
||
focusEl.style.display = 'flex';
|
||
}
|
||
|
||
const tipEl = document.getElementById('aiBriefingTip');
|
||
const tipTextEl = document.getElementById('aiBriefingTipText');
|
||
if (data.tip) {
|
||
tipTextEl.textContent = data.tip;
|
||
tipEl.classList.remove('hidden');
|
||
tipEl.style.display = 'flex';
|
||
}
|
||
|
||
if (data.warnings && data.warnings.length > 0) {
|
||
const warnContainer = document.getElementById('aiBriefingWarnings');
|
||
warnContainer.innerHTML = data.warnings.map(w =>
|
||
`<div class="flex items-center space-x-1.5 text-[11px] text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/50 rounded-lg px-2.5 py-1">
|
||
<i data-lucide="alert-triangle" class="w-3 h-3 flex-shrink-0"></i>
|
||
<span>${w}</span>
|
||
</div>`
|
||
).join('');
|
||
warnContainer.classList.remove('hidden');
|
||
}
|
||
|
||
// Wissel skeleton ↔ content
|
||
document.getElementById('aiBriefingLoading').classList.add('hidden');
|
||
document.getElementById('aiBriefingContent').classList.remove('hidden');
|
||
|
||
_aiBriefingLoaded = true;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
|
||
} catch (err) {
|
||
// Netwerk- of serverfout: banner verbergen
|
||
banner.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Laad de briefing 1,5 seconden na paginalading (niet blokkeren)
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
setTimeout(loadDailyBriefing, 1500);
|
||
});
|
||
<?php endif; ?>
|
||
|
||
|
||
function openWidgetManagerModal() {
|
||
renderWidgetManagerList();
|
||
const modal = document.getElementById('widgetManagerModal');
|
||
if (modal) {
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
}
|
||
}
|
||
|
||
function closeWidgetManagerModal() {
|
||
const modal = document.getElementById('widgetManagerModal');
|
||
if (modal) {
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
}
|
||
|
||
function renderWidgetManagerList() {
|
||
const list = document.getElementById('widgetManagerList');
|
||
if (!list) return;
|
||
|
||
list.innerHTML = focizWidgets.map((w, index) => {
|
||
return `
|
||
<div class="flex items-center justify-between p-3 rounded-xl border ${w.enabled ? 'bg-slate-50/90 dark:bg-[#0c0f17]/70 border-slate-200/80 dark:border-slate-800/80' : 'bg-slate-100/40 dark:bg-slate-900/30 border-dashed border-slate-200 dark:border-slate-800/60 opacity-60'} transition-all gap-3">
|
||
<div class="flex items-center space-x-3 min-w-0 flex-1">
|
||
<div class="flex flex-col space-y-0.5">
|
||
<button type="button" onclick="moveWidgetOrder(${index}, -1)" ${index === 0 ? 'disabled' : ''} class="p-1 text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 disabled:opacity-20 disabled:cursor-not-allowed rounded transition">
|
||
<i data-lucide="chevron-up" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
<button type="button" onclick="moveWidgetOrder(${index}, 1)" ${index === focizWidgets.length - 1 ? 'disabled' : ''} class="p-1 text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 disabled:opacity-20 disabled:cursor-not-allowed rounded transition">
|
||
<i data-lucide="chevron-down" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<label class="flex items-center space-x-2.5 cursor-pointer min-w-0 flex-1 select-none">
|
||
<input type="checkbox" ${w.enabled ? 'checked' : ''} onchange="toggleWidgetEnabled('${w.id}', this.checked)" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex items-center space-x-2">
|
||
<span class="text-xs font-bold text-slate-900 dark:text-white truncate">${w.title}</span>
|
||
<span class="px-2 py-0.5 text-[9px] font-bold rounded-full bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">${w.category || 'Algemeen'}</span>
|
||
</div>
|
||
<p class="text-[10px] text-slate-400 truncate mt-0.5">${w.desc || ''}</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="flex items-center space-x-2 flex-shrink-0">
|
||
<select onchange="changeWidgetSpan('${w.id}', this.value)" class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||
<option value="12" ${w.col_span == 12 ? 'selected' : ''}>100% (Hele breedte)</option>
|
||
<option value="8" ${w.col_span == 8 ? 'selected' : ''}>66% (Tweederde)</option>
|
||
<option value="7" ${w.col_span == 7 ? 'selected' : ''}>58% (Grote helft)</option>
|
||
<option value="6" ${w.col_span == 6 ? 'selected' : ''}>50% (Halve breedte)</option>
|
||
<option value="5" ${w.col_span == 5 ? 'selected' : ''}>42% (Kleine helft)</option>
|
||
<option value="4" ${w.col_span == 4 ? 'selected' : ''}>33% (Eenderde)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function toggleWidgetEnabled(id, isEnabled) {
|
||
const w = focizWidgets.find(item => item.id === id);
|
||
if (w) {
|
||
w.enabled = isEnabled;
|
||
renderWidgetManagerList();
|
||
}
|
||
}
|
||
|
||
function changeWidgetSpan(id, span) {
|
||
const w = focizWidgets.find(item => item.id === id);
|
||
if (w) {
|
||
w.col_span = parseInt(span, 10);
|
||
}
|
||
}
|
||
|
||
function moveWidgetOrder(index, direction) {
|
||
const targetIndex = index + direction;
|
||
if (targetIndex < 0 || targetIndex >= focizWidgets.length) return;
|
||
const temp = focizWidgets[index];
|
||
focizWidgets[index] = focizWidgets[targetIndex];
|
||
focizWidgets[targetIndex] = temp;
|
||
focizWidgets.forEach((item, idx) => { item.order = idx + 1; });
|
||
renderWidgetManagerList();
|
||
}
|
||
|
||
function resetWidgetLayoutToDefault() {
|
||
if (!confirm('Standaard raster indeling en alle widgets herstellen?')) return;
|
||
focizWidgets = JSON.parse(JSON.stringify(defaultWidgetLayout));
|
||
renderWidgetManagerList();
|
||
}
|
||
|
||
async function saveWidgetLayoutConfig() {
|
||
const btn = document.getElementById('btnSaveWidgetLayout');
|
||
const origHtml = btn.innerHTML;
|
||
btn.innerText = 'Opslaan...';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
focizWidgets.forEach((item, idx) => {
|
||
item.order = idx + 1;
|
||
});
|
||
const payload = focizWidgets.map((w) => {
|
||
const obj = {
|
||
id: w.id,
|
||
enabled: !!w.enabled,
|
||
col_span: parseInt(w.col_span, 10) || 6,
|
||
order: w.order
|
||
};
|
||
if (w.custom_title) {
|
||
obj.custom_title = w.custom_title;
|
||
}
|
||
return obj;
|
||
});
|
||
|
||
try {
|
||
localStorage.setItem('fociz_widget_layout_cache', JSON.stringify(payload));
|
||
} catch(e) {}
|
||
|
||
const res = await fetch('/api/planner.php?action=save_widget_layout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ layout: payload }),
|
||
keepalive: true
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
btn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5"></i><span>Opgeslagen!</span>';
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
setTimeout(() => {
|
||
closeWidgetManagerModal();
|
||
window.location.reload();
|
||
}, 600);
|
||
} else {
|
||
alert(data.error || 'Kon widget layout niet opslaan');
|
||
btn.innerHTML = origHtml;
|
||
btn.disabled = false;
|
||
}
|
||
} catch (e) {
|
||
alert('Netwerkfout bij opslaan widget layout');
|
||
btn.innerHTML = origHtml;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// --- HTML Escaping Helper (XSS preventie) ---
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// --- Floating Toast Notificatie ---
|
||
function showWidgetToast(htmlMessage, type = 'success', duration = 3500) {
|
||
let toast = document.getElementById('widgetGlobalToast');
|
||
if (!toast) {
|
||
toast = document.createElement('div');
|
||
toast.id = 'widgetGlobalToast';
|
||
toast.className = 'fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-auto flex items-center space-x-2.5 px-4 py-2.5 rounded-2xl bg-slate-900/95 dark:bg-white text-white dark:text-slate-900 text-xs font-semibold shadow-2xl border border-slate-700/60 dark:border-slate-200/80';
|
||
document.body.appendChild(toast);
|
||
}
|
||
|
||
const icon = type === 'success' ? 'check-circle-2' : 'info';
|
||
toast.innerHTML = `
|
||
<div class="w-5 h-5 rounded-full ${type === 'success' ? 'bg-emerald-500/20 text-emerald-400 dark:text-emerald-600' : 'bg-indigo-500/20 text-indigo-400 dark:text-indigo-600'} flex items-center justify-center flex-shrink-0">
|
||
<i data-lucide="${icon}" class="w-3.5 h-3.5"></i>
|
||
</div>
|
||
<div class="flex-1">${htmlMessage}</div>
|
||
`;
|
||
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
|
||
toast.classList.remove('translate-y-20', 'opacity-0');
|
||
toast.classList.add('translate-y-0', 'opacity-100');
|
||
|
||
if (window._widgetToastTimeout) clearTimeout(window._widgetToastTimeout);
|
||
window._widgetToastTimeout = setTimeout(() => {
|
||
toast.classList.add('translate-y-20', 'opacity-0');
|
||
toast.classList.remove('translate-y-0', 'opacity-100');
|
||
}, duration);
|
||
}
|
||
|
||
// --- Agenda Dagen Blokkeren & Rechtermuisklik Context Menu ---
|
||
const CAL_BLOCK_REASONS = <?= json_encode(array_values(array_filter($calendarBlockReasons, fn($r) => !empty($r['is_active'])))) ?>;
|
||
const CAL_BLOCK_DEFAULT_TITLES = {};
|
||
if (Array.isArray(CAL_BLOCK_REASONS)) {
|
||
CAL_BLOCK_REASONS.forEach(r => {
|
||
if (r && r.code) CAL_BLOCK_DEFAULT_TITLES[r.code] = r.title;
|
||
});
|
||
}
|
||
|
||
function selectCalBlockType(type) {
|
||
const input = document.getElementById('calBlockType');
|
||
if (input) input.value = type;
|
||
}
|
||
|
||
function updateCalBlockDefaultTitle(type) {
|
||
const titleInput = document.getElementById('calBlockTitle');
|
||
const currentVal = titleInput ? titleInput.value.trim() : '';
|
||
selectCalBlockType(type);
|
||
|
||
if (titleInput && (!currentVal || Object.values(CAL_BLOCK_DEFAULT_TITLES).includes(currentVal))) {
|
||
titleInput.value = CAL_BLOCK_DEFAULT_TITLES[type] || '';
|
||
}
|
||
}
|
||
|
||
// --- Rechtermuisklik Context Menu voor Dagen ---
|
||
let activeContextMenuDay = null;
|
||
|
||
function openDayContextMenu(e, dateStr, blockId = 0, blockTitle = '') {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
activeContextMenuDay = {
|
||
date: dateStr,
|
||
blockId: blockId,
|
||
title: blockTitle
|
||
};
|
||
|
||
const menu = document.getElementById('dayContextMenu');
|
||
const label = document.getElementById('dayContextMenuDateLabel');
|
||
const statusBadge = document.getElementById('dayContextMenuStatusBadge');
|
||
const actions = document.getElementById('dayContextMenuActions');
|
||
|
||
if (!menu || !label || !actions) return;
|
||
|
||
// Nederlandse dag en maand formatteren
|
||
const dParts = dateStr.split('-');
|
||
const dObj = new Date(parseInt(dParts[0], 10), parseInt(dParts[1], 10) - 1, parseInt(dParts[2], 10));
|
||
const dayNames = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
|
||
const monthNames = ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
|
||
const formatted = `${dayNames[dObj.getDay()]} ${dObj.getDate()} ${monthNames[dObj.getMonth()]}`;
|
||
|
||
label.textContent = formatted;
|
||
|
||
if (blockId > 0) {
|
||
statusBadge.innerHTML = `<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border border-amber-300/80 dark:border-amber-700/80">${escapeHtml(blockTitle || 'Geblokkeerd')}</span>`;
|
||
|
||
actions.innerHTML = `
|
||
<button type="button" onclick="quickUnblockDay(${blockId})" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer font-medium">
|
||
<i data-lucide="trash-2" class="w-3.5 h-3.5 flex-shrink-0 text-rose-500"></i>
|
||
<span>Blokkade opheffen</span>
|
||
</button>
|
||
<button type="button" onclick="closeDayContextMenu(); openCalendarBlockModal({id: ${blockId}, start_date: '${dateStr}', end_date: '${dateStr}', title: '${escapeHtml(blockTitle)}'}, '${dateStr}')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-slate-100 dark:hover:bg-slate-800/80 text-slate-700 dark:text-slate-300 transition cursor-pointer font-medium">
|
||
<i data-lucide="calendar" class="w-3.5 h-3.5 flex-shrink-0 text-slate-400"></i>
|
||
<span>Periode aanpassen...</span>
|
||
</button>
|
||
`;
|
||
} else {
|
||
statusBadge.innerHTML = `<span class="text-[9px] font-normal text-slate-400">Beschikbaar</span>`;
|
||
|
||
let reasonsHtml = '';
|
||
if (Array.isArray(CAL_BLOCK_REASONS) && CAL_BLOCK_REASONS.length > 0) {
|
||
reasonsHtml = CAL_BLOCK_REASONS.map(r => `
|
||
<button type="button" onclick="quickBlockDay('${dateStr}', '${escapeHtml(r.title)}', '${escapeHtml(r.code)}')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-amber-50 dark:hover:bg-amber-950/50 text-slate-800 dark:text-slate-200 transition cursor-pointer font-medium">
|
||
<span class="text-sm leading-none">${escapeHtml(r.icon || '📅')}</span>
|
||
<span>Hele dag: ${escapeHtml(r.title)}</span>
|
||
</button>
|
||
`).join('');
|
||
} else {
|
||
reasonsHtml = `
|
||
<button type="button" onclick="quickBlockDay('${dateStr}', 'Vakantie', 'VACATION')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-amber-50 dark:hover:bg-amber-950/50 text-slate-800 dark:text-slate-200 transition cursor-pointer font-medium">
|
||
<span class="text-sm leading-none">🏖️</span>
|
||
<span>Hele dag: Vakantie</span>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
actions.innerHTML = `
|
||
${reasonsHtml}
|
||
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
|
||
<button type="button" onclick="closeDayContextMenu(); openCalendarBlockModal(null, '${dateStr}')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-slate-100 dark:hover:bg-slate-800/80 text-slate-600 dark:text-slate-400 transition cursor-pointer font-medium">
|
||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5 flex-shrink-0 text-slate-400"></i>
|
||
<span>Meerdere dagen blokkeren...</span>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
if (window.lucide && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
|
||
menu.classList.remove('hidden');
|
||
|
||
const menuWidth = 220;
|
||
const menuHeight = menu.offsetHeight || 180;
|
||
let posX = Math.max(12, Math.min(window.innerWidth - menuWidth - 16, e.clientX));
|
||
let posY = Math.max(12, Math.min(window.innerHeight - menuHeight - 16, e.clientY));
|
||
|
||
menu.style.left = `${posX}px`;
|
||
menu.style.top = `${posY}px`;
|
||
}
|
||
|
||
function closeDayContextMenu() {
|
||
const menu = document.getElementById('dayContextMenu');
|
||
if (menu) {
|
||
menu.classList.add('hidden');
|
||
}
|
||
activeContextMenuDay = null;
|
||
}
|
||
|
||
// --- Rechtermuisklik Context Menu voor Taken & Outlook Events (Release v2.2) ---
|
||
let activeContextMenuTask = null;
|
||
|
||
function handleTaskContextMenu(e, taskData) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
closeDayContextMenu();
|
||
activeContextMenuTask = taskData;
|
||
|
||
const menu = document.getElementById('taskContextMenu');
|
||
const titleEl = document.getElementById('taskContextMenuTitle');
|
||
const priorityBadge = document.getElementById('taskContextMenuPriorityBadge');
|
||
const metaEl = document.getElementById('taskContextMenuMeta');
|
||
const btnOpen = document.getElementById('taskCtxBtnOpen');
|
||
const btnEdit = document.getElementById('taskCtxBtnEdit');
|
||
const btnEditLabel = document.getElementById('taskCtxBtnEditLabel');
|
||
const btnToggle = document.getElementById('taskCtxBtnToggle');
|
||
const toggleLabel = document.getElementById('taskCtxBtnToggleLabel');
|
||
const btnSubtasks = document.getElementById('taskCtxBtnSubtasks');
|
||
const btnOutlook = document.getElementById('taskCtxBtnOutlook');
|
||
const btnDelete = document.getElementById('taskCtxBtnDelete');
|
||
const btnDeleteLabel = document.getElementById('taskCtxBtnDeleteLabel');
|
||
const divider = document.getElementById('taskCtxDivider');
|
||
|
||
if (!menu) return;
|
||
|
||
const isOutlookEvent = Boolean(taskData && (taskData.is_outlook_event || taskData.source === 'OUTLOOK_LIVE'));
|
||
|
||
if (titleEl) titleEl.textContent = (isOutlookEvent ? (taskData.subject || taskData.title) : taskData.title) || 'Item';
|
||
|
||
if (priorityBadge) {
|
||
if (isOutlookEvent) {
|
||
priorityBadge.textContent = 'AFSPRAAK';
|
||
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-sky-100 text-sky-700 dark:bg-sky-950 dark:text-sky-400 border border-sky-300 dark:border-sky-800';
|
||
} else {
|
||
const p = taskData.priority || 'P2';
|
||
priorityBadge.textContent = p;
|
||
if (p === 'P1') {
|
||
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-400 border border-rose-300 dark:border-rose-800';
|
||
} else if (p === 'P2') {
|
||
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-400 border border-amber-300 dark:border-amber-800';
|
||
} else {
|
||
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border border-slate-300 dark:border-slate-700';
|
||
}
|
||
}
|
||
}
|
||
|
||
if (metaEl) {
|
||
let meta = '';
|
||
if (taskData.start) {
|
||
meta += `${taskData.start}${taskData.end ? ' - ' + taskData.end : ''}`;
|
||
if (taskData.duration) meta += ` (${taskData.duration}m)`;
|
||
} else if (taskData.duration) {
|
||
meta += `${taskData.duration}m`;
|
||
}
|
||
if (isOutlookEvent) {
|
||
meta += (meta ? ' · ' : '') + 'Outlook';
|
||
if (taskData.location && taskData.location !== 'Geen locatie') {
|
||
meta += ` · ${taskData.location}`;
|
||
}
|
||
} else if (taskData.source && taskData.source !== 'FOCIZ') {
|
||
meta += (meta ? ' · ' : '') + taskData.source;
|
||
}
|
||
metaEl.textContent = meta || (taskData.date || 'Vandaag');
|
||
}
|
||
|
||
// Pas labels en zichtbaarheid aan op basis van type
|
||
if (isOutlookEvent) {
|
||
if (btnEditLabel) btnEditLabel.textContent = 'Afspraak bewerken';
|
||
if (btnDeleteLabel) btnDeleteLabel.textContent = 'Afspraak verwijderen';
|
||
if (btnToggle) btnToggle.classList.add('hidden');
|
||
if (btnSubtasks) btnSubtasks.classList.add('hidden');
|
||
if (btnOutlook) btnOutlook.classList.add('hidden');
|
||
} else {
|
||
if (btnEditLabel) btnEditLabel.textContent = 'Taak bewerken';
|
||
if (btnDeleteLabel) btnDeleteLabel.textContent = 'Taak verwijderen';
|
||
if (btnToggle) {
|
||
btnToggle.classList.remove('hidden');
|
||
if (toggleLabel) {
|
||
toggleLabel.textContent = taskData.is_completed ? 'Markeren als openstaand' : 'Markeren als voltooid';
|
||
}
|
||
}
|
||
if (btnSubtasks) btnSubtasks.classList.remove('hidden');
|
||
if (btnOutlook) {
|
||
if (window.isOutlookConnected && !taskData.external_id && !taskData.is_completed) {
|
||
btnOutlook.classList.remove('hidden');
|
||
} else {
|
||
btnOutlook.classList.add('hidden');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Koppel actieknoppen
|
||
if (btnOpen) {
|
||
btnOpen.onclick = () => {
|
||
closeTaskContextMenu();
|
||
openTaskDetailModal(taskData);
|
||
};
|
||
}
|
||
|
||
if (btnEdit) {
|
||
btnEdit.onclick = () => {
|
||
closeTaskContextMenu();
|
||
openEditTaskModal(taskData);
|
||
};
|
||
}
|
||
|
||
if (btnToggle) {
|
||
btnToggle.onclick = () => {
|
||
closeTaskContextMenu();
|
||
toggleTask(taskData.id, taskData.is_completed ? 0 : 1);
|
||
};
|
||
}
|
||
|
||
if (btnSubtasks) {
|
||
btnSubtasks.onclick = () => {
|
||
closeTaskContextMenu();
|
||
if (typeof toggleSubtaskPanel === 'function') {
|
||
toggleSubtaskPanel(taskData.id);
|
||
} else {
|
||
openTaskDetailModal(taskData);
|
||
}
|
||
};
|
||
}
|
||
|
||
if (btnOutlook) {
|
||
btnOutlook.onclick = () => {
|
||
closeTaskContextMenu();
|
||
if (typeof syncTaskToOutlook === 'function') {
|
||
syncTaskToOutlook(taskData.id);
|
||
}
|
||
};
|
||
}
|
||
|
||
if (btnDelete) {
|
||
btnDelete.onclick = () => {
|
||
closeTaskContextMenu();
|
||
if (isOutlookEvent) {
|
||
deleteOutlookEvent(taskData.id, taskData.subject || taskData.title);
|
||
} else {
|
||
deleteTask(taskData.id);
|
||
}
|
||
};
|
||
}
|
||
|
||
if (window.lucide && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
|
||
menu.classList.remove('hidden');
|
||
|
||
const menuWidth = 240;
|
||
const menuHeight = menu.offsetHeight || 220;
|
||
let posX = Math.max(12, Math.min(window.innerWidth - menuWidth - 16, e.clientX));
|
||
let posY = Math.max(12, Math.min(window.innerHeight - menuHeight - 16, e.clientY));
|
||
|
||
menu.style.left = `${posX}px`;
|
||
menu.style.top = `${posY}px`;
|
||
}
|
||
|
||
function closeTaskContextMenu() {
|
||
const menu = document.getElementById('taskContextMenu');
|
||
if (menu) {
|
||
menu.classList.add('hidden');
|
||
}
|
||
activeContextMenuTask = null;
|
||
}
|
||
|
||
// --- Task Detail Modal (Volledige inhoud bekijken) ---
|
||
let currentDetailTaskId = null;
|
||
let currentDetailTaskData = null;
|
||
|
||
async function openTaskDetailModal(taskDataOrId) {
|
||
let task = null;
|
||
let taskId = 0;
|
||
|
||
if (typeof taskDataOrId === 'object' && taskDataOrId !== null) {
|
||
task = taskDataOrId;
|
||
taskId = task.id;
|
||
} else {
|
||
taskId = parseInt(taskDataOrId, 10);
|
||
}
|
||
|
||
currentDetailTaskId = taskId;
|
||
currentDetailTaskData = task;
|
||
|
||
const modal = document.getElementById('taskDetailModal');
|
||
if (!modal) return;
|
||
|
||
// Toon direct modal met basisgegevens
|
||
if (task) {
|
||
populateTaskDetailModal(task, _subtaskCache[taskId] || []);
|
||
}
|
||
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
|
||
const isOutlookEvent = Boolean(task && (task.is_outlook_event || task.source === 'OUTLOOK_LIVE'));
|
||
|
||
// Als het een Outlook event is, hoeven we geen Fociz task ID API aan te roepen
|
||
if (!isOutlookEvent && taskId) {
|
||
try {
|
||
const resp = await fetch(`/api/planner.php?action=get_task_details&task_id=${taskId}`);
|
||
const data = await resp.json();
|
||
if (data.success && data.task) {
|
||
currentDetailTaskData = {
|
||
id: data.task.id,
|
||
title: data.task.title,
|
||
description: data.task.description || '',
|
||
priority: data.task.priority || 'P2',
|
||
start: data.task.time_block_start || '',
|
||
end: data.task.time_block_end || '',
|
||
duration: parseInt(data.task.duration_minutes, 10) || 30,
|
||
is_completed: parseInt(data.task.is_completed, 10) === 1,
|
||
source: data.task.source || 'FOCIZ',
|
||
external_id: data.task.external_id || '',
|
||
external_url: data.task.external_url || '',
|
||
date: data.task.plan_date || (task ? task.date : '')
|
||
};
|
||
_subtaskCache[taskId] = data.subtasks || [];
|
||
populateTaskDetailModal(currentDetailTaskData, data.subtasks || []);
|
||
}
|
||
} catch (err) {
|
||
console.error('Fout bij ophalen taakdetails:', err);
|
||
}
|
||
}
|
||
|
||
if (window.lucide && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function populateTaskDetailModal(task, subtasks) {
|
||
const titleEl = document.getElementById('taskDetailTitle');
|
||
const priorityBadge = document.getElementById('taskDetailPriorityBadge');
|
||
const priorityIcon = document.getElementById('taskDetailPriorityIcon');
|
||
const sourceBadge = document.getElementById('taskDetailSourceBadge');
|
||
const statusBadge = document.getElementById('taskDetailStatusBadge');
|
||
const dateEl = document.getElementById('taskDetailDate');
|
||
const timeSlotEl = document.getElementById('taskDetailTimeSlot');
|
||
const durationEl = document.getElementById('taskDetailDuration');
|
||
const descEl = document.getElementById('taskDetailDescription');
|
||
const subtasksSection = document.getElementById('taskDetailSubtasksSection');
|
||
const subtasksList = document.getElementById('taskDetailSubtasksList');
|
||
const extBlock = document.getElementById('taskDetailExternalBlock');
|
||
const extLabel = document.getElementById('taskDetailExternalLabel');
|
||
const extLink = document.getElementById('taskDetailExternalLink');
|
||
const btnEdit = document.getElementById('taskDetailBtnEdit');
|
||
const btnEditLabel = document.getElementById('taskDetailBtnEditLabel');
|
||
const btnDelete = document.getElementById('taskDetailBtnDelete');
|
||
const btnDeleteLabel = document.getElementById('taskDetailBtnDeleteLabel');
|
||
|
||
const isOutlookEvent = Boolean(task && (task.is_outlook_event || task.source === 'OUTLOOK_LIVE'));
|
||
|
||
if (titleEl) titleEl.textContent = (isOutlookEvent ? (task.subject || task.title) : task.title) || 'Geen titel';
|
||
|
||
// Prioriteit & icoon
|
||
if (isOutlookEvent) {
|
||
if (priorityBadge) {
|
||
priorityBadge.textContent = 'Outlook Afspraak';
|
||
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
|
||
}
|
||
if (priorityIcon) {
|
||
priorityIcon.className = 'w-10 h-10 rounded-2xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center flex-shrink-0 mt-0.5';
|
||
priorityIcon.innerHTML = '<i data-lucide="calendar" class="w-5 h-5"></i>';
|
||
}
|
||
} else {
|
||
const p = task.priority || 'P2';
|
||
if (priorityBadge) {
|
||
priorityBadge.textContent = p === 'P1' ? 'P1 · Hoge Prioriteit' : (p === 'P2' ? 'P2 · Belangrijk' : 'P3 · Regulier');
|
||
if (p === 'P1') {
|
||
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-rose-50 dark:bg-rose-500/15 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/30';
|
||
if (priorityIcon) priorityIcon.className = 'w-10 h-10 rounded-2xl bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/20 text-rose-600 dark:text-rose-400 flex items-center justify-center flex-shrink-0 mt-0.5';
|
||
} else if (p === 'P2') {
|
||
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-amber-50 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/30';
|
||
if (priorityIcon) priorityIcon.className = 'w-10 h-10 rounded-2xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center flex-shrink-0 mt-0.5';
|
||
} else {
|
||
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700';
|
||
if (priorityIcon) priorityIcon.className = 'w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 flex items-center justify-center flex-shrink-0 mt-0.5';
|
||
}
|
||
}
|
||
if (priorityIcon) {
|
||
priorityIcon.innerHTML = '<i data-lucide="check-square" class="w-5 h-5"></i>';
|
||
}
|
||
}
|
||
|
||
// Source
|
||
if (sourceBadge) {
|
||
if (isOutlookEvent) {
|
||
sourceBadge.textContent = 'OUTLOOK 365';
|
||
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
|
||
} else {
|
||
const src = (task.source || 'FOCIZ').toUpperCase();
|
||
sourceBadge.textContent = src;
|
||
if (src === 'JIRA') {
|
||
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-indigo-50 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-500/30';
|
||
} else if (src === 'OUTLOOK' || src === 'MICROSOFT') {
|
||
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
|
||
} else if (src === 'OUTLOOK_MAIL' || src === 'MAIL') {
|
||
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-violet-50 dark:bg-violet-500/15 text-violet-700 dark:text-violet-400 border border-violet-200 dark:border-violet-500/30';
|
||
} else if (src === 'BRAINSTORM') {
|
||
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-amber-50 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/30';
|
||
} else {
|
||
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Status
|
||
if (statusBadge) {
|
||
if (isOutlookEvent) {
|
||
statusBadge.textContent = 'Agenda Afspraak';
|
||
statusBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-50 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-500/30';
|
||
} else {
|
||
if (task.is_completed) {
|
||
statusBadge.textContent = 'Voltooid';
|
||
statusBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30';
|
||
} else {
|
||
statusBadge.textContent = 'Openstaand';
|
||
statusBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Datum & Tijd
|
||
if (dateEl) {
|
||
dateEl.textContent = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : 'Vandaag');
|
||
}
|
||
if (timeSlotEl) {
|
||
timeSlotEl.textContent = (task.start) ? `${task.start} - ${task.end || '?'}` : 'Geen tijdblok';
|
||
}
|
||
if (durationEl) {
|
||
let dur = task.duration;
|
||
if (!dur && task.start && task.end) {
|
||
const sm = parseTimeToMinutes(task.start);
|
||
const em = parseTimeToMinutes(task.end);
|
||
if (sm !== null && em !== null && em > sm) dur = em - sm;
|
||
}
|
||
durationEl.textContent = `${dur || 30} minuten`;
|
||
}
|
||
|
||
// Beschrijving / Locatie
|
||
if (descEl) {
|
||
if (isOutlookEvent) {
|
||
let descParts = [];
|
||
if (task.location && task.location !== 'Geen locatie') {
|
||
descParts.push(`Locatie: ${task.location}`);
|
||
}
|
||
if (task.description && task.description.trim()) {
|
||
descParts.push(task.description.trim());
|
||
}
|
||
if (descParts.length > 0) {
|
||
descEl.textContent = descParts.join('\n\n');
|
||
descEl.classList.remove('italic', 'text-slate-400');
|
||
} else {
|
||
descEl.textContent = 'Geen extra notities of locatie voor deze Outlook-afspraak.';
|
||
descEl.classList.add('italic', 'text-slate-400');
|
||
}
|
||
} else {
|
||
if (task.description && task.description.trim()) {
|
||
descEl.textContent = task.description;
|
||
descEl.classList.remove('italic', 'text-slate-400');
|
||
} else {
|
||
descEl.textContent = 'Geen beschrijving of notities opgegeven voor deze taak.';
|
||
descEl.classList.add('italic', 'text-slate-400');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Subtaken: verbergen voor Outlook afspraken
|
||
if (subtasksSection) {
|
||
if (isOutlookEvent) {
|
||
subtasksSection.classList.add('hidden');
|
||
} else {
|
||
subtasksSection.classList.remove('hidden');
|
||
if (subtasksList) {
|
||
renderSubtasksInDetailModal(task.id, subtasks || []);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Externe link
|
||
if (extBlock && extLink && extLabel) {
|
||
const extUrl = isOutlookEvent ? (task.meeting_url || task.web_link || task.external_url) : task.external_url;
|
||
if (extUrl) {
|
||
extBlock.classList.remove('hidden');
|
||
extLink.href = extUrl;
|
||
if (isOutlookEvent) {
|
||
if (task.meeting_url && task.meeting_url.includes('teams.microsoft.com')) {
|
||
extLabel.textContent = 'Deelnemen via Microsoft Teams';
|
||
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Deelnemen');
|
||
} else {
|
||
extLabel.textContent = 'Gekoppeld in Microsoft Outlook Agenda';
|
||
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'In Outlook');
|
||
}
|
||
} else if (task.source === 'JIRA') {
|
||
extLabel.textContent = `Gekoppeld Jira Ticket: ${task.external_id || 'Jira'}`;
|
||
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
|
||
} else if (task.source === 'OUTLOOK' || task.source === 'MICROSOFT') {
|
||
extLabel.textContent = 'Gekoppeld in Microsoft Outlook Agenda';
|
||
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
|
||
} else if (task.source === 'OUTLOOK_MAIL') {
|
||
extLabel.textContent = 'Afkomstig uit Outlook E-mail';
|
||
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
|
||
} else {
|
||
extLabel.textContent = 'Externe koppeling';
|
||
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
|
||
}
|
||
} else {
|
||
extBlock.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Buttons
|
||
if (btnEditLabel) {
|
||
btnEditLabel.textContent = isOutlookEvent ? 'Afspraak Bewerken' : 'Taak Bewerken';
|
||
}
|
||
if (btnDeleteLabel) {
|
||
btnDeleteLabel.textContent = isOutlookEvent ? 'Afspraak Verwijderen' : 'Verwijderen';
|
||
}
|
||
|
||
if (btnEdit) {
|
||
btnEdit.onclick = () => {
|
||
closeTaskDetailModal();
|
||
openEditTaskModal(task);
|
||
};
|
||
}
|
||
if (btnDelete) {
|
||
btnDelete.onclick = () => {
|
||
closeTaskDetailModal();
|
||
if (isOutlookEvent) {
|
||
deleteOutlookEvent(task.id, task.subject || task.title);
|
||
} else {
|
||
deleteTask(task.id);
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
function renderSubtasksInDetailModal(taskId, subtasks) {
|
||
const list = document.getElementById('taskDetailSubtasksList');
|
||
const countEl = document.getElementById('taskDetailSubtasksCount');
|
||
if (!list) return;
|
||
|
||
if (!subtasks || subtasks.length === 0) {
|
||
list.innerHTML = '<p class="text-[11px] text-slate-400 italic py-1">Nog geen subtaken of checklist items.</p>';
|
||
if (countEl) countEl.textContent = '0 items';
|
||
return;
|
||
}
|
||
|
||
const completedCount = subtasks.filter(s => parseInt(s.is_completed, 10) === 1).length;
|
||
if (countEl) countEl.textContent = `${completedCount} van ${subtasks.length} gereed`;
|
||
|
||
list.innerHTML = subtasks.map(s => {
|
||
const isDone = parseInt(s.is_completed, 10) === 1;
|
||
return `
|
||
<div class="flex items-center justify-between p-2 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200/80 dark:border-slate-800/80 text-xs">
|
||
<label class="flex items-center space-x-2.5 flex-1 min-w-0 cursor-pointer">
|
||
<input type="checkbox" ${isDone ? 'checked' : ''} onchange="toggleSubtaskFromDetailModal(${taskId}, ${s.id}, this.checked)" class="rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 w-4 h-4 cursor-pointer">
|
||
<span class="${isDone ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200'} truncate">${escapeHtml(s.title)}</span>
|
||
</label>
|
||
<button type="button" onclick="deleteSubtaskFromDetailModal(${taskId}, ${s.id})" class="p-1 text-slate-400 hover:text-rose-500 rounded transition cursor-pointer">
|
||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
if (window.lucide && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
async function toggleSubtaskFromDetailModal(taskId, subtaskId, isChecked) {
|
||
try {
|
||
await fetch('/api/planner.php?action=toggle_subtask', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ subtask_id: subtaskId, is_completed: isChecked ? 1 : 0 })
|
||
});
|
||
const resp = await fetch(`/api/planner.php?action=get_subtasks&task_id=${taskId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
_subtaskCache[taskId] = data.subtasks || [];
|
||
renderSubtasksInDetailModal(taskId, data.subtasks || []);
|
||
if (typeof renderSubtasks === 'function') renderSubtasks(taskId);
|
||
}
|
||
} catch (e) {
|
||
console.error('Fout bij afvinken subtaak:', e);
|
||
}
|
||
}
|
||
|
||
async function deleteSubtaskFromDetailModal(taskId, subtaskId) {
|
||
try {
|
||
await fetch('/api/planner.php?action=delete_subtask', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ subtask_id: subtaskId })
|
||
});
|
||
const resp = await fetch(`/api/planner.php?action=get_subtasks&task_id=${taskId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
_subtaskCache[taskId] = data.subtasks || [];
|
||
renderSubtasksInDetailModal(taskId, data.subtasks || []);
|
||
if (typeof renderSubtasks === 'function') renderSubtasks(taskId);
|
||
}
|
||
} catch (e) {
|
||
console.error('Fout bij verwijderen subtaak:', e);
|
||
}
|
||
}
|
||
|
||
async function addSubtaskFromDetailModal() {
|
||
const input = document.getElementById('taskDetailNewSubtaskInput');
|
||
if (!input || !currentDetailTaskId) return;
|
||
const title = input.value.trim();
|
||
if (!title) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/planner.php?action=add_subtask', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ task_id: currentDetailTaskId, title: title })
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
input.value = '';
|
||
const subResp = await fetch(`/api/planner.php?action=get_subtasks&task_id=${currentDetailTaskId}`);
|
||
const subData = await subResp.json();
|
||
if (subData.success) {
|
||
_subtaskCache[currentDetailTaskId] = subData.subtasks || [];
|
||
renderSubtasksInDetailModal(currentDetailTaskId, subData.subtasks || []);
|
||
if (typeof renderSubtasks === 'function') renderSubtasks(currentDetailTaskId);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Fout bij toevoegen subtaak:', e);
|
||
}
|
||
}
|
||
|
||
function closeTaskDetailModal() {
|
||
const modal = document.getElementById('taskDetailModal');
|
||
if (modal) {
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
currentDetailTaskId = null;
|
||
currentDetailTaskData = null;
|
||
}
|
||
|
||
document.addEventListener('click', function(e) {
|
||
const dayMenu = document.getElementById('dayContextMenu');
|
||
if (dayMenu && !dayMenu.contains(e.target)) {
|
||
closeDayContextMenu();
|
||
}
|
||
const taskMenu = document.getElementById('taskContextMenu');
|
||
if (taskMenu && !taskMenu.contains(e.target)) {
|
||
closeTaskContextMenu();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
closeDayContextMenu();
|
||
closeTaskContextMenu();
|
||
closeTaskDetailModal();
|
||
}
|
||
});
|
||
|
||
async function quickBlockDay(dateStr, title, blockType) {
|
||
closeDayContextMenu();
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=save_calendar_block', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
title: title,
|
||
block_type: blockType,
|
||
start_date: dateStr,
|
||
end_date: dateStr,
|
||
sync_to_outlook: true
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (data && data.success) {
|
||
showWidgetToast(`${title} ingepland voor ${dateStr}`, 'success');
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 300);
|
||
} else {
|
||
alert(data.error || 'Fout bij inplannen van blokkade');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij inplannen van blokkade');
|
||
}
|
||
}
|
||
|
||
async function quickUnblockDay(blockId) {
|
||
closeDayContextMenu();
|
||
if (!blockId) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=delete_calendar_block', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: blockId })
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (data && data.success) {
|
||
showWidgetToast('Blokkade opgeheven', 'success');
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 300);
|
||
} else {
|
||
alert(data.error || 'Fout bij opheffen van blokkade');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij opheffen van blokkade');
|
||
}
|
||
}
|
||
|
||
function openCalendarBlockModal(existingBlock = null, defaultDate = null) {
|
||
const modal = document.getElementById('calendarBlockModal');
|
||
if (!modal) return;
|
||
|
||
const idInput = document.getElementById('calBlockId');
|
||
const titleInput = document.getElementById('calBlockTitle');
|
||
const startInput = document.getElementById('calBlockStartDate');
|
||
const endInput = document.getElementById('calBlockEndDate');
|
||
const notesInput = document.getElementById('calBlockNotes');
|
||
const deleteBtn = document.getElementById('btnDeleteCurrentCalBlock');
|
||
const modalTitle = document.getElementById('calendarBlockModalTitle');
|
||
|
||
const selDate = defaultDate || '<?= $selectedDate ?>';
|
||
|
||
if (existingBlock && existingBlock.id) {
|
||
if (idInput) idInput.value = existingBlock.id;
|
||
if (titleInput) titleInput.value = existingBlock.title || '';
|
||
if (startInput) startInput.value = existingBlock.start_date || selDate;
|
||
if (endInput) endInput.value = existingBlock.end_date || existingBlock.start_date || selDate;
|
||
if (notesInput) notesInput.value = existingBlock.notes || '';
|
||
selectCalBlockType(existingBlock.block_type || 'VACATION');
|
||
if (deleteBtn) {
|
||
deleteBtn.classList.remove('hidden');
|
||
deleteBtn.classList.add('flex');
|
||
}
|
||
if (modalTitle) modalTitle.textContent = 'Blokkade Aanpassen';
|
||
} else {
|
||
if (idInput) idInput.value = 0;
|
||
selectCalBlockType('VACATION');
|
||
if (titleInput) titleInput.value = 'Vakantie';
|
||
if (startInput) startInput.value = selDate;
|
||
if (endInput) endInput.value = selDate;
|
||
if (notesInput) notesInput.value = '';
|
||
if (deleteBtn) {
|
||
deleteBtn.classList.add('hidden');
|
||
deleteBtn.classList.remove('flex');
|
||
}
|
||
if (modalTitle) modalTitle.textContent = 'Meerdere Dagen Blokkeren';
|
||
}
|
||
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function closeCalendarBlockModal() {
|
||
const modal = document.getElementById('calendarBlockModal');
|
||
if (modal) {
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
}
|
||
|
||
async function submitCalendarBlock(event) {
|
||
event.preventDefault();
|
||
const btn = document.getElementById('btnSaveCalBlock');
|
||
const origHtml = btn ? btn.innerHTML : '';
|
||
if (btn) {
|
||
btn.innerText = 'Opslaan...';
|
||
btn.disabled = true;
|
||
}
|
||
|
||
const id = parseInt(document.getElementById('calBlockId')?.value, 10) || 0;
|
||
const blockType = document.getElementById('calBlockType')?.value || 'VACATION';
|
||
const title = document.getElementById('calBlockTitle')?.value.trim();
|
||
const startDate = document.getElementById('calBlockStartDate')?.value;
|
||
const endDate = document.getElementById('calBlockEndDate')?.value;
|
||
const notes = document.getElementById('calBlockNotes')?.value.trim();
|
||
const syncOutlook = document.getElementById('calBlockSyncOutlook')?.checked || false;
|
||
|
||
if (!title) {
|
||
alert('Vul een titel in voor de blokkade.');
|
||
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=save_calendar_block', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
id: id,
|
||
title: title,
|
||
block_type: blockType,
|
||
start_date: startDate,
|
||
end_date: endDate,
|
||
notes: notes,
|
||
sync_to_outlook: syncOutlook
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
showWidgetToast(id > 0 ? 'Blokkade succesvol bijgewerkt' : 'Dagen succesvol geblokkeerd', 'success');
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 600);
|
||
} else {
|
||
alert(data.error || 'Er is een fout opgetreden bij het opslaan.');
|
||
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij opslaan van blokkade.');
|
||
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
|
||
}
|
||
}
|
||
|
||
async function deleteCalendarBlock(blockId) {
|
||
if (!blockId) return;
|
||
if (!confirm('Weet je zeker dat je deze agenda-blokkade wilt opheffen?')) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=delete_calendar_block', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: blockId })
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
showWidgetToast('Blokkade opgeheven', 'success');
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 600);
|
||
} else {
|
||
alert(data.error || 'Kon blokkade niet opheffen.');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij opheffen van blokkade.');
|
||
}
|
||
}
|
||
|
||
function deleteCurrentCalendarBlock() {
|
||
const id = parseInt(document.getElementById('calBlockId')?.value, 10) || 0;
|
||
if (id > 0) {
|
||
deleteCalendarBlock(id);
|
||
}
|
||
}
|
||
|
||
// --- Bepaal de actuele widget layout en exacte volgorde uit het scherm (DOM) ---
|
||
function getActiveWidgetLayoutPayload() {
|
||
const grid = document.getElementById('focizWidgetGrid');
|
||
const seenIds = new Set();
|
||
const payload = [];
|
||
let currentOrder = 1;
|
||
|
||
// 1. Zichtbare widgets in het raster in hun exacte visuele volgorde op het scherm
|
||
if (grid) {
|
||
const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
|
||
wrappers.forEach(wrap => {
|
||
const wId = wrap.getAttribute('data-widget-id');
|
||
if (wId && !seenIds.has(wId)) {
|
||
seenIds.add(wId);
|
||
const item = focizWidgets.find(x => x.id === wId);
|
||
const colSpan = parseInt(wrap.getAttribute('data-col-span'), 10) || (item ? parseInt(item.col_span, 10) : 6);
|
||
const widgetObj = {
|
||
id: wId,
|
||
enabled: true,
|
||
col_span: colSpan,
|
||
order: currentOrder++
|
||
};
|
||
if (item && item.custom_title) {
|
||
widgetObj.custom_title = item.custom_title;
|
||
}
|
||
payload.push(widgetObj);
|
||
if (item) {
|
||
item.order = widgetObj.order;
|
||
item.col_span = colSpan;
|
||
item.enabled = true;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 2. Uitgeschakelde of verborgen widgets achteraan toevoegen
|
||
focizWidgets.forEach(w => {
|
||
if (!seenIds.has(w.id)) {
|
||
seenIds.add(w.id);
|
||
const hiddenObj = {
|
||
id: w.id,
|
||
enabled: !!w.enabled,
|
||
col_span: parseInt(w.col_span, 10) || 6,
|
||
order: currentOrder++
|
||
};
|
||
if (w.custom_title) {
|
||
hiddenObj.custom_title = w.custom_title;
|
||
}
|
||
payload.push(hiddenObj);
|
||
}
|
||
});
|
||
|
||
// 3. Sorteer focizWidgets array in het geheugen synchroon met de nieuwe volgorde
|
||
focizWidgets.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||
|
||
return payload;
|
||
}
|
||
|
||
// --- Achtergrond Auto-Save voor Widget Layout met keepalive ---
|
||
async function persistWidgetLayoutAuto(customPayload = null) {
|
||
try {
|
||
const payload = customPayload || getActiveWidgetLayoutPayload();
|
||
|
||
try {
|
||
localStorage.setItem('fociz_widget_layout_cache', JSON.stringify(payload));
|
||
} catch(e) {}
|
||
|
||
const res = await fetch('/api/planner.php?action=save_widget_layout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ layout: payload }),
|
||
keepalive: true
|
||
});
|
||
const data = await res.json();
|
||
return data.success;
|
||
} catch (e) {
|
||
console.warn('Fout bij automatisch opslaan widget layout:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// --- In-Widget Inline Breedte Aanpassen (1/3, 1/2, 2/3, 100%) ---
|
||
function setWidgetSpan(wId, span) {
|
||
span = parseInt(span, 10);
|
||
const wrapper = document.getElementById(`widgetWrapper_${wId}`) || document.querySelector(`.fociz-widget-wrapper[data-widget-id="${wId}"]`);
|
||
if (!wrapper) return;
|
||
|
||
// Verwijder alle mogelijke col-span klassen
|
||
const spanClasses = ['lg:col-span-12', 'lg:col-span-8', 'lg:col-span-7', 'lg:col-span-6', 'lg:col-span-5', 'lg:col-span-4'];
|
||
spanClasses.forEach(c => wrapper.classList.remove(c));
|
||
|
||
// Voeg nieuwe klasse toe
|
||
wrapper.classList.add(`lg:col-span-${span}`);
|
||
wrapper.setAttribute('data-col-span', span);
|
||
|
||
// Knoppen styling bijwerken binnen het instellingen dropdown menu
|
||
const buttons = wrapper.querySelectorAll('.widget-span-btn');
|
||
buttons.forEach(btn => {
|
||
const bSpan = parseInt(btn.getAttribute('data-span'), 10);
|
||
if (bSpan === span) {
|
||
btn.className = "widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-2xs";
|
||
} else {
|
||
btn.className = "widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700";
|
||
}
|
||
});
|
||
|
||
// Update in-memory array
|
||
const item = focizWidgets.find(x => x.id === wId);
|
||
if (item) {
|
||
item.col_span = span;
|
||
}
|
||
|
||
// Sluit het instellingen menu
|
||
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
|
||
|
||
// Trigger resize voor SVG en diagrammen
|
||
window.dispatchEvent(new Event('resize'));
|
||
|
||
// Automatisch opslaan naar database
|
||
persistWidgetLayoutAuto();
|
||
|
||
const spanDescriptions = {
|
||
4: '1/3 schermbreedte (33%)',
|
||
6: '1/2 schermbreedte (50%)',
|
||
8: '2/3 schermbreedte (66%)',
|
||
12: 'volledige breedte (100%)'
|
||
};
|
||
showWidgetToast(`Formaat aangepast naar ${spanDescriptions[span] || span + ' kolommen'}`, 'success');
|
||
}
|
||
|
||
// --- Widget Naam / Titel Aanpassen ---
|
||
function saveWidgetTitle(wId) {
|
||
const input = document.getElementById(`widgetTitleInput_${wId}`);
|
||
if (!input) return;
|
||
|
||
const defaultTitle = input.getAttribute('data-default-title') || '';
|
||
let newTitle = input.value.trim();
|
||
if (!newTitle) {
|
||
newTitle = defaultTitle;
|
||
input.value = defaultTitle;
|
||
}
|
||
|
||
// Directe DOM-update op de titelbalk van de widget
|
||
const titleEl = document.getElementById(`widgetTitleText_${wId}`);
|
||
if (titleEl) {
|
||
titleEl.textContent = newTitle;
|
||
}
|
||
|
||
// Werk de in-memory widget catalogus bij
|
||
const item = focizWidgets.find(x => x.id === wId);
|
||
if (item) {
|
||
if (newTitle !== defaultTitle) {
|
||
item.custom_title = newTitle;
|
||
item.title = newTitle;
|
||
} else {
|
||
delete item.custom_title;
|
||
item.title = defaultTitle;
|
||
}
|
||
}
|
||
|
||
// Sluit het instellingen menu
|
||
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
|
||
|
||
// Automatisch opslaan naar database
|
||
persistWidgetLayoutAuto();
|
||
showWidgetToast(`Widgetnaam aangepast naar "${escapeHtml(newTitle)}"`, 'success');
|
||
}
|
||
|
||
function resetWidgetTitle(wId, defaultTitle) {
|
||
const input = document.getElementById(`widgetTitleInput_${wId}`);
|
||
if (input) {
|
||
input.value = defaultTitle;
|
||
}
|
||
|
||
const titleEl = document.getElementById(`widgetTitleText_${wId}`);
|
||
if (titleEl) {
|
||
titleEl.textContent = defaultTitle;
|
||
}
|
||
|
||
const item = focizWidgets.find(x => x.id === wId);
|
||
if (item) {
|
||
delete item.custom_title;
|
||
item.title = defaultTitle;
|
||
}
|
||
|
||
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
|
||
|
||
persistWidgetLayoutAuto();
|
||
showWidgetToast('Standaard widgetnaam hersteld', 'info');
|
||
}
|
||
|
||
// --- Widget Instellingen Popover Menu (Tandwieltje) ---
|
||
function toggleWidgetSettingsMenu(wId, event) {
|
||
if (event) {
|
||
event.stopPropagation();
|
||
event.preventDefault();
|
||
}
|
||
const menu = document.getElementById(`widgetSettingsMenu_${wId}`);
|
||
if (!menu) return;
|
||
const isCurrentlyHidden = menu.classList.contains('hidden');
|
||
// Sluit alle open menu's eerst
|
||
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
|
||
if (isCurrentlyHidden) {
|
||
menu.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// --- Widget Inklappen / Uitklappen ---
|
||
function toggleWidgetCollapse(wId) {
|
||
const body = document.getElementById(`widget_body_${wId}`);
|
||
const icon = document.getElementById(`collapse_icon_${wId}`);
|
||
if (!body) return;
|
||
|
||
const isNowHidden = body.classList.toggle('hidden');
|
||
if (icon) {
|
||
if (isNowHidden) {
|
||
icon.classList.add('rotate-180');
|
||
} else {
|
||
icon.classList.remove('rotate-180');
|
||
}
|
||
}
|
||
try {
|
||
localStorage.setItem(`fociz_widget_collapsed_${wId}`, isNowHidden ? '1' : '0');
|
||
} catch (e) {}
|
||
}
|
||
|
||
// --- Dagenselectie (Weekstrook) Inklappen / Uitklappen ---
|
||
function toggleDaySelectionCollapse() {
|
||
const strip = document.getElementById('weekDaysStripContainer');
|
||
const icon = document.getElementById('iconToggleDaySelection');
|
||
const btn = document.getElementById('btnToggleDaySelection');
|
||
if (!strip) return;
|
||
|
||
const isCurrentlyHidden = strip.classList.contains('hidden');
|
||
if (isCurrentlyHidden) {
|
||
strip.classList.remove('hidden');
|
||
if (icon) icon.classList.remove('rotate-180');
|
||
if (btn) btn.title = 'Dagen inklappen';
|
||
try { localStorage.setItem('fociz_days_selection_collapsed', '0'); } catch(e) {}
|
||
} else {
|
||
strip.classList.add('hidden');
|
||
if (icon) icon.classList.add('rotate-180');
|
||
if (btn) btn.title = 'Dagen tonen';
|
||
try { localStorage.setItem('fociz_days_selection_collapsed', '1'); } catch(e) {}
|
||
}
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function initPlannerDaySelectionCollapse() {
|
||
try {
|
||
if (localStorage.getItem('fociz_days_selection_collapsed') === '1') {
|
||
const strip = document.getElementById('weekDaysStripContainer');
|
||
const icon = document.getElementById('iconToggleDaySelection');
|
||
const btn = document.getElementById('btnToggleDaySelection');
|
||
if (strip) strip.classList.add('hidden');
|
||
if (icon) icon.classList.add('rotate-180');
|
||
if (btn) btn.title = 'Dagen tonen';
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
// --- Widget Verbergen van Scherm met Undo Toast ---
|
||
function hideWidgetOnScreen(wId) {
|
||
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
|
||
const wrapper = document.getElementById(`widgetWrapper_${wId}`) || document.querySelector(`.fociz-widget-wrapper[data-widget-id="${wId}"]`);
|
||
const item = focizWidgets.find(x => x.id === wId);
|
||
const title = item ? item.title : 'Widget';
|
||
|
||
if (wrapper) {
|
||
wrapper.style.transition = 'all 0.25s ease';
|
||
wrapper.style.opacity = '0';
|
||
wrapper.style.transform = 'scale(0.95)';
|
||
setTimeout(() => {
|
||
wrapper.remove();
|
||
}, 250);
|
||
}
|
||
|
||
if (item) {
|
||
item.enabled = false;
|
||
}
|
||
|
||
persistWidgetLayoutAuto();
|
||
showWidgetToast(`Widget "${title}" verborgen <button type="button" onclick="undoHideWidget('${wId}')" class="underline font-bold ml-2 text-indigo-400 hover:text-indigo-300 cursor-pointer">Ongedaan maken</button>`, 'info', 6000);
|
||
}
|
||
|
||
function undoHideWidget(wId) {
|
||
const item = focizWidgets.find(x => x.id === wId);
|
||
if (item) {
|
||
item.enabled = true;
|
||
}
|
||
persistWidgetLayoutAuto();
|
||
showWidgetToast('Widget herstellen...', 'success', 2000);
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 400);
|
||
}
|
||
|
||
// --- Direct On-Screen Drag & Drop Engine (Hover Ghost, Blueprint Grid & Auto-Scroll) ---
|
||
let activeDraggedWrapper = null;
|
||
let dragCandidate = null;
|
||
let isDraggingActive = false;
|
||
let floatingGhost = null;
|
||
let dropPlaceholder = null;
|
||
let grabOffsetX = 0;
|
||
let grabOffsetY = 0;
|
||
let currentPointerX = 0;
|
||
let currentPointerY = 0;
|
||
let autoScrollAnimId = null;
|
||
let dragInitialIndex = -1;
|
||
|
||
function initPlannerWidgets() {
|
||
const grid = document.getElementById('focizWidgetGrid');
|
||
if (!grid) return;
|
||
|
||
// Herstel dagenselectie inklapstatus
|
||
initPlannerDaySelectionCollapse();
|
||
|
||
// Herstel eventuele ingeklapte widgets vanuit localStorage
|
||
document.querySelectorAll('.fociz-widget-wrapper').forEach(wrapper => {
|
||
const wId = wrapper.getAttribute('data-widget-id');
|
||
try {
|
||
if (localStorage.getItem(`fociz_widget_collapsed_${wId}`) === '1') {
|
||
const body = document.getElementById(`widget_body_${wId}`);
|
||
const icon = document.getElementById(`collapse_icon_${wId}`);
|
||
if (body) body.classList.add('hidden');
|
||
if (icon) icon.classList.add('rotate-180');
|
||
}
|
||
} catch(e) {}
|
||
});
|
||
|
||
// Luister naar drag handles met Pointer Events voor soepele hovering en scroll
|
||
document.querySelectorAll('.widget-drag-handle').forEach(handle => {
|
||
handle.setAttribute('draggable', 'false'); // Uitschakelen van krakkemikkige native browser drag
|
||
handle.style.touchAction = 'none';
|
||
|
||
handle.onpointerdown = (e) => {
|
||
// Alleen primaire muisklik (links)
|
||
if (e.button !== 0) return;
|
||
// Negeer als er op knoppen in de buurt geklikt wordt
|
||
if (e.target.closest('.widget-settings-btn') || e.target.closest('.widget-settings-dropdown')) return;
|
||
|
||
const wrapper = handle.closest('.fociz-widget-wrapper');
|
||
if (!wrapper) return;
|
||
|
||
dragCandidate = {
|
||
wrapper: wrapper,
|
||
handle: handle,
|
||
startX: e.clientX,
|
||
startY: e.clientY
|
||
};
|
||
};
|
||
});
|
||
}
|
||
|
||
// Globale pointer listeners (blijven soepel actief zelfs als cursor snel beweegt)
|
||
window.addEventListener('pointermove', (e) => {
|
||
currentPointerX = e.clientX;
|
||
currentPointerY = e.clientY;
|
||
|
||
if (!isDraggingActive && dragCandidate) {
|
||
// Controleer of de gebruiker minimaal 5px heeft bewogen (voorkomt valse triggers bij simpele kliks)
|
||
const dist = Math.hypot(e.clientX - dragCandidate.startX, e.clientY - dragCandidate.startY);
|
||
if (dist > 5) {
|
||
startWidgetDragging(dragCandidate.wrapper, dragCandidate.startX, dragCandidate.startY);
|
||
}
|
||
}
|
||
|
||
if (isDraggingActive) {
|
||
updateFloatingGhostPosition();
|
||
updateDropTargetFromPointer();
|
||
}
|
||
});
|
||
|
||
window.addEventListener('pointerup', (e) => {
|
||
if (isDraggingActive) {
|
||
finishWidgetDragging();
|
||
}
|
||
dragCandidate = null;
|
||
});
|
||
|
||
window.addEventListener('pointercancel', () => {
|
||
if (isDraggingActive) {
|
||
cancelWidgetDragging();
|
||
}
|
||
dragCandidate = null;
|
||
});
|
||
|
||
window.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && isDraggingActive) {
|
||
cancelWidgetDragging();
|
||
}
|
||
});
|
||
|
||
// Start het oppakken & slepen van de widget
|
||
function startWidgetDragging(wrapper, startX, startY) {
|
||
const grid = document.getElementById('focizWidgetGrid');
|
||
if (!grid || !wrapper) return;
|
||
|
||
isDraggingActive = true;
|
||
activeDraggedWrapper = wrapper;
|
||
|
||
// Sluit openstaande popovers
|
||
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
|
||
|
||
// Bepaal beginpositie
|
||
const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
|
||
dragInitialIndex = allWrappers.indexOf(wrapper);
|
||
|
||
// 1. Toon het 12-koloms Blueprint Achtergrond Raster
|
||
const blueprint = document.getElementById('widgetGridBlueprint');
|
||
if (blueprint) {
|
||
blueprint.classList.remove('hidden');
|
||
blueprint.classList.add('block');
|
||
}
|
||
|
||
// 2. Creëer de zwevende Hover Ghost kaart
|
||
const rect = wrapper.getBoundingClientRect();
|
||
grabOffsetX = startX - rect.left;
|
||
grabOffsetY = startY - rect.top;
|
||
|
||
floatingGhost = wrapper.cloneNode(true);
|
||
floatingGhost.id = 'widgetFloatingGhost';
|
||
floatingGhost.removeAttribute('data-widget-id');
|
||
floatingGhost.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
|
||
floatingGhost.querySelectorAll('.widget-settings-dropdown').forEach(d => d.remove());
|
||
|
||
floatingGhost.style.position = 'fixed';
|
||
floatingGhost.style.left = `${rect.left}px`;
|
||
floatingGhost.style.top = `${rect.top}px`;
|
||
floatingGhost.style.width = `${rect.width}px`;
|
||
floatingGhost.style.height = `${rect.height}px`;
|
||
floatingGhost.style.pointerEvents = 'none';
|
||
floatingGhost.style.zIndex = '999999';
|
||
floatingGhost.style.transform = 'scale(1.02) rotate(1.2deg)';
|
||
floatingGhost.style.opacity = '0.94';
|
||
floatingGhost.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease';
|
||
floatingGhost.style.boxShadow = '0 25px 60px -10px rgba(79, 70, 229, 0.45), 0 0 0 2px rgba(99, 102, 241, 0.7)';
|
||
floatingGhost.style.cursor = 'grabbing';
|
||
document.body.appendChild(floatingGhost);
|
||
|
||
// 3. Plaats een duidelijke Drop Slot Placeholder in het raster
|
||
const span = wrapper.getAttribute('data-col-span') || '6';
|
||
dropPlaceholder = document.createElement('div');
|
||
dropPlaceholder.id = 'widgetDropPlaceholder';
|
||
dropPlaceholder.className = `lg:col-span-${span} col-span-1 min-w-0 rounded-2xl border-2 border-dashed border-indigo-500 dark:border-indigo-400 bg-indigo-50/70 dark:bg-indigo-950/50 flex flex-col items-center justify-center p-6 transition-all duration-200 shadow-inner`;
|
||
dropPlaceholder.style.minHeight = `${Math.max(130, Math.min(rect.height, 280))}px`;
|
||
dropPlaceholder.innerHTML = `
|
||
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 font-bold text-xs animate-pulse">
|
||
<svg class="w-5 h-5 animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
|
||
<span>Laat los om hier te plaatsen</span>
|
||
</div>
|
||
`;
|
||
wrapper.after(dropPlaceholder);
|
||
wrapper.style.display = 'none';
|
||
|
||
document.body.classList.add('select-none', 'cursor-grabbing');
|
||
|
||
// 4. Start automatische soepele scroll bij nadering van schermranden
|
||
startAutoScroller();
|
||
}
|
||
|
||
function updateFloatingGhostPosition() {
|
||
if (!floatingGhost) return;
|
||
floatingGhost.style.left = `${currentPointerX - grabOffsetX}px`;
|
||
floatingGhost.style.top = `${currentPointerY - grabOffsetY}px`;
|
||
}
|
||
|
||
function updateDropTargetFromPointer() {
|
||
const grid = document.getElementById('focizWidgetGrid');
|
||
if (!grid || !dropPlaceholder || !activeDraggedWrapper) return;
|
||
|
||
const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')).filter(w => w !== activeDraggedWrapper);
|
||
let targetFound = false;
|
||
|
||
for (const wrap of wrappers) {
|
||
const rect = wrap.getBoundingClientRect();
|
||
if (currentPointerY >= rect.top - 15 && currentPointerY <= rect.bottom + 15 &&
|
||
currentPointerX >= rect.left - 15 && currentPointerX <= rect.right + 15) {
|
||
|
||
const midY = rect.top + rect.height / 2;
|
||
const midX = rect.left + rect.width / 2;
|
||
const isAfter = (currentPointerY > midY) || (Math.abs(currentPointerY - midY) < rect.height * 0.35 && currentPointerX > midX);
|
||
|
||
if (isAfter) {
|
||
if (wrap.nextElementSibling !== dropPlaceholder) {
|
||
wrap.after(dropPlaceholder);
|
||
}
|
||
} else {
|
||
if (wrap.previousElementSibling !== dropPlaceholder) {
|
||
wrap.before(dropPlaceholder);
|
||
}
|
||
}
|
||
targetFound = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!targetFound && wrappers.length > 0) {
|
||
const firstRect = wrappers[0].getBoundingClientRect();
|
||
const lastRect = wrappers[wrappers.length - 1].getBoundingClientRect();
|
||
if (currentPointerY < firstRect.top) {
|
||
if (grid.firstElementChild !== dropPlaceholder) {
|
||
grid.prepend(dropPlaceholder);
|
||
}
|
||
} else if (currentPointerY > lastRect.bottom) {
|
||
if (grid.lastElementChild !== dropPlaceholder) {
|
||
grid.appendChild(dropPlaceholder);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Auto-scroller wanneer je met de widget naar de boven- of onderkant van het scherm beweegt
|
||
function startAutoScroller() {
|
||
stopAutoScroller();
|
||
function scrollStep() {
|
||
if (!isDraggingActive) return;
|
||
const viewportHeight = window.innerHeight;
|
||
const threshold = 110;
|
||
|
||
if (currentPointerY < threshold && window.scrollY > 0) {
|
||
const factor = (threshold - currentPointerY) / threshold;
|
||
const speed = Math.max(3, Math.round(factor * 26));
|
||
window.scrollBy(0, -speed);
|
||
updateDropTargetFromPointer();
|
||
} else if (currentPointerY > viewportHeight - threshold) {
|
||
const factor = (currentPointerY - (viewportHeight - threshold)) / threshold;
|
||
const speed = Math.max(3, Math.round(factor * 26));
|
||
window.scrollBy(0, speed);
|
||
updateDropTargetFromPointer();
|
||
}
|
||
autoScrollAnimId = requestAnimationFrame(scrollStep);
|
||
}
|
||
autoScrollAnimId = requestAnimationFrame(scrollStep);
|
||
}
|
||
|
||
function stopAutoScroller() {
|
||
if (autoScrollAnimId) {
|
||
cancelAnimationFrame(autoScrollAnimId);
|
||
autoScrollAnimId = null;
|
||
}
|
||
}
|
||
|
||
// Voltooi het slepen (loslaten & AJAX opslaan)
|
||
function finishWidgetDragging() {
|
||
isDraggingActive = false;
|
||
stopAutoScroller();
|
||
|
||
document.body.classList.remove('select-none', 'cursor-grabbing');
|
||
|
||
// Verberg blueprint raster
|
||
const blueprint = document.getElementById('widgetGridBlueprint');
|
||
if (blueprint) {
|
||
blueprint.classList.add('hidden');
|
||
blueprint.classList.remove('block');
|
||
}
|
||
|
||
// Verwijder zwevende ghost
|
||
if (floatingGhost) {
|
||
floatingGhost.remove();
|
||
floatingGhost = null;
|
||
}
|
||
|
||
// Plaats originele widget terug op de plek van de placeholder
|
||
if (dropPlaceholder && activeDraggedWrapper) {
|
||
dropPlaceholder.replaceWith(activeDraggedWrapper);
|
||
activeDraggedWrapper.style.display = '';
|
||
dropPlaceholder = null;
|
||
|
||
// Subtiel succes flitsje op de neergezette widget
|
||
activeDraggedWrapper.classList.add('ring-2', 'ring-indigo-500', 'shadow-xl');
|
||
setTimeout(() => {
|
||
activeDraggedWrapper?.classList.remove('ring-2', 'ring-indigo-500', 'shadow-xl');
|
||
}, 400);
|
||
}
|
||
|
||
// Controleer of de volgorde gewijzigd is en sla op via AJAX
|
||
saveCurrentGridOrderIfChanged();
|
||
activeDraggedWrapper = null;
|
||
}
|
||
|
||
// Annuleer slepen (bijv. via Escape)
|
||
function cancelWidgetDragging() {
|
||
isDraggingActive = false;
|
||
stopAutoScroller();
|
||
|
||
document.body.classList.remove('select-none', 'cursor-grabbing');
|
||
|
||
const blueprint = document.getElementById('widgetGridBlueprint');
|
||
if (blueprint) {
|
||
blueprint.classList.add('hidden');
|
||
blueprint.classList.remove('block');
|
||
}
|
||
|
||
if (floatingGhost) {
|
||
floatingGhost.remove();
|
||
floatingGhost = null;
|
||
}
|
||
|
||
if (dropPlaceholder && activeDraggedWrapper) {
|
||
dropPlaceholder.remove();
|
||
dropPlaceholder = null;
|
||
activeDraggedWrapper.style.display = '';
|
||
}
|
||
|
||
activeDraggedWrapper = null;
|
||
}
|
||
|
||
// Controleer gewijzigde volgorde en sla asynchroon op via AJAX
|
||
function saveCurrentGridOrderIfChanged() {
|
||
const payload = getActiveWidgetLayoutPayload();
|
||
persistWidgetLayoutAuto(payload);
|
||
showWidgetToast('Widget indeling opgeslagen', 'success');
|
||
}
|
||
|
||
// Sluit popup menu's als er buiten wordt geklikt
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.widget-settings-dropdown') && !e.target.closest('.widget-settings-btn')) {
|
||
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
|
||
}
|
||
});
|
||
|
||
// --- Jira Queue & Tickets Widget Engine (v2.0) ---
|
||
const plannerJiraAssigned = <?= json_encode($jiraAssignedIssues, JSON_UNESCAPED_UNICODE) ?>;
|
||
const plannerJiraAll = <?= json_encode($jiraAllIssues, JSON_UNESCAPED_UNICODE) ?>;
|
||
const plannerTicketStore = new Map();
|
||
plannerJiraAll.forEach(iss => {
|
||
if (iss && iss.key) plannerTicketStore.set(iss.key, iss);
|
||
});
|
||
plannerJiraAssigned.forEach(iss => {
|
||
if (iss && iss.key && !plannerTicketStore.has(iss.key)) plannerTicketStore.set(iss.key, iss);
|
||
});
|
||
|
||
const plannedExternalTasksMap = <?= json_encode($plannedExternalIds, JSON_UNESCAPED_UNICODE) ?> || {};
|
||
const plannedExternalIdsSet = new Set(Object.keys(plannedExternalTasksMap));
|
||
let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue
|
||
|
||
function isIssueInQueue(iss, qCode) {
|
||
if (!qCode) return true;
|
||
const code = qCode.toUpperCase();
|
||
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
|
||
const sum = (iss.summary || '').toLowerCase();
|
||
|
||
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat'];
|
||
const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t));
|
||
|
||
if (code === 'DX') return matchesDx;
|
||
if (code === 'SD') return !matchesDx;
|
||
return true;
|
||
}
|
||
|
||
function setPlannerJiraQueue(queueCode) {
|
||
activePlannerJiraQueue = queueCode;
|
||
|
||
// Tab styling bijwerken
|
||
['all', 'my', 'DX', 'SD'].forEach(tab => {
|
||
const btn = document.getElementById(`pjPill_${tab}`);
|
||
if (!btn) return;
|
||
const isActive = (tab === 'all' && !queueCode) || (tab === queueCode);
|
||
if (isActive) {
|
||
btn.className = "planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-indigo-600 text-white shadow-2xs flex-shrink-0 cursor-pointer";
|
||
} else {
|
||
btn.className = "planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer";
|
||
}
|
||
});
|
||
|
||
filterPlannerJira();
|
||
}
|
||
|
||
function filterPlannerJira() {
|
||
const searchVal = (document.getElementById('plannerJiraSearch')?.value || '').toLowerCase().trim();
|
||
const listContainer = document.getElementById('plannerJiraListContainer');
|
||
const emptyState = document.getElementById('plannerJiraEmptyState');
|
||
if (!listContainer) return;
|
||
|
||
// Tellers bijwerken
|
||
let dxCount = 0;
|
||
let sdCount = 0;
|
||
plannerJiraAll.forEach(iss => {
|
||
if (isIssueInQueue(iss, 'DX')) dxCount++;
|
||
else sdCount++;
|
||
});
|
||
const elAll = document.getElementById('pjCount_all');
|
||
const elMy = document.getElementById('pjCount_my');
|
||
const elDx = document.getElementById('pjCount_DX');
|
||
const elSd = document.getElementById('pjCount_SD');
|
||
if (elAll) elAll.innerText = plannerJiraAll.length;
|
||
if (elMy) elMy.innerText = plannerJiraAssigned.length;
|
||
if (elDx) elDx.innerText = dxCount;
|
||
if (elSd) elSd.innerText = sdCount;
|
||
|
||
// Filteren
|
||
let filtered = plannerJiraAll.filter(iss => {
|
||
if (activePlannerJiraQueue === 'my') {
|
||
const isAssigned = plannerJiraAssigned.some(a => a.key === iss.key);
|
||
if (!isAssigned) return false;
|
||
} else if (activePlannerJiraQueue === 'DX') {
|
||
if (!isIssueInQueue(iss, 'DX')) return false;
|
||
} else if (activePlannerJiraQueue === 'SD') {
|
||
if (!isIssueInQueue(iss, 'SD')) return false;
|
||
}
|
||
|
||
if (searchVal) {
|
||
const k = (iss.key || '').toLowerCase();
|
||
const s = (iss.summary || '').toLowerCase();
|
||
const p = (iss.project || '').toLowerCase();
|
||
const ol = (iss.onderwerp_logging || '').toLowerCase();
|
||
if (!k.includes(searchVal) && !s.includes(searchVal) && !p.includes(searchVal) && !ol.includes(searchVal)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
|
||
// Badge totale resultaten bijwerken
|
||
const badge = document.getElementById('plannerJiraCountBadge');
|
||
if (badge) badge.innerText = filtered.length;
|
||
|
||
if (filtered.length === 0) {
|
||
listContainer.innerHTML = '';
|
||
if (emptyState) emptyState.classList.remove('hidden');
|
||
return;
|
||
}
|
||
|
||
if (emptyState) emptyState.classList.add('hidden');
|
||
|
||
listContainer.innerHTML = filtered.slice(0, 15).map(issue => {
|
||
const keyEsc = escapeHtml(issue.key || '');
|
||
const summaryEsc = escapeHtml(issue.summary || '');
|
||
const statusEsc = escapeHtml(issue.status || 'To Do');
|
||
const priorityEsc = escapeHtml(issue.priority || 'Medium');
|
||
const isDx = isIssueInQueue(issue, 'DX');
|
||
const queuePill = isDx
|
||
? '<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20">DX</span>'
|
||
: '<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-500/20">SD</span>';
|
||
|
||
const isPlanned = plannedExternalIdsSet.has(issue.key);
|
||
const planBtn = isPlanned
|
||
? `<button
|
||
onclick="quickImportJiraByKey(this, '${keyEsc}')"
|
||
data-jira-key="${keyEsc}"
|
||
class="plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
|
||
title="Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)"
|
||
>
|
||
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
|
||
<span>Ingepland</span>
|
||
</button>`
|
||
: `<button
|
||
onclick="quickImportJiraByKey(this, '${keyEsc}')"
|
||
data-jira-key="${keyEsc}"
|
||
class="plan-btn-jira 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="calendar-plus" class="w-3 h-3"></i>
|
||
<span>Plan</span>
|
||
</button>`;
|
||
|
||
return `
|
||
<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 min-w-0 flex-1 cursor-pointer" onclick="openJiraTicketDetail('${keyEsc}')" title="Klik voor ticketdetails">
|
||
<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">
|
||
${keyEsc}
|
||
</span>
|
||
${queuePill}
|
||
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
|
||
${summaryEsc}
|
||
</span>
|
||
</div>
|
||
<span class="text-[10px] text-slate-500 mt-0.5 block">
|
||
${statusEsc} • ${priorityEsc}
|
||
</span>
|
||
</div>
|
||
|
||
${planBtn}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
// --- Jira Ticket Detail Modal ---
|
||
let _jiraDetailCurrentIssue = null;
|
||
|
||
function openJiraTicketDetail(issueKey) {
|
||
const issue = plannerTicketStore.get(issueKey);
|
||
if (!issue) return;
|
||
_jiraDetailCurrentIssue = issue;
|
||
|
||
// Vul header
|
||
document.getElementById('jiraDetailKey').textContent = issue.key || '—';
|
||
document.getElementById('jiraDetailSummary').textContent = issue.summary || '—';
|
||
|
||
// Status badge
|
||
const statusEl = document.getElementById('jiraDetailStatusBadge');
|
||
const status = issue.status || '—';
|
||
statusEl.textContent = status;
|
||
statusEl.className = 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400';
|
||
|
||
// Priority badge
|
||
const priorityEl = document.getElementById('jiraDetailPriorityBadge');
|
||
const prio = issue.priority || '—';
|
||
priorityEl.textContent = prio;
|
||
const prioClass = prio === 'Highest' || prio === 'High'
|
||
? 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400'
|
||
: prio === 'Medium'
|
||
? 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400'
|
||
: 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400';
|
||
priorityEl.className = prioClass;
|
||
|
||
// Meta velden
|
||
document.getElementById('jiraDetailAssignee').textContent = issue.assignee_name || issue.assignee || 'Niet toegewezen';
|
||
document.getElementById('jiraDetailSp').textContent = issue.story_points ? `${issue.story_points} SP` : '—';
|
||
document.getElementById('jiraDetailProject').textContent = issue.project_key || issue.project || (issue.key ? issue.key.split('-')[0] : '—');
|
||
document.getElementById('jiraDetailType').textContent = issue.issue_type || issue.issuetype || 'Story';
|
||
|
||
// Beschrijving
|
||
const descSection = document.getElementById('jiraDetailDescSection');
|
||
const descEl = document.getElementById('jiraDetailDesc');
|
||
if (issue.description) {
|
||
descEl.textContent = issue.description;
|
||
descSection.classList.remove('hidden');
|
||
} else {
|
||
descSection.classList.add('hidden');
|
||
}
|
||
|
||
// Labels
|
||
const labelsSection = document.getElementById('jiraDetailLabelsSection');
|
||
const labelsEl = document.getElementById('jiraDetailLabels');
|
||
if (issue.labels && issue.labels.length > 0) {
|
||
labelsEl.innerHTML = issue.labels.map(l =>
|
||
`<span class="px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-[10px] font-semibold border border-slate-200 dark:border-slate-700">${escapeHtml(l)}</span>`
|
||
).join('');
|
||
labelsSection.classList.remove('hidden');
|
||
} else {
|
||
labelsSection.classList.add('hidden');
|
||
}
|
||
|
||
// Link naar Jira
|
||
const linkEl = document.getElementById('jiraDetailOpenLink');
|
||
if (issue.url) {
|
||
linkEl.href = issue.url;
|
||
linkEl.classList.remove('hidden');
|
||
} else {
|
||
linkEl.classList.add('hidden');
|
||
}
|
||
|
||
// Knop inplannen / ingepland status
|
||
const detailPlanBtn = document.getElementById('btnJiraDetailPlan');
|
||
if (detailPlanBtn) {
|
||
if (plannedExternalIdsSet.has(issue.key)) {
|
||
detailPlanBtn.className = "flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs transition cursor-pointer shadow-xs";
|
||
detailPlanBtn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 stroke-[2.5]"></i><span>Ingepland</span>';
|
||
} else {
|
||
detailPlanBtn.className = "flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition cursor-pointer";
|
||
detailPlanBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i><span>Inplannen</span>';
|
||
}
|
||
}
|
||
|
||
const modal = document.getElementById('jiraTicketDetailModal');
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function closeJiraTicketDetailModal() {
|
||
const modal = document.getElementById('jiraTicketDetailModal');
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
|
||
function openJiraPlanFromDetail() {
|
||
closeJiraTicketDetailModal();
|
||
if (_jiraDetailCurrentIssue) {
|
||
openJiraPlanModal(_jiraDetailCurrentIssue.key);
|
||
}
|
||
}
|
||
|
||
// --- Jira Plan Modal (met datum, tijdsuggestie & aanpasbare titel) ---
|
||
let _jiraPlanCurrentIssue = null;
|
||
|
||
function clearJiraPlanStartTime() {
|
||
const input = document.getElementById('jiraPlanStartTime');
|
||
if (input) {
|
||
input.value = '';
|
||
input.focus();
|
||
}
|
||
const notice = document.getElementById('jiraPlanOpenBlockNotice');
|
||
if (notice) notice.classList.add('hidden');
|
||
if (typeof updateJiraPlanEndTimeDisplay === 'function') {
|
||
updateJiraPlanEndTimeDisplay();
|
||
}
|
||
}
|
||
|
||
function toggleJiraPlanDescription() {
|
||
const wrapper = document.getElementById('jiraPlanDescWrapper');
|
||
const chevron = document.getElementById('jiraPlanDescChevron');
|
||
const label = document.getElementById('jiraPlanDescToggleLabel');
|
||
if (!wrapper) return;
|
||
const isHidden = wrapper.classList.contains('hidden');
|
||
if (isHidden) {
|
||
wrapper.classList.remove('hidden');
|
||
if (chevron) chevron.classList.add('rotate-180');
|
||
if (label) label.textContent = 'Inhoud inklappen';
|
||
} else {
|
||
wrapper.classList.add('hidden');
|
||
if (chevron) chevron.classList.remove('rotate-180');
|
||
if (label) label.textContent = 'Inhoud van het item bekijken';
|
||
}
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function openJiraPlanModal(issueKey) {
|
||
const issue = plannerTicketStore.get(issueKey);
|
||
if (!issue) return;
|
||
_jiraPlanCurrentIssue = issue;
|
||
|
||
// Vul header
|
||
document.getElementById('jiraPlanModalKey').textContent = issue.key || '—';
|
||
document.getElementById('jiraPlanModalSummary').textContent = issue.summary || '—';
|
||
document.getElementById('jiraPlanModalStatus').textContent = issue.status || '—';
|
||
document.getElementById('jiraPlanModalPriority').textContent = issue.priority || '—';
|
||
|
||
// SP badge
|
||
const spBadge = document.getElementById('jiraPlanModalSpBadge');
|
||
if (issue.story_points) {
|
||
spBadge.textContent = `${issue.story_points} SP`;
|
||
spBadge.classList.remove('hidden');
|
||
} else {
|
||
spBadge.classList.add('hidden');
|
||
}
|
||
|
||
// Vul beschrijving (10 regels scrollbaar venstertje) en reset naar ingeklapt
|
||
const descEl = document.getElementById('jiraPlanModalDescription');
|
||
if (descEl) {
|
||
const descText = (issue.description && issue.description.trim()) ? issue.description.trim() : 'Geen beschrijving beschikbaar voor dit ticket.';
|
||
descEl.textContent = descText;
|
||
}
|
||
const descWrapper = document.getElementById('jiraPlanDescWrapper');
|
||
const descChevron = document.getElementById('jiraPlanDescChevron');
|
||
const descLabel = document.getElementById('jiraPlanDescToggleLabel');
|
||
if (descWrapper) descWrapper.classList.add('hidden');
|
||
if (descChevron) descChevron.classList.remove('rotate-180');
|
||
if (descLabel) descLabel.textContent = 'Inhoud van het item bekijken';
|
||
|
||
// Controleer of dit ticket al is ingepland
|
||
const existingTask = (typeof plannedExternalTasksMap !== 'undefined' && plannedExternalTasksMap[issue.key]) ? plannedExternalTasksMap[issue.key] : null;
|
||
const isAlreadyPlanned = Boolean(existingTask);
|
||
|
||
const taskIdInput = document.getElementById('jiraPlanTaskId');
|
||
if (taskIdInput) {
|
||
taskIdInput.value = (existingTask && existingTask.task_id) ? existingTask.task_id : '';
|
||
}
|
||
|
||
const modalTitle = document.getElementById('jiraPlanModalTitle');
|
||
const scheduledBanner = document.getElementById('jiraPlanAlreadyScheduledBanner');
|
||
const scheduledDetails = document.getElementById('jiraPlanAlreadyScheduledDetails');
|
||
const titleInput = document.getElementById('jiraPlanCustomTitle');
|
||
const dateInput = document.getElementById('jiraPlanDate');
|
||
const startTimeInput = document.getElementById('jiraPlanStartTime');
|
||
|
||
if (isAlreadyPlanned && existingTask) {
|
||
if (modalTitle) modalTitle.textContent = 'Jira Ticket Herplannen';
|
||
if (scheduledBanner) scheduledBanner.classList.remove('hidden');
|
||
if (scheduledDetails) {
|
||
const timeInfo = existingTask.time_block_start
|
||
? `om ${escapeHtml(existingTask.time_block_start)}${existingTask.time_block_end ? ' - ' + escapeHtml(existingTask.time_block_end) : ''} (${existingTask.duration_minutes}m)`
|
||
: `(zonder specifiek tijdblok, ${existingTask.duration_minutes}m)`;
|
||
scheduledDetails.innerHTML = `Momenteel ingepland op <strong>${escapeHtml(existingTask.date)}</strong> ${timeInfo}. Pas hieronder de datum, tijd of duur aan om te herplannen.`;
|
||
}
|
||
if (titleInput) {
|
||
titleInput.value = existingTask.title || `Oppakken Jira item ${issue.key}`;
|
||
}
|
||
if (dateInput) {
|
||
dateInput.value = existingTask.date || targetDate;
|
||
}
|
||
setJiraPlanDuration(existingTask.duration_minutes || 30);
|
||
if (existingTask.priority) {
|
||
document.getElementById('jiraPlanPriority').value = existingTask.priority;
|
||
}
|
||
if (startTimeInput) {
|
||
startTimeInput.value = existingTask.time_block_start || '';
|
||
}
|
||
} else {
|
||
if (modalTitle) modalTitle.textContent = 'Jira Ticket Inplannen';
|
||
if (scheduledBanner) scheduledBanner.classList.add('hidden');
|
||
if (titleInput) {
|
||
titleInput.value = `Oppakken Jira item ${issue.key}`;
|
||
}
|
||
if (dateInput) {
|
||
dateInput.value = targetDate;
|
||
}
|
||
|
||
// Standaard duur: 15 min (of op basis van SP: 1 SP = 30 min)
|
||
let defaultDuration = 15;
|
||
if (issue.story_points && issue.story_points > 0) {
|
||
const spMin = Math.round(issue.story_points * 30);
|
||
defaultDuration = [15, 30, 60, 90].reduce((prev, curr) =>
|
||
Math.abs(curr - spMin) < Math.abs(prev - spMin) ? curr : prev
|
||
);
|
||
}
|
||
setJiraPlanDuration(defaultDuration);
|
||
|
||
// Prioriteit: vertaal Jira priority naar Fociz priority
|
||
const jiraPrio = (issue.priority || '').toLowerCase();
|
||
const focizPrio = (jiraPrio === 'highest' || jiraPrio === 'critical') ? 'P1'
|
||
: (jiraPrio === 'high') ? 'P2'
|
||
: 'P3';
|
||
document.getElementById('jiraPlanPriority').value = focizPrio;
|
||
|
||
// Slimme starttijd: stel het volgende vrije kwartier voor
|
||
const now = new Date();
|
||
const todayStr = now.toISOString().slice(0, 10);
|
||
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
|
||
if (startTimeInput) {
|
||
startTimeInput.value = suggestTime;
|
||
}
|
||
}
|
||
|
||
const notice = document.getElementById('jiraPlanOpenBlockNotice');
|
||
if (notice) notice.classList.add('hidden');
|
||
updateJiraPlanEndTimeDisplay();
|
||
|
||
// Reset submit button state
|
||
const submitBtn = document.getElementById('btnSubmitJiraPlan');
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
if (isAlreadyPlanned) {
|
||
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
|
||
submitBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i><span>Herplannen</span>';
|
||
} else {
|
||
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
|
||
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
|
||
}
|
||
}
|
||
|
||
const modal = document.getElementById('jiraPlanModal');
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function closeJiraPlanModal() {
|
||
const modal = document.getElementById('jiraPlanModal');
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
const submitBtn = document.getElementById('btnSubmitJiraPlan');
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
|
||
}
|
||
_jiraPlanCurrentIssue = null;
|
||
}
|
||
|
||
function updateJiraPlanEndTimeDisplay() {
|
||
const startTimeInput = document.getElementById('jiraPlanStartTime');
|
||
const badge = document.getElementById('jiraPlanEndTimeBadge');
|
||
const durationInput = document.getElementById('jiraPlanDuration');
|
||
if (!badge) return;
|
||
|
||
const startTime = startTimeInput ? startTimeInput.value.trim() : '';
|
||
const duration = durationInput ? (parseInt(durationInput.value, 10) || 15) : 15;
|
||
|
||
if (!startTime) {
|
||
badge.textContent = '';
|
||
return;
|
||
}
|
||
|
||
const parts = startTime.split(':').map(Number);
|
||
if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {
|
||
badge.textContent = '';
|
||
return;
|
||
}
|
||
|
||
const endTotalMin = parts[0] * 60 + parts[1] + duration;
|
||
const endH = Math.floor(endTotalMin / 60);
|
||
const endM = endTotalMin % 60;
|
||
const endTimeStr = `${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')}`;
|
||
badge.textContent = `(tot ${endTimeStr})`;
|
||
}
|
||
|
||
function clearJiraPlanStartTime() {
|
||
const startInput = document.getElementById('jiraPlanStartTime');
|
||
if (startInput) {
|
||
startInput.value = '';
|
||
updateJiraPlanEndTimeDisplay();
|
||
}
|
||
}
|
||
|
||
async function planJiraInNextOpenBlock() {
|
||
const durationInput = document.getElementById('jiraPlanDuration');
|
||
const duration = durationInput ? (parseInt(durationInput.value, 10) || 15) : 15;
|
||
const btn = document.getElementById('btnJiraPlanOpenBlock');
|
||
const notice = document.getElementById('jiraPlanOpenBlockNotice');
|
||
const noticeText = document.getElementById('jiraPlanOpenBlockNoticeText');
|
||
|
||
let origBtnHtml = '';
|
||
if (btn) {
|
||
origBtnHtml = btn.innerHTML;
|
||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
|
||
btn.disabled = true;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
duration_minutes: duration
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success && data.slot) {
|
||
const slot = data.slot;
|
||
const dateInput = document.getElementById('jiraPlanDate');
|
||
if (dateInput) dateInput.value = slot.date;
|
||
|
||
const startInput = document.getElementById('jiraPlanStartTime');
|
||
if (startInput) startInput.value = slot.start;
|
||
|
||
updateJiraPlanEndTimeDisplay();
|
||
|
||
if (notice && noticeText) {
|
||
noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`;
|
||
notice.classList.remove('hidden');
|
||
}
|
||
} else {
|
||
alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij zoeken naar een open blok.');
|
||
} finally {
|
||
if (btn) {
|
||
btn.innerHTML = origBtnHtml;
|
||
btn.disabled = false;
|
||
}
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function setJiraPlanDuration(minutes) {
|
||
document.getElementById('jiraPlanDuration').value = minutes;
|
||
document.querySelectorAll('.jira-plan-dur-btn').forEach(btn => {
|
||
const dur = parseInt(btn.getAttribute('data-dur'), 10);
|
||
if (dur === minutes) {
|
||
btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-xs';
|
||
} else {
|
||
btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:border-indigo-400';
|
||
}
|
||
});
|
||
updateJiraPlanEndTimeDisplay();
|
||
}
|
||
|
||
function suggestNextFreeSlot() {
|
||
const now = new Date();
|
||
let h = now.getHours();
|
||
let m = now.getMinutes();
|
||
m = Math.ceil((m + 1) / 15) * 15;
|
||
if (m >= 60) { h += 1; m = 0; }
|
||
|
||
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
|
||
const wEnd = (typeof FOCIZ_WORKDAY_END !== 'undefined' && FOCIZ_WORKDAY_END) ? FOCIZ_WORKDAY_END : '17:00';
|
||
const [startH, startM] = wStart.split(':').map(Number);
|
||
const [endH, endM] = wEnd.split(':').map(Number);
|
||
|
||
const curMin = h * 60 + m;
|
||
const startMin = startH * 60 + startM;
|
||
const endMin = endH * 60 + endM;
|
||
|
||
if (curMin < startMin) {
|
||
return `${String(startH).padStart(2,'0')}:${String(startM).padStart(2,'0')}`;
|
||
}
|
||
if (curMin >= endMin) {
|
||
return `${String(startH).padStart(2,'0')}:${String(startM).padStart(2,'0')}`;
|
||
}
|
||
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
|
||
}
|
||
|
||
async function submitJiraPlanModal() {
|
||
if (!_jiraPlanCurrentIssue) return;
|
||
const issue = _jiraPlanCurrentIssue;
|
||
|
||
const titleInput = document.getElementById('jiraPlanCustomTitle');
|
||
const customTitle = titleInput ? titleInput.value.trim() : '';
|
||
const finalTitle = customTitle || `Oppakken Jira item ${issue.key}`;
|
||
|
||
const dateInput = document.getElementById('jiraPlanDate');
|
||
let planDate = (dateInput && dateInput.value) ? dateInput.value : ((typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||
|
||
const duration = parseInt(document.getElementById('jiraPlanDuration').value, 10) || 15;
|
||
let startTime = document.getElementById('jiraPlanStartTime').value.trim();
|
||
const priority = document.getElementById('jiraPlanPriority').value;
|
||
|
||
// Controleer of de ingeplande tijd buiten de gezette werktijden valt
|
||
if (startTime) {
|
||
const [hh, mm] = startTime.split(':').map(Number);
|
||
const endMin = hh * 60 + mm + duration;
|
||
const endTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
|
||
|
||
const check = checkWorkdayBounds(planDate, startTime, endTime, duration);
|
||
if (check.isOutside) {
|
||
const answer = await promptOutsideWorkhoursModal(check);
|
||
if (answer === 'CANCEL') {
|
||
return;
|
||
}
|
||
if (answer === 'NEE') {
|
||
// Automatisch naar volgende werkdag met eerder gezette voorwaarden
|
||
try {
|
||
const nextSlot = await findSlotOnNextWorkday(duration, 0);
|
||
planDate = nextSlot.date;
|
||
startTime = nextSlot.start;
|
||
} catch (err) {
|
||
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const btn = document.getElementById('btnSubmitJiraPlan');
|
||
const origHtml = btn ? btn.innerHTML : '';
|
||
if (btn) {
|
||
btn.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
|
||
btn.disabled = true;
|
||
}
|
||
|
||
const existingTaskId = parseInt(document.getElementById('jiraPlanTaskId')?.value || '0', 10);
|
||
const isUpdate = existingTaskId > 0;
|
||
|
||
try {
|
||
let res, data;
|
||
let finalStartTime = startTime;
|
||
let finalEndTime = '';
|
||
if (startTime) {
|
||
const [hh, mm] = startTime.split(':').map(Number);
|
||
const endMin = hh * 60 + mm + duration;
|
||
finalEndTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
|
||
}
|
||
|
||
if (isUpdate) {
|
||
// Bestaande taak bijwerken / verplaatsen naar eventueel andere datum
|
||
res = await fetch('/api/planner.php?action=update_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
task_id: existingTaskId,
|
||
title: finalTitle,
|
||
priority: priority,
|
||
duration_minutes: duration,
|
||
time_block_start: finalStartTime,
|
||
time_block_end: finalEndTime,
|
||
date: planDate
|
||
})
|
||
});
|
||
data = await res.json();
|
||
} else {
|
||
// Nieuwe taak toevoegen
|
||
const payload = {
|
||
title: finalTitle,
|
||
priority: priority,
|
||
source: 'JIRA',
|
||
external_id: issue.key,
|
||
external_url: issue.url || '',
|
||
duration_minutes: duration,
|
||
date: planDate
|
||
};
|
||
if (finalStartTime) {
|
||
payload.time_block_start = finalStartTime;
|
||
payload.time_block_end = finalEndTime;
|
||
}
|
||
|
||
res = await fetch('/api/planner.php?action=add_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
data = await res.json();
|
||
}
|
||
|
||
if (data.success) {
|
||
closeJiraPlanModal();
|
||
const actionVerb = isUpdate ? 'geherpland' : 'ingepland';
|
||
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> succesvol ${actionVerb}`, 'success');
|
||
|
||
// Live update van cache en sets
|
||
const savedTaskId = isUpdate ? existingTaskId : (data.task ? data.task.id : existingTaskId);
|
||
if (typeof plannedExternalTasksMap !== 'undefined') {
|
||
plannedExternalTasksMap[issue.key] = {
|
||
task_id: savedTaskId,
|
||
title: finalTitle,
|
||
priority: priority,
|
||
time_block_start: finalStartTime || '',
|
||
time_block_end: finalEndTime || '',
|
||
duration_minutes: duration,
|
||
date: planDate
|
||
};
|
||
}
|
||
if (typeof plannedExternalIdsSet !== 'undefined') {
|
||
plannedExternalIdsSet.add(issue.key);
|
||
}
|
||
|
||
// Knoppen bijwerken
|
||
document.querySelectorAll(`.plan-btn-jira[data-jira-key="${issue.key}"]`).forEach(btn => {
|
||
btn.className = "plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer";
|
||
btn.title = "Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)";
|
||
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>';
|
||
});
|
||
const detailPlanBtn = document.getElementById('btnJiraDetailPlan');
|
||
if (detailPlanBtn && typeof _jiraPlanCurrentIssue !== 'undefined' && _jiraPlanCurrentIssue && _jiraPlanCurrentIssue.key === issue.key) {
|
||
detailPlanBtn.className = "flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs transition cursor-pointer shadow-xs";
|
||
detailPlanBtn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 stroke-[2.5]"></i><span>Ingepland</span>';
|
||
}
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) lucide.createIcons();
|
||
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
} else {
|
||
alert(data.error || 'Kon ticket niet inplannen');
|
||
}
|
||
} catch(e) {
|
||
alert('Netwerkfout bij inplannen');
|
||
} finally {
|
||
if (btn) {
|
||
btn.innerHTML = origHtml || '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
|
||
btn.disabled = false;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function quickImportJiraByKey(btn, issueKey) {
|
||
// Plan knop → open de plan modal (niet direct importeren)
|
||
openJiraPlanModal(issueKey);
|
||
}
|
||
|
||
|
||
// --- Direct E-mail Inplannen als Dagtaak ---
|
||
let _emailPlanCurrentData = null;
|
||
|
||
function toggleEmailPlanPreview() {
|
||
const wrapper = document.getElementById('emailPlanDescWrapper');
|
||
const chevron = document.getElementById('emailPlanDescChevron');
|
||
const label = document.getElementById('emailPlanDescToggleLabel');
|
||
if (!wrapper) return;
|
||
const isHidden = wrapper.classList.contains('hidden');
|
||
if (isHidden) {
|
||
wrapper.classList.remove('hidden');
|
||
if (chevron) chevron.classList.add('rotate-180');
|
||
if (label) label.textContent = 'Inhoud verbergen';
|
||
} else {
|
||
wrapper.classList.add('hidden');
|
||
if (chevron) chevron.classList.remove('rotate-180');
|
||
if (label) label.textContent = 'Inhoud van het bericht bekijken';
|
||
}
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function openPlanEmailDirect(mailId, subject, sender, webLink, bodyPreview = '') {
|
||
_emailPlanCurrentData = { mailId, subject, sender, webLink, bodyPreview };
|
||
|
||
document.getElementById('planEmailId').value = mailId || '';
|
||
document.getElementById('planEmailWebLink').value = webLink || '';
|
||
document.getElementById('planEmailSender').value = sender || 'Outlook Mail';
|
||
document.getElementById('planEmailModalSenderBadge').textContent = sender || 'Outlook Mail';
|
||
document.getElementById('planEmailModalSenderName').textContent = sender || 'Outlook Mail';
|
||
document.getElementById('planEmailModalSubject').textContent = subject || '(Geen onderwerp)';
|
||
|
||
// Titel input
|
||
const titleInput = document.getElementById('planEmailTitle');
|
||
if (titleInput) {
|
||
titleInput.value = subject ? `[Mail] ${subject}` : '[Mail] E-mail opvolgen';
|
||
}
|
||
|
||
// Body preview
|
||
const descEl = document.getElementById('emailPlanModalDescription');
|
||
if (descEl) {
|
||
descEl.textContent = (bodyPreview && bodyPreview.trim()) ? bodyPreview.trim() : 'Geen berichtvoorbeeld beschikbaar.';
|
||
}
|
||
const descWrapper = document.getElementById('emailPlanDescWrapper');
|
||
const descChevron = document.getElementById('emailPlanDescChevron');
|
||
const descLabel = document.getElementById('emailPlanDescToggleLabel');
|
||
if (descWrapper) descWrapper.classList.add('hidden');
|
||
if (descChevron) descChevron.classList.remove('rotate-180');
|
||
if (descLabel) descLabel.textContent = 'Inhoud van het bericht bekijken';
|
||
|
||
// Datum: actieve planner datum of vandaag
|
||
const dateInput = document.getElementById('planEmailDate');
|
||
const targetDate = (typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10);
|
||
if (dateInput) {
|
||
dateInput.value = targetDate;
|
||
}
|
||
|
||
// Duur: standaard 30 minuten
|
||
setEmailPlanDuration(30);
|
||
|
||
// Prioriteit: standaard P2
|
||
const prioEl = document.getElementById('planEmailPriority');
|
||
if (prioEl) prioEl.value = 'P2';
|
||
|
||
// Starttijd: suggestie
|
||
const now = new Date();
|
||
const todayStr = now.toISOString().slice(0, 10);
|
||
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
|
||
document.getElementById('planEmailStartTime').value = suggestTime;
|
||
|
||
const notice = document.getElementById('emailPlanOpenBlockNotice');
|
||
if (notice) notice.classList.add('hidden');
|
||
updatePlanEmailEndTimeDisplay();
|
||
|
||
// Reset submit button state
|
||
const submitBtn = document.getElementById('btnSubmitPlanEmailDirect');
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
|
||
}
|
||
|
||
const modal = document.getElementById('planEmailDirectModal');
|
||
if (modal) {
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
}
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function closePlanEmailDirectModal() {
|
||
const modal = document.getElementById('planEmailDirectModal');
|
||
if (modal) {
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
const submitBtn = document.getElementById('btnSubmitPlanEmailDirect');
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
|
||
}
|
||
_emailPlanCurrentData = null;
|
||
}
|
||
|
||
function setEmailPlanDuration(minutes) {
|
||
const durInput = document.getElementById('planEmailDuration');
|
||
if (durInput) durInput.value = minutes;
|
||
document.querySelectorAll('.email-plan-dur-btn').forEach(btn => {
|
||
const dur = parseInt(btn.getAttribute('data-dur'), 10);
|
||
if (dur === minutes) {
|
||
btn.className = 'email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-xs';
|
||
} else {
|
||
btn.className = 'email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:border-indigo-400';
|
||
}
|
||
});
|
||
updatePlanEmailEndTimeDisplay();
|
||
}
|
||
|
||
function clearPlanEmailStartTime() {
|
||
const startInput = document.getElementById('planEmailStartTime');
|
||
if (startInput) {
|
||
startInput.value = '';
|
||
updatePlanEmailEndTimeDisplay();
|
||
}
|
||
}
|
||
|
||
function updatePlanEmailEndTimeDisplay() {
|
||
const startTimeInput = document.getElementById('planEmailStartTime');
|
||
const badge = document.getElementById('planEmailEndTimeBadge');
|
||
const durationInput = document.getElementById('planEmailDuration');
|
||
if (!badge) return;
|
||
|
||
const startTime = startTimeInput ? startTimeInput.value.trim() : '';
|
||
const duration = durationInput ? (parseInt(durationInput.value, 10) || 30) : 30;
|
||
|
||
if (!startTime) {
|
||
badge.textContent = '';
|
||
return;
|
||
}
|
||
|
||
const parts = startTime.split(':').map(Number);
|
||
if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {
|
||
badge.textContent = '';
|
||
return;
|
||
}
|
||
|
||
const endTotalMin = parts[0] * 60 + parts[1] + duration;
|
||
const endH = Math.floor(endTotalMin / 60);
|
||
const endM = endTotalMin % 60;
|
||
const endTimeStr = `${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')}`;
|
||
badge.textContent = `(tot ${endTimeStr})`;
|
||
}
|
||
|
||
async function planEmailInNextOpenBlock() {
|
||
const durationInput = document.getElementById('planEmailDuration');
|
||
const duration = durationInput ? (parseInt(durationInput.value, 10) || 30) : 30;
|
||
const btn = document.getElementById('btnEmailPlanOpenBlock');
|
||
const notice = document.getElementById('emailPlanOpenBlockNotice');
|
||
const noticeText = document.getElementById('emailPlanOpenBlockNoticeText');
|
||
|
||
let origBtnHtml = '';
|
||
if (btn) {
|
||
origBtnHtml = btn.innerHTML;
|
||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
|
||
btn.disabled = true;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
duration_minutes: duration
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success && data.slot) {
|
||
const slot = data.slot;
|
||
const dateInput = document.getElementById('planEmailDate');
|
||
if (dateInput) dateInput.value = slot.date;
|
||
|
||
const startInput = document.getElementById('planEmailStartTime');
|
||
if (startInput) startInput.value = slot.start;
|
||
|
||
updatePlanEmailEndTimeDisplay();
|
||
|
||
if (notice && noticeText) {
|
||
noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`;
|
||
notice.classList.remove('hidden');
|
||
}
|
||
} else {
|
||
alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij zoeken naar een open blok.');
|
||
} finally {
|
||
if (btn) {
|
||
btn.innerHTML = origBtnHtml;
|
||
btn.disabled = false;
|
||
}
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
async function submitPlanEmailDirect() {
|
||
const mailId = document.getElementById('planEmailId').value;
|
||
const title = document.getElementById('planEmailTitle').value.trim();
|
||
const priority = document.getElementById('planEmailPriority').value;
|
||
const duration = parseInt(document.getElementById('planEmailDuration').value, 10) || 30;
|
||
let startTime = document.getElementById('planEmailStartTime').value.trim();
|
||
const syncOutlook = document.getElementById('planEmailSyncOutlook') ? document.getElementById('planEmailSyncOutlook').checked : false;
|
||
const webLink = document.getElementById('planEmailWebLink').value;
|
||
const sender = document.getElementById('planEmailSender').value;
|
||
|
||
const dateInput = document.getElementById('planEmailDate');
|
||
let planDate = (dateInput && dateInput.value) ? dateInput.value : ((typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||
|
||
if (!title) {
|
||
alert('Vul een taaktitel in');
|
||
return;
|
||
}
|
||
|
||
if (startTime) {
|
||
const [hh, mm] = startTime.split(':').map(Number);
|
||
const endMin = hh * 60 + mm + duration;
|
||
const endTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
|
||
|
||
const check = checkWorkdayBounds(planDate, startTime, endTime, duration);
|
||
if (check.isOutside) {
|
||
const answer = await promptOutsideWorkhoursModal(check);
|
||
if (answer === 'CANCEL') {
|
||
return;
|
||
}
|
||
if (answer === 'NEE') {
|
||
try {
|
||
const nextSlot = await findSlotOnNextWorkday(duration, 0);
|
||
planDate = nextSlot.date;
|
||
startTime = nextSlot.start;
|
||
} catch (err) {
|
||
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const btn = document.getElementById('btnSubmitPlanEmailDirect');
|
||
const origHtml = btn ? btn.innerHTML : '';
|
||
if (btn) {
|
||
btn.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
|
||
btn.disabled = true;
|
||
}
|
||
|
||
try {
|
||
let timeBlockEnd = null;
|
||
if (startTime) {
|
||
const [hh, mm] = startTime.split(':').map(Number);
|
||
const endMin = hh * 60 + mm + duration;
|
||
timeBlockEnd = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
|
||
}
|
||
|
||
const res = await fetch('/api/mail.php?action=plan_email', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
mail_id: mailId,
|
||
title: title,
|
||
sender: sender,
|
||
web_link: webLink,
|
||
priority: priority,
|
||
duration_minutes: duration,
|
||
time_block_start: startTime,
|
||
time_block_end: timeBlockEnd,
|
||
sync_outlook: syncOutlook,
|
||
date: planDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
closePlanEmailDirectModal();
|
||
showWidgetToast(`<strong>E-mail</strong> succesvol ingepland als dagtaak`, 'success');
|
||
|
||
// Live update: voeg toe aan plannedExternalIdsSet en transformeer knoppen naar groen 'Ingepland'
|
||
if (mailId) {
|
||
if (typeof plannedExternalIdsSet !== 'undefined') {
|
||
plannedExternalIdsSet.add(mailId);
|
||
}
|
||
document.querySelectorAll(`.plan-btn-mail[data-mail-id="${mailId}"]`).forEach(btn => {
|
||
btn.className = "plan-btn-mail flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition cursor-pointer";
|
||
btn.title = "Deze e-mail is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)";
|
||
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>';
|
||
});
|
||
if (typeof lucide !== 'undefined' && lucide.createIcons) lucide.createIcons();
|
||
}
|
||
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
} else {
|
||
alert(data.error || 'Kon e-mail niet inplannen');
|
||
}
|
||
} catch(err) {
|
||
alert('Netwerkfout bij inplannen van e-mail');
|
||
} finally {
|
||
if (btn) {
|
||
btn.innerHTML = origHtml || '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
|
||
btn.disabled = false;
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Scratchpad Debounced Auto-Save ---
|
||
let scratchpadTimer = null;
|
||
function autoSaveScratchpad() {
|
||
const statusEl = document.getElementById('scratchpadSaveStatus');
|
||
if (statusEl) statusEl.innerText = 'Wijzigingen worden opgeslagen...';
|
||
|
||
clearTimeout(scratchpadTimer);
|
||
scratchpadTimer = setTimeout(async () => {
|
||
const notes = document.getElementById('dailyNotesInput').value;
|
||
try {
|
||
await fetch('/api/planner.php?action=save_scratchpad', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ notes: notes, date: activePlannerDate })
|
||
});
|
||
if (statusEl) statusEl.innerText = 'Automatisch opgeslagen';
|
||
} catch (e) {
|
||
if (statusEl) statusEl.innerText = 'Fout bij opslaan';
|
||
}
|
||
}, 800);
|
||
}
|
||
|
||
// --- Habit Tracker Logica ---
|
||
function updateHabitCount() {
|
||
const checkboxes = document.querySelectorAll('#habitTrackerWidget input[type="checkbox"]');
|
||
const total = checkboxes.length;
|
||
let checked = 0;
|
||
checkboxes.forEach(cb => { if (cb.checked) checked++; });
|
||
const badge = document.getElementById('habitProgressBadge');
|
||
if (badge) {
|
||
badge.innerText = `${checked}/${total} gedaan`;
|
||
if (checked === total) {
|
||
badge.classList.remove('text-rose-600', 'dark:text-rose-400');
|
||
badge.classList.add('text-emerald-600', 'dark:text-emerald-400');
|
||
} else {
|
||
badge.classList.add('text-rose-600', 'dark:text-rose-400');
|
||
badge.classList.remove('text-emerald-600', 'dark:text-emerald-400');
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Focus Sounds Web Audio Synthesizer ---
|
||
let activeSoundAudio = null;
|
||
let currentSoundType = null;
|
||
function toggleSound(type, btn) {
|
||
const statusText = document.getElementById('soundStatusText');
|
||
const allBtns = document.querySelectorAll('.sound-btn');
|
||
|
||
if (currentSoundType === type) {
|
||
if (activeSoundAudio) {
|
||
activeSoundAudio.pause();
|
||
activeSoundAudio = null;
|
||
}
|
||
currentSoundType = null;
|
||
if (statusText) statusText.innerText = 'Gepauzeerd';
|
||
allBtns.forEach(b => b.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-50/80', 'dark:bg-indigo-500/20'));
|
||
return;
|
||
}
|
||
|
||
if (activeSoundAudio) {
|
||
activeSoundAudio.pause();
|
||
}
|
||
allBtns.forEach(b => b.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-50/80', 'dark:bg-indigo-500/20'));
|
||
|
||
currentSoundType = type;
|
||
btn.classList.add('ring-2', 'ring-indigo-500', 'bg-indigo-50/80', 'dark:bg-indigo-500/20');
|
||
if (statusText) {
|
||
const names = { rain: 'Regen speelt...', forest: 'Bos speelt...', cafe: 'Koffiebar speelt...', whitenoise: 'Witte ruis speelt...' };
|
||
statusText.innerText = names[type] || 'Focus sound actief';
|
||
}
|
||
}
|
||
|
||
// --- Real-Time Custom Events van Notifications.js ---
|
||
window.addEventListener('fociz:mail-updated', () => {
|
||
if (document.getElementById('plannerMailWidget')) {
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
}
|
||
});
|
||
|
||
window.addEventListener('fociz:jira-updated', () => {
|
||
if (document.getElementById('jiraPanel')) {
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
}
|
||
});
|
||
|
||
window.addEventListener('fociz:calendar-updated', () => {
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
});
|
||
|
||
// Veilige stubs voor verwijderde Focus Modus timer (voorkomt JavaScript runtime fouten)
|
||
function selectFocusTask(title) {}
|
||
function updateTimerDisplay() {}
|
||
function setTimerPreset(min) {}
|
||
function toggleTimer() {}
|
||
function resetTimer() {}
|
||
function saveTimerStateBeforeReload() {}
|
||
function restoreTimerStateIfAny() {}
|
||
|
||
// --- Globale Planner Status & Naadloos AJAX Datumbeheer ---
|
||
let activePlannerDate = '<?= $selectedDate ?>';
|
||
let weekStartDate = '<?= $weekStart ?>';
|
||
let weekEndDate = '<?= $weekEnd ?>';
|
||
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
|
||
let isDateSwitching = false;
|
||
const FOCIZ_WORKDAY_START = '<?= $workdayStartStr ?>';
|
||
const FOCIZ_WORKDAY_END = '<?= $workdayEndStr ?>';
|
||
|
||
/**
|
||
* Controleert of een gepland tijdstip buiten de gezette werktijden of in het weekend valt
|
||
*/
|
||
function checkWorkdayBounds(planDate, startTime, endTime, duration) {
|
||
if (!startTime || !startTime.trim()) {
|
||
return { isOutside: false };
|
||
}
|
||
|
||
const start = startTime.trim();
|
||
let [sH, sM] = start.split(':').map(Number);
|
||
if (isNaN(sH) || isNaN(sM)) return { isOutside: false };
|
||
|
||
let dur = parseInt(duration, 10);
|
||
if (isNaN(dur) || dur <= 0) dur = 30;
|
||
|
||
let end = (endTime && endTime.trim()) ? endTime.trim() : '';
|
||
let eH, eM;
|
||
if (end) {
|
||
[eH, eM] = end.split(':').map(Number);
|
||
}
|
||
if (isNaN(eH) || isNaN(eM)) {
|
||
const totalEndMin = sH * 60 + sM + dur;
|
||
eH = Math.floor(totalEndMin / 60);
|
||
eM = totalEndMin % 60;
|
||
end = `${String(eH).padStart(2,'0')}:${String(eM).padStart(2,'0')}`;
|
||
}
|
||
|
||
const startMin = sH * 60 + sM;
|
||
const endMin = eH * 60 + eM;
|
||
|
||
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
|
||
const wEnd = (typeof FOCIZ_WORKDAY_END !== 'undefined' && FOCIZ_WORKDAY_END) ? FOCIZ_WORKDAY_END : '17:00';
|
||
|
||
const [wsH, wsM] = wStart.split(':').map(Number);
|
||
const [weH, weM] = wEnd.split(':').map(Number);
|
||
const wStartMin = wsH * 60 + wsM;
|
||
const wEndMin = weH * 60 + weM;
|
||
|
||
let isOutside = false;
|
||
let reason = '';
|
||
|
||
if (planDate) {
|
||
const d = new Date(planDate + 'T12:00:00');
|
||
const dayOfWeek = d.getDay(); // 0 = Zo, 6 = Za
|
||
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
||
isOutside = true;
|
||
reason = 'Het gekozen tijdstip valt in het weekend.';
|
||
}
|
||
}
|
||
|
||
if (startMin < wStartMin || endMin > wEndMin || startMin >= wEndMin) {
|
||
isOutside = true;
|
||
reason = `Het gekozen tijdstip (${start} - ${end}) valt buiten de gezette werktijden (${wStart} - ${wEnd}).`;
|
||
}
|
||
|
||
return {
|
||
isOutside,
|
||
reason,
|
||
start,
|
||
end,
|
||
duration: dur,
|
||
workdayStart: wStart,
|
||
workdayEnd: wEnd,
|
||
date: planDate
|
||
};
|
||
}
|
||
|
||
let _outsideWorkhoursResolver = null;
|
||
|
||
function promptOutsideWorkhoursModal(checkResult) {
|
||
return new Promise((resolve) => {
|
||
_outsideWorkhoursResolver = resolve;
|
||
const modal = document.getElementById('outsideWorkhoursModal');
|
||
const selectedEl = document.getElementById('outsideWorkhoursSelectedSlot');
|
||
const boundsEl = document.getElementById('outsideWorkhoursBounds');
|
||
|
||
if (selectedEl) {
|
||
selectedEl.textContent = `${checkResult.start} - ${checkResult.end} (${checkResult.duration}m)`;
|
||
}
|
||
if (boundsEl) {
|
||
boundsEl.textContent = `${checkResult.workdayStart} tot ${checkResult.workdayEnd}`;
|
||
}
|
||
|
||
if (modal) {
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
setTimeout(() => {
|
||
const noBtn = document.getElementById('btnOutsideWorkhoursNo');
|
||
if (noBtn) noBtn.focus();
|
||
}, 50);
|
||
} else {
|
||
const ok = confirm(`Let op! Je plant buiten werktijden (${checkResult.workdayStart} - ${checkResult.workdayEnd}).\n\nKlik OK (Ja) om toch zo in te plannen, of Annuleren (Nee) om automatisch naar de volgende werkdag te gaan.`);
|
||
resolve(ok ? 'JA' : 'NEE');
|
||
}
|
||
});
|
||
}
|
||
|
||
function closeOutsideWorkhoursModal(choice) {
|
||
const modal = document.getElementById('outsideWorkhoursModal');
|
||
if (modal) {
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
if (typeof _outsideWorkhoursResolver === 'function') {
|
||
const res = _outsideWorkhoursResolver;
|
||
_outsideWorkhoursResolver = null;
|
||
res(choice);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Zoekt automatisch het eerstvolgende open kwartierblok op de volgende werkdag
|
||
*/
|
||
async function findSlotOnNextWorkday(duration, taskId = 0) {
|
||
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
duration_minutes: duration,
|
||
task_id: taskId,
|
||
start_from_next_workday: true
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success && data.slot) {
|
||
return data.slot;
|
||
}
|
||
throw new Error(data.error || 'Geen vrij open blok gevonden op de volgende werkdag.');
|
||
}
|
||
|
||
// Globale sneltoetsafhandeling voor modals (Escape om te sluiten, Enter voor actie)
|
||
window.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
const outsideModal = document.getElementById('outsideWorkhoursModal');
|
||
if (outsideModal && !outsideModal.classList.contains('hidden')) {
|
||
closeOutsideWorkhoursModal('CANCEL');
|
||
return;
|
||
}
|
||
const jiraModal = document.getElementById('jiraPlanModal');
|
||
if (jiraModal && !jiraModal.classList.contains('hidden')) {
|
||
closeJiraPlanModal();
|
||
return;
|
||
}
|
||
const editModal = document.getElementById('editTaskModal');
|
||
if (editModal && !editModal.classList.contains('hidden')) {
|
||
closeEditTaskModal();
|
||
return;
|
||
}
|
||
const emailModal = document.getElementById('planEmailDirectModal');
|
||
if (emailModal && !emailModal.classList.contains('hidden')) {
|
||
closePlanEmailDirectModal();
|
||
return;
|
||
}
|
||
const detailModal = document.getElementById('jiraTicketDetailModal');
|
||
if (detailModal && !detailModal.classList.contains('hidden')) {
|
||
closeJiraTicketDetailModal();
|
||
return;
|
||
}
|
||
} else if (e.key === 'Enter') {
|
||
const outsideModal = document.getElementById('outsideWorkhoursModal');
|
||
if (outsideModal && !outsideModal.classList.contains('hidden')) {
|
||
e.preventDefault();
|
||
closeOutsideWorkhoursModal('NEE');
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 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 todayStr = '<?= $today ?>';
|
||
const newUrl = (newDate === todayStr) ? 'planner.php' : `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();
|
||
}
|
||
initPlannerWidgets();
|
||
if (document.getElementById('plannerJiraListContainer')) {
|
||
filterPlannerJira();
|
||
}
|
||
|
||
// 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);
|
||
} 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') || '<?= $today ?>';
|
||
if (dateFromUrl !== activePlannerDate) {
|
||
switchPlannerDate(dateFromUrl, null, false);
|
||
}
|
||
});
|
||
|
||
// Schoon direct de browser URL op indien deze ?date=VANDAAG bevat
|
||
if (window.history && window.history.replaceState) {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
if (urlParams.get('date') === '<?= $today ?>') {
|
||
window.history.replaceState({}, document.title, 'planner.php');
|
||
}
|
||
}
|
||
|
||
// --- 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;
|
||
}
|
||
|
||
let planDate = activePlannerDate;
|
||
if (time) {
|
||
const check = checkWorkdayBounds(planDate, time, endTime, duration);
|
||
if (check.isOutside) {
|
||
const answer = await promptOutsideWorkhoursModal(check);
|
||
if (answer === 'CANCEL') {
|
||
return;
|
||
}
|
||
if (answer === 'NEE') {
|
||
try {
|
||
const nextSlot = await findSlotOnNextWorkday(duration, 0);
|
||
planDate = nextSlot.date;
|
||
time = nextSlot.start;
|
||
endTime = nextSlot.end;
|
||
} catch (err) {
|
||
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
|
||
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: planDate
|
||
})
|
||
});
|
||
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(planDate, 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 || '';
|
||
const descEl = document.getElementById('editTaskDescription');
|
||
if (descEl) descEl.value = task.description || '';
|
||
document.getElementById('editTaskPriority').value = task.priority || 'P2';
|
||
document.getElementById('editTaskStartTime').value = task.start || '';
|
||
document.getElementById('editTaskEndTime').value = task.end || '';
|
||
|
||
// Datum
|
||
const dateInput = document.getElementById('editTaskDate');
|
||
if (dateInput) {
|
||
dateInput.value = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||
}
|
||
|
||
// Reset statusbericht van open blok
|
||
const notice = document.getElementById('editTaskOpenBlockNotice');
|
||
if (notice) notice.classList.add('hidden');
|
||
|
||
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 clearEditTaskTimes() {
|
||
const s = document.getElementById('editTaskStartTime');
|
||
const e = document.getElementById('editTaskEndTime');
|
||
if (s) s.value = '';
|
||
if (e) e.value = '';
|
||
const notice = document.getElementById('editTaskOpenBlockNotice');
|
||
if (notice) notice.classList.add('hidden');
|
||
}
|
||
|
||
async function planInNextOpenBlock() {
|
||
const duration = getSelectedEditTaskDurationMinutes();
|
||
const taskId = parseInt(document.getElementById('editTaskId')?.value, 10) || 0;
|
||
const btn = document.getElementById('btnPlanOpenBlock');
|
||
const notice = document.getElementById('editTaskOpenBlockNotice');
|
||
const noticeText = document.getElementById('editTaskOpenBlockNoticeText');
|
||
|
||
let origBtnHtml = '';
|
||
if (btn) {
|
||
origBtnHtml = btn.innerHTML;
|
||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
|
||
btn.disabled = true;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
duration_minutes: duration,
|
||
task_id: taskId
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success && data.slot) {
|
||
const slot = data.slot;
|
||
|
||
// Stel datum en tijden in
|
||
const dateInput = document.getElementById('editTaskDate');
|
||
if (dateInput) {
|
||
dateInput.value = slot.date;
|
||
}
|
||
const startInput = document.getElementById('editTaskStartTime');
|
||
if (startInput) {
|
||
startInput.value = slot.start;
|
||
}
|
||
const endInput = document.getElementById('editTaskEndTime');
|
||
if (endInput) {
|
||
endInput.value = slot.end;
|
||
}
|
||
|
||
// Toon statusmelding in dialoog
|
||
if (notice && noticeText) {
|
||
noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`;
|
||
notice.classList.remove('hidden');
|
||
}
|
||
} else {
|
||
alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij zoeken naar een open blok.');
|
||
} finally {
|
||
if (btn) {
|
||
btn.innerHTML = origBtnHtml;
|
||
btn.disabled = false;
|
||
}
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
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`;
|
||
}
|
||
|
||
// --- Taak & Tijdblok Bewerken Modal Logica ---
|
||
function openEditTaskModal(task) {
|
||
if (!task) return;
|
||
const isOutlook = Boolean(task.is_outlook_event || task.source === 'OUTLOOK_LIVE');
|
||
|
||
document.getElementById('editTaskId').value = task.id;
|
||
const isOutlookInput = document.getElementById('editTaskIsOutlook');
|
||
if (isOutlookInput) isOutlookInput.value = isOutlook ? '1' : '0';
|
||
|
||
const headerTitle = document.getElementById('editTaskModalHeaderTitle');
|
||
const headerSubtitle = document.getElementById('editTaskModalHeaderSubtitle');
|
||
const titleLabel = document.getElementById('editTaskTitleLabel');
|
||
const priorityContainer = document.getElementById('editTaskPriorityContainer');
|
||
const saveBtn = document.getElementById('btnSaveEditTask');
|
||
|
||
if (isOutlook) {
|
||
if (headerTitle) headerTitle.textContent = 'Outlook Afspraak Bewerken';
|
||
if (headerSubtitle) headerSubtitle.textContent = 'Pas onderwerp, tijdblok en datum van je agenda-afspraak aan';
|
||
if (titleLabel) titleLabel.textContent = 'Onderwerp';
|
||
if (priorityContainer) priorityContainer.classList.add('hidden');
|
||
if (saveBtn) saveBtn.textContent = 'Afspraak Opslaan';
|
||
} else {
|
||
if (headerTitle) headerTitle.textContent = 'Taak & Tijdblok Bewerken';
|
||
if (headerSubtitle) headerSubtitle.textContent = 'Pas titel, start-/eindtijd en tijdsduur vrij aan';
|
||
if (titleLabel) titleLabel.textContent = 'Taaktitel';
|
||
if (priorityContainer) priorityContainer.classList.remove('hidden');
|
||
if (saveBtn) saveBtn.textContent = 'Wijzigingen Opslaan';
|
||
}
|
||
|
||
document.getElementById('editTaskTitle').value = (isOutlook ? (task.subject || task.title) : task.title) || '';
|
||
const descEl = document.getElementById('editTaskDescription');
|
||
if (descEl) descEl.value = task.description || '';
|
||
document.getElementById('editTaskPriority').value = task.priority || 'P2';
|
||
document.getElementById('editTaskStartTime').value = task.start || '';
|
||
document.getElementById('editTaskEndTime').value = task.end || '';
|
||
|
||
// Datum
|
||
const dateInput = document.getElementById('editTaskDate');
|
||
if (dateInput) {
|
||
dateInput.value = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||
}
|
||
|
||
// Reset statusbericht van open blok
|
||
const notice = document.getElementById('editTaskOpenBlockNotice');
|
||
if (notice) notice.classList.add('hidden');
|
||
|
||
let dur = task.duration;
|
||
if (!dur && task.start && task.end) {
|
||
const sm = parseTimeToMinutes(task.start);
|
||
const em = parseTimeToMinutes(task.end);
|
||
if (sm !== null && em !== null && em > sm) dur = em - sm;
|
||
}
|
||
dur = dur || 30;
|
||
setSelectedEditTaskDuration(dur);
|
||
|
||
const outlookNotice = document.getElementById('editTaskOutlookNotice');
|
||
if (outlookNotice) {
|
||
if (isOutlook) {
|
||
outlookNotice.classList.remove('hidden');
|
||
outlookNotice.innerHTML = '<i data-lucide="refresh-cw" class="w-3.5 h-3.5 flex-shrink-0"></i><span>Wijzigingen worden direct gesynchroniseerd naar Microsoft Outlook 365.</span>';
|
||
} else if (task.source === 'OUTLOOK' || task.external_id) {
|
||
outlookNotice.classList.remove('hidden');
|
||
outlookNotice.innerHTML = '<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>';
|
||
} 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');
|
||
}
|
||
}
|
||
|
||
async function submitEditTask() {
|
||
const isOutlook = document.getElementById('editTaskIsOutlook')?.value === '1';
|
||
const taskId = document.getElementById('editTaskId').value;
|
||
const title = document.getElementById('editTaskTitle').value.trim();
|
||
const priority = document.getElementById('editTaskPriority').value;
|
||
const dateInput = document.getElementById('editTaskDate');
|
||
let taskDate = (dateInput && dateInput.value) ? dateInput.value.trim() : (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||
let startTime = document.getElementById('editTaskStartTime').value.trim();
|
||
let endTime = document.getElementById('editTaskEndTime').value.trim();
|
||
const duration = getSelectedEditTaskDurationMinutes();
|
||
const saveBtn = document.getElementById('btnSaveEditTask');
|
||
|
||
if (!title) {
|
||
alert(isOutlook ? 'Vul een onderwerp in voor de afspraak.' : 'Vul een taaktitel in.');
|
||
return;
|
||
}
|
||
|
||
// Controleer of de geplande tijd buiten de gezette werktijden valt
|
||
if (startTime) {
|
||
const check = checkWorkdayBounds(taskDate, startTime, endTime, duration);
|
||
if (check.isOutside) {
|
||
const answer = await promptOutsideWorkhoursModal(check);
|
||
if (answer === 'CANCEL') {
|
||
return;
|
||
}
|
||
if (answer === 'NEE') {
|
||
// Automatisch naar volgende werkdag met eerder gezette voorwaarden
|
||
try {
|
||
const nextSlot = await findSlotOnNextWorkday(duration, isOutlook ? 0 : parseInt(taskId, 10));
|
||
taskDate = nextSlot.date;
|
||
startTime = nextSlot.start;
|
||
endTime = nextSlot.end;
|
||
} catch (err) {
|
||
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
|
||
return;
|
||
}
|
||
}
|
||
// Bij 'JA': ga gewoon door met het buiten werktijd ingevulde tijdstip
|
||
}
|
||
}
|
||
|
||
if (saveBtn) {
|
||
saveBtn.innerText = isOutlook ? 'Afspraak Opslaan...' : 'Opslaan...';
|
||
saveBtn.disabled = true;
|
||
}
|
||
|
||
try {
|
||
const description = document.getElementById('editTaskDescription')?.value.trim() || '';
|
||
|
||
if (isOutlook) {
|
||
const res = await fetch('/api/planner.php?action=update_outlook_event', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
event_id: taskId,
|
||
title: title,
|
||
description: description,
|
||
time_block_start: startTime,
|
||
time_block_end: endTime,
|
||
date: taskDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
closeEditTaskModal();
|
||
const targetDate = data.date || taskDate;
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(targetDate, null, false);
|
||
}
|
||
if (typeof showWidgetToast === 'function') {
|
||
showWidgetToast(`Outlook afspraak <strong>${escapeHtml(title)}</strong> succesvol bijgewerkt`, 'success');
|
||
}
|
||
} else {
|
||
alert(data.error || 'Kon afspraak niet bijwerken in Microsoft Outlook');
|
||
if (saveBtn) {
|
||
saveBtn.innerText = 'Afspraak Opslaan';
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
} else {
|
||
const res = await fetch('/api/planner.php?action=update_task', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
task_id: parseInt(taskId, 10),
|
||
title: title,
|
||
description: description,
|
||
priority: priority,
|
||
time_block_start: startTime,
|
||
time_block_end: endTime,
|
||
duration_minutes: duration,
|
||
date: taskDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
closeEditTaskModal();
|
||
const targetDate = data.date || taskDate;
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(targetDate, null, false);
|
||
}
|
||
if (typeof showWidgetToast === 'function') {
|
||
showWidgetToast(`Taak <strong>${escapeHtml(title)}</strong> succesvol bijgewerkt`, 'success');
|
||
}
|
||
} else {
|
||
alert(data.error || 'Kon taak niet bijwerken');
|
||
if (saveBtn) {
|
||
saveBtn.innerText = 'Wijzigingen Opslaan';
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
alert('Netwerkfout bij bijwerken');
|
||
if (saveBtn) {
|
||
saveBtn.innerText = isOutlook ? 'Afspraak Opslaan' : 'Wijzigingen Opslaan';
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function deleteOutlookEvent(eventId, eventSubject) {
|
||
const displayTitle = eventSubject ? ` "${eventSubject}"` : '';
|
||
if (!confirm(`Weet je zeker dat je de Outlook afspraak${displayTitle} wilt verwijderen uit je Microsoft Outlook agenda?`)) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/planner.php?action=delete_outlook_event', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ event_id: eventId })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
if (typeof showWidgetToast === 'function') {
|
||
showWidgetToast(`Afspraak <strong>${escapeHtml(eventSubject || 'Outlook')}</strong> verwijderd`, 'info');
|
||
}
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
} else {
|
||
alert(data.error || 'Kon afspraak niet verwijderen uit Outlook');
|
||
}
|
||
} catch (err) {
|
||
alert('Netwerkfout bij verwijderen van Outlook afspraak');
|
||
}
|
||
}
|
||
|
||
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(() => {
|
||
switchPlannerDate(activePlannerDate, null, false);
|
||
}, 300);
|
||
} 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);
|
||
}
|
||
|
||
// --- V2.0: Subtaken & Checklists ---
|
||
const _subtaskCache = {}; // task_id -> subtasks array
|
||
|
||
async function toggleSubtaskPanel(taskId) {
|
||
const panel = document.getElementById(`subtask-panel-${taskId}`);
|
||
if (!panel) return;
|
||
|
||
const isHidden = panel.classList.contains('hidden');
|
||
if (isHidden) {
|
||
panel.classList.remove('hidden');
|
||
// Laad subtaken als nog niet geladen
|
||
if (!_subtaskCache[taskId]) {
|
||
await loadSubtasks(taskId);
|
||
} else {
|
||
renderSubtasks(taskId);
|
||
}
|
||
// Focus invoerveld
|
||
const inp = document.getElementById(`subtask-input-${taskId}`);
|
||
if (inp) setTimeout(() => inp.focus(), 50);
|
||
} else {
|
||
panel.classList.add('hidden');
|
||
}
|
||
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
async function loadSubtasks(taskId) {
|
||
try {
|
||
const resp = await fetch(`/api/planner.php?action=get_subtasks&task_id=${taskId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
_subtaskCache[taskId] = data.subtasks || [];
|
||
renderSubtasks(taskId);
|
||
}
|
||
} catch (e) {
|
||
console.error('Subtaken laden mislukt:', e);
|
||
}
|
||
}
|
||
|
||
function renderSubtasks(taskId) {
|
||
const list = document.getElementById(`subtask-list-${taskId}`);
|
||
if (!list) return;
|
||
|
||
const subtasks = _subtaskCache[taskId] || [];
|
||
if (subtasks.length === 0) {
|
||
list.innerHTML = '<p class="text-[11px] text-slate-400 italic">Nog geen subtaken. Voeg hieronder toe.</p>';
|
||
return;
|
||
}
|
||
|
||
const doneCount = subtasks.filter(s => s.is_completed == 1).length;
|
||
const totalCount = subtasks.length;
|
||
|
||
list.innerHTML = `
|
||
<div class="flex items-center justify-between mb-1">
|
||
<span class="text-[10px] font-bold text-slate-500 dark:text-slate-400">${doneCount}/${totalCount} afgerond</span>
|
||
<div class="flex-1 mx-2 h-1 bg-slate-200 dark:bg-slate-800 rounded-full overflow-hidden">
|
||
<div class="h-full bg-indigo-500 transition-all duration-300" style="width: ${totalCount > 0 ? Math.round((doneCount/totalCount)*100) : 0}%"></div>
|
||
</div>
|
||
</div>
|
||
${subtasks.map(s => `
|
||
<div class="flex items-center space-x-2 group/sub py-1" id="subtask-item-${s.id}">
|
||
<button onclick="toggleSubtask(${s.id}, ${s.is_completed ? 0 : 1}, ${taskId})"
|
||
class="w-4 h-4 rounded border flex items-center justify-center flex-shrink-0 transition ${s.is_completed ? 'bg-indigo-600 border-indigo-600 text-white' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900'}">
|
||
${s.is_completed ? '<svg xmlns="http://www.w3.org/2000/svg" class="w-2.5 h-2.5 stroke-[3]" viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
|
||
</button>
|
||
<span class="text-xs flex-1 ${s.is_completed ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-700 dark:text-slate-300'}">${escapeHtml(s.title)}</span>
|
||
<button onclick="deleteSubtask(${s.id}, ${taskId})"
|
||
class="p-0.5 text-slate-300 hover:text-red-500 dark:text-slate-700 dark:hover:text-red-400 opacity-0 group-hover/sub:opacity-100 transition cursor-pointer rounded">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||
</button>
|
||
</div>
|
||
`).join('')}
|
||
`;
|
||
}
|
||
|
||
async function addSubtask(taskId) {
|
||
const inp = document.getElementById(`subtask-input-${taskId}`);
|
||
if (!inp) return;
|
||
const title = inp.value.trim();
|
||
if (!title) return;
|
||
|
||
inp.disabled = true;
|
||
try {
|
||
const resp = await fetch('/api/planner.php?action=add_subtask', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ task_id: taskId, title })
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success && data.subtask) {
|
||
if (!_subtaskCache[taskId]) _subtaskCache[taskId] = [];
|
||
_subtaskCache[taskId].push(data.subtask);
|
||
renderSubtasks(taskId);
|
||
inp.value = '';
|
||
} else {
|
||
alert(data.error || 'Kan subtaak niet toevoegen');
|
||
}
|
||
} catch (e) {
|
||
alert('Netwerkfout bij toevoegen subtaak');
|
||
} finally {
|
||
inp.disabled = false;
|
||
inp.focus();
|
||
}
|
||
}
|
||
|
||
async function toggleSubtask(subtaskId, newState, taskId) {
|
||
try {
|
||
await fetch('/api/planner.php?action=toggle_subtask', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ subtask_id: subtaskId, is_completed: newState })
|
||
});
|
||
// Update cache
|
||
if (_subtaskCache[taskId]) {
|
||
const sub = _subtaskCache[taskId].find(s => s.id == subtaskId);
|
||
if (sub) sub.is_completed = newState;
|
||
}
|
||
renderSubtasks(taskId);
|
||
} catch (e) {
|
||
console.error('Subtaak toggle mislukt:', e);
|
||
}
|
||
}
|
||
|
||
async function deleteSubtask(subtaskId, taskId) {
|
||
try {
|
||
await fetch('/api/planner.php?action=delete_subtask', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ subtask_id: subtaskId })
|
||
});
|
||
if (_subtaskCache[taskId]) {
|
||
_subtaskCache[taskId] = _subtaskCache[taskId].filter(s => s.id != subtaskId);
|
||
}
|
||
renderSubtasks(taskId);
|
||
} catch (e) {
|
||
console.error('Subtaak verwijderen mislukt:', e);
|
||
}
|
||
}
|
||
|
||
// Helper: XSS-safe escaping voor subtaak titels in JS
|
||
function escapeHtml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// --- 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();
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Drag & Drop van Taken & Afspraken op Tijdlijn ---
|
||
let draggedTimelineItem = null;
|
||
|
||
function handleTimelineItemDragStart(e, type, item) {
|
||
draggedTimelineItem = { type, item };
|
||
try {
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
e.dataTransfer.setData('text/plain', JSON.stringify({ type, id: item.id }));
|
||
} catch (err) {}
|
||
|
||
// Voeg visuele feedback toe aan het versleepte element
|
||
setTimeout(() => {
|
||
if (e.target) {
|
||
e.target.classList.add('opacity-40', 'scale-[0.98]');
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
function handleTimelineItemDragEnd(e) {
|
||
if (e.target) {
|
||
e.target.classList.remove('opacity-40', 'scale-[0.98]');
|
||
}
|
||
// Verwijder eventuele achtergebleven drag-over styles van slots
|
||
document.querySelectorAll('.timeline-hour-slot').forEach(slot => {
|
||
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
|
||
});
|
||
}
|
||
|
||
function handleTimelineSlotDragOver(e) {
|
||
if (!draggedTimelineItem) return;
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = 'move';
|
||
const slot = e.currentTarget;
|
||
if (slot && !slot.classList.contains('ring-2')) {
|
||
slot.classList.add('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
|
||
}
|
||
}
|
||
|
||
function handleTimelineSlotDragLeave(e) {
|
||
const slot = e.currentTarget;
|
||
if (slot && !slot.contains(e.relatedTarget)) {
|
||
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
|
||
}
|
||
}
|
||
|
||
async function handleTimelineSlotDrop(e, targetHour) {
|
||
e.preventDefault();
|
||
const slot = e.currentTarget;
|
||
if (slot) {
|
||
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
|
||
}
|
||
|
||
if (!draggedTimelineItem) return;
|
||
const { type, item } = draggedTimelineItem;
|
||
draggedTimelineItem = null;
|
||
|
||
// Bepaal de oorspronkelijke tijdsduur van het item in minuten
|
||
let durationMinutes = 30;
|
||
if (item.duration && parseInt(item.duration, 10) > 0) {
|
||
durationMinutes = parseInt(item.duration, 10);
|
||
} else if (item.start && item.end) {
|
||
const sMins = parseTimeToMinutes(item.start);
|
||
const eMins = parseTimeToMinutes(item.end);
|
||
if (sMins !== null && eMins !== null && eMins > sMins) {
|
||
durationMinutes = eMins - sMins;
|
||
}
|
||
}
|
||
|
||
// Nieuwe starttijd is het drop-uurslot (bijv. "14:00")
|
||
const newStart = targetHour;
|
||
const startMins = parseTimeToMinutes(newStart);
|
||
let newEnd = '';
|
||
if (startMins !== null) {
|
||
newEnd = formatMinutesToTime(startMins + durationMinutes);
|
||
}
|
||
|
||
const itemTitle = (type === 'outlook' ? (item.subject || item.title) : item.title) || 'Item';
|
||
const planDate = item.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||
|
||
if (typeof showWidgetToast === 'function') {
|
||
showWidgetToast(`Verplaatsen naar <strong>${newStart} - ${newEnd}</strong>...`, 'info');
|
||
}
|
||
|
||
try {
|
||
if (type === 'outlook') {
|
||
const res = await fetch('/api/planner.php?action=update_outlook_event', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
event_id: item.id,
|
||
title: itemTitle,
|
||
description: item.description || '',
|
||
time_block_start: newStart,
|
||
time_block_end: newEnd,
|
||
date: planDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
if (typeof showWidgetToast === 'function') {
|
||
showWidgetToast(`Outlook afspraak <strong>${escapeHtml(itemTitle)}</strong> verplaatst naar ${newStart} - ${newEnd}`, 'success');
|
||
}
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
} else {
|
||
alert(data.error || 'Kon afspraak niet verplaatsen in Outlook');
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
}
|
||
} else {
|
||
// Reguliere Fociz taak
|
||
const res = await fetch('/api/planner.php?action=update_task', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
task_id: parseInt(item.id, 10),
|
||
title: itemTitle,
|
||
description: item.description || '',
|
||
priority: item.priority || 'P2',
|
||
time_block_start: newStart,
|
||
time_block_end: newEnd,
|
||
duration_minutes: durationMinutes,
|
||
date: planDate
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
if (typeof showWidgetToast === 'function') {
|
||
showWidgetToast(`Taak <strong>${escapeHtml(itemTitle)}</strong> verplaatst naar ${newStart} - ${newEnd}`, 'success');
|
||
}
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
} else {
|
||
alert(data.error || 'Kon taak niet verplaatsen');
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Drag and drop fout:', err);
|
||
alert('Netwerkfout bij verplaatsen van item');
|
||
if (typeof switchPlannerDate === 'function') {
|
||
switchPlannerDate(planDate, null, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 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)}`, {
|
||
headers: isManual ? {} : { 'X-Background-Poll': '1' }
|
||
});
|
||
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();
|
||
initPlannerWidgets();
|
||
if (document.getElementById('plannerJiraListContainer')) {
|
||
filterPlannerJira();
|
||
}
|
||
|
||
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'; ?>
|