Compare commits
21 Commits
4d8c10f638
...
8843b7bb9d
| 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_PORT="3306"
|
||||||
DB_NAME="fociz"
|
DB_NAME="fociz"
|
||||||
DB_USER="root"
|
DB_USER="root"
|
||||||
DB_PASS="uw_wachtwoord"
|
DB_PASS=""
|
||||||
|
|
||||||
|
# Omgeving: 'development' (knippert rood) of 'live' (knippert groen)
|
||||||
|
APP_ENV="development"
|
||||||
|
|
||||||
# Applicatie URL (voor uitnodigingslinks)
|
# Applicatie URL (voor uitnodigingslinks)
|
||||||
APP_URL="https://uw-domein.nl"
|
APP_URL="https://uw-domein.nl"
|
||||||
|
|
||||||
# Applicatie Geheime Sleutel (willekeurige tekenreeks)
|
# Applicatie Geheime Sleutel (willekeurige tekenreeks)
|
||||||
APP_SECRET="fociz-maria-db-super-secret-key-998877"
|
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
|
### 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
|
### 4. Eerste Beheerder Instellen
|
||||||
1. Open uw browser en ga naar `https://uw-domein.nl`.
|
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]);
|
$stmtPlan->execute([$user['id'], $date]);
|
||||||
$planId = $db->lastInsertId();
|
$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("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status)
|
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status, source, external_id, external_url)
|
||||||
VALUES (?, ?, ?, ?, ?, 'TODO')
|
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();
|
$taskId = (int)$db->lastInsertId();
|
||||||
|
|
||||||
echo json_encode([
|
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;
|
$error = null;
|
||||||
$success = false;
|
$success = false;
|
||||||
$backupCodes = [];
|
$backupCodes = [];
|
||||||
|
$email = '';
|
||||||
|
|
||||||
// Haal of genereer een tijdelijke sessie-sleutel voor de 2FA configuratie
|
// Haal of genereer een tijdelijke sessie-sleutel voor de 2FA configuratie
|
||||||
if (empty($_SESSION['setup_totp_secret'])) {
|
if (empty($_SESSION['setup_totp_secret'])) {
|
||||||
@@ -148,7 +149,7 @@ $pageTitle = "Fociz — Eerste Initialisatie";
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-slate-300 mb-1">Volledige naam</label>
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-pdo_mysql": "*",
|
||||||
|
"ext-mysqli": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+68
-15
@@ -1,40 +1,93 @@
|
|||||||
<?php
|
<?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
|
$envVars = [];
|
||||||
$envFile = __DIR__ . '/../.env';
|
|
||||||
|
// 1. Zoek naar .env of val terug op .env.example
|
||||||
|
$envCandidates = [
|
||||||
|
__DIR__ . '/../.env',
|
||||||
|
__DIR__ . '/../.env.example',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($envCandidates as $envFile) {
|
||||||
if (file_exists($envFile)) {
|
if (file_exists($envFile)) {
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if ($lines !== false) {
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if (str_starts_with(trim($line), '#')) continue;
|
$line = trim($line);
|
||||||
|
// Negeer lege regels en commentaarregels (# of //)
|
||||||
|
if ($line === '' || str_starts_with($line, '#') || str_starts_with($line, '//')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (strpos($line, '=') !== false) {
|
if (strpos($line, '=') !== false) {
|
||||||
list($name, $value) = explode('=', $line, 2);
|
list($name, $value) = explode('=', $line, 2);
|
||||||
$name = trim($name);
|
$name = trim($name);
|
||||||
|
$value = trim($value);
|
||||||
|
// Verwijder omlijstende aanhalingstekens (" of ')
|
||||||
$value = trim($value, " \t\n\r\0\x0B\"'");
|
$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));
|
// Sla op in array en zet in globale PHP variabelen
|
||||||
|
$envVars[$name] = $value;
|
||||||
$_ENV[$name] = $value;
|
$_ENV[$name] = $value;
|
||||||
$_SERVER[$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');
|
* Veilige helper om een omgevingsvariabele op te vragen
|
||||||
define('DB_PORT', getenv('DB_PORT') ?: '3306');
|
*/
|
||||||
define('DB_NAME', getenv('DB_NAME') ?: 'fociz');
|
if (!function_exists('fociz_env')) {
|
||||||
define('DB_USER', getenv('DB_USER') ?: 'focizlive_user');
|
function fociz_env(string $key, $default = null) {
|
||||||
define('DB_PASS', getenv('DB_PASS') !== false ? getenv('DB_PASS') : 'F0c1z_Live_Secure_2026!');
|
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');
|
define('DB_CHARSET', 'utf8mb4');
|
||||||
|
|
||||||
// Applicatie instellingen
|
// Applicatie instellingen (Live defaults)
|
||||||
define('APP_NAME', 'Fociz');
|
define('APP_NAME', 'Fociz');
|
||||||
define('APP_URL', getenv('APP_URL') ?: ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'localhost')));
|
define('APP_URL', fociz_env('APP_URL', ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'fociz.nl'))));
|
||||||
define('APP_SECRET', getenv('APP_SECRET') ?: 'fociz-maria-db-super-secret-key-998877');
|
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
|
// Tijdzone
|
||||||
date_default_timezone_set('Europe/Amsterdam');
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+107
-33
@@ -8,8 +8,35 @@ require_once __DIR__ . '/config.php';
|
|||||||
function get_db_connection(): PDO {
|
function get_db_connection(): PDO {
|
||||||
static $pdo = null;
|
static $pdo = null;
|
||||||
|
|
||||||
if ($pdo === null) {
|
if ($pdo !== null) {
|
||||||
$dsn = sprintf(
|
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',
|
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||||
DB_HOST,
|
DB_HOST,
|
||||||
DB_PORT,
|
DB_PORT,
|
||||||
@@ -17,52 +44,99 @@ function get_db_connection(): PDO {
|
|||||||
DB_CHARSET
|
DB_CHARSET
|
||||||
);
|
);
|
||||||
|
|
||||||
$options = [
|
// 2. CHECK: Bestaat de database al?
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
// We proberen eerst direct verbinding te maken met de database.
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
// Als de database al bestaat, lukt dit direct zonder database-aanmaakrechten te vereisen!
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
$pdo = new PDO($dsnWithDb, DB_USER, DB_PASS, $options);
|
||||||
// Zorg ervoor dat tabellen bestaan
|
|
||||||
|
// De database bestaat al! Controleer enkel of de tabellen al aanwezig zijn
|
||||||
ensure_tables_exist($pdo);
|
ensure_tables_exist($pdo);
|
||||||
|
return $pdo;
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// Als de database zelf nog niet bestaat, probeer deze aan te maken indien gebruiker rechten heeft
|
// Foutcode 1049 betekent: Database 'DB_NAME' bestaat nog NIET op de MariaDB server
|
||||||
if ($e->getCode() == 1049) {
|
if ($e->getCode() == 1049 || str_contains($e->getMessage(), 'Unknown database')) {
|
||||||
|
// Probeer de database eenmalig aan te maken indien de gebruiker CREATE rechten heeft
|
||||||
try {
|
try {
|
||||||
$tempDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET);
|
$serverDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET);
|
||||||
$tempPdo = new PDO($tempDsn, DB_USER, DB_PASS, $options);
|
$serverPdo = new PDO($serverDsn, DB_USER, DB_PASS, $options);
|
||||||
$tempPdo->exec(sprintf('CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci', DB_NAME));
|
$serverPdo->exec(sprintf(
|
||||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
'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);
|
ensure_tables_exist($pdo);
|
||||||
return $pdo;
|
return $pdo;
|
||||||
} catch (PDOException $ex) {
|
|
||||||
die("Database verbindingsfout: Kan database '" . htmlspecialchars(DB_NAME) . "' niet aanmaken of vinden. Maak deze handmatig aan in MariaDB.");
|
} 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;'>
|
||||||
die("MariaDB Verbindingsfout: " . htmlspecialchars($e->getMessage()) . "<br><small>Controleer uw instellingen in config/config.php of .env</small>");
|
<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>
|
||||||
|
");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $pdo;
|
// 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 {
|
function ensure_tables_exist(PDO $pdo): void {
|
||||||
try {
|
try {
|
||||||
|
// Controleer of de hoofdtabel 'users' al bestaat
|
||||||
$result = $pdo->query("SHOW TABLES LIKE 'users'");
|
$result = $pdo->query("SHOW TABLES LIKE 'users'");
|
||||||
if ($result->rowCount() === 0) {
|
if ($result && $result->rowCount() > 0) {
|
||||||
$schemaFile = __DIR__ . '/../schema.sql';
|
// Tabellen bestaan al! Geen actie nodig.
|
||||||
if (file_exists($schemaFile)) {
|
return;
|
||||||
$sql = file_get_contents($schemaFile);
|
|
||||||
$pdo->exec($sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Tabellen bestaan waarschijnlijk al of er is een permissiebeperking
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (Exception $e) {
|
||||||
|
error_log("Fout bij aanmaken tabellen: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -13,7 +13,6 @@ function start_secure_session(): void {
|
|||||||
session_set_cookie_params([
|
session_set_cookie_params([
|
||||||
'lifetime' => 7 * 24 * 60 * 60, // 7 dagen
|
'lifetime' => 7 * 24 * 60 * 60, // 7 dagen
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'domain' => '',
|
|
||||||
'secure' => $isSecure,
|
'secure' => $isSecure,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
@@ -26,9 +25,13 @@ function start_secure_session(): void {
|
|||||||
* Controleert of er al een beheerder bestaat in MariaDB
|
* Controleert of er al een beheerder bestaat in MariaDB
|
||||||
*/
|
*/
|
||||||
function has_admin_user(): bool {
|
function has_admin_user(): bool {
|
||||||
|
try {
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
|
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
|
||||||
return ((int)$stmt->fetchColumn()) > 0;
|
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 DEV OMGEVING: $environment = 'development'; (Rood met 'Development' + radar ping)
|
||||||
* - OP DE MAIN OMGEVING: $environment = 'live'; (Groen met 'Live' + 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')
|
require_once __DIR__ . '/../config/config.php';
|
||||||
$environment = 'live'; // <-- ZET DIT OP DE MAIN SERVER OP 'live'
|
|
||||||
|
|
||||||
// Optionele fallback als het in .env gedefinieerd is
|
// Standaard waarde voor de main branch: 'live' (kan overschreven worden via APP_ENV)
|
||||||
if (getenv('APP_ENV')) {
|
$environment = defined('APP_ENV') ? APP_ENV : 'live';
|
||||||
$envVal = strtolower(trim(getenv('APP_ENV')));
|
|
||||||
if (in_array($envVal, ['live', 'production', 'prod', 'main'])) {
|
if (function_exists('fociz_env') && fociz_env('APP_ENV')) {
|
||||||
$environment = 'live';
|
$environment = fociz_env('APP_ENV');
|
||||||
} elseif (in_array($envVal, ['dev', 'development', 'staging', 'test'])) {
|
|
||||||
$environment = 'development';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$isLive = in_array(strtolower($environment), ['live', 'production', 'prod', 'main']);
|
$isLive = in_array(strtolower(trim($environment)), ['live', 'production', 'prod', 'main']);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- FLASHY OMGEVINGSINDICATOR LINKS BOVENIN -->
|
<!-- FLASHY OMGEVINGSINDICATOR LINKS BOVENIN -->
|
||||||
@@ -66,4 +62,3 @@ $isLive = in_array(strtolower($environment), ['live', 'production', 'prod', 'mai
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</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)
|
* 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));
|
$userInputCode = trim(str_replace(' ', '', $userInputCode));
|
||||||
if (strlen($userInputCode) !== 6 || !ctype_digit($userInputCode)) {
|
if (strlen($userInputCode) !== 6 || !ctype_digit($userInputCode)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+522
-125
@@ -1,34 +1,151 @@
|
|||||||
<?php
|
<?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/header.php';
|
||||||
|
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||||
|
require_once __DIR__ . '/includes/integrations/jira.php';
|
||||||
|
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
$userId = $currentUser['id'];
|
$userId = $currentUser['id'];
|
||||||
|
|
||||||
// Formulierverwerking voor integratie instellingen
|
|
||||||
$message = null;
|
$message = null;
|
||||||
|
$messageType = 'success';
|
||||||
|
|
||||||
|
// Query parameters (bijv. na OAuth redirect)
|
||||||
|
if (!empty($_GET['success'])) {
|
||||||
|
$message = $_GET['success'];
|
||||||
|
$messageType = 'success';
|
||||||
|
} elseif (!empty($_GET['error'])) {
|
||||||
|
$message = $_GET['error'];
|
||||||
|
$messageType = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST Verwerking
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$provider = $_POST['provider'] ?? '';
|
$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;
|
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
|
||||||
$settings = json_encode([
|
|
||||||
'client_id' => $_POST['client_id'] ?? '',
|
// Haal bestaande tokens op
|
||||||
'client_secret' => $_POST['client_secret'] ?? '',
|
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
||||||
'tenant_id' => $_POST['tenant_id'] ?? '',
|
$stmtExisting->execute([$userId]);
|
||||||
'jira_domain' => $_POST['jira_domain'] ?? '',
|
$existing = $stmtExisting->fetch();
|
||||||
'jira_email' => $_POST['jira_email'] ?? '',
|
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
|
||||||
'jira_api_token' => $_POST['jira_api_token'] ?? '',
|
|
||||||
|
// 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("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
|
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
|
||||||
VALUES (?, ?, 1, ?, ?, NOW())
|
VALUES (?, 'OUTLOOK', 1, ?, ?, NOW())
|
||||||
ON DUPLICATE KEY UPDATE is_demo_mode = VALUES(is_demo_mode), settings = VALUES(settings), last_sync_at = 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]);
|
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
|
||||||
$message = "Instellingen voor {$provider} succesvol opgeslagen.";
|
$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) ?: [];
|
$configs[$c['provider']]['settings_parsed'] = json_decode($c['settings'] ?? '{}', true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jira demo issues
|
// Bepaal statussen
|
||||||
$jiraIssues = [
|
$isOutlookConnected = !empty($configs['OUTLOOK']['access_token']) && empty($configs['OUTLOOK']['is_demo_mode']);
|
||||||
[
|
$outlookSettings = $configs['OUTLOOK']['settings_parsed'] ?? [];
|
||||||
'key' => 'FOC-101',
|
|
||||||
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
|
$isJiraConnected = !empty($configs['JIRA']) && empty($configs['JIRA']['is_demo_mode']) && !empty($configs['JIRA']['settings_parsed']['jira_api_token']);
|
||||||
'status' => 'In Progress',
|
$jiraSettings = $configs['JIRA']['settings_parsed'] ?? [];
|
||||||
'priority' => 'High',
|
|
||||||
'epic' => 'Security & Access',
|
// Haal actuele Jira issues op
|
||||||
],
|
$forceRefresh = isset($_GET['refresh_jira']);
|
||||||
[
|
$jiraData = jira_get_user_issues($userId, $forceRefresh);
|
||||||
'key' => 'FOC-102',
|
$jiraIssues = $jiraData['issues'] ?? [];
|
||||||
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
|
$jiraIsDemo = !empty($jiraData['is_demo']);
|
||||||
'status' => 'To Do',
|
|
||||||
'priority' => 'High',
|
// Callback redirect URI
|
||||||
'epic' => 'Brainstorming Module',
|
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
|
||||||
],
|
|
||||||
[
|
|
||||||
'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',
|
|
||||||
]
|
|
||||||
];
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 shadow-xl">
|
<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">
|
<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>
|
<i data-lucide="layers" class="w-4 h-4"></i>
|
||||||
<span>Connectiviteit & Synchronisatie</span>
|
<span>Live Koppelingen & Data Synchronisatie</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl sm:text-3xl font-black text-white tracking-tight">
|
<h1 class="text-2xl sm:text-3xl font-black text-white tracking-tight">
|
||||||
Integraties Hub
|
Integraties Hub
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-slate-400 text-xs sm:text-sm mt-1">
|
<p class="text-slate-400 text-xs sm:text-sm mt-1 max-w-2xl">
|
||||||
Beheer uw koppelingen met Microsoft Outlook (agenda & taken), Microsoft Teams (meetings) en Atlassian Jira (issues & roadmap).
|
Koppel Microsoft 365 Outlook (voor realtime agenda & Teams meetings) en Atlassian Jira Cloud (om direct uw toegewezen tickets in te plannen).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Snelle status pills -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl border text-xs font-semibold <?= $isOutlookConnected ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400' : 'bg-slate-800/80 border-slate-700 text-slate-400' ?>">
|
||||||
|
<span class="w-2 h-2 rounded-full <?= $isOutlookConnected ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500' ?>"></span>
|
||||||
|
<span>Outlook: <?= $isOutlookConnected ? 'Verbonden' : 'Niet verbonden' ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl border text-xs font-semibold <?= $isJiraConnected ? 'bg-sky-500/10 border-sky-500/30 text-sky-400' : 'bg-slate-800/80 border-slate-700 text-slate-400' ?>">
|
||||||
|
<span class="w-2 h-2 rounded-full <?= $isJiraConnected ? 'bg-sky-400 animate-pulse' : 'bg-slate-500' ?>"></span>
|
||||||
|
<span>Jira: <?= $isJiraConnected ? 'Verbonden' : 'Niet verbonden' ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash message -->
|
||||||
<?php if ($message): ?>
|
<?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">
|
<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' ?>">
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<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>
|
<span><?= htmlspecialchars($message) ?></span>
|
||||||
</div>
|
</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; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Integratie Cards Grid -->
|
<!-- 3 Koloms Grid voor Providers -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
<!-- 1. OUTLOOK -->
|
<!-- 1. MICROSOFT OUTLOOK -->
|
||||||
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col justify-between">
|
<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>
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between mb-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">
|
<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>
|
<i data-lucide="calendar" class="w-5 h-5"></i>
|
||||||
</div>
|
</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">
|
<?php if ($isOutlookConnected): ?>
|
||||||
<i data-lucide="check-circle" class="w-3 h-3"></i>
|
<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>Actief</span>
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
|
<span>Live Verbonden</span>
|
||||||
|
</span>
|
||||||
|
<?php elseif (!empty($configs['OUTLOOK']['is_demo_mode'])): ?>
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-semibold">
|
||||||
|
Sandbox Modus
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 border border-slate-700 text-slate-400 text-xs font-semibold">
|
||||||
|
Niet Gekoppeld
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-bold text-white mb-1">Microsoft Outlook (365)</h3>
|
||||||
|
<p class="text-xs text-slate-400 leading-relaxed">
|
||||||
|
Synchroniseert automatisch uw zakelijke agenda-afspraken en meetings naar de dagplanner tijdlijn.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($isOutlookConnected): ?>
|
||||||
|
<div class="bg-slate-950/70 rounded-xl p-3.5 border border-emerald-500/20 text-xs space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
|
<span>Account:</span>
|
||||||
|
<span class="text-emerald-400 font-semibold truncate max-w-[180px]" title="<?= htmlspecialchars($outlookSettings['account_email'] ?? '') ?>">
|
||||||
|
<?= htmlspecialchars($outlookSettings['account_email'] ?? 'Verbonden') ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
<span>Status:</span>
|
<span>Naam:</span>
|
||||||
<span class="text-emerald-400 font-semibold">Gekoppeld</span>
|
<span class="text-slate-200"><?= htmlspecialchars($outlookSettings['account_name'] ?? '-') ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-slate-400">
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
<span>Sync frequentie:</span>
|
<span>Laatste Sync:</span>
|
||||||
<span class="text-slate-200">Elke 15 min</span>
|
<span class="text-slate-300 font-mono text-[11px]"><?= htmlspecialchars($configs['OUTLOOK']['last_sync_at'] ?? 'Zojuist') ?></span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Instellingen formulier -->
|
<!-- 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">
|
<input type="hidden" name="provider" value="OUTLOOK">
|
||||||
|
|
||||||
<label class="flex items-center space-x-2 text-xs text-slate-300">
|
<div>
|
||||||
<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">
|
<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>
|
<span>Gebruik interactieve Sandbox data</span>
|
||||||
</label>
|
</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">
|
<div class="flex space-x-2 pt-2">
|
||||||
Instellingen Bijwerken
|
<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>
|
||||||
|
|
||||||
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. MICROSOFT TEAMS -->
|
<!-- 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 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>
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between mb-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">
|
<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>
|
<i data-lucide="video" class="w-5 h-5"></i>
|
||||||
</div>
|
</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">
|
<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>
|
<i data-lucide="check-circle" class="w-3 h-3"></i>
|
||||||
<span>Actief</span>
|
<span>Inbegrepen bij Outlook</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<h3 class="text-base font-bold text-white mb-1">Microsoft Teams</h3>
|
<h3 class="text-base font-bold text-white mb-1">Microsoft Teams</h3>
|
||||||
<p class="text-xs text-slate-400 mb-4">
|
<p class="text-xs text-slate-400 leading-relaxed">
|
||||||
Toont vandaag geplande Teams meetings met directe 'Deelnemen' links en kanaalupdates.
|
Detecteert Teams vergaderingen automatisch vanuit uw Outlook agenda en toont directe 'Deelnemen' knoppen.
|
||||||
</p>
|
</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">
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
<span>Direct Deelnemen:</span>
|
<span>Direct Deelnemen:</span>
|
||||||
<span class="text-emerald-400 font-semibold">Ondersteund</span>
|
<span class="text-emerald-400 font-semibold">Ondersteund</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-slate-400">
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
<span>Meldingen:</span>
|
<span>Meeting detectie:</span>
|
||||||
<span class="text-slate-200">Ingeschakeld</span>
|
<span class="text-slate-200">Via Graph API</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="/integrations.php" class="pt-3 border-t border-slate-800 space-y-3">
|
<div class="p-3 bg-indigo-950/30 border border-indigo-900/50 rounded-xl text-xs text-indigo-200/80">
|
||||||
|
<p class="leading-relaxed">
|
||||||
|
Teams vergaderingen worden automatisch uitgelezen zodra Microsoft Outlook is gekoppeld. U hoeft voor Teams geen aparte koppeling te configureren!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="/integrations.php" class="pt-4 border-t border-slate-800 space-y-3 mt-4">
|
||||||
<input type="hidden" name="provider" value="TEAMS">
|
<input type="hidden" name="provider" value="TEAMS">
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
|
||||||
<label class="flex items-center space-x-2 text-xs text-slate-300">
|
<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>
|
<span>Gebruik interactieve Sandbox data</span>
|
||||||
</label>
|
</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">
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. ATLASSIAN JIRA -->
|
<!-- 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 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>
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between mb-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">
|
<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>
|
<i data-lucide="trello" class="w-5 h-5"></i>
|
||||||
</div>
|
</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">
|
<?php if ($isJiraConnected): ?>
|
||||||
<i data-lucide="check-circle" class="w-3 h-3"></i>
|
<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>Actief</span>
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
|
<span>Live Verbonden</span>
|
||||||
|
</span>
|
||||||
|
<?php elseif (!empty($configs['JIRA']['is_demo_mode'])): ?>
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-semibold">
|
||||||
|
Sandbox Modus
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full bg-slate-800 border border-slate-700 text-slate-400 text-xs font-semibold">
|
||||||
|
Niet Gekoppeld
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-bold text-white mb-1">Atlassian Jira Cloud</h3>
|
||||||
|
<p class="text-xs text-slate-400 leading-relaxed">
|
||||||
|
Koppelt uw Jira tickets rechtstreeks aan de Fociz dagplanning en roadmap.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($isJiraConnected): ?>
|
||||||
|
<div class="bg-slate-950/70 rounded-xl p-3.5 border border-sky-500/20 text-xs space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
|
<span>Ingelogd als:</span>
|
||||||
|
<span class="text-sky-400 font-semibold truncate max-w-[170px]" title="<?= htmlspecialchars($jiraSettings['jira_email'] ?? '') ?>">
|
||||||
|
<?= htmlspecialchars($jiraSettings['account_name'] ?: ($jiraSettings['jira_email'] ?? '')) ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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 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">
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
<span>Two-way Status Sync:</span>
|
<span>Domein:</span>
|
||||||
<span class="text-emerald-400 font-semibold">Ondersteund</span>
|
<span class="text-slate-200 font-mono text-[11px] truncate max-w-[170px]"><?= htmlspecialchars($jiraSettings['jira_domain'] ?? '-') ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-slate-400">
|
<div class="flex items-center justify-between text-slate-400">
|
||||||
<span>Roadmap Linkage:</span>
|
<span>Tickets Toegewezen:</span>
|
||||||
<span class="text-slate-200">Actief</span>
|
<span class="text-emerald-400 font-bold"><?= count($jiraIssues) ?> open taken</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="bg-slate-950/50 rounded-xl border border-slate-800/80 p-3 text-xs text-slate-400">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-300 font-semibold">Atlassian API Token nodig?</span>
|
||||||
|
<a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" class="text-sky-400 hover:text-sky-300 font-semibold flex items-center space-x-1">
|
||||||
|
<span>Genereer Token</span>
|
||||||
|
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<input type="hidden" name="provider" value="JIRA">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[10px] text-slate-400 mb-0.5">Jira Cloud Domein</label>
|
<label class="block text-[10px] font-semibold 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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<label class="flex items-center space-x-2 text-xs text-slate-300 pt-1">
|
<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>
|
<span>Gebruik interactieve Sandbox data</span>
|
||||||
</label>
|
</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">
|
<div class="flex space-x-2 pt-2">
|
||||||
Jira Opslaan
|
<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>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gesynchroniseerde Jira Issues & 1-Click Import naar Dagplanning -->
|
<!-- 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="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">
|
<div class="flex items-center space-x-2">
|
||||||
<i data-lucide="check-square" class="w-5 h-5 text-sky-400"></i>
|
<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">
|
<h3 class="text-sm font-bold text-white uppercase tracking-wider">
|
||||||
Jira Issues Toegewezen aan U
|
Jira Issues Toegewezen aan U
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
<?php if ($jiraIsDemo): ?>
|
||||||
<span class="text-xs text-slate-400 font-mono"><?= count($jiraIssues) ?> issues</span>
|
<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>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<span class="text-xs text-slate-400 font-mono"><?= count($jiraIssues) ?> issues</span>
|
||||||
|
<a href="/integrations.php?refresh_jira=1" class="flex items-center space-x-1 text-xs text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 px-2.5 py-1 rounded-lg border border-slate-700 transition">
|
||||||
|
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Verversen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($jiraIssues)): ?>
|
||||||
|
<div class="text-center py-10 text-slate-500 text-xs">
|
||||||
|
Geen openstaande tickets gevonden die aan u zijn toegewezen in Jira.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<?php foreach ($jiraIssues as $issue): ?>
|
<?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">
|
$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>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<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">
|
<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']) ?>
|
<?= htmlspecialchars($issue['key']) ?>
|
||||||
</span>
|
</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">
|
<span class="text-[10px] font-semibold px-2 py-0.5 rounded bg-slate-800 text-slate-300">
|
||||||
<?= htmlspecialchars($issue['status']) ?>
|
<?= htmlspecialchars($issue['status']) ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="text-xs font-bold text-white leading-snug">
|
|
||||||
|
<h4 class="text-xs font-bold text-white leading-snug group-hover:text-sky-300 transition">
|
||||||
<?= htmlspecialchars($issue['summary']) ?>
|
<?= htmlspecialchars($issue['summary']) ?>
|
||||||
</h4>
|
</h4>
|
||||||
<span class="text-[10px] text-slate-500 mt-1 block">Epic: <?= htmlspecialchars($issue['epic']) ?></span>
|
|
||||||
|
<div class="flex items-center space-x-3 mt-2 text-[10px] text-slate-500">
|
||||||
|
<span>Type: <?= htmlspecialchars($issue['issue_type'] ?? 'Issue') ?></span>
|
||||||
|
<?php if (!empty($issue['due_date'])): ?>
|
||||||
|
<span>Vervaldatum: <?= htmlspecialchars($issue['due_date']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
|
<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>
|
<span class="text-[10px] font-bold px-2 py-0.5 rounded border <?= $priorityClass ?>">
|
||||||
<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">
|
<?= 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>
|
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||||
<span>Plan als Dagtaak</span>
|
<span>Plan als P1</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function importJiraToTasks(title, jiraKey) {
|
function copyToClipboard(elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
if (!el) return;
|
||||||
|
navigator.clipboard.writeText(el.value).then(() => {
|
||||||
|
alert('Omleidings-URI gekopieerd naar klembord!');
|
||||||
|
}).catch(() => {
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert('Omleidings-URI gekopieerd!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePasswordVisibility(inputId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (!input) return;
|
||||||
|
input.type = input.type === 'password' ? 'text' : 'password';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importJiraToTasks(btn, title, jiraKey, url, priority = 'P1') {
|
||||||
|
const originalHtml = btn.innerHTML;
|
||||||
|
btn.innerText = 'Toevoegen...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await fetch('/api/planner.php?action=add_task', {
|
const res = await fetch('/api/planner.php?action=add_task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: `[${jiraKey}] ${title}`,
|
title: `[${jiraKey}] ${title}`,
|
||||||
priority: 'P1',
|
priority: priority,
|
||||||
|
source: 'JIRA',
|
||||||
|
external_id: jiraKey,
|
||||||
|
external_url: url,
|
||||||
date: '<?= date('Y-m-d') ?>'
|
date: '<?= date('Y-m-d') ?>'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(`Jira issue ${jiraKey} is toegevoegd aan uw dagplanning!`);
|
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>
|
</script>
|
||||||
|
|
||||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
||||||
|
|||||||
+201
-22
@@ -1,15 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Fociz Dagplanning, Focus Modus & Time-blocking
|
* 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/header.php';
|
||||||
|
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||||
|
require_once __DIR__ . '/includes/integrations/jira.php';
|
||||||
|
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
$userId = $currentUser['id'];
|
$userId = $currentUser['id'];
|
||||||
$today = date('Y-m-d');
|
$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 = $db->prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?");
|
||||||
$stmtPlan->execute([$userId, $today]);
|
$stmtPlan->execute([$userId, $today]);
|
||||||
$dailyPlan = $stmtPlan->fetch();
|
$dailyPlan = $stmtPlan->fetch();
|
||||||
@@ -24,7 +27,7 @@ if (!$dailyPlan) {
|
|||||||
$dailyPlan = ['id' => $planId, 'plan_date' => $today, 'focus_goal' => 'Mijn belangrijkste doel van vandaag behalen', 'notes' => ''];
|
$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("
|
$stmtTasks = $db->prepare("
|
||||||
SELECT * FROM tasks
|
SELECT * FROM tasks
|
||||||
WHERE user_id = ? AND (daily_plan_id = ? OR daily_plan_id IS NULL)
|
WHERE user_id = ? AND (daily_plan_id = ? OR daily_plan_id IS NULL)
|
||||||
@@ -33,7 +36,25 @@ $stmtTasks = $db->prepare("
|
|||||||
$stmtTasks->execute([$userId, $dailyPlan['id']]);
|
$stmtTasks->execute([$userId, $dailyPlan['id']]);
|
||||||
$tasks = $stmtTasks->fetchAll();
|
$tasks = $stmtTasks->fetchAll();
|
||||||
|
|
||||||
// Outlook demo / gesynchroniseerde events
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 = [
|
$outlookEvents = [
|
||||||
[
|
[
|
||||||
'id' => 'evt-1',
|
'id' => 'evt-1',
|
||||||
@@ -42,6 +63,7 @@ $outlookEvents = [
|
|||||||
'end' => '09:30',
|
'end' => '09:30',
|
||||||
'location' => 'Microsoft Teams',
|
'location' => 'Microsoft Teams',
|
||||||
'meeting_url' => 'https://teams.microsoft.com',
|
'meeting_url' => 'https://teams.microsoft.com',
|
||||||
|
'source' => 'SANDBOX',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => 'evt-2',
|
'id' => 'evt-2',
|
||||||
@@ -50,6 +72,7 @@ $outlookEvents = [
|
|||||||
'end' => '12:00',
|
'end' => '12:00',
|
||||||
'location' => 'Vergaderruimte A & Teams',
|
'location' => 'Vergaderruimte A & Teams',
|
||||||
'meeting_url' => 'https://teams.microsoft.com',
|
'meeting_url' => 'https://teams.microsoft.com',
|
||||||
|
'source' => 'SANDBOX',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => 'evt-3',
|
'id' => 'evt-3',
|
||||||
@@ -58,10 +81,30 @@ $outlookEvents = [
|
|||||||
'end' => '15:30',
|
'end' => '15:30',
|
||||||
'location' => 'Online Teams Call',
|
'location' => 'Online Teams Call',
|
||||||
'meeting_url' => 'https://teams.microsoft.com',
|
'meeting_url' => 'https://teams.microsoft.com',
|
||||||
|
'source' => 'SANDBOX',
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$hours = ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00"];
|
// 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
|
// Nederlandse datumformattering
|
||||||
$dutchDays = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
|
$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">
|
<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="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 class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -83,11 +126,34 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<!-- Integratie Status Badges -->
|
||||||
<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">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
|
<?php if ($isOutlookConnected): ?>
|
||||||
<span>Outlook & Teams Verbonden</span>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,7 +183,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
|
||||||
<!-- Linker kolom: Focus Timer & Taken -->
|
<!-- Linker kolom: Focus Timer & Taken -->
|
||||||
@@ -212,7 +278,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
|||||||
<div class="space-y-2" id="tasksContainer">
|
<div class="space-y-2" id="tasksContainer">
|
||||||
<?php if (empty($tasks)): ?>
|
<?php if (empty($tasks)): ?>
|
||||||
<div class="text-center py-8 text-slate-500 text-xs">
|
<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>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($tasks as $task): ?>
|
<?php foreach ($tasks as $task): ?>
|
||||||
@@ -225,15 +291,31 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
<span class="text-xs font-medium block truncate <?= $task['is_completed'] ? 'line-through text-slate-500' : 'text-slate-200' ?>">
|
<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']) ?>
|
<?= htmlspecialchars($task['title']) ?>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2 mt-0.5">
|
||||||
<?php if (!empty($task['time_block_start'])): ?>
|
<?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">
|
<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>
|
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
|
||||||
<span><?= htmlspecialchars($task['time_block_start']) ?></span>
|
<span><?= htmlspecialchars($task['time_block_start']) ?></span>
|
||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
<?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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -258,6 +340,61 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Dagnotities & Reflectie -->
|
||||||
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl backdrop-blur-sm">
|
<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">
|
<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>
|
<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>
|
<h3 class="text-sm font-bold text-white uppercase tracking-wider">Tijdlijn & Time-blocking</h3>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<?php foreach ($hours as $hour):
|
<?php foreach ($hours as $hour):
|
||||||
$hourInt = (int)explode(':', $hour)[0];
|
$hourInt = (int)explode(':', $hour)[0];
|
||||||
|
|
||||||
// Zoek Outlook meetings
|
// Zoek Outlook meetings die binnen dit uur vallen
|
||||||
$slotEvents = array_filter($outlookEvents, function($e) use ($hourInt) {
|
$slotEvents = array_filter($outlookEvents, function($e) use ($hourInt) {
|
||||||
|
if (empty($e['start'])) return false;
|
||||||
return (int)explode(':', $e['start'])[0] === $hourInt;
|
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) {
|
$slotTasks = array_filter($tasks, function($t) use ($hourInt) {
|
||||||
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $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">
|
<div class="flex-1 space-y-1.5">
|
||||||
<!-- Outlook meetings -->
|
<!-- Outlook meetings -->
|
||||||
<?php foreach ($slotEvents as $evt): ?>
|
<?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">
|
<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">
|
<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>
|
||||||
<span class="font-semibold truncate"><?= htmlspecialchars($evt['subject']) ?></span>
|
|
||||||
</div>
|
</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">
|
<?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>
|
<i data-lucide="video" class="w-3 h-3"></i>
|
||||||
<span>Deelnemen</span>
|
<span>Deelnemen</span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
@@ -374,7 +521,6 @@ function setTimerPreset(minutes) {
|
|||||||
totalDuration = minutes * 60;
|
totalDuration = minutes * 60;
|
||||||
updateTimerDisplay();
|
updateTimerDisplay();
|
||||||
|
|
||||||
// Toggle styling
|
|
||||||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
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('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';
|
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) {
|
async function toggleTask(taskId, isCompleted) {
|
||||||
await fetch('/api/planner.php?action=toggle_task', {
|
await fetch('/api/planner.php?action=toggle_task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -491,4 +671,3 @@ async function deleteTask(taskId) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user