Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8843b7bb9d | |||
| 67b3da5f69 | |||
| bf83949396 | |||
| ce0dc64e76 | |||
| af9c9cc322 | |||
| c7408db956 | |||
| 17b0557d1d | |||
| 36e38b4df3 | |||
| 39f770b28b | |||
| 8caac5c524 | |||
| cc42f16e3b | |||
| 800c3043de | |||
| bf2c1f8a25 | |||
| 4af0a31738 | |||
| 88d6cd8b01 | |||
| c1a9da4173 | |||
| 97a16b8554 | |||
| ec233f3f9c | |||
| f22417e6e3 | |||
| 1ed8c2c7f7 | |||
| 2c5aa29e51 |
+15
-1
@@ -3,10 +3,24 @@ DB_HOST="127.0.0.1"
|
||||
DB_PORT="3306"
|
||||
DB_NAME="fociz"
|
||||
DB_USER="root"
|
||||
DB_PASS="uw_wachtwoord"
|
||||
DB_PASS=""
|
||||
|
||||
# Omgeving: 'development' (knippert rood) of 'live' (knippert groen)
|
||||
APP_ENV="development"
|
||||
|
||||
# Applicatie URL (voor uitnodigingslinks)
|
||||
APP_URL="https://uw-domein.nl"
|
||||
|
||||
# Applicatie Geheime Sleutel (willekeurige tekenreeks)
|
||||
APP_SECRET="fociz-maria-db-super-secret-key-998877"
|
||||
|
||||
# Microsoft 365 / Outlook (Optionele globale defaults; kan ook via web UI worden ingesteld)
|
||||
MICROSOFT_CLIENT_ID=""
|
||||
MICROSOFT_CLIENT_SECRET=""
|
||||
MICROSOFT_TENANT_ID="common"
|
||||
|
||||
# Atlassian Jira Cloud (Optionele globale defaults; kan ook via web UI worden ingesteld)
|
||||
JIRA_DOMAIN=""
|
||||
JIRA_EMAIL=""
|
||||
JIRA_API_TOKEN=""
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Zorg dat omgevingsspecifieke bestanden nooit worden overschreven bij merges tussen dev en main
|
||||
config/config.php merge=ours
|
||||
includes/env_badge.php merge=ours
|
||||
@@ -1,16 +0,0 @@
|
||||
# Fociz Apache Configuratie
|
||||
DirectoryIndex index.php index.html
|
||||
|
||||
# UTF-8 standaard
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
# Blokkeer directe toegang tot SQL schema en configuratiebestanden
|
||||
<FilesMatch "^(schema\.sql|\.env|\.git.*)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# PHP foutmeldingen in productie (kan uitgeschakeld worden indien gewenst)
|
||||
php_flag display_errors Off
|
||||
php_value error_reporting E_ALL
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
# Installeer MariaDB/MySQL drivers
|
||||
RUN docker-php-ext-install pdo pdo_mysql mysqli
|
||||
|
||||
# Activeer Apache mod_rewrite voor schone URL's
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# Zorg dat Apache op poort 80 luistert
|
||||
EXPOSE 80
|
||||
@@ -85,9 +85,9 @@ APP_URL="https://uw-domein.nl"
|
||||
```
|
||||
|
||||
### 3. Automatische Tabellen Aanmaak
|
||||
U hoeft **niet** handmatig SQL-commando's uit te voeren: zodra u de website voor het eerst opent in uw browser, detecteert Fociz automatisch MariaDB en worden alle tabellen en relaties uit `schema.sql` vanzelf aangemaakt!
|
||||
U hoeft **niet** handmatig SQL-commando's uit te voeren: zodra u de website voor het eerst opent in uw browser, detecteert Fociz automatisch MariaDB en worden alle tabellen en relaties uit `config/schema.sql` vanzelf aangemaakt!
|
||||
|
||||
*(Optioneel kunt u `schema.sql` ook zelf importeren via phpMyAdmin of de MariaDB CLI).*
|
||||
*(Optioneel kunt u `config/schema.sql` ook zelf importeren via phpMyAdmin of de MariaDB CLI).*
|
||||
|
||||
### 4. Eerste Beheerder Instellen
|
||||
1. Open uw browser en ga naar `https://uw-domein.nl`.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX API voor Integraties (Outlook, Teams, 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();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
|
||||
|
||||
try {
|
||||
// 1. Jira Verbinding Testen & Optioneel Opslaan
|
||||
if ($action === 'test_jira') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$domain = trim($input['jira_domain'] ?? '');
|
||||
$email = trim($input['jira_email'] ?? '');
|
||||
$apiToken = trim($input['jira_api_token'] ?? '');
|
||||
$save = !empty($input['save']);
|
||||
|
||||
// Als er geen nieuw token is meegegeven, kijk of er een opgeslagen token is
|
||||
if (empty($apiToken)) {
|
||||
$stmt = $db->prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
|
||||
$stmt->execute([$user['id']]);
|
||||
$saved = json_decode($stmt->fetchColumn() ?: '{}', true);
|
||||
$apiToken = $saved['jira_api_token'] ?? '';
|
||||
if (empty($domain)) $domain = $saved['jira_domain'] ?? '';
|
||||
if (empty($email)) $email = $saved['jira_email'] ?? '';
|
||||
}
|
||||
|
||||
$res = jira_test_connection($domain, $email, $apiToken);
|
||||
|
||||
if ($res['success'] && $save) {
|
||||
$jiraUser = $res['user'];
|
||||
$settings = json_encode([
|
||||
'jira_domain' => jira_normalize_domain($domain),
|
||||
'jira_email' => $email,
|
||||
'jira_api_token' => $apiToken,
|
||||
'account_name' => $jiraUser['displayName'] ?? '',
|
||||
'account_id' => $jiraUser['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_enabled = 1,
|
||||
is_demo_mode = 0,
|
||||
settings = VALUES(settings),
|
||||
last_sync_at = NOW()
|
||||
");
|
||||
$stmt->execute([$user['id'], $settings]);
|
||||
|
||||
// Wis eventuele oude sessie cache
|
||||
unset($_SESSION["jira_issues_user_{$user['id']}"]);
|
||||
}
|
||||
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Jira Issues Ophalen (Toegewezen aan de gebruiker)
|
||||
if ($action === 'get_jira_issues') {
|
||||
$force = !empty($_GET['force']) || !empty($_POST['force']);
|
||||
$res = jira_get_user_issues($user['id'], $force);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Outlook Agenda Afspraken Ophalen
|
||||
if ($action === 'get_outlook_events') {
|
||||
$date = $_GET['date'] ?? date('Y-m-d');
|
||||
$events = microsoft_fetch_today_events($user['id'], $date);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'date' => $date,
|
||||
'events' => $events,
|
||||
'count' => count($events),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Integratie Ontkoppelen
|
||||
if ($action === 'disconnect') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$provider = strtoupper(trim($input['provider'] ?? ''));
|
||||
|
||||
if ($provider === 'OUTLOOK') {
|
||||
microsoft_disconnect($user['id']);
|
||||
echo json_encode(['success' => true, 'message' => 'Microsoft Outlook ontkoppeld']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($provider === 'JIRA') {
|
||||
jira_disconnect($user['id']);
|
||||
echo json_encode(['success' => true, 'message' => 'Atlassian Jira ontkoppeld']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['error' => 'Onbekende provider']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['error' => 'Onbekende actie']);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
+7
-3
@@ -53,11 +53,15 @@ try {
|
||||
$stmtPlan->execute([$user['id'], $date]);
|
||||
$planId = $db->lastInsertId();
|
||||
|
||||
$source = !empty($data['source']) ? substr(trim($data['source']), 0, 50) : 'FOCIZ';
|
||||
$externalId = !empty($data['external_id']) ? substr(trim($data['external_id']), 0, 100) : null;
|
||||
$externalUrl = !empty($data['external_url']) ? trim($data['external_url']) : null;
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'TODO')
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status, source, external_id, external_url)
|
||||
VALUES (?, ?, ?, ?, ?, 'TODO', ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart]);
|
||||
$stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $source, $externalId, $externalUrl]);
|
||||
$taskId = (int)$db->lastInsertId();
|
||||
|
||||
echo json_encode([
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Microsoft 365 OAuth 2.0 Callback Handler
|
||||
* Verwerkt de autorisatiecode, wisselt tokens uit en slaat de verbinding op.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
|
||||
$user = require_auth();
|
||||
$db = get_db_connection();
|
||||
|
||||
// 1. Foutafhandeling vanuit Microsoft
|
||||
if (!empty($_GET['error'])) {
|
||||
$errorDescription = $_GET['error_description'] ?? $_GET['error'];
|
||||
header('Location: /integrations.php?error=' . urlencode("Microsoft melding: {$errorDescription}"));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. CSRF State validatie
|
||||
$receivedState = $_GET['state'] ?? '';
|
||||
$expectedState = $_SESSION['ms_oauth_state'] ?? '';
|
||||
unset($_SESSION['ms_oauth_state']);
|
||||
|
||||
if (empty($receivedState) || !hash_equals($expectedState, $receivedState)) {
|
||||
header('Location: /integrations.php?error=' . urlencode('Ongeldige sessie of CSRF-controle mislukt. Probeer opnieuw in te loggen via Microsoft.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Autorisatiecode controleren
|
||||
$code = $_GET['code'] ?? '';
|
||||
if (empty($code)) {
|
||||
header('Location: /integrations.php?error=' . urlencode('Geen autorisatiecode ontvangen van Microsoft.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Instellingen ophalen
|
||||
$stmt = $db->prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
||||
$stmt->execute([$user['id']]);
|
||||
$row = $stmt->fetch();
|
||||
$savedSettings = json_decode($row['settings'] ?? '{}', true) ?: [];
|
||||
|
||||
$clientId = !empty($savedSettings['client_id']) ? $savedSettings['client_id'] : fociz_env('MICROSOFT_CLIENT_ID', '');
|
||||
$clientSecret = !empty($savedSettings['client_secret']) ? $savedSettings['client_secret'] : fociz_env('MICROSOFT_CLIENT_SECRET', '');
|
||||
$tenantId = !empty($savedSettings['tenant_id']) ? $savedSettings['tenant_id'] : fociz_env('MICROSOFT_TENANT_ID', 'common');
|
||||
|
||||
if (empty($clientId) || empty($clientSecret)) {
|
||||
header('Location: /integrations.php?error=' . urlencode('Client ID of Client Secret ontbreekt in Fociz. Vul deze eerst in bij de Outlook instellingen.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
|
||||
|
||||
// 5. Code omwisselen voor access & refresh tokens
|
||||
$tokenResult = microsoft_exchange_code($code, $clientId, $clientSecret, $redirectUri, $tenantId);
|
||||
|
||||
if (!$tokenResult['success']) {
|
||||
header('Location: /integrations.php?error=' . urlencode('Token uitwisseling mislukt: ' . $tokenResult['error']));
|
||||
exit;
|
||||
}
|
||||
|
||||
$accessToken = $tokenResult['access_token'];
|
||||
$refreshToken = $tokenResult['refresh_token'];
|
||||
$expiresIn = $tokenResult['expires_in'];
|
||||
$expiresAt = time() + $expiresIn;
|
||||
|
||||
// 6. Microsoft gebruikersprofiel ophalen
|
||||
$profile = microsoft_get_user_profile($accessToken);
|
||||
$accountEmail = $profile['mail'] ?? ($profile['userPrincipalName'] ?? $user['email']);
|
||||
$accountName = $profile['displayName'] ?? 'Microsoft 365 Account';
|
||||
|
||||
// 7. Opslaan in MariaDB
|
||||
$newSettings = array_merge($savedSettings, [
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'tenant_id' => $tenantId,
|
||||
'expires_at' => $expiresAt,
|
||||
'account_email' => $accountEmail,
|
||||
'account_name' => $accountName,
|
||||
]);
|
||||
|
||||
$saveStmt = $db->prepare("
|
||||
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, access_token, refresh_token, settings, last_sync_at)
|
||||
VALUES (?, 'OUTLOOK', 1, 0, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_enabled = 1,
|
||||
is_demo_mode = 0,
|
||||
access_token = VALUES(access_token),
|
||||
refresh_token = VALUES(refresh_token),
|
||||
settings = VALUES(settings),
|
||||
last_sync_at = NOW()
|
||||
");
|
||||
$saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]);
|
||||
|
||||
// 8. Succesvol terugkeren naar de Integraties Hub
|
||||
header('Location: /integrations.php?success=' . urlencode("Microsoft Outlook succesvol gekoppeld als {$accountEmail}!"));
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Startpunt voor Microsoft 365 OAuth 2.0 Flow
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
|
||||
$user = require_auth();
|
||||
$db = get_db_connection();
|
||||
|
||||
// Haal eventueel opgeslagen client_id en tenant_id op uit database
|
||||
$stmt = $db->prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
||||
$stmt->execute([$user['id']]);
|
||||
$row = $stmt->fetch();
|
||||
$savedSettings = json_decode($row['settings'] ?? '{}', true) ?: [];
|
||||
|
||||
$clientId = !empty($_GET['client_id'])
|
||||
? trim($_GET['client_id'])
|
||||
: (!empty($savedSettings['client_id']) ? $savedSettings['client_id'] : fociz_env('MICROSOFT_CLIENT_ID', ''));
|
||||
|
||||
$tenantId = !empty($_GET['tenant_id'])
|
||||
? trim($_GET['tenant_id'])
|
||||
: (!empty($savedSettings['tenant_id']) ? $savedSettings['tenant_id'] : fociz_env('MICROSOFT_TENANT_ID', 'common'));
|
||||
|
||||
if (empty($clientId)) {
|
||||
header('Location: /integrations.php?error=missing_ms_client_id');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Bepaal de dynamische of geconfigureerde redirect URI
|
||||
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
|
||||
|
||||
// Genereer unieke CSRF state
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$_SESSION['ms_oauth_state'] = $state;
|
||||
$_SESSION['ms_oauth_user_id'] = $user['id'];
|
||||
|
||||
// Stuur gebruiker door naar Microsoft login
|
||||
$authUrl = microsoft_get_auth_url($clientId, $redirectUri, $tenantId, $state);
|
||||
header("Location: {$authUrl}");
|
||||
exit;
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ if (has_admin_user()) {
|
||||
$error = null;
|
||||
$success = false;
|
||||
$backupCodes = [];
|
||||
$email = '';
|
||||
|
||||
// Haal of genereer een tijdelijke sessie-sleutel voor de 2FA configuratie
|
||||
if (empty($_SESSION['setup_totp_secret'])) {
|
||||
@@ -148,7 +149,7 @@ $pageTitle = "Fociz — Eerste Initialisatie";
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-300 mb-1">Volledige naam</label>
|
||||
<input type="text" name="name" required placeholder="Bijv. Niek Rabelink" value="<?= htmlspecialchars($_POST['name'] ?? '') ?>" class="w-full bg-slate-800/80 border border-slate-700 rounded-xl px-4 py-2.5 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 text-sm transition">
|
||||
<input type="text" name="name" required placeholder="Bijv. Jan de Vries" value="<?= htmlspecialchars($_POST['name'] ?? '') ?>" class="w-full bg-slate-800/80 border border-slate-700 rounded-xl px-4 py-2.5 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 text-sm transition">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-mysqli": "*"
|
||||
}
|
||||
}
|
||||
|
||||
+77
-24
@@ -1,40 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Applicatie- en Databaseconfiguratie
|
||||
* Fociz Applicatie- en Databaseconfiguratie (MAIN / LIVE BRANCH)
|
||||
* Laadt automatisch .env (of .env.example indien .env niet bestaat)
|
||||
*/
|
||||
|
||||
// Automatisch eenvoudige .env ondersteuning indien aanwezig
|
||||
$envFile = __DIR__ . '/../.env';
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with(trim($line), '#')) continue;
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($name, $value) = explode('=', $line, 2);
|
||||
$name = trim($name);
|
||||
$value = trim($value, " \t\n\r\0\x0B\"'");
|
||||
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
|
||||
putenv(sprintf('%s=%s', $name, $value));
|
||||
$_ENV[$name] = $value;
|
||||
$_SERVER[$name] = $value;
|
||||
$envVars = [];
|
||||
|
||||
// 1. Zoek naar .env of val terug op .env.example
|
||||
$envCandidates = [
|
||||
__DIR__ . '/../.env',
|
||||
__DIR__ . '/../.env.example',
|
||||
];
|
||||
|
||||
foreach ($envCandidates as $envFile) {
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines !== false) {
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
// Negeer lege regels en commentaarregels (# of //)
|
||||
if ($line === '' || str_starts_with($line, '#') || str_starts_with($line, '//')) {
|
||||
continue;
|
||||
}
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($name, $value) = explode('=', $line, 2);
|
||||
$name = trim($name);
|
||||
$value = trim($value);
|
||||
// Verwijder omlijstende aanhalingstekens (" of ')
|
||||
$value = trim($value, " \t\n\r\0\x0B\"'");
|
||||
|
||||
// Sla op in array en zet in globale PHP variabelen
|
||||
$envVars[$name] = $value;
|
||||
$_ENV[$name] = $value;
|
||||
$_SERVER[$name] = $value;
|
||||
@putenv("{$name}={$value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Stop zodra we het eerste geldige bestand hebben ingeladen
|
||||
}
|
||||
}
|
||||
|
||||
// MariaDB Verbindingsgegevens (pas deze aan naar uw server instellingen of gebruik .env)
|
||||
define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
|
||||
define('DB_PORT', getenv('DB_PORT') ?: '3306');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'fociz');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'focizlive_user');
|
||||
define('DB_PASS', getenv('DB_PASS') !== false ? getenv('DB_PASS') : 'F0c1z_Live_Secure_2026!');
|
||||
/**
|
||||
* Veilige helper om een omgevingsvariabele op te vragen
|
||||
*/
|
||||
if (!function_exists('fociz_env')) {
|
||||
function fociz_env(string $key, $default = null) {
|
||||
global $envVars;
|
||||
if (isset($envVars[$key]) && $envVars[$key] !== '') {
|
||||
return $envVars[$key];
|
||||
}
|
||||
if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
|
||||
return $_ENV[$key];
|
||||
}
|
||||
if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') {
|
||||
return $_SERVER[$key];
|
||||
}
|
||||
$val = @getenv($key);
|
||||
if ($val !== false && $val !== '') {
|
||||
return $val;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
// MariaDB Verbindingsgegevens (Live defaults voor main branch; overschrijfbaar via server .env)
|
||||
define('DB_HOST', fociz_env('DB_HOST', '192.168.2.33'));
|
||||
define('DB_PORT', fociz_env('DB_PORT', '3306'));
|
||||
define('DB_NAME', fociz_env('DB_NAME', 'fociz'));
|
||||
define('DB_USER', fociz_env('DB_USER', 'focizlive_user'));
|
||||
define('DB_PASS', fociz_env('DB_PASS', 'F0c1z_Live_Secure_2026!'));
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// Applicatie instellingen
|
||||
// Applicatie instellingen (Live defaults)
|
||||
define('APP_NAME', 'Fociz');
|
||||
define('APP_URL', getenv('APP_URL') ?: ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'localhost')));
|
||||
define('APP_SECRET', getenv('APP_SECRET') ?: 'fociz-maria-db-super-secret-key-998877');
|
||||
define('APP_URL', fociz_env('APP_URL', ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'fociz.nl'))));
|
||||
define('APP_SECRET', fociz_env('APP_SECRET', 'fociz-maria-db-super-secret-key-998877'));
|
||||
define('APP_ENV', fociz_env('APP_ENV', 'live')); // 'live' voor de main branch
|
||||
|
||||
// Tijdzone
|
||||
date_default_timezone_set('Europe/Amsterdam');
|
||||
|
||||
// Foutrapportage: in live verbergen we fouten voor bezoekers
|
||||
if (APP_ENV !== 'live') {
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
} else {
|
||||
ini_set('display_errors', '0');
|
||||
ini_set('display_startup_errors', '0');
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
|
||||
}
|
||||
|
||||
+119
-45
@@ -8,61 +8,135 @@ require_once __DIR__ . '/config.php';
|
||||
function get_db_connection(): PDO {
|
||||
static $pdo = null;
|
||||
|
||||
if ($pdo === null) {
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
DB_HOST,
|
||||
DB_PORT,
|
||||
DB_NAME,
|
||||
DB_CHARSET
|
||||
);
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
// Zorg ervoor dat tabellen bestaan
|
||||
ensure_tables_exist($pdo);
|
||||
} catch (PDOException $e) {
|
||||
// Als de database zelf nog niet bestaat, probeer deze aan te maken indien gebruiker rechten heeft
|
||||
if ($e->getCode() == 1049) {
|
||||
try {
|
||||
$tempDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET);
|
||||
$tempPdo = new PDO($tempDsn, DB_USER, DB_PASS, $options);
|
||||
$tempPdo->exec(sprintf('CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci', DB_NAME));
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
ensure_tables_exist($pdo);
|
||||
return $pdo;
|
||||
} catch (PDOException $ex) {
|
||||
die("Database verbindingsfout: Kan database '" . htmlspecialchars(DB_NAME) . "' niet aanmaken of vinden. Maak deze handmatig aan in MariaDB.");
|
||||
}
|
||||
}
|
||||
die("MariaDB Verbindingsfout: " . htmlspecialchars($e->getMessage()) . "<br><small>Controleer uw instellingen in config/config.php of .env</small>");
|
||||
}
|
||||
if ($pdo !== null) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
// 1. Controleer of de PDO MySQL/MariaDB driver aanwezig is in PHP
|
||||
$availableDrivers = PDO::getAvailableDrivers();
|
||||
if (!in_array('mysql', $availableDrivers)) {
|
||||
$driversList = !empty($availableDrivers) ? implode(', ', $availableDrivers) : 'geen';
|
||||
die("
|
||||
<div style='font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: #090d16; color: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px;'>
|
||||
<div style='max-width: 580px; background: #111827; border: 1px solid #ef4444; border-radius: 16px; padding: 30px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.5);'>
|
||||
<h2 style='color: #ef4444; margin-top: 0;'>⚠️ PHP MySQL Driver Ontbreekt (pdo_mysql)</h2>
|
||||
<p style='color: #94a3b8; font-size: 14px;'>De extensie <strong>pdo_mysql</strong> is niet actief in deze PHP-omgeving.</p>
|
||||
</div>
|
||||
</div>
|
||||
");
|
||||
}
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_TIMEOUT => 5,
|
||||
];
|
||||
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
|
||||
$options[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = true;
|
||||
}
|
||||
|
||||
$dsnWithDb = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
DB_HOST,
|
||||
DB_PORT,
|
||||
DB_NAME,
|
||||
DB_CHARSET
|
||||
);
|
||||
|
||||
// 2. CHECK: Bestaat de database al?
|
||||
// We proberen eerst direct verbinding te maken met de database.
|
||||
// Als de database al bestaat, lukt dit direct zonder database-aanmaakrechten te vereisen!
|
||||
try {
|
||||
$pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options);
|
||||
|
||||
// De database bestaat al! Controleer enkel of de tabellen al aanwezig zijn
|
||||
ensure_tables_exist($pdo);
|
||||
return $pdo;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Foutcode 1049 betekent: Database 'DB_NAME' bestaat nog NIET op de MariaDB server
|
||||
if ($e->getCode() == 1049 || str_contains($e->getMessage(), 'Unknown database')) {
|
||||
// Probeer de database eenmalig aan te maken indien de gebruiker CREATE rechten heeft
|
||||
try {
|
||||
$serverDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET);
|
||||
$serverPdo = new PDO($serverDsn, DB_USER, DB_PASS, $options);
|
||||
$serverPdo->exec(sprintf(
|
||||
'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',
|
||||
DB_NAME
|
||||
));
|
||||
|
||||
// Nu opnieuw verbinden met de nieuw aangemaakte database
|
||||
$pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options);
|
||||
ensure_tables_exist($pdo);
|
||||
return $pdo;
|
||||
|
||||
} catch (PDOException $createEx) {
|
||||
die("
|
||||
<div style='font-family: sans-serif; background: #090d16; color: #fff; padding: 40px; text-align: center; min-height: 100vh; display: flex; align-items: center; justify-content: center;'>
|
||||
<div style='max-width: 500px; background: #111827; border: 1px solid #f59e0b; border-radius: 16px; padding: 30px; text-align: left;'>
|
||||
<h2 style='color: #f59e0b; margin-top: 0;'>⚠️ Database '" . htmlspecialchars(DB_NAME) . "' bestaat nog niet</h2>
|
||||
<p style='color: #94a3b8; font-size: 14px; line-height: 1.6;'>
|
||||
De database <strong>" . htmlspecialchars(DB_NAME) . "</strong> bestaat nog niet op MariaDB server <strong>" . htmlspecialchars(DB_HOST) . "</strong> en de gebruiker <em>" . htmlspecialchars(DB_USER) . "</em> heeft geen rechten om automatisch databases aan te maken.
|
||||
</p>
|
||||
<p style='color: #cbd5e1; font-size: 13px;'>
|
||||
Maak de database <code>" . htmlspecialchars(DB_NAME) . "</code> eenmalig aan via phpMyAdmin, HeidiSQL of de MariaDB CLI:
|
||||
</p>
|
||||
<code style='display: block; background: #000; color: #4ade80; padding: 10px; border-radius: 8px; font-size: 12px; margin: 10px 0;'>
|
||||
CREATE DATABASE IF NOT EXISTS `" . htmlspecialchars(DB_NAME) . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
// Andere verbindingsfout (bijv. verkeerd wachtwoord, poort of host onbereikbaar)
|
||||
die("
|
||||
<div style='font-family: sans-serif; background: #090d16; color: #fff; padding: 40px; text-align: center; min-height: 100vh; display: flex; align-items: center; justify-content: center;'>
|
||||
<div style='max-width: 520px; background: #111827; border: 1px solid #ef4444; border-radius: 16px; padding: 30px; text-align: left;'>
|
||||
<h2 style='color: #ef4444; margin-top: 0;'>MariaDB Verbindingsfout (" . htmlspecialchars((string)$e->getCode()) . ")</h2>
|
||||
<p style='color: #cbd5e1; font-size: 14px;'>" . htmlspecialchars($e->getMessage()) . "</p>
|
||||
<div style='background: #0b0f17; border: 1px solid #1f2937; padding: 15px; border-radius: 10px; font-size: 13px; margin-top: 15px;'>
|
||||
Host: <strong>" . htmlspecialchars(DB_HOST) . "</strong><br>
|
||||
Poort: <strong>" . htmlspecialchars(DB_PORT) . "</strong><br>
|
||||
Database: <strong>" . htmlspecialchars(DB_NAME) . "</strong><br>
|
||||
Gebruiker: <strong>" . htmlspecialchars(DB_USER) . "</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zorgt ervoor dat alle tabellen uit schema.sql aanwezig zijn in MariaDB
|
||||
* Zorgt ervoor dat alle benodigde tabellen uit schema.sql aanwezig zijn in MariaDB
|
||||
* Voert niets uit als de tabellen al bestaan!
|
||||
*/
|
||||
function ensure_tables_exist(PDO $pdo): void {
|
||||
try {
|
||||
// Controleer of de hoofdtabel 'users' al bestaat
|
||||
$result = $pdo->query("SHOW TABLES LIKE 'users'");
|
||||
if ($result->rowCount() === 0) {
|
||||
$schemaFile = __DIR__ . '/../schema.sql';
|
||||
if (file_exists($schemaFile)) {
|
||||
$sql = file_get_contents($schemaFile);
|
||||
$pdo->exec($sql);
|
||||
if ($result && $result->rowCount() > 0) {
|
||||
// Tabellen bestaan al! Geen actie nodig.
|
||||
return;
|
||||
}
|
||||
|
||||
// Tabellen ontbreken nog: voer schema.sql uit statement voor statement
|
||||
$schemaFile = file_exists(__DIR__ . '/schema.sql') ? (__DIR__ . '/schema.sql') : (__DIR__ . '/../schema.sql');
|
||||
if (file_exists($schemaFile)) {
|
||||
$sql = file_get_contents($schemaFile);
|
||||
$statements = array_filter(array_map('trim', explode(';', $sql)));
|
||||
foreach ($statements as $stmt) {
|
||||
// Verwijder eventuele SQL commentaren aan het begin
|
||||
$stmtClean = preg_replace('/^--.*$/m', '', $stmt);
|
||||
$stmtClean = trim($stmtClean);
|
||||
if (!empty($stmtClean)) {
|
||||
$pdo->exec($stmtClean);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// Tabellen bestaan waarschijnlijk al of er is een permissiebeperking
|
||||
} catch (Exception $e) {
|
||||
error_log("Fout bij aanmaken tabellen: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -13,7 +13,6 @@ function start_secure_session(): void {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 7 * 24 * 60 * 60, // 7 dagen
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => $isSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
@@ -26,9 +25,13 @@ function start_secure_session(): void {
|
||||
* Controleert of er al een beheerder bestaat in MariaDB
|
||||
*/
|
||||
function has_admin_user(): bool {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
|
||||
return ((int)$stmt->fetchColumn()) > 0;
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
|
||||
return ((int)$stmt->fetchColumn()) > 0;
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
-13
@@ -9,23 +9,19 @@
|
||||
* - OP DE DEV OMGEVING: $environment = 'development'; (Rood met 'Development' + radar ping)
|
||||
* - OP DE MAIN OMGEVING: $environment = 'live'; (Groen met 'Live' + radar ping)
|
||||
*
|
||||
* U kunt de waarde hieronder aanpassen, of instellen via APP_ENV in .env.
|
||||
* U kunt dit hieronder aanpassen, of instellen via APP_ENV in .env / .env.example!
|
||||
*/
|
||||
|
||||
// Bepaal de omgeving: 'development' of 'live' (of 'main')
|
||||
$environment = 'live'; // <-- ZET DIT OP DE MAIN SERVER OP 'live'
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
// Optionele fallback als het in .env gedefinieerd is
|
||||
if (getenv('APP_ENV')) {
|
||||
$envVal = strtolower(trim(getenv('APP_ENV')));
|
||||
if (in_array($envVal, ['live', 'production', 'prod', 'main'])) {
|
||||
$environment = 'live';
|
||||
} elseif (in_array($envVal, ['dev', 'development', 'staging', 'test'])) {
|
||||
$environment = 'development';
|
||||
}
|
||||
// Standaard waarde voor de main branch: 'live' (kan overschreven worden via APP_ENV)
|
||||
$environment = defined('APP_ENV') ? APP_ENV : 'live';
|
||||
|
||||
if (function_exists('fociz_env') && fociz_env('APP_ENV')) {
|
||||
$environment = fociz_env('APP_ENV');
|
||||
}
|
||||
|
||||
$isLive = in_array(strtolower($environment), ['live', 'production', 'prod', 'main']);
|
||||
$isLive = in_array(strtolower(trim($environment)), ['live', 'production', 'prod', 'main']);
|
||||
?>
|
||||
|
||||
<!-- FLASHY OMGEVINGSINDICATOR LINKS BOVENIN -->
|
||||
@@ -66,4 +62,3 @@ $isLive = in_array(strtolower($environment), ['live', 'production', 'prod', 'mai
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Pure PHP HTTP Client Helper
|
||||
* Ondersteunt cURL met automatische fallback naar stream wrappers (file_get_contents).
|
||||
*/
|
||||
|
||||
if (!function_exists('fociz_http_request')) {
|
||||
/**
|
||||
* Voert een HTTP request uit en retourneert de statuscode, body en geparsede JSON.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options [
|
||||
* 'method' => 'GET'|'POST'|'PUT'|'DELETE',
|
||||
* 'headers' => ['Header-Name: Value'] of ['Header-Name' => 'Value'],
|
||||
* 'body' => string|array,
|
||||
* 'timeout' => int (seconden, default 12),
|
||||
* 'verify_ssl' => bool (default true)
|
||||
* ]
|
||||
* @return array ['status' => int, 'body' => string, 'json' => ?array, 'error' => ?string]
|
||||
*/
|
||||
function fociz_http_request(string $url, array $options = []): array {
|
||||
$method = strtoupper($options['method'] ?? 'GET');
|
||||
$timeout = (int)($options['timeout'] ?? 12);
|
||||
$verifySsl = $options['verify_ssl'] ?? true;
|
||||
|
||||
// Normaliseer headers naar 'Name: Value' regels
|
||||
$rawHeaders = $options['headers'] ?? [];
|
||||
$headerLines = [];
|
||||
foreach ($rawHeaders as $k => $v) {
|
||||
if (is_numeric($k)) {
|
||||
$headerLines[] = $v;
|
||||
} else {
|
||||
$headerLines[] = "{$k}: {$v}";
|
||||
}
|
||||
}
|
||||
|
||||
// Bereid payload voor
|
||||
$body = $options['body'] ?? null;
|
||||
if (is_array($body)) {
|
||||
$hasJsonHeader = false;
|
||||
foreach ($headerLines as $h) {
|
||||
if (stripos($h, 'application/json') !== false) {
|
||||
$hasJsonHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($hasJsonHeader) {
|
||||
$body = json_encode($body);
|
||||
} else {
|
||||
$body = http_build_query($body);
|
||||
$headerLines[] = 'Content-Type: application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
|
||||
// Voeg standaard User-Agent toe als die er niet is
|
||||
$hasUserAgent = false;
|
||||
foreach ($headerLines as $h) {
|
||||
if (stripos($h, 'User-Agent:') !== false) {
|
||||
$hasUserAgent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasUserAgent) {
|
||||
$headerLines[] = 'User-Agent: Fociz-Integrations/1.0';
|
||||
}
|
||||
|
||||
// 1. Probeer cURL indien beschikbaar
|
||||
if (extension_loaded('curl')) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 6);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerLines);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifySsl);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifySsl ? 2 : 0);
|
||||
|
||||
if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
$curlError = curl_error($ch);
|
||||
$statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($responseBody === false && !empty($curlError)) {
|
||||
return [
|
||||
'status' => $statusCode ?: 0,
|
||||
'body' => '',
|
||||
'json' => null,
|
||||
'error' => "cURL fout: {$curlError}"
|
||||
];
|
||||
}
|
||||
|
||||
$parsedJson = json_decode((string)$responseBody, true);
|
||||
|
||||
return [
|
||||
'status' => $statusCode,
|
||||
'body' => (string)$responseBody,
|
||||
'json' => is_array($parsedJson) ? $parsedJson : null,
|
||||
'error' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Fallback: PHP Streams (file_get_contents)
|
||||
$contextOptions = [
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => implode("\r\n", $headerLines),
|
||||
'timeout' => $timeout,
|
||||
'ignore_errors' => true // Zorgt dat we ook bij 4xx / 5xx responses de body en statuscode kunnen lezen
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => $verifySsl,
|
||||
'verify_peer_name' => $verifySsl
|
||||
]
|
||||
];
|
||||
|
||||
if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
|
||||
$contextOptions['http']['content'] = $body;
|
||||
}
|
||||
|
||||
$context = stream_context_create($contextOptions);
|
||||
$responseBody = @file_get_contents($url, false, $context);
|
||||
|
||||
$statusCode = 0;
|
||||
if (!empty($http_response_header)) {
|
||||
// Zoek de laatste HTTP status regel (bij eventuele redirects)
|
||||
foreach (array_reverse($http_response_header) as $hdr) {
|
||||
if (preg_match('#HTTP/\d\.\d\s+(\d{3})#i', $hdr, $matches)) {
|
||||
$statusCode = (int)$matches[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($responseBody === false) {
|
||||
$lastErr = error_get_last();
|
||||
return [
|
||||
'status' => $statusCode ?: 0,
|
||||
'body' => '',
|
||||
'json' => null,
|
||||
'error' => 'Stream fout: ' . ($lastErr['message'] ?? 'Kan verbinding niet voltooien')
|
||||
];
|
||||
}
|
||||
|
||||
$parsedJson = json_decode((string)$responseBody, true);
|
||||
|
||||
return [
|
||||
'status' => $statusCode,
|
||||
'body' => (string)$responseBody,
|
||||
'json' => is_array($parsedJson) ? $parsedJson : null,
|
||||
'error' => null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
/**
|
||||
* Atlassian Jira Cloud REST API Integratie voor Fociz
|
||||
* Beheert verbindingstest, ophalen van aan de gebruiker toegewezen issues en import naar dagtaken.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../http.php';
|
||||
require_once __DIR__ . '/../../config/database.php';
|
||||
|
||||
/**
|
||||
* Normaliseert het Jira domein naar een schone URL zonder slash aan het einde
|
||||
*/
|
||||
function jira_normalize_domain(string $domain): string {
|
||||
$domain = trim($domain);
|
||||
if (!preg_match('#^https?://#i', $domain)) {
|
||||
$domain = 'https://' . $domain;
|
||||
}
|
||||
return rtrim($domain, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Genereert de Basic Authentication header voor Atlassian REST API
|
||||
*/
|
||||
function jira_get_auth_header(string $email, string $apiToken): string {
|
||||
return 'Basic ' . base64_encode(trim($email) . ':' . trim($apiToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test de verbinding met Atlassian Jira Cloud via /rest/api/3/myself
|
||||
*/
|
||||
function jira_test_connection(string $domain, string $email, string $apiToken): array {
|
||||
$cleanDomain = jira_normalize_domain($domain);
|
||||
if (empty($cleanDomain) || empty($email) || empty($apiToken)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Vul een Jira domein, e-mailadres en Atlassian API token in.',
|
||||
];
|
||||
}
|
||||
|
||||
$authHeader = jira_get_auth_header($email, $apiToken);
|
||||
$url = $cleanDomain . '/rest/api/3/myself';
|
||||
|
||||
$response = fociz_http_request($url, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => $authHeader,
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
// Fallback naar api v2 indien v3 niet beschikbaar is
|
||||
if ($response['status'] === 404) {
|
||||
$urlV2 = $cleanDomain . '/rest/api/2/myself';
|
||||
$response = fociz_http_request($urlV2, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => $authHeader,
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($response['status'] === 200 && is_array($response['json'])) {
|
||||
$user = $response['json'];
|
||||
return [
|
||||
'success' => true,
|
||||
'user' => [
|
||||
'displayName' => $user['displayName'] ?? 'Atlassian Gebruiker',
|
||||
'emailAddress' => $user['emailAddress'] ?? $email,
|
||||
'accountId' => $user['accountId'] ?? ($user['key'] ?? ''),
|
||||
'active' => $user['active'] ?? true,
|
||||
'timeZone' => $user['timeZone'] ?? 'Europe/Amsterdam',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$errMsg = 'Verbinding mislukt met Jira.';
|
||||
if ($response['status'] === 401) {
|
||||
$errMsg = 'Authenticatie geweigerd (HTTP 401). Controleer uw e-mailadres en zorg dat u een geldig Atlassian API token gebruikt.';
|
||||
} elseif ($response['status'] === 403) {
|
||||
$errMsg = 'Toegang geweigerd (HTTP 403). Uw account heeft onvoldoende rechten op dit Jira domein.';
|
||||
} elseif ($response['status'] === 404) {
|
||||
$errMsg = 'Jira Cloud domein niet gevonden (HTTP 404). Controleer het ingevoerde domein (bijv. uwbedrijf.atlassian.net).';
|
||||
} elseif (!empty($response['error'])) {
|
||||
$errMsg = "Netwerkfout: {$response['error']}";
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errMsg,
|
||||
'status' => $response['status'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker
|
||||
*/
|
||||
function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array {
|
||||
$cleanDomain = jira_normalize_domain($domain);
|
||||
$authHeader = jira_get_auth_header($email, $apiToken);
|
||||
|
||||
// JQL: Alle openstaande taken toegewezen aan mij, gesorteerd op recent bijgewerkt
|
||||
$jql = 'assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC';
|
||||
|
||||
$params = [
|
||||
'jql' => $jql,
|
||||
'maxResults' => min($maxResults, 100),
|
||||
'fields' => 'summary,status,priority,issuetype,parent,project,duedate,updated',
|
||||
];
|
||||
|
||||
$url = $cleanDomain . '/rest/api/3/search?' . http_build_query($params);
|
||||
|
||||
$response = fociz_http_request($url, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => $authHeader,
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 12,
|
||||
]);
|
||||
|
||||
// Fallback naar api v2
|
||||
if ($response['status'] === 404) {
|
||||
$urlV2 = $cleanDomain . '/rest/api/2/search?' . http_build_query($params);
|
||||
$response = fociz_http_request($urlV2, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => $authHeader,
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 12,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($response['status'] !== 200 || !isset($response['json']['issues'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $response['json']['errorMessages'][0] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen'),
|
||||
'issues' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$rawIssues = $response['json']['issues'];
|
||||
$issues = [];
|
||||
|
||||
foreach ($rawIssues as $item) {
|
||||
$fields = $item['fields'] ?? [];
|
||||
$key = $item['key'] ?? '';
|
||||
$summary = $fields['summary'] ?? '(Geen titel)';
|
||||
$statusName = $fields['status']['name'] ?? 'To Do';
|
||||
$priorityName = $fields['priority']['name'] ?? 'Medium';
|
||||
$issueTypeName = $fields['issuetype']['name'] ?? 'Taak';
|
||||
$projectName = $fields['project']['name'] ?? 'Project';
|
||||
$dueDate = $fields['duedate'] ?? null;
|
||||
|
||||
$issues[] = [
|
||||
'key' => $key,
|
||||
'summary' => $summary,
|
||||
'status' => $statusName,
|
||||
'priority' => $priorityName,
|
||||
'issue_type' => $issueTypeName,
|
||||
'project' => $projectName,
|
||||
'due_date' => $dueDate,
|
||||
'url' => "{$cleanDomain}/browse/{$key}",
|
||||
'source' => 'JIRA_LIVE',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'issues' => $issues,
|
||||
'total' => count($issues),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de Jira tickets op voor een Fociz gebruiker
|
||||
* Schakelt naadloos tussen live Jira tickets en sandbox demotickets
|
||||
*/
|
||||
function jira_get_user_issues(int $userId, bool $forceRefresh = false): array {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA' AND is_enabled = 1");
|
||||
$stmt->execute([$userId]);
|
||||
$integration = $stmt->fetch();
|
||||
|
||||
// Als de integratie niet is ingesteld of in demo mode staat, geef interactieve fallback issues terug
|
||||
if (!$integration || !empty($integration['is_demo_mode'])) {
|
||||
return [
|
||||
'is_demo' => true,
|
||||
'success' => true,
|
||||
'issues' => [
|
||||
[
|
||||
'key' => 'FOC-101',
|
||||
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
|
||||
'status' => 'In Progress',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Fociz Platform',
|
||||
'due_date' => date('Y-m-d', strtotime('+2 days')),
|
||||
'url' => '#',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-102',
|
||||
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
|
||||
'status' => 'To Do',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Feature',
|
||||
'project' => 'Fociz Platform',
|
||||
'due_date' => date('Y-m-d', strtotime('+4 days')),
|
||||
'url' => '#',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-103',
|
||||
'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API',
|
||||
'status' => 'In Review',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Integrations Hub',
|
||||
'due_date' => date('Y-m-d'),
|
||||
'url' => '#',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-104',
|
||||
'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren',
|
||||
'status' => 'To Do',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Roadmap Module',
|
||||
'due_date' => date('Y-m-d', strtotime('+7 days')),
|
||||
'url' => '#',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$settings = json_decode($integration['settings'] ?? '{}', true) ?: [];
|
||||
$domain = $settings['jira_domain'] ?? fociz_env('JIRA_DOMAIN', '');
|
||||
$email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', '');
|
||||
$apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', '');
|
||||
|
||||
if (empty($domain) || empty($email) || empty($apiToken)) {
|
||||
return [
|
||||
'is_demo' => false,
|
||||
'success' => false,
|
||||
'error' => 'Jira configuratie is onvolledig.',
|
||||
'issues' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Sessiecache voor 90 seconden om onnodige Jira API calls bij snelle pagina refreshes te beperken
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
@session_start();
|
||||
}
|
||||
$cacheKey = "jira_issues_user_{$userId}";
|
||||
$cached = $_SESSION[$cacheKey] ?? null;
|
||||
|
||||
if (!$forceRefresh && $cached && is_array($cached) && ($cached['timestamp'] ?? 0) > (time() - 90)) {
|
||||
return [
|
||||
'is_demo' => false,
|
||||
'success' => true,
|
||||
'issues' => $cached['issues'],
|
||||
'from_cache' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$result = jira_fetch_assigned_issues($domain, $email, $apiToken, 50);
|
||||
|
||||
if ($result['success']) {
|
||||
$_SESSION[$cacheKey] = [
|
||||
'timestamp' => time(),
|
||||
'issues' => $result['issues'],
|
||||
];
|
||||
|
||||
// Werk last_sync_at bij
|
||||
$stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?");
|
||||
$stmtUpdate->execute([$integration['id']]);
|
||||
|
||||
return [
|
||||
'is_demo' => false,
|
||||
'success' => true,
|
||||
'issues' => $result['issues'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'is_demo' => false,
|
||||
'success' => false,
|
||||
'error' => $result['error'],
|
||||
'issues' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ontkoppelt Jira integratie voor een gebruiker
|
||||
*/
|
||||
function jira_disconnect(int $userId): bool {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
@session_start();
|
||||
}
|
||||
unset($_SESSION["jira_issues_user_{$userId}"]);
|
||||
return $stmt->execute([$userId]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
/**
|
||||
* Microsoft 365 / Graph API Integratie voor Fociz
|
||||
* Beheert OAuth2 authenticatie, token refresh en kalendersynchronisatie.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../http.php';
|
||||
require_once __DIR__ . '/../../config/database.php';
|
||||
|
||||
const MS_OAUTH_AUTHORIZE_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize';
|
||||
const MS_OAUTH_TOKEN_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
|
||||
const MS_GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0';
|
||||
const MS_DEFAULT_SCOPES = 'offline_access openid profile email User.Read Calendars.Read';
|
||||
|
||||
/**
|
||||
* Genereert de Microsoft OAuth2 login URL
|
||||
*/
|
||||
function microsoft_get_auth_url(string $clientId, string $redirectUri, string $tenantId = 'common', string $state = ''): string {
|
||||
$tenant = !empty($tenantId) ? trim($tenantId) : 'common';
|
||||
$baseUrl = sprintf(MS_OAUTH_AUTHORIZE_URL, urlencode($tenant));
|
||||
|
||||
$params = [
|
||||
'client_id' => trim($clientId),
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => trim($redirectUri),
|
||||
'response_mode' => 'query',
|
||||
'scope' => MS_DEFAULT_SCOPES,
|
||||
'state' => $state,
|
||||
'prompt' => 'select_account',
|
||||
];
|
||||
|
||||
return $baseUrl . '?' . http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wisselt de ontvangen autorisatiecode om voor access- en refresh tokens
|
||||
*/
|
||||
function microsoft_exchange_code(string $code, string $clientId, string $clientSecret, string $redirectUri, string $tenantId = 'common'): array {
|
||||
$tenant = !empty($tenantId) ? trim($tenantId) : 'common';
|
||||
$tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($tenant));
|
||||
|
||||
$payload = [
|
||||
'client_id' => trim($clientId),
|
||||
'client_secret' => trim($clientSecret),
|
||||
'code' => trim($code),
|
||||
'redirect_uri' => trim($redirectUri),
|
||||
'grant_type' => 'authorization_code',
|
||||
'scope' => MS_DEFAULT_SCOPES,
|
||||
];
|
||||
|
||||
$response = fociz_http_request($tokenUrl, [
|
||||
'method' => 'POST',
|
||||
'body' => $payload,
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($response['status'] !== 200 || empty($response['json']['access_token'])) {
|
||||
$errorMsg = $response['json']['error_description'] ?? ($response['error'] ?? 'Onbekende fout bij token uitwisseling');
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errorMsg,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'access_token' => $response['json']['access_token'],
|
||||
'refresh_token' => $response['json']['refresh_token'] ?? null,
|
||||
'expires_in' => (int)($response['json']['expires_in'] ?? 3600),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt het Microsoft gebruikersprofiel op via Graph API
|
||||
*/
|
||||
function microsoft_get_user_profile(string $accessToken): ?array {
|
||||
$response = fociz_http_request(MS_GRAPH_BASE_URL . '/me', [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$accessToken}",
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if ($response['status'] === 200 && is_array($response['json'])) {
|
||||
return [
|
||||
'displayName' => $response['json']['displayName'] ?? 'Microsoft 365 Gebruiker',
|
||||
'mail' => $response['json']['mail'] ?? ($response['json']['userPrincipalName'] ?? ''),
|
||||
'id' => $response['json']['id'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt een geldige access_token op voor de gebruiker; vernieuwt automatisch indien verlopen
|
||||
*/
|
||||
function microsoft_get_valid_token(int $userId): ?string {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||
$stmt->execute([$userId]);
|
||||
$integration = $stmt->fetch();
|
||||
|
||||
if (!$integration || empty($integration['access_token'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$settings = json_decode($integration['settings'] ?? '{}', true) ?: [];
|
||||
$expiresAt = (int)($settings['expires_at'] ?? 0);
|
||||
|
||||
// Als token nog minstens 4 minuten geldig is, direct retourneren
|
||||
if ($expiresAt > (time() + 240)) {
|
||||
return $integration['access_token'];
|
||||
}
|
||||
|
||||
// Token is verlopen of verloopt bijna; refresh met refresh_token
|
||||
$refreshToken = $integration['refresh_token'] ?? '';
|
||||
$clientId = $settings['client_id'] ?? fociz_env('MICROSOFT_CLIENT_ID', '');
|
||||
$clientSecret = $settings['client_secret'] ?? fociz_env('MICROSOFT_CLIENT_SECRET', '');
|
||||
$tenantId = $settings['tenant_id'] ?? fociz_env('MICROSOFT_TENANT_ID', 'common');
|
||||
|
||||
if (empty($refreshToken) || empty($clientId) || empty($clientSecret)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode(!empty($tenantId) ? $tenantId : 'common'));
|
||||
$payload = [
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'scope' => MS_DEFAULT_SCOPES,
|
||||
];
|
||||
|
||||
$response = fociz_http_request($tokenUrl, [
|
||||
'method' => 'POST',
|
||||
'body' => $payload,
|
||||
'timeout' => 15,
|
||||
]);
|
||||
|
||||
if ($response['status'] === 200 && !empty($response['json']['access_token'])) {
|
||||
$newAccessToken = $response['json']['access_token'];
|
||||
$newRefreshToken = $response['json']['refresh_token'] ?? $refreshToken;
|
||||
$expiresIn = (int)($response['json']['expires_in'] ?? 3600);
|
||||
$settings['expires_at'] = time() + $expiresIn;
|
||||
|
||||
$updateStmt = $db->prepare("
|
||||
UPDATE integrations
|
||||
SET access_token = ?, refresh_token = ?, settings = ?, last_sync_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$updateStmt->execute([$newAccessToken, $newRefreshToken, json_encode($settings), $integration['id']]);
|
||||
|
||||
return $newAccessToken;
|
||||
}
|
||||
|
||||
error_log("Microsoft token refresh mislukt voor user {$userId}: " . ($response['error'] ?? json_encode($response['json'])));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt agenda-afspraken van Microsoft Outlook op voor de opgegeven datum
|
||||
*/
|
||||
function microsoft_fetch_today_events(int $userId, ?string $date = null): array {
|
||||
$targetDate = $date ?: date('Y-m-d');
|
||||
$token = microsoft_get_valid_token($userId);
|
||||
|
||||
if (!$token) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Venster van 00:00:00 tot 23:59:59 lokale tijd
|
||||
$startIso = "{$targetDate}T00:00:00";
|
||||
$endIso = "{$targetDate}T23:59:59";
|
||||
|
||||
$url = MS_GRAPH_BASE_URL . '/me/calendarView?' . http_build_query([
|
||||
'startDateTime' => $startIso,
|
||||
'endDateTime' => $endIso,
|
||||
'$select' => 'id,subject,bodyPreview,start,end,location,isAllDay,webLink,onlineMeeting,isCancelled',
|
||||
'$orderby' => 'start/dateTime',
|
||||
'$top' => '50',
|
||||
]);
|
||||
|
||||
$response = fociz_http_request($url, [
|
||||
'method' => 'GET',
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
'Accept' => 'application/json',
|
||||
'Prefer' => 'outlook.timezone="Europe/Amsterdam"',
|
||||
],
|
||||
'timeout' => 12,
|
||||
]);
|
||||
|
||||
if ($response['status'] !== 200 || empty($response['json']['value'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rawEvents = $response['json']['value'];
|
||||
$events = [];
|
||||
|
||||
foreach ($rawEvents as $e) {
|
||||
// Annuleringen overslaan
|
||||
if (!empty($e['isCancelled'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rawStart = $e['start']['dateTime'] ?? '';
|
||||
$rawEnd = $e['end']['dateTime'] ?? '';
|
||||
|
||||
// Formatteer naar H:i (bijv. 09:30)
|
||||
$startTime = '';
|
||||
$endTime = '';
|
||||
if (!empty($rawStart)) {
|
||||
$startTime = date('H:i', strtotime($rawStart));
|
||||
}
|
||||
if (!empty($rawEnd)) {
|
||||
$endTime = date('H:i', strtotime($rawEnd));
|
||||
}
|
||||
|
||||
// Teams of meeting URL detecteren
|
||||
$meetingUrl = $e['onlineMeeting']['joinUrl'] ?? ($e['webLink'] ?? 'https://outlook.office.com');
|
||||
if (empty($meetingUrl) && !empty($e['bodyPreview'])) {
|
||||
if (preg_match('#https://teams\.microsoft\.com/l/meetup-join/[^\s"\'<>]+#i', $e['bodyPreview'], $m)) {
|
||||
$meetingUrl = $m[0];
|
||||
}
|
||||
}
|
||||
|
||||
$locationName = $e['location']['displayName'] ?? '';
|
||||
if (empty($locationName) && !empty($e['onlineMeeting']['joinUrl'])) {
|
||||
$locationName = 'Microsoft Teams';
|
||||
}
|
||||
|
||||
$events[] = [
|
||||
'id' => $e['id'] ?? uniqid('ms_'),
|
||||
'subject' => !empty($e['subject']) ? $e['subject'] : '(Geen onderwerp)',
|
||||
'start' => $startTime ?: '09:00',
|
||||
'end' => $endTime ?: '10:00',
|
||||
'start_raw' => $rawStart,
|
||||
'end_raw' => $rawEnd,
|
||||
'location' => $locationName ?: 'Geen locatie',
|
||||
'meeting_url' => $meetingUrl,
|
||||
'is_all_day' => !empty($e['isAllDay']),
|
||||
'source' => 'OUTLOOK_LIVE',
|
||||
];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ontkoppelt Microsoft integratie voor een gebruiker
|
||||
*/
|
||||
function microsoft_disconnect(int $userId): bool {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
||||
return $stmt->execute([$userId]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ class TOTP {
|
||||
/**
|
||||
* Verifieert een ingevoerde 6-cijferige 2FA code (met 1 tijdstap speling naar voor/achter)
|
||||
*/
|
||||
public static function verifyCode(string $secret, string $userInputCode, int $window = 1): boolean {
|
||||
public static function verifyCode(string $secret, string $userInputCode, int $window = 1): bool {
|
||||
$userInputCode = trim(str_replace(' ', '', $userInputCode));
|
||||
if (strlen($userInputCode) !== 6 || !ctype_digit($userInputCode)) {
|
||||
return false;
|
||||
|
||||
+567
-170
@@ -1,34 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Integraties Hub (Outlook, Teams, Jira)
|
||||
* Fociz Integraties Hub (Microsoft Outlook, Microsoft Teams, Atlassian Jira)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/includes/integrations/jira.php';
|
||||
|
||||
$db = get_db_connection();
|
||||
$userId = $currentUser['id'];
|
||||
|
||||
// Formulierverwerking voor integratie instellingen
|
||||
$message = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$provider = $_POST['provider'] ?? '';
|
||||
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
|
||||
$settings = json_encode([
|
||||
'client_id' => $_POST['client_id'] ?? '',
|
||||
'client_secret' => $_POST['client_secret'] ?? '',
|
||||
'tenant_id' => $_POST['tenant_id'] ?? '',
|
||||
'jira_domain' => $_POST['jira_domain'] ?? '',
|
||||
'jira_email' => $_POST['jira_email'] ?? '',
|
||||
'jira_api_token' => $_POST['jira_api_token'] ?? '',
|
||||
]);
|
||||
$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
|
||||
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);
|
||||
$message = "Microsoft Outlook is succesvol ontkoppeld.";
|
||||
} elseif ($provider === 'JIRA') {
|
||||
jira_disconnect($userId);
|
||||
$message = "Atlassian Jira is succesvol ontkoppeld.";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Outlook Opslaan
|
||||
elseif ($provider === 'OUTLOOK') {
|
||||
$clientId = trim($_POST['client_id'] ?? '');
|
||||
$clientSecret = trim($_POST['client_secret'] ?? '');
|
||||
$tenantId = trim($_POST['tenant_id'] ?? '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,
|
||||
]);
|
||||
|
||||
if (in_array($provider, ['OUTLOOK', 'TEAMS', 'JIRA'])) {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
|
||||
VALUES (?, ?, 1, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE is_demo_mode = VALUES(is_demo_mode), settings = VALUES(settings), last_sync_at = NOW()
|
||||
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, $provider, $isDemo, $settings]);
|
||||
$message = "Instellingen voor {$provider} succesvol opgeslagen.";
|
||||
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
|
||||
$message = "Instellingen voor Microsoft Outlook zijn opgeslagen.";
|
||||
|
||||
// Als gebruiker direct op 'Koppel nu' klikte
|
||||
if ($action === 'connect_oauth') {
|
||||
header("Location: /auth/oauth_microsoft.php?client_id=" . urlencode($clientId) . "&tenant_id=" . urlencode($tenantId));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 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}";
|
||||
}
|
||||
} else {
|
||||
$message = "Jira instellingen opgeslagen.";
|
||||
}
|
||||
|
||||
$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}"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,263 +159,543 @@ foreach ($configsRaw as $c) {
|
||||
$configs[$c['provider']]['settings_parsed'] = json_decode($c['settings'] ?? '{}', true) ?: [];
|
||||
}
|
||||
|
||||
// Jira demo issues
|
||||
$jiraIssues = [
|
||||
[
|
||||
'key' => 'FOC-101',
|
||||
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
|
||||
'status' => 'In Progress',
|
||||
'priority' => 'High',
|
||||
'epic' => 'Security & Access',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-102',
|
||||
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
|
||||
'status' => 'To Do',
|
||||
'priority' => 'High',
|
||||
'epic' => 'Brainstorming Module',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-103',
|
||||
'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API',
|
||||
'status' => 'In Review',
|
||||
'priority' => 'Medium',
|
||||
'epic' => 'Integrations Hub',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-104',
|
||||
'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren',
|
||||
'status' => 'To Do',
|
||||
'priority' => 'Medium',
|
||||
'epic' => 'Roadmap Module',
|
||||
]
|
||||
];
|
||||
// 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-slate-900/80 border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<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>Connectiviteit & Synchronisatie</span>
|
||||
<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>
|
||||
<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">
|
||||
Beheer uw koppelingen met Microsoft Outlook (agenda & taken), Microsoft Teams (meetings) en Atlassian Jira (issues & roadmap).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Flash message -->
|
||||
<?php if ($message): ?>
|
||||
<div class="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs font-semibold flex items-center space-x-2">
|
||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||
<span><?= htmlspecialchars($message) ?></span>
|
||||
<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; ?>
|
||||
|
||||
<!-- Integratie Cards Grid -->
|
||||
<!-- 3 Koloms Grid voor Providers -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- 1. OUTLOOK -->
|
||||
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- 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>
|
||||
<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>Actief</span>
|
||||
</span>
|
||||
<?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>
|
||||
|
||||
<h3 class="text-base font-bold text-white mb-1">Microsoft Outlook</h3>
|
||||
<p class="text-xs text-slate-400 mb-4">
|
||||
Synchroniseert uw agenda-afspraken en e-mails automatisch naar uw dagplanning en time-blocks.
|
||||
</p>
|
||||
|
||||
<div class="bg-slate-950/60 rounded-xl p-3 border border-slate-800/80 text-xs space-y-2 mb-4">
|
||||
<div class="flex items-center justify-between text-slate-400">
|
||||
<span>Status:</span>
|
||||
<span class="text-emerald-400 font-semibold">Gekoppeld</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-slate-400">
|
||||
<span>Sync frequentie:</span>
|
||||
<span class="text-slate-200">Elke 15 min</span>
|
||||
</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-3 border-t border-slate-800 space-y-3">
|
||||
<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">
|
||||
|
||||
<label class="flex items-center space-x-2 text-xs text-slate-300">
|
||||
<input type="checkbox" name="is_demo_mode" value="1" <?= empty($configs['OUTLOOK']) || !empty($configs['OUTLOOK']['is_demo_mode']) ? 'checked' : '' ?> class="rounded bg-slate-900 border-slate-700 text-emerald-500">
|
||||
<div>
|
||||
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">Application (Client) ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="client_id"
|
||||
value="<?= htmlspecialchars($outlookSettings['client_id'] ?? fociz_env('MICROSOFT_CLIENT_ID', '')) ?>"
|
||||
placeholder="e.g. 11111111-2222-3333-4444-555555555555"
|
||||
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>
|
||||
|
||||
<div>
|
||||
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">Client Secret</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="msClientSecret"
|
||||
name="client_secret"
|
||||
value="<?= !empty($outlookSettings['client_secret']) ? '••••••••••••••••' : htmlspecialchars(fociz_env('MICROSOFT_CLIENT_SECRET', '')) ?>"
|
||||
placeholder="Client Secret Value"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[10px] font-semibold text-slate-400 mb-0.5">Directory (Tenant) ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tenant_id"
|
||||
value="<?= htmlspecialchars($outlookSettings['tenant_id'] ?? fociz_env('MICROSOFT_TENANT_ID', 'common')) ?>"
|
||||
placeholder="common of specifiek GUID"
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<button type="submit" class="w-full py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold transition">
|
||||
Instellingen Bijwerken
|
||||
</button>
|
||||
<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">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<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>Actief</span>
|
||||
<span>Inbegrepen bij Outlook</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-base font-bold text-white mb-1">Microsoft Teams</h3>
|
||||
<p class="text-xs text-slate-400 mb-4">
|
||||
Toont vandaag geplande Teams meetings met directe 'Deelnemen' links en kanaalupdates.
|
||||
</p>
|
||||
<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 border border-slate-800/80 text-xs space-y-2 mb-4">
|
||||
<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>Meldingen:</span>
|
||||
<span class="text-slate-200">Ingeschakeld</span>
|
||||
<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-3 border-t border-slate-800 space-y-3">
|
||||
<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-emerald-500">
|
||||
<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">
|
||||
Instellingen Bijwerken
|
||||
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">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<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>
|
||||
<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>Actief</span>
|
||||
</span>
|
||||
<?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>
|
||||
|
||||
<h3 class="text-base font-bold text-white mb-1">Atlassian Jira</h3>
|
||||
<p class="text-xs text-slate-400 mb-4">
|
||||
Koppelt Jira Epics en Issues direct aan uw roadmap items en takenlijst.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<div class="bg-slate-950/60 rounded-xl p-3 border border-slate-800/80 text-xs space-y-2 mb-4">
|
||||
<div class="flex items-center justify-between text-slate-400">
|
||||
<span>Two-way Status Sync:</span>
|
||||
<span class="text-emerald-400 font-semibold">Ondersteund</span>
|
||||
<?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>
|
||||
<div class="flex items-center justify-between text-slate-400">
|
||||
<span>Roadmap Linkage:</span>
|
||||
<span class="text-slate-200">Actief</span>
|
||||
<?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>
|
||||
|
||||
<form method="POST" action="/integrations.php" class="pt-3 border-t border-slate-800 space-y-2">
|
||||
<!-- 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] text-slate-400 mb-0.5">Jira Cloud Domein</label>
|
||||
<input type="text" name="jira_domain" placeholder="uwbedrijf.atlassian.net" value="<?= htmlspecialchars($configs['JIRA']['settings_parsed']['jira_domain'] ?? '') ?>" class="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1 text-xs text-white placeholder-slate-600 focus:outline-none">
|
||||
<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-emerald-500">
|
||||
<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>
|
||||
|
||||
<button type="submit" class="w-full py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold transition">
|
||||
Jira Opslaan
|
||||
</button>
|
||||
<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 items-center justify-between pb-3 border-b border-slate-800">
|
||||
<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>
|
||||
<span class="text-xs text-slate-400 font-mono"><?= count($jiraIssues) ?> issues</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<?php foreach ($jiraIssues as $issue): ?>
|
||||
<div class="bg-slate-950/60 border border-slate-800 hover:border-slate-700 rounded-xl p-4 flex flex-col justify-between transition">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-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] font-semibold px-2 py-0.5 rounded bg-slate-800 text-slate-300">
|
||||
<?= htmlspecialchars($issue['status']) ?>
|
||||
</span>
|
||||
<?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>
|
||||
<h4 class="text-xs font-bold text-white leading-snug">
|
||||
<?= htmlspecialchars($issue['summary']) ?>
|
||||
</h4>
|
||||
<span class="text-[10px] text-slate-500 mt-1 block">Epic: <?= htmlspecialchars($issue['epic']) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
|
||||
<span class="text-[10px] font-bold text-amber-400"><?= htmlspecialchars($issue['priority']) ?> Priority</span>
|
||||
<button onclick="importJiraToTasks('<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>')" 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 Dagtaak</span>
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function importJiraToTasks(title, jiraKey) {
|
||||
const res = await fetch('/api/planner.php?action=add_task', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
title: `[${jiraKey}] ${title}`,
|
||||
priority: 'P1',
|
||||
date: '<?= date('Y-m-d') ?>'
|
||||
})
|
||||
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!');
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert(`Jira issue ${jiraKey} is toegevoegd aan uw dagplanning!`);
|
||||
}
|
||||
|
||||
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'; ?>
|
||||
|
||||
|
||||
+237
-58
@@ -1,15 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Dagplanning, Focus Modus & Time-blocking
|
||||
* Gekoppeld aan Microsoft Outlook 365 (Agenda) en Atlassian Jira (Tickets)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/includes/integrations/jira.php';
|
||||
|
||||
$db = get_db_connection();
|
||||
$userId = $currentUser['id'];
|
||||
$today = date('Y-m-d');
|
||||
|
||||
// Haal dagplanning op of maak aan
|
||||
// 1. Haal dagplanning op of maak aan
|
||||
$stmtPlan = $db->prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?");
|
||||
$stmtPlan->execute([$userId, $today]);
|
||||
$dailyPlan = $stmtPlan->fetch();
|
||||
@@ -24,7 +27,7 @@ if (!$dailyPlan) {
|
||||
$dailyPlan = ['id' => $planId, 'plan_date' => $today, 'focus_goal' => 'Mijn belangrijkste doel van vandaag behalen', 'notes' => ''];
|
||||
}
|
||||
|
||||
// Haal taken op voor vandaag
|
||||
// 2. Haal taken op voor vandaag
|
||||
$stmtTasks = $db->prepare("
|
||||
SELECT * FROM tasks
|
||||
WHERE user_id = ? AND (daily_plan_id = ? OR daily_plan_id IS NULL)
|
||||
@@ -33,35 +36,75 @@ $stmtTasks = $db->prepare("
|
||||
$stmtTasks->execute([$userId, $dailyPlan['id']]);
|
||||
$tasks = $stmtTasks->fetchAll();
|
||||
|
||||
// Outlook demo / gesynchroniseerde events
|
||||
$outlookEvents = [
|
||||
[
|
||||
'id' => 'evt-1',
|
||||
'subject' => 'Daily Team Standup & Focus Sync',
|
||||
'start' => '09:00',
|
||||
'end' => '09:30',
|
||||
'location' => 'Microsoft Teams',
|
||||
'meeting_url' => 'https://teams.microsoft.com',
|
||||
],
|
||||
[
|
||||
'id' => 'evt-2',
|
||||
'subject' => 'Sprint & Roadmap Review met Stakeholders',
|
||||
'start' => '11:00',
|
||||
'end' => '12:00',
|
||||
'location' => 'Vergaderruimte A & Teams',
|
||||
'meeting_url' => 'https://teams.microsoft.com',
|
||||
],
|
||||
[
|
||||
'id' => 'evt-3',
|
||||
'subject' => 'Architectuur & MariaDB Database Evaluatie',
|
||||
'start' => '14:30',
|
||||
'end' => '15:30',
|
||||
'location' => 'Online Teams Call',
|
||||
'meeting_url' => 'https://teams.microsoft.com',
|
||||
]
|
||||
];
|
||||
// 3. Haal integratiestatussen op
|
||||
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ?");
|
||||
$stmtInt->execute([$userId]);
|
||||
$userIntegrations = [];
|
||||
foreach ($stmtInt->fetchAll() as $row) {
|
||||
$userIntegrations[$row['provider']] = $row;
|
||||
}
|
||||
|
||||
$hours = ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00"];
|
||||
$isOutlookConnected = !empty($userIntegrations['OUTLOOK']['access_token']) && empty($userIntegrations['OUTLOOK']['is_demo_mode']);
|
||||
$outlookEvents = [];
|
||||
$isLiveOutlook = false;
|
||||
|
||||
if ($isOutlookConnected) {
|
||||
$outlookEvents = microsoft_fetch_today_events($userId, $today);
|
||||
$isLiveOutlook = !empty($outlookEvents) || true; // Live koppeling is actief
|
||||
}
|
||||
|
||||
// Fallback naar sandbox data indien Outlook niet live gekoppeld is
|
||||
if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
$outlookEvents = [
|
||||
[
|
||||
'id' => 'evt-1',
|
||||
'subject' => 'Daily Team Standup & Focus Sync',
|
||||
'start' => '09:00',
|
||||
'end' => '09:30',
|
||||
'location' => 'Microsoft Teams',
|
||||
'meeting_url' => 'https://teams.microsoft.com',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'id' => 'evt-2',
|
||||
'subject' => 'Sprint & Roadmap Review met Stakeholders',
|
||||
'start' => '11:00',
|
||||
'end' => '12:00',
|
||||
'location' => 'Vergaderruimte A & Teams',
|
||||
'meeting_url' => 'https://teams.microsoft.com',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'id' => 'evt-3',
|
||||
'subject' => 'Architectuur & MariaDB Database Evaluatie',
|
||||
'start' => '14:30',
|
||||
'end' => '15:30',
|
||||
'location' => 'Online Teams Call',
|
||||
'meeting_url' => 'https://teams.microsoft.com',
|
||||
'source' => 'SANDBOX',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 4. Haal Jira issues op voor de snelle import widget
|
||||
$jiraData = jira_get_user_issues($userId);
|
||||
$jiraIssues = $jiraData['issues'] ?? [];
|
||||
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
|
||||
|
||||
// 5. Tijdlijn uren bepalen
|
||||
$startHour = 8;
|
||||
$endHour = 18;
|
||||
foreach ($outlookEvents as $e) {
|
||||
if (!empty($e['start'])) {
|
||||
$h = (int)explode(':', $e['start'])[0];
|
||||
if ($h < $startHour && $h >= 6) $startHour = $h;
|
||||
if ($h > $endHour && $h <= 22) $endHour = $h + 1;
|
||||
}
|
||||
}
|
||||
$hours = [];
|
||||
for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
$hours[] = sprintf('%02d:00', $h);
|
||||
}
|
||||
|
||||
// Nederlandse datumformattering
|
||||
$dutchDays = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
|
||||
@@ -70,7 +113,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Top Banner: Datum & Hoofddoel -->
|
||||
<!-- Top Banner: Datum, Hoofddoel & Integraties Status -->
|
||||
<div class="bg-gradient-to-r from-slate-900 via-[#0e1626] to-slate-900 border border-slate-800 rounded-2xl p-6 shadow-2xl">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div>
|
||||
@@ -83,11 +126,34 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="/integrations.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-blue-950/60 border border-blue-800/60 text-blue-300 hover:text-white hover:bg-blue-900/60 text-xs font-medium transition">
|
||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
|
||||
<span>Outlook & Teams Verbonden</span>
|
||||
</a>
|
||||
<!-- Integratie Status Badges -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<?php if ($isOutlookConnected): ?>
|
||||
<div class="flex items-center space-x-2 px-3 py-1.5 rounded-xl bg-blue-950/60 border border-blue-800/60 text-blue-300 text-xs font-medium">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span>Outlook 365 Live</span>
|
||||
<button onclick="location.reload()" title="Agenda Verversen" class="p-1 text-blue-400 hover:text-white rounded transition">
|
||||
<i data-lucide="refresh-cw" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?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">
|
||||
<i data-lucide="link-2" class="w-3.5 h-3.5 text-blue-400"></i>
|
||||
<span>Koppel Outlook</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isJiraLive): ?>
|
||||
<a href="#jiraPanel" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-sky-950/60 border border-sky-800/60 text-sky-300 hover:text-white text-xs font-medium transition">
|
||||
<span class="w-2 h-2 rounded-full bg-sky-400 animate-pulse"></span>
|
||||
<span>Jira: <?= count($jiraIssues) ?> tickets</span>
|
||||
</a>
|
||||
<?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">
|
||||
<i data-lucide="trello" class="w-3.5 h-3.5 text-sky-400"></i>
|
||||
<span>Koppel Jira</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +183,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Grid: Timer + Takenlijst (Links) & Timeblocking (Rechts) -->
|
||||
<!-- Main Grid: Timer + Takenlijst + Jira (Links) & Timeblocking (Rechts) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
<!-- Linker kolom: Focus Timer & Taken -->
|
||||
@@ -212,7 +278,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<div class="space-y-2" id="tasksContainer">
|
||||
<?php if (empty($tasks)): ?>
|
||||
<div class="text-center py-8 text-slate-500 text-xs">
|
||||
Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' om te beginnen.
|
||||
Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' of importeer een Jira ticket hieronder.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($tasks as $task): ?>
|
||||
@@ -225,15 +291,31 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
</button>
|
||||
|
||||
<div class="truncate">
|
||||
<span class="text-xs font-medium block truncate <?= $task['is_completed'] ? 'line-through text-slate-500' : 'text-slate-200' ?>">
|
||||
<?= htmlspecialchars($task['title']) ?>
|
||||
</span>
|
||||
<?php if (!empty($task['time_block_start'])): ?>
|
||||
<span class="text-[10px] text-slate-400 font-mono flex items-center space-x-1 mt-0.5">
|
||||
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
|
||||
<span><?= htmlspecialchars($task['time_block_start']) ?></span>
|
||||
<div class="flex items-center space-x-1.5 truncate">
|
||||
<?php if ($task['source'] === 'JIRA'): ?>
|
||||
<span class="px-1.5 py-0.2 rounded bg-sky-500/10 border border-sky-500/20 text-sky-400 font-mono text-[9px] font-bold">
|
||||
JIRA
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="text-xs font-medium truncate <?= $task['is_completed'] ? 'line-through text-slate-500' : 'text-slate-200' ?>">
|
||||
<?= htmlspecialchars($task['title']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 mt-0.5">
|
||||
<?php if (!empty($task['time_block_start'])): ?>
|
||||
<span class="text-[10px] text-slate-400 font-mono flex items-center space-x-1">
|
||||
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
|
||||
<span><?= htmlspecialchars($task['time_block_start']) ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($task['external_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] text-sky-400 hover:underline flex items-center space-x-0.5">
|
||||
<span>Open in Jira</span>
|
||||
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -258,6 +340,61 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jira Snelle Import Widget -->
|
||||
<div id="jiraPanel" class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl backdrop-blur-sm">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-800 mb-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="trello" class="w-4 h-4 text-sky-400"></i>
|
||||
<h3 class="text-sm font-bold text-white uppercase tracking-wider">
|
||||
Mijn Jira Tickets
|
||||
</h3>
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-500/20 text-sky-400 border border-sky-500/30' : 'bg-slate-800 text-slate-400' ?>">
|
||||
<?= count($jiraIssues) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a href="/integrations.php" class="text-xs text-slate-400 hover:text-sky-300 transition flex items-center space-x-1">
|
||||
<span>Beheren</span>
|
||||
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (empty($jiraIssues)): ?>
|
||||
<div class="text-center py-6 text-slate-500 text-xs">
|
||||
Geen openstaande tickets gevonden in Jira.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto pr-1">
|
||||
<?php foreach (array_slice($jiraIssues, 0, 6) as $issue): ?>
|
||||
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-950/60 border border-slate-800/80 hover:border-slate-700 transition">
|
||||
<div class="truncate mr-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-mono text-[10px] font-bold text-sky-400 bg-sky-500/10 px-1.5 py-0.5 rounded">
|
||||
<?= htmlspecialchars($issue['key']) ?>
|
||||
</span>
|
||||
<span class="text-xs font-semibold text-white truncate">
|
||||
<?= htmlspecialchars($issue['summary']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[10px] text-slate-500 mt-0.5 block">
|
||||
<?= htmlspecialchars($issue['status']) ?> • <?= htmlspecialchars($issue['priority']) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick="quickImportJira(this, '<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>', '<?= htmlspecialchars(addslashes($issue['url'] ?? '')) ?>')"
|
||||
class="flex items-center space-x-1 text-[11px] font-bold text-emerald-400 hover:text-emerald-300 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 px-2 py-1 rounded-lg transition flex-shrink-0"
|
||||
title="Plan in als dagtaak"
|
||||
>
|
||||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||
<span>Plan</span>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Dagnotities & Reflectie -->
|
||||
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl backdrop-blur-sm">
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
@@ -282,19 +419,25 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<i data-lucide="clock" class="w-5 h-5 text-emerald-400"></i>
|
||||
<h3 class="text-sm font-bold text-white uppercase tracking-wider">Tijdlijn & Time-blocking</h3>
|
||||
</div>
|
||||
<span class="text-xs text-slate-400">08:00 – 18:00</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<?php if ($isOutlookConnected): ?>
|
||||
<span class="px-2 py-0.5 rounded bg-blue-500/10 border border-blue-500/20 text-blue-400 text-[10px] font-bold">Outlook 365</span>
|
||||
<?php endif; ?>
|
||||
<span class="text-xs text-slate-400 font-mono"><?= $hours[0] ?> – <?= end($hours) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<?php foreach ($hours as $hour):
|
||||
$hourInt = (int)explode(':', $hour)[0];
|
||||
|
||||
// Zoek Outlook meetings
|
||||
// Zoek Outlook meetings die binnen dit uur vallen
|
||||
$slotEvents = array_filter($outlookEvents, function($e) use ($hourInt) {
|
||||
if (empty($e['start'])) return false;
|
||||
return (int)explode(':', $e['start'])[0] === $hourInt;
|
||||
});
|
||||
|
||||
// Zoek taken in dit blok
|
||||
// Zoek taken gepland in dit uur
|
||||
$slotTasks = array_filter($tasks, function($t) use ($hourInt) {
|
||||
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $hourInt;
|
||||
});
|
||||
@@ -307,17 +450,21 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<!-- Outlook meetings -->
|
||||
<?php foreach ($slotEvents as $evt): ?>
|
||||
<div class="flex items-center justify-between p-2 rounded-xl bg-blue-950/40 border border-blue-800/50 text-blue-200 text-xs shadow-sm">
|
||||
<div class="flex items-center justify-between p-2 rounded-xl bg-blue-950/50 border border-blue-800/60 text-blue-100 text-xs shadow-sm">
|
||||
<div class="flex items-center space-x-2 truncate mr-2">
|
||||
<span class="px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 font-mono text-[10px] font-bold">
|
||||
<?= $evt['start'] ?> - <?= $evt['end'] ?>
|
||||
<?= htmlspecialchars($evt['start']) ?> - <?= htmlspecialchars($evt['end']) ?>
|
||||
</span>
|
||||
<span class="font-semibold truncate" title="<?= htmlspecialchars($evt['subject']) ?>">
|
||||
<?= htmlspecialchars($evt['subject']) ?>
|
||||
</span>
|
||||
<span class="font-semibold truncate"><?= htmlspecialchars($evt['subject']) ?></span>
|
||||
</div>
|
||||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-blue-400 hover:text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded-lg flex-shrink-0">
|
||||
<i data-lucide="video" class="w-3 h-3"></i>
|
||||
<span>Deelnemen</span>
|
||||
</a>
|
||||
<?php if (!empty($evt['meeting_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-blue-300 hover:text-white bg-blue-600/30 hover:bg-blue-600/50 px-2 py-0.5 rounded-lg flex-shrink-0 transition">
|
||||
<i data-lucide="video" class="w-3 h-3"></i>
|
||||
<span>Deelnemen</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -374,7 +521,6 @@ function setTimerPreset(minutes) {
|
||||
totalDuration = minutes * 60;
|
||||
updateTimerDisplay();
|
||||
|
||||
// Toggle styling
|
||||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
||||
document.getElementById('btnPreset25').className = minutes === 25 ? 'px-2.5 py-1 rounded-lg font-bold bg-emerald-500 text-slate-950' : 'px-2.5 py-1 rounded-lg text-slate-400 hover:text-slate-200';
|
||||
document.getElementById('btnPreset50').className = minutes === 50 ? 'px-2.5 py-1 rounded-lg font-bold bg-emerald-500 text-slate-950' : 'px-2.5 py-1 rounded-lg text-slate-400 hover:text-slate-200';
|
||||
@@ -470,6 +616,40 @@ async function submitNewTask() {
|
||||
}
|
||||
}
|
||||
|
||||
async function quickImportJira(btn, title, jiraKey, url) {
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerText = '...';
|
||||
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: 'P1',
|
||||
source: 'JIRA',
|
||||
external_id: jiraKey,
|
||||
external_url: url,
|
||||
date: '<?= $today ?>'
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i>';
|
||||
location.reload();
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTask(taskId, isCompleted) {
|
||||
await fetch('/api/planner.php?action=toggle_task', {
|
||||
method: 'POST',
|
||||
@@ -491,4 +671,3 @@ async function deleteTask(taskId) {
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user