738 lines
42 KiB
PHP
738 lines
42 KiB
PHP
<?php
|
|
/**
|
|
* Fociz Integraties Hub (Microsoft Outlook, Microsoft Teams, Atlassian Jira)
|
|
*/
|
|
|
|
require_once __DIR__ . '/includes/auth.php';
|
|
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
|
require_once __DIR__ . '/includes/integrations/jira.php';
|
|
|
|
$user = require_auth();
|
|
$db = get_db_connection();
|
|
$userId = $user['id'];
|
|
|
|
$message = null;
|
|
$messageType = 'success';
|
|
|
|
// Query parameters (bijv. na OAuth redirect)
|
|
if (!empty($_GET['success'])) {
|
|
$message = $_GET['success'];
|
|
$messageType = 'success';
|
|
} elseif (!empty($_GET['error'])) {
|
|
$message = $_GET['error'];
|
|
$messageType = 'error';
|
|
}
|
|
|
|
// 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') {
|
|
$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;
|
|
|
|
// Haal bestaande tokens op
|
|
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
|
$stmtExisting->execute([$userId]);
|
|
$existing = $stmtExisting->fetch();
|
|
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
|
|
|
|
// Als geheim veld leeg is gelaten, behoud het bestaande geheim
|
|
if (empty($clientSecret) && !empty($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)]);
|
|
|
|
// Als gebruiker direct op 'Koppel nu' klikte
|
|
if ($action === 'connect_oauth') {
|
|
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?client_id=" . urlencode($clientId) . "&tenant_id=" . urlencode($tenantId));
|
|
exit;
|
|
}
|
|
} else {
|
|
$message = "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 (empty($jiraToken) && !empty($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';
|
|
?>
|
|
|
|
<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">
|
|
Koppel Microsoft 365 Outlook (voor realtime agenda & Teams meetings) en Atlassian Jira Cloud (om direct uw toegewezen tickets in te plannen).
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Snelle status pills -->
|
|
<div class="flex items-center space-x-2">
|
|
<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; ?>
|
|
|
|
<!-- 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.Read</code>, <code>User.Read</code>, <code>offline_access</code>.</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<!-- Instellingen 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'] ?? fociz_env('MICROSOFT_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="<?= !empty($outlookSettings['client_secret']) ? '••••••••••••••••' : htmlspecialchars(fociz_env('MICROSOFT_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')" class="absolute right-2 top-2 text-slate-500 hover:text-slate-300">
|
|
<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 > 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>• Laat simpelweg <code>common</code> staan (werkt voor elk Microsoft account).</p>
|
|
<p>• Of vul <code>organizations</code> in voor elk zakelijk Microsoft 365 werkaccount.</p>
|
|
<p>• U hoeft alleen uw specifieke Tenant GUID in te vullen als uw Azure app is ingesteld op <em>Single tenant (Alleen mijn eigen organisatie)</em>.</p>
|
|
</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['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">
|
|
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 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?')" 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>
|
|
|
|
<!-- 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="<?= !empty($jiraSettings['jira_api_token']) ? '••••••••••••••••' : htmlspecialchars(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')" class="absolute right-2 top-2 text-slate-500 hover:text-slate-300">
|
|
<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) {
|
|
const input = document.getElementById(inputId);
|
|
if (!input) return;
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
|
}
|
|
|
|
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'; ?>
|