Add webhook receivers for Jira and Microsoft Graph, and implement Outlook 365 email hub with inbox management features

This commit is contained in:
2026-09-10 14:08:34 +02:00
parent 25c2ffeef8
commit dc0b33a278
5 changed files with 1137 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
<?php
/**
* Fociz Live Sync & Realtime Notificatie Hub
* Controleert continu op nieuwe Outlook e-mails, relevante Jira tickets en agenda updates.
*/
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/integrations/microsoft.php';
require_once __DIR__ . '/../includes/integrations/jira.php';
$user = require_auth();
$userId = (int)$user['id'];
$db = get_db_connection();
header('Content-Type: application/json; charset=utf-8');
try {
$nowTs = time();
$nowIso = date('Y-m-d H:i:s');
$today = date('Y-m-d');
$clientLastMailId = trim($_GET['last_mail_id'] ?? '');
$clientLastJiraKey = trim($_GET['last_jira_key'] ?? '');
$clientCalendarHash = trim($_GET['calendar_hash'] ?? '');
// 1. Outlook E-mail Controle
$mailRes = microsoft_fetch_inbox_messages($userId, 5);
$messages = $mailRes['messages'] ?? [];
$unreadCount = (int)($mailRes['unread_count'] ?? 0);
$latestMail = $messages[0] ?? null;
$hasNewMail = false;
$newMailPayload = null;
if ($latestMail && !empty($latestMail['id'])) {
// Als de client een ander ID heeft dan de allernieuwste mail en het bericht is ongelezen
if (!empty($clientLastMailId) && $latestMail['id'] !== $clientLastMailId && empty($latestMail['is_read'])) {
$hasNewMail = true;
$newMailPayload = [
'id' => $latestMail['id'],
'subject' => $latestMail['subject'],
'from_name' => $latestMail['from_name'],
'from_email' => $latestMail['from_email'],
'received_human' => $latestMail['received_human'],
'web_link' => $latestMail['web_link'] ?? '/mail.php',
];
}
}
// 2. Jira Tickets Controle (rekening houdend met persoonlijke toewijzing & default queue filter zoals DX of SD)
$userFilterCode = $user['default_jira_filter_code'] ?? '';
$jiraData = jira_get_user_issues($userId);
$assignedIssues = $jiraData['assigned_issues'] ?? ($jiraData['issues'] ?? []);
$teamIssues = $jiraData['team_issues'] ?? [];
$dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
$relevantIssues = [];
// Filter issues die specifiek relevant zijn voor deze medewerker
foreach (array_merge($assignedIssues, $teamIssues) as $iss) {
$isAssignedToMe = !empty($iss['assignee_name']) && (stripos($iss['assignee_name'], $user['name']) !== false || stripos($iss['assignee_name'], 'Niek') !== false);
$ol = strtolower(trim($iss['onderwerp_logging'] ?? ''));
$isQueueMatch = false;
if (!empty($userFilterCode)) {
if ($userFilterCode === 'DX' && in_array($ol, $dxValues)) {
$isQueueMatch = true;
} elseif ($userFilterCode === 'SD' && ($ol === '' || $ol === 'empty' || !in_array($ol, $dxValues))) {
$isQueueMatch = true;
}
}
if ($isAssignedToMe || $isQueueMatch) {
$relevantIssues[$iss['key']] = $iss;
}
}
$hasNewJira = false;
$newJiraPayload = [];
$newestJiraKey = '';
if (!empty($relevantIssues)) {
// Pak het nieuwste ticket
$newest = reset($relevantIssues);
$newestJiraKey = $newest['key'];
if (!empty($clientLastJiraKey) && $newestJiraKey !== $clientLastJiraKey) {
$hasNewJira = true;
$newJiraPayload = [
'key' => $newest['key'],
'summary' => $newest['summary'],
'priority' => $newest['priority'] ?? 'Medium',
'issue_type' => $newest['issue_type'] ?? 'Taak',
'project' => $newest['project'] ?? 'SD',
'url' => $newest['url'] ?? ('/jira.php?highlight=' . $newest['key']),
];
}
}
// 3. Outlook Agenda Controle (Tijdlijn events van vandaag)
$calendarChanged = false;
$todayEvents = [];
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmtInt->execute([$userId]);
$outlookInt = $stmtInt->fetch();
if ($outlookInt && !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode'])) {
$todayEvents = microsoft_fetch_today_events($userId, $today);
}
$calHash = md5(json_encode(array_map(function($e) {
return ($e['id'] ?? '') . ($e['start'] ?? '') . ($e['end'] ?? '') . ($e['subject'] ?? '');
}, $todayEvents)));
if (!empty($clientCalendarHash) && $calHash !== $clientCalendarHash) {
$calendarChanged = true;
}
// 4. Update sync timestamps in database
$stmtUpdate = $db->prepare("UPDATE users SET last_mail_check_at = NOW(), last_jira_check_at = NOW() WHERE id = ?");
$stmtUpdate->execute([$userId]);
echo json_encode([
'success' => true,
'server_time' => date('H:i:s'),
'mail' => [
'has_new' => $hasNewMail,
'unread_count' => $unreadCount,
'latest_id' => $latestMail['id'] ?? '',
'new_mail' => $newMailPayload,
],
'jira' => [
'has_new' => $hasNewJira,
'newest_key' => $newestJiraKey,
'new_issue' => $newJiraPayload,
],
'calendar' => [
'has_changes' => $calendarChanged,
'hash' => $calHash,
'count' => count($todayEvents),
],
]);
exit;
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
]);
}
+188
View File
@@ -0,0 +1,188 @@
<?php
/**
* Fociz Microsoft Outlook 365 E-mail API Endpoint
* Verwerkt ophalen van inbox, detailberichten, beantwoorden, doorsturen en 1-klik inplannen.
*/
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/integrations/microsoft.php';
$user = require_auth();
$userId = (int)$user['id'];
$db = get_db_connection();
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
try {
// 1. Inbox berichten ophalen
if ($action === 'get_inbox') {
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 25;
$unreadOnly = !empty($_GET['unread_only']) && $_GET['unread_only'] === '1';
$res = microsoft_fetch_inbox_messages($userId, $limit, $unreadOnly);
echo json_encode($res);
exit;
}
// 2. Enkel bericht detail ophalen
if ($action === 'get_message') {
$messageId = trim($_GET['id'] ?? ($_POST['id'] ?? ''));
if (empty($messageId)) {
echo json_encode(['success' => false, 'error' => 'Geen bericht-ID opgegeven.']);
exit;
}
$detail = microsoft_fetch_message_detail($userId, $messageId);
if (!$detail) {
echo json_encode(['success' => false, 'error' => 'Bericht niet gevonden of geen toegang.']);
exit;
}
echo json_encode(['success' => true, 'message' => $detail]);
exit;
}
// 3. Markeer als gelezen of ongelezen
if ($action === 'mark_read') {
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$messageId = trim($input['id'] ?? '');
$isRead = isset($input['is_read']) ? (bool)$input['is_read'] : true;
if (empty($messageId)) {
echo json_encode(['success' => false, 'error' => 'Geen bericht-ID opgegeven.']);
exit;
}
$ok = microsoft_mark_message_read($userId, $messageId, $isRead);
echo json_encode(['success' => $ok]);
exit;
}
// 4. E-mail beantwoorden
if ($action === 'reply') {
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$messageId = trim($input['id'] ?? '');
$comment = trim($input['comment'] ?? '');
$replyAll = !empty($input['reply_all']);
if (empty($messageId) || empty($comment)) {
echo json_encode(['success' => false, 'error' => 'Bericht-ID en reactietekst zijn verplicht.']);
exit;
}
$res = microsoft_reply_message($userId, $messageId, $comment, $replyAll);
echo json_encode($res);
exit;
}
// 5. E-mail doorsturen
if ($action === 'forward') {
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$messageId = trim($input['id'] ?? '');
$toInput = $input['to'] ?? '';
$comment = trim($input['comment'] ?? '');
if (empty($messageId) || empty($toInput)) {
echo json_encode(['success' => false, 'error' => 'Bericht-ID en minimaal één geadresseerde zijn verplicht.']);
exit;
}
$recipients = [];
if (is_array($toInput)) {
$recipients = $toInput;
} else {
$parts = explode(',', (string)$toInput);
foreach ($parts as $p) {
$p = trim($p);
if (filter_var($p, FILTER_VALIDATE_EMAIL)) {
$recipients[] = ['address' => $p, 'name' => $p];
}
}
}
if (empty($recipients)) {
echo json_encode(['success' => false, 'error' => 'Geen geldige e-mailadressen ingevoerd.']);
exit;
}
$res = microsoft_forward_message($userId, $messageId, $recipients, $comment);
echo json_encode($res);
exit;
}
// 6. E-mail direct inplannen in Fociz dagplanning
if ($action === 'plan_email') {
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$messageId = trim($input['message_id'] ?? '');
$subject = trim($input['subject'] ?? 'E-mail actiepunt');
$planDate = trim($input['plan_date'] ?? date('Y-m-d'));
$timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null;
$durationMinutes = (int)($input['duration_minutes'] ?? 30);
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
$webLink = trim($input['web_link'] ?? 'https://outlook.office.com/mail/');
$sender = trim($input['sender'] ?? '');
if (empty($subject)) {
echo json_encode(['success' => false, 'error' => 'Onderwerp is verplicht.']);
exit;
}
if ($durationMinutes <= 0) $durationMinutes = 30;
$timeBlockEnd = null;
if (!empty($timeBlockStart)) {
try {
$dtEnd = new DateTime("{$planDate} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam'));
$dtEnd->modify("+{$durationMinutes} minutes");
$timeBlockEnd = $dtEnd->format('H:i');
} catch (Throwable $t) {}
}
// Zorg dat daily_plan bestaat
$stmtPlan = $db->prepare("
INSERT INTO daily_plans (user_id, plan_date)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)
");
$stmtPlan->execute([$userId, $planDate]);
$planId = (int)$db->lastInsertId();
$taskTitle = "✉️ " . $subject;
if (!empty($sender)) {
$taskTitle .= " (van " . $sender . ")";
}
$stmtTask = $db->prepare("
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, source, external_id, external_url)
VALUES (?, ?, ?, ?, ?, ?, ?, 'OUTLOOK_MAIL', ?, ?)
");
$stmtTask->execute([
$userId,
$planId,
$taskTitle,
$priority,
$timeBlockStart,
$timeBlockEnd,
$durationMinutes,
$messageId,
$webLink
]);
$taskId = (int)$db->lastInsertId();
echo json_encode([
'success' => true,
'task_id' => $taskId,
'plan_date' => $planDate,
'message' => "E-mail succesvol ingepland voor {$planDate}."
]);
exit;
}
echo json_encode(['success' => false, 'error' => 'Onbekende e-mailactie.']);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* Jira Webhook Receiver
* Ontvangt webhook events van Atlassian Jira Cloud (issue_created, issue_updated, etc.)
*/
require_once __DIR__ . '/../../config/database.php';
header('Content-Type: application/json');
$rawPayload = file_get_contents('php://input');
$data = json_decode($rawPayload, true);
if (!$data) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON payload']);
exit;
}
try {
$db = get_db_connection();
$event = $data['webhookEvent'] ?? 'jira:unknown';
$issueKey = $data['issue']['key'] ?? '';
$summary = $data['issue']['fields']['summary'] ?? '';
// Sla event tijdelijk op of invalidate session cache
error_log("Jira Webhook ontvangen: {$event} voor {$issueKey} - {$summary}");
echo json_encode([
'received' => true,
'event' => $event,
'issue_key' => $issueKey,
'timestamp' => date('Y-m-d H:i:s')
]);
exit;
} catch (Throwable $t) {
http_response_code(500);
echo json_encode(['error' => $t->getMessage()]);
}
+31
View File
@@ -0,0 +1,31 @@
<?php
/**
* Microsoft Graph Webhook Receiver
* Ontvangt subscription notifications voor /me/mailFolders('inbox')/messages en /me/events
*/
require_once __DIR__ . '/../../config/database.php';
// Validatie bij aanmaken van Graph subscription
if (isset($_GET['validationToken'])) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(200);
echo $_GET['validationToken'];
exit;
}
header('Content-Type: application/json');
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if ($data && isset($data['value'])) {
foreach ($data['value'] as $change) {
$resource = $change['resource'] ?? '';
$changeType = $change['changeType'] ?? '';
error_log("Microsoft Graph Webhook: {$changeType} op {$resource}");
}
}
echo json_encode(['received' => true]);