From 9da056b48073ce5acd2c52393d9309c3f6a8d671 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Tue, 8 Sep 2026 12:47:11 +0200 Subject: [PATCH] Add roadmap management feature with database schema and user management - Implemented roadmap.php for displaying and managing roadmap items with filters for quarter and status. - Created schema.sql to define the database structure for users, invites, daily plans, tasks, brainstorm sessions, brainstorm cards, roadmap items, and integrations. - Developed users.php for admin panel to manage team members and invitations, including functionality to create and revoke invites. --- .env.example | 12 + .gitignore | 10 + .htaccess | 16 ++ README.md | 86 ++++++++ api/brainstorm.php | 146 +++++++++++++ api/planner.php | 111 ++++++++++ api/roadmap.php | 93 ++++++++ auth/invite.php | 185 ++++++++++++++++ auth/login.php | 212 ++++++++++++++++++ auth/logout.php | 29 +++ auth/setup.php | 209 ++++++++++++++++++ brainstorm.php | 171 +++++++++++++++ brainstorm_canvas.php | 371 +++++++++++++++++++++++++++++++ config/config.php | 40 ++++ config/database.php | 68 ++++++ includes/auth.php | 136 ++++++++++++ includes/footer.php | 24 ++ includes/header.php | 144 ++++++++++++ includes/totp.php | 132 +++++++++++ index.php | 21 ++ integrations.php | 304 ++++++++++++++++++++++++++ planner.php | 494 ++++++++++++++++++++++++++++++++++++++++++ roadmap.php | 329 ++++++++++++++++++++++++++++ schema.sql | 128 +++++++++++ users.php | 266 +++++++++++++++++++++++ 25 files changed, 3737 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 README.md create mode 100644 api/brainstorm.php create mode 100644 api/planner.php create mode 100644 api/roadmap.php create mode 100644 auth/invite.php create mode 100644 auth/login.php create mode 100644 auth/logout.php create mode 100644 auth/setup.php create mode 100644 brainstorm.php create mode 100644 brainstorm_canvas.php create mode 100644 config/config.php create mode 100644 config/database.php create mode 100644 includes/auth.php create mode 100644 includes/footer.php create mode 100644 includes/header.php create mode 100644 includes/totp.php create mode 100644 index.php create mode 100644 integrations.php create mode 100644 planner.php create mode 100644 roadmap.php create mode 100644 schema.sql create mode 100644 users.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..475721d --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# MariaDB Verbinding +DB_HOST="127.0.0.1" +DB_PORT="3306" +DB_NAME="fociz" +DB_USER="root" +DB_PASS="uw_wachtwoord" + +# Applicatie URL (voor uitnodigingslinks) +APP_URL="https://uw-domein.nl" + +# Applicatie Geheime Sleutel (willekeurige tekenreeks) +APP_SECRET="fociz-maria-db-super-secret-key-998877" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..514281b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Environment +.env +.env*.local + +# Systeem & Editor +.DS_Store +Thumbs.db +*.log +.vscode/ +.idea/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e736250 --- /dev/null +++ b/.htaccess @@ -0,0 +1,16 @@ +# Fociz Apache Configuratie +DirectoryIndex index.php index.html + +# UTF-8 standaard +AddDefaultCharset UTF-8 + +# Blokkeer directe toegang tot SQL schema en configuratiebestanden + + Order allow,deny + Deny from all + + +# PHP foutmeldingen in productie (kan uitgeschakeld worden indien gewenst) +php_flag display_errors Off +php_value error_reporting E_ALL + diff --git a/README.md b/README.md new file mode 100644 index 0000000..050e774 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# Fociz — Focus, Dagplanning, Brainstorm & Roadmap Systeem + +Fociz is een compleet, modern webplatform gebouwd in **PHP 8 & MariaDB** voor dagplanning, werkstructurering, interactieve brainstormsessies die met één klik omgezet kunnen worden in een roadmap, en koppelingen met Outlook, Teams en Jira. + +--- + +## 🔒 Beveiliging & Toegangsbeheer + +- **Strikt afgesloten**: Geen openbare registratie. Enkel toegankelijk na inloggen met tweetrapsauthenticatie (2FA). +- **First-run Admin Setup (`/auth/setup.php`)**: + - Bij het allereerste gebruik detecteert Fociz automatisch dat er nog geen beheerder bestaat en start de setup wizard. + - De beheerder voert naam, e-mail en wachtwoord in en scant de 2FA QR-code met bijv. **Google Authenticator**, **Microsoft Authenticator** of **1Password**. + - Na verificatie van de 6-cijferige code wordt de beheerder opgeslagen en wordt `/auth/setup.php` definitief vergrendeld. +- **Invite-only Registratie (`/auth/invite.php?token=...`)**: + - Nieuwe collega's kunnen enkel toetreden met een unieke uitnodigingslink aangemaakt door de beheerder (`/users.php`). + - Dwingt direct het instellen van een wachtwoord en 2FA-registratie af. +- **Inloggen (`/auth/login.php`)**: + - Stap 1: E-mailadres & wachtwoord (`password_hash` / `password_verify`). + - Stap 2: 6-cijferige TOTP verificatiecode (of herstelcode). + +--- + +## 🚀 Kernmodules + +1. **Dagplanning & Focus (`/planner.php`)**: + - **Dagdoel**: Leg uw nummer 1 focuspunt van de dag vast met één klik. + - **Focus Modus**: Ingebouwde Pomodoro/Stopwatch timer (25 min Focus, 50 min Deep Work, 5 min Pauze) met visuele voortgang en taakselectie. + - **Dagtaken & Prioriteiten**: Taken indelen op prioriteit (P1 Brandend, P2 Belangrijk, P3 Regulier), snel toevoegen en afvinken. + - **Tijdlijn & Time-blocking**: Uuroverzicht (08:00 - 18:00) met time-blocking voor taken en gesynchroniseerde Outlook meetings. + - **Dagnotities**: Snel kladblok voor reflecties en memo's. + +2. **Brainstorm Canvas (`/brainstorm.php` & `/brainstorm_canvas.php`)**: + - Interactief whiteboard met sticky notes ingedeeld in 4 categorieën: + - 💡 **Ideeën & Concepten** + - ⚠️ **Knelpunten & Uitdagingen** + - 🚀 **Oplossingen & Features** + - ⚡ **Quick Wins** + - Stemmen (upvoting) op ideeën. + - **Zet om naar Roadmap**: Selecteer brainstormkaarten en zet ze direct om naar planbare roadmap-items met kwartaal, deadline en prioriteit. + +3. **Roadmap & Tijdlijn (`/roadmap.php`)**: + - Kwartaaloverzicht (Q1, Q2, Q3, Q4) en statussen (Gepland, In Uitvoering, In Review, Voltooid). + - Voortgangsbalken (0 - 100%) en deadlines. + - Gekoppeld aan de oorspronkelijke brainstormkaarten en optioneel Jira Issue Keys (bijv. `FOC-101`). + +4. **Integraties Hub (`/integrations.php`)**: + - **Microsoft Outlook**: Synchroniseert agenda-afspraken direct naar de dagplanning. + - **Microsoft Teams**: Directe deelnamelinks naar online meetings en kanaalnotificaties. + - **Atlassian Jira**: Issues synchroniseren en direct met één klik inplannen als dagtaak. + - **Sandbox / Demo Modus**: Schakel met één vinkje tussen live productie en interactieve testdata. + +5. **Team & Invites Beheer (`/users.php` - Admin only)**: + - Genereer beveiligde uitnodigingslinks voor collega's met kopieerknop. + - Overzicht van actieve gebruikers, rollen en 2FA-status. + +--- + +## 🛠️ Installatie op uw Server + +### 1. Bestanden uploaden +Push of upload alle bestanden naar de gewenste webdirectory op uw server (bijv. `/var/www/fociz` of `public_html`). + +### 2. MariaDB Database configureren +Pas de databasegegevens aan in `.env` (of direct in `config/config.php`): +```env +DB_HOST="127.0.0.1" +DB_PORT="3306" +DB_NAME="fociz" +DB_USER="uw_mariadb_gebruiker" +DB_PASS="uw_mariadb_wachtwoord" +APP_URL="https://uw-domein.nl" +``` + +### 3. Automatische Tabellen Aanmaak +U hoeft **niet** handmatig SQL-commando's uit te voeren: zodra u de website voor het eerst opent in uw browser, detecteert Fociz automatisch MariaDB en worden alle tabellen en relaties uit `schema.sql` vanzelf aangemaakt! + +*(Optioneel kunt u `schema.sql` ook zelf importeren via phpMyAdmin of de MariaDB CLI).* + +### 4. Eerste Beheerder Instellen +1. Open uw browser en ga naar `https://uw-domein.nl`. +2. U wordt automatisch doorverwezen naar `/auth/setup.php`. +3. Vul uw naam, e-mailadres en gewenst wachtwoord in. +4. Scan de QR-code met Google Authenticator of Microsoft Authenticator. +5. Vul de 6-cijferige code in om te bevestigen. +6. **Klaar!** U kunt nu direct inloggen en collega's uitnodigen. + diff --git a/api/brainstorm.php b/api/brainstorm.php new file mode 100644 index 0000000..8c3cdc8 --- /dev/null +++ b/api/brainstorm.php @@ -0,0 +1,146 @@ + 'Inhoud is verplicht']); + exit; + } + + $stmt = $db->prepare(" + INSERT INTO brainstorm_cards (session_id, content, category, color, author_name, votes) + VALUES (?, ?, ?, ?, ?, 0) + "); + $stmt->execute([$sessionId, $content, $category, $color, $user['name']]); + $cardId = (int)$db->lastInsertId(); + + echo json_encode([ + 'success' => true, + 'card' => [ + 'id' => $cardId, + 'content' => $content, + 'category' => $category, + 'color' => $color, + 'author_name' => $user['name'], + 'votes' => 0, + 'converted_to_roadmap_item_id' => null + ] + ]); + exit; + } + + // 2. Stemmen / Upvote op kaart + if ($action === 'vote_card') { + $data = json_decode(file_get_contents('php://input'), true); + $cardId = (int)($data['card_id'] ?? 0); + + $stmt = $db->prepare("UPDATE brainstorm_cards SET votes = votes + 1 WHERE id = ?"); + $stmt->execute([$cardId]); + + $stmtVotes = $db->prepare("SELECT votes FROM brainstorm_cards WHERE id = ?"); + $stmtVotes->execute([$cardId]); + $votes = (int)$stmtVotes->fetchColumn(); + + echo json_encode(['success' => true, 'votes' => $votes]); + exit; + } + + // 3. Kaart verwijderen + if ($action === 'delete_card') { + $data = json_decode(file_get_contents('php://input'), true); + $cardId = (int)($data['card_id'] ?? 0); + + $stmt = $db->prepare("DELETE FROM brainstorm_cards WHERE id = ?"); + $stmt->execute([$cardId]); + + echo json_encode(['success' => true]); + exit; + } + + // 4. Omzetten naar Roadmap! + if ($action === 'convert_to_roadmap') { + $data = json_decode(file_get_contents('php://input'), true); + $cardIds = $data['card_ids'] ?? []; + $quarter = $data['quarter'] ?? 'Q3'; + $type = $data['type'] ?? 'FEATURE'; + $priority = $data['priority'] ?? 'MEDIUM'; + $dueDate = !empty($data['due_date']) ? $data['due_date'] : null; + + if (empty($cardIds) || !is_array($cardIds)) { + echo json_encode(['error' => 'Selecteer ten minste één kaart om om te zetten.']); + exit; + } + + $db->beginTransaction(); + + $convertedCount = 0; + foreach ($cardIds as $cardId) { + $cardId = (int)$cardId; + $stmtCard = $db->prepare("SELECT * FROM brainstorm_cards WHERE id = ?"); + $stmtCard->execute([$cardId]); + $card = $stmtCard->fetch(); + + if ($card) { + // Maak titel (eerste zin of max 80 tekens) + $firstLine = explode("\n", trim($card['content']))[0]; + $title = mb_substr($firstLine, 0, 80); + + $stmtRoadmap = $db->prepare(" + INSERT INTO roadmap_items (title, description, type, status, quarter, priority, progress, due_date, origin_brainstorm_card_id, created_by_id) + VALUES (?, ?, ?, 'PLANNED', ?, ?, 0, ?, ?, ?) + "); + $stmtRoadmap->execute([ + $title, + $card['content'], + $type, + $quarter, + $priority, + $dueDate, + $cardId, + $user['id'] + ]); + $roadmapId = (int)$db->lastInsertId(); + + // Koppel terug aan brainstorm kaart + $stmtUpdateCard = $db->prepare("UPDATE brainstorm_cards SET converted_to_roadmap_item_id = ? WHERE id = ?"); + $stmtUpdateCard->execute([$roadmapId, $cardId]); + + $convertedCount++; + } + } + + $db->commit(); + + echo json_encode([ + 'success' => true, + 'message' => "{$convertedCount} brainstorm items succesvol omgezet naar de Roadmap!", + 'converted_count' => $convertedCount + ]); + exit; + } + + echo json_encode(['error' => 'Onbekende actie']); +} catch (Exception $e) { + if ($db->inTransaction()) $db->rollBack(); + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} + diff --git a/api/planner.php b/api/planner.php new file mode 100644 index 0000000..3e839cc --- /dev/null +++ b/api/planner.php @@ -0,0 +1,111 @@ +prepare(" + INSERT INTO daily_plans (user_id, plan_date, focus_goal, notes) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE focus_goal = VALUES(focus_goal), notes = VALUES(notes) + "); + $stmt->execute([$user['id'], $date, $focusGoal, $notes]); + + echo json_encode(['success' => true]); + exit; + } + + // 2. Taak toevoegen + if ($action === 'add_task') { + $data = json_decode(file_get_contents('php://input'), true); + $title = trim($data['title'] ?? ''); + $priority = in_array($data['priority'] ?? '', ['P1', 'P2', 'P3']) ? $data['priority'] : 'P2'; + $timeBlockStart = $data['time_block_start'] ?? null; + $date = $data['date'] ?? $today; + + if (empty($title)) { + echo json_encode(['error' => 'Titel is verplicht']); + exit; + } + + // Haal of maak daily plan op + $stmtPlan = $db->prepare(" + INSERT INTO daily_plans (user_id, plan_date) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id) + "); + $stmtPlan->execute([$user['id'], $date]); + $planId = $db->lastInsertId(); + + $stmt = $db->prepare(" + INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status) + VALUES (?, ?, ?, ?, ?, 'TODO') + "); + $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart]); + $taskId = (int)$db->lastInsertId(); + + echo json_encode([ + 'success' => true, + 'task' => [ + 'id' => $taskId, + 'title' => $title, + 'priority' => $priority, + 'time_block_start' => $timeBlockStart, + 'is_completed' => 0, + 'status' => 'TODO' + ] + ]); + exit; + } + + // 3. Taak status toggelen (voltooid / open) + if ($action === 'toggle_task') { + $data = json_decode(file_get_contents('php://input'), true); + $taskId = (int)($data['task_id'] ?? 0); + $isCompleted = !empty($data['is_completed']) ? 1 : 0; + $status = $isCompleted ? 'DONE' : 'TODO'; + + $stmt = $db->prepare(" + UPDATE tasks SET is_completed = ?, status = ? + WHERE id = ? AND user_id = ? + "); + $stmt->execute([$isCompleted, $status, $taskId, $user['id']]); + + echo json_encode(['success' => true]); + exit; + } + + // 4. Taak verwijderen + if ($action === 'delete_task') { + $data = json_decode(file_get_contents('php://input'), true); + $taskId = (int)($data['task_id'] ?? 0); + + $stmt = $db->prepare("DELETE FROM tasks WHERE id = ? AND user_id = ?"); + $stmt->execute([$taskId, $user['id']]); + + echo json_encode(['success' => true]); + exit; + } + + echo json_encode(['error' => 'Onbekende actie']); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} + diff --git a/api/roadmap.php b/api/roadmap.php new file mode 100644 index 0000000..d617236 --- /dev/null +++ b/api/roadmap.php @@ -0,0 +1,93 @@ + 'Titel is verplicht']); + exit; + } + + $stmt = $db->prepare(" + INSERT INTO roadmap_items (title, description, quarter, type, priority, status, progress, due_date, jira_issue_key, created_by_id) + VALUES (?, ?, ?, ?, ?, 'PLANNED', 0, ?, ?, ?) + "); + $stmt->execute([$title, $description, $quarter, $type, $priority, $dueDate, $jiraKey, $user['id']]); + + echo json_encode(['success' => true]); + exit; + } + + // 2. Status of voortgang bijwerken + if ($action === 'update_item') { + $data = json_decode(file_get_contents('php://input'), true); + $id = (int)($data['id'] ?? 0); + $status = $data['status'] ?? null; + $progress = isset($data['progress']) ? (int)$data['progress'] : null; + $jiraKey = isset($data['jira_key']) ? trim($data['jira_key']) : null; + + $fields = []; + $params = []; + + if ($status !== null) { + $fields[] = "status = ?"; + $params[] = $status; + } + if ($progress !== null) { + $fields[] = "progress = ?"; + $params[] = max(0, min(100, $progress)); + } + if ($jiraKey !== null) { + $fields[] = "jira_issue_key = ?"; + $params[] = $jiraKey; + } + + if (!empty($fields)) { + $params[] = $id; + $sql = "UPDATE roadmap_items SET " . implode(", ", $fields) . " WHERE id = ?"; + $stmt = $db->prepare($sql); + $stmt->execute($params); + } + + echo json_encode(['success' => true]); + exit; + } + + // 3. Item verwijderen + if ($action === 'delete_item') { + $data = json_decode(file_get_contents('php://input'), true); + $id = (int)($data['id'] ?? 0); + + $stmt = $db->prepare("DELETE FROM roadmap_items WHERE id = ?"); + $stmt->execute([$id]); + + echo json_encode(['success' => true]); + exit; + } + + echo json_encode(['error' => 'Onbekende actie']); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} + diff --git a/auth/invite.php b/auth/invite.php new file mode 100644 index 0000000..c8d05f2 --- /dev/null +++ b/auth/invite.php @@ -0,0 +1,185 @@ + + + + +
+

Ongeldige Uitnodiging

+

Deze uitnodigingslink is ongeldig, verlopen of al eerder gebruikt.

+ Naar Inloggen +
+ + + "); +} + +$error = null; +if (empty($_SESSION['invite_totp_secret'])) { + $_SESSION['invite_totp_secret'] = TOTP::generateSecret(16); +} +$secret = $_SESSION['invite_totp_secret']; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $name = trim($_POST['name'] ?? ''); + $password = $_POST['password'] ?? ''; + $confirmPassword = $_POST['confirm_password'] ?? ''; + $totpCode = trim($_POST['totp_code'] ?? ''); + + if (empty($name) || empty($password) || empty($totpCode)) { + $error = "Alle velden (inclusief 2FA verificatiecode) zijn verplicht."; + } elseif (strlen($password) < 8) { + $error = "Het wachtwoord moet minimaal 8 tekens lang zijn."; + } elseif ($password !== $confirmPassword) { + $error = "Wachtwoorden komen niet overeen."; + } elseif (!TOTP::verifyCode($secret, $totpCode)) { + $error = "Ongeldige 2FA verificatiecode. Controleer de tijd op uw smartphone."; + } else { + try { + $db = get_db_connection(); + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + $backupCodes = TOTP::generateBackupCodes(8); + + $db->beginTransaction(); + + // 1. Maak gebruiker aan + $stmt = $db->prepare(" + INSERT INTO users (name, email, password_hash, role, two_factor_secret, two_factor_enabled, backup_codes) + VALUES (?, ?, ?, ?, ?, 1, ?) + "); + $stmt->execute([ + $name, + $invite['email'], + $passwordHash, + $invite['role'], + $secret, + json_encode($backupCodes) + ]); + + $newUserId = (int)$db->lastInsertId(); + + // 2. Markeer invite als gebruikt + $stmtInvite = $db->prepare("UPDATE invites SET used_at = NOW() WHERE id = ?"); + $stmtInvite->execute([$invite['id']]); + + $db->commit(); + + // Start sessie + $_SESSION['user_id'] = $newUserId; + $_SESSION['user_name'] = $name; + $_SESSION['user_email'] = $invite['email']; + $_SESSION['user_role'] = $invite['role']; + $_SESSION['is_2fa_verified'] = true; + unset($_SESSION['invite_totp_secret']); + + header('Location: /planner.php'); + exit; + } catch (Exception $e) { + if ($db->inTransaction()) $db->rollBack(); + $error = "Fout bij registreren: " . $e->getMessage(); + } + } +} + +$otpAuthUrl = TOTP::getOtpAuthUrl($invite['email'], $secret, 'Fociz'); +$qrCodeUrl = TOTP::getQrCodeUrl($otpAuthUrl, 180); +$pageTitle = "Fociz — Uitnodiging Accepteren"; +?> + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + +
+
+
+ +
+

Welkom bij Fociz

+

+ Uitgenodigd door voor +

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+

+ Verplichte Tweestapsverificatie (2FA) +

+

+ Scan deze QR-code in uw authenticator app (Google Authenticator, Microsoft Authenticator of 1Password): +

+ +
+ 2FA QR Code +
+ +
+ Sleutel: +
+ +
+ + +
+
+ + +
+
+ + + + + diff --git a/auth/login.php b/auth/login.php new file mode 100644 index 0000000..d0a697a --- /dev/null +++ b/auth/login.php @@ -0,0 +1,212 @@ +prepare("SELECT id, name, email, password_hash, two_factor_secret, two_factor_enabled FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + // Succesvol stap 1: zet tijdelijke pending sessie voor 2FA + $_SESSION['pending_2fa_user_id'] = $user['id']; + $_SESSION['pending_2fa_user_email'] = $user['email']; + $_SESSION['pending_2fa_user_name'] = $user['name']; + $step = 2; + } else { + $error = "Onjuist e-mailadres of wachtwoord."; + } + } + } + // STAP 2: 2FA TOTP Code verificatie + elseif ($step === 2) { + $userId = $_SESSION['pending_2fa_user_id'] ?? null; + $totpCode = trim(str_replace(' ', '', $_POST['totp_code'] ?? '')); + + if (!$userId || empty($totpCode)) { + $error = "Vul de 6-cijferige 2FA verificatiecode in."; + } else { + $stmt = $db->prepare("SELECT id, name, email, role, two_factor_secret, backup_codes FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + $isValid = false; + + // 1. Controleer TOTP code + if (!empty($user['two_factor_secret']) && TOTP::verifyCode($user['two_factor_secret'], $totpCode)) { + $isValid = true; + } + + // 2. Controleer optioneel backup codes + if (!$isValid && !empty($user['backup_codes'])) { + $backupCodes = json_decode($user['backup_codes'], true) ?: []; + $codeUpper = strtoupper($totpCode); + $key = array_search($codeUpper, $backupCodes); + if ($key !== false) { + $isValid = true; + // Verbruik de backup code + unset($backupCodes[$key]); + $stmtUpdate = $db->prepare("UPDATE users SET backup_codes = ? WHERE id = ?"); + $stmtUpdate->execute([json_encode(array_values($backupCodes)), $userId]); + } + } + + if ($isValid) { + // Volledige login succesvol + $_SESSION['user_id'] = $user['id']; + $_SESSION['user_name'] = $user['name']; + $_SESSION['user_email'] = $user['email']; + $_SESSION['user_role'] = $user['role']; + $_SESSION['is_2fa_verified'] = true; + + unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']); + + header('Location: /planner.php'); + exit; + } else { + $error = "Onjuiste 2FA code. Controleer uw authenticator app en probeer opnieuw."; + } + } + } +} + +$pageTitle = "Fociz — Inloggen"; +?> + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + +
+ +
+
+ +
+

Fociz

+

+ Veilig inloggen met tweestapsverificatie (2FA) +

+
+ + +
+ + +
+ + + + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ + + + +
+
+
+ +
+

+ Voer uw 6-cijferige verificatiecode in +

+

+ Gekoppeld aan +

+
+ +
+ + + Tip: U kunt ook een 8-cijferige herstelcode invoeren. + +
+ +
+ + Terug + + +
+
+ + + +
+ Toegang tot Fociz is strikt op uitnodiging. Vraag uw beheerder om een uitnodigingslink. +
+
+ + + + + diff --git a/auth/logout.php b/auth/logout.php new file mode 100644 index 0000000..ab8d885 --- /dev/null +++ b/auth/logout.php @@ -0,0 +1,29 @@ +prepare(" + INSERT INTO users (name, email, password_hash, role, two_factor_secret, two_factor_enabled, backup_codes) + VALUES (?, ?, ?, 'ADMIN', ?, 1, ?) + "); + $stmt->execute([ + $name, + $email, + $passwordHash, + $secret, + json_encode($backupCodes) + ]); + + $adminId = (int)$db->lastInsertId(); + + // Automatische sessie starten + $_SESSION['user_id'] = $adminId; + $_SESSION['user_name'] = $name; + $_SESSION['user_email'] = $email; + $_SESSION['user_role'] = 'ADMIN'; + $_SESSION['is_2fa_verified'] = true; + unset($_SESSION['setup_totp_secret']); + + $success = true; + } catch (Exception $e) { + $error = "Fout bij aanmaken beheerder in MariaDB: " . $e->getMessage(); + } + } +} + +$otpAuthUrl = TOTP::getOtpAuthUrl(!empty($email) ? $email : 'admin@fociz.local', $secret, 'Fociz'); +$qrCodeUrl = TOTP::getQrCodeUrl($otpAuthUrl, 200); +$pageTitle = "Fociz — Eerste Initialisatie"; +?> + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + +
+ +
+
+ +
+

Fociz Initialisatie

+

+ Eenmalige configuratie van het hoofdbeheerdersaccount met verplichte 2FA +

+
+ + +
+ + +
+ + + + +
+
+ +
+
+

Beheerder Geïnstalleerd!

+

+ Uw account is aangemaakt en direct beveiligd met 2FA. Bewaar deze eenmalige herstelcodes veilig: +

+
+ +
+
HERSTELCODES (NOODGEBRUIK):
+
+ + + +
+
+ + + Start met Fociz Dagplanning + + +
+ + + +
+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+

+ + Tweestapsverificatie (2FA) Koppelen +

+

+ Scan onderstaande QR-code met uw authenticator app (Google Authenticator, Microsoft Authenticator of 1Password): +

+ +
+ 2FA QR Code +
+ +
+ Sleutel: +
+ +
+ + +
+
+ + +
+ + +
+ + + + + diff --git a/brainstorm.php b/brainstorm.php new file mode 100644 index 0000000..e0f6a73 --- /dev/null +++ b/brainstorm.php @@ -0,0 +1,171 @@ +prepare(" + INSERT INTO brainstorm_sessions (title, theme, description, created_by_id) + VALUES (?, ?, ?, ?) + "); + $stmt->execute([$title, $theme, $description, $currentUser['id']]); + $newId = $db->lastInsertId(); + header("Location: /brainstorm_canvas.php?id={$newId}"); + exit; + } else { + $error = "Titel van de brainstormsessie is verplicht."; + } +} + +// Haal alle sessies op +$stmtSessions = $db->query(" + SELECT s.*, u.name as creator_name, + (SELECT COUNT(*) FROM brainstorm_cards WHERE session_id = s.id) as cards_count + FROM brainstorm_sessions s + JOIN users u ON s.created_by_id = u.id + ORDER BY s.created_at DESC +"); +$sessions = $stmtSessions->fetchAll(); +?> + +
+ +
+
+
+ + Creatie & Innovatie +
+

+ Brainstorm Sessies +

+

+ Brainstorm met collega's, verzamel ideeën en knelpunten, en transformeer ze direct tot een werkbare roadmap. +

+
+ + +
+ + +
+ +
+ + + +
+ +
+ +

Nog geen brainstormsessies gestart

+

+ Start uw eerste brainstormsessie om ideeën te verzamelen en ze met één klik om te vormen naar planningsitems. +

+ +
+ + + +
+
+ + + + + + kaarten + +
+ +

+ +

+ + +

+ +

+ +
+ +
+ Door +
+ Open canvas + +
+
+
+ + +
+
+ + + + + + diff --git a/brainstorm_canvas.php b/brainstorm_canvas.php new file mode 100644 index 0000000..3f69f91 --- /dev/null +++ b/brainstorm_canvas.php @@ -0,0 +1,371 @@ +prepare(" + SELECT s.*, u.name as creator_name + FROM brainstorm_sessions s + JOIN users u ON s.created_by_id = u.id + WHERE s.id = ? +"); +$stmtSession->execute([$sessionId]); +$session = $stmtSession->fetch(); + +if (!$session) { + echo "
Brainstormsessie niet gevonden. Terug
"; + require_once __DIR__ . '/includes/footer.php'; + exit; +} + +// Haal alle kaarten op van deze sessie +$stmtCards = $db->prepare(" + SELECT * FROM brainstorm_cards + WHERE session_id = ? + ORDER BY votes DESC, created_at ASC +"); +$stmtCards->execute([$sessionId]); +$cards = $stmtCards->fetchAll(); + +$categories = [ + 'Ideeën' => ['title' => 'Ideeën & Concepten', 'icon' => 'lightbulb', 'color' => 'amber', 'border' => 'border-amber-500/40', 'bg' => 'bg-amber-950/20', 'badge' => 'bg-amber-500/20 text-amber-300'], + 'Knelpunten' => ['title' => 'Knelpunten & Uitdagingen', 'icon' => 'alert-triangle', 'color' => 'rose', 'border' => 'border-rose-500/40', 'bg' => 'bg-rose-950/20', 'badge' => 'bg-rose-500/20 text-rose-300'], + 'Oplossingen' => ['title' => 'Oplossingen & Features', 'icon' => 'rocket', 'color' => 'emerald', 'border' => 'border-emerald-500/40', 'bg' => 'bg-emerald-950/20', 'badge' => 'bg-emerald-500/20 text-emerald-300'], + 'Quick Wins' => ['title' => 'Quick Wins (Direct te doen)', 'icon' => 'zap', 'color' => 'blue', 'border' => 'border-blue-500/40', 'bg' => 'bg-blue-950/20', 'badge' => 'bg-blue-500/20 text-blue-300'], +]; +?> + +
+ +
+
+ + + +
+
+ + + + Gestart door +
+

+ +

+
+
+ +
+ + + + +
+
+ + +
+ $catConfig): + $catCards = array_filter($cards, fn($c) => $c['category'] === $catKey); + ?> +
+ +
+
+ +

+
+ + + +
+ + +
+ +
+ Nog geen kaarten in deze categorie. +
+ + +
+ + +
+ + + + + + Op Roadmap + + +
+ + +

+ +

+ + +
+ + +
+ + + + + +
+
+
+ + +
+ + + +
+ +
+
+ + + + + + + + + + + diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..7b989e9 --- /dev/null +++ b/config/config.php @@ -0,0 +1,40 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + // Zorg ervoor dat tabellen bestaan + ensure_tables_exist($pdo); + } catch (PDOException $e) { + // Als de database zelf nog niet bestaat, probeer deze aan te maken indien gebruiker rechten heeft + if ($e->getCode() == 1049) { + try { + $tempDsn = sprintf('mysql:host=%s;port=%s;charset=%s', DB_HOST, DB_PORT, DB_CHARSET); + $tempPdo = new PDO($tempDsn, DB_USER, DB_PASS, $options); + $tempPdo->exec(sprintf('CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci', DB_NAME)); + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + ensure_tables_exist($pdo); + return $pdo; + } catch (PDOException $ex) { + die("Database verbindingsfout: Kan database '" . htmlspecialchars(DB_NAME) . "' niet aanmaken of vinden. Maak deze handmatig aan in MariaDB."); + } + } + die("MariaDB Verbindingsfout: " . htmlspecialchars($e->getMessage()) . "
Controleer uw instellingen in config/config.php of .env"); + } + } + + return $pdo; +} + +/** + * Zorgt ervoor dat alle tabellen uit schema.sql aanwezig zijn in MariaDB + */ +function ensure_tables_exist(PDO $pdo): void { + try { + $result = $pdo->query("SHOW TABLES LIKE 'users'"); + if ($result->rowCount() === 0) { + $schemaFile = __DIR__ . '/../schema.sql'; + if (file_exists($schemaFile)) { + $sql = file_get_contents($schemaFile); + $pdo->exec($sql); + } + } + } catch (PDOException $e) { + // Tabellen bestaan waarschijnlijk al of er is een permissiebeperking + } +} + diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..5e33ef2 --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,136 @@ + 7 * 24 * 60 * 60, // 7 dagen + 'path' => '/', + 'domain' => '', + 'secure' => $isSecure, + 'httponly' => true, + 'samesite' => 'Lax' + ]); + session_start(); + } +} + +/** + * Controleert of er al een beheerder bestaat in MariaDB + */ +function has_admin_user(): bool { + $db = get_db_connection(); + $stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'"); + return ((int)$stmt->fetchColumn()) > 0; +} + +/** + * Haalt de huidige ingelogde gebruiker op uit de database + */ +function get_logged_in_user(): ?array { + start_secure_session(); + + if (empty($_SESSION['user_id']) || empty($_SESSION['is_2fa_verified'])) { + return null; + } + + $db = get_db_connection(); + $stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?"); + $stmt->execute([$_SESSION['user_id']]); + $user = $stmt->fetch(); + + return $user ?: null; +} + +/** + * Beschermt pagina's: dwingt login + 2FA af + */ +function require_auth(): array { + start_secure_session(); + + // 1. Controleer of eerste setup nodig is + if (!has_admin_user()) { + header('Location: /auth/setup.php'); + exit; + } + + // 2. Controleer of gebruiker ingelogd én 2FA geverifieerd is + $user = get_logged_in_user(); + if (!$user) { + header('Location: /auth/login.php'); + exit; + } + + return $user; +} + +/** + * Dwingt beheerder (ADMIN) rol af + */ +function require_admin(): array { + $user = require_auth(); + if ($user['role'] !== 'ADMIN') { + header('Location: /planner.php'); + exit; + } + return $user; +} + +/** + * Maakt een nieuwe uitnodiging aan voor een collega + */ +function create_invite(int $createdById, string $email, string $role = 'MEMBER'): string { + $db = get_db_connection(); + $token = bin2hex(random_bytes(24)); + $expiresAt = date('Y-m-d H:i:s', strtotime('+7 days')); + + $stmt = $db->prepare("INSERT INTO invites (token, email, role, expires_at, created_by_id) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$token, strtolower(trim($email)), $role, $expiresAt, $createdById]); + + return $token; +} + +/** + * Valideert een invite token + */ +function validate_invite(string $token): ?array { + $db = get_db_connection(); + $stmt = $db->prepare(" + SELECT i.*, u.name as inviter_name, u.email as inviter_email + FROM invites i + JOIN users u ON i.created_by_id = u.id + WHERE i.token = ? + "); + $stmt->execute([$token]); + $invite = $stmt->fetch(); + + if (!$invite) return null; + if ($invite['used_at'] !== null) return null; + if (strtotime($invite['expires_at']) < time()) return null; + + return $invite; +} + +/** + * CSRF token helpers + */ +function get_csrf_token(): string { + start_secure_session(); + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + return $_SESSION['csrf_token']; +} + +function verify_csrf_token(?string $token): bool { + start_secure_session(); + return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); +} + diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..d2afcbb --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..1a2e3d7 --- /dev/null +++ b/includes/header.php @@ -0,0 +1,144 @@ + + + + + + + <?= htmlspecialchars($pageTitle ?? 'Fociz — Focus, Dagplanning & Roadmap') ?> + + + + + + + + + + +
+ +
+ + +
+ diff --git a/includes/totp.php b/includes/totp.php new file mode 100644 index 0000000..556e84b --- /dev/null +++ b/includes/totp.php @@ -0,0 +1,132 @@ += 8) { + $bufferBits -= 8; + $binary .= chr(($buffer >> $bufferBits) & 0xFF); + } + } + + return $binary; + } + + /** + * Berekent de 6-cijferige TOTP code voor een bepaald tijdsinterval + */ + public static function getCode(string $secret, ?int $timestamp = null): string { + if ($timestamp === null) { + $timestamp = time(); + } + + $timeCounter = floor($timestamp / 30); + $binaryKey = self::base32Decode($secret); + + // Converteer tijdsinterval naar 8-byte big-endian binaire string + $binaryTime = pack('N*', 0) . pack('N*', $timeCounter); + + $hash = hash_hmac('sha1', $binaryTime, $binaryKey, true); + $offset = ord($hash[strlen($hash) - 1]) & 0x0F; + + $binaryCode = ( + ((ord($hash[$offset]) & 0x7F) << 24) | + ((ord($hash[$offset + 1]) & 0xFF) << 16) | + ((ord($hash[$offset + 2]) & 0xFF) << 8) | + (ord($hash[$offset + 3]) & 0xFF) + ); + + $otp = $binaryCode % 1000000; + return str_pad((string)$otp, 6, '0', STR_PAD_LEFT); + } + + /** + * 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 { + $userInputCode = trim(str_replace(' ', '', $userInputCode)); + if (strlen($userInputCode) !== 6 || !ctype_digit($userInputCode)) { + return false; + } + + $currentTime = time(); + for ($i = -$window; $i <= $window; $i++) { + $calcCode = self::getCode($secret, $currentTime + ($i * 30)); + if (hash_equals($calcCode, $userInputCode)) { + return true; + } + } + + return false; + } + + /** + * Maakt de otpauth:// URL aan voor authenticator apps + */ + public static function getOtpAuthUrl(string $email, string $secret, string $issuer = 'Fociz'): string { + return sprintf( + 'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30', + rawurlencode($issuer), + rawurlencode($email), + $secret, + rawurlencode($issuer) + ); + } + + /** + * Genereert een QR code afbeelding URL (via QuickChart HTTPS) + */ + public static function getQrCodeUrl(string $otpAuthUrl, int $size = 220): string { + return sprintf( + 'https://quickchart.io/qr?text=%s&size=%d&margin=1&ecLevel=M', + urlencode($otpAuthUrl), + $size + ); + } + + /** + * Genereert een lijst met eenmalige herstelcodes + */ + public static function generateBackupCodes(int $count = 8): array { + $codes = []; + for ($i = 0; $i < $count; $i++) { + $bytes = bin2hex(random_bytes(4)); + $codes[] = strtoupper(substr($bytes, 0, 4) . '-' . substr($bytes, 4, 4)); + } + return $codes; + } +} + diff --git a/index.php b/index.php new file mode 100644 index 0000000..424938f --- /dev/null +++ b/index.php @@ -0,0 +1,21 @@ + $_POST['client_id'] ?? '', + 'client_secret' => $_POST['client_secret'] ?? '', + 'tenant_id' => $_POST['tenant_id'] ?? '', + 'jira_domain' => $_POST['jira_domain'] ?? '', + 'jira_email' => $_POST['jira_email'] ?? '', + 'jira_api_token' => $_POST['jira_api_token'] ?? '', + ]); + + if (in_array($provider, ['OUTLOOK', 'TEAMS', 'JIRA'])) { + $stmt = $db->prepare(" + INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at) + VALUES (?, ?, 1, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE is_demo_mode = VALUES(is_demo_mode), settings = VALUES(settings), last_sync_at = NOW() + "); + $stmt->execute([$userId, $provider, $isDemo, $settings]); + $message = "Instellingen voor {$provider} succesvol opgeslagen."; + } +} + +// Haal huidige configuraties op +$stmtConfigs = $db->prepare("SELECT * FROM integrations WHERE user_id = ?"); +$stmtConfigs->execute([$userId]); +$configsRaw = $stmtConfigs->fetchAll(); +$configs = []; +foreach ($configsRaw as $c) { + $configs[$c['provider']] = $c; + $configs[$c['provider']]['settings_parsed'] = json_decode($c['settings'] ?? '{}', true) ?: []; +} + +// Jira demo issues +$jiraIssues = [ + [ + 'key' => 'FOC-101', + 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', + 'status' => 'In Progress', + 'priority' => 'High', + 'epic' => 'Security & Access', + ], + [ + 'key' => 'FOC-102', + 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', + 'status' => 'To Do', + 'priority' => 'High', + 'epic' => 'Brainstorming Module', + ], + [ + 'key' => 'FOC-103', + 'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API', + 'status' => 'In Review', + 'priority' => 'Medium', + 'epic' => 'Integrations Hub', + ], + [ + 'key' => 'FOC-104', + 'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren', + 'status' => 'To Do', + 'priority' => 'Medium', + 'epic' => 'Roadmap Module', + ] +]; +?> + +
+ +
+
+ + Connectiviteit & Synchronisatie +
+

+ Integraties Hub +

+

+ Beheer uw koppelingen met Microsoft Outlook (agenda & taken), Microsoft Teams (meetings) en Atlassian Jira (issues & roadmap). +

+
+ + +
+ + +
+ + + +
+ + +
+
+
+
+ +
+ + + Actief + +
+ +

Microsoft Outlook

+

+ Synchroniseert uw agenda-afspraken en e-mails automatisch naar uw dagplanning en time-blocks. +

+ +
+
+ Status: + Gekoppeld +
+
+ Sync frequentie: + Elke 15 min +
+
+
+ + +
+ + + + + +
+
+ + +
+
+
+
+ +
+ + + Actief + +
+ +

Microsoft Teams

+

+ Toont vandaag geplande Teams meetings met directe 'Deelnemen' links en kanaalupdates. +

+ +
+
+ Direct Deelnemen: + Ondersteund +
+
+ Meldingen: + Ingeschakeld +
+
+
+ +
+ + + + + +
+
+ + +
+
+
+
+ +
+ + + Actief + +
+ +

Atlassian Jira

+

+ Koppelt Jira Epics en Issues direct aan uw roadmap items en takenlijst. +

+ +
+
+ Two-way Status Sync: + Ondersteund +
+
+ Roadmap Linkage: + Actief +
+
+
+ +
+ + +
+ + +
+ + + + +
+
+
+ + +
+
+
+ +

+ Jira Issues Toegewezen aan U +

+
+ issues +
+ +
+ +
+
+
+ + + + + + +
+

+ +

+ Epic: +
+ +
+ Priority + +
+
+ +
+
+
+ + + + + diff --git a/planner.php b/planner.php new file mode 100644 index 0000000..f1d05af --- /dev/null +++ b/planner.php @@ -0,0 +1,494 @@ +prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?"); +$stmtPlan->execute([$userId, $today]); +$dailyPlan = $stmtPlan->fetch(); + +if (!$dailyPlan) { + $stmtCreatePlan = $db->prepare(" + INSERT INTO daily_plans (user_id, plan_date, focus_goal) + VALUES (?, ?, 'Mijn belangrijkste doel van vandaag behalen') + "); + $stmtCreatePlan->execute([$userId, $today]); + $planId = (int)$db->lastInsertId(); + $dailyPlan = ['id' => $planId, 'plan_date' => $today, 'focus_goal' => 'Mijn belangrijkste doel van vandaag behalen', 'notes' => '']; +} + +// Haal taken op voor vandaag +$stmtTasks = $db->prepare(" + SELECT * FROM tasks + WHERE user_id = ? AND (daily_plan_id = ? OR daily_plan_id IS NULL) + ORDER BY is_completed ASC, priority ASC, time_block_start ASC, created_at DESC +"); +$stmtTasks->execute([$userId, $dailyPlan['id']]); +$tasks = $stmtTasks->fetchAll(); + +// Outlook demo / gesynchroniseerde events +$outlookEvents = [ + [ + 'id' => 'evt-1', + 'subject' => 'Daily Team Standup & Focus Sync', + 'start' => '09:00', + 'end' => '09:30', + 'location' => 'Microsoft Teams', + 'meeting_url' => 'https://teams.microsoft.com', + ], + [ + 'id' => 'evt-2', + 'subject' => 'Sprint & Roadmap Review met Stakeholders', + 'start' => '11:00', + 'end' => '12:00', + 'location' => 'Vergaderruimte A & Teams', + 'meeting_url' => 'https://teams.microsoft.com', + ], + [ + 'id' => 'evt-3', + 'subject' => 'Architectuur & MariaDB Database Evaluatie', + 'start' => '14:30', + 'end' => '15:30', + 'location' => 'Online Teams Call', + 'meeting_url' => 'https://teams.microsoft.com', + ] +]; + +$hours = ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00"]; + +// Nederlandse datumformattering +$dutchDays = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag']; +$dutchMonths = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december']; +$formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(int)date('n')] . ' ' . date('Y'); +?> + +
+ +
+
+
+
+ + +
+

+ Dagplanning & Structuur +

+
+ + +
+ + +
+ +
+ + +
+
+
+ + +
+ + +
+ + +
+
+
+
+ +
+
+

Focus Modus

+

Pomodoro & diepe concentratie

+
+
+ +
+ + + +
+
+ +
+
25:00
+
+ Actieve focus: + Kies een taak hieronder +
+ +
+
+
+
+ +
+ + +
+
+ + +
+
+
+

+ Dagtaken & Prioriteiten + () +

+
+ + +
+ + + + + +
+ +
+ Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' om te beginnen. +
+ + +
+
+ + +
+ + + + + + + + + +
+
+ +
+ + + + + + + + + +
+
+ + +
+
+ + +
+
+ +

Dagnotities & Kladblok

+
+ +
+
+ + +
+
+
+
+ +

Tijdlijn & Time-blocking

+
+ 08:00 – 18:00 +
+ +
+ +
+
+ +
+ +
+ + +
+
+ + - + + +
+ + + Deelnemen + +
+ + + + +
+
+ + + + +
+ + + +
+ + + + + +
+
+ +
+
+
+
+
+ + + + + diff --git a/roadmap.php b/roadmap.php new file mode 100644 index 0000000..c109df3 --- /dev/null +++ b/roadmap.php @@ -0,0 +1,329 @@ +prepare($sql); +$stmt->execute($params); +$roadmapItems = $stmt->fetchAll(); + +$quarters = ['Q1', 'Q2', 'Q3', 'Q4']; +?> + +
+ +
+
+
+ + Planning & Strategie +
+

+ Roadmap & Tijdlijn +

+

+ Centraal overzicht van alle mijlpalen, features en doelen, direct voortgekomen uit brainstorms en gesynchroniseerd met Jira. +

+
+ + +
+ + +
+ + + + +
+ Status: + +
+
+ + +
+ +
+ +

Geen items gevonden op de roadmap

+

+ Ga naar een Brainstorm sessie en zet ideeën met één klik om naar de roadmap, of voeg handmatig een item toe. +

+
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +
+
+ + +
+

+ +

+ +

+ +

+ +
+ + + +
+ + Ontstaan uit brainstormsessie +
+ + + +
+ +
+ Voortgang: +
+
+
+ % +
+ + + +
+
+ + +
+ + + + Deadline: + + + Toegevoegd door +
+
+
+ + +
+
+ + + + + + + + diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..c6a3aaa --- /dev/null +++ b/schema.sql @@ -0,0 +1,128 @@ +-- Fociz MariaDB Databaseschema +-- UTF-8 / utf8mb4 ondersteuning voor emoticons, sticky notes en internationale tekens + +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(120) NOT NULL, + `email` VARCHAR(191) NOT NULL UNIQUE, + `password_hash` VARCHAR(255) NOT NULL, + `role` ENUM('ADMIN', 'MEMBER') NOT NULL DEFAULT 'MEMBER', + `two_factor_secret` VARCHAR(120) NULL, + `two_factor_enabled` TINYINT(1) NOT NULL DEFAULT 0, + `backup_codes` TEXT NULL, + `avatar_url` VARCHAR(255) NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `invites` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `token` VARCHAR(64) NOT NULL UNIQUE, + `email` VARCHAR(191) NOT NULL, + `role` ENUM('ADMIN', 'MEMBER') NOT NULL DEFAULT 'MEMBER', + `expires_at` DATETIME NOT NULL, + `used_at` DATETIME NULL, + `created_by_id` INT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX (`token`), + CONSTRAINT `fk_invites_user` FOREIGN KEY (`created_by_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `daily_plans` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `plan_date` DATE NOT NULL, + `focus_goal` TEXT NULL, + `secondary_goals` TEXT NULL, + `notes` TEXT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uk_user_date` (`user_id`, `plan_date`), + CONSTRAINT `fk_dailyplans_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `tasks` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `daily_plan_id` INT NULL, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NULL, + `status` ENUM('TODO', 'IN_PROGRESS', 'DONE', 'CANCELLED') NOT NULL DEFAULT 'TODO', + `priority` ENUM('P1', 'P2', 'P3') NOT NULL DEFAULT 'P2', + `time_block_start` VARCHAR(10) NULL, + `time_block_end` VARCHAR(10) NULL, + `duration_minutes` INT NULL DEFAULT 30, + `is_completed` TINYINT(1) NOT NULL DEFAULT 0, + `source` VARCHAR(50) NOT NULL DEFAULT 'FOCIZ', + `external_id` VARCHAR(100) NULL, + `external_url` TEXT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX (`user_id`), + CONSTRAINT `fk_tasks_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_tasks_plan` FOREIGN KEY (`daily_plan_id`) REFERENCES `daily_plans` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `brainstorm_sessions` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NULL, + `theme` VARCHAR(100) NOT NULL DEFAULT 'Algemeen', + `created_by_id` INT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX (`created_by_id`), + CONSTRAINT `fk_brainstorm_user` FOREIGN KEY (`created_by_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `brainstorm_cards` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `session_id` INT NOT NULL, + `content` TEXT NOT NULL, + `category` VARCHAR(100) NOT NULL DEFAULT 'Ideeën', + `color` VARCHAR(20) NOT NULL DEFAULT 'amber', + `votes` INT NOT NULL DEFAULT 0, + `author_name` VARCHAR(120) NULL, + `converted_to_roadmap_item_id` INT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX (`session_id`), + CONSTRAINT `fk_cards_session` FOREIGN KEY (`session_id`) REFERENCES `brainstorm_sessions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `roadmap_items` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NULL, + `type` VARCHAR(50) NOT NULL DEFAULT 'FEATURE', + `status` VARCHAR(50) NOT NULL DEFAULT 'PLANNED', + `quarter` VARCHAR(20) NOT NULL DEFAULT 'Q3', + `priority` VARCHAR(20) NOT NULL DEFAULT 'MEDIUM', + `progress` INT NOT NULL DEFAULT 0, + `start_date` DATE NULL, + `due_date` DATE NULL, + `jira_issue_key` VARCHAR(50) NULL, + `origin_brainstorm_card_id` INT NULL, + `created_by_id` INT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX (`created_by_id`), + CONSTRAINT `fk_roadmap_user` FOREIGN KEY (`created_by_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `integrations` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `provider` ENUM('OUTLOOK', 'TEAMS', 'JIRA') NOT NULL, + `is_enabled` TINYINT(1) NOT NULL DEFAULT 1, + `is_demo_mode` TINYINT(1) NOT NULL DEFAULT 1, + `access_token` TEXT NULL, + `refresh_token` TEXT NULL, + `settings` TEXT NULL, + `last_sync_at` DATETIME NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uk_user_provider` (`user_id`, `provider`), + CONSTRAINT `fk_integrations_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + diff --git a/users.php b/users.php new file mode 100644 index 0000000..15f3b7d --- /dev/null +++ b/users.php @@ -0,0 +1,266 @@ +prepare("SELECT id FROM users WHERE email = ?"); + $stmtCheck->execute([$email]); + if ($stmtCheck->fetch()) { + $error = "Er bestaat al een gebruiker met dit e-mailadres."; + } else { + $token = create_invite($adminUser['id'], $email, $role); + $baseUrl = rtrim(APP_URL, '/'); + $generatedLink = "{$baseUrl}/auth/invite.php?token={$token}"; + $message = "Uitnodigingslink succesvol aangemaakt!"; + } + } +} + +// Uitnodiging intrekken +if (isset($_GET['delete_invite'])) { + $inviteId = (int)$_GET['delete_invite']; + $stmtDel = $db->prepare("DELETE FROM invites WHERE id = ?"); + $stmtDel->execute([$inviteId]); + header('Location: /users.php'); + exit; +} + +// Gebruiker verwijderen +if (isset($_GET['delete_user'])) { + $delUserId = (int)$_GET['delete_user']; + if ($delUserId !== (int)$adminUser['id']) { + $stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?"); + $stmtDelUser->execute([$delUserId]); + } + header('Location: /users.php'); + exit; +} + +// Haal alle gebruikers op +$users = $db->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll(); + +// Haal alle uitnodigingen op +$invites = $db->query(" + SELECT i.*, u.name as inviter_name + FROM invites i + JOIN users u ON i.created_by_id = u.id + ORDER BY i.created_at DESC +")->fetchAll(); +?> + +
+ +
+
+ + Beheerderspaneel +
+

+ Team & Uitnodigingen +

+

+ Fociz is strikt afgesloten: nieuwe collega's kunnen enkel toegang krijgen na het ontvangen van een uitnodigingslink en verplichte 2FA-activatie. +

+
+ + +
+ + +
+ + + +
+
+ + +
+

+ Kopieer onderstaande beveiligde registratielink en stuur deze naar de collega: +

+
+ + +
+
+ + + +
+

+ + Nieuwe Collega Uitnodigen +

+ +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+
+

+ + Geregistreerde Teamleden () +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
Naam & E-mailRol2FA BeveiligingLid sindsActies
+
+
+
+ + + + + + + 2FA Actief + + + + + + + + + + Uzelf + +
+
+
+ + +
+
+

+ + Uitnodigingen Status () +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Uitgenodigd E-mailadresRolStatusVerloopt opActie
Nog geen uitnodigingen verstuurd.
+ + Geaccepteerd + + Verlopen + + In afwachting + + + + + +
+
+
+
+ + + + +