Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15caa5f341 | |||
| fb38c42fe9 |
@@ -32,6 +32,40 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1b. Widget layout opslaan
|
||||
if ($action === 'save_widget_layout') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$layout = $data['layout'] ?? null;
|
||||
|
||||
if (!is_array($layout)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Ongeldig layout formaat']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE users SET fociz_widget_layout = ? WHERE id = ?");
|
||||
$stmt->execute([json_encode($layout), $user['id']]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Widget indeling succesvol opgeslagen']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1c. Scratchpad snelle notities opslaan
|
||||
if ($action === 'save_scratchpad') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$date = $data['date'] ?? $today;
|
||||
$notes = $data['notes'] ?? '';
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO daily_plans (user_id, plan_date, notes)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE notes = VALUES(notes)
|
||||
");
|
||||
$stmt->execute([$user['id'], $date, $notes]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Taak toevoegen
|
||||
if ($action === 'add_task') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
+102
-2
@@ -408,13 +408,113 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 7b. Real-time Live Sync (Mail, Jira & Agenda)
|
||||
let lastKnownMailId = localStorage.getItem('fociz_last_mail_id') || '';
|
||||
let lastKnownJiraKey = localStorage.getItem('fociz_last_jira_key') || '';
|
||||
let lastCalendarHash = '';
|
||||
let isLiveSyncRunning = false;
|
||||
let isInitialLiveSync = true;
|
||||
|
||||
async function checkLiveSync() {
|
||||
if (isLiveSyncRunning) return;
|
||||
isLiveSyncRunning = true;
|
||||
try {
|
||||
const url = `/api/live_sync.php?last_mail_id=${encodeURIComponent(lastKnownMailId)}&last_jira_key=${encodeURIComponent(lastKnownJiraKey)}&calendar_hash=${encodeURIComponent(lastCalendarHash)}`;
|
||||
const res = await fetch(url, { cache: 'no-store' });
|
||||
if (res.status === 401) return;
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data && data.success) {
|
||||
// Update mail badge in navigatie
|
||||
const mailBadge = document.getElementById('navMailBadge');
|
||||
if (mailBadge && typeof data.mail?.unread_count !== 'undefined') {
|
||||
if (data.mail.unread_count > 0) {
|
||||
mailBadge.innerText = data.mail.unread_count;
|
||||
mailBadge.classList.remove('hidden');
|
||||
} else {
|
||||
mailBadge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// E-mail melding
|
||||
if (!isInitialLiveSync && data.mail?.has_new && data.mail.new_mail) {
|
||||
const m = data.mail.new_mail;
|
||||
sendBrowserNotification(
|
||||
`✉️ Nieuwe mail: ${m.from_name}`,
|
||||
`${m.subject} • ${m.received_human || 'Zojuist'}`,
|
||||
m.web_link || '/mail.php',
|
||||
`fociz-mail-${m.id}`
|
||||
);
|
||||
}
|
||||
|
||||
// Jira ticket melding
|
||||
if (!isInitialLiveSync && data.jira?.has_new && data.jira.new_issue) {
|
||||
const j = data.jira.new_issue;
|
||||
sendBrowserNotification(
|
||||
`⚡ Nieuw Jira ticket: [${j.key}]`,
|
||||
`${j.summary} (${j.priority})`,
|
||||
j.url || '/jira.php',
|
||||
`fociz-jira-${j.key}`
|
||||
);
|
||||
}
|
||||
|
||||
// Dispatch custom event voor actieve pagina's
|
||||
if (data.mail?.has_new || (data.mail?.latest_id && data.mail.latest_id !== lastKnownMailId)) {
|
||||
window.dispatchEvent(new CustomEvent('fociz:mail-updated', { detail: data.mail }));
|
||||
}
|
||||
if (data.jira?.has_new || (data.jira?.newest_key && data.jira.newest_key !== lastKnownJiraKey)) {
|
||||
window.dispatchEvent(new CustomEvent('fociz:jira-updated', { detail: data.jira }));
|
||||
}
|
||||
if (data.calendar?.has_changes) {
|
||||
window.dispatchEvent(new CustomEvent('fociz:calendar-updated', { detail: data.calendar }));
|
||||
}
|
||||
|
||||
if (data.mail?.latest_id) {
|
||||
lastKnownMailId = data.mail.latest_id;
|
||||
localStorage.setItem('fociz_last_mail_id', lastKnownMailId);
|
||||
}
|
||||
if (data.jira?.newest_key) {
|
||||
lastKnownJiraKey = data.jira.newest_key;
|
||||
localStorage.setItem('fociz_last_jira_key', lastKnownJiraKey);
|
||||
}
|
||||
if (data.calendar?.hash) {
|
||||
lastCalendarHash = data.calendar.hash;
|
||||
}
|
||||
|
||||
isInitialLiveSync = false;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
// Stil negeren bij netwerkvertraging
|
||||
} finally {
|
||||
isLiveSyncRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Initialisatie
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshEvents();
|
||||
// Polling interval elke 60 seconden
|
||||
checkLiveSync();
|
||||
|
||||
// Polling interval elke 60 seconden voor agenda evaluatie
|
||||
setInterval(refreshEvents, 60000);
|
||||
// Evaluatie interval elke 15 seconden
|
||||
// Evaluatie interval elke 15 seconden voor agenda popups
|
||||
setInterval(evaluateEvents, 15000);
|
||||
// Live sync interval elke 12 seconden voor instant mail, Jira & calendar updates
|
||||
setInterval(checkLiveSync, 12000);
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
checkLiveSync();
|
||||
refreshEvents();
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkLiveSync();
|
||||
refreshEvents();
|
||||
}
|
||||
});
|
||||
|
||||
updateDotStatus();
|
||||
});
|
||||
|
||||
|
||||
@@ -148,6 +148,9 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme",
|
||||
"ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled",
|
||||
"ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url",
|
||||
"ALTER TABLE users ADD COLUMN fociz_widget_layout TEXT NULL AFTER default_jira_filter_code",
|
||||
"ALTER TABLE users ADD COLUMN last_mail_check_at DATETIME NULL AFTER fociz_widget_layout",
|
||||
"ALTER TABLE users ADD COLUMN last_jira_check_at DATETIME NULL AFTER last_mail_check_at",
|
||||
"ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)",
|
||||
"CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
"CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
@@ -15,6 +15,9 @@ CREATE TABLE IF NOT EXISTS `users` (
|
||||
`notification_settings` TEXT NULL,
|
||||
`avatar_url` VARCHAR(255) NULL,
|
||||
`default_jira_filter_code` VARCHAR(50) NULL,
|
||||
`fociz_widget_layout` TEXT NULL,
|
||||
`last_mail_check_at` DATETIME NULL,
|
||||
`last_jira_check_at` DATETIME NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
+14
-4
@@ -161,8 +161,14 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<!-- Nav Tabs -->
|
||||
<nav class="hidden md:flex items-center space-x-1">
|
||||
<a href="/planner.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'planner.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||||
<span>Planning</span>
|
||||
<i data-lucide="layout-dashboard" class="w-4 h-4"></i>
|
||||
<span>Fociz</span>
|
||||
</a>
|
||||
|
||||
<a href="/mail.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition relative <?= $currentPage === 'mail.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
<i data-lucide="mail" class="w-4 h-4 text-sky-500"></i>
|
||||
<span>Mail</span>
|
||||
<span id="navMailBadge" class="hidden px-1.5 py-0.2 rounded-full text-[10px] font-black bg-sky-600 text-white leading-none"></span>
|
||||
</a>
|
||||
|
||||
<!-- Dropdown: Projecten (Bord, Tijdlijn, Brainstorm, Roadmap) -->
|
||||
@@ -275,8 +281,12 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<!-- Mobile Navigation -->
|
||||
<div class="flex md:hidden items-center justify-around py-2 border-t border-slate-200/80 dark:border-slate-800/60 text-xs">
|
||||
<a href="/planner.php" class="flex flex-col items-center py-1 <?= $currentPage === 'planner.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Planning</span>
|
||||
<i data-lucide="layout-dashboard" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Fociz</span>
|
||||
</a>
|
||||
<a href="/mail.php" class="flex flex-col items-center py-1 <?= $currentPage === 'mail.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="mail" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Mail</span>
|
||||
</a>
|
||||
<a href="/board.php" class="flex flex-col items-center py-1 <?= $currentPage === 'board.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="kanban" class="w-4 h-4 mb-0.5"></i>
|
||||
|
||||
@@ -10,7 +10,7 @@ require_once __DIR__ . '/../../config/database.php';
|
||||
const MS_OAUTH_AUTHORIZE_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize';
|
||||
const MS_OAUTH_TOKEN_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
|
||||
const MS_GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0';
|
||||
const MS_DEFAULT_SCOPES = 'offline_access User.Read Calendars.ReadWrite';
|
||||
const MS_DEFAULT_SCOPES = 'offline_access User.Read Calendars.ReadWrite Mail.ReadWrite Mail.Send';
|
||||
|
||||
/**
|
||||
* Haalt de centrale Microsoft App Registratie configuratie op (Client ID, Secret, Tenant ID)
|
||||
@@ -881,3 +881,477 @@ function microsoft_reconcile_tasks_with_outlook(int $userId, string $startDate,
|
||||
'updated' => $updatedCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* ==============================================================================
|
||||
* MICROSOFT 365 E-MAIL INTEGRATIE (GRAPH API)
|
||||
* ==============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Levert realistische demo e-mails voor accounts zonder actieve Outlook verbinding
|
||||
*/
|
||||
function microsoft_get_mail_demo_data(): array {
|
||||
$now = time();
|
||||
return [
|
||||
[
|
||||
'id' => 'demo-mail-1',
|
||||
'subject' => '⚡ Urgent: UWV Koppeling REST-endpoints en Datamigratie timing',
|
||||
'body_preview' => 'Beste Niek, we hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers...',
|
||||
'body_html' => '<p>Beste Niek,</p><p>We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de <strong>UWV REST-endpoints</strong> en de validatie van werknemersdossiers.</p><p>Zou je kunnen verifiëren of onze SSO-koppeling en de datadump export hier al op aansluiten? Het zou fijn zijn als we dit vanmiddag kort kunnen afstemmen in de planning.</p><p>Met vriendelijke groet,<br><strong>Directie UWV Koppelingen & Integraties</strong></p>',
|
||||
'from_name' => 'Directie UWV Integraties',
|
||||
'from_email' => 'koppelingen@uwv-overheid.nl',
|
||||
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
|
||||
'cc_recipients' => [],
|
||||
'received_at' => date('Y-m-d H:i:s', $now - (12 * 60)), // 12 minuten geleden
|
||||
'received_human' => '12 min geleden',
|
||||
'is_read' => false,
|
||||
'has_attachments' => true,
|
||||
'importance' => 'high',
|
||||
'web_link' => 'https://outlook.office.com/mail/inbox',
|
||||
'is_demo' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'demo-mail-2',
|
||||
'subject' => 'Factuur & Urenverantwoording September — Goedkeuring vereist',
|
||||
'body_preview' => 'Hallo team, bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de week accorderen...',
|
||||
'body_html' => '<p>Hallo team,</p><p>Bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de werkweek accorderen in het systeem.</p><p>Laat het weten indien er correcties nodig zijn op de dagregistraties.</p><p>Hartelijke groet,<br><strong>Financiële Administratie Fociz</strong></p>',
|
||||
'from_name' => 'Financiële Administratie',
|
||||
'from_email' => 'facturatie@fociz.nl',
|
||||
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
|
||||
'cc_recipients' => [],
|
||||
'received_at' => date('Y-m-d H:i:s', $now - (65 * 60)), // 1 uur geleden
|
||||
'received_human' => '1 uur geleden',
|
||||
'is_read' => false,
|
||||
'has_attachments' => false,
|
||||
'importance' => 'normal',
|
||||
'web_link' => 'https://outlook.office.com/mail/inbox',
|
||||
'is_demo' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'demo-mail-3',
|
||||
'subject' => 'Jira SSO Migratie & Staging server testomgeving',
|
||||
'body_preview' => 'Goedemiddag, de testomgeving voor de nieuwe Atlassian API migratie (CHANGE-2046) is succesvol klaargezet. We kunnen starten met de verificatietesten...',
|
||||
'body_html' => '<p>Goedemiddag,</p><p>De testomgeving voor de nieuwe Atlassian API migratie (CHANGE-2046) is succesvol klaargezet. We kunnen starten met de verificatietesten op de Servicedesk wachtrijen en DX filters.</p><p>Alle endpoint logs zijn beschikbaar via het beheerpaneel.</p><p>Groeten,<br><strong>Team DevOps & Infrastructuur</strong></p>',
|
||||
'from_name' => 'DevOps Systeembeheer',
|
||||
'from_email' => 'infra@fociz.nl',
|
||||
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
|
||||
'cc_recipients' => [],
|
||||
'received_at' => date('Y-m-d H:i:s', $now - (3 * 3600)), // 3 uur geleden
|
||||
'received_human' => '3 uur geleden',
|
||||
'is_read' => true,
|
||||
'has_attachments' => true,
|
||||
'importance' => 'normal',
|
||||
'web_link' => 'https://outlook.office.com/mail/inbox',
|
||||
'is_demo' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'demo-mail-4',
|
||||
'subject' => 'Herinnering: Kwartaal Roadmap Review & Strategie sessie',
|
||||
'body_preview' => 'Beste collega\'s, vergeet niet je prioriteiten en feature requests in te dienen in de Fociz Roadmap module voor de meeting van aanstaande donderdag...',
|
||||
'body_html' => '<p>Beste collega\'s,</p><p>Vergeet niet je prioriteiten en feature requests in te dienen in de <strong>Fociz Roadmap</strong> module voor de meeting van aanstaande donderdag.</p><p>We behandelen dan tevens de nieuwe Kanban borden en de Outlook time-blocking ervaring.</p><p>Tot dan!<br><strong>Product Management</strong></p>',
|
||||
'from_name' => 'Product Management',
|
||||
'from_email' => 'product@fociz.nl',
|
||||
'to_recipients' => [['name' => 'Alle Medewerkers', 'address' => 'team@fociz.nl']],
|
||||
'cc_recipients' => [],
|
||||
'received_at' => date('Y-m-d H:i:s', $now - (24 * 3600)), // Gisteren
|
||||
'received_human' => 'Gisteren',
|
||||
'is_read' => true,
|
||||
'has_attachments' => false,
|
||||
'importance' => 'normal',
|
||||
'web_link' => 'https://outlook.office.com/mail/inbox',
|
||||
'is_demo' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'demo-mail-5',
|
||||
'subject' => 'Welkom bij Fociz Live — Handige tips voor jouw dagplanning',
|
||||
'body_preview' => 'Welkom bij Fociz! Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw dagrooster?...',
|
||||
'body_html' => '<p>Welkom bij Fociz!</p><p>Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw persoonlijke dagplanning?</p><p>Hierdoor houd je maximale grip op je werkdag en behoud je de ultieme focus.</p><p>Veel productiviteit gewenst,<br><strong>Het Fociz Team</strong></p>',
|
||||
'from_name' => 'Fociz Support Team',
|
||||
'from_email' => 'no-reply@fociz.nl',
|
||||
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
|
||||
'cc_recipients' => [],
|
||||
'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)), // 2 dagen geleden
|
||||
'received_human' => '2 dagen geleden',
|
||||
'is_read' => true,
|
||||
'has_attachments' => false,
|
||||
'importance' => 'low',
|
||||
'web_link' => 'https://outlook.office.com/mail/inbox',
|
||||
'is_demo' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data)
|
||||
*/
|
||||
function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unreadOnly = false): array {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||
$stmt->execute([$userId]);
|
||||
$integration = $stmt->fetch();
|
||||
|
||||
$isLive = !empty($integration['access_token']) && empty($integration['is_demo_mode']);
|
||||
if (!$isLive) {
|
||||
$demo = microsoft_get_mail_demo_data();
|
||||
if ($unreadOnly) {
|
||||
$demo = array_values(array_filter($demo, function($m) { return empty($m['is_read']); }));
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'is_demo' => true,
|
||||
'messages' => array_slice($demo, 0, $limit),
|
||||
'unread_count' => count(array_filter($demo, function($m) { return empty($m['is_read']); })),
|
||||
'total_count' => count($demo),
|
||||
];
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Geen geldig Microsoft 365 token beschikbaar.',
|
||||
'messages' => [],
|
||||
'unread_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$queryParams = [
|
||||
'$top' => min(50, max(1, $limit)),
|
||||
'$select' => 'id,subject,bodyPreview,from,toRecipients,receivedDateTime,isRead,hasAttachments,importance,webLink',
|
||||
'$orderby' => 'receivedDateTime desc',
|
||||
];
|
||||
|
||||
if ($unreadOnly) {
|
||||
$queryParams['$filter'] = 'isRead eq false';
|
||||
}
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/mailFolders/inbox/messages?' . http_build_query($queryParams);
|
||||
$res = fociz_http_request($url, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Accept' => 'application/json',
|
||||
'Prefer' => 'outlook.body-content-type="text"',
|
||||
],
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($res['status'] !== 200 || !isset($res['json']['value'])) {
|
||||
error_log("Microsoft fetch inbox error: HTTP {$res['status']} - " . ($res['error'] ?? json_encode($res['json'])));
|
||||
// Val terug op demo data indien API fout optreedt
|
||||
$demo = microsoft_get_mail_demo_data();
|
||||
return [
|
||||
'success' => true,
|
||||
'is_demo' => true,
|
||||
'messages' => array_slice($demo, 0, $limit),
|
||||
'unread_count' => count(array_filter($demo, function($m) { return empty($m['is_read']); })),
|
||||
'total_count' => count($demo),
|
||||
'warning' => 'Kon live e-mails niet ophalen van Microsoft Graph. Demomodus actief.',
|
||||
];
|
||||
}
|
||||
|
||||
$tzAmsterdam = new DateTimeZone('Europe/Amsterdam');
|
||||
$messages = [];
|
||||
|
||||
foreach ($res['json']['value'] as $msg) {
|
||||
$fromObj = $msg['from']['emailAddress'] ?? [];
|
||||
$fromName = $fromObj['name'] ?? ($fromObj['address'] ?? 'Onbekende afzender');
|
||||
$fromEmail = $fromObj['address'] ?? '';
|
||||
|
||||
$receivedIso = $msg['receivedDateTime'] ?? '';
|
||||
$receivedFormatted = '';
|
||||
$receivedHuman = '';
|
||||
|
||||
if (!empty($receivedIso)) {
|
||||
try {
|
||||
$dt = new DateTime($receivedIso);
|
||||
$dt->setTimezone($tzAmsterdam);
|
||||
$receivedFormatted = $dt->format('Y-m-d H:i:s');
|
||||
|
||||
$todayStr = date('Y-m-d');
|
||||
$msgDate = $dt->format('Y-m-d');
|
||||
if ($msgDate === $todayStr) {
|
||||
$receivedHuman = $dt->format('H:i');
|
||||
} elseif ($msgDate === date('Y-m-d', strtotime('-1 day'))) {
|
||||
$receivedHuman = 'Gisteren ' . $dt->format('H:i');
|
||||
} else {
|
||||
$receivedHuman = $dt->format('d-m H:i');
|
||||
}
|
||||
} catch (Throwable $t) {
|
||||
$receivedFormatted = $receivedIso;
|
||||
$receivedHuman = substr($receivedIso, 0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'id' => $msg['id'] ?? '',
|
||||
'subject' => $msg['subject'] ?: '(Geen onderwerp)',
|
||||
'body_preview' => $msg['bodyPreview'] ?? '',
|
||||
'from_name' => $fromName,
|
||||
'from_email' => $fromEmail,
|
||||
'to_recipients' => array_map(function($r) {
|
||||
return [
|
||||
'name' => $r['emailAddress']['name'] ?? '',
|
||||
'address' => $r['emailAddress']['address'] ?? '',
|
||||
];
|
||||
}, $msg['toRecipients'] ?? []),
|
||||
'received_at' => $receivedFormatted,
|
||||
'received_human' => $receivedHuman,
|
||||
'is_read' => !empty($msg['isRead']),
|
||||
'has_attachments' => !empty($msg['hasAttachments']),
|
||||
'importance' => strtolower($msg['importance'] ?? 'normal'),
|
||||
'web_link' => $msg['webLink'] ?? 'https://outlook.office.com/mail/',
|
||||
'is_demo' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$unreadCount = count(array_filter($messages, function($m) { return empty($m['is_read']); }));
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'is_demo' => false,
|
||||
'messages' => $messages,
|
||||
'unread_count' => $unreadCount,
|
||||
'total_count' => count($messages),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de volledige inhoud en details van een enkel e-mailbericht op
|
||||
*/
|
||||
function microsoft_fetch_message_detail(int $userId, string $messageId): ?array {
|
||||
// Check op demo ID
|
||||
if (strpos($messageId, 'demo-mail-') === 0) {
|
||||
$demo = microsoft_get_mail_demo_data();
|
||||
foreach ($demo as $m) {
|
||||
if ($m['id'] === $messageId) {
|
||||
$m['is_read'] = true;
|
||||
return $m;
|
||||
}
|
||||
}
|
||||
return $demo[0] ?? null;
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/messages/' . urlencode($messageId) . '?$select=id,subject,body,bodyPreview,from,toRecipients,ccRecipients,receivedDateTime,isRead,hasAttachments,importance,webLink';
|
||||
$res = fociz_http_request($url, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($res['status'] !== 200 || !isset($res['json']['id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$msg = $res['json'];
|
||||
$tzAmsterdam = new DateTimeZone('Europe/Amsterdam');
|
||||
$receivedFormatted = '';
|
||||
$receivedHuman = '';
|
||||
|
||||
if (!empty($msg['receivedDateTime'])) {
|
||||
try {
|
||||
$dt = new DateTime($msg['receivedDateTime']);
|
||||
$dt->setTimezone($tzAmsterdam);
|
||||
$receivedFormatted = $dt->format('Y-m-d H:i:s');
|
||||
$receivedHuman = $dt->format('d-m-Y H:i');
|
||||
} catch (Throwable $t) {
|
||||
$receivedFormatted = $msg['receivedDateTime'];
|
||||
$receivedHuman = $msg['receivedDateTime'];
|
||||
}
|
||||
}
|
||||
|
||||
$bodyType = strtolower($msg['body']['contentType'] ?? 'text');
|
||||
$bodyContent = $msg['body']['content'] ?? '';
|
||||
|
||||
// Markeer direct als gelezen via Graph API
|
||||
if (empty($msg['isRead'])) {
|
||||
microsoft_mark_message_read($userId, $messageId, true);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $msg['id'],
|
||||
'subject' => $msg['subject'] ?: '(Geen onderwerp)',
|
||||
'body_preview' => $msg['bodyPreview'] ?? '',
|
||||
'body_html' => ($bodyType === 'html') ? $bodyContent : nl2br(htmlspecialchars($bodyContent)),
|
||||
'body_text' => ($bodyType === 'text') ? $bodyContent : strip_tags($bodyContent),
|
||||
'from_name' => $msg['from']['emailAddress']['name'] ?? ($msg['from']['emailAddress']['address'] ?? 'Onbekend'),
|
||||
'from_email' => $msg['from']['emailAddress']['address'] ?? '',
|
||||
'to_recipients' => array_map(function($r) {
|
||||
return [
|
||||
'name' => $r['emailAddress']['name'] ?? '',
|
||||
'address' => $r['emailAddress']['address'] ?? '',
|
||||
];
|
||||
}, $msg['toRecipients'] ?? []),
|
||||
'cc_recipients' => array_map(function($r) {
|
||||
return [
|
||||
'name' => $r['emailAddress']['name'] ?? '',
|
||||
'address' => $r['emailAddress']['address'] ?? '',
|
||||
];
|
||||
}, $msg['ccRecipients'] ?? []),
|
||||
'received_at' => $receivedFormatted,
|
||||
'received_human' => $receivedHuman,
|
||||
'is_read' => true,
|
||||
'has_attachments' => !empty($msg['hasAttachments']),
|
||||
'importance' => strtolower($msg['importance'] ?? 'normal'),
|
||||
'web_link' => $msg['webLink'] ?? 'https://outlook.office.com/mail/',
|
||||
'is_demo' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Markeert een e-mailbericht als gelezen of ongelezen
|
||||
*/
|
||||
function microsoft_mark_message_read(int $userId, string $messageId, bool $isRead = true): bool {
|
||||
if (strpos($messageId, 'demo-mail-') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) return false;
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/messages/' . urlencode($messageId);
|
||||
$res = fociz_http_request($url, [
|
||||
'method' => 'PATCH',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => ['isRead' => $isRead],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
return ($res['status'] >= 200 && $res['status'] < 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Beantwoordt een e-mailbericht via Microsoft Graph API
|
||||
*/
|
||||
function microsoft_reply_message(int $userId, string $messageId, string $comment, bool $replyAll = false): array {
|
||||
if (strpos($messageId, 'demo-mail-') === 0) {
|
||||
return ['success' => true, 'message' => 'Demo antwoord succesvol gesimuleerd.'];
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) {
|
||||
return ['success' => false, 'error' => 'Geen geldige Microsoft verbinding.'];
|
||||
}
|
||||
|
||||
$action = $replyAll ? 'replyAll' : 'reply';
|
||||
$url = MS_GRAPH_BASE_URL . '/me/messages/' . urlencode($messageId) . '/' . $action;
|
||||
|
||||
$res = fociz_http_request($url, [
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => [
|
||||
'comment' => $comment,
|
||||
],
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($res['status'] >= 200 && $res['status'] < 300) {
|
||||
return ['success' => true, 'message' => 'Antwoord succesvol verzonden via Outlook.'];
|
||||
}
|
||||
|
||||
$errMsg = $res['json']['error']['message'] ?? ($res['error'] ?? 'Fout bij verzenden van antwoord.');
|
||||
return ['success' => false, 'error' => $errMsg];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stuurt een e-mailbericht door via Microsoft Graph API
|
||||
*/
|
||||
function microsoft_forward_message(int $userId, string $messageId, array $toRecipients, string $comment = ''): array {
|
||||
if (strpos($messageId, 'demo-mail-') === 0) {
|
||||
return ['success' => true, 'message' => 'Demo doorsturen succesvol gesimuleerd.'];
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) {
|
||||
return ['success' => false, 'error' => 'Geen geldige Microsoft verbinding.'];
|
||||
}
|
||||
|
||||
$recipientsPayload = [];
|
||||
foreach ($toRecipients as $r) {
|
||||
$addr = is_array($r) ? ($r['address'] ?? '') : trim($r);
|
||||
$name = is_array($r) ? ($r['name'] ?? $addr) : $addr;
|
||||
if (!empty($addr)) {
|
||||
$recipientsPayload[] = [
|
||||
'emailAddress' => [
|
||||
'address' => $addr,
|
||||
'name' => $name,
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($recipientsPayload)) {
|
||||
return ['success' => false, 'error' => 'Geen geldige ontvangers opgegeven om door te sturen.'];
|
||||
}
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/messages/' . urlencode($messageId) . '/forward';
|
||||
$res = fociz_http_request($url, [
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => [
|
||||
'toRecipients' => $recipientsPayload,
|
||||
'comment' => $comment,
|
||||
],
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($res['status'] >= 200 && $res['status'] < 300) {
|
||||
return ['success' => true, 'message' => 'E-mail succesvol doorgestuurd via Outlook.'];
|
||||
}
|
||||
|
||||
$errMsg = $res['json']['error']['message'] ?? ($res['error'] ?? 'Fout bij doorsturen van e-mail.');
|
||||
return ['success' => false, 'error' => $errMsg];
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt het aantal ongelezen inbox e-mails op
|
||||
*/
|
||||
function microsoft_get_unread_mail_count(int $userId): int {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||
$stmt->execute([$userId]);
|
||||
$integration = $stmt->fetch();
|
||||
|
||||
if (!$integration || empty($integration['access_token']) || !empty($integration['is_demo_mode'])) {
|
||||
$demo = microsoft_get_mail_demo_data();
|
||||
return count(array_filter($demo, function($m) { return empty($m['is_read']); }));
|
||||
}
|
||||
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
if (!$token) return 0;
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/mailFolders/inbox?$select=unreadItemCount';
|
||||
$res = fociz_http_request($url, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if ($res['status'] === 200 && isset($res['json']['unreadItemCount'])) {
|
||||
return (int)$res['json']['unreadItemCount'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user