diff --git a/api/mail.php b/api/mail.php
index 4708eca..0b7e526 100644
--- a/api/mail.php
+++ b/api/mail.php
@@ -22,13 +22,22 @@ 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;
+ // 0. Mail mappen ophalen
+ if ($action === 'get_folders') {
+ $forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
+ $res = microsoft_fetch_mail_folders($userId, $forceRefresh);
+ echo json_encode($res);
+ exit;
+ }
+
+ // 1. Inbox of specifieke map berichten ophalen
+ if ($action === 'get_folder_messages' || $action === 'get_inbox') {
+ $folderId = trim($_GET['folder_id'] ?? ($_POST['folder_id'] ?? 'inbox'));
+ $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 30;
$unreadOnly = !empty($_GET['unread_only']) && $_GET['unread_only'] === '1';
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
- $res = microsoft_fetch_inbox_messages($userId, $limit, $unreadOnly, $forceRefresh);
+ $res = microsoft_fetch_folder_messages($userId, $folderId, $limit, $unreadOnly, $forceRefresh);
echo json_encode($res);
exit;
}
diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php
index 679789d..f02dee3 100644
--- a/includes/integrations/microsoft.php
+++ b/includes/integrations/microsoft.php
@@ -940,11 +940,156 @@ function microsoft_reconcile_tasks_with_outlook(int $userId, string $startDate,
/**
* Levert realistische demo e-mails voor accounts zonder actieve Outlook verbinding
*/
-function microsoft_get_mail_demo_data(): array {
+function microsoft_get_mail_demo_data(?string $folderId = 'inbox'): array {
$now = time();
+ $fId = strtolower(trim($folderId ?: 'inbox'));
+
+ if ($fId === 'demo-folder-dx') {
+ return [
+ [
+ 'id' => 'demo-mail-dx-1',
+ 'folder_id' => 'demo-folder-dx',
+ 'subject' => '⚡ Update: REST endpoint v2 specificatie voor UWV & Koppelingen',
+ 'body_preview' => 'Beste collega, hierbij de bijgewerkte documentatie voor de REST endpoints en de schema-wijzigingen voor de datadump...',
+ 'body_html' => '
Beste collega,
Hierbij de bijgewerkte documentatie voor de REST endpoints en de schema-wijzigingen voor de datadump.
Graag controleren of de data mapping aansluit bij onze migratiestappen.
Met vriendelijke groet,Team Koppelingen & DX
',
+ 'from_name' => 'Koppelingen Team DX',
+ 'from_email' => 'dx-koppelingen@fociz.nl',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (25 * 60)),
+ 'received_human' => '25 min geleden',
+ 'is_read' => false,
+ 'has_attachments' => true,
+ 'importance' => 'high',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ [
+ 'id' => 'demo-mail-dx-2',
+ 'folder_id' => 'demo-folder-dx',
+ 'subject' => 'Migratie draaiboek koppelingen fase 3 gereed',
+ 'body_preview' => 'Het draaiboek voor de uitrol van de vernieuwde interfaces is goedgekeurd door de lead architect...',
+ 'body_html' => 'Het draaiboek voor de uitrol van de vernieuwde interfaces is goedgekeurd door de lead architect. Zie de bijlage voor de tijdsplanning.
',
+ 'from_name' => 'Lead Architect',
+ 'from_email' => 'architect@fociz.nl',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (4 * 3600)),
+ 'received_human' => '4 uur geleden',
+ 'is_read' => true,
+ 'has_attachments' => false,
+ 'importance' => 'normal',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ [
+ 'id' => 'demo-mail-dx-3',
+ 'folder_id' => 'demo-folder-dx',
+ 'subject' => 'SSO Certificaat verificatie voor staging omgeving',
+ 'body_preview' => 'Het SSL/TLS certificaat voor de authenticatiekoppeling verloopt over 30 dagen. Het nieuwe certificaat is klaargezet...',
+ 'body_html' => 'Het SSL/TLS certificaat voor de authenticatiekoppeling verloopt over 30 dagen. Het nieuwe certificaat is klaargezet op de staging server.
',
+ 'from_name' => 'Security & Certificaten',
+ 'from_email' => 'security@fociz.nl',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (26 * 3600)),
+ 'received_human' => 'Gisteren',
+ 'is_read' => true,
+ 'has_attachments' => false,
+ 'importance' => 'normal',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ ];
+ }
+
+ if ($fId === 'demo-folder-klanten') {
+ return [
+ [
+ 'id' => 'demo-mail-klant-1',
+ 'folder_id' => 'demo-folder-klanten',
+ 'subject' => 'Project kick-off partnerportaal integratie',
+ 'body_preview' => 'Beste Fociz team, bedankt voor de toelichting gisteren. We kijken uit naar de gezamenlijke sprintstart volgende maand...',
+ 'body_html' => 'Beste Fociz team,
Bedankt voor de toelichting gisteren. We kijken uit naar de gezamenlijke sprintstart volgende maand.
Vriendelijke groet,Partner Relatiemanager
',
+ 'from_name' => 'Partner Relaties',
+ 'from_email' => 'partners@klantportaal.nl',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (5 * 3600)),
+ 'received_human' => '5 uur geleden',
+ 'is_read' => true,
+ 'has_attachments' => false,
+ 'importance' => 'normal',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ [
+ 'id' => 'demo-mail-klant-2',
+ 'folder_id' => 'demo-folder-klanten',
+ 'subject' => 'Vraag over API sleutels en documentatie',
+ 'body_preview' => 'Hallo, kunnen we de sandbox tokens alvast ontvangen voor onze ontwikkelaars?...',
+ 'body_html' => 'Hallo, kunnen we de sandbox tokens alvast ontvangen voor onze ontwikkelaars? Alvast dank!
',
+ 'from_name' => 'IT Lead Klant',
+ 'from_email' => 'tech@partnerbedrijf.com',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (28 * 3600)),
+ 'received_human' => 'Gisteren',
+ 'is_read' => true,
+ 'has_attachments' => false,
+ 'importance' => 'normal',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ ];
+ }
+
+ if ($fId === 'demo-folder-fin') {
+ return [
+ [
+ 'id' => 'demo-mail-fin-1',
+ 'folder_id' => 'demo-folder-fin',
+ 'subject' => 'Nieuwe factuur September licenties ter goedkeuring',
+ 'body_preview' => 'Beste directie, bijgaand de specificatie van de maandelijks cloud infrastructuur kosten ter akkoord...',
+ 'body_html' => 'Beste directie,
Bijgaand de specificatie van de maandelijkse cloud infrastructuur kosten ter akkoord. Graag uiterlijk vrijdag verwerken.
',
+ 'from_name' => 'Financiële Administratie',
+ 'from_email' => 'facturen@fociz.nl',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (40 * 60)),
+ 'received_human' => '40 min geleden',
+ 'is_read' => false,
+ 'has_attachments' => true,
+ 'importance' => 'high',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ [
+ 'id' => 'demo-mail-fin-2',
+ 'folder_id' => 'demo-folder-fin',
+ 'subject' => 'Betaalbevestiging software koppelingen',
+ 'body_preview' => 'Uw betaling voor de kwartaallicenties is succesvol verwerkt...',
+ 'body_html' => 'Uw betaling voor de kwartaallicenties is succesvol verwerkt. Factuurnummer INV-2026-09.
',
+ 'from_name' => 'Accounts Payable',
+ 'from_email' => 'billing@cloudvendor.com',
+ 'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
+ 'cc_recipients' => [],
+ 'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)),
+ 'received_human' => '2 dagen geleden',
+ 'is_read' => true,
+ 'has_attachments' => false,
+ 'importance' => 'low',
+ 'web_link' => 'https://outlook.office.com/mail/',
+ 'is_demo' => true,
+ ],
+ ];
+ }
+
+ // Standaard Inbox demo berichten
return [
[
'id' => 'demo-mail-1',
+ 'folder_id' => 'inbox',
'subject' => '⚡ Urgent: UWV Koppeling REST-endpoints en Datamigratie timing',
'body_preview' => 'Beste collega, we hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers...',
'body_html' => 'Beste collega,
We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers.
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.
Met vriendelijke groet,Directie UWV Koppelingen & Integraties
',
@@ -952,7 +1097,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'koppelingen@uwv-overheid.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
- 'received_at' => date('Y-m-d H:i:s', $now - (12 * 60)), // 12 minuten geleden
+ 'received_at' => date('Y-m-d H:i:s', $now - (12 * 60)),
'received_human' => '12 min geleden',
'is_read' => false,
'has_attachments' => true,
@@ -962,6 +1107,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-2',
+ 'folder_id' => 'inbox',
'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' => 'Hallo team,
Bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de werkweek accorderen in het systeem.
Laat het weten indien er correcties nodig zijn op de dagregistraties.
Hartelijke groet,Financiële Administratie Fociz
',
@@ -969,7 +1115,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'facturatie@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
- 'received_at' => date('Y-m-d H:i:s', $now - (65 * 60)), // 1 uur geleden
+ 'received_at' => date('Y-m-d H:i:s', $now - (65 * 60)),
'received_human' => '1 uur geleden',
'is_read' => false,
'has_attachments' => false,
@@ -979,6 +1125,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-3',
+ 'folder_id' => 'inbox',
'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' => 'Goedemiddag,
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.
Alle endpoint logs zijn beschikbaar via het beheerpaneel.
Groeten,Team DevOps & Infrastructuur
',
@@ -986,7 +1133,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'infra@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
- 'received_at' => date('Y-m-d H:i:s', $now - (3 * 3600)), // 3 uur geleden
+ 'received_at' => date('Y-m-d H:i:s', $now - (3 * 3600)),
'received_human' => '3 uur geleden',
'is_read' => true,
'has_attachments' => true,
@@ -996,6 +1143,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-4',
+ 'folder_id' => 'inbox',
'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' => 'Beste collega\'s,
Vergeet niet je prioriteiten en feature requests in te dienen in de Fociz Roadmap module voor de meeting van aanstaande donderdag.
We behandelen dan tevens de nieuwe Kanban borden en de Outlook time-blocking ervaring.
Tot dan!Product Management
',
@@ -1003,7 +1151,7 @@ function microsoft_get_mail_demo_data(): array {
'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_at' => date('Y-m-d H:i:s', $now - (24 * 3600)),
'received_human' => 'Gisteren',
'is_read' => true,
'has_attachments' => false,
@@ -1013,6 +1161,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-5',
+ 'folder_id' => 'inbox',
'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' => 'Welkom bij Fociz!
Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw persoonlijke dagplanning?
Hierdoor houd je maximale grip op je werkdag en behoud je de ultieme focus.
Veel productiviteit gewenst,Het Fociz Team
',
@@ -1020,7 +1169,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'no-reply@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
- 'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)), // 2 dagen geleden
+ 'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)),
'received_human' => '2 dagen geleden',
'is_read' => true,
'has_attachments' => false,
@@ -1032,10 +1181,10 @@ function microsoft_get_mail_demo_data(): array {
}
/**
- * Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data)
+ * Haalt alle beschikbare mailmappen op van Microsoft Graph API (exclusief SPAM/Junk, Prullenbak/Deleted, Outbox en Concepten)
*/
-function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unreadOnly = false, bool $forceRefresh = false): array {
- $cacheKey = "ms_inbox_{$userId}_{$limit}_" . ($unreadOnly ? '1' : '0');
+function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false): array {
+ $cacheKey = "ms_folders_{$userId}";
if (!$forceRefresh) {
$cached = fociz_cache_get($cacheKey);
if ($cached !== null && is_array($cached)) {
@@ -1050,13 +1199,208 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$isLive = !empty($integration['access_token']) && empty($integration['is_demo_mode']);
if (!$isLive) {
- $demo = microsoft_get_mail_demo_data();
+ $demoFolders = [
+ [
+ 'id' => 'inbox',
+ 'name' => 'Postvak IN',
+ 'unread_count' => 2,
+ 'total_count' => 5,
+ 'is_inbox' => true,
+ ],
+ [
+ 'id' => 'demo-folder-dx',
+ 'name' => 'Projecten & DX Koppelingen',
+ 'unread_count' => 1,
+ 'total_count' => 3,
+ 'is_inbox' => false,
+ ],
+ [
+ 'id' => 'demo-folder-klanten',
+ 'name' => 'Klanten & Partners',
+ 'unread_count' => 0,
+ 'total_count' => 2,
+ 'is_inbox' => false,
+ ],
+ [
+ 'id' => 'demo-folder-fin',
+ 'name' => 'Financiën & Facturatie',
+ 'unread_count' => 1,
+ 'total_count' => 2,
+ 'is_inbox' => false,
+ ],
+ ];
+ $totalUnread = array_sum(array_column($demoFolders, 'unread_count'));
+ return [
+ 'success' => true,
+ 'is_demo' => true,
+ 'folders' => $demoFolders,
+ 'total_unread' => $totalUnread,
+ ];
+ }
+
+ $token = microsoft_get_valid_token($userId);
+ if (!$token) {
+ return [
+ 'success' => false,
+ 'error' => 'Geen geldig Microsoft 365 token beschikbaar.',
+ 'folders' => [],
+ 'total_unread' => 0,
+ ];
+ }
+
+ $url = MS_GRAPH_BASE_URL . '/me/mailFolders?$top=100&$select=id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount,wellKnownName';
+ $res = fociz_http_request($url, [
+ 'method' => 'GET',
+ 'headers' => [
+ 'Authorization' => "Bearer {$token}",
+ 'Accept' => 'application/json',
+ ],
+ 'timeout' => 15,
+ ]);
+
+ if ($res['status'] !== 200 || !isset($res['json']['value'])) {
+ $graphError = $res['json']['error']['message'] ?? ($res['error'] ?? 'HTTP ' . $res['status']);
+ error_log("Microsoft fetch folders error: HTTP {$res['status']} - {$graphError}");
+ return [
+ 'success' => false,
+ 'error' => $graphError,
+ 'folders' => [
+ [
+ 'id' => 'inbox',
+ 'name' => 'Postvak IN',
+ 'unread_count' => 0,
+ 'total_count' => 0,
+ 'is_inbox' => true,
+ ]
+ ],
+ 'total_unread' => 0,
+ ];
+ }
+
+ $rawFolders = $res['json']['value'];
+
+ // Haal eventuele submappen op voor mappen met childFolderCount > 0
+ foreach ($res['json']['value'] as $f) {
+ if (!empty($f['childFolderCount']) && (int)$f['childFolderCount'] > 0 && !empty($f['id'])) {
+ $childUrl = MS_GRAPH_BASE_URL . '/me/mailFolders/' . urlencode($f['id']) . '/childFolders?$top=50&$select=id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount,wellKnownName';
+ $cRes = fociz_http_request($childUrl, [
+ 'method' => 'GET',
+ 'headers' => [
+ 'Authorization' => "Bearer {$token}",
+ 'Accept' => 'application/json',
+ ],
+ 'timeout' => 10,
+ ]);
+ if ($cRes['status'] === 200 && !empty($cRes['json']['value'])) {
+ foreach ($cRes['json']['value'] as $child) {
+ $rawFolders[] = $child;
+ }
+ }
+ }
+ }
+
+ // Uitsluitingen conform gebruikerswens: SPAM / Ongewenst, Prullenbak / Deleted, Outbox en Drafts
+ $excludedWellKnown = ['junkemail', 'deleteditems', 'outbox', 'drafts', 'conflicts', 'localfailures', 'serverfailures', 'syncissues'];
+ $excludedTerms = ['junk', 'spam', 'ongewenst', 'ongewenste e-mail', 'ongewenste email', 'deleted items', 'verwijderde items', 'prullenbak', 'trash', 'verwijderd', 'outbox', 'postvak uit', 'drafts', 'concepten'];
+
+ $folders = [];
+ $seenIds = [];
+
+ foreach ($rawFolders as $rf) {
+ $fId = $rf['id'] ?? '';
+ if (empty($fId) || isset($seenIds[$fId])) continue;
+ $seenIds[$fId] = true;
+
+ $wk = strtolower(trim($rf['wellKnownName'] ?? ''));
+ if (in_array($wk, $excludedWellKnown, true)) continue;
+
+ $dName = trim($rf['displayName'] ?? '');
+ $dNameLower = strtolower($dName);
+
+ $skip = false;
+ foreach ($excludedTerms as $term) {
+ if ($dNameLower === $term || strpos($dNameLower, $term) !== false) {
+ $skip = true;
+ break;
+ }
+ }
+ if ($skip) continue;
+
+ $isInbox = ($wk === 'inbox' || $dNameLower === 'inbox' || $dNameLower === 'postvak in');
+
+ $folders[] = [
+ 'id' => $isInbox ? 'inbox' : $fId,
+ 'name' => $dName ?: 'Map',
+ 'unread_count' => (int)($rf['unreadItemCount'] ?? 0),
+ 'total_count' => (int)($rf['totalItemCount'] ?? 0),
+ 'is_inbox' => $isInbox,
+ ];
+ }
+
+ // Zorg dat Inbox altijd vooraan staat
+ $hasInbox = false;
+ foreach ($folders as $f) {
+ if (!empty($f['is_inbox'])) {
+ $hasInbox = true;
+ break;
+ }
+ }
+ if (!$hasInbox) {
+ array_unshift($folders, [
+ 'id' => 'inbox',
+ 'name' => 'Postvak IN',
+ 'unread_count' => 0,
+ 'total_count' => 0,
+ 'is_inbox' => true,
+ ]);
+ }
+
+ usort($folders, function($a, $b) {
+ if (!empty($a['is_inbox'])) return -1;
+ if (!empty($b['is_inbox'])) return 1;
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ $totalUnread = array_sum(array_column($folders, 'unread_count'));
+
+ $result = [
+ 'success' => true,
+ 'is_demo' => false,
+ 'folders' => $folders,
+ 'total_unread' => $totalUnread,
+ ];
+ fociz_cache_set($cacheKey, $result, 60); // 60 seconden cache
+ return $result;
+}
+
+/**
+ * Haalt berichten op voor een specifieke map van Microsoft Graph API (of retourneert demo data)
+ */
+function microsoft_fetch_folder_messages(int $userId, string $folderId = 'inbox', int $limit = 30, bool $unreadOnly = false, bool $forceRefresh = false): array {
+ $cleanFolderId = trim($folderId ?: 'inbox');
+ $cacheKey = "ms_folder_{$userId}_" . md5($cleanFolderId) . "_{$limit}_" . ($unreadOnly ? '1' : '0');
+ if (!$forceRefresh) {
+ $cached = fociz_cache_get($cacheKey);
+ if ($cached !== null && is_array($cached)) {
+ return $cached;
+ }
+ }
+
+ $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($cleanFolderId);
if ($unreadOnly) {
$demo = array_values(array_filter($demo, function($m) { return empty($m['is_read']); }));
}
return [
'success' => true,
'is_demo' => true,
+ 'folder_id' => $cleanFolderId,
'messages' => array_slice($demo, 0, $limit),
'unread_count' => count(array_filter($demo, function($m) { return empty($m['is_read']); })),
'total_count' => count($demo),
@@ -1068,6 +1412,7 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
return [
'success' => false,
'error' => 'Geen geldig Microsoft 365 token beschikbaar.',
+ 'folder_id' => $cleanFolderId,
'messages' => [],
'unread_count' => 0,
];
@@ -1083,7 +1428,8 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$queryParams['$filter'] = 'isRead eq false';
}
- $url = MS_GRAPH_BASE_URL . '/me/mailFolders/inbox/messages?' . http_build_query($queryParams);
+ $folderEndpoint = ($cleanFolderId === 'inbox') ? 'inbox' : urlencode($cleanFolderId);
+ $url = MS_GRAPH_BASE_URL . '/me/mailFolders/' . $folderEndpoint . '/messages?' . http_build_query($queryParams);
$res = fociz_http_request($url, [
'method' => 'GET',
'headers' => [
@@ -1097,14 +1443,15 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
if ($res['status'] !== 200 || !isset($res['json']['value'])) {
$graphError = $res['json']['error']['message'] ?? ($res['error'] ?? 'HTTP ' . $res['status']);
$graphCode = $res['json']['error']['code'] ?? '';
- error_log("Microsoft fetch inbox error: HTTP {$res['status']} [{$graphCode}] - {$graphError}");
+ error_log("Microsoft fetch folder messages error for {$cleanFolderId}: HTTP {$res['status']} [{$graphCode}] - {$graphError}");
- $demo = microsoft_get_mail_demo_data();
+ $demo = microsoft_get_mail_demo_data($cleanFolderId);
$needsReconsent = ($res['status'] === 403 || stripos($graphError, 'access') !== false || stripos($graphError, 'privilege') !== false || stripos($graphError, 'denied') !== false || stripos($graphError, 'consent') !== false);
return [
'success' => true,
'is_demo' => true,
+ 'folder_id' => $cleanFolderId,
'messages' => array_slice($demo, 0, $limit),
'unread_count' => count(array_filter($demo, function($m) { return empty($m['is_read']); })),
'total_count' => count($demo),
@@ -1153,6 +1500,7 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$messages[] = [
'id' => $msg['id'] ?? '',
+ 'folder_id' => $cleanFolderId,
'subject' => $msg['subject'] ?: '(Geen onderwerp)',
'body_preview' => $msg['bodyPreview'] ?? '',
'from_name' => $fromName,
@@ -1178,6 +1526,7 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$result = [
'success' => true,
'is_demo' => false,
+ 'folder_id' => $cleanFolderId,
'messages' => $messages,
'unread_count' => $unreadCount,
'total_count' => count($messages),
@@ -1186,6 +1535,13 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
return $result;
}
+/**
+ * Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data)
+ */
+function microsoft_fetch_inbox_messages(int $userId, int $limit = 30, bool $unreadOnly = false, bool $forceRefresh = false): array {
+ return microsoft_fetch_folder_messages($userId, 'inbox', $limit, $unreadOnly, $forceRefresh);
+}
+
/**
* Haalt de volledige inhoud en details van een enkel e-mailbericht op
*/
diff --git a/jira.php b/jira.php
index 130b401..85127b7 100644
--- a/jira.php
+++ b/jira.php
@@ -234,15 +234,42 @@ $rawProjectStatuses = $jiraData['project_statuses'] ?? [];
$projectStatusesData = [];
$allStatusesMap = [];
-foreach (array_keys($activeProjectKeys) as $pKey) {
- $pName = $coreProjects[$pKey]['name'] ?? ($allProjects[$pKey]['name'] ?? $pKey);
- $pColor = $coreProjects[$pKey]['color'] ?? 'slate';
+$allTargetKeys = array_keys($activeProjectKeys);
+if ($isCodeVisible('DX') && !in_array('DX', $allTargetKeys, true)) {
+ $allTargetKeys[] = 'DX';
+}
+if (!empty($queueFilters)) {
+ foreach ($queueFilters as $qf) {
+ $qCode = strtoupper(trim($qf['filter_code'] ?? ''));
+ if (!empty($qCode) && $isCodeVisible($qCode) && !in_array($qCode, $allTargetKeys, true)) {
+ $allTargetKeys[] = $qCode;
+ }
+ }
+}
+
+foreach ($allTargetKeys as $pKey) {
+ if ($pKey === 'DX') {
+ $pName = 'DX Koppelingen';
+ $pColor = 'amber';
+ } else {
+ $pName = $coreProjects[$pKey]['name'] ?? ($allProjects[$pKey]['name'] ?? $pKey);
+ $pColor = $coreProjects[$pKey]['color'] ?? 'slate';
+ if (!isset($coreProjects[$pKey]) && !empty($queueFilters)) {
+ foreach ($queueFilters as $qf) {
+ if (strtoupper(trim($qf['filter_code'] ?? '')) === $pKey && !empty($qf['filter_name'])) {
+ $pName = $qf['filter_name'];
+ break;
+ }
+ }
+ }
+ }
$pStatuses = [];
// 1. Statussen uit Jira API voor dit project
- if (!empty($rawProjectStatuses[$pKey])) {
- foreach ($rawProjectStatuses[$pKey] as $st) {
+ $apiPKey = ($pKey === 'DX') ? 'SD' : $pKey;
+ if (!empty($rawProjectStatuses[$apiPKey])) {
+ foreach ($rawProjectStatuses[$apiPKey] as $st) {
$sName = trim($st['name'] ?? '');
if (empty($sName)) continue;
$sKey = strtolower($sName);
@@ -256,11 +283,25 @@ foreach (array_keys($activeProjectKeys) as $pKey) {
// 2. Statussen die op de tickets van dit project voorkomen toevoegen/mergen
foreach ($allIssues as $iss) {
- $issPKey = strtoupper(trim($iss['project_key'] ?? ''));
- if (empty($issPKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) {
- $issPKey = strtoupper($m[1]);
+ $matches = false;
+ if ($pKey === 'DX') {
+ $matches = $issueMatchesQueue($iss, 'DX');
+ } else {
+ $issPKey = strtoupper(trim($iss['project_key'] ?? ''));
+ if (empty($issPKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) {
+ $issPKey = strtoupper($m[1]);
+ }
+ $matches = ($issPKey === $pKey);
+ if (!$matches && !empty($queueFilters)) {
+ foreach ($queueFilters as $qf) {
+ if (strtoupper(trim($qf['filter_code'] ?? '')) === $pKey) {
+ $matches = $issueMatchesQueue($iss, $pKey);
+ break;
+ }
+ }
+ }
}
- if ($issPKey === $pKey && !empty($iss['status'])) {
+ if ($matches && !empty($iss['status'])) {
$sName = trim($iss['status']);
$sKey = strtolower($sName);
if (!isset($pStatuses[$sKey])) {
@@ -1114,53 +1155,42 @@ function toggleCurrentProjectOnlyAssigned() {
}
function updateAssignedPillState() {
- const targetKey = (currentProject || 'ALL').toUpperCase().trim();
- const isAssigned = projectOnlyAssigned.has(targetKey) || (currentProject === '' && projectOnlyAssigned.has('ALL'));
- const btn = document.getElementById('btnToggleAssignedToMePill');
- const badge = document.getElementById('badgeAssignedToMeCount');
- const label = document.getElementById('labelAssignedToMePill');
- if (!btn) return;
-
- if (isAssigned) {
- btn.className = "flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold border border-indigo-600 bg-indigo-600 text-white shadow-xs flex-shrink-0 cursor-pointer";
- if (badge) {
- badge.className = "px-1.5 py-0.5 rounded-full text-[10px] font-black bg-white/20 text-white";
- }
- } else {
- btn.className = "flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-semibold border transition cursor-pointer shadow-2xs flex-shrink-0 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:border-slate-300 dark:hover:border-slate-600";
- if (badge) {
- badge.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300";
- }
- }
-
- if (label) {
- if (currentProject) {
- label.innerHTML = `Toegewezen aan mij (${escapeHtml(currentProject)}) `;
- } else {
- label.innerHTML = `Toegewezen aan mij (uitvoerder) `;
- }
- }
-
const all = getDeduplicatedIssues();
- let targetList = all;
- if (currentProject) {
- targetList = all.filter(iss => isIssueInQueue(iss, currentProject));
+ const myCountAll = all.filter(iss => isIssueAssignedToMe(iss)).length;
+
+ // Update 'Alle Projecten' dropdown
+ const chkAll = document.getElementById('chkOnlyAssigned_all');
+ if (chkAll) {
+ chkAll.checked = projectOnlyAssigned.has('ALL');
}
- const myCount = targetList.filter(iss => isIssueAssignedToMe(iss)).length;
- if (badge) {
- badge.innerText = myCount;
+ const cntAllEl = document.getElementById('projAssignedCount_all');
+ if (cntAllEl) {
+ cntAllEl.innerText = myCountAll;
+ }
+ const btnMijnAll = document.getElementById('btnDropdownMijn_all');
+ if (btnMijnAll) {
+ if (projectOnlyAssigned.has('ALL')) {
+ btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
+ } else {
+ btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
+ }
}
+ // Update per-project dropdowns
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
const chk = document.getElementById(`chkOnlyAssigned_${pKey}`);
if (chk) {
chk.checked = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
}
- const cCnt = all.filter(iss => {
+ let projIssues = all.filter(iss => {
const k = getIssueProjectKey(iss);
- return (k === pKey || (!k && (iss.project || '').toUpperCase().includes(pKey))) && isIssueAssignedToMe(iss);
- }).length;
+ return (k === pKey || (!k && (iss.project || '').toUpperCase().includes(pKey)));
+ });
+ if (pKey === 'DX' || !['SD', 'IN', 'AAN', 'GEN'].includes(pKey)) {
+ projIssues = all.filter(iss => isIssueInQueue(iss, pKey));
+ }
+ const cCnt = projIssues.filter(iss => isIssueAssignedToMe(iss)).length;
const cntEl = document.getElementById(`projAssignedCount_${pKey}`);
if (cntEl) cntEl.innerText = cCnt;
@@ -1174,6 +1204,10 @@ function updateAssignedPillState() {
}
}
});
+
+ if (typeof updateProjectPills === 'function') {
+ updateProjectPills();
+ }
}
// Hulpfunctie voor deduplicatie van tickets (assigned + team)
@@ -1327,258 +1361,338 @@ function isIssueStatusSelected(iss) {
if (compound && selectedStatuses.has(compound)) return true;
if (selectedStatuses.has(issStatus)) return true;
+ if (typeof isIssueInQueue === 'function' && isIssueInQueue(iss, 'DX') && selectedStatuses.has(`dx::${issStatus}`)) return true;
return false;
}
-// Renders the per-project status dropdowns into #projectStatusDropdownsWrapper
-function renderStatusCheckboxes() {
- const container = document.getElementById('projectStatusDropdownsWrapper');
- if (!container) return;
+// Renders each project's status dropdown directly inside its anchored container (#projectDropdownMenu_${pKey})
+function buildStatusCheckboxRowHtml(pKey, st, sIdx, prefix = '') {
+ const sNorm = (st.name || '').toLowerCase().trim();
+ const pKeyUpper = (pKey || '').toUpperCase().trim();
+ const pKeyLower = (pKey || '').toLowerCase().trim();
+ const compoundVal = `${pKeyLower}::${sNorm}`;
+ const cat = (st.status_category || '').toLowerCase().trim();
+ const isDone = (cat === 'done' || isDoneStatus(sNorm));
- if (!projectStatusesData || projectStatusesData.length === 0) {
- container.innerHTML = 'Geen project statussen gevonden ';
- return;
+ const isChecked = selectedStatuses.has(compoundVal) || selectedStatuses.has(sNorm);
+ if (isChecked) {
+ selectedStatuses.add(compoundVal);
}
+ let dotColor = 'bg-blue-500';
+ if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) {
+ dotColor = 'bg-slate-400';
+ } else if (isDone) {
+ dotColor = 'bg-emerald-500';
+ } else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) {
+ dotColor = 'bg-amber-500';
+ }
+
+ const safeKey = `${pKeyUpper}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`;
+ const safeId = `chkStatus_${prefix}${pKeyUpper}_${sIdx}`;
+
+ return `
+
+
+
+
+ ${escapeHtml(st.name)}
+
+ 0
+
+ `;
+}
+
+function renderStatusCheckboxes() {
const colorClasses = {
sky: {
badge: 'bg-sky-100 text-sky-800 dark:bg-sky-950/70 dark:text-sky-300 border border-sky-300 dark:border-sky-800',
- hdrBg: 'bg-sky-50/70 dark:bg-sky-950/40'
+ hdrBg: 'bg-sky-50/80 dark:bg-sky-950/40'
},
amber: {
badge: 'bg-amber-100 text-amber-800 dark:bg-amber-950/70 dark:text-amber-300 border border-amber-300 dark:border-amber-800',
- hdrBg: 'bg-amber-50/70 dark:bg-amber-950/40'
+ hdrBg: 'bg-amber-50/80 dark:bg-amber-950/40'
},
emerald: {
badge: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/70 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800',
- hdrBg: 'bg-emerald-50/70 dark:bg-emerald-950/40'
+ hdrBg: 'bg-emerald-50/80 dark:bg-emerald-950/40'
},
indigo: {
badge: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-300 dark:border-indigo-800',
- hdrBg: 'bg-indigo-50/70 dark:bg-indigo-950/40'
+ hdrBg: 'bg-indigo-50/80 dark:bg-indigo-950/40'
},
slate: {
badge: 'bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-200 border border-slate-300 dark:border-slate-700',
- hdrBg: 'bg-slate-50 dark:bg-slate-800/40'
+ hdrBg: 'bg-slate-50/80 dark:bg-slate-800/40'
}
};
- let html = '';
- projectStatusesData.forEach((proj, pIdx) => {
+ // 1. Dropdown voor "Alle Projecten" (#projectDropdownMenu_all)
+ const menuAll = document.getElementById('projectDropdownMenu_all');
+ if (menuAll) {
+ let allStatusesGroupedHtml = '';
+ (projectStatusesData || []).forEach(proj => {
+ const pKey = (proj.key || '').toUpperCase().trim();
+ const pColor = proj.color || 'slate';
+ const styles = colorClasses[pColor] || colorClasses.slate;
+ const statuses = proj.statuses || [];
+ if (statuses.length === 0) return;
+
+ let rows = '';
+ statuses.forEach((st, sIdx) => {
+ rows += buildStatusCheckboxRowHtml(pKey, st, sIdx, 'all_');
+ });
+
+ allStatusesGroupedHtml += `
+
+
+
+ ${escapeHtml(pKey)}
+ ${escapeHtml(proj.name || pKey)}
+
+
+ Alles
+ •
+ Geen
+
+
+
+ ${rows}
+
+
+ `;
+ });
+
+ menuAll.innerHTML = `
+
+
+
+ Alle Projecten
+ (0)
+
+
+
+ Alles
+
+ •
+
+ Actief
+
+ •
+
+ Mijn
+
+ •
+
+ Geen
+
+
+
+
+
+
+
+
+
+
+
+
Toegewezen aan mij (uitvoerder)
+
Alleen mijn taken in alle projecten
+
+
+
+ 0
+
+
+
+
+
+ `;
+ }
+
+ // 2. Dropdown per afzonderlijk project (#projectDropdownMenu_${pKey})
+ (projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
- const pKeyLower = pKey.toLowerCase();
+ const menuEl = document.getElementById(`projectDropdownMenu_${pKey}`);
+ if (!menuEl) return;
+
const pColor = proj.color || 'slate';
const styles = colorClasses[pColor] || colorClasses.slate;
const statuses = proj.statuses || [];
let statusRowsHtml = '';
statuses.forEach((st, sIdx) => {
- const sNorm = (st.name || '').toLowerCase().trim();
- const compoundVal = `${pKeyLower}::${sNorm}`;
- const cat = (st.status_category || '').toLowerCase().trim();
- const isDone = (cat === 'done' || isDoneStatus(sNorm));
-
- const isChecked = selectedStatuses.has(compoundVal) || selectedStatuses.has(sNorm);
- if (isChecked) {
- selectedStatuses.add(compoundVal);
- }
-
- let dotColor = 'bg-blue-500';
- if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) {
- dotColor = 'bg-slate-400';
- } else if (isDone) {
- dotColor = 'bg-emerald-500';
- } else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) {
- dotColor = 'bg-amber-500';
- }
-
- const safeKey = `${pKey}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`;
- const safeId = `chkStatus_${pKey}_${sIdx}`;
-
- statusRowsHtml += `
-
-
-
-
- ${escapeHtml(st.name)}
-
- 0
-
- `;
+ statusRowsHtml += buildStatusCheckboxRowHtml(pKey, st, sIdx, '');
});
- html += `
-
-
+ const isAssigned = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
+
+ menuEl.innerHTML = `
+
+
${escapeHtml(pKey)}
-
Alle
-
-
-
-
-
-
-
- ${escapeHtml(pKey)}
- ${escapeHtml(proj.name || pKey)}
- (0)
-
-
-
- Alles
-
- •
-
- Actief
-
- •
-
- Mijn
-
- •
-
- Geen
-
-
-
-
-
-
-
-
-
-
-
-
Toegewezen aan mij (uitvoerder)
-
Alleen mijn taken in ${escapeHtml(pKey)}
-
-
-
- 0
-
-
-
-
-
- ${statusRowsHtml}
-
+
${escapeHtml(proj.name || pKey)}
+
(0)
+
+
+ Alles
+
+ •
+
+ Actief
+
+ •
+
+ Mijn
+
+ •
+
+ Geen
+
+
+
+
+
+
+
+
+
+
+
+
Toegewezen aan mij (uitvoerder)
+
Alleen mijn taken in ${escapeHtml(pKey)}
+
+
+
+ 0
+
+
+
+
+
`;
});
- container.innerHTML = html;
if (typeof lucide !== 'undefined') lucide.createIcons();
updateStatusCounts();
updateStatusDropdownLabel();
- updateProjectStatusDropdownVisibility();
- updateAssignedPillState();
+ updateProjectPills();
}
-function updateProjectStatusDropdownVisibility() {
- const items = document.querySelectorAll('.project-status-dropdown-item');
- const curNorm = (currentProject || '').toUpperCase().trim();
- items.forEach(item => {
- const pKey = (item.getAttribute('data-project') || '').toUpperCase().trim();
- if (!curNorm) {
- // "Alle Projecten": toon alle toegewezen project dropdowns
- item.classList.remove('hidden');
- } else if (curNorm === pKey) {
- item.classList.remove('hidden');
- } else if (curNorm === 'DX' && pKey === 'SD') {
- item.classList.remove('hidden');
- } else {
- item.classList.add('hidden');
- }
- });
-}
-
-function toggleProjectStatusDropdown(projectKey, force) {
- const targetKey = (projectKey || '').toUpperCase().trim();
- const menu = document.getElementById(`statusDropdownMenu_${targetKey}`);
- const chevron = document.getElementById(`statusDropdownChevron_${targetKey}`);
- const btn = document.getElementById(`statusDropdownBtn_${targetKey}`);
+function toggleProjectDropdown(projectKey, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ const targetKey = (projectKey || 'all').trim();
+ const menu = document.getElementById(`projectDropdownMenu_${targetKey}`);
+ const chevron = document.getElementById(`projChevron_${targetKey}`);
+ const btnArrow = document.getElementById(`btnProjectArrow_${targetKey}`);
if (!menu) return;
- const willOpen = (typeof force === 'boolean') ? force : menu.classList.contains('hidden');
+ const isOpen = !menu.classList.contains('hidden');
+ closeAllProjectDropdowns();
- // Sluit alle andere geopende status dropdowns
- closeAllProjectStatusDropdowns(targetKey);
-
- if (willOpen) {
+ if (!isOpen) {
menu.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
- if (btn) btn.setAttribute('aria-expanded', 'true');
+ if (btnArrow) btnArrow.setAttribute('aria-expanded', 'true');
+
+ // Check of menu over de rechter rand van het scherm loopt
+ const rect = menu.getBoundingClientRect();
+ if (rect.right > (window.innerWidth || document.documentElement.clientWidth) - 16) {
+ menu.classList.remove('left-0');
+ menu.classList.add('right-0');
+ } else {
+ menu.classList.remove('right-0');
+ menu.classList.add('left-0');
+ }
+
updateStatusCounts();
- } else {
- menu.classList.add('hidden');
- if (chevron) chevron.classList.remove('rotate-180');
- if (btn) btn.setAttribute('aria-expanded', 'false');
}
}
-function closeAllProjectStatusDropdowns(exceptKey = '') {
- const exceptNorm = (exceptKey || '').toUpperCase().trim();
- (projectStatusesData || []).forEach(proj => {
- const pKey = (proj.key || '').toUpperCase().trim();
- if (pKey === exceptNorm) return;
- const menu = document.getElementById(`statusDropdownMenu_${pKey}`);
- const chevron = document.getElementById(`statusDropdownChevron_${pKey}`);
- const btn = document.getElementById(`statusDropdownBtn_${pKey}`);
- if (menu && !menu.classList.contains('hidden')) {
+function closeAllProjectDropdowns(exceptKey = '') {
+ const exceptNorm = (exceptKey || '').trim();
+ document.querySelectorAll('.project-dropdown-menu').forEach(menu => {
+ const mKey = menu.id.replace('projectDropdownMenu_', '');
+ if (mKey === exceptNorm) return;
+ if (!menu.classList.contains('hidden')) {
menu.classList.add('hidden');
+ const chevron = document.getElementById(`projChevron_${mKey}`);
if (chevron) chevron.classList.remove('rotate-180');
- if (btn) btn.setAttribute('aria-expanded', 'false');
+ const btnArrow = document.getElementById(`btnProjectArrow_${mKey}`);
+ if (btnArrow) btnArrow.setAttribute('aria-expanded', 'false');
}
});
}
+// Backwards compatibility aliases
+function toggleProjectStatusDropdown(projectKey, force) { toggleProjectDropdown(projectKey); }
+function closeAllProjectStatusDropdowns(exceptKey) { closeAllProjectDropdowns(exceptKey); }
+function toggleStatusDropdown(force) { toggleProjectDropdown(currentProject || 'all'); }
+function updateProjectStatusDropdownVisibility() {}
+
// Sluit dropdowns bij klikken buiten dropdowns
document.addEventListener('click', (e) => {
- const isInside = e.target.closest('.project-status-dropdown-item');
- if (!isInside) {
- closeAllProjectStatusDropdowns();
+ if (!e.target.closest('.project-pill-container')) {
+ closeAllProjectDropdowns();
}
});
-function toggleStatusDropdown(force) {
- if (currentProject) {
- toggleProjectStatusDropdown(currentProject, force);
- } else if (projectStatusesData && projectStatusesData.length > 0) {
- toggleProjectStatusDropdown(projectStatusesData[0].key, force);
- }
-}
-
function updateStatusCounts() {
const all = getDeduplicatedIssues();
+ // 1. Totaal en Toegewezen aan mij per project updaten in de dropdowns
+ const allAssignedCount = all.filter(iss => isIssueAssignedToMe(iss)).length;
+ const elAllAssigned = document.getElementById('projAssignedCount_all');
+ if (elAllAssigned) elAllAssigned.innerText = allAssignedCount;
+
+ const elAllTotal = document.getElementById('projDropdownTotal_all');
+ if (elAllTotal) elAllTotal.innerText = `(${all.length})`;
+
+ const chkAll = document.getElementById('chkOnlyAssigned_all');
+ if (chkAll) chkAll.checked = projectOnlyAssigned.has('ALL');
+
+ const btnMijnAll = document.getElementById('btnDropdownMijn_all');
+ if (btnMijnAll) {
+ if (projectOnlyAssigned.has('ALL')) {
+ btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
+ } else {
+ btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
+ }
+ }
+
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
+
let projIssues = all.filter(iss => {
const k = getIssueProjectKey(iss);
const pn = (iss.project || '').toUpperCase().trim();
@@ -1586,60 +1700,81 @@ function updateStatusCounts() {
return pn.includes(pKey);
});
- // Project filter meenemen indien actief
- if (currentProject) {
- projIssues = projIssues.filter(iss => isIssueInQueue(iss, currentProject));
+ if (pKey === 'DX' || !['SD', 'IN', 'AAN', 'GEN'].includes(pKey)) {
+ projIssues = all.filter(iss => isIssueInQueue(iss, pKey));
}
- // Toegewezen aan mij filter per project meenemen in de status tellingen
- if (isProjectOnlyAssignedActive(currentProject, pKey)) {
- projIssues = projIssues.filter(iss => isIssueAssignedToMe(iss));
+ const projTotalCount = projIssues.length;
+ const projAssignedCount = projIssues.filter(iss => isIssueAssignedToMe(iss)).length;
+
+ const elDropdownTotal = document.getElementById(`projDropdownTotal_${pKey}`);
+ if (elDropdownTotal) elDropdownTotal.innerText = `(${projTotalCount})`;
+
+ const elAssignedCount = document.getElementById(`projAssignedCount_${pKey}`);
+ if (elAssignedCount) elAssignedCount.innerText = projAssignedCount;
+
+ const chk = document.getElementById(`chkOnlyAssigned_${pKey}`);
+ if (chk) chk.checked = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
+
+ const btnMijn = document.getElementById(`btnDropdownMijn_${pKey}`);
+ if (btnMijn) {
+ if (projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL')) {
+ btnMijn.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
+ } else {
+ btnMijn.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
+ }
}
- const projTotalEl = document.getElementById(`projTotal_${pKey}`);
- if (projTotalEl) {
- projTotalEl.innerText = `(${projIssues.length})`;
+ // Tellingen per status
+ let statusBaseList = projIssues;
+ if (projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL')) {
+ statusBaseList = statusBaseList.filter(iss => isIssueAssignedToMe(iss));
}
(proj.statuses || []).forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
const safeKey = `${pKey}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`;
- const el = document.getElementById(`statusCount_${safeKey}`);
- if (el) {
- const cnt = projIssues.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length;
- el.innerText = cnt;
- }
+ const cnt = statusBaseList.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length;
+
+ const countEls = document.querySelectorAll(`.status-count-${safeKey}`);
+ countEls.forEach(el => el.innerText = cnt);
+ const singleEl = document.getElementById(`statusCount_${safeKey}`);
+ if (singleEl) singleEl.innerText = cnt;
});
});
- updateAssignedPillState();
+ updateProjectCounts();
+ updateProjectPills();
}
-function onStatusCheckboxChange(projectKey) {
- const pNorm = (projectKey || '').toUpperCase().trim();
- const pKeyLower = pNorm.toLowerCase();
- const chks = document.querySelectorAll(`.status-checkbox-${pNorm}`);
-
- chks.forEach(chk => {
- const val = chk.value.toLowerCase().trim();
- if (chk.checked) {
- selectedStatuses.add(val);
+function onStatusCheckboxChange(projectKey, changedVal, isChecked) {
+ if (changedVal) {
+ const normVal = changedVal.toLowerCase().trim();
+ if (isChecked) {
+ selectedStatuses.add(normVal);
} else {
- selectedStatuses.delete(val);
+ selectedStatuses.delete(normVal);
}
- });
+ // Sync checkboxes that have this same value across dropdowns
+ const chks = document.querySelectorAll('.status-checkbox');
+ chks.forEach(chk => {
+ if (chk.value && chk.value.toLowerCase().trim() === normVal) {
+ chk.checked = isChecked;
+ }
+ });
+ }
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
+ updateStatusCounts();
applyFilters();
}
function selectProjectStatuses(projectKey, selectAll) {
const targetKey = (projectKey || '').toUpperCase().trim();
- const pKeyLower = targetKey.toLowerCase();
const chks = document.querySelectorAll(`.status-checkbox[data-project="${targetKey}"]`);
chks.forEach(chk => {
@@ -1657,6 +1792,7 @@ function selectProjectStatuses(projectKey, selectAll) {
} catch (e) {}
updateStatusDropdownLabel();
+ updateStatusCounts();
applyFilters();
}
@@ -1680,6 +1816,29 @@ function selectProjectActiveStatuses(projectKey) {
} catch (e) {}
updateStatusDropdownLabel();
+ updateStatusCounts();
+ applyFilters();
+}
+
+function selectAllActiveStatuses() {
+ const chks = document.querySelectorAll('.status-checkbox');
+ chks.forEach(chk => {
+ const isDone = chk.getAttribute('data-is-done') === '1';
+ chk.checked = !isDone;
+ const val = chk.value.toLowerCase().trim();
+ if (!isDone) {
+ selectedStatuses.add(val);
+ } else {
+ selectedStatuses.delete(val);
+ }
+ });
+
+ try {
+ localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
+ } catch (e) {}
+
+ updateStatusDropdownLabel();
+ updateStatusCounts();
applyFilters();
}
@@ -1698,91 +1857,29 @@ function selectAllStatuses(selectAll) {
} catch (e) {}
updateStatusDropdownLabel();
+ updateStatusCounts();
applyFilters();
}
function updateStatusDropdownLabel() {
let hasAnyCustomFilter = false;
- (projectStatusesData || []).forEach(proj => {
- const pKey = (proj.key || '').toUpperCase().trim();
- const pKeyLower = pKey.toLowerCase();
- const labelEl = document.getElementById(`statusDropdownLabel_${pKey}`);
- const badgeEl = document.getElementById(`statusDropdownBadge_${pKey}`);
- if (!labelEl) return;
+ if (currentProject !== '' || projectOnlyAssigned.size > 0 || (document.getElementById('jiraSearchInput')?.value || '').trim().length > 0) {
+ hasAnyCustomFilter = true;
+ }
- const statuses = proj.statuses || [];
- const total = statuses.length;
- let selectedCount = 0;
- let activeTotal = 0;
- let activeSelected = 0;
- let singleStatusName = '';
-
- statuses.forEach(st => {
- const sNorm = (st.name || '').toLowerCase().trim();
- const compound = `${pKeyLower}::${sNorm}`;
- const cat = (st.status_category || '').toLowerCase().trim();
- const isDone = (cat === 'done' || isDoneStatus(sNorm));
-
- if (!isDone) activeTotal++;
-
- if (selectedStatuses.has(compound) || selectedStatuses.has(sNorm)) {
- selectedCount++;
- singleStatusName = st.name;
- if (!isDone) activeSelected++;
- }
- });
-
- if (selectedCount < total) {
- hasAnyCustomFilter = true;
- }
-
- if (selectedCount === total || (total === 0 && selectedCount === 0)) {
- labelEl.innerText = "Alle statussen";
- if (badgeEl) {
- badgeEl.innerText = total;
- badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400";
- }
- } else if (selectedCount === 0) {
- labelEl.innerText = "Geen";
- if (badgeEl) {
- badgeEl.innerText = "0";
- badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-100 dark:bg-rose-950/60 text-rose-700 dark:text-rose-300";
- }
- } else if (selectedCount === activeTotal && activeSelected === activeTotal && activeTotal > 0) {
- labelEl.innerText = "Actief";
- if (badgeEl) {
- badgeEl.innerText = `${activeSelected}/${total}`;
- badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-100 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-300";
- }
- } else if (selectedCount === 1) {
- labelEl.innerText = singleStatusName;
- if (badgeEl) {
- badgeEl.innerText = `1/${total}`;
- badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
- }
- } else {
- labelEl.innerText = `${selectedCount} statussen`;
- if (badgeEl) {
- badgeEl.innerText = `${selectedCount}/${total}`;
- badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
- }
- }
-
- // Toon indicator indien Toegewezen aan mij actief is voor dit project
- const isAssignedOnly = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
- if (isAssignedOnly) {
- hasAnyCustomFilter = true;
- labelEl.innerText += " • Mijn";
- }
- });
+ const total = getTotalStatusCount();
+ if (selectedStatuses.size > 0 && selectedStatuses.size < total) {
+ hasAnyCustomFilter = true;
+ }
const btnReset = document.getElementById('btnResetFiltersInline');
if (btnReset) {
- const isCustom = hasAnyCustomFilter || currentProject !== '' || projectOnlyAssigned.size > 0 || document.getElementById('jiraSearchInput')?.value;
- if (isCustom) btnReset.classList.remove('hidden');
+ if (hasAnyCustomFilter) btnReset.classList.remove('hidden');
else btnReset.classList.add('hidden');
}
+
+ updateActiveFilterBadge();
}
function initStatusSelection() {
@@ -1844,49 +1941,119 @@ function initStatusSelection() {
// Project selectie via pill tabs
function selectProject(proj) {
currentProject = (proj || '').trim();
- closeAllProjectStatusDropdowns();
+ closeAllProjectDropdowns();
updateProjectPills();
- updateProjectStatusDropdownVisibility();
- updateAssignedPillState();
updateStatusCounts();
applyFilters();
}
function updateProjectPills() {
- const pills = document.querySelectorAll('.project-pill');
+ const containers = document.querySelectorAll('.project-pill-container');
const curNorm = (currentProject || '').toUpperCase().trim();
- pills.forEach(p => {
- const pKey = (p.getAttribute('data-project') || '').toUpperCase().trim();
+
+ containers.forEach(container => {
+ const pKey = (container.getAttribute('data-project') || '').toUpperCase().trim();
const isSelected = (pKey === curNorm);
-
+ const btn = container.querySelector('.project-pill-btn');
+ const arrow = container.querySelector('.project-pill-arrow');
+ const dot = container.querySelector(`[id^="projFilterActiveDot_"]`);
+
+ // Check of filters actief zijn voor dit project
+ let hasFilter = false;
if (pKey === '') {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900 shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0";
- } else if (pKey === 'SD') {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-sky-600 text-white shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-sky-50 dark:bg-sky-950/40 text-sky-800 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 border border-sky-300 dark:border-sky-800 flex-shrink-0";
- } else if (pKey === 'IN') {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 flex-shrink-0";
- } else if (pKey === 'AAN') {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-600 text-white shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 border border-emerald-300 dark:border-emerald-800 flex-shrink-0";
- } else if (pKey === 'GEN') {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 border border-indigo-300 dark:border-indigo-800 flex-shrink-0";
- } else if (pKey === 'DX') {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 flex-shrink-0";
+ const isAssigned = projectOnlyAssigned.has('ALL');
+ const total = getTotalStatusCount();
+ const isCustomStatus = (selectedStatuses.size > 0 && selectedStatuses.size < total);
+ hasFilter = isAssigned || isCustomStatus;
} else {
- p.className = isSelected
- ? "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs flex-shrink-0"
- : "project-pill flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0";
+ const isAssigned = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
+ const projObj = (projectStatusesData || []).find(p => (p.key || '').toUpperCase().trim() === pKey);
+ let isCustomStatus = false;
+ if (projObj && (projObj.statuses || []).length > 0) {
+ const pKeyLower = pKey.toLowerCase();
+ const totalProjStatuses = projObj.statuses.length;
+ let checkedCount = 0;
+ projObj.statuses.forEach(st => {
+ const sNorm = (st.name || '').toLowerCase().trim();
+ const compound = `${pKeyLower}::${sNorm}`;
+ if (selectedStatuses.has(compound) || selectedStatuses.has(sNorm)) {
+ checkedCount++;
+ }
+ });
+ if (checkedCount < totalProjStatuses) {
+ isCustomStatus = true;
+ }
+ }
+ hasFilter = isAssigned || isCustomStatus;
+ }
+
+ if (dot) {
+ if (hasFilter) {
+ dot.classList.remove('hidden');
+ } else {
+ dot.classList.add('hidden');
+ }
+ }
+
+ if (!btn || !arrow) return;
+
+ // Pas styling toe op de linker knop en rechter pijltjesknop
+ if (pKey === '') {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-white/20 dark:border-slate-800 bg-slate-900 dark:bg-white text-white dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-slate-100 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-slate-200 dark:border-slate-700 border-l-slate-300 dark:border-l-slate-600 bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition cursor-pointer relative";
+ }
+ } else if (pKey === 'SD') {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-sky-600 text-white shadow-xs";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-sky-500 bg-sky-600 text-white hover:bg-sky-700 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-sky-50 dark:bg-sky-950/40 text-sky-800 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 border border-sky-300 dark:border-sky-800 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-sky-300 dark:border-sky-800 border-l-sky-200 dark:border-l-sky-800 bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 transition cursor-pointer relative";
+ }
+ } else if (pKey === 'GEN') {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-indigo-500 bg-indigo-600 text-white hover:bg-indigo-700 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 border border-indigo-300 dark:border-indigo-800 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-indigo-300 dark:border-indigo-800 border-l-indigo-200 dark:border-l-indigo-800 bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 transition cursor-pointer relative";
+ }
+ } else if (pKey === 'IN') {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-amber-500 bg-amber-600 text-white hover:bg-amber-700 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-amber-300 dark:border-amber-800 border-l-amber-200 dark:border-l-amber-800 bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 transition cursor-pointer relative";
+ }
+ } else if (pKey === 'AAN') {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-emerald-600 text-white shadow-xs";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-emerald-500 bg-emerald-600 text-white hover:bg-emerald-700 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 border border-emerald-300 dark:border-emerald-800 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-emerald-300 dark:border-emerald-800 border-l-emerald-200 dark:border-l-emerald-800 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 transition cursor-pointer relative";
+ }
+ } else if (pKey === 'DX') {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-amber-500 bg-amber-600 text-white hover:bg-amber-700 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-amber-300 dark:border-amber-800 border-l-amber-200 dark:border-l-amber-800 bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 transition cursor-pointer relative";
+ }
+ } else {
+ if (isSelected) {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-indigo-500 bg-indigo-600 text-white hover:bg-indigo-700 transition cursor-pointer relative";
+ } else {
+ btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 border-r-0";
+ arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-slate-200 dark:border-slate-700 border-l-slate-200 dark:border-l-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition cursor-pointer relative";
+ }
}
});
}
@@ -2170,8 +2337,8 @@ function updateProjectCounts() {
elAll.innerText = allFiltered.length;
}
- const pills = document.querySelectorAll('.project-pill');
- pills.forEach(p => {
+ const containers = document.querySelectorAll('.project-pill-container, .project-pill');
+ containers.forEach(p => {
const pKey = p.getAttribute('data-project');
if (!pKey) return;
const el = document.getElementById(`projCount_${pKey}`);
diff --git a/mail.php b/mail.php
index 58c390d..e445207 100644
--- a/mail.php
+++ b/mail.php
@@ -14,15 +14,22 @@ $isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$db = get_db_connection();
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
-// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
+// Haal alle beschikbare mailmappen op (exclusief SPAM/Junk, Prullenbak/Deleted, Outbox, Drafts)
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
+$foldersData = microsoft_fetch_mail_folders($userId, $forceRefresh);
+$mailFolders = $foldersData['folders'] ?? [];
+$totalUnreadCount = (int)($foldersData['total_unread'] ?? 0);
+
+// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
$inboxData = microsoft_fetch_inbox_messages($userId, 30, false, $forceRefresh);
$messages = $inboxData['messages'] ?? [];
-$isDemo = !empty($inboxData['is_demo']);
-$unreadCount = (int)($inboxData['unread_count'] ?? 0);
-$warning = $inboxData['warning'] ?? '';
+$isDemo = !empty($inboxData['is_demo']) || !empty($foldersData['is_demo']);
+$warning = $inboxData['warning'] ?? ($foldersData['warning'] ?? '');
$needsReconsent = !empty($inboxData['needs_reconsent']);
+// Bereken actuele ongelezen aantal: neem het hoogste van total_unread over mappen en inbox unread_count
+$unreadCount = max($totalUnreadCount, (int)($inboxData['unread_count'] ?? 0));
+
// Controleer status
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmtInt->execute([$userId]);
@@ -31,6 +38,7 @@ $isOutlookConfigured = !empty($outlookInt['access_token']) && empty($outlookInt[
$isLiveMail = $isOutlookConfigured && !$isDemo;
$jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
+$jsonFolders = json_encode($mailFolders, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
@@ -143,15 +151,15 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
-
+
- Inbox Berichten (= count($messages) ?> )
+ Inbox & Mappen (= count($messages) ?> )
Sorteer: Nieuwste eerst
-
@@ -527,7 +535,12 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q