Dev #16
+145
-10
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
$user = require_auth();
|
||||
$db = get_db_connection();
|
||||
|
||||
@@ -36,8 +37,11 @@ try {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$title = trim($data['title'] ?? '');
|
||||
$priority = in_array($data['priority'] ?? '', ['P1', 'P2', 'P3']) ? $data['priority'] : 'P2';
|
||||
$timeBlockStart = $data['time_block_start'] ?? null;
|
||||
$date = $data['date'] ?? $today;
|
||||
$timeBlockStart = !empty($data['time_block_start']) ? trim($data['time_block_start']) : null;
|
||||
$durationMinutes = (int)($data['duration_minutes'] ?? 30);
|
||||
if ($durationMinutes <= 0) $durationMinutes = 30;
|
||||
$date = !empty($data['date']) ? trim($data['date']) : $today;
|
||||
$syncOutlook = !empty($data['sync_to_outlook']);
|
||||
|
||||
if (empty($title)) {
|
||||
echo json_encode(['error' => 'Titel is verplicht']);
|
||||
@@ -54,14 +58,44 @@ try {
|
||||
$planId = $db->lastInsertId();
|
||||
|
||||
$source = !empty($data['source']) ? substr(trim($data['source']), 0, 50) : 'FOCIZ';
|
||||
$externalId = !empty($data['external_id']) ? substr(trim($data['external_id']), 0, 100) : null;
|
||||
$externalId = !empty($data['external_id']) ? substr(trim($data['external_id']), 0, 255) : null;
|
||||
$externalUrl = !empty($data['external_url']) ? trim($data['external_url']) : null;
|
||||
$timeBlockEnd = null;
|
||||
|
||||
// Als er een tijdblok is en de gebruiker heeft synchronisatie naar Outlook gekozen (of source is OUTLOOK)
|
||||
if (($syncOutlook || $source === 'OUTLOOK') && !empty($timeBlockStart) && empty($externalId)) {
|
||||
$msResult = microsoft_create_event($user['id'], [
|
||||
'subject' => $title,
|
||||
'description' => "Gepland via Fociz Dagplanning ({$priority})",
|
||||
'date' => $date,
|
||||
'start_time' => $timeBlockStart,
|
||||
'duration_minutes' => $durationMinutes,
|
||||
]);
|
||||
|
||||
if (!empty($msResult['success']) && !empty($msResult['event_id'])) {
|
||||
$source = 'OUTLOOK';
|
||||
$externalId = $msResult['event_id'];
|
||||
$externalUrl = $msResult['web_link'] ?? 'https://outlook.office.com';
|
||||
$timeBlockEnd = $msResult['end_time'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// Bereken timeBlockEnd indien nog niet gezet maar wel starttijd bekend
|
||||
if (!empty($timeBlockStart) && empty($timeBlockEnd)) {
|
||||
try {
|
||||
$dtEnd = new DateTime("{$date} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam'));
|
||||
$dtEnd->modify("+{$durationMinutes} minutes");
|
||||
$timeBlockEnd = $dtEnd->format('H:i');
|
||||
} catch (Throwable $e) {
|
||||
// negeer
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status, source, external_id, external_url)
|
||||
VALUES (?, ?, ?, ?, ?, 'TODO', ?, ?, ?)
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, status, source, external_id, external_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'TODO', ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $source, $externalId, $externalUrl]);
|
||||
$stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl]);
|
||||
$taskId = (int)$db->lastInsertId();
|
||||
|
||||
echo json_encode([
|
||||
@@ -71,14 +105,102 @@ try {
|
||||
'title' => $title,
|
||||
'priority' => $priority,
|
||||
'time_block_start' => $timeBlockStart,
|
||||
'time_block_end' => $timeBlockEnd,
|
||||
'duration_minutes' => $durationMinutes,
|
||||
'is_completed' => 0,
|
||||
'status' => 'TODO'
|
||||
]
|
||||
'status' => 'TODO',
|
||||
'source' => $source,
|
||||
'external_id' => $externalId,
|
||||
'external_url' => $externalUrl,
|
||||
],
|
||||
'outlook_synced' => ($source === 'OUTLOOK' && !empty($externalId)),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Taak status toggelen (voltooid / open)
|
||||
// 3. Bestaande taak handmatig naar Microsoft Outlook synchroniseren
|
||||
if ($action === 'sync_task_to_outlook') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$taskId = (int)($data['task_id'] ?? 0);
|
||||
|
||||
if (!$taskId) {
|
||||
echo json_encode(['error' => 'Geen taak-ID opgegeven']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Haal taak inclusief plan datum op
|
||||
$stmtTask = $db->prepare("
|
||||
SELECT t.*, dp.plan_date
|
||||
FROM tasks t
|
||||
LEFT JOIN daily_plans dp ON t.daily_plan_id = dp.id
|
||||
WHERE t.id = ? AND t.user_id = ?
|
||||
");
|
||||
$stmtTask->execute([$taskId, $user['id']]);
|
||||
$task = $stmtTask->fetch();
|
||||
|
||||
if (!$task) {
|
||||
echo json_encode(['error' => 'Taak niet gevonden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Als al gesynchroniseerd
|
||||
if (!empty($task['external_id']) && $task['source'] === 'OUTLOOK') {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'already_synced' => true,
|
||||
'external_id' => $task['external_id'],
|
||||
'external_url' => $task['external_url']
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$planDate = $task['plan_date'] ?: $today;
|
||||
$startTime = !empty($task['time_block_start']) ? $task['time_block_start'] : ($data['time_block_start'] ?? '09:00');
|
||||
$duration = (int)($task['duration_minutes'] ?: 30);
|
||||
|
||||
$msResult = microsoft_create_event($user['id'], [
|
||||
'subject' => $task['title'],
|
||||
'description' => "Gepland via Fociz Dagplanning (" . $task['priority'] . ")",
|
||||
'date' => $planDate,
|
||||
'start_time' => $startTime,
|
||||
'duration_minutes' => $duration,
|
||||
]);
|
||||
|
||||
if (empty($msResult['success']) || empty($msResult['event_id'])) {
|
||||
echo json_encode(['error' => $msResult['error'] ?? 'Kon afspraak niet aanmaken in Microsoft Outlook']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Sla Outlook ID en bron op in de taak
|
||||
$stmtUp = $db->prepare("
|
||||
UPDATE tasks
|
||||
SET source = 'OUTLOOK',
|
||||
external_id = ?,
|
||||
external_url = ?,
|
||||
time_block_start = COALESCE(time_block_start, ?),
|
||||
time_block_end = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
");
|
||||
$stmtUp->execute([
|
||||
$msResult['event_id'],
|
||||
$msResult['web_link'] ?? 'https://outlook.office.com',
|
||||
$msResult['start_time'],
|
||||
$msResult['end_time'],
|
||||
$taskId,
|
||||
$user['id']
|
||||
]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'external_id' => $msResult['event_id'],
|
||||
'external_url' => $msResult['web_link'] ?? 'https://outlook.office.com',
|
||||
'start_time' => $msResult['start_time'],
|
||||
'end_time' => $msResult['end_time'],
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Taak status toggelen (voltooid / open)
|
||||
if ($action === 'toggle_task') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$taskId = (int)($data['task_id'] ?? 0);
|
||||
@@ -95,11 +217,24 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Taak verwijderen
|
||||
// 5. Taak verwijderen (verwijder ook eventueel gekoppelde Outlook afspraak)
|
||||
if ($action === 'delete_task') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$taskId = (int)($data['task_id'] ?? 0);
|
||||
|
||||
// Haal eerst op of deze taak aan Outlook was gekoppeld
|
||||
$stmtCheck = $db->prepare("SELECT external_id, source FROM tasks WHERE id = ? AND user_id = ?");
|
||||
$stmtCheck->execute([$taskId, $user['id']]);
|
||||
$taskToDelete = $stmtCheck->fetch();
|
||||
|
||||
if ($taskToDelete && !empty($taskToDelete['external_id']) && $taskToDelete['source'] === 'OUTLOOK') {
|
||||
try {
|
||||
microsoft_delete_event($user['id'], $taskToDelete['external_id']);
|
||||
} catch (Throwable $t) {
|
||||
error_log("Fout bij verwijderen Outlook afspraak voor taak {$taskId}: " . $t->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM tasks WHERE id = ? AND user_id = ?");
|
||||
$stmt->execute([$taskId, $user['id']]);
|
||||
|
||||
|
||||
+6
-1
@@ -118,7 +118,12 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
// Controleer of de hoofdtabel 'users' al bestaat
|
||||
$result = $pdo->query("SHOW TABLES LIKE 'users'");
|
||||
if ($result && $result->rowCount() > 0) {
|
||||
// Tabellen bestaan al! Geen actie nodig.
|
||||
// Tabellen bestaan al. Voer eventuele niet-destructieve kolom-upgrades uit indien nodig
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE tasks MODIFY external_id VARCHAR(255) NULL");
|
||||
} catch (Throwable $t) {
|
||||
// Kolom bestaat wellicht al of heeft al de juiste lengte
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS `tasks` (
|
||||
`duration_minutes` INT NULL DEFAULT 30,
|
||||
`is_completed` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`source` VARCHAR(50) NOT NULL DEFAULT 'FOCIZ',
|
||||
`external_id` VARCHAR(100) NULL,
|
||||
`external_id` VARCHAR(255) NULL,
|
||||
`external_url` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
@@ -269,6 +269,10 @@ function microsoft_get_valid_token(int $userId): ?string {
|
||||
}
|
||||
|
||||
error_log("Microsoft token refresh mislukt voor user {$userId}: " . ($response['error'] ?? json_encode($response['json'])));
|
||||
// Fallback: indien het huidige token nog aanwezig is, probeer dit toch te gebruiken
|
||||
if (!empty($integration['access_token'])) {
|
||||
return $integration['access_token'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -283,9 +287,21 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array
|
||||
return [];
|
||||
}
|
||||
|
||||
// Venster van 00:00:00 tot 23:59:59 lokale tijd
|
||||
$startIso = "{$targetDate}T00:00:00";
|
||||
$endIso = "{$targetDate}T23:59:59";
|
||||
// Bereken het dagvenster in Europe/Amsterdam en converteer naar UTC ISO 8601 (eindigend op 'Z')
|
||||
// Microsoft Graph vereist strikt een ISO 8601 formaat met tijdzone of 'Z' (bijv. 2026-09-09T00:00:00Z)
|
||||
try {
|
||||
$tzLocal = new DateTimeZone('Europe/Amsterdam');
|
||||
$tzUtc = new DateTimeZone('UTC');
|
||||
|
||||
$dtStart = new DateTime("{$targetDate} 00:00:00", $tzLocal);
|
||||
$dtEnd = new DateTime("{$targetDate} 23:59:59", $tzLocal);
|
||||
|
||||
$startIso = $dtStart->setTimezone($tzUtc)->format('Y-m-d\TH:i:s\Z');
|
||||
$endIso = $dtEnd->setTimezone($tzUtc)->format('Y-m-d\TH:i:s\Z');
|
||||
} catch (Throwable $e) {
|
||||
$startIso = "{$targetDate}T00:00:00Z";
|
||||
$endIso = "{$targetDate}T23:59:59Z";
|
||||
}
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/calendarView?' . http_build_query([
|
||||
'startDateTime' => $startIso,
|
||||
@@ -305,7 +321,12 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array
|
||||
'timeout' => 12,
|
||||
]);
|
||||
|
||||
if ($response['status'] !== 200 || empty($response['json']['value'])) {
|
||||
if ($response['status'] !== 200) {
|
||||
error_log("microsoft_fetch_today_events error (HTTP {$response['status']}): " . ($response['error'] ?? json_encode($response['json'])));
|
||||
return [];
|
||||
}
|
||||
|
||||
if (empty($response['json']['value'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -321,14 +342,23 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array
|
||||
$rawStart = $e['start']['dateTime'] ?? '';
|
||||
$rawEnd = $e['end']['dateTime'] ?? '';
|
||||
|
||||
// Formatteer naar H:i (bijv. 09:30)
|
||||
// Formatteer naar H:i (bijv. 09:30). Omdat we 'Prefer: outlook.timezone="Europe/Amsterdam"'
|
||||
// meesturen, levert Graph de tijden al aan in de Nederlandse tijdzone.
|
||||
$startTime = '';
|
||||
$endTime = '';
|
||||
if (!empty($rawStart)) {
|
||||
$startTime = date('H:i', strtotime($rawStart));
|
||||
if (preg_match('/T(\d{2}:\d{2})/', $rawStart, $m)) {
|
||||
$startTime = $m[1];
|
||||
} else {
|
||||
$startTime = date('H:i', strtotime($rawStart));
|
||||
}
|
||||
}
|
||||
if (!empty($rawEnd)) {
|
||||
$endTime = date('H:i', strtotime($rawEnd));
|
||||
if (preg_match('/T(\d{2}:\d{2})/', $rawEnd, $m)) {
|
||||
$endTime = $m[1];
|
||||
} else {
|
||||
$endTime = date('H:i', strtotime($rawEnd));
|
||||
}
|
||||
}
|
||||
|
||||
// Teams of meeting URL detecteren
|
||||
@@ -353,6 +383,7 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array
|
||||
'end_raw' => $rawEnd,
|
||||
'location' => $locationName ?: 'Geen locatie',
|
||||
'meeting_url' => $meetingUrl,
|
||||
'web_link' => $e['webLink'] ?? '',
|
||||
'is_all_day' => !empty($e['isAllDay']),
|
||||
'source' => 'OUTLOOK_LIVE',
|
||||
];
|
||||
@@ -361,6 +392,127 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maakt een nieuwe afspraak aan in de Microsoft Outlook agenda via Graph API
|
||||
*
|
||||
* @param int $userId
|
||||
* @param array $eventData [
|
||||
* 'subject' => string,
|
||||
* 'description' => ?string,
|
||||
* 'date' => ?string (Y-m-d),
|
||||
* 'start_time' => ?string (H:i),
|
||||
* 'end_time' => ?string (H:i),
|
||||
* 'duration_minutes' => ?int,
|
||||
* ]
|
||||
* @return array ['success' => bool, 'event_id' => ?string, 'web_link' => ?string, 'error' => ?string]
|
||||
*/
|
||||
function microsoft_create_event(int $userId, array $eventData): array {
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) {
|
||||
return ['success' => false, 'error' => 'Geen geldige Outlook 365 verbinding gevonden'];
|
||||
}
|
||||
|
||||
$subject = trim($eventData['subject'] ?? 'Fociz Dagplanning Taak');
|
||||
$description = trim($eventData['description'] ?? 'Gepland via Fociz Dagplanning & Structuur');
|
||||
$date = !empty($eventData['date']) ? trim($eventData['date']) : date('Y-m-d');
|
||||
$startTime = !empty($eventData['start_time']) ? trim($eventData['start_time']) : '09:00';
|
||||
|
||||
// Als tijd alleen een uur is (bijv. "9" of "09"), converteer naar "09:00"
|
||||
if (preg_match('/^(\d{1,2})$/', $startTime, $m)) {
|
||||
$startTime = sprintf('%02d:00', (int)$m[1]);
|
||||
}
|
||||
|
||||
// Bepaal de eindtijd
|
||||
$endTime = !empty($eventData['end_time']) ? trim($eventData['end_time']) : '';
|
||||
$duration = (int)($eventData['duration_minutes'] ?? 30);
|
||||
if ($duration <= 0) $duration = 30;
|
||||
|
||||
if (empty($endTime)) {
|
||||
try {
|
||||
$dt = new DateTime("{$date} {$startTime}:00", new DateTimeZone('Europe/Amsterdam'));
|
||||
$dt->modify("+{$duration} minutes");
|
||||
$endTime = $dt->format('H:i');
|
||||
} catch (Throwable $e) {
|
||||
$endTime = '10:00';
|
||||
}
|
||||
}
|
||||
|
||||
$startDateTimeStr = "{$date}T{$startTime}:00";
|
||||
$endDateTimeStr = "{$date}T{$endTime}:00";
|
||||
|
||||
$payload = [
|
||||
'subject' => $subject,
|
||||
'body' => [
|
||||
'contentType' => 'HTML',
|
||||
'content' => nl2br(htmlspecialchars($description)) . '<br><br><small style="color: #64748b;">Aangemaakt via <a href="https://dev.fociz.nl/planner.php">Fociz Dagplanning</a></small>',
|
||||
],
|
||||
'start' => [
|
||||
'dateTime' => $startDateTimeStr,
|
||||
'timeZone' => 'Europe/Amsterdam',
|
||||
],
|
||||
'end' => [
|
||||
'dateTime' => $endDateTimeStr,
|
||||
'timeZone' => 'Europe/Amsterdam',
|
||||
],
|
||||
'showAs' => 'busy',
|
||||
'isReminderOn' => true,
|
||||
'reminderMinutesBeforeStart' => 15,
|
||||
];
|
||||
|
||||
$response = fociz_http_request(MS_GRAPH_BASE_URL . '/me/events', [
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'body' => $payload,
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($response['status'] === 201 && is_array($response['json'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'event_id' => $response['json']['id'] ?? null,
|
||||
'web_link' => $response['json']['webLink'] ?? null,
|
||||
'start_time'=> $startTime,
|
||||
'end_time' => $endTime,
|
||||
];
|
||||
}
|
||||
|
||||
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "Fout bij aanmaken Outlook afspraak (HTTP {$response['status']})");
|
||||
error_log("microsoft_create_event fout: {$errMsg}");
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errMsg,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verwijdert een afspraak uit Microsoft Outlook via Graph API
|
||||
*/
|
||||
function microsoft_delete_event(int $userId, string $eventId): bool {
|
||||
if (empty($eventId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/events/' . urlencode($eventId);
|
||||
$response = fociz_http_request($url, [
|
||||
'method' => 'DELETE',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
],
|
||||
'timeout' => 12,
|
||||
]);
|
||||
|
||||
return in_array($response['status'], [200, 204, 404]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ontkoppelt Microsoft integratie voor een gebruiker
|
||||
* Wist uitsluitend de persoonlijke tokens; behoudt de centrale Azure App gegevens indien aanwezig.
|
||||
|
||||
+162
-36
@@ -50,15 +50,13 @@ $isLiveOutlook = false;
|
||||
|
||||
if ($isOutlookConnected) {
|
||||
$outlookEvents = microsoft_fetch_today_events($userId, $today);
|
||||
$isLiveOutlook = !empty($outlookEvents) || true; // Live koppeling is actief
|
||||
}
|
||||
|
||||
// Fallback naar sandbox data indien Outlook niet live gekoppeld is
|
||||
if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
$isLiveOutlook = true;
|
||||
} else {
|
||||
// Alleen tonen indien de gebruiker Microsoft Outlook nog niet gekoppeld heeft
|
||||
$outlookEvents = [
|
||||
[
|
||||
'id' => 'evt-1',
|
||||
'subject' => 'Daily Team Standup & Focus Sync',
|
||||
'subject' => 'Voorbeeld: Team Standup & Sync',
|
||||
'start' => '09:00',
|
||||
'end' => '09:30',
|
||||
'location' => 'Microsoft Teams',
|
||||
@@ -67,7 +65,7 @@ if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
],
|
||||
[
|
||||
'id' => 'evt-2',
|
||||
'subject' => 'Sprint & Roadmap Review met Stakeholders',
|
||||
'subject' => 'Voorbeeld: Roadmap Review met Stakeholders',
|
||||
'start' => '11:00',
|
||||
'end' => '12:00',
|
||||
'location' => 'Vergaderruimte A & Teams',
|
||||
@@ -76,7 +74,7 @@ if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
],
|
||||
[
|
||||
'id' => 'evt-3',
|
||||
'subject' => 'Architectuur & MariaDB Database Evaluatie',
|
||||
'subject' => 'Voorbeeld: Architectuur & DB Evaluatie',
|
||||
'start' => '14:30',
|
||||
'end' => '15:30',
|
||||
'location' => 'Online Teams Call',
|
||||
@@ -91,16 +89,38 @@ $jiraData = jira_get_user_issues($userId);
|
||||
$jiraIssues = $jiraData['issues'] ?? [];
|
||||
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
|
||||
|
||||
// Filter Outlook events die afkomstig zijn van Fociz taken om duplicaten op de tijdlijn te voorkomen
|
||||
$syncedTaskExternalIds = [];
|
||||
foreach ($tasks as $t) {
|
||||
if (!empty($t['external_id'])) {
|
||||
$syncedTaskExternalIds[$t['external_id']] = true;
|
||||
}
|
||||
}
|
||||
$displayedOutlookEvents = [];
|
||||
foreach ($outlookEvents as $evt) {
|
||||
if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) {
|
||||
continue;
|
||||
}
|
||||
$displayedOutlookEvents[] = $evt;
|
||||
}
|
||||
|
||||
// 5. Tijdlijn uren bepalen
|
||||
$startHour = 8;
|
||||
$endHour = 18;
|
||||
foreach ($outlookEvents as $e) {
|
||||
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 <= 22) $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 <= 22) $endHour = $h + 1;
|
||||
}
|
||||
}
|
||||
$hours = [];
|
||||
for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
$hours[] = sprintf('%02d:00', $h);
|
||||
@@ -254,11 +274,11 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Taaktitel</label>
|
||||
<input type="text" id="newTaskTitle" placeholder="Wat moet er vandaag gedaan worden?" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-3 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-emerald-500">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Prioriteit</label>
|
||||
<select id="newTaskPriority" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-slate-900 dark:text-white text-xs focus:outline-none">
|
||||
<option value="P1">P1 - Brandend / Hoogste Focus</option>
|
||||
<option value="P1">P1 - Brandend / Focus</option>
|
||||
<option value="P2" selected>P2 - Belangrijk (Standaard)</option>
|
||||
<option value="P3">P3 - Regulier / Lage druk</option>
|
||||
</select>
|
||||
@@ -267,7 +287,31 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Tijdblok (Starttijd)</label>
|
||||
<input type="text" id="newTaskTime" placeholder="Bijv. 09:30" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-slate-900 dark:text-white text-xs font-mono focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Tijdsduur</label>
|
||||
<select id="newTaskDuration" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-slate-900 dark:text-white text-xs focus:outline-none">
|
||||
<option value="15">15 minuten</option>
|
||||
<option value="30" selected>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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($isOutlookConnected): ?>
|
||||
<div class="p-2.5 rounded-xl bg-blue-50/70 dark:bg-blue-950/40 border border-blue-200/70 dark:border-blue-800/40">
|
||||
<label class="flex items-center space-x-2 text-xs text-blue-900 dark:text-blue-200 cursor-pointer select-none">
|
||||
<input type="checkbox" id="newTaskSyncOutlook" checked class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 border-slate-300 dark:border-slate-700">
|
||||
<span class="flex items-center space-x-1.5 font-medium">
|
||||
<i data-lucide="calendar" class="w-3.5 h-3.5 text-blue-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-4 py-1.5 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold rounded-lg text-xs transition cursor-pointer">Opslaan</button>
|
||||
<button onclick="toggleAddTaskForm()" class="px-3 py-1.5 bg-slate-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-400 rounded-lg text-xs transition cursor-pointer">Annuleren</button>
|
||||
@@ -296,6 +340,11 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<span class="px-1.5 py-0.2 rounded bg-sky-500/10 border border-sky-500/20 text-sky-600 dark:text-sky-400 font-mono text-[9px] font-bold">
|
||||
JIRA
|
||||
</span>
|
||||
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
|
||||
<span class="px-1.5 py-0.2 rounded bg-blue-500/10 border border-blue-500/20 text-blue-600 dark:text-blue-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-medium truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
|
||||
<?= htmlspecialchars($task['title']) ?>
|
||||
@@ -306,12 +355,12 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?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']) ?></span>
|
||||
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($task['external_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] text-sky-600 dark:text-sky-400 hover:underline flex items-center space-x-0.5">
|
||||
<span>Open in Jira</span>
|
||||
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-blue-600 dark:text-blue-400' : 'text-sky-600 dark:text-sky-400' ?> hover:underline flex items-center space-x-0.5">
|
||||
<span><?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'Open in Outlook' : 'Open in Jira' ?></span>
|
||||
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
@@ -324,6 +373,12 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?= htmlspecialchars($task['priority']) ?>
|
||||
</span>
|
||||
|
||||
<?php if ($isOutlookConnected && empty($task['external_id']) && !$task['is_completed']): ?>
|
||||
<button onclick="syncTaskToOutlook(<?= $task['id'] ?>, this)" title="Direct synchroniseren naar Outlook Agenda" class="p-1.5 rounded-lg text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-500/10 transition">
|
||||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$task['is_completed']): ?>
|
||||
<button onclick="selectFocusTask('<?= htmlspecialchars(addslashes($task['title'])) ?>')" title="Koppel aan Focus Timer" class="p-1.5 rounded-lg text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-500/10 transition">
|
||||
<i data-lucide="flame" class="w-3.5 h-3.5"></i>
|
||||
@@ -431,8 +486,8 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?php foreach ($hours as $hour):
|
||||
$hourInt = (int)explode(':', $hour)[0];
|
||||
|
||||
// Zoek Outlook meetings die binnen dit uur vallen
|
||||
$slotEvents = array_filter($outlookEvents, function($e) use ($hourInt) {
|
||||
// Zoek Outlook meetings die binnen dit uur vallen (gefilterd op Fociz duplicaten)
|
||||
$slotEvents = array_filter($displayedOutlookEvents, function($e) use ($hourInt) {
|
||||
if (empty($e['start'])) return false;
|
||||
return (int)explode(':', $e['start'])[0] === $hourInt;
|
||||
});
|
||||
@@ -459,12 +514,19 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?= htmlspecialchars($evt['subject']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if (!empty($evt['meeting_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-white bg-blue-600 hover:bg-blue-700 px-2 py-0.5 rounded-lg flex-shrink-0 transition">
|
||||
<i data-lucide="video" class="w-3 h-3"></i>
|
||||
<span>Deelnemen</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
<?php if (!empty($evt['meeting_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-white bg-blue-600 hover:bg-blue-700 px-2 py-0.5 rounded-lg flex-shrink-0 transition">
|
||||
<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="flex items-center space-x-1 text-[10px] text-blue-600 dark:text-blue-300 hover:underline px-1.5 py-0.5 transition">
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
<span>Outlook</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -473,13 +535,26 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<div class="flex items-center justify-between p-2 rounded-xl border text-xs <?= $t['is_completed'] ? 'bg-slate-100 dark:bg-slate-900/40 border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-800/40 text-emerald-900 dark:text-emerald-100' ?>">
|
||||
<div class="flex items-center space-x-2 truncate mr-2">
|
||||
<span class="px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 font-mono text-[10px] font-bold">
|
||||
<?= htmlspecialchars($t['time_block_start']) ?>
|
||||
<?= htmlspecialchars($t['time_block_start']) ?><?= !empty($t['time_block_end']) ? ' - ' . htmlspecialchars($t['time_block_end']) : '' ?>
|
||||
</span>
|
||||
<span class="font-semibold truncate"><?= htmlspecialchars($t['title']) ?></span>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-700 dark:text-emerald-400">
|
||||
<?= $t['priority'] ?>
|
||||
</span>
|
||||
<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-bold px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-700 dark:text-blue-400 border border-blue-500/20 flex items-center space-x-1" title="In Outlook agenda">
|
||||
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
|
||||
<span class="hidden sm:inline">In Outlook</span>
|
||||
</span>
|
||||
<?php elseif ($isOutlookConnected && !$t['is_completed']): ?>
|
||||
<button onclick="syncTaskToOutlook(<?= $t['id'] ?>, this)" title="Synchroniseer naar Outlook Agenda" class="text-[9px] font-bold px-1.5 py-0.5 rounded bg-blue-500/10 hover:bg-blue-500/20 text-blue-700 dark:text-blue-300 border border-blue-500/20 flex items-center space-x-1 transition cursor-pointer">
|
||||
<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-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-700 dark:text-emerald-400">
|
||||
<?= $t['priority'] ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -597,24 +672,75 @@ function toggleAddTaskForm() {
|
||||
|
||||
function quickSchedule(hour) {
|
||||
toggleAddTaskForm();
|
||||
document.getElementById('newTaskTime').value = hour;
|
||||
const timeInput = document.getElementById('newTaskTime');
|
||||
if (timeInput) timeInput.value = hour;
|
||||
const outlookCheckbox = document.getElementById('newTaskSyncOutlook');
|
||||
if (outlookCheckbox) outlookCheckbox.checked = true;
|
||||
}
|
||||
|
||||
async function submitNewTask() {
|
||||
const title = document.getElementById('newTaskTitle').value.trim();
|
||||
const priority = document.getElementById('newTaskPriority').value;
|
||||
const time = document.getElementById('newTaskTime').value.trim();
|
||||
const duration = document.getElementById('newTaskDuration') ? parseInt(document.getElementById('newTaskDuration').value) : 30;
|
||||
const syncOutlook = document.getElementById('newTaskSyncOutlook') ? document.getElementById('newTaskSyncOutlook').checked : false;
|
||||
|
||||
if (!title) return;
|
||||
|
||||
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, date: '<?= $today ?>' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
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,
|
||||
duration_minutes: duration,
|
||||
sync_to_outlook: syncOutlook,
|
||||
date: '<?= $today ?>'
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Fout bij opslaan van de taak');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij opslaan van de taak');
|
||||
}
|
||||
}
|
||||
|
||||
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-emerald-500"></i>';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 600);
|
||||
} else {
|
||||
alert(data.error || 'Kon taak niet synchroniseren naar Outlook');
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij synchroniseren naar Outlook');
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +788,7 @@ async function toggleTask(taskId, isCompleted) {
|
||||
}
|
||||
|
||||
async function deleteTask(taskId) {
|
||||
if (!confirm('Taak verwijderen?')) return;
|
||||
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'},
|
||||
|
||||
Reference in New Issue
Block a user