Merge pull request 'Dev' (#25) from dev into main
Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
@@ -8,3 +8,5 @@ Thumbs.db
|
||||
*.log
|
||||
.vscode/
|
||||
.idea/
|
||||
.continue/
|
||||
.continueignore
|
||||
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Kanban / Trello Bord API
|
||||
* Beheert borden, kolommen/lanes, kaarten, toewijzingen, tijdspaden & Outlook 365 integratie
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$currentUser = require_auth(true);
|
||||
$db = get_db_connection();
|
||||
ensure_tables_exist($db);
|
||||
|
||||
$userId = (int)($currentUser['id'] ?? 0);
|
||||
$userRole = $currentUser['role'] ?? 'MEMBER';
|
||||
$isAdmin = ($userRole === 'ADMIN');
|
||||
|
||||
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
|
||||
/**
|
||||
* Controleert of de gebruiker toegang heeft tot het bord
|
||||
*/
|
||||
function check_board_access(PDO $db, int $boardId, int $userId, bool $isAdmin): bool {
|
||||
if ($isAdmin) return true;
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM board_members WHERE board_id = ? AND user_id = ?");
|
||||
$stmt->execute([$boardId, $userId]);
|
||||
return ((int)$stmt->fetchColumn()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controleert of de gebruiker beheerder is van het bord
|
||||
*/
|
||||
function check_board_admin(PDO $db, int $boardId, int $userId, bool $isAdmin): bool {
|
||||
if ($isAdmin) return true;
|
||||
$stmt = $db->prepare("SELECT role FROM board_members WHERE board_id = ? AND user_id = ?");
|
||||
$stmt->execute([$boardId, $userId]);
|
||||
$role = $stmt->fetchColumn();
|
||||
return ($role === 'ADMIN');
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Alle beschikbare borden ophalen voor deze gebruiker
|
||||
if ($action === 'get_boards') {
|
||||
if ($isAdmin) {
|
||||
$stmt = $db->query("
|
||||
SELECT b.*, u.name as creator_name,
|
||||
(SELECT COUNT(*) FROM board_cards WHERE board_id = b.id) as card_count
|
||||
FROM boards b
|
||||
LEFT JOIN users u ON b.created_by_id = u.id
|
||||
WHERE b.is_archived = 0
|
||||
ORDER BY b.id ASC
|
||||
");
|
||||
$boards = $stmt->fetchAll();
|
||||
} else {
|
||||
$stmt = $db->prepare("
|
||||
SELECT b.*, u.name as creator_name,
|
||||
(SELECT COUNT(*) FROM board_cards WHERE board_id = b.id) as card_count
|
||||
FROM boards b
|
||||
JOIN board_members bm ON b.id = bm.board_id
|
||||
LEFT JOIN users u ON b.created_by_id = u.id
|
||||
WHERE bm.user_id = ? AND b.is_archived = 0
|
||||
ORDER BY b.id ASC
|
||||
");
|
||||
$stmt->execute([$userId]);
|
||||
$boards = $stmt->fetchAll();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'boards' => $boards,
|
||||
'is_admin' => $isAdmin
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Volledige bord-data ophalen (lanes, kaarten, toewijzingen, autorisaties)
|
||||
if ($action === 'get_board_data') {
|
||||
$boardId = (int)($input['board_id'] ?? ($_GET['board_id'] ?? 0));
|
||||
|
||||
// Als geen board_id is meegegeven, pak het eerste beschikbare bord
|
||||
if ($boardId <= 0) {
|
||||
if ($isAdmin) {
|
||||
$boardId = (int)$db->query("SELECT id FROM boards WHERE is_archived = 0 ORDER BY id ASC LIMIT 1")->fetchColumn();
|
||||
} else {
|
||||
$stmtFirst = $db->prepare("SELECT b.id FROM boards b JOIN board_members bm ON b.id = bm.board_id WHERE bm.user_id = ? AND b.is_archived = 0 ORDER BY b.id ASC LIMIT 1");
|
||||
$stmtFirst->execute([$userId]);
|
||||
$boardId = (int)$stmtFirst->fetchColumn();
|
||||
}
|
||||
}
|
||||
|
||||
if ($boardId <= 0) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'has_board' => false,
|
||||
'message' => 'Geen actieve borden gevonden.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!check_board_access($db, $boardId, $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Geen toegang tot dit bord. Vraag de beheerder om autorisatie.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Haal bord-informatie op
|
||||
$stmtB = $db->prepare("SELECT b.*, u.name as creator_name FROM boards b LEFT JOIN users u ON b.created_by_id = u.id WHERE b.id = ?");
|
||||
$stmtB->execute([$boardId]);
|
||||
$board = $stmtB->fetch();
|
||||
|
||||
// Haal lanes op
|
||||
$stmtL = $db->prepare("SELECT * FROM board_lanes WHERE board_id = ? ORDER BY position ASC, id ASC");
|
||||
$stmtL->execute([$boardId]);
|
||||
$lanes = $stmtL->fetchAll();
|
||||
|
||||
// Haal kaarten op met assignee en schedule
|
||||
$stmtC = $db->prepare("
|
||||
SELECT c.*,
|
||||
u.name as assignee_name, u.email as assignee_email, u.avatar_url as assignee_avatar,
|
||||
s.id as schedule_id, s.schedule_type, s.start_date, s.end_date, s.start_time, s.end_time, s.days_of_week, s.outlook_event_id, s.outlook_synced_at
|
||||
FROM board_cards c
|
||||
LEFT JOIN users u ON c.assignee_id = u.id
|
||||
LEFT JOIN board_card_schedules s ON c.id = s.card_id
|
||||
WHERE c.board_id = ?
|
||||
ORDER BY c.position ASC, c.id DESC
|
||||
");
|
||||
$stmtC->execute([$boardId]);
|
||||
$cards = $stmtC->fetchAll();
|
||||
|
||||
// Groepeer kaarten per lane
|
||||
$cardsByLane = [];
|
||||
foreach ($lanes as $lane) {
|
||||
$cardsByLane[$lane['id']] = [];
|
||||
}
|
||||
foreach ($cards as $card) {
|
||||
$lId = $card['lane_id'];
|
||||
if (!isset($cardsByLane[$lId])) {
|
||||
$cardsByLane[$lId] = [];
|
||||
}
|
||||
$cardsByLane[$lId][] = $card;
|
||||
}
|
||||
|
||||
// Haal teamleden en Outlook integratie status op (voor toewijzing)
|
||||
$usersStmt = $db->query("SELECT id, name, email, role, avatar_url FROM users ORDER BY name ASC");
|
||||
$teamMembers = $usersStmt->fetchAll();
|
||||
|
||||
// Check welke collega's een actieve Outlook koppeling hebben
|
||||
$outlookActiveUsers = [];
|
||||
$intStmt = $db->query("SELECT user_id FROM integrations WHERE provider = 'OUTLOOK' AND is_enabled = 1 AND access_token IS NOT NULL");
|
||||
foreach ($intStmt->fetchAll() as $row) {
|
||||
$outlookActiveUsers[(int)$row['user_id']] = true;
|
||||
}
|
||||
foreach ($teamMembers as &$tm) {
|
||||
$tm['has_outlook'] = !empty($outlookActiveUsers[(int)$tm['id']]);
|
||||
}
|
||||
unset($tm);
|
||||
|
||||
// Haal geautoriseerde leden van dit bord op
|
||||
$membersStmt = $db->prepare("SELECT user_id, role FROM board_members WHERE board_id = ?");
|
||||
$membersStmt->execute([$boardId]);
|
||||
$authorizedMembers = [];
|
||||
foreach ($membersStmt->fetchAll() as $m) {
|
||||
$authorizedMembers[(int)$m['user_id']] = $m['role'];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'has_board' => true,
|
||||
'board' => $board,
|
||||
'lanes' => $lanes,
|
||||
'cards_by_lane' => $cardsByLane,
|
||||
'team_members' => $teamMembers,
|
||||
'authorized_members' => $authorizedMembers,
|
||||
'is_board_admin' => check_board_admin($db, $boardId, $userId, $isAdmin),
|
||||
'current_user_id' => $userId,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Nieuw bord aanmaken (Beheerder)
|
||||
if ($action === 'create_board') {
|
||||
if (!$isAdmin) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Alleen beheerders kunnen nieuwe borden aanmaken.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = trim($input['title'] ?? '');
|
||||
$description = trim($input['description'] ?? '');
|
||||
|
||||
if (empty($title)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Vul een bordtitel in.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO boards (title, description, created_by_id) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $userId]);
|
||||
$boardId = (int)$db->lastInsertId();
|
||||
|
||||
// Voeg maker toe als beheerder van het bord
|
||||
$db->prepare("INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, 'ADMIN')")
|
||||
->execute([$boardId, $userId]);
|
||||
|
||||
// Standaard lanes aanmaken
|
||||
$defaultLanes = [
|
||||
['title' => 'Open', 'color' => 'slate', 'pos' => 0],
|
||||
['title' => 'In uitvoering', 'color' => 'amber', 'pos' => 1],
|
||||
['title' => 'Testen', 'color' => 'purple', 'pos' => 2],
|
||||
['title' => 'Klaar', 'color' => 'emerald', 'pos' => 3],
|
||||
];
|
||||
$laneStmt = $db->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)");
|
||||
foreach ($defaultLanes as $l) {
|
||||
$laneStmt->execute([$boardId, $l['title'], $l['color'], $l['pos']]);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'board_id' => $boardId,
|
||||
'message' => 'Bord succesvol aangemaakt!'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Bord autorisaties / leden beheren (Beheerder)
|
||||
if ($action === 'manage_members') {
|
||||
$boardId = (int)($input['board_id'] ?? 0);
|
||||
if (!check_board_admin($db, $boardId, $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Geen beheerrechten voor dit bord.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$memberIds = $input['member_ids'] ?? [];
|
||||
if (!is_array($memberIds)) $memberIds = [];
|
||||
|
||||
// Huidige leden bijwerken
|
||||
$db->prepare("DELETE FROM board_members WHERE board_id = ?")->execute([$boardId]);
|
||||
$insertStmt = $db->prepare("INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, 'MEMBER')");
|
||||
foreach ($memberIds as $mId) {
|
||||
$mId = (int)$mId;
|
||||
if ($mId > 0) {
|
||||
$insertStmt->execute([$boardId, $mId]);
|
||||
}
|
||||
}
|
||||
// Zorg dat beheerder altijd lid blijft
|
||||
$db->prepare("INSERT IGNORE INTO board_members (board_id, user_id, role) VALUES (?, ?, 'ADMIN')")->execute([$boardId, $userId]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Toegangsrechten voor het bord succesvol bijgewerkt.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5. Kolom / Lane toevoegen of bewerken
|
||||
if ($action === 'save_lane') {
|
||||
$boardId = (int)($input['board_id'] ?? 0);
|
||||
$laneId = (int)($input['lane_id'] ?? 0);
|
||||
$title = trim($input['title'] ?? '');
|
||||
$color = trim($input['color'] ?? 'slate');
|
||||
|
||||
if (!check_board_admin($db, $boardId, $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Alleen bordbeheerders kunnen kolommen beheren.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($title)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Vul een kolomnaam in.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($laneId > 0) {
|
||||
$stmt = $db->prepare("UPDATE board_lanes SET title = ?, color = ? WHERE id = ? AND board_id = ?");
|
||||
$stmt->execute([$title, $color, $laneId, $boardId]);
|
||||
} else {
|
||||
$maxPos = (int)$db->query("SELECT COALESCE(MAX(position), -1) FROM board_lanes WHERE board_id = {$boardId}")->fetchColumn();
|
||||
$stmt = $db->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$boardId, $title, $color, $maxPos + 1]);
|
||||
$laneId = (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'lane_id' => $laneId, 'message' => 'Kolom opgeslagen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 6. Kolom / Lane verwijderen
|
||||
if ($action === 'delete_lane') {
|
||||
$boardId = (int)($input['board_id'] ?? 0);
|
||||
$laneId = (int)($input['lane_id'] ?? 0);
|
||||
|
||||
if (!check_board_admin($db, $boardId, $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Geen beheerrechten.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Controleer of er kaarten in staan
|
||||
$cardCount = (int)$db->query("SELECT COUNT(*) FROM board_cards WHERE lane_id = {$laneId}")->fetchColumn();
|
||||
if ($cardCount > 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'Verplaats of verwijder eerst de taken in deze kolom alvorens de kolom te verwijderen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db->prepare("DELETE FROM board_lanes WHERE id = ? AND board_id = ?")->execute([$laneId, $boardId]);
|
||||
echo json_encode(['success' => true, 'message' => 'Kolom verwijderd.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 7. Kaart (Post-it / Opdracht) aanmaken of bewerken
|
||||
if ($action === 'save_card') {
|
||||
$boardId = (int)($input['board_id'] ?? 0);
|
||||
$cardId = (int)($input['card_id'] ?? 0);
|
||||
$laneId = (int)($input['lane_id'] ?? 0);
|
||||
$title = trim($input['title'] ?? '');
|
||||
$description = trim($input['description'] ?? '');
|
||||
$assigneeId = !empty($input['assignee_id']) ? (int)$input['assignee_id'] : null;
|
||||
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
|
||||
$color = in_array($input['color'] ?? '', ['yellow', 'blue', 'green', 'purple', 'pink', 'orange', 'slate']) ? $input['color'] : 'yellow';
|
||||
|
||||
if (!check_board_access($db, $boardId, $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Geen toegang tot dit bord.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($title)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Vul een titel in voor de taak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($cardId > 0) {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE board_cards
|
||||
SET title = ?, description = ?, assignee_id = ?, priority = ?, color = ?, lane_id = ?
|
||||
WHERE id = ? AND board_id = ?
|
||||
");
|
||||
$stmt->execute([$title, $description, $assigneeId, $priority, $color, $laneId, $cardId, $boardId]);
|
||||
} else {
|
||||
$maxPos = (int)$db->query("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = {$laneId}")->fetchColumn();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$boardId, $laneId, $title, $description, $assigneeId, $priority, $color, $maxPos + 1, $userId]);
|
||||
$cardId = (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
// Tijdspad opslaan en optioneel Outlook agenda synchroniseren indien meegegeven
|
||||
$scheduleData = $input['schedule'] ?? null;
|
||||
$outlookSyncMsg = null;
|
||||
if (!empty($scheduleData) && is_array($scheduleData) && !empty($scheduleData['enabled'])) {
|
||||
$scheduleType = in_array($scheduleData['type'] ?? '', ['SINGLE', 'DAILY_RANGE']) ? $scheduleData['type'] : 'SINGLE';
|
||||
$startDate = trim($scheduleData['start_date'] ?? date('Y-m-d'));
|
||||
$endDate = ($scheduleType === 'DAILY_RANGE' && !empty($scheduleData['end_date'])) ? trim($scheduleData['end_date']) : $startDate;
|
||||
$startTime = trim($scheduleData['start_time'] ?? '14:00');
|
||||
$endTime = trim($scheduleData['end_time'] ?? '15:00');
|
||||
$daysOfWeek = trim($scheduleData['days_of_week'] ?? '1,2,3,4,5');
|
||||
$syncOutlook = !empty($scheduleData['sync_outlook']);
|
||||
|
||||
$outlookEventId = null;
|
||||
$outlookSyncedAt = null;
|
||||
|
||||
// Als Outlook sync gevraagd is én er een assignee is:
|
||||
if ($syncOutlook && $assigneeId > 0) {
|
||||
// Controleer of de assignee Outlook gekoppeld heeft
|
||||
$stmtCheckInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||
$stmtCheckInt->execute([$assigneeId]);
|
||||
$outlookInt = $stmtCheckInt->fetch();
|
||||
|
||||
if (!empty($outlookInt['access_token'])) {
|
||||
$startTs = strtotime("{$startDate} {$startTime}");
|
||||
$endTs = strtotime("{$startDate} {$endTime}");
|
||||
$durationMins = max(15, (int)(($endTs - $startTs) / 60));
|
||||
|
||||
// Maak agenda-afspraak aan in Outlook
|
||||
$eventPayload = [
|
||||
'subject' => "[Fociz Bord] {$title}",
|
||||
'description' => "Opdracht toegewezen via Fociz Bord:\n\n{$description}\n\nTijdspad: {$startTime} - {$endTime}",
|
||||
'date' => $startDate,
|
||||
'start_time' => $startTime,
|
||||
'end_time' => $endTime,
|
||||
'duration_minutes' => $durationMins,
|
||||
];
|
||||
|
||||
$resCreate = microsoft_create_event($assigneeId, $eventPayload);
|
||||
if (!empty($resCreate['success'])) {
|
||||
$outlookEventId = $resCreate['id'] ?? null;
|
||||
$outlookSyncedAt = date('Y-m-d H:i:s');
|
||||
$outlookSyncMsg = 'Taak succesvol ingepland in de Outlook agenda van de collega!';
|
||||
} else {
|
||||
$outlookSyncMsg = 'Taak opgeslagen, maar Outlook-sync mislukt: ' . ($resCreate['error'] ?? 'onbekende fout');
|
||||
}
|
||||
} else {
|
||||
$outlookSyncMsg = 'Let op: De toegewezen collega heeft Microsoft Outlook 365 nog niet gekoppeld.';
|
||||
}
|
||||
}
|
||||
|
||||
// Opslaan in board_card_schedules
|
||||
$stmtSched = $db->prepare("
|
||||
INSERT INTO board_card_schedules (card_id, schedule_type, start_date, end_date, start_time, end_time, days_of_week, outlook_event_id, outlook_synced_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
schedule_type = VALUES(schedule_type),
|
||||
start_date = VALUES(start_date),
|
||||
end_date = VALUES(end_date),
|
||||
start_time = VALUES(start_time),
|
||||
end_time = VALUES(end_time),
|
||||
days_of_week = VALUES(days_of_week),
|
||||
outlook_event_id = COALESCE(VALUES(outlook_event_id), outlook_event_id),
|
||||
outlook_synced_at = COALESCE(VALUES(outlook_synced_at), outlook_synced_at)
|
||||
");
|
||||
$stmtSched->execute([$cardId, $scheduleType, $startDate, $endDate, $startTime, $endTime, $daysOfWeek, $outlookEventId, $outlookSyncedAt]);
|
||||
} elseif (isset($scheduleData['enabled']) && !$scheduleData['enabled']) {
|
||||
// Tijdspad uitschakelen indien expliciet uitgezet
|
||||
$db->prepare("DELETE FROM board_card_schedules WHERE card_id = ?")->execute([$cardId]);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'card_id' => $cardId,
|
||||
'outlook_message' => $outlookSyncMsg,
|
||||
'message' => 'Taak succesvol opgeslagen!'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 8. Kaart verplaatsen naar een andere lane of positie (Drag & Drop / Quick move)
|
||||
if ($action === 'move_card') {
|
||||
$cardId = (int)($input['card_id'] ?? 0);
|
||||
$targetLaneId = (int)($input['target_lane_id'] ?? 0);
|
||||
$targetPosition = (int)($input['target_position'] ?? 0);
|
||||
|
||||
$card = $db->query("SELECT * FROM board_cards WHERE id = {$cardId}")->fetch();
|
||||
if (!$card) {
|
||||
echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!check_board_access($db, (int)$card['board_id'], $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Geen toegang.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE board_cards SET lane_id = ?, position = ? WHERE id = ?");
|
||||
$stmt->execute([$targetLaneId, $targetPosition, $cardId]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 9. Kaart verwijderen
|
||||
if ($action === 'delete_card') {
|
||||
$cardId = (int)($input['card_id'] ?? 0);
|
||||
|
||||
$stmtCard = $db->prepare("
|
||||
SELECT c.*, s.outlook_event_id
|
||||
FROM board_cards c
|
||||
LEFT JOIN board_card_schedules s ON c.id = s.card_id
|
||||
WHERE c.id = ?
|
||||
");
|
||||
$stmtCard->execute([$cardId]);
|
||||
$card = $stmtCard->fetch();
|
||||
|
||||
if (!$card) {
|
||||
echo json_encode(['success' => false, 'error' => 'Taak niet gevonden.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!check_board_access($db, (int)$card['board_id'], $userId, $isAdmin)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Geen rechten.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Indien gekoppeld aan Outlook van de assignee, verwijder ook daar de afspraak
|
||||
if (!empty($card['outlook_event_id']) && !empty($card['assignee_id'])) {
|
||||
try {
|
||||
microsoft_delete_event((int)$card['assignee_id'], $card['outlook_event_id']);
|
||||
} catch (Throwable $t) {}
|
||||
}
|
||||
|
||||
$db->prepare("DELETE FROM board_cards WHERE id = ?")->execute([$cardId]);
|
||||
echo json_encode(['success' => true, 'message' => 'Taak verwijderd.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => false, 'error' => 'Onbekende actie.']);
|
||||
} catch (Throwable $e) {
|
||||
error_log("Fociz Board API Fout: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Serverfout: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
"ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings",
|
||||
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme",
|
||||
"ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled",
|
||||
"ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)",
|
||||
];
|
||||
foreach ($colUpgrades as $sql) {
|
||||
try {
|
||||
@@ -155,6 +156,37 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
// Stil negeren indien kolom al bestaat
|
||||
}
|
||||
}
|
||||
|
||||
// Zorg dat er ten minste één standaard bord met standaard lanes aanwezig is
|
||||
try {
|
||||
$boardCount = (int)$pdo->query("SELECT COUNT(*) FROM boards")->fetchColumn();
|
||||
if ($boardCount === 0) {
|
||||
$firstAdminId = (int)$pdo->query("SELECT id FROM users ORDER BY (role = 'ADMIN') DESC, id ASC LIMIT 1")->fetchColumn();
|
||||
if ($firstAdminId > 0) {
|
||||
$pdo->prepare("INSERT INTO boards (title, description, created_by_id) VALUES (?, ?, ?)")
|
||||
->execute(['Hoofdbord', 'Standaard team- en takenbord voor opdrachten', $firstAdminId]);
|
||||
$boardId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Maak beheerder lid van het bord
|
||||
$pdo->prepare("INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, 'ADMIN')")
|
||||
->execute([$boardId, $firstAdminId]);
|
||||
|
||||
// Maak de 4 standaard lanes aan
|
||||
$defaultLanes = [
|
||||
['title' => 'Open', 'color' => 'slate', 'pos' => 0],
|
||||
['title' => 'In uitvoering', 'color' => 'amber', 'pos' => 1],
|
||||
['title' => 'Testen', 'color' => 'purple', 'pos' => 2],
|
||||
['title' => 'Klaar', 'color' => 'emerald', 'pos' => 3],
|
||||
];
|
||||
$laneStmt = $pdo->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)");
|
||||
foreach ($defaultLanes as $l) {
|
||||
$laneStmt->execute([$boardId, $l['title'], $l['color'], $l['pos']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Stil negeren indien tabellen nog in opbouw zijn
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("Fout bij controleren tabellen: " . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -133,3 +133,77 @@ CREATE TABLE IF NOT EXISTS `system_settings` (
|
||||
`setting_value` TEXT NULL,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Fociz Kanban / Trello Borden, Lanes, Kaarten & Tijdspadkoppelingen
|
||||
CREATE TABLE IF NOT EXISTS `boards` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`created_by_id` INT NOT NULL,
|
||||
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`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_boards_creator` 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 `board_members` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`board_id` INT NOT NULL,
|
||||
`user_id` INT NOT NULL,
|
||||
`role` ENUM('ADMIN', 'MEMBER', 'VIEWER') NOT NULL DEFAULT 'MEMBER',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uk_board_user` (`board_id`, `user_id`),
|
||||
CONSTRAINT `fk_bmembers_board` FOREIGN KEY (`board_id`) REFERENCES `boards` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_bmembers_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 `board_lanes` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`board_id` INT NOT NULL,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
`color` VARCHAR(30) NOT NULL DEFAULT 'slate',
|
||||
`position` INT NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX (`board_id`, `position`),
|
||||
CONSTRAINT `fk_blanes_board` FOREIGN KEY (`board_id`) REFERENCES `boards` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `board_cards` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`board_id` INT NOT NULL,
|
||||
`lane_id` INT NOT NULL,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`assignee_id` INT NULL,
|
||||
`priority` ENUM('P1', 'P2', 'P3') NOT NULL DEFAULT 'P2',
|
||||
`color` VARCHAR(30) NOT NULL DEFAULT 'yellow',
|
||||
`position` INT NOT NULL DEFAULT 0,
|
||||
`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 (`board_id`, `lane_id`, `position`),
|
||||
INDEX (`assignee_id`),
|
||||
CONSTRAINT `fk_bcards_board` FOREIGN KEY (`board_id`) REFERENCES `boards` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_bcards_lane` FOREIGN KEY (`lane_id`) REFERENCES `board_lanes` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_bcards_assignee` FOREIGN KEY (`assignee_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_bcards_creator` 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 `board_card_schedules` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`card_id` INT NOT NULL,
|
||||
`schedule_type` ENUM('SINGLE', 'DAILY_RANGE') NOT NULL DEFAULT 'SINGLE',
|
||||
`start_date` DATE NOT NULL,
|
||||
`end_date` DATE NOT NULL,
|
||||
`start_time` VARCHAR(10) NOT NULL DEFAULT '14:00',
|
||||
`end_time` VARCHAR(10) NOT NULL DEFAULT '15:00',
|
||||
`days_of_week` VARCHAR(50) NULL DEFAULT '1,2,3,4,5',
|
||||
`outlook_event_id` VARCHAR(255) NULL,
|
||||
`outlook_synced_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_sched_card` (`card_id`),
|
||||
CONSTRAINT `fk_bsched_card` FOREIGN KEY (`card_id`) REFERENCES `board_cards` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
+54
-6
@@ -13,7 +13,7 @@ $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUse
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'Fociz — Focus, Dagplanning & Roadmap') ?></title>
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'Fociz — Focus, Planning & Roadmap') ?></title>
|
||||
|
||||
<!-- Anti-Flicker Theme Script: direct in head zodat er nooit een flits optreedt -->
|
||||
<script>
|
||||
@@ -159,16 +159,60 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<nav class="hidden md:flex items-center space-x-1">
|
||||
<a href="/planner.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'planner.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||||
<span>Dagplanning</span>
|
||||
<span>Planning</span>
|
||||
</a>
|
||||
<a href="/brainstorm.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= in_array($currentPage, ['brainstorm.php', 'brainstorm_canvas.php']) ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
|
||||
<!-- Dropdown: Projecten (Bord, Brainstorm, Roadmap) -->
|
||||
<?php
|
||||
$isProjectsActive = in_array($currentPage, ['board.php', 'brainstorm.php', 'brainstorm_canvas.php', 'roadmap.php']);
|
||||
?>
|
||||
<div class="relative group/projects">
|
||||
<button type="button" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer <?= $isProjectsActive ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
<i data-lucide="folder-kanban" class="w-4 h-4"></i>
|
||||
<span>Projecten</span>
|
||||
<i data-lucide="chevron-down" class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200 group-hover/projects:rotate-180"></i>
|
||||
</button>
|
||||
<div class="absolute left-0 top-full pt-2 w-64 hidden group-hover/projects:block z-50 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-xl p-2 space-y-1 backdrop-blur-md">
|
||||
<!-- 1. Bord (Trello / Kanban) -->
|
||||
<a href="/board.php" class="flex items-start space-x-3 p-2.5 rounded-xl transition <?= $currentPage === 'board.php' ? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300' : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/70' ?>">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<i data-lucide="trello" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold leading-tight flex items-center gap-1.5">
|
||||
<span>Bord</span>
|
||||
<span class="text-[9px] font-bold px-1.5 py-0.5 rounded bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300">Nieuw</span>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500 dark:text-slate-400 leading-snug">Lanes, post-its & Outlook planning</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- 2. Brainstorm -->
|
||||
<a href="/brainstorm.php" class="flex items-start space-x-3 p-2.5 rounded-xl transition <?= in_array($currentPage, ['brainstorm.php', 'brainstorm_canvas.php']) ? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300' : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/70' ?>">
|
||||
<div class="w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<i data-lucide="lightbulb" class="w-4 h-4"></i>
|
||||
<span>Brainstorm</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold leading-tight">Brainstorm</div>
|
||||
<div class="text-[11px] text-slate-500 dark:text-slate-400 leading-snug">Ideeënsessies & canvas</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/roadmap.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'roadmap.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
|
||||
<!-- 3. Roadmap -->
|
||||
<a href="/roadmap.php" class="flex items-start space-x-3 p-2.5 rounded-xl transition <?= $currentPage === 'roadmap.php' ? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300' : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/70' ?>">
|
||||
<div class="w-8 h-8 rounded-lg bg-sky-100 dark:bg-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<i data-lucide="map" class="w-4 h-4"></i>
|
||||
<span>Roadmap</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold leading-tight">Roadmap</div>
|
||||
<div class="text-[11px] text-slate-500 dark:text-slate-400 leading-snug">Strategische planning & kwartalen</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/profile.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'profile.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
|
||||
<i data-lucide="user" class="w-4 h-4"></i>
|
||||
<span>Mijn Profiel</span>
|
||||
@@ -219,6 +263,10 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<i data-lucide="calendar" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Planning</span>
|
||||
</a>
|
||||
<a href="/board.php" class="flex flex-col items-center py-1 <?= $currentPage === 'board.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="trello" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Bord</span>
|
||||
</a>
|
||||
<a href="/brainstorm.php" class="flex flex-col items-center py-1 <?= in_array($currentPage, ['brainstorm.php', 'brainstorm_canvas.php']) ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="lightbulb" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Brainstorm</span>
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ if ($isAjaxRequest) {
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
$currentUser = require_auth(true);
|
||||
} else {
|
||||
$pageTitle = 'Fociz — Planning & Structuur';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
}
|
||||
|
||||
@@ -329,7 +330,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
|
||||
Dagplanning & Structuur
|
||||
Planning & Structuur
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -957,7 +958,7 @@ if ($isAjaxRequest) {
|
||||
'syncHash' => $initialSyncHash,
|
||||
'focusGoal' => $dailyPlan['focus_goal'] ?? '',
|
||||
'dailyNotes' => $dailyPlan['notes'] ?? '',
|
||||
'title' => 'Fociz — Dagplanning (' . $formattedSelectedDate . ')',
|
||||
'title' => 'Fociz — Planning (' . $formattedSelectedDate . ')',
|
||||
'html' => $contentHtml,
|
||||
]);
|
||||
exit;
|
||||
|
||||
Reference in New Issue
Block a user