Compare commits

..

2 Commits

10 changed files with 1397 additions and 949 deletions
+12 -6
View File
@@ -56,13 +56,19 @@ In het bestand **`includes/env_badge.php`**:
- Voortgangsbalken (0 - 100%) en deadlines. - Voortgangsbalken (0 - 100%) en deadlines.
- Gekoppeld aan de oorspronkelijke brainstormkaarten en optioneel Jira Issue Keys (bijv. `FOC-101`). - Gekoppeld aan de oorspronkelijke brainstormkaarten en optioneel Jira Issue Keys (bijv. `FOC-101`).
4. **Integraties Hub (`/integrations.php`)**: 4. **Mijn Profiel & Persoonlijke Koppelingen (`/profile.php`)**:
- **Microsoft Outlook**: Synchroniseert agenda-afspraken direct naar de dagplanning. - **Microsoft Outlook & Teams**: Koppel met 1 klik uw persoonlijke werkagenda voor dagelijkse afsprakensynchronisatie en directe Teams meeting links.
- **Microsoft Teams**: Directe deelnamelinks naar online meetings en kanaalnotificaties. - **Atlassian Jira**: Koppel uw persoonlijke Atlassian API token om aan u toegewezen tickets in te zien en in te plannen.
- **Atlassian Jira**: Issues synchroniseren en direct met één klik inplannen als dagtaak. - **Browser Push Meldingen**: Configureer persoonlijke waarschuwingen (vooraankondigingstijd, herhalingsfrequentie, audio chime en live test).
- **Sandbox / Demo Modus**: Schakel met één vinkje tussen live productie en interactieve testdata. - **Account & Beveiliging**: Wachtwoord wijzigen en 2FA beveiligingsstatus.
5. **Team & Invites Beheer (`/users.php` - Admin only)**: 5. **Systeembeheer & Integraties Portaal (`/admin.php` - Admin only)**:
- Centrale Microsoft 365 Azure App Registratie (Client ID, Client Secret, Tenant ID, Admin Consent verlenen).
- Standaard Jira bedrijfsdomein voor alle teamleden.
- Systeem & Team Monitor (statistieken over actieve leden, agenda koppelingen en API-status).
- Extensibiliteit Hub voor toekomstige modules (AI Co-Pilot, werkdag standaarden).
6. **Team & Invites Beheer (`/users.php` - Admin only)**:
- Genereer beveiligde uitnodigingslinks voor collega's met kopieerknop. - Genereer beveiligde uitnodigingslinks voor collega's met kopieerknop.
- Overzicht van actieve gebruikers, rollen en 2FA-status. - Overzicht van actieve gebruikers, rollen en 2FA-status.
+593
View File
@@ -0,0 +1,593 @@
<?php
/**
* Fociz Systeembeheer & Centrale Integraties Portaal (Uitsluitend voor Beheerders)
*/
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
// Strikte autorisatie: alleen gebruikers met de rol 'ADMIN' hebben toegang
$adminUser = require_admin();
$currentUser = $adminUser;
$userId = (int)$adminUser['id'];
$db = get_db_connection();
$message = null;
$messageType = 'success';
if (!empty($_GET['success'])) {
$message = $_GET['success'];
$messageType = 'success';
} elseif (!empty($_GET['error'])) {
$message = $_GET['error'];
$messageType = 'error';
}
// POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// 1. Centrale Azure App Registratie opslaan
if ($action === 'save_azure') {
$clientId = trim($_POST['client_id'] ?? '');
$clientSecret = trim($_POST['client_secret'] ?? '');
$tenantId = trim($_POST['tenant_id'] ?? 'common');
if (empty($tenantId)) $tenantId = 'common';
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($clientSecret === '' || $clientSecret === '••••••••••••••••') && !empty($existingSettings['client_secret']) && $existingSettings['client_secret'] !== '••••••••••••••••') {
$clientSecret = $existingSettings['client_secret'];
}
$newSettings = array_merge($existingSettings, [
'client_id' => $clientId,
'client_secret' => $clientSecret,
'tenant_id' => $tenantId,
]);
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'OUTLOOK', 1, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
is_demo_mode = VALUES(is_demo_mode),
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
$message = "Centrale Microsoft 365 Azure App instellingen zijn succesvol opgeslagen.";
$messageType = 'success';
}
// 2. Centraal Jira Bedrijfsdomein opslaan
elseif ($action === 'save_jira_domain') {
$rawDomain = trim($_POST['jira_domain'] ?? '');
$companyDomain = !empty($rawDomain) ? jira_normalize_domain($rawDomain) : '';
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
$existingSettings['jira_domain'] = $companyDomain;
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'JIRA', 1, 0, ?, NOW())
ON DUPLICATE KEY UPDATE
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, json_encode($existingSettings)]);
$message = "Standaard Atlassian Jira bedrijfsdomein is succesvol opgeslagen.";
$messageType = 'success';
}
}
// Haal actuele configuratie op
$appConfig = microsoft_get_app_config();
$companyJiraDomain = jira_get_company_domain();
// Redirect URI
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
// Admin Consent URL
$tenantForConsent = !empty($appConfig['tenant_id']) ? $appConfig['tenant_id'] : 'common';
$adminConsentUrl = "https://login.microsoftonline.com/" . urlencode($tenantForConsent) . "/adminconsent?client_id=" . urlencode($appConfig['client_id']) . "&redirect_uri=" . urlencode($redirectUri);
// Statistieken ophalen voor Systeem & Team Monitor
$totalUsers = (int)$db->query("SELECT COUNT(*) FROM users")->fetchColumn();
$totalAdmins = (int)$db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'")->fetchColumn();
$totalMembers = $totalUsers - $totalAdmins;
$pendingInvites = (int)$db->query("SELECT COUNT(*) FROM invites WHERE used_at IS NULL AND expires_at > NOW()")->fetchColumn();
$outlookConnectedCount = (int)$db->query("SELECT COUNT(*) FROM integrations WHERE provider = 'OUTLOOK' AND access_token IS NOT NULL")->fetchColumn();
$jiraConnectedCount = (int)$db->query("SELECT COUNT(*) FROM integrations WHERE provider = 'JIRA' AND settings LIKE '%jira_api_token%' AND settings NOT LIKE '%\"jira_api_token\":\"\"%'")->fetchColumn();
// Haal recente gebruikers op voor snelle inspectie
$recentUsers = $db->query("
SELECT u.id, u.name, u.email, u.role, u.created_at,
(SELECT COUNT(*) FROM integrations i WHERE i.user_id = u.id AND i.provider = 'OUTLOOK' AND i.access_token IS NOT NULL) as has_outlook,
(SELECT COUNT(*) FROM integrations i WHERE i.user_id = u.id AND i.provider = 'JIRA' AND i.settings LIKE '%jira_api_token%' AND i.settings NOT LIKE '%\"jira_api_token\":\"\"%') as has_jira
FROM users u
ORDER BY u.created_at DESC
LIMIT 6
")->fetchAll();
$pageTitle = 'Systeembeheer & Integraties — Fociz';
require_once __DIR__ . '/includes/header.php';
?>
<div class="space-y-8 max-w-7xl mx-auto pb-12">
<!-- Top Header Banner -->
<div class="relative overflow-hidden bg-slate-900/90 border border-slate-800 rounded-3xl p-6 sm:p-8 shadow-2xl backdrop-blur-xl">
<div class="absolute -top-24 -right-24 w-80 h-80 bg-emerald-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div class="absolute -bottom-24 -left-24 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div class="relative z-10 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="space-y-2">
<div class="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs font-semibold tracking-wide">
<i data-lucide="shield-alert" class="w-3.5 h-3.5"></i>
<span>Beheerderspaneel • Alleen toegankelijk voor Beheer</span>
</div>
<h1 class="text-3xl sm:text-4xl font-black text-white tracking-tight">
Systeembeheer & Integraties
</h1>
<p class="text-sm text-slate-400 max-w-2xl leading-relaxed">
Beheer hier de centrale organisatiekoppelingen zoals Microsoft 365 Azure App Registratie en Jira, inspecteer de teamstatus en configureer toekomstige modules.
</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<a href="/users.php" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-slate-800/90 hover:bg-slate-700 text-slate-200 border border-slate-700 text-xs font-semibold transition shadow-md">
<i data-lucide="users" class="w-4 h-4 text-emerald-400"></i>
<span>Team & Invites (<?= $totalUsers ?>)</span>
</a>
<a href="/profile.php" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-slate-950 text-xs font-bold transition shadow-lg shadow-emerald-600/20">
<i data-lucide="user" class="w-4 h-4"></i>
<span>Mijn Persoonlijk Profiel</span>
</a>
</div>
</div>
</div>
<!-- Meldingen Banner -->
<?php if ($message): ?>
<div class="p-4 rounded-2xl flex items-start space-x-3 text-sm animate-in fade-in duration-200 <?= $messageType === 'success' ? 'bg-emerald-500/10 border border-emerald-500/30 text-emerald-200' : 'bg-red-500/10 border border-red-500/30 text-red-200' ?>">
<i data-lucide="<?= $messageType === 'success' ? 'check-circle-2' : 'alert-triangle' ?>" class="w-5 h-5 flex-shrink-0 mt-0.5 <?= $messageType === 'success' ? 'text-emerald-400' : 'text-red-400' ?>"></i>
<div class="flex-1 font-medium"><?= htmlspecialchars($message) ?></div>
</div>
<?php endif; ?>
<!-- Stat kaarten rij -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-4 flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 flex-shrink-0">
<i data-lucide="users" class="w-6 h-6"></i>
</div>
<div>
<span class="text-xs text-slate-400 font-medium block">Totaal Teamleden</span>
<span class="text-2xl font-black text-white"><?= $totalUsers ?></span>
<span class="text-[10px] text-slate-500 block"><?= $totalAdmins ?> Beheerders • <?= $totalMembers ?> Collega's</span>
</div>
</div>
<div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-4 flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 flex-shrink-0">
<i data-lucide="calendar" class="w-6 h-6"></i>
</div>
<div>
<span class="text-xs text-slate-400 font-medium block">Outlook Live Gekoppeld</span>
<span class="text-2xl font-black text-white"><?= $outlookConnectedCount ?></span>
<span class="text-[10px] text-emerald-400 block"><?= $appConfig['is_configured'] ? 'Azure App Actief' : 'Configuratie nodig' ?></span>
</div>
</div>
<div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-4 flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-400 flex-shrink-0">
<i data-lucide="check-square" class="w-6 h-6"></i>
</div>
<div>
<span class="text-xs text-slate-400 font-medium block">Jira Gekoppeld</span>
<span class="text-2xl font-black text-white"><?= $jiraConnectedCount ?></span>
<span class="text-[10px] text-slate-500 block"><?= !empty($companyJiraDomain) ? 'Domein ingesteld' : 'Geen domein' ?></span>
</div>
</div>
<div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-4 flex items-center space-x-4">
<div class="w-12 h-12 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400 flex-shrink-0">
<i data-lucide="mail" class="w-6 h-6"></i>
</div>
<div>
<span class="text-xs text-slate-400 font-medium block">Openstaande Invites</span>
<span class="text-2xl font-black text-white"><?= $pendingInvites ?></span>
<a href="/users.php" class="text-[10px] text-amber-400 hover:underline block">Beheer uitnodigingen &rarr;</a>
</div>
</div>
</div>
<!-- Hoofdgrid met Beheer Secties -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Linkerkolom: Integraties Beheer (8 cols) -->
<div class="lg:col-span-8 space-y-8">
<!-- 1. Microsoft 365 Azure App Registratie -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 font-bold shadow-md">
<i data-lucide="cloud" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-lg font-bold text-white tracking-tight">Microsoft 365 & Azure App Registratie</h2>
<?php if ($appConfig['is_configured']): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[10px] font-bold">
Geconfigureerd
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-amber-500/15 border border-amber-500/30 text-amber-400 text-[10px] font-bold">
Configuratie Vereist
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-400">Centrale OAuth 2.0 instellingen voor de hele organisatie</p>
</div>
</div>
<?php if ($appConfig['is_configured']): ?>
<a href="<?= htmlspecialchars($adminConsentUrl) ?>" target="_blank" rel="noopener noreferrer" class="inline-flex items-center space-x-2 px-3.5 py-2 rounded-xl bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 border border-blue-500/30 text-xs font-semibold transition" title="Verleen namens alle gebruikers beheerdersgoedkeuring in Azure">
<i data-lucide="shield-check" class="w-4 h-4 text-blue-400"></i>
<span>Admin Consent Verlenen</span>
<i data-lucide="external-link" class="w-3.5 h-3.5 text-blue-400"></i>
</a>
<?php endif; ?>
</div>
<form method="POST" class="mt-6 space-y-5">
<input type="hidden" name="action" value="save_azure">
<!-- Redirect URI Kopieerbox -->
<div class="p-4 rounded-2xl bg-slate-950/70 border border-slate-800 space-y-2">
<div class="flex items-center justify-between">
<label class="text-xs font-semibold text-slate-300 flex items-center space-x-1.5">
<i data-lucide="link" class="w-3.5 h-3.5 text-blue-400"></i>
<span>Redirect URI (Terugbel-URL voor Azure Portal)</span>
</label>
<span class="text-[10px] text-slate-500">Kopieer dit exact naar de Azure Web Redirect URIs</span>
</div>
<div class="flex items-center space-x-2">
<input type="text" readonly id="adminRedirectUri" value="<?= htmlspecialchars($redirectUri) ?>" class="flex-1 bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs font-mono text-emerald-400 select-all focus:outline-none">
<button type="button" onclick="copyAdminRedirectUri()" class="px-3 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold flex items-center space-x-1.5 transition">
<i data-lucide="copy" class="w-3.5 h-3.5" id="copyIcon"></i>
<span id="copyText">Kopiëren</span>
</button>
</div>
</div>
<!-- Client ID & Tenant ID Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Application (Client) ID <span class="text-red-400">*</span>
</label>
<input type="text" name="client_id" required value="<?= htmlspecialchars($appConfig['client_id']) ?>" placeholder="bijv. 38314451-1d05-4205-88ec-..." class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-blue-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Directory (Tenant) ID
</label>
<input type="text" name="tenant_id" value="<?= htmlspecialchars($appConfig['tenant_id']) ?>" placeholder="common of uwbedrijf.nl" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-blue-500">
<span class="text-[10px] text-slate-500 mt-1 block">Gebruik <code class="text-blue-400">common</code> of uw specifieke domeinnaam (bijv. planningsagenda.nl)</span>
</div>
</div>
<!-- Client Secret met toggle -->
<div>
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-semibold text-slate-300">
Client Secret (Value) <span class="text-red-400">*</span>
</label>
<span class="text-[10px] text-slate-500">Wordt veilig en versleuteld opgeslagen</span>
</div>
<div class="relative">
<input type="password" id="adminClientSecret" name="client_secret" value="<?= !empty($appConfig['client_secret']) ? '••••••••••••••••' : '' ?>" placeholder="Voer de Secret Value in uit Azure..." class="w-full bg-slate-950 border border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-blue-500">
<button type="button" onclick="toggleAdminSecret()" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-500 hover:text-slate-300">
<i data-lucide="eye" class="w-4 h-4" id="eyeSecretIcon"></i>
</button>
</div>
</div>
<div class="pt-2 flex items-center justify-between">
<div class="text-[11px] text-slate-400">
Zodra opgeslagen kunnen al uw collega's met 1 klik hun Outlook agenda koppelen.
</div>
<button type="submit" class="px-5 py-2.5 rounded-xl bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold transition shadow-lg shadow-blue-500/20">
Azure Gegevens Opslaan
</button>
</div>
</form>
<!-- Azure Setup Documentatie Accordion -->
<div class="mt-6 pt-6 border-t border-slate-800/80">
<button type="button" onclick="toggleAdminGuide()" class="w-full flex items-center justify-between text-xs font-semibold text-slate-300 hover:text-white transition">
<span class="flex items-center space-x-2">
<i data-lucide="help-circle" class="w-4 h-4 text-blue-400"></i>
<span>Hoe configureer ik de Azure App Registratie in 4 stappen?</span>
</span>
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-200" id="guideArrow"></i>
</button>
<div id="adminGuideContent" class="hidden mt-4 p-4 rounded-2xl bg-slate-950/60 border border-slate-800 text-xs text-slate-300 space-y-3 leading-relaxed">
<div class="flex items-start space-x-2.5">
<span class="w-5 h-5 rounded-full bg-blue-500/20 text-blue-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">1</span>
<div>
<strong class="text-white">Azure Portal:</strong> Ga naar <a href="https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps" target="_blank" class="text-blue-400 underline">portal.azure.com</a> &rarr; <em>Microsoft Entra ID</em> &rarr; <em>App registrations</em> &rarr; <em>New registration</em>. Kies "Accounts in any organizational directory and personal Microsoft accounts" (of alleen uw organisatie).
</div>
</div>
<div class="flex items-start space-x-2.5">
<span class="w-5 h-5 rounded-full bg-blue-500/20 text-blue-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">2</span>
<div>
<strong class="text-white">Redirect URI:</strong> Kies platform <strong>Web</strong> en vul exact in: <code class="text-emerald-400 font-mono"><?= htmlspecialchars($redirectUri) ?></code>.
</div>
</div>
<div class="flex items-start space-x-2.5">
<span class="w-5 h-5 rounded-full bg-blue-500/20 text-blue-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">3</span>
<div>
<strong class="text-white">API Permissions:</strong> Ga naar <em>API permissions</em> &rarr; <em>Add a permission</em> &rarr; <em>Microsoft Graph</em> &rarr; <strong>Delegated permissions</strong> &rarr; Vink aan: <code class="text-blue-400">User.Read</code> en <code class="text-blue-400">Calendars.ReadWrite</code>.
</div>
</div>
<div class="flex items-start space-x-2.5">
<span class="w-5 h-5 rounded-full bg-blue-500/20 text-blue-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">4</span>
<div>
<strong class="text-white">Beheerdersgoedkeuring (Admin Consent):</strong> Klik op <em>"Grant admin consent for [Uw Organisatie]"</em>. Er verschijnt direct een groen vinkje bij alle regels. Hiermee hoeven individuele collega's geen beheerder meer te vragen!
</div>
</div>
</div>
</div>
</div>
<!-- 2. Atlassian Jira Bedrijfsinstellingen -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl">
<div class="flex items-center space-x-3.5 pb-6 border-b border-slate-800/80">
<div class="w-12 h-12 rounded-2xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-400 font-bold shadow-md">
<i data-lucide="trello" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-lg font-bold text-white tracking-tight">Atlassian Jira Organisatie-instellingen</h2>
<?php if (!empty($companyJiraDomain)): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[10px] font-bold">
Actief
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 text-slate-400 text-[10px] font-bold">
Optioneel
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-400">Standaard Jira Cloud domein voor het hele team</p>
</div>
</div>
<form method="POST" class="mt-6 space-y-4">
<input type="hidden" name="action" value="save_jira_domain">
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Standaard Jira Bedrijfsdomein
</label>
<input type="text" name="jira_domain" value="<?= htmlspecialchars($companyJiraDomain) ?>" placeholder="https://uwbedrijf.atlassian.net" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500 font-mono">
<p class="text-[11px] text-slate-400 mt-1.5 leading-relaxed">
Wanneer u dit instelt, wordt dit domein automatisch vooraf ingevuld op de profielpagina van alle collega's. Zij hoeven dan enkel nog hun eigen persoonlijke e-mailadres en Atlassian API-token in te voeren.
</p>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-lg shadow-indigo-500/20">
Jira Domein Opslaan
</button>
</div>
</form>
</div>
<!-- 3. Toekomstige Systeemmodules Hub -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-6">
<div class="flex items-center space-x-3.5 pb-4 border-b border-slate-800/80">
<div class="w-12 h-12 rounded-2xl bg-teal-500/10 border border-teal-500/20 flex items-center justify-center text-teal-400 font-bold shadow-md">
<i data-lucide="cpu" class="w-6 h-6"></i>
</div>
<div>
<h2 class="text-lg font-bold text-white tracking-tight">Toekomstige Modules & Extensies</h2>
<p class="text-xs text-slate-400">Voorbereid architectuurkader voor nieuwe bedrijfssystemen</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Module 1: AI Brainstorm -->
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between space-y-3">
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-bold text-white flex items-center space-x-1.5">
<i data-lucide="sparkles" class="w-4 h-4 text-amber-400"></i>
<span>AI & Brainstorm Co-Pilot</span>
</span>
<span class="px-2 py-0.5 rounded-full bg-amber-500/10 text-amber-400 text-[10px] font-semibold border border-amber-500/20">
Voorbereid
</span>
</div>
<p class="text-[11px] text-slate-400 leading-relaxed">
Centrale OpenAI of Anthropic Claude API-sleutels instellen voor automatische categorisatie van sticky notes en taakschattingen.
</p>
</div>
<div class="pt-2 border-t border-slate-900 text-[10px] text-slate-500">
Gepland voor release v1.5
</div>
</div>
<!-- Module 2: Dagplanning Standaarden -->
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between space-y-3">
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-bold text-white flex items-center space-x-1.5">
<i data-lucide="clock" class="w-4 h-4 text-emerald-400"></i>
<span>Organisatie Werkdag Standaarden</span>
</span>
<span class="px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 text-[10px] font-semibold border border-emerald-500/20">
Binnenkort
</span>
</div>
<p class="text-[11px] text-slate-400 leading-relaxed">
Standaard kantoortijden instellen (bijv. 08:30 - 17:00), standaard Pomodoro tijden (25m / 50m) en lunchpauze blokken.
</p>
</div>
<div class="pt-2 border-t border-slate-900 text-[10px] text-slate-500">
Gepland voor release v1.4
</div>
</div>
</div>
</div>
</div>
<!-- Rechterkolom: Systeem & Team Monitor (4 cols) -->
<div class="lg:col-span-4 space-y-6">
<!-- Team Status Overzicht -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 shadow-xl space-y-5">
<div class="flex items-center justify-between pb-4 border-b border-slate-800/80">
<div class="flex items-center space-x-2.5">
<i data-lucide="activity" class="w-4 h-4 text-emerald-400"></i>
<h3 class="text-sm font-bold text-white">Team & Integratiestatus</h3>
</div>
<a href="/users.php" class="text-[11px] text-emerald-400 hover:underline font-semibold">
Alle gebruikers &rarr;
</a>
</div>
<div class="space-y-3">
<?php foreach ($recentUsers as $u): ?>
<div class="p-3 rounded-2xl bg-slate-950/60 border border-slate-800/70 flex items-center justify-between">
<div class="flex items-center space-x-3 overflow-hidden">
<div class="w-8 h-8 rounded-full bg-emerald-600/20 border border-emerald-500/30 text-emerald-400 font-bold flex items-center justify-center text-xs flex-shrink-0">
<?= strtoupper(substr($u['name'], 0, 1)) ?>
</div>
<div class="truncate">
<span class="text-xs font-semibold text-white block truncate"><?= htmlspecialchars($u['name']) ?></span>
<span class="text-[10px] text-slate-400 block truncate"><?= htmlspecialchars($u['email']) ?></span>
</div>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
<span title="Outlook: <?= $u['has_outlook'] ? 'Live Gekoppeld' : 'Niet gekoppeld' ?>" class="w-5 h-5 rounded-md flex items-center justify-center text-[10px] <?= $u['has_outlook'] ? 'bg-blue-500/20 text-blue-300 border border-blue-500/30' : 'bg-slate-800 text-slate-600' ?>">
<i data-lucide="calendar" class="w-3 h-3"></i>
</span>
<span title="Jira: <?= $u['has_jira'] ? 'Gekoppeld' : 'Niet gekoppeld' ?>" class="w-5 h-5 rounded-md flex items-center justify-center text-[10px] <?= $u['has_jira'] ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'bg-slate-800 text-slate-600' ?>">
<i data-lucide="check-square" class="w-3 h-3"></i>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="pt-2">
<a href="/users.php" class="w-full flex items-center justify-center space-x-2 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold transition border border-slate-700">
<i data-lucide="user-plus" class="w-3.5 h-3.5 text-emerald-400"></i>
<span>Nieuwe Collega Uitnodigen</span>
</a>
</div>
</div>
<!-- Systeem Gezondheid & Beveiliging -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 shadow-xl space-y-4">
<div class="flex items-center space-x-2 text-emerald-400 text-xs font-semibold">
<i data-lucide="shield-check" class="w-4 h-4"></i>
<span>Systeem & Beveiliging</span>
</div>
<div class="space-y-2.5 text-xs text-slate-300">
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-950/60 border border-slate-800">
<span>Tweestapsverificatie (2FA)</span>
<span class="text-emerald-400 font-bold flex items-center space-x-1">
<i data-lucide="check" class="w-3 h-3"></i>
<span>Verplicht</span>
</span>
</div>
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-950/60 border border-slate-800">
<span>Database Versie</span>
<span class="text-slate-400 font-mono text-[11px]">MariaDB 10.x</span>
</div>
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-950/60 border border-slate-800">
<span>Push Notificaties</span>
<span class="text-emerald-400 font-bold">Actief</span>
</div>
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-950/60 border border-slate-800">
<span>Graph API Sync</span>
<span class="text-blue-400 font-bold">Live v1.0</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function copyAdminRedirectUri() {
const input = document.getElementById('adminRedirectUri');
if (!input) return;
input.select();
navigator.clipboard.writeText(input.value).then(() => {
const copyText = document.getElementById('copyText');
const copyIcon = document.getElementById('copyIcon');
if (copyText) copyText.innerText = 'Gekopieerd!';
if (copyIcon) copyIcon.setAttribute('data-lucide', 'check');
if (window.lucide) lucide.createIcons();
setTimeout(() => {
if (copyText) copyText.innerText = 'Kopiëren';
if (copyIcon) copyIcon.setAttribute('data-lucide', 'copy');
if (window.lucide) lucide.createIcons();
}, 2000);
});
}
function toggleAdminSecret() {
const input = document.getElementById('adminClientSecret');
const icon = document.getElementById('eyeSecretIcon');
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.setAttribute('data-lucide', 'eye-off');
} else {
input.type = 'password';
icon.setAttribute('data-lucide', 'eye');
}
if (window.lucide) lucide.createIcons();
}
function toggleAdminGuide() {
const content = document.getElementById('adminGuideContent');
const arrow = document.getElementById('guideArrow');
if (!content) return;
content.classList.toggle('hidden');
if (arrow) {
arrow.classList.toggle('rotate-180');
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
+8 -8
View File
@@ -46,7 +46,7 @@ $db = get_db_connection();
// 3. Foutafhandeling vanuit Microsoft (bijv. gebruiker weigerde machtiging) // 3. Foutafhandeling vanuit Microsoft (bijv. gebruiker weigerde machtiging)
if (!empty($_GET['error'])) { if (!empty($_GET['error'])) {
$errorDescription = $_GET['error_description'] ?? $_GET['error']; $errorDescription = $_GET['error_description'] ?? $_GET['error'];
header('Location: /integrations.php?error=' . urlencode("Microsoft melding: {$errorDescription}")); header('Location: /profile.php?error=' . urlencode("Microsoft melding: {$errorDescription}"));
exit; exit;
} }
@@ -55,20 +55,20 @@ $expectedState = $_SESSION['ms_oauth_state'] ?? '';
unset($_SESSION['ms_oauth_state'], $_SESSION['ms_oauth_user_id']); unset($_SESSION['ms_oauth_state'], $_SESSION['ms_oauth_user_id']);
if (!$stateUserId && (empty($receivedState) || empty($expectedState) || !hash_equals($expectedState, $receivedState))) { if (!$stateUserId && (empty($receivedState) || empty($expectedState) || !hash_equals($expectedState, $receivedState))) {
header('Location: /integrations.php?error=' . urlencode('Ongeldige sessie of CSRF-controle mislukt. Probeer opnieuw in te loggen via Microsoft.')); header('Location: /profile.php?error=' . urlencode('Ongeldige sessie of CSRF-controle mislukt. Probeer opnieuw in te loggen via Microsoft.'));
exit; exit;
} }
// Afhandeling van beheerderstoestemming (Admin Consent) // Afhandeling van beheerderstoestemming (Admin Consent)
if (!empty($_GET['admin_consent'])) { if (!empty($_GET['admin_consent'])) {
header('Location: /integrations.php?success=' . urlencode('Beheerdersgoedkeuring voor uw organisatie is succesvol verleend! Klik nu op "Koppel Outlook" om uw agenda te synchroniseren.')); header('Location: /admin.php?success=' . urlencode('Beheerdersgoedkeuring voor uw organisatie is succesvol verleend! U en uw collega\'s kunnen nu direct hun Outlook agenda koppelen via Mijn Profiel.'));
exit; exit;
} }
// 5. Autorisatiecode controleren // 5. Autorisatiecode controleren
$code = $_GET['code'] ?? ''; $code = $_GET['code'] ?? '';
if (empty($code)) { if (empty($code)) {
header('Location: /integrations.php?error=' . urlencode('Geen autorisatiecode ontvangen van Microsoft.')); header('Location: /profile.php?error=' . urlencode('Geen autorisatiecode ontvangen van Microsoft.'));
exit; exit;
} }
@@ -80,7 +80,7 @@ $clientSecret = $appConfig['client_secret'];
$tenantId = $appConfig['tenant_id']; $tenantId = $appConfig['tenant_id'];
if (empty($clientId) || empty($clientSecret)) { if (empty($clientId) || empty($clientSecret)) {
header('Location: /integrations.php?error=' . urlencode('Microsoft 365 configuratie (Client ID of Secret) ontbreekt in Fociz. Neem contact op met de beheerder.')); header('Location: /profile.php?error=' . urlencode('Microsoft 365 configuratie (Client ID of Secret) ontbreekt in Fociz. Neem contact op met de beheerder.'));
exit; exit;
} }
@@ -90,7 +90,7 @@ $redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
$tokenResult = microsoft_exchange_code($code, $clientId, $clientSecret, $redirectUri, $tenantId); $tokenResult = microsoft_exchange_code($code, $clientId, $clientSecret, $redirectUri, $tenantId);
if (!$tokenResult['success']) { if (!$tokenResult['success']) {
header('Location: /integrations.php?error=' . urlencode('Token uitwisseling mislukt: ' . $tokenResult['error'])); header('Location: /profile.php?error=' . urlencode('Token uitwisseling mislukt: ' . $tokenResult['error']));
exit; exit;
} }
@@ -136,7 +136,7 @@ $saveStmt = $db->prepare("
"); ");
$saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]); $saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]);
// 8. Succesvol terugkeren naar de Integraties Hub // 8. Succesvol terugkeren naar de Persoonlijke Profiel Hub
header('Location: /integrations.php?success=' . urlencode("Microsoft Outlook succesvol gekoppeld als {$accountEmail}!")); header('Location: /profile.php?success=' . urlencode("Microsoft Outlook succesvol gekoppeld als {$accountEmail}!"));
exit; exit;
+2 -2
View File
@@ -17,9 +17,9 @@ $tenantId = !empty($_GET['tenant_id']) ? trim($_GET['tenant_id']) : $appConfig['
if (empty($clientId)) { if (empty($clientId)) {
$err = ($user['role'] === 'ADMIN') $err = ($user['role'] === 'ADMIN')
? 'Vul eerst de Application (Client) ID en het Client Secret in bij de Outlook instellingen om te koppelen met Microsoft 365.' ? 'Vul eerst de Application (Client) ID en het Client Secret in bij het Systeembeheer om te koppelen met Microsoft 365.'
: 'Microsoft 365 is nog niet geconfigureerd door de beheerder van uw organisatie. Vraag uw beheerder om de koppeling in te stellen.'; : 'Microsoft 365 is nog niet geconfigureerd door de beheerder van uw organisatie. Vraag uw beheerder om de koppeling in te stellen.';
header('Location: /integrations.php?error=' . urlencode($err)); header('Location: /profile.php?error=' . urlencode($err));
exit; exit;
} }
+20 -13
View File
@@ -82,14 +82,18 @@ require_once __DIR__ . '/env_badge.php';
<i data-lucide="map" class="w-4 h-4"></i> <i data-lucide="map" class="w-4 h-4"></i>
<span>Roadmap</span> <span>Roadmap</span>
</a> </a>
<a href="/integrations.php" class="flex items-center space-x-2 px-3.5 py-2 rounded-lg text-sm font-medium transition <?= $currentPage === 'integrations.php' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60' ?>"> <a href="/profile.php" class="flex items-center space-x-2 px-3.5 py-2 rounded-lg text-sm font-medium transition <?= $currentPage === 'profile.php' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60' ?>">
<i data-lucide="layers" class="w-4 h-4"></i> <i data-lucide="user" class="w-4 h-4"></i>
<span>Integraties</span> <span>Mijn Profiel</span>
</a> </a>
<?php if ($currentUser['role'] === 'ADMIN'): ?> <?php if ($currentUser['role'] === 'ADMIN'): ?>
<a href="/users.php" class="flex items-center space-x-2 px-3.5 py-2 rounded-lg text-sm font-medium transition <?= $currentPage === 'users.php' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60' ?>"> <a href="/users.php" class="flex items-center space-x-2 px-3.5 py-2 rounded-lg text-sm font-medium transition <?= $currentPage === 'users.php' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60' ?>">
<i data-lucide="users" class="w-4 h-4"></i> <i data-lucide="users" class="w-4 h-4"></i>
<span>Team & Invites</span> <span>Team</span>
</a>
<a href="/admin.php" class="flex items-center space-x-2 px-3.5 py-2 rounded-lg text-sm font-medium transition <?= $currentPage === 'admin.php' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60' ?>">
<i data-lucide="shield-check" class="w-4 h-4 text-emerald-400"></i>
<span>Beheer</span>
</a> </a>
<?php endif; ?> <?php endif; ?>
</nav> </nav>
@@ -107,12 +111,15 @@ require_once __DIR__ . '/env_badge.php';
<span>2FA Actief</span> <span>2FA Actief</span>
</div> </div>
<div class="text-right hidden sm:block"> <a href="/profile.php" class="text-right hidden sm:block group hover:opacity-90 transition" title="Mijn Profiel & Instellingen">
<div class="text-xs font-semibold text-slate-200"><?= htmlspecialchars($currentUser['name']) ?></div> <div class="text-xs font-semibold text-slate-200 group-hover:text-emerald-400 transition flex items-center justify-end space-x-1">
<span><?= htmlspecialchars($currentUser['name']) ?></span>
<i data-lucide="chevron-right" class="w-3 h-3 text-slate-500 group-hover:text-emerald-400 transition"></i>
</div>
<div class="text-[10px] text-slate-500"> <div class="text-[10px] text-slate-500">
<?= $currentUser['role'] === 'ADMIN' ? 'Beheerder' : 'Collega' ?> • <?= htmlspecialchars($currentUser['email']) ?> <?= $currentUser['role'] === 'ADMIN' ? 'Beheerder' : 'Collega' ?> • <?= htmlspecialchars($currentUser['email']) ?>
</div> </div>
</div> </a>
<a href="/auth/logout.php" title="Veilig uitloggen" class="p-2 rounded-lg text-slate-400 hover:text-red-400 hover:bg-red-500/10 border border-transparent hover:border-red-500/20 transition"> <a href="/auth/logout.php" title="Veilig uitloggen" class="p-2 rounded-lg text-slate-400 hover:text-red-400 hover:bg-red-500/10 border border-transparent hover:border-red-500/20 transition">
<i data-lucide="log-out" class="w-4 h-4"></i> <i data-lucide="log-out" class="w-4 h-4"></i>
@@ -134,18 +141,18 @@ require_once __DIR__ . '/env_badge.php';
<i data-lucide="map" class="w-4 h-4 mb-0.5"></i> <i data-lucide="map" class="w-4 h-4 mb-0.5"></i>
<span>Roadmap</span> <span>Roadmap</span>
</a> </a>
<a href="/integrations.php" class="flex flex-col items-center py-1 <?= $currentPage === 'integrations.php' ? 'text-emerald-400 font-bold' : 'text-slate-400' ?>"> <a href="/profile.php" class="flex flex-col items-center py-1 <?= $currentPage === 'profile.php' ? 'text-emerald-400 font-bold' : 'text-slate-400' ?>">
<i data-lucide="layers" class="w-4 h-4 mb-0.5"></i> <i data-lucide="user" class="w-4 h-4 mb-0.5"></i>
<span>Koppelingen</span> <span>Profiel</span>
</a> </a>
<button type="button" onclick="openNotificationModal()" class="flex flex-col items-center py-1 text-slate-400 hover:text-white"> <button type="button" onclick="openNotificationModal()" class="flex flex-col items-center py-1 text-slate-400 hover:text-white">
<i data-lucide="bell" class="w-4 h-4 mb-0.5"></i> <i data-lucide="bell" class="w-4 h-4 mb-0.5"></i>
<span>Meldingen</span> <span>Meldingen</span>
</button> </button>
<?php if ($currentUser['role'] === 'ADMIN'): ?> <?php if ($currentUser['role'] === 'ADMIN'): ?>
<a href="/users.php" class="flex flex-col items-center py-1 <?= $currentPage === 'users.php' ? 'text-emerald-400 font-bold' : 'text-slate-400' ?>"> <a href="/admin.php" class="flex flex-col items-center py-1 <?= $currentPage === 'admin.php' ? 'text-emerald-400 font-bold' : 'text-slate-400' ?>">
<i data-lucide="users" class="w-4 h-4 mb-0.5"></i> <i data-lucide="shield" class="w-4 h-4 mb-0.5"></i>
<span>Team</span> <span>Beheer</span>
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
+46 -1
View File
@@ -18,6 +18,51 @@ function jira_normalize_domain(string $domain): string {
return rtrim($domain, '/'); return rtrim($domain, '/');
} }
/**
* Haalt het centrale bedrijfsdomein voor Atlassian Jira op
*/
function jira_get_company_domain(): string {
$domain = fociz_env('JIRA_DOMAIN', '');
if (!empty($domain)) {
return jira_normalize_domain($domain);
}
try {
$db = get_db_connection();
// Zoek bij beheerder(s) in de integrations tabel
$stmt = $db->query("
SELECT i.settings
FROM integrations i
JOIN users u ON i.user_id = u.id
WHERE u.role = 'ADMIN' AND i.provider = 'JIRA' AND i.settings IS NOT NULL
ORDER BY i.updated_at DESC
LIMIT 1
");
$row = $stmt->fetch();
if (!empty($row['settings'])) {
$settings = json_decode($row['settings'], true) ?: [];
if (!empty($settings['jira_domain'])) {
return jira_normalize_domain($settings['jira_domain']);
}
}
// Fallback naar een willekeurige JIRA rij
$fallback = $db->query("
SELECT settings FROM integrations WHERE provider = 'JIRA' AND settings LIKE '%jira_domain%' LIMIT 1
")->fetch();
if (!empty($fallback['settings'])) {
$settings = json_decode($fallback['settings'], true) ?: [];
if (!empty($settings['jira_domain'])) {
return jira_normalize_domain($settings['jira_domain']);
}
}
} catch (Throwable $e) {
error_log("jira_get_company_domain error: " . $e->getMessage());
}
return '';
}
/** /**
* Genereert de Basic Authentication header voor Atlassian REST API * Genereert de Basic Authentication header voor Atlassian REST API
*/ */
@@ -240,7 +285,7 @@ function jira_get_user_issues(int $userId, bool $forceRefresh = false): array {
} }
$settings = json_decode($integration['settings'] ?? '{}', true) ?: []; $settings = json_decode($integration['settings'] ?? '{}', true) ?: [];
$domain = $settings['jira_domain'] ?? fociz_env('JIRA_DOMAIN', ''); $domain = !empty($settings['jira_domain']) ? $settings['jira_domain'] : jira_get_company_domain();
$email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', ''); $email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', '');
$apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', ''); $apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', '');
+9 -915
View File
@@ -1,925 +1,19 @@
<?php <?php
/** /**
* Fociz Integraties Hub (Microsoft Outlook, Microsoft Teams, Atlassian Jira) * Fociz Integraties Doorverwijzer
* Beheerders worden automatisch doorgeleid naar Systeembeheer (/admin.php).
* Collega's worden automatisch doorgeleid naar hun Persoonlijk Profiel (/profile.php).
*/ */
require_once __DIR__ . '/includes/auth.php'; require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
$user = require_auth(); $user = require_auth();
$db = get_db_connection();
$userId = $user['id'];
$message = null; $queryString = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
$messageType = 'success';
// Query parameters (bijv. na OAuth redirect) if ($user['role'] === 'ADMIN') {
if (!empty($_GET['success'])) { header('Location: /admin.php' . $queryString);
$message = $_GET['success']; } else {
$messageType = 'success'; header('Location: /profile.php' . $queryString);
} elseif (!empty($_GET['error'])) {
$message = $_GET['error'];
$messageType = 'error';
} }
exit;
// POST Verwerking VÓÓR het inladen van header.php zodat HTTP redirects altijd werken
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$provider = strtoupper(trim($_POST['provider'] ?? ''));
$action = $_POST['action'] ?? 'save';
// 1. Ontkoppelen
if ($action === 'disconnect') {
if ($provider === 'OUTLOOK') {
microsoft_disconnect($userId);
header('Location: /integrations.php?success=' . urlencode('Microsoft Outlook is succesvol ontkoppeld.'));
exit;
} elseif ($provider === 'JIRA') {
jira_disconnect($userId);
header('Location: /integrations.php?success=' . urlencode('Atlassian Jira is succesvol ontkoppeld.'));
exit;
}
}
// 2. Outlook Opslaan of Koppelen
elseif ($provider === 'OUTLOOK') {
$isAdmin = ($user['role'] === 'ADMIN');
$appConfig = microsoft_get_app_config();
// A. Koppelen via OAuth (zowel voor Collega als Beheerder)
if ($action === 'connect_oauth') {
// Als de beheerder vanaf het admin-formulier koppelt en eventueel gewijzigde velden meestuurt
if ($isAdmin && isset($_POST['client_id'])) {
$clientId = trim($_POST['client_id'] ?? '');
$clientSecret = trim($_POST['client_secret'] ?? '');
$tenantId = trim($_POST['tenant_id'] ?? 'common');
if (empty($tenantId)) $tenantId = 'common';
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($clientSecret === '' || $clientSecret === '••••••••••••••••') && !empty($existingSettings['client_secret']) && $existingSettings['client_secret'] !== '••••••••••••••••') {
$clientSecret = $existingSettings['client_secret'];
}
$newSettings = array_merge($existingSettings, [
'client_id' => $clientId,
'client_secret' => $clientSecret,
'tenant_id' => $tenantId,
]);
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'OUTLOOK', 1, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
is_demo_mode = VALUES(is_demo_mode),
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
if (empty($clientId)) {
$message = "Vul eerst de Application (Client) ID in van uw Azure App Registratie.";
$messageType = 'error';
} elseif (empty($clientSecret)) {
$message = "Vul eerst het Client Secret in van uw Azure App Registratie.";
$messageType = 'error';
} else {
header("Location: /auth/oauth_microsoft.php");
exit;
}
} else {
// Collega of Beheerder zonder inline velden
if (!$appConfig['is_configured']) {
$message = "Microsoft 365 is nog niet geconfigureerd door de beheerder. Vraag een beheerder om de Application ID en Client Secret in te stellen.";
$messageType = 'error';
} else {
header("Location: /auth/oauth_microsoft.php");
exit;
}
}
}
// B. Alleen Beheerders mogen de centrale Azure instellingen opslaan
elseif ($action === 'save') {
if (!$isAdmin) {
$message = "Alleen beheerders kunnen centrale integratie-instellingen wijzigen.";
$messageType = 'error';
} else {
$clientId = trim($_POST['client_id'] ?? '');
$clientSecret = trim($_POST['client_secret'] ?? '');
$tenantId = trim($_POST['tenant_id'] ?? 'common');
if (empty($tenantId)) $tenantId = 'common';
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($clientSecret === '' || $clientSecret === '••••••••••••••••') && !empty($existingSettings['client_secret']) && $existingSettings['client_secret'] !== '••••••••••••••••') {
$clientSecret = $existingSettings['client_secret'];
}
$newSettings = array_merge($existingSettings, [
'client_id' => $clientId,
'client_secret' => $clientSecret,
'tenant_id' => $tenantId,
]);
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'OUTLOOK', 1, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
is_demo_mode = VALUES(is_demo_mode),
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
$message = "Centrale instellingen voor Microsoft Outlook zijn succesvol opgeslagen.";
$messageType = 'success';
}
}
}
// 3. Teams Opslaan
elseif ($provider === 'TEAMS') {
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, last_sync_at)
VALUES (?, 'TEAMS', 1, ?, NOW())
ON DUPLICATE KEY UPDATE is_demo_mode = VALUES(is_demo_mode), last_sync_at = NOW()
");
$stmt->execute([$userId, $isDemo]);
$message = "Instellingen voor Microsoft Teams zijn bijgewerkt.";
}
// 4. Jira Opslaan & Testen
elseif ($provider === 'JIRA') {
$jiraDomain = jira_normalize_domain($_POST['jira_domain'] ?? '');
$jiraEmail = trim($_POST['jira_email'] ?? '');
$jiraToken = trim($_POST['jira_api_token'] ?? '');
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
// Haal bestaande instellingen op
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($jiraToken === '' || $jiraToken === '••••••••••••••••') && !empty($existingSettings['jira_api_token']) && $existingSettings['jira_api_token'] !== '••••••••••••••••') {
$jiraToken = $existingSettings['jira_api_token'];
}
// Als niet in demo mode, test verbinding
$accountName = $existingSettings['account_name'] ?? '';
$accountId = $existingSettings['account_id'] ?? '';
if (!$isDemo && !empty($jiraDomain) && !empty($jiraEmail) && !empty($jiraToken)) {
$testRes = jira_test_connection($jiraDomain, $jiraEmail, $jiraToken);
if (!$testRes['success']) {
$message = "Jira Verbindingsfout: " . $testRes['error'];
$messageType = 'error';
} else {
$accountName = $testRes['user']['displayName'] ?? 'Jira Gebruiker';
$accountId = $testRes['user']['accountId'] ?? '';
$message = "Verbinding met Jira geslaagd! Ingelogd als: {$accountName}";
$messageType = 'success';
}
} else {
$message = "Jira instellingen opgeslagen.";
$messageType = 'success';
}
$newSettings = [
'jira_domain' => $jiraDomain,
'jira_email' => $jiraEmail,
'jira_api_token' => $jiraToken,
'account_name' => $accountName,
'account_id' => $accountId,
];
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'JIRA', 1, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
is_demo_mode = VALUES(is_demo_mode),
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
// Wis cache
unset($_SESSION["jira_issues_user_{$userId}"]);
}
}
// Laad nu pas de HTML header in
require_once __DIR__ . '/includes/header.php';
// Haal huidige configuraties op
$stmtConfigs = $db->prepare("SELECT * FROM integrations WHERE user_id = ?");
$stmtConfigs->execute([$userId]);
$configsRaw = $stmtConfigs->fetchAll();
$configs = [];
foreach ($configsRaw as $c) {
$configs[$c['provider']] = $c;
$configs[$c['provider']]['settings_parsed'] = json_decode($c['settings'] ?? '{}', true) ?: [];
}
// Bepaal statussen
$isOutlookConnected = !empty($configs['OUTLOOK']['access_token']) && empty($configs['OUTLOOK']['is_demo_mode']);
$outlookSettings = $configs['OUTLOOK']['settings_parsed'] ?? [];
$isJiraConnected = !empty($configs['JIRA']) && empty($configs['JIRA']['is_demo_mode']) && !empty($configs['JIRA']['settings_parsed']['jira_api_token']);
$jiraSettings = $configs['JIRA']['settings_parsed'] ?? [];
// Haal actuele Jira issues op
$forceRefresh = isset($_GET['refresh_jira']);
$jiraData = jira_get_user_issues($userId, $forceRefresh);
$jiraIssues = $jiraData['issues'] ?? [];
$jiraIsDemo = !empty($jiraData['is_demo']);
// Callback redirect URI
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
$appConfig = microsoft_get_app_config();
$isAdmin = ($user['role'] === 'ADMIN');
// Voor beheerders tonen we eventueel centraal opgeslagen waarden als fallback in de inputvelden
if ($isAdmin) {
if (empty($outlookSettings['client_id']) && !empty($appConfig['client_id'])) {
$outlookSettings['client_id'] = $appConfig['client_id'];
}
if (empty($outlookSettings['client_secret']) && !empty($appConfig['client_secret'])) {
$outlookSettings['client_secret'] = $appConfig['client_secret'];
}
if (empty($outlookSettings['tenant_id']) && !empty($appConfig['tenant_id'])) {
$outlookSettings['tenant_id'] = $appConfig['tenant_id'];
}
}
?>
<div class="space-y-6">
<!-- Header -->
<div class="bg-gradient-to-r from-slate-900 via-[#0d1527] to-slate-900 border border-slate-800 rounded-2xl p-6 shadow-2xl">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div class="flex items-center space-x-2 text-blue-400 text-xs font-semibold uppercase tracking-wider mb-1">
<i data-lucide="layers" class="w-4 h-4"></i>
<span>Live Koppelingen & Data Synchronisatie</span>
</div>
<h1 class="text-2xl sm:text-3xl font-black text-white tracking-tight">
Integraties Hub
</h1>
<p class="text-slate-400 text-xs sm:text-sm mt-1 max-w-2xl">
<?= $isAdmin
? 'Beheer centrale organisatie-integraties en koppel uw persoonlijke Outlook agenda en Jira tickets.'
: 'Koppel uw persoonlijke Microsoft 365 Outlook agenda en Atlassian Jira tickets om direct uw dagplanning in Fociz te vullen.' ?>
</p>
</div>
<!-- Snelle status pills -->
<div class="flex items-center space-x-2">
<span class="px-2.5 py-1 rounded-xl text-xs font-semibold <?= $isAdmin ? 'bg-emerald-500/10 border border-emerald-500/30 text-emerald-400' : 'bg-slate-800/80 border border-slate-700 text-slate-300' ?>">
<?= $isAdmin ? 'Beheerdersweergave' : 'Collega Weergave' ?>
</span>
<div class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl border text-xs font-semibold <?= $isOutlookConnected ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400' : 'bg-slate-800/80 border-slate-700 text-slate-400' ?>">
<span class="w-2 h-2 rounded-full <?= $isOutlookConnected ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500' ?>"></span>
<span>Outlook: <?= $isOutlookConnected ? 'Verbonden' : 'Niet verbonden' ?></span>
</div>
<div class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl border text-xs font-semibold <?= $isJiraConnected ? 'bg-sky-500/10 border-sky-500/30 text-sky-400' : 'bg-slate-800/80 border-slate-700 text-slate-400' ?>">
<span class="w-2 h-2 rounded-full <?= $isJiraConnected ? 'bg-sky-400 animate-pulse' : 'bg-slate-500' ?>"></span>
<span>Jira: <?= $isJiraConnected ? 'Verbonden' : 'Niet verbonden' ?></span>
</div>
</div>
</div>
</div>
<!-- Flash message -->
<?php if ($message): ?>
<div class="p-4 rounded-xl text-xs font-semibold flex items-center justify-between border <?= $messageType === 'error' ? 'bg-red-500/10 border-red-500/30 text-red-400' : 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400' ?>">
<div class="flex items-center space-x-2">
<i data-lucide="<?= $messageType === 'error' ? 'alert-triangle' : 'check-circle' ?>" class="w-4 h-4"></i>
<span><?= htmlspecialchars($message) ?></span>
</div>
<button onclick="this.parentElement.remove()" class="text-slate-500 hover:text-slate-300">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<?php endif; ?>
<!-- 3 Koloms Grid voor Providers -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 1. MICROSOFT OUTLOOK -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col justify-between relative overflow-hidden backdrop-blur-sm">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/30 text-blue-400 flex items-center justify-center font-bold">
<i data-lucide="calendar" class="w-5 h-5"></i>
</div>
<?php if ($isOutlookConnected): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-semibold flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
<span>Live Verbonden</span>
</span>
<?php elseif (!empty($configs['OUTLOOK']['is_demo_mode'])): ?>
<span class="px-2.5 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-semibold">
Sandbox Modus
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 border border-slate-700 text-slate-400 text-xs font-semibold">
Niet Gekoppeld
</span>
<?php endif; ?>
</div>
<div>
<h3 class="text-base font-bold text-white mb-1">Microsoft Outlook (365)</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Synchroniseert automatisch uw zakelijke agenda-afspraken en meetings naar de dagplanner tijdlijn.
</p>
</div>
<?php if ($isOutlookConnected): ?>
<div class="bg-slate-950/70 rounded-xl p-3.5 border border-emerald-500/20 text-xs space-y-2">
<div class="flex items-center justify-between text-slate-400">
<span>Account:</span>
<span class="text-emerald-400 font-semibold truncate max-w-[180px]" title="<?= htmlspecialchars($outlookSettings['account_email'] ?? '') ?>">
<?= htmlspecialchars($outlookSettings['account_email'] ?? 'Verbonden') ?>
</span>
</div>
<div class="flex items-center justify-between text-slate-400">
<span>Naam:</span>
<span class="text-slate-200"><?= htmlspecialchars($outlookSettings['account_name'] ?? '-') ?></span>
</div>
<div class="flex items-center justify-between text-slate-400">
<span>Laatste Sync:</span>
<span class="text-slate-300 font-mono text-[11px]"><?= htmlspecialchars($configs['OUTLOOK']['last_sync_at'] ?? 'Zojuist') ?></span>
</div>
</div>
<?php endif; ?>
<?php if ($isAdmin): ?>
<!-- ADMIN WEERGAVE: Centrale melding & Instructies Accordion -->
<div class="p-2.5 bg-blue-500/10 border border-blue-500/20 rounded-xl text-[11px] text-blue-300 flex items-start space-x-2">
<i data-lucide="shield" class="w-4 h-4 flex-shrink-0 mt-0.5 text-blue-400"></i>
<div class="leading-relaxed">
<strong class="text-white block font-semibold">Centrale Beheerdersinstellingen</strong>
<span>Deze Azure App gegevens gelden voor alle collega's in de organisatie. Gewone gebruikers zien deze technische details niet en hoeven alleen op 'Koppelen' te klikken.</span>
</div>
</div>
<!-- Azure App Instructies Accordion -->
<details class="group text-xs bg-slate-950/50 rounded-xl border border-slate-800/80 p-3">
<summary class="cursor-pointer font-semibold text-blue-400 flex items-center justify-between">
<span class="flex items-center space-x-1.5">
<i data-lucide="help-circle" class="w-3.5 h-3.5"></i>
<span>Azure App Registratie Instructies</span>
</span>
<i data-lucide="chevron-down" class="w-3.5 h-3.5 group-open:rotate-180 transition"></i>
</summary>
<div class="mt-3 space-y-2 text-slate-400 text-[11px] leading-relaxed border-t border-slate-800/60 pt-2">
<p>1. Open <a href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade" target="_blank" class="text-blue-400 underline">Azure App Registraties</a> en maak een nieuwe registratie.</p>
<p>2. Kies <em>Accounts in elke organisatiemap (...) en persoonlijke Microsoft-accounts</em>.</p>
<p>3. Voeg een <strong>Web</strong> Omleidings-URI toe:</p>
<div class="flex items-center space-x-1 bg-slate-900 p-1.5 rounded border border-slate-800">
<input type="text" readonly id="msRedirectUriInput" value="<?= htmlspecialchars($redirectUri) ?>" class="bg-transparent text-[10px] text-emerald-400 font-mono flex-1 focus:outline-none">
<button onclick="copyToClipboard('msRedirectUriInput')" class="text-slate-400 hover:text-white px-1.5 py-0.5 rounded bg-slate-800 text-[10px]">Kopieer</button>
</div>
<p>4. Maak een geheim aan onder <em>Certificaten en geheimen</em>.</p>
<p>5. Voeg API-machtigingen toe: <code>Calendars.ReadWrite</code>, <code>User.Read</code>, <code>offline_access</code>.</p>
<p>6. <strong class="text-amber-300">Cruciaal voor organisaties:</strong> Klik direct boven de tabel met machtigingen op de knop <strong class="text-white">"Beheerdersgoedkeuring verlenen voor [Organisatie]"</strong> (<em>Grant admin consent</em>) en bevestig met 'Ja'. Hiermee voorkomt u de melding <em>"Beheerdersgoedkeuring vereist"</em>.</p>
<?php
$curClientId = !empty($outlookSettings['client_id']) ? $outlookSettings['client_id'] : $appConfig['client_id'];
$curTenantId = !empty($outlookSettings['tenant_id']) ? $outlookSettings['tenant_id'] : (!empty($appConfig['tenant_id']) ? $appConfig['tenant_id'] : 'organizations');
if (!empty($curClientId)):
$adminConsentUrl = "https://login.microsoftonline.com/" . urlencode($curTenantId) . "/v2.0/adminconsent?client_id=" . urlencode($curClientId) . "&redirect_uri=" . urlencode($redirectUri) . "&scope=" . urlencode("https://graph.microsoft.com/.default");
?>
<div class="mt-2 pt-2 border-t border-slate-800/80">
<a href="<?= htmlspecialchars($adminConsentUrl) ?>" target="_blank" class="inline-flex items-center space-x-1.5 text-[11px] text-amber-400 hover:text-amber-300 underline font-semibold">
<i data-lucide="shield-alert" class="w-3.5 h-3.5"></i>
<span>Direct Beheerdersgoedkeuring verlenen via Microsoft &rarr;</span>
</a>
</div>
<?php endif; ?>
</div>
</details>
<?php else: ?>
<!-- COLLEGA WEERGAVE: Informatiebanner over status -->
<?php if (!$appConfig['is_configured']): ?>
<div class="p-3.5 bg-amber-500/10 border border-amber-500/20 rounded-xl text-xs text-amber-300 flex items-start space-x-3">
<i data-lucide="clock" class="w-5 h-5 flex-shrink-0 text-amber-400 mt-0.5"></i>
<div>
<strong class="block font-semibold mb-0.5">Configuratie vereist door beheerder</strong>
<span class="text-[11px] text-slate-300 leading-relaxed">
Uw beheerder heeft de Microsoft 365 integratie nog niet geconfigureerd voor uw organisatie. Vraag uw beheerder om de Azure App gegevens in te stellen.
</span>
</div>
</div>
<?php elseif (!$isOutlookConnected): ?>
<div class="p-3.5 bg-blue-500/10 border border-blue-500/20 rounded-xl text-xs text-slate-300 flex items-start space-x-3">
<i data-lucide="shield-check" class="w-5 h-5 flex-shrink-0 text-blue-400 mt-0.5"></i>
<div>
<strong class="text-white block font-semibold mb-0.5">Gereed voor Koppeling</strong>
<span class="text-[11px] text-slate-300 leading-relaxed">
Uw beheerder heeft Microsoft 365 klaargezet voor uw organisatie. Koppel uw persoonlijke werkagenda om al uw meetings en Teams-afspraken automatisch in uw dagplanning te laden.
</span>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php if ($isAdmin): ?>
<!-- ADMIN FORMULIER -->
<form method="POST" action="/integrations.php" class="pt-4 border-t border-slate-800 space-y-2.5 mt-4">
<input type="hidden" name="provider" value="OUTLOOK">
<div>
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">
Application (Client) ID <span class="text-blue-400">*</span>
</label>
<input
type="text"
name="client_id"
value="<?= htmlspecialchars($outlookSettings['client_id'] ?? '') ?>"
placeholder="bijv. e8f498c1-1234-4567-89ab-cdef01234567"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-blue-500 font-mono"
>
<p class="text-[10px] text-slate-500 mt-0.5">Te vinden op de Overzichtspagina (Overview) van uw Azure App Registratie.</p>
</div>
<div>
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">
Client Secret (Geheim) <span class="text-blue-400">*</span>
</label>
<div class="relative">
<input
type="password"
id="msClientSecret"
name="client_secret"
value="<?= htmlspecialchars(!empty($outlookSettings['client_secret']) && $outlookSettings['client_secret'] !== '••••••••••••••••' ? $outlookSettings['client_secret'] : '') ?>"
placeholder="Plak hier de Waarde (Value) van uw Client Secret"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 pr-8 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-blue-500 font-mono"
>
<button type="button" onclick="togglePasswordVisibility('msClientSecret', this)" class="absolute right-2 top-2 text-slate-500 hover:text-slate-300 transition" title="Toon / verberg geheim">
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
</button>
</div>
<p class="text-[10px] text-slate-500 mt-0.5">Aangemaakt onder <em>Certificaten en geheimen &gt; Nieuw clientgeheim</em> (kopieer de <strong>Waarde / Value</strong>).</p>
</div>
<div>
<div class="flex items-center justify-between mb-0.5">
<label class="text-[10px] font-semibold text-slate-400">Directory (Tenant) ID</label>
<span class="text-[9px] text-blue-400 font-semibold bg-blue-500/10 px-1.5 py-0.5 rounded">Standaard: common</span>
</div>
<input
type="text"
name="tenant_id"
value="<?= htmlspecialchars(!empty($outlookSettings['tenant_id']) ? $outlookSettings['tenant_id'] : 'common') ?>"
placeholder="common of organizations"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-blue-500 font-mono"
>
<div class="text-[10px] text-slate-400 mt-1.5 leading-relaxed bg-slate-950/80 p-2.5 rounded-lg border border-slate-800/80 space-y-1">
<p class="font-semibold text-slate-300 flex items-center space-x-1">
<span>💡 Wat moet hier staan?</span>
</p>
<p>• Voor uw organisatie: vul uw domein in (bijv. <code>planningsagenda.nl</code>) of uw Tenant GUID.</p>
<p>• Voor multi-tenant apps: vul <code>organizations</code> of <code>common</code> in.</p>
</div>
<?php
$curClientId = !empty($outlookSettings['client_id']) ? $outlookSettings['client_id'] : $appConfig['client_id'];
$curTenantId = !empty($outlookSettings['tenant_id']) ? $outlookSettings['tenant_id'] : (!empty($appConfig['tenant_id']) ? $appConfig['tenant_id'] : 'organizations');
if (!empty($curClientId)):
$adminConsentUrl = "https://login.microsoftonline.com/" . urlencode($curTenantId) . "/v2.0/adminconsent?client_id=" . urlencode($curClientId) . "&redirect_uri=" . urlencode($redirectUri) . "&scope=" . urlencode("https://graph.microsoft.com/.default");
?>
<div class="mt-2 p-2.5 rounded-lg bg-amber-500/10 border border-amber-500/30 text-[11px] flex items-start space-x-2">
<i data-lucide="shield-alert" class="w-4 h-4 flex-shrink-0 mt-0.5 text-amber-400"></i>
<div class="text-slate-300 leading-snug flex-1">
<strong class="text-amber-300 block">Krijgt u "Beheerdersgoedkeuring vereist"?</strong>
<span>Stuur deze officiële Microsoft goedkeuringslink naar uw IT-beheerder om Fociz met 1 klik goed te keuren voor <?= htmlspecialchars($curTenantId) ?>:</span>
<div class="mt-2 flex items-center space-x-2">
<input type="text" readonly id="adminConsentInput" value="<?= htmlspecialchars($adminConsentUrl) ?>" class="bg-slate-950/80 border border-slate-800 rounded px-2 py-1 text-[10px] text-amber-300 font-mono flex-1 focus:outline-none select-all">
<button type="button" onclick="copyToClipboard('adminConsentInput')" class="bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500/40 text-amber-300 text-[10px] font-semibold px-2 py-1 rounded transition flex items-center space-x-1 flex-shrink-0">
<i data-lucide="copy" class="w-3 h-3"></i>
<span>Kopieer Link</span>
</button>
</div>
<div class="mt-1.5">
<a href="<?= htmlspecialchars($adminConsentUrl) ?>" target="_blank" class="inline-flex items-center space-x-1 font-bold text-amber-400 hover:text-white underline">
<span>Zelf beheerder? Open de goedkeuring direct &rarr;</span>
</a>
</div>
</div>
</div>
<?php endif; ?>
</div>
<label class="flex items-center space-x-2 text-xs text-slate-300 pt-1">
<input type="checkbox" name="is_demo_mode" value="1" <?= !empty($configs['OUTLOOK']['is_demo_mode']) ? 'checked' : '' ?> class="rounded bg-slate-900 border-slate-700 text-blue-500">
<span>Gebruik interactieve Sandbox data</span>
</label>
<div class="flex space-x-2 pt-2">
<button type="submit" name="action" value="save" class="flex-1 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold transition">
Instellingen Opslaan
</button>
<button type="submit" name="action" value="connect_oauth" class="flex-1 py-2 rounded-xl bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold transition flex items-center justify-center space-x-1 shadow-md shadow-blue-500/20">
<i data-lucide="key" class="w-3.5 h-3.5"></i>
<span><?= $isOutlookConnected ? 'Opnieuw Koppelen' : 'Koppel Mijn Outlook' ?></span>
</button>
</div>
<?php if ($isOutlookConnected): ?>
<button type="submit" name="action" value="disconnect" onclick="return confirm('Weet u zeker dat u Microsoft Outlook wilt ontkoppelen voor uw account?')" class="w-full py-1.5 text-[11px] text-red-400 hover:text-red-300 transition text-center block">
Mijn Koppeling Verwijderen
</button>
<?php endif; ?>
</form>
<?php else: ?>
<!-- COLLEGA FORMULIER: Eenvoudig & Veilig (1-klik) -->
<div class="pt-4 border-t border-slate-800 mt-4">
<?php if (!$appConfig['is_configured']): ?>
<button type="button" disabled class="w-full py-2.5 rounded-xl bg-slate-800/60 border border-slate-700/60 text-slate-500 text-xs font-semibold cursor-not-allowed flex items-center justify-center space-x-2">
<i data-lucide="lock" class="w-3.5 h-3.5"></i>
<span>Wachten op Configuratie Beheerder</span>
</button>
<?php elseif ($isOutlookConnected): ?>
<form method="POST" action="/integrations.php" class="space-y-2">
<input type="hidden" name="provider" value="OUTLOOK">
<button type="submit" name="action" value="connect_oauth" class="w-full py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold transition flex items-center justify-center space-x-2">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
<span>Opnieuw Aanmelden bij Microsoft</span>
</button>
<button type="submit" name="action" value="disconnect" onclick="return confirm('Weet u zeker dat u uw persoonlijke Outlook agenda wilt ontkoppelen?')" class="w-full py-2 rounded-xl bg-red-500/10 hover:bg-red-500/20 border border-red-500/20 text-red-400 text-xs font-semibold transition flex items-center justify-center space-x-1.5">
<i data-lucide="link-2-off" class="w-3.5 h-3.5"></i>
<span>Mijn Agenda Ontkoppelen</span>
</button>
</form>
<?php else: ?>
<form method="POST" action="/integrations.php">
<input type="hidden" name="provider" value="OUTLOOK">
<button type="submit" name="action" value="connect_oauth" class="w-full py-3 rounded-xl bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold transition flex items-center justify-center space-x-2 shadow-lg shadow-blue-500/25">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
<span>Koppel Mijn Microsoft Outlook Agenda</span>
</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<!-- 2. MICROSOFT TEAMS -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col justify-between relative overflow-hidden backdrop-blur-sm">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="w-10 h-10 rounded-xl bg-indigo-500/10 border border-indigo-500/30 text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="video" class="w-5 h-5"></i>
</div>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-medium flex items-center space-x-1">
<i data-lucide="check-circle" class="w-3 h-3"></i>
<span>Inbegrepen bij Outlook</span>
</span>
</div>
<div>
<h3 class="text-base font-bold text-white mb-1">Microsoft Teams</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Detecteert Teams vergaderingen automatisch vanuit uw Outlook agenda en toont directe 'Deelnemen' knoppen.
</p>
</div>
<div class="bg-slate-950/60 rounded-xl p-3.5 border border-slate-800/80 text-xs space-y-2">
<div class="flex items-center justify-between text-slate-400">
<span>Direct Deelnemen:</span>
<span class="text-emerald-400 font-semibold">Ondersteund</span>
</div>
<div class="flex items-center justify-between text-slate-400">
<span>Meeting detectie:</span>
<span class="text-slate-200">Via Graph API</span>
</div>
<div class="flex items-center justify-between text-slate-400">
<span>Status:</span>
<span class="text-indigo-400 font-semibold"><?= $isOutlookConnected ? 'Actief' : 'Koppel Outlook' ?></span>
</div>
</div>
<div class="p-3 bg-indigo-950/30 border border-indigo-900/50 rounded-xl text-xs text-indigo-200/80">
<p class="leading-relaxed">
Teams vergaderingen worden automatisch uitgelezen zodra Microsoft Outlook is gekoppeld. U hoeft voor Teams geen aparte koppeling te configureren!
</p>
</div>
</div>
<form method="POST" action="/integrations.php" class="pt-4 border-t border-slate-800 space-y-3 mt-4">
<input type="hidden" name="provider" value="TEAMS">
<input type="hidden" name="action" value="save">
<label class="flex items-center space-x-2 text-xs text-slate-300">
<input type="checkbox" name="is_demo_mode" value="1" <?= empty($configs['TEAMS']) || !empty($configs['TEAMS']['is_demo_mode']) ? 'checked' : '' ?> class="rounded bg-slate-900 border-slate-700 text-indigo-500">
<span>Gebruik interactieve Sandbox data</span>
</label>
<button type="submit" class="w-full py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold transition">
Teams Instellingen Opslaan
</button>
</form>
</div>
<!-- 3. ATLASSIAN JIRA -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col justify-between relative overflow-hidden backdrop-blur-sm">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="w-10 h-10 rounded-xl bg-sky-500/10 border border-sky-500/30 text-sky-400 flex items-center justify-center font-bold">
<i data-lucide="trello" class="w-5 h-5"></i>
</div>
<?php if ($isJiraConnected): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-semibold flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
<span>Live Verbonden</span>
</span>
<?php elseif (!empty($configs['JIRA']['is_demo_mode'])): ?>
<span class="px-2.5 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-semibold">
Sandbox Modus
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 border border-slate-700 text-slate-400 text-xs font-semibold">
Niet Gekoppeld
</span>
<?php endif; ?>
</div>
<div>
<h3 class="text-base font-bold text-white mb-1">Atlassian Jira Cloud</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Koppelt uw Jira tickets rechtstreeks aan de Fociz dagplanning en roadmap.
</p>
</div>
<?php if ($isJiraConnected): ?>
<div class="bg-slate-950/70 rounded-xl p-3.5 border border-sky-500/20 text-xs space-y-2">
<div class="flex items-center justify-between text-slate-400">
<span>Ingelogd als:</span>
<span class="text-sky-400 font-semibold truncate max-w-[170px]" title="<?= htmlspecialchars($jiraSettings['jira_email'] ?? '') ?>">
<?= htmlspecialchars($jiraSettings['account_name'] ?: ($jiraSettings['jira_email'] ?? '')) ?>
</span>
</div>
<div class="flex items-center justify-between text-slate-400">
<span>Domein:</span>
<span class="text-slate-200 font-mono text-[11px] truncate max-w-[170px]"><?= htmlspecialchars($jiraSettings['jira_domain'] ?? '-') ?></span>
</div>
<div class="flex items-center justify-between text-slate-400">
<span>Tickets Toegewezen:</span>
<span class="text-emerald-400 font-bold"><?= count($jiraIssues) ?> open taken</span>
</div>
</div>
<?php endif; ?>
<div class="bg-slate-950/50 rounded-xl border border-slate-800/80 p-3 text-xs text-slate-400">
<div class="flex items-center justify-between">
<span class="text-slate-300 font-semibold">Atlassian API Token nodig?</span>
<a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" class="text-sky-400 hover:text-sky-300 font-semibold flex items-center space-x-1">
<span>Genereer Token</span>
<i data-lucide="external-link" class="w-3 h-3"></i>
</a>
</div>
</div>
</div>
<!-- Jira Formulier -->
<form method="POST" action="/integrations.php" class="pt-4 border-t border-slate-800 space-y-2.5 mt-4">
<input type="hidden" name="provider" value="JIRA">
<div>
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">Jira Cloud Domein</label>
<input
type="text"
name="jira_domain"
value="<?= htmlspecialchars($jiraSettings['jira_domain'] ?? fociz_env('JIRA_DOMAIN', '')) ?>"
placeholder="uwbedrijf.atlassian.net"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-sky-500 font-mono"
>
</div>
<div>
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">Atlassian E-mailadres</label>
<input
type="email"
name="jira_email"
value="<?= htmlspecialchars($jiraSettings['jira_email'] ?? fociz_env('JIRA_EMAIL', '')) ?>"
placeholder="collega@uwbedrijf.nl"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-sky-500"
>
</div>
<div>
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">Atlassian API Token</label>
<div class="relative">
<input
type="password"
id="jiraApiTokenInput"
name="jira_api_token"
value="<?= htmlspecialchars(!empty($jiraSettings['jira_api_token']) && $jiraSettings['jira_api_token'] !== '••••••••••••••••' ? $jiraSettings['jira_api_token'] : fociz_env('JIRA_API_TOKEN', '')) ?>"
placeholder="Plak hier uw Atlassian API token"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 pr-8 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-sky-500 font-mono"
>
<button type="button" onclick="togglePasswordVisibility('jiraApiTokenInput', this)" class="absolute right-2 top-2 text-slate-500 hover:text-slate-300 transition" title="Toon / verberg token">
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<label class="flex items-center space-x-2 text-xs text-slate-300 pt-1">
<input type="checkbox" name="is_demo_mode" value="1" <?= empty($configs['JIRA']) || !empty($configs['JIRA']['is_demo_mode']) ? 'checked' : '' ?> class="rounded bg-slate-900 border-slate-700 text-sky-500">
<span>Gebruik interactieve Sandbox data</span>
</label>
<div class="flex space-x-2 pt-2">
<button type="submit" name="action" value="save" class="w-full py-2 rounded-xl bg-sky-600 hover:bg-sky-500 text-white text-xs font-bold transition flex items-center justify-center space-x-1.5 shadow-md shadow-sky-500/20">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span>Verbinding Testen & Opslaan</span>
</button>
</div>
<?php if ($isJiraConnected): ?>
<button type="submit" name="action" value="disconnect" onclick="return confirm('Weet u zeker dat u Atlassian Jira wilt ontkoppelen?')" class="w-full py-1.5 text-[11px] text-red-400 hover:text-red-300 transition text-center block">
Koppeling Verwijderen
</button>
<?php endif; ?>
</form>
</div>
</div>
<!-- Gesynchroniseerde Jira Issues & 1-Click Import naar Dagplanning -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b border-slate-800">
<div class="flex items-center space-x-2">
<i data-lucide="check-square" class="w-5 h-5 text-sky-400"></i>
<h3 class="text-sm font-bold text-white uppercase tracking-wider">
Jira Issues Toegewezen aan U
</h3>
<?php if ($jiraIsDemo): ?>
<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">Sandbox</span>
<?php else: ?>
<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">Live Jira</span>
<?php endif; ?>
</div>
<div class="flex items-center space-x-3">
<span class="text-xs text-slate-400 font-mono"><?= count($jiraIssues) ?> issues</span>
<a href="/integrations.php?refresh_jira=1" class="flex items-center space-x-1 text-xs text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 px-2.5 py-1 rounded-lg border border-slate-700 transition">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
<span>Verversen</span>
</a>
</div>
</div>
<?php if (empty($jiraIssues)): ?>
<div class="text-center py-10 text-slate-500 text-xs">
Geen openstaande tickets gevonden die aan u zijn toegewezen in Jira.
</div>
<?php else: ?>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<?php foreach ($jiraIssues as $issue):
$priority = $issue['priority'] ?? 'Medium';
$priorityClass = match(strtolower($priority)) {
'highest', 'high', 'hoog' => 'text-red-400 bg-red-500/10 border-red-500/20',
'medium', 'gemiddeld' => 'text-amber-400 bg-amber-500/10 border-amber-500/20',
default => 'text-slate-400 bg-slate-800/60 border-slate-700',
};
?>
<div class="bg-slate-950/70 border border-slate-800/80 hover:border-slate-700 rounded-xl p-4 flex flex-col justify-between transition group">
<div>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="font-mono text-xs font-bold text-sky-400 bg-sky-500/10 border border-sky-500/20 px-2 py-0.5 rounded">
<?= htmlspecialchars($issue['key']) ?>
</span>
<span class="text-[10px] text-slate-400 font-medium truncate max-w-[140px]">
<?= htmlspecialchars($issue['project'] ?? '') ?>
</span>
</div>
<span class="text-[10px] font-semibold px-2 py-0.5 rounded bg-slate-800 text-slate-300">
<?= htmlspecialchars($issue['status']) ?>
</span>
</div>
<h4 class="text-xs font-bold text-white leading-snug group-hover:text-sky-300 transition">
<?= htmlspecialchars($issue['summary']) ?>
</h4>
<div class="flex items-center space-x-3 mt-2 text-[10px] text-slate-500">
<span>Type: <?= htmlspecialchars($issue['issue_type'] ?? 'Issue') ?></span>
<?php if (!empty($issue['due_date'])): ?>
<span>Vervaldatum: <?= htmlspecialchars($issue['due_date']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
<span class="text-[10px] font-bold px-2 py-0.5 rounded border <?= $priorityClass ?>">
<?= htmlspecialchars($priority) ?>
</span>
<div class="flex items-center space-x-2">
<?php if (!empty($issue['url']) && $issue['url'] !== '#'): ?>
<a href="<?= htmlspecialchars($issue['url']) ?>" target="_blank" class="p-1 rounded-lg text-slate-500 hover:text-sky-400 transition" title="Open in Jira">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
<button
onclick="importJiraToTasks(this, '<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>', '<?= htmlspecialchars(addslashes($issue['url'] ?? '')) ?>', 'P1')"
class="flex items-center space-x-1 text-[11px] font-semibold text-emerald-400 hover:text-emerald-300 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 px-2.5 py-1 rounded-lg transition"
>
<i data-lucide="plus" class="w-3 h-3"></i>
<span>Plan als P1</span>
</button>
<button
onclick="importJiraToTasks(this, '<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>', '<?= htmlspecialchars(addslashes($issue['url'] ?? '')) ?>', 'P2')"
class="flex items-center space-x-1 text-[11px] font-semibold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 border border-slate-700 px-2.5 py-1 rounded-lg transition"
>
<span>Plan P2</span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<script>
function copyToClipboard(elementId) {
const el = document.getElementById(elementId);
if (!el) return;
navigator.clipboard.writeText(el.value).then(() => {
alert('Omleidings-URI gekopieerd naar klembord!');
}).catch(() => {
el.select();
document.execCommand('copy');
alert('Omleidings-URI gekopieerd!');
});
}
function togglePasswordVisibility(inputId, btn) {
const input = document.getElementById(inputId);
if (!input) return;
const isNowText = input.type === 'password';
input.type = isNowText ? 'text' : 'password';
if (btn) {
btn.innerHTML = isNowText
? '<i data-lucide="eye-off" class="w-3.5 h-3.5 text-blue-400"></i>'
: '<i data-lucide="eye" class="w-3.5 h-3.5 text-slate-500"></i>';
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
}
async function importJiraToTasks(btn, title, jiraKey, url, priority = 'P1') {
const originalHtml = btn.innerHTML;
btn.innerText = 'Toevoegen...';
btn.disabled = true;
try {
const res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: `[${jiraKey}] ${title}`,
priority: priority,
source: 'JIRA',
external_id: jiraKey,
external_url: url,
date: '<?= date('Y-m-d') ?>'
})
});
const data = await res.json();
if (data.success) {
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i><span>Gepland!</span>';
btn.className = 'flex items-center space-x-1 text-[11px] font-semibold text-emerald-400 bg-emerald-500/20 border border-emerald-500/30 px-2.5 py-1 rounded-lg';
if (typeof lucide !== 'undefined') lucide.createIcons();
} else {
btn.innerHTML = originalHtml;
btn.disabled = false;
alert(data.error || 'Kon taak niet toevoegen');
}
} catch(err) {
btn.innerHTML = originalHtml;
btn.disabled = false;
alert('Netwerkfout bij toevoegen van taak');
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
+3 -3
View File
@@ -137,7 +137,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
</button> </button>
</div> </div>
<?php else: ?> <?php else: ?>
<a href="/integrations.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-800/80 border border-slate-700 text-slate-300 hover:text-white hover:bg-slate-700 text-xs font-medium transition"> <a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-800/80 border border-slate-700 text-slate-300 hover:text-white hover:bg-slate-700 text-xs font-medium transition">
<i data-lucide="link-2" class="w-3.5 h-3.5 text-blue-400"></i> <i data-lucide="link-2" class="w-3.5 h-3.5 text-blue-400"></i>
<span>Koppel Outlook</span> <span>Koppel Outlook</span>
</a> </a>
@@ -149,7 +149,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
<span>Jira: <?= count($jiraIssues) ?> tickets</span> <span>Jira: <?= count($jiraIssues) ?> tickets</span>
</a> </a>
<?php else: ?> <?php else: ?>
<a href="/integrations.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-800/80 border border-slate-700 text-slate-300 hover:text-white hover:bg-slate-700 text-xs font-medium transition"> <a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-800/80 border border-slate-700 text-slate-300 hover:text-white hover:bg-slate-700 text-xs font-medium transition">
<i data-lucide="trello" class="w-3.5 h-3.5 text-sky-400"></i> <i data-lucide="trello" class="w-3.5 h-3.5 text-sky-400"></i>
<span>Koppel Jira</span> <span>Koppel Jira</span>
</a> </a>
@@ -353,7 +353,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
</span> </span>
</div> </div>
<a href="/integrations.php" class="text-xs text-slate-400 hover:text-sky-300 transition flex items-center space-x-1"> <a href="/profile.php" class="text-xs text-slate-400 hover:text-sky-300 transition flex items-center space-x-1">
<span>Beheren</span> <span>Beheren</span>
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i> <i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
</a> </a>
+703
View File
@@ -0,0 +1,703 @@
<?php
/**
* Fociz Persoonlijk Profiel & Koppelingen Portaal (Voor alle gebruikers: Beheerders & Collega's)
*/
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
// Iedere ingelogde gebruiker heeft toegang tot zijn/haar eigen profiel
$user = require_auth();
$userId = (int)$user['id'];
$db = get_db_connection();
$message = null;
$messageType = 'success';
if (!empty($_GET['success'])) {
$message = $_GET['success'];
$messageType = 'success';
} elseif (!empty($_GET['error'])) {
$message = $_GET['error'];
$messageType = 'error';
}
// POST Verwerking VÓÓR headers zodat redirects en mutaties soepel verlopen
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// 1. Outlook koppelen via OAuth
if ($action === 'connect_outlook') {
$appConfig = microsoft_get_app_config();
if (!$appConfig['is_configured']) {
$message = ($user['role'] === 'ADMIN')
? "Configureer eerst de Application (Client) ID en het Client Secret in het Systeembeheer."
: "Microsoft 365 is nog niet ingesteld door de beheerder van uw organisatie. Vraag uw beheerder om de koppeling te activeren.";
$messageType = 'error';
} else {
header("Location: /auth/oauth_microsoft.php");
exit;
}
}
// 2. Outlook ontkoppelen (alleen persoonlijke tokens van deze gebruiker wissen)
elseif ($action === 'disconnect_outlook') {
microsoft_disconnect($userId);
header('Location: /profile.php?success=' . urlencode('Uw Microsoft Outlook agenda is succesvol ontkoppeld.'));
exit;
}
// 3. Jira Opslaan & Testen
elseif ($action === 'save_jira') {
$companyDomain = jira_get_company_domain();
$rawDomain = trim($_POST['jira_domain'] ?? '');
$jiraDomain = !empty($rawDomain) ? jira_normalize_domain($rawDomain) : $companyDomain;
$jiraEmail = trim($_POST['jira_email'] ?? '');
$jiraToken = trim($_POST['jira_api_token'] ?? '');
// Haal bestaande instellingen op
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($jiraToken === '' || $jiraToken === '••••••••••••••••') && !empty($existingSettings['jira_api_token']) && $existingSettings['jira_api_token'] !== '••••••••••••••••') {
$jiraToken = $existingSettings['jira_api_token'];
}
$accountName = $existingSettings['account_name'] ?? '';
$accountId = $existingSettings['account_id'] ?? '';
if (!empty($jiraDomain) && !empty($jiraEmail) && !empty($jiraToken)) {
$testRes = jira_test_connection($jiraDomain, $jiraEmail, $jiraToken);
if (!$testRes['success']) {
$message = "Jira Verbindingsfout: " . $testRes['error'];
$messageType = 'error';
} else {
$accountName = $testRes['user']['displayName'] ?? 'Jira Gebruiker';
$accountId = $testRes['user']['accountId'] ?? '';
$message = "Verbinding met Jira geslaagd! Ingelogd als: {$accountName}";
$messageType = 'success';
}
} else {
$message = "Jira instellingen opgeslagen.";
$messageType = 'success';
}
$newSettings = [
'jira_domain' => $jiraDomain,
'jira_email' => $jiraEmail,
'jira_api_token' => $jiraToken,
'account_name' => $accountName,
'account_id' => $accountId,
];
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'JIRA', 1, 0, ?, NOW())
ON DUPLICATE KEY UPDATE
is_demo_mode = 0,
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, json_encode($newSettings)]);
unset($_SESSION["jira_issues_user_{$userId}"]);
}
// 4. Jira ontkoppelen
elseif ($action === 'disconnect_jira') {
jira_disconnect($userId);
header('Location: /profile.php?success=' . urlencode('Atlassian Jira is succesvol ontkoppeld.'));
exit;
}
// 5. Notificatievoorkeuren opslaan
elseif ($action === 'save_notifications') {
$settings = [
'enabled' => isset($_POST['notif_enabled']) ? 1 : 0,
'lead_time_minutes' => (int)($_POST['lead_time_minutes'] ?? 5),
'repeat_count' => (int)($_POST['repeat_count'] ?? 2),
'repeat_interval_minutes' => (int)($_POST['repeat_interval_minutes'] ?? 2),
'sound_enabled' => isset($_POST['sound_enabled']) ? 1 : 0,
];
update_user_notification_settings($userId, $settings);
$message = "Uw persoonlijke notificatievoorkeuren zijn succesvol opgeslagen.";
$messageType = 'success';
}
// 6. Profielnaam bijwerken
elseif ($action === 'update_profile') {
$name = trim($_POST['name'] ?? '');
if (strlen($name) < 2) {
$message = "Voer een geldige naam in van minimaal 2 tekens.";
$messageType = 'error';
} else {
$stmt = $db->prepare("UPDATE users SET name = ? WHERE id = ?");
$stmt->execute([$name, $userId]);
$_SESSION['user_name'] = $name;
$user['name'] = $name;
$message = "Uw profielnaam is succesvol bijgewerkt.";
$messageType = 'success';
}
}
// 7. Wachtwoord wijzigen
elseif ($action === 'change_password') {
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$stmtU = $db->prepare("SELECT password_hash FROM users WHERE id = ?");
$stmtU->execute([$userId]);
$storedHash = $stmtU->fetchColumn();
if (!password_verify($currentPassword, $storedHash)) {
$message = "Uw huidige wachtwoord is niet correct ingevoerd.";
$messageType = 'error';
} elseif (strlen($newPassword) < 8) {
$message = "Het nieuwe wachtwoord moet minimaal 8 tekens lang zijn.";
$messageType = 'error';
} elseif ($newPassword !== $confirmPassword) {
$message = "Het nieuwe wachtwoord en de bevestiging komen niet overeen.";
$messageType = 'error';
} else {
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
$stmtUp = $db->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$stmtUp->execute([$newHash, $userId]);
$message = "Uw wachtwoord is succesvol gewijzigd.";
$messageType = 'success';
}
}
}
// Actuele gegevens ophalen
$stmtUser = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmtUser->execute([$userId]);
$userProfile = $stmtUser->fetch();
// Outlook status ophalen
$stmtOutlook = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtOutlook->execute([$userId]);
$outlookRow = $stmtOutlook->fetch();
$isOutlookConnected = !empty($outlookRow['access_token']) && empty($outlookRow['is_demo_mode']);
$outlookSettings = json_decode($outlookRow['settings'] ?? '{}', true) ?: [];
// Jira status ophalen
$stmtJira = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
$stmtJira->execute([$userId]);
$jiraRow = $stmtJira->fetch();
$jiraSettings = json_decode($jiraRow['settings'] ?? '{}', true) ?: [];
$isJiraConnected = !empty($jiraSettings['jira_api_token']);
$companyJiraDomain = jira_get_company_domain();
$activeJiraDomain = !empty($jiraSettings['jira_domain']) ? $jiraSettings['jira_domain'] : $companyJiraDomain;
// Centrale Azure App configuratie status
$appConfig = microsoft_get_app_config();
// Notificatie-instellingen
$notifSettings = get_user_notification_settings($userId);
$pageTitle = 'Mijn Profiel & Koppelingen — Fociz';
require_once __DIR__ . '/includes/header.php';
?>
<div class="space-y-8 max-w-5xl mx-auto pb-12">
<!-- Profiel Header Card -->
<div class="relative overflow-hidden bg-slate-900/90 border border-slate-800 rounded-3xl p-6 sm:p-8 shadow-2xl backdrop-blur-xl">
<div class="absolute -top-20 -right-20 w-72 h-72 bg-emerald-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div class="relative z-10 flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div class="flex items-center space-x-4 sm:space-x-5">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-tr from-emerald-600 to-teal-400 p-0.5 shadow-xl shadow-emerald-500/20 flex-shrink-0">
<div class="w-full h-full rounded-2xl bg-slate-950 flex items-center justify-center font-black text-2xl sm:text-3xl text-emerald-400 uppercase">
<?= strtoupper(substr($userProfile['name'], 0, 1)) ?>
</div>
</div>
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<h1 class="text-2xl sm:text-3xl font-black text-white tracking-tight">
<?= htmlspecialchars($userProfile['name']) ?>
</h1>
<?php if ($userProfile['role'] === 'ADMIN'): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/20 border border-emerald-500/40 text-emerald-400 text-xs font-bold flex items-center space-x-1">
<i data-lucide="shield" class="w-3 h-3"></i>
<span>Beheerder</span>
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-blue-500/20 border border-blue-500/40 text-blue-400 text-xs font-bold flex items-center space-x-1">
<i data-lucide="user" class="w-3 h-3"></i>
<span>Collega</span>
</span>
<?php endif; ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-semibold flex items-center space-x-1">
<i data-lucide="shield-check" class="w-3 h-3"></i>
<span>2FA Beveiligd</span>
</span>
</div>
<p class="text-xs text-slate-400 flex items-center space-x-2">
<span><?= htmlspecialchars($userProfile['email']) ?></span>
<span>•</span>
<span>Lid sinds <?= date('d-m-Y', strtotime($userProfile['created_at'])) ?></span>
</p>
</div>
</div>
<?php if ($userProfile['role'] === 'ADMIN'): ?>
<div class="flex-shrink-0">
<a href="/admin.php" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 text-xs font-semibold transition shadow-md">
<i data-lucide="shield-alert" class="w-4 h-4 text-emerald-400"></i>
<span>Systeembeheer Openen</span>
</a>
</div>
<?php endif; ?>
</div>
</div>
<!-- Meldingen Banner -->
<?php if ($message): ?>
<div class="p-4 rounded-2xl flex items-start space-x-3 text-sm animate-in fade-in duration-200 <?= $messageType === 'success' ? 'bg-emerald-500/10 border border-emerald-500/30 text-emerald-200' : 'bg-red-500/10 border border-red-500/30 text-red-200' ?>">
<i data-lucide="<?= $messageType === 'success' ? 'check-circle-2' : 'alert-triangle' ?>" class="w-5 h-5 flex-shrink-0 mt-0.5 <?= $messageType === 'success' ? 'text-emerald-400' : 'text-red-400' ?>"></i>
<div class="flex-1 font-medium"><?= htmlspecialchars($message) ?></div>
</div>
<?php endif; ?>
<!-- Sectie 1: Mijn Microsoft 365 Outlook Agenda -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 font-bold shadow-md">
<i data-lucide="calendar" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-lg font-bold text-white tracking-tight">Mijn Microsoft 365 Agenda</h2>
<?php if ($isOutlookConnected): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[10px] font-bold flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
<span>Live Verbonden</span>
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 text-slate-400 text-[10px] font-bold">
Niet Gekoppeld
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-400">Synchroniseer uw agenda-afspraken automatisch met uw dagplanning</p>
</div>
</div>
<?php if ($isOutlookConnected): ?>
<div class="flex items-center space-x-2">
<a href="/auth/oauth_microsoft.php" class="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium transition" title="Opnieuw autoriseren bij Microsoft">
Opnieuw Verbinden
</a>
<form method="POST" onsubmit="return confirm('Weet u zeker dat u uw Microsoft Outlook agenda wilt ontkoppelen?');">
<input type="hidden" name="action" value="disconnect_outlook">
<button type="submit" class="px-3 py-1.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 text-xs font-medium transition">
Ontkoppelen
</button>
</form>
</div>
<?php endif; ?>
</div>
<div class="mt-6">
<?php if ($isOutlookConnected): ?>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800 space-y-1.5">
<span class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 block">Gekoppeld Microsoft Account</span>
<span class="text-sm font-bold text-white block truncate"><?= htmlspecialchars($outlookSettings['account_name'] ?? 'Microsoft Gebruiker') ?></span>
<span class="text-xs text-emerald-400 block truncate"><?= htmlspecialchars($outlookSettings['account_email'] ?? $userProfile['email']) ?></span>
</div>
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800 space-y-1.5">
<span class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 block">Microsoft Teams Koppeling</span>
<span class="text-sm font-bold text-white flex items-center space-x-1.5">
<i data-lucide="video" class="w-4 h-4 text-blue-400"></i>
<span>Automatische Meeting Links</span>
</span>
<span class="text-xs text-slate-400 block">Vergaderlinks in uw agenda worden direct klikbaar in uw dagplanning</span>
</div>
</div>
<div class="mt-4 p-3.5 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-300 text-xs flex items-center justify-between">
<div class="flex items-center space-x-2">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-400 flex-shrink-0"></i>
<span>Uw agenda synchroniseert live. Afspraken van vandaag verschijnen direct in uw <a href="/planner.php" class="underline font-semibold text-white">Dagplanning</a>.</span>
</div>
<a href="/planner.php" class="px-3 py-1 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-200 font-semibold text-xs transition flex-shrink-0">
Naar Dagplanning &rarr;
</a>
</div>
<?php else: ?>
<div class="p-6 rounded-2xl bg-slate-950/60 border border-slate-800 text-center space-y-4">
<div class="w-14 h-14 rounded-2xl bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center mx-auto">
<i data-lucide="calendar-plus" class="w-7 h-7"></i>
</div>
<div class="max-w-md mx-auto space-y-1">
<h3 class="text-base font-bold text-white">Verbind uw Microsoft Outlook Agenda</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Krijg uw dagelijkse afspraken automatisch te zien in Fociz en ontvang herinneringen vóórdat uw vergaderingen beginnen.
</p>
</div>
<?php if ($appConfig['is_configured']): ?>
<div>
<form method="POST">
<input type="hidden" name="action" value="connect_outlook">
<button type="submit" class="inline-flex items-center space-x-2 px-6 py-3 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-bold text-xs transition shadow-xl shadow-blue-500/25">
<i data-lucide="log-in" class="w-4 h-4"></i>
<span>Koppel Mijn Outlook Agenda</span>
</button>
</form>
<span class="text-[11px] text-slate-500 block mt-2">U wordt veilig doorgestuurd naar Microsoft om in te loggen</span>
</div>
<?php else: ?>
<div class="p-3.5 rounded-xl bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs max-w-lg mx-auto">
<i data-lucide="alert-circle" class="w-4 h-4 text-amber-400 inline mr-1 -mt-0.5"></i>
Microsoft 365 koppeling is nog niet geconfigureerd door de beheerder.
<?php if ($userProfile['role'] === 'ADMIN'): ?>
<a href="/admin.php" class="underline font-bold text-white ml-1">Stel Azure in via Systeembeheer &rarr;</a>
<?php else: ?>
Vraag uw beheerder om dit in te schakelen.
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Sectie 2: Mijn Atlassian Jira Koppeling -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-400 font-bold shadow-md">
<i data-lucide="check-square" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-lg font-bold text-white tracking-tight">Mijn Atlassian Jira Koppeling</h2>
<?php if ($isJiraConnected): ?>
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[10px] font-bold">
Verbonden
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 text-slate-400 text-[10px] font-bold">
Niet Gekoppeld
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-400">Importeer uw toegewezen Jira issues direct naar uw dagelijkse takenlijst</p>
</div>
</div>
<?php if ($isJiraConnected): ?>
<form method="POST" onsubmit="return confirm('Weet u zeker dat u Jira wilt ontkoppelen?');">
<input type="hidden" name="action" value="disconnect_jira">
<button type="submit" class="px-3 py-1.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 text-xs font-medium transition">
Jira Ontkoppelen
</button>
</form>
<?php endif; ?>
</div>
<form method="POST" class="mt-6 space-y-4">
<input type="hidden" name="action" value="save_jira">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Jira Cloud Domein
</label>
<input type="text" name="jira_domain" value="<?= htmlspecialchars($activeJiraDomain) ?>" placeholder="https://uwbedrijf.atlassian.net" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500 font-mono">
<span class="text-[10px] text-slate-500 mt-1 block">Standaard ingevuld door beheerder indien beschikbaar</span>
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Uw Jira E-mailadres
</label>
<input type="email" name="jira_email" value="<?= htmlspecialchars($jiraSettings['jira_email'] ?? $userProfile['email']) ?>" placeholder="naam@bedrijf.nl" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500">
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-semibold text-slate-300">
Persoonlijk Atlassian API Token
</label>
<a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" rel="noopener noreferrer" class="text-[10px] text-indigo-400 hover:underline flex items-center space-x-1">
<span>API Token Aanmaken</span>
<i data-lucide="external-link" class="w-3 h-3"></i>
</a>
</div>
<div class="relative">
<input type="password" id="jiraApiTokenInput" name="jira_api_token" value="<?= !empty($jiraSettings['jira_api_token']) ? '••••••••••••••••' : '' ?>" placeholder="Plak hier uw Atlassian API token..." class="w-full bg-slate-950 border border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500">
<button type="button" onclick="toggleJiraToken()" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-500 hover:text-slate-300">
<i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i>
</button>
</div>
</div>
<div class="pt-2 flex items-center justify-between">
<?php if ($isJiraConnected && !empty($jiraSettings['account_name'])): ?>
<span class="text-xs text-emerald-400 flex items-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Verbonden als: <strong><?= htmlspecialchars($jiraSettings['account_name']) ?></strong></span>
</span>
<?php else: ?>
<span class="text-[11px] text-slate-400">Verbinding wordt automatisch gevalideerd bij het opslaan</span>
<?php endif; ?>
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-lg shadow-indigo-500/20">
Jira Opslaan & Testen
</button>
</div>
</form>
</div>
<!-- Sectie 3: Mijn Meldingen & Agenda Push Notificaties -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl">
<div class="flex items-center space-x-3.5 pb-6 border-b border-slate-800/80">
<div class="w-12 h-12 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 font-bold shadow-md">
<i data-lucide="bell" class="w-6 h-6"></i>
</div>
<div>
<h2 class="text-lg font-bold text-white tracking-tight">Mijn Meldingen & Agenda Herinneringen</h2>
<p class="text-xs text-slate-400">Krijg realtime meldingen vanuit uw browser zodra een afspraak nadert</p>
</div>
</div>
<div class="mt-6 space-y-5">
<!-- Browser Permissie Banner in Profiel -->
<div id="profileNotifPermGranted" class="hidden p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400 flex-shrink-0"></i>
<div>
<strong class="block text-xs text-white">Browser Pushberichten zijn Actief</strong>
<span class="text-[11px] text-emerald-300/80">Uw browser heeft toestemming om bureaubladmeldingen weer te geven.</span>
</div>
</div>
<button type="button" onclick="focizTestNotification()" class="px-3 py-1.5 rounded-xl bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 font-bold text-xs transition flex items-center space-x-1.5 flex-shrink-0">
<i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Verzend Test Melding</span>
</button>
</div>
<div id="profileNotifPermRequest" class="p-4 rounded-2xl bg-blue-500/10 border border-blue-500/30 text-blue-200 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex items-center space-x-2.5">
<i data-lucide="bell-ring" class="w-5 h-5 text-blue-400 flex-shrink-0"></i>
<div>
<strong class="block text-xs text-white">Toestemming van Browser Vereist</strong>
<span class="text-[11px] text-blue-300/80">Klik op de knop om pushmeldingen in te schakelen voor Fociz.</span>
</div>
</div>
<button type="button" onclick="focizRequestPermission()" class="px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-bold text-xs transition shadow flex-shrink-0">
Toestemming Verlenen
</button>
</div>
<div id="profileNotifPermDenied" class="hidden p-4 rounded-2xl bg-red-500/10 border border-red-500/30 text-red-300 text-xs leading-relaxed">
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-400 inline mr-1 -mt-0.5"></i>
Meldingen zijn momenteel geblokkeerd in uw browser. Klik links in de adresbalk op het slotje/site-instellingen en zet "Meldingen" op "Toestaan".
</div>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="save_notifications">
<div class="flex items-center justify-between p-4 rounded-2xl bg-slate-950/60 border border-slate-800">
<div>
<span class="text-xs font-bold text-white block">Meldingen Actief</span>
<span class="text-[11px] text-slate-400">Automatisch waarschuwen bij naderende afspraken</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="notif_enabled" <?= !empty($notifSettings['enabled']) ? 'checked' : '' ?> class="sr-only peer">
<div class="w-11 h-6 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-emerald-600"></div>
</label>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Vooraankondiging
</label>
<select name="lead_time_minutes" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white focus:outline-none focus:border-emerald-500">
<option value="0" <?= (int)$notifSettings['lead_time_minutes'] === 0 ? 'selected' : '' ?>>0 minuten (Precies op aanvang)</option>
<option value="2" <?= (int)$notifSettings['lead_time_minutes'] === 2 ? 'selected' : '' ?>>2 minuten van tevoren</option>
<option value="5" <?= (int)$notifSettings['lead_time_minutes'] === 5 ? 'selected' : '' ?>>5 minuten van tevoren (Aanbevolen)</option>
<option value="10" <?= (int)$notifSettings['lead_time_minutes'] === 10 ? 'selected' : '' ?>>10 minuten van tevoren</option>
<option value="15" <?= (int)$notifSettings['lead_time_minutes'] === 15 ? 'selected' : '' ?>>15 minuten van tevoren</option>
<option value="30" <?= (int)$notifSettings['lead_time_minutes'] === 30 ? 'selected' : '' ?>>30 minuten van tevoren</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Aantal herhalingen
</label>
<select name="repeat_count" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white focus:outline-none focus:border-emerald-500">
<option value="1" <?= (int)$notifSettings['repeat_count'] === 1 ? 'selected' : '' ?>>1x (Alleen eerste melding)</option>
<option value="2" <?= (int)$notifSettings['repeat_count'] === 2 ? 'selected' : '' ?>>2x herhalen (Aanbevolen)</option>
<option value="3" <?= (int)$notifSettings['repeat_count'] === 3 ? 'selected' : '' ?>>3x herhalen</option>
<option value="5" <?= (int)$notifSettings['repeat_count'] === 5 ? 'selected' : '' ?>>5x herhalen</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Herhalingsinterval
</label>
<select name="repeat_interval_minutes" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white focus:outline-none focus:border-emerald-500">
<option value="1" <?= (int)$notifSettings['repeat_interval_minutes'] === 1 ? 'selected' : '' ?>>Elke 1 minuut</option>
<option value="2" <?= (int)$notifSettings['repeat_interval_minutes'] === 2 ? 'selected' : '' ?>>Elke 2 minuten</option>
<option value="3" <?= (int)$notifSettings['repeat_interval_minutes'] === 3 ? 'selected' : '' ?>>Elke 3 minuten</option>
<option value="5" <?= (int)$notifSettings['repeat_interval_minutes'] === 5 ? 'selected' : '' ?>>Elke 5 minuten</option>
</select>
</div>
</div>
<div class="flex items-center justify-between p-4 rounded-2xl bg-slate-950/60 border border-slate-800">
<div class="flex items-center space-x-2.5">
<i data-lucide="volume-2" class="w-4 h-4 text-emerald-400"></i>
<div>
<span class="text-xs font-bold text-white block">Subtiel Geluidssignaal</span>
<span class="text-[11px] text-slate-400">Speel een zachte chime af bij binnenkomst van de herinnering</span>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="sound_enabled" <?= !empty($notifSettings['sound_enabled']) ? 'checked' : '' ?> class="sr-only peer">
<div class="w-11 h-6 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-emerald-600"></div>
</label>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-slate-950 text-xs font-bold transition shadow-lg shadow-emerald-600/20">
Notificatievoorkeuren Opslaan
</button>
</div>
</form>
</div>
</div>
<!-- Sectie 4: Account & Beveiliging (Wachtwoord & 2FA) -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Profielnaam formulier -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-4">
<div class="flex items-center space-x-3 pb-4 border-b border-slate-800/80">
<i data-lucide="user" class="w-5 h-5 text-emerald-400"></i>
<h3 class="text-base font-bold text-white">Persoonlijke Gegevens</h3>
</div>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="update_profile">
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
Weergavenaam
</label>
<input type="text" name="name" required value="<?= htmlspecialchars($userProfile['name']) ?>" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-white focus:outline-none focus:border-emerald-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1.5">
E-mailadres
</label>
<input type="email" readonly disabled value="<?= htmlspecialchars($userProfile['email']) ?>" class="w-full bg-slate-950/50 border border-slate-800/60 rounded-xl px-3.5 py-2.5 text-xs text-slate-500 cursor-not-allowed">
<span class="text-[10px] text-slate-500 mt-1 block">E-mailadres is gekoppeld aan uw account en 2FA.</span>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold transition border border-slate-700">
Naam Opslaan
</button>
</div>
</form>
</div>
<!-- Wachtwoord Wijzigen Formulier -->
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-4">
<div class="flex items-center space-x-3 pb-4 border-b border-slate-800/80">
<i data-lucide="key" class="w-5 h-5 text-emerald-400"></i>
<h3 class="text-base font-bold text-white">Wachtwoord Wijzigen</h3>
</div>
<form method="POST" class="space-y-3">
<input type="hidden" name="action" value="change_password">
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1">
Huidig Wachtwoord
</label>
<input type="password" name="current_password" required class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white focus:outline-none focus:border-emerald-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1">
Nieuw Wachtwoord (min. 8 tekens)
</label>
<input type="password" name="new_password" required minlength="8" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white focus:outline-none focus:border-emerald-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-300 mb-1">
Bevestig Nieuw Wachtwoord
</label>
<input type="password" name="confirm_password" required minlength="8" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white focus:outline-none focus:border-emerald-500">
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold transition border border-slate-700">
Wachtwoord Wijzigen
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function toggleJiraToken() {
const input = document.getElementById('jiraApiTokenInput');
const icon = document.getElementById('eyeJiraIcon');
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.setAttribute('data-lucide', 'eye-off');
} else {
input.type = 'password';
icon.setAttribute('data-lucide', 'eye');
}
if (window.lucide) lucide.createIcons();
}
// Sync banner status met browser permissie
document.addEventListener('DOMContentLoaded', () => {
if (!("Notification" in window)) return;
const grantedEl = document.getElementById('profileNotifPermGranted');
const reqEl = document.getElementById('profileNotifPermRequest');
const deniedEl = document.getElementById('profileNotifPermDenied');
if (Notification.permission === 'granted') {
if (grantedEl) grantedEl.classList.remove('hidden');
if (reqEl) reqEl.classList.add('hidden');
if (deniedEl) deniedEl.classList.add('hidden');
} else if (Notification.permission === 'denied') {
if (grantedEl) grantedEl.classList.add('hidden');
if (reqEl) reqEl.classList.add('hidden');
if (deniedEl) deniedEl.classList.remove('hidden');
} else {
if (grantedEl) grantedEl.classList.add('hidden');
if (reqEl) reqEl.classList.remove('hidden');
if (deniedEl) deniedEl.classList.add('hidden');
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
+1 -1
View File
@@ -118,7 +118,7 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
<!-- Jira Issue Key link --> <!-- Jira Issue Key link -->
<?php if (!empty($item['jira_issue_key'])): ?> <?php if (!empty($item['jira_issue_key'])): ?>
<a href="/integrations.php" class="flex items-center space-x-1 px-2 py-0.5 rounded-full bg-blue-950/60 border border-blue-800/60 text-blue-300 text-[10px] font-mono hover:underline"> <a href="/profile.php" class="flex items-center space-x-1 px-2 py-0.5 rounded-full bg-blue-950/60 border border-blue-800/60 text-blue-300 text-[10px] font-mono hover:underline">
<i data-lucide="layers" class="w-3 h-3"></i> <i data-lucide="layers" class="w-3 h-3"></i>
<span><?= htmlspecialchars($item['jira_issue_key']) ?></span> <span><?= htmlspecialchars($item['jira_issue_key']) ?></span>
</a> </a>