Dev #32
@@ -72,6 +72,171 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2b. Jira Dashboard Data Ophalen (Toegewezen + Team issues + Stats)
|
||||
if ($action === 'get_jira_dashboard') {
|
||||
$force = !empty($_GET['force']) || !empty($_POST['force']);
|
||||
$res = jira_get_dashboard_data($user['id'], $force);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2c. Jira Issue inplannen in Dagplanner
|
||||
if ($action === 'plan_jira_task') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$key = trim($input['key'] ?? '');
|
||||
$summary = trim($input['summary'] ?? '');
|
||||
$url = trim($input['url'] ?? '');
|
||||
$planDate = !empty($input['plan_date']) ? trim($input['plan_date']) : date('Y-m-d');
|
||||
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
|
||||
$timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null;
|
||||
$timeBlockEnd = !empty($input['time_block_end']) ? trim($input['time_block_end']) : null;
|
||||
$durationMinutes = !empty($input['duration_minutes']) ? (int)$input['duration_minutes'] : 45;
|
||||
|
||||
if (empty($key) || empty($summary)) {
|
||||
echo json_encode(['error' => 'Ticket key en titel zijn verplicht']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = "[{$key}] {$summary}";
|
||||
|
||||
// Zoek of maak daily plan aan voor deze datum
|
||||
$stmtPlan = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?");
|
||||
$stmtPlan->execute([$user['id'], $planDate]);
|
||||
$planId = $stmtPlan->fetchColumn();
|
||||
|
||||
if (!$planId) {
|
||||
$stmtCreatePlan = $db->prepare("INSERT INTO daily_plans (user_id, plan_date) VALUES (?, ?)");
|
||||
$stmtCreatePlan->execute([$user['id'], $planDate]);
|
||||
$planId = (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
// Voeg taak toe
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, status, source, external_id, external_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'TODO', 'JIRA', ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$user['id'],
|
||||
$planId,
|
||||
$title,
|
||||
$priority,
|
||||
$timeBlockStart,
|
||||
$timeBlockEnd,
|
||||
$durationMinutes,
|
||||
$key,
|
||||
$url
|
||||
]);
|
||||
$taskId = (int)$db->lastInsertId();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'message' => "Ticket {$key} is succesvol ingepland voor " . date('d-m-Y', strtotime($planDate)) . "!",
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2d. Jira Issue toevoegen aan een Kanban Bord kolom
|
||||
if ($action === 'board_jira_card') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$boardId = (int)($input['board_id'] ?? 0);
|
||||
$laneId = (int)($input['lane_id'] ?? 0);
|
||||
$key = trim($input['key'] ?? '');
|
||||
$summary = trim($input['summary'] ?? '');
|
||||
$description = trim($input['description'] ?? '');
|
||||
$url = trim($input['url'] ?? '');
|
||||
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
|
||||
$color = in_array($input['color'] ?? '', ['yellow', 'blue', 'green', 'purple', 'red', 'pink', 'indigo', 'orange']) ? $input['color'] : 'blue';
|
||||
|
||||
if (empty($boardId) || empty($laneId) || empty($key) || empty($summary)) {
|
||||
echo json_encode(['error' => 'Bord, kolom, ticket key en titel zijn verplicht']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Controleer toegang tot bord
|
||||
$isAdmin = ($user['role'] ?? '') === 'ADMIN';
|
||||
if (!$isAdmin) {
|
||||
$stmtCheck = $db->prepare("SELECT role FROM board_members WHERE board_id = ? AND user_id = ?");
|
||||
$stmtCheck->execute([$boardId, $user['id']]);
|
||||
if (!$stmtCheck->fetchColumn()) {
|
||||
echo json_encode(['error' => 'Geen toegang tot dit bord']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Controleer of lane bij bord hoort
|
||||
$stmtLane = $db->prepare("SELECT id FROM board_lanes WHERE id = ? AND board_id = ?");
|
||||
$stmtLane->execute([$laneId, $boardId]);
|
||||
if (!$stmtLane->fetchColumn()) {
|
||||
echo json_encode(['error' => 'Kolom hoort niet bij dit bord']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = "[{$key}] {$summary}";
|
||||
$descFull = trim(($url ? "🔗 Jira: {$url}\n\n" : "") . $description);
|
||||
|
||||
$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,
|
||||
$descFull,
|
||||
$user['id'],
|
||||
$priority,
|
||||
$color,
|
||||
$maxPos + 1,
|
||||
$user['id']
|
||||
]);
|
||||
$cardId = (int)$db->lastInsertId();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'card_id' => $cardId,
|
||||
'message' => "Ticket {$key} is succesvol als kaart op het bord geplaatst!",
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2e. Beschikbare borden en kolommen ophalen voor selectiedialoog
|
||||
if ($action === 'get_boards_and_lanes') {
|
||||
$isAdmin = ($user['role'] ?? '') === 'ADMIN';
|
||||
if ($isAdmin) {
|
||||
$boards = $db->query("SELECT id, title FROM boards WHERE is_archived = 0 ORDER BY id ASC")->fetchAll();
|
||||
} else {
|
||||
$stmt = $db->prepare("
|
||||
SELECT b.id, b.title
|
||||
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
|
||||
");
|
||||
$stmt->execute([$user['id']]);
|
||||
$boards = $stmt->fetchAll();
|
||||
}
|
||||
|
||||
$boardIds = array_column($boards, 'id');
|
||||
$lanesByBoard = [];
|
||||
if (!empty($boardIds)) {
|
||||
$inClause = implode(',', array_map('intval', $boardIds));
|
||||
$lanes = $db->query("SELECT id, board_id, title, color FROM board_lanes WHERE board_id IN ({$inClause}) ORDER BY position ASC")->fetchAll();
|
||||
foreach ($lanes as $lane) {
|
||||
$lanesByBoard[$lane['board_id']][] = $lane;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($boards as &$b) {
|
||||
$b['lanes'] = $lanesByBoard[$b['id']] ?? [];
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'boards' => $boards]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Outlook Agenda Afspraken Ophalen
|
||||
if ($action === 'get_outlook_events') {
|
||||
$date = $_GET['date'] ?? date('Y-m-d');
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-01" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#4338ca"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-01)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fbcfe8"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fbcfe8"/>
|
||||
<path d="M30 36 C33 23 67 23 70 36 C68 40 64 42 64 42 C54 35 46 35 36 42 Z" fill="#334155"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-02" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0ea5e9"/>
|
||||
<stop offset="100%" stop-color="#0284c7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-02)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<circle cx="34" cy="30" r="10" fill="#78350f"/>
|
||||
<circle cx="50" cy="24" r="11" fill="#78350f"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#78350f"/>
|
||||
<circle cx="28" cy="42" r="8" fill="#78350f"/>
|
||||
<circle cx="72" cy="42" r="8" fill="#78350f"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-03" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#10b981"/>
|
||||
<stop offset="100%" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-03)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fef08a"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fef08a"/>
|
||||
<path d="M26 38 C28 22 72 22 74 38 Z" fill="#1e293b"/>
|
||||
<path d="M30 38 H86 C88 38 88 41 84 43 H30 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-04" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#8b5cf6"/>
|
||||
<stop offset="100%" stop-color="#6d28d9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-04)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#ffedd5"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#ffedd5"/>
|
||||
<circle cx="50" cy="28" r="12" fill="#1e293b"/>
|
||||
<path d="M30 36 C34 24 66 24 70 36 C70 42 66 46 66 46 C56 36 44 36 34 46 Z" fill="#1e293b"/>
|
||||
<path d="M68 36 C78 30 84 44 78 52 C74 46 72 40 68 36 Z" fill="#1e293b"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-05" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f43f5e"/>
|
||||
<stop offset="100%" stop-color="#be123c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-05)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<path d="M30 36 C33 23 67 23 70 36 C68 40 64 42 64 42 C54 35 46 35 36 42 Z" fill="#18181b"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M40 56 C44 62 56 62 60 56 C62 64 54 68 50 68 C46 68 38 64 40 56 Z" fill="#18181b"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-06" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f59e0b"/>
|
||||
<stop offset="100%" stop-color="#d97706"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-06)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fcd34d"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fcd34d"/>
|
||||
<circle cx="34" cy="30" r="10" fill="#451a03"/>
|
||||
<circle cx="50" cy="24" r="11" fill="#451a03"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#451a03"/>
|
||||
<circle cx="28" cy="42" r="8" fill="#451a03"/>
|
||||
<circle cx="72" cy="42" r="8" fill="#451a03"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-07" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#14b8a6"/>
|
||||
<stop offset="100%" stop-color="#0f766e"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-07)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<path d="M26 38 C28 22 72 22 74 38 Z" fill="#1e293b"/>
|
||||
<path d="M30 38 H86 C88 38 88 41 84 43 H30 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M26 46 C26 26 74 26 74 46" fill="none" stroke="#0f172a" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<rect x="23" y="42" width="6" height="12" rx="3" fill="#38bdf8"/>
|
||||
<rect x="71" y="42" width="6" height="12" rx="3" fill="#38bdf8"/>
|
||||
<path d="M26 52 C26 62 40 62 44 60" fill="none" stroke="#0f172a" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="44" cy="60" r="2.5" fill="#0f172a"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-08" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#3b82f6"/>
|
||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-08)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fbcfe8"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fbcfe8"/>
|
||||
<circle cx="50" cy="28" r="12" fill="#334155"/>
|
||||
<path d="M30 36 C34 24 66 24 70 36 C70 42 66 46 66 46 C56 36 44 36 34 46 Z" fill="#334155"/>
|
||||
<path d="M68 36 C78 30 84 44 78 52 C74 46 72 40 68 36 Z" fill="#334155"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-09" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ec4899"/>
|
||||
<stop offset="100%" stop-color="#be185d"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-09)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<path d="M30 36 C33 23 67 23 70 36 C68 40 64 42 64 42 C54 35 46 35 36 42 Z" fill="#7c2d12"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-10" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#64748b"/>
|
||||
<stop offset="100%" stop-color="#334155"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-10)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<circle cx="34" cy="30" r="10" fill="#0f172a"/>
|
||||
<circle cx="50" cy="24" r="11" fill="#0f172a"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#0f172a"/>
|
||||
<circle cx="28" cy="42" r="8" fill="#0f172a"/>
|
||||
<circle cx="72" cy="42" r="8" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M40 56 C44 62 56 62 60 56 C62 64 54 68 50 68 C46 68 38 64 40 56 Z" fill="#0f172a"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-11" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#84cc16"/>
|
||||
<stop offset="100%" stop-color="#4d7c0f"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-11)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#ffedd5"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#ffedd5"/>
|
||||
<path d="M26 38 C28 22 72 22 74 38 Z" fill="#1e293b"/>
|
||||
<path d="M30 38 H86 C88 38 88 41 84 43 H30 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-12" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f97316"/>
|
||||
<stop offset="100%" stop-color="#c2410c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-12)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fcd34d"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fcd34d"/>
|
||||
<circle cx="50" cy="28" r="12" fill="#1e1b4b"/>
|
||||
<path d="M30 36 C34 24 66 24 70 36 C70 42 66 46 66 46 C56 36 44 36 34 46 Z" fill="#1e1b4b"/>
|
||||
<path d="M68 36 C78 30 84 44 78 52 C74 46 72 40 68 36 Z" fill="#1e1b4b"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-13" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#a855f7"/>
|
||||
<stop offset="100%" stop-color="#7e22ce"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-13)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fbcfe8"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fbcfe8"/>
|
||||
<path d="M30 36 C33 23 67 23 70 36 C68 40 64 42 64 42 C54 35 46 35 36 42 Z" fill="#4c1d95"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-14" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#06b6d4"/>
|
||||
<stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-14)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#ffedd5"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#ffedd5"/>
|
||||
<circle cx="34" cy="30" r="10" fill="#422006"/>
|
||||
<circle cx="50" cy="24" r="11" fill="#422006"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#422006"/>
|
||||
<circle cx="28" cy="42" r="8" fill="#422006"/>
|
||||
<circle cx="72" cy="42" r="8" fill="#422006"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M26 46 C26 26 74 26 74 46" fill="none" stroke="#0f172a" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<rect x="23" y="42" width="6" height="12" rx="3" fill="#38bdf8"/>
|
||||
<rect x="71" y="42" width="6" height="12" rx="3" fill="#38bdf8"/>
|
||||
<path d="M26 52 C26 62 40 62 44 60" fill="none" stroke="#0f172a" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="44" cy="60" r="2.5" fill="#0f172a"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-15" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e11d48"/>
|
||||
<stop offset="100%" stop-color="#9f1239"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-15)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<path d="M26 38 C28 22 72 22 74 38 Z" fill="#1e293b"/>
|
||||
<path d="M30 38 H86 C88 38 88 41 84 43 H30 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M40 56 C44 62 56 62 60 56 C62 64 54 68 50 68 C46 68 38 64 40 56 Z" fill="#172554"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-16" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#475569"/>
|
||||
<stop offset="100%" stop-color="#1e293b"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-16)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fef08a"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fef08a"/>
|
||||
<circle cx="50" cy="28" r="12" fill="#3b0764"/>
|
||||
<path d="M30 36 C34 24 66 24 70 36 C70 42 66 46 66 46 C56 36 44 36 34 46 Z" fill="#3b0764"/>
|
||||
<path d="M68 36 C78 30 84 44 78 52 C74 46 72 40 68 36 Z" fill="#3b0764"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-17" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d946ef"/>
|
||||
<stop offset="100%" stop-color="#a21caf"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-17)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<path d="M30 36 C33 23 67 23 70 36 C68 40 64 42 64 42 C54 35 46 35 36 42 Z" fill="#b91c1c"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-18" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#059669"/>
|
||||
<stop offset="100%" stop-color="#065f46"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-18)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#ffedd5"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#ffedd5"/>
|
||||
<circle cx="34" cy="30" r="10" fill="#1e293b"/>
|
||||
<circle cx="50" cy="24" r="11" fill="#1e293b"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#1e293b"/>
|
||||
<circle cx="28" cy="42" r="8" fill="#1e293b"/>
|
||||
<circle cx="72" cy="42" r="8" fill="#1e293b"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-19" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2563eb"/>
|
||||
<stop offset="100%" stop-color="#1e40af"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-19)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fbcfe8"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fbcfe8"/>
|
||||
<path d="M26 38 C28 22 72 22 74 38 Z" fill="#1e293b"/>
|
||||
<path d="M30 38 H86 C88 38 88 41 84 43 H30 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-20" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ea580c"/>
|
||||
<stop offset="100%" stop-color="#9a3412"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-20)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fcd34d"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fcd34d"/>
|
||||
<circle cx="50" cy="28" r="12" fill="#7c2d12"/>
|
||||
<path d="M30 36 C34 24 66 24 70 36 C70 42 66 46 66 46 C56 36 44 36 34 46 Z" fill="#7c2d12"/>
|
||||
<path d="M68 36 C78 30 84 44 78 52 C74 46 72 40 68 36 Z" fill="#7c2d12"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M40 56 C44 62 56 62 60 56 C62 64 54 68 50 68 C46 68 38 64 40 56 Z" fill="#7c2d12"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-21" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#7c3aed"/>
|
||||
<stop offset="100%" stop-color="#5b21b6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-21)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<path d="M30 36 C33 23 67 23 70 36 C68 40 64 42 64 42 C54 35 46 35 36 42 Z" fill="#e11d48"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M26 46 C26 26 74 26 74 46" fill="none" stroke="#0f172a" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<rect x="23" y="42" width="6" height="12" rx="3" fill="#38bdf8"/>
|
||||
<rect x="71" y="42" width="6" height="12" rx="3" fill="#38bdf8"/>
|
||||
<path d="M26 52 C26 62 40 62 44 60" fill="none" stroke="#0f172a" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="44" cy="60" r="2.5" fill="#0f172a"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-22" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0284c7"/>
|
||||
<stop offset="100%" stop-color="#0369a1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-22)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#ffedd5"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#ffedd5"/>
|
||||
<circle cx="34" cy="30" r="10" fill="#1e293b"/>
|
||||
<circle cx="50" cy="24" r="11" fill="#1e293b"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#1e293b"/>
|
||||
<circle cx="28" cy="42" r="8" fill="#1e293b"/>
|
||||
<circle cx="72" cy="42" r="8" fill="#1e293b"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-23" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0d9488"/>
|
||||
<stop offset="100%" stop-color="#115e59"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-23)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fbcfe8"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fbcfe8"/>
|
||||
<path d="M26 38 C28 22 72 22 74 38 Z" fill="#1e293b"/>
|
||||
<path d="M30 38 H86 C88 38 88 41 84 43 H30 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<linearGradient id="bg_avatar-24" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#4f46e5"/>
|
||||
<stop offset="100%" stop-color="#3730a3"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#bg_avatar-24)"/>
|
||||
<ellipse cx="50" cy="94" rx="34" ry="24" fill="#ffffff" fill-opacity="0.2"/>
|
||||
<rect x="42" y="58" width="16" height="14" rx="4" fill="#fed7aa"/>
|
||||
<ellipse cx="50" cy="46" rx="20" ry="22" fill="#fed7aa"/>
|
||||
<circle cx="50" cy="28" r="12" fill="#0f172a"/>
|
||||
<path d="M30 36 C34 24 66 24 70 36 C70 42 66 46 66 46 C56 36 44 36 34 46 Z" fill="#0f172a"/>
|
||||
<path d="M68 36 C78 30 84 44 78 52 C74 46 72 40 68 36 Z" fill="#0f172a"/>
|
||||
<circle cx="43" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="57" cy="45" r="2.5" fill="#1e293b"/>
|
||||
<circle cx="44" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="58" cy="44" r="0.8" fill="#ffffff"/>
|
||||
<circle cx="43" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<circle cx="57" cy="45" r="6" fill="none" stroke="#0f172a" stroke-width="2"/>
|
||||
<line x1="49" y1="45" x2="51" y2="45" stroke="#0f172a" stroke-width="2"/>
|
||||
<path d="M45 54 Q50 60 55 54" fill="none" stroke="#991b1b" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<linearGradient id="focizGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="50%" stop-color="#4f46e5"/>
|
||||
<stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
<filter id="focizShadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#4f46e5" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Achtergrond met afgeronde hoeken (superellipse / squircl) -->
|
||||
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#focizGrad)" filter="url(#focizShadow)"/>
|
||||
<!-- Subtiele binnenrand highlight -->
|
||||
<rect x="3" y="3" width="58" height="58" rx="15" fill="none" stroke="#ffffff" stroke-opacity="0.25" stroke-width="1.5"/>
|
||||
<!-- Karakteristieke Fociz 'F' -->
|
||||
<path d="M22 17 H43 C44.6569 17 46 18.3431 46 20 C46 21.6569 44.6569 23 43 23 H28 V29 H39 C40.6569 29 42 30.3431 42 32 C42 33.6569 40.6569 35 39 35 H28 V44 C28 45.6569 26.6569 47 25 47 C23.3431 47 22 45.6569 22 44 V17 Z" fill="#ffffff"/>
|
||||
<!-- Subtiele focus stip / accent -->
|
||||
<circle cx="43" cy="43" r="3.5" fill="#38bdf8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<linearGradient id="focizGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="50%" stop-color="#4f46e5"/>
|
||||
<stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
<filter id="focizShadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#4f46e5" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Achtergrond met afgeronde hoeken (superellipse / squircl) -->
|
||||
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#focizGrad)" filter="url(#focizShadow)"/>
|
||||
<!-- Subtiele binnenrand highlight -->
|
||||
<rect x="3" y="3" width="58" height="58" rx="15" fill="none" stroke="#ffffff" stroke-opacity="0.25" stroke-width="1.5"/>
|
||||
<!-- Karakteristieke Fociz 'F' -->
|
||||
<path d="M22 17 H43 C44.6569 17 46 18.3431 46 20 C46 21.6569 44.6569 23 43 23 H28 V29 H39 C40.6569 29 42 30.3431 42 32 C42 33.6569 40.6569 35 39 35 H28 V44 C28 45.6569 26.6569 47 25 47 C23.3431 47 22 45.6569 22 44 V17 Z" fill="#ffffff"/>
|
||||
<!-- Subtiele focus stip / accent -->
|
||||
<circle cx="43" cy="43" r="3.5" fill="#38bdf8"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -53,12 +53,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Verstuur herstelmail via de nieuwe FocizMailer
|
||||
$mailResult = FocizMailer::sendPasswordResetEmail($user['email'], $user['name'], $resetUrl);
|
||||
if (!$mailResult['success']) {
|
||||
error_log("Fout bij verzenden wachtwoordherstel mail naar {$email}: " . ($mailResult['error'] ?? 'Onbekend'));
|
||||
}
|
||||
}
|
||||
|
||||
// Anti-enumeratie: Toon altijd succes ongeacht of het e-mailadres bestaat
|
||||
$errMsg = $mailResult['error'] ?? 'Mailserver fout';
|
||||
error_log("Fout bij verzenden wachtwoordherstel mail naar {$email}: {$errMsg}");
|
||||
$error = "De herstelmail kon niet worden verzonden via de mailserver: " . htmlspecialchars($errMsg);
|
||||
} else {
|
||||
$submitted = true;
|
||||
}
|
||||
} else {
|
||||
// In development omgeving geven we directe diagnostische feedback
|
||||
if (defined('APP_ENV') && APP_ENV === 'development') {
|
||||
$error = "Er is geen Fociz account gevonden met het e-mailadres '{$email}'. Controleer of dit e-mailadres correct is ingevoerd.";
|
||||
} else {
|
||||
// Anti-enumeratie op productie: toon altijd succes
|
||||
$submitted = true;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log("Wachtwoord vergeten fout: " . $e->getMessage());
|
||||
@@ -75,6 +84,8 @@ $pageTitle = "Fociz — Wachtwoord Vergeten";
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<script>
|
||||
(function() {
|
||||
var activeTheme = localStorage.getItem('fociz_theme') || 'dark';
|
||||
|
||||
@@ -105,6 +105,8 @@ $pageTitle = "Fociz — Uitnodiging Accepteren";
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<!-- Anti-flicker script: Laad direct het gekozen thema uit localStorage -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -136,6 +136,8 @@ $pageTitle = "Fociz — Inloggen";
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<!-- Anti-flicker script: Laad direct het gekozen thema uit localStorage vóór DOM render -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -78,6 +78,8 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<script>
|
||||
(function() {
|
||||
var activeTheme = localStorage.getItem('fociz_theme') || 'dark';
|
||||
|
||||
@@ -90,6 +90,8 @@ $pageTitle = "Fociz — Eerste Initialisatie";
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<!-- Anti-flicker script: Laad direct het gekozen thema uit localStorage -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -677,11 +677,12 @@ function renderCards() {
|
||||
let assigneeHtml = '<span class="text-[10px] text-slate-400 italic">Niet toegewezen</span>';
|
||||
if (card.assignee_name) {
|
||||
const initial = card.assignee_name.charAt(0).toUpperCase();
|
||||
const avatarMarkup = card.assignee_avatar
|
||||
? `<img src="${escapeHtml(card.assignee_avatar)}" class="w-5 h-5 rounded-full object-cover flex-shrink-0 bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-800/60">`
|
||||
: `<div class="w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center flex-shrink-0">${initial}</div>`;
|
||||
assigneeHtml = `
|
||||
<div class="flex items-center space-x-1.5" title="Toegewezen aan: ${escapeHtml(card.assignee_name)}">
|
||||
<div class="w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center">
|
||||
${initial}
|
||||
</div>
|
||||
${avatarMarkup}
|
||||
<span class="text-[11px] font-medium text-slate-700 dark:text-slate-300 truncate max-w-[110px]">
|
||||
${escapeHtml(card.assignee_name)}
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ define('SMTP_PORT', (int)fociz_env('SMTP_PORT', 587));
|
||||
define('SMTP_USER', fociz_env('SMTP_USER', 'info@fociz.nl'));
|
||||
define('SMTP_PASS', fociz_env('SMTP_PASS', '6swpQSBg5uWFmjar7F6a'));
|
||||
define('SMTP_SECURE', fociz_env('SMTP_SECURE', 'tls')); // 'tls' (STARTTLS port 587) of 'ssl' (port 465)
|
||||
define('SMTP_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'no-reply@fociz.nl'));
|
||||
define('SMTP_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'info@fociz.nl'));
|
||||
define('SMTP_FROM_NAME', fociz_env('SMTP_FROM_NAME', 'Fociz'));
|
||||
define('SMTP_REPLY_TO', fociz_env('SMTP_REPLY_TO', 'info@fociz.nl'));
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<linearGradient id="focizGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="50%" stop-color="#4f46e5"/>
|
||||
<stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
<filter id="focizShadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#4f46e5" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Achtergrond met afgeronde hoeken (superellipse / squircl) -->
|
||||
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#focizGrad)" filter="url(#focizShadow)"/>
|
||||
<!-- Subtiele binnenrand highlight -->
|
||||
<rect x="3" y="3" width="58" height="58" rx="15" fill="none" stroke="#ffffff" stroke-opacity="0.25" stroke-width="1.5"/>
|
||||
<!-- Karakteristieke Fociz 'F' -->
|
||||
<path d="M22 17 H43 C44.6569 17 46 18.3431 46 20 C46 21.6569 44.6569 23 43 23 H28 V29 H39 C40.6569 29 42 30.3431 42 32 C42 33.6569 40.6569 35 39 35 H28 V44 C28 45.6569 26.6569 47 25 47 C23.3431 47 22 45.6569 22 44 V17 Z" fill="#ffffff"/>
|
||||
<!-- Subtiele focus stip / accent -->
|
||||
<circle cx="43" cy="43" r="3.5" fill="#38bdf8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
+20
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<linearGradient id="focizGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="50%" stop-color="#4f46e5"/>
|
||||
<stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
<filter id="focizShadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#4f46e5" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Achtergrond met afgeronde hoeken (superellipse / squircl) -->
|
||||
<rect x="2" y="2" width="60" height="60" rx="16" fill="url(#focizGrad)" filter="url(#focizShadow)"/>
|
||||
<!-- Subtiele binnenrand highlight -->
|
||||
<rect x="3" y="3" width="58" height="58" rx="15" fill="none" stroke="#ffffff" stroke-opacity="0.25" stroke-width="1.5"/>
|
||||
<!-- Karakteristieke Fociz 'F' -->
|
||||
<path d="M22 17 H43 C44.6569 17 46 18.3431 46 20 C46 21.6569 44.6569 23 43 23 H28 V29 H39 C40.6569 29 42 30.3431 42 32 C42 33.6569 40.6569 35 39 35 H28 V44 C28 45.6569 26.6569 47 25 47 C23.3431 47 22 45.6569 22 44 V17 Z" fill="#ffffff"/>
|
||||
<!-- Subtiele focus stip / accent -->
|
||||
<circle cx="43" cy="43" r="3.5" fill="#38bdf8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
+2
-2
@@ -211,7 +211,7 @@ function get_logged_in_user(): ?array {
|
||||
|
||||
// 2. Controleer actieve PHP sessie (mits behorend bij vandaag)
|
||||
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?");
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, created_at FROM users WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
$user = $stmt->fetch();
|
||||
if ($user) {
|
||||
@@ -229,7 +229,7 @@ function get_logged_in_user(): ?array {
|
||||
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart of server idle timeout (uitsluitend vandaag!)
|
||||
$dayToken = $_COOKIE['fociz_day_session'] ?? ($_COOKIE['fociz_remember'] ?? null);
|
||||
if (!empty($dayToken) && is_string($dayToken)) {
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
|
||||
$stmt->execute([$dayToken, $today]);
|
||||
$user = $stmt->fetch();
|
||||
if ($user) {
|
||||
|
||||
+14
-7
@@ -15,6 +15,9 @@ $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUse
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'Fociz — Focus, Planning & Roadmap') ?></title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<!-- Anti-Flicker Theme Script: direct in head zodat er nooit een flits optreedt -->
|
||||
<script>
|
||||
(function() {
|
||||
@@ -221,9 +224,9 @@ require_once __DIR__ . '/env_badge.php';
|
||||
</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>
|
||||
<a href="/jira.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'jira.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="layers" class="w-4 h-4 text-sky-500"></i>
|
||||
<span>Jira</span>
|
||||
</a>
|
||||
<?php if ($currentUser['role'] === 'ADMIN'): ?>
|
||||
<a href="/users.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'users.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' ?>">
|
||||
@@ -242,10 +245,14 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<div class="flex items-center space-x-2 sm:space-x-3">
|
||||
<!-- Profiel link rechtsboven -->
|
||||
<a href="/profile.php" class="flex items-center space-x-2.5 group hover:opacity-90 transition px-2 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/60" title="Mijn Profiel & Instellingen">
|
||||
<div class="w-7 h-7 rounded-lg bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 shadow-xs">
|
||||
<div class="w-7 h-7 rounded-lg bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 shadow-xs overflow-hidden">
|
||||
<?php if (!empty($currentUser['avatar_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($currentUser['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[6px] object-cover bg-white dark:bg-slate-950">
|
||||
<?php else: ?>
|
||||
<div class="w-full h-full rounded-[6px] bg-white dark:bg-slate-950 flex items-center justify-center font-bold text-[11px] text-indigo-600 dark:text-indigo-400 uppercase">
|
||||
<?= strtoupper(substr($currentUser['name'], 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="text-left hidden sm:block">
|
||||
<div class="text-xs font-semibold text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition leading-tight">
|
||||
@@ -283,9 +290,9 @@ require_once __DIR__ . '/env_badge.php';
|
||||
<i data-lucide="map" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Roadmap</span>
|
||||
</a>
|
||||
<a href="/profile.php" class="flex flex-col items-center py-1 <?= $currentPage === 'profile.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="user" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Profiel</span>
|
||||
<a href="/jira.php" class="flex flex-col items-center py-1 <?= $currentPage === 'jira.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
<i data-lucide="layers" class="w-4 h-4 mb-0.5"></i>
|
||||
<span>Jira</span>
|
||||
</a>
|
||||
<?php if ($currentUser['role'] === 'ADMIN'): ?>
|
||||
<a href="/admin.php" class="flex flex-col items-center py-1 <?= $currentPage === 'admin.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
|
||||
|
||||
+293
-48
@@ -142,17 +142,17 @@ function jira_test_connection(string $domain, string $email, string $apiToken):
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* Haalt issues op via een flexibele JQL query via Atlassian REST API
|
||||
*/
|
||||
function jira_fetch_issues(string $domain, string $email, string $apiToken, string $jql, 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',
|
||||
'fields' => 'summary,status,priority,issuetype,parent,project,assignee,reporter,duedate,updated',
|
||||
];
|
||||
|
||||
$url = $cleanDomain . '/rest/api/3/search?' . http_build_query($params);
|
||||
@@ -195,19 +195,32 @@ function jira_fetch_assigned_issues(string $domain, string $email, string $apiTo
|
||||
$key = $item['key'] ?? '';
|
||||
$summary = $fields['summary'] ?? '(Geen titel)';
|
||||
$statusName = $fields['status']['name'] ?? 'To Do';
|
||||
$statusCategory = $fields['status']['statusCategory']['key'] ?? 'indeterminate';
|
||||
$priorityName = $fields['priority']['name'] ?? 'Medium';
|
||||
$issueTypeName = $fields['issuetype']['name'] ?? 'Taak';
|
||||
$issueTypeIcon = $fields['issuetype']['iconUrl'] ?? '';
|
||||
$projectName = $fields['project']['name'] ?? 'Project';
|
||||
$projectKey = $fields['project']['key'] ?? '';
|
||||
$dueDate = $fields['duedate'] ?? null;
|
||||
$updated = $fields['updated'] ?? null;
|
||||
|
||||
$assigneeName = $fields['assignee']['displayName'] ?? 'Niet toegewezen';
|
||||
$assigneeAvatar = $fields['assignee']['avatarUrls']['48x48'] ?? ($fields['assignee']['avatarUrls']['32x32'] ?? '');
|
||||
|
||||
$issues[] = [
|
||||
'key' => $key,
|
||||
'summary' => $summary,
|
||||
'status' => $statusName,
|
||||
'status_category' => $statusCategory,
|
||||
'priority' => $priorityName,
|
||||
'issue_type' => $issueTypeName,
|
||||
'issue_type_icon' => $issueTypeIcon,
|
||||
'project' => $projectName,
|
||||
'project_key' => $projectKey,
|
||||
'assignee_name' => $assigneeName,
|
||||
'assignee_avatar' => $assigneeAvatar,
|
||||
'due_date' => $dueDate,
|
||||
'updated' => $updated,
|
||||
'url' => "{$cleanDomain}/browse/{$key}",
|
||||
'source' => 'JIRA_LIVE',
|
||||
];
|
||||
@@ -221,66 +234,271 @@ function jira_fetch_assigned_issues(string $domain, string $email, string $apiTo
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de Jira tickets op voor een Fociz gebruiker
|
||||
* Schakelt naadloos tussen live Jira tickets en sandbox demotickets
|
||||
* Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker
|
||||
*/
|
||||
function jira_get_user_issues(int $userId, bool $forceRefresh = false): array {
|
||||
function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array {
|
||||
$jql = 'assignee = currentUser() ORDER BY updated DESC';
|
||||
return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt openstaande team issues op die horen bij de projecten van de gebruiker
|
||||
*/
|
||||
function jira_fetch_team_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array {
|
||||
$jql = 'assignee != currentUser() AND statusCategory != Done ORDER BY updated DESC';
|
||||
return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de complete dashboard data op (persoonlijk + team) inclusief statistieken
|
||||
*/
|
||||
function jira_get_dashboard_data(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
|
||||
// Sandbox / Demo dataset indien niet gekoppeld of expliciet in demomodus
|
||||
if (!$integration || !empty($integration['is_demo_mode'])) {
|
||||
return [
|
||||
'is_demo' => true,
|
||||
'success' => true,
|
||||
'issues' => [
|
||||
$assigned = [
|
||||
[
|
||||
'key' => 'FOC-101',
|
||||
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Fociz Platform',
|
||||
'project_key' => 'FOC',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+2 days')),
|
||||
'url' => '#',
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-101',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-102',
|
||||
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Feature',
|
||||
'project' => 'Fociz Platform',
|
||||
'project_key' => 'FOC',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+4 days')),
|
||||
'url' => '#',
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-102',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-103',
|
||||
'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API',
|
||||
'status' => 'In Review',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Integrations Hub',
|
||||
'project_key' => 'INT',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d'),
|
||||
'url' => '#',
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-103',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-104',
|
||||
'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Roadmap Module',
|
||||
'project_key' => 'ROAD',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+7 days')),
|
||||
'url' => '#',
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-104',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-108',
|
||||
'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox',
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Highest',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'UI/UX Redesign',
|
||||
'project_key' => 'UI',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+1 day')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-108',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-112',
|
||||
'summary' => 'Avatar upload beveiliging en compressie optimalisatie',
|
||||
'status' => 'Done',
|
||||
'status_category' => 'done',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Security',
|
||||
'project_key' => 'SEC',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('-1 day')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-112',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
];
|
||||
|
||||
$team = [
|
||||
[
|
||||
'key' => 'FOC-95',
|
||||
'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab',
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Highest',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'Fociz Platform',
|
||||
'project_key' => 'FOC',
|
||||
'assignee_name' => 'Lisa Bakker',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+1 day')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-95',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-98',
|
||||
'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Notifications',
|
||||
'project_key' => 'NOTIF',
|
||||
'assignee_name' => 'Daan van Dijk',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+3 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-98',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-105',
|
||||
'summary' => 'Database indexering optimaliseren voor grote audit logs',
|
||||
'status' => 'In Review',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Database & Core',
|
||||
'project_key' => 'CORE',
|
||||
'assignee_name' => 'Sophie de Vries',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+5 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-105',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-107',
|
||||
'summary' => 'Exporteer wekelijkse team rapportage naar PDF en Excel spreadsheet',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'Low',
|
||||
'issue_type' => 'Feature',
|
||||
'project' => 'Reporting',
|
||||
'project_key' => 'REP',
|
||||
'assignee_name' => 'Thomas Jansen',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+10 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-107',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-110',
|
||||
'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Realtime Engine',
|
||||
'project_key' => 'RT',
|
||||
'assignee_name' => 'Bram Meijer',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+6 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-110',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-114',
|
||||
'summary' => 'Datum picker springt terug naar 1970 bij lege invoer',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'UI/UX Redesign',
|
||||
'project_key' => 'UI',
|
||||
'assignee_name' => 'Niet toegewezen',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => null,
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-114',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-89',
|
||||
'summary' => 'GDPR / AVG compliancy en geautomatiseerd data retentiebeleid',
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Epic',
|
||||
'project' => 'Security',
|
||||
'project_key' => 'SEC',
|
||||
'assignee_name' => 'Lisa Bakker',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+14 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-89',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-82',
|
||||
'summary' => 'Performance optimalisatie van vector avatar rendering in navigatiebalk',
|
||||
'status' => 'Done',
|
||||
'status_category' => 'done',
|
||||
'priority' => 'Low',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'UI/UX Redesign',
|
||||
'project_key' => 'UI',
|
||||
'assignee_name' => 'Sophie de Vries',
|
||||
'assignee_avatar' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('-3 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-82',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'is_demo' => true,
|
||||
'success' => true,
|
||||
'domain' => 'sandbox.atlassian.net',
|
||||
'email' => 'demo@fociz.nl',
|
||||
'account_name' => 'Demo Account',
|
||||
'last_sync_at' => date('Y-m-d H:i:s'),
|
||||
'assigned_issues' => $assigned,
|
||||
'team_issues' => $team,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -288,56 +506,81 @@ function jira_get_user_issues(int $userId, bool $forceRefresh = false): array {
|
||||
$domain = !empty($settings['jira_domain']) ? $settings['jira_domain'] : jira_get_company_domain();
|
||||
$email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', '');
|
||||
$apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', '');
|
||||
$accName = $settings['account_name'] ?? 'Atlassian Gebruiker';
|
||||
|
||||
if (empty($domain) || empty($email) || empty($apiToken)) {
|
||||
return [
|
||||
'is_demo' => false,
|
||||
'success' => false,
|
||||
'error' => 'Jira configuratie is onvolledig.',
|
||||
'issues' => [],
|
||||
'error' => 'Jira configuratie is onvolledig. Controleer je instellingen.',
|
||||
'assigned_issues' => [],
|
||||
'team_issues' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Sessiecache voor 90 seconden om onnodige Jira API calls bij snelle pagina refreshes te beperken
|
||||
// Sessiecache voor 60 seconden
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
@session_start();
|
||||
}
|
||||
$cacheKey = "jira_issues_user_{$userId}";
|
||||
$cacheKey = "jira_dashboard_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,
|
||||
];
|
||||
if (!$forceRefresh && $cached && is_array($cached) && ($cached['timestamp'] ?? 0) > (time() - 60)) {
|
||||
return array_merge($cached['data'], ['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'],
|
||||
];
|
||||
}
|
||||
// Ophalen van toegewezen taken
|
||||
$assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 50);
|
||||
// Ophalen van teamtaken
|
||||
$teamRes = jira_fetch_team_issues($domain, $email, $apiToken, 50);
|
||||
|
||||
if (!$assignedRes['success'] && !$teamRes['success']) {
|
||||
return [
|
||||
'is_demo' => false,
|
||||
'success' => false,
|
||||
'error' => $result['error'],
|
||||
'issues' => [],
|
||||
'error' => $assignedRes['error'] ?? 'Kan Jira tickets niet ophalen.',
|
||||
'assigned_issues' => [],
|
||||
'team_issues' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$assignedIssues = $assignedRes['success'] ? $assignedRes['issues'] : [];
|
||||
$teamIssues = $teamRes['success'] ? $teamRes['issues'] : [];
|
||||
|
||||
$data = [
|
||||
'is_demo' => false,
|
||||
'success' => true,
|
||||
'domain' => $domain,
|
||||
'email' => $email,
|
||||
'account_name' => $accName,
|
||||
'last_sync_at' => date('Y-m-d H:i:s'),
|
||||
'assigned_issues' => $assignedIssues,
|
||||
'team_issues' => $teamIssues,
|
||||
];
|
||||
|
||||
$_SESSION[$cacheKey] = [
|
||||
'timestamp' => time(),
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
// Werk last_sync_at bij in database
|
||||
$stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?");
|
||||
$stmtUpdate->execute([$integration['id']]);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de Jira tickets op voor een Fociz gebruiker (backwards compatibility)
|
||||
*/
|
||||
function jira_get_user_issues(int $userId, bool $forceRefresh = false): array {
|
||||
$data = jira_get_dashboard_data($userId, $forceRefresh);
|
||||
return [
|
||||
'is_demo' => $data['is_demo'] ?? false,
|
||||
'success' => $data['success'] ?? false,
|
||||
'error' => $data['error'] ?? null,
|
||||
'issues' => $data['assigned_issues'] ?? [],
|
||||
'from_cache' => $data['from_cache'] ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -351,6 +594,8 @@ function jira_disconnect(int $userId): bool {
|
||||
@session_start();
|
||||
}
|
||||
unset($_SESSION["jira_issues_user_{$userId}"]);
|
||||
unset($_SESSION["jira_dashboard_user_{$userId}"]);
|
||||
return $stmt->execute([$userId]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+190
-1
@@ -185,6 +185,86 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Standaard avatar kiezen uit bibliotheek
|
||||
elseif ($action === 'select_preset_avatar') {
|
||||
$preset = trim($_POST['avatar_preset'] ?? '');
|
||||
if (preg_match('/^\/assets\/avatars\/avatar-(0[1-9]|1[0-9]|2[0-4])\.svg$/', $preset)) {
|
||||
$stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
|
||||
$stmtUp->execute([$preset, $userId]);
|
||||
$_SESSION['user_avatar_url'] = $preset;
|
||||
$user['avatar_url'] = $preset;
|
||||
$message = "Uw avatar is succesvol bijgewerkt!";
|
||||
$messageType = 'success';
|
||||
} else {
|
||||
$message = "Ongeldige avatar geselecteerd.";
|
||||
$messageType = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Eigen profielfoto uploaden
|
||||
elseif ($action === 'upload_custom_avatar') {
|
||||
if (!isset($_FILES['avatar_file']) || $_FILES['avatar_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
$message = "Selecteer een geldige afbeelding om te uploaden.";
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
$file = $_FILES['avatar_file'];
|
||||
$maxSize = 4 * 1024 * 1024; // 4MB
|
||||
if ($file['size'] > $maxSize) {
|
||||
$message = "De afbeelding is te groot (maximaal 4MB toegestaan).";
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mime, $allowedTypes)) {
|
||||
$message = "Alleen JPG, PNG, WebP of GIF afbeeldingen zijn toegestaan.";
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
$ext = match($mime) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
default => 'jpg'
|
||||
};
|
||||
|
||||
$uploadDir = __DIR__ . '/assets/uploads/avatars';
|
||||
if (!is_dir($uploadDir)) {
|
||||
@mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$filename = "avatar_{$userId}_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
|
||||
$targetPath = $uploadDir . '/' . $filename;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||
$webPath = "/assets/uploads/avatars/{$filename}";
|
||||
$stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
|
||||
$stmtUp->execute([$webPath, $userId]);
|
||||
$_SESSION['user_avatar_url'] = $webPath;
|
||||
$user['avatar_url'] = $webPath;
|
||||
$message = "Uw profielfoto is succesvol geüpload en ingesteld!";
|
||||
$messageType = 'success';
|
||||
} else {
|
||||
$message = "Er is een fout opgetreden bij het verplaatsen van het geüploade bestand.";
|
||||
$messageType = 'error';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Avatar verwijderen
|
||||
elseif ($action === 'remove_avatar') {
|
||||
$stmtUp = $db->prepare("UPDATE users SET avatar_url = NULL WHERE id = ?");
|
||||
$stmtUp->execute([$userId]);
|
||||
unset($_SESSION['user_avatar_url']);
|
||||
$user['avatar_url'] = null;
|
||||
$message = "Uw avatar is teruggezet naar de standaard weergave met uw initialen.";
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
// Actuele gegevens ophalen
|
||||
@@ -228,10 +308,14 @@ require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
<div class="relative z-10 flex flex-col sm:flex-row sm:items-center justify-between gap-6">
|
||||
<div class="flex items-center space-x-4 sm:space-x-5">
|
||||
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 shadow-sm flex-shrink-0">
|
||||
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 shadow-sm flex-shrink-0 overflow-hidden">
|
||||
<?php if (!empty($userProfile['avatar_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($userProfile['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-2xl object-cover bg-white dark:bg-[#0c0f17]">
|
||||
<?php else: ?>
|
||||
<div class="w-full h-full rounded-2xl bg-white dark:bg-[#0c0f17] flex items-center justify-center font-bold text-2xl sm:text-3xl text-indigo-600 dark:text-indigo-400 uppercase">
|
||||
<?= strtoupper(substr($userProfile['name'], 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
@@ -419,6 +503,100 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sectie 0b: Profielfoto & Avatar Beheer -->
|
||||
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<div class="flex items-center space-x-3.5">
|
||||
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-indigo-500 to-pink-500 p-0.5 shadow-xs flex-shrink-0 flex items-center justify-center text-white">
|
||||
<i data-lucide="smile" class="w-6 h-6"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">Profielfoto & Avatar</h2>
|
||||
<span class="px-2.5 py-0.5 rounded-full <?= !empty($userProfile['avatar_url']) ? 'bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/20 text-teal-700 dark:text-teal-400' : 'bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400' ?> text-[10px] font-semibold">
|
||||
<?= !empty($userProfile['avatar_url']) ? 'Aangepaste Avatar Actief' : 'Standaard Initiaal-Badge' ?>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Kies een mini-avatar uit de 24 stijlvolle karakters of upload een eigen afbeelding.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($userProfile['avatar_url'])): ?>
|
||||
<form method="POST" action="/profile.php" class="self-start sm:self-auto">
|
||||
<input type="hidden" name="action" value="remove_avatar">
|
||||
<button type="submit" onclick="return confirm('Avatar herstellen naar standaard initiaal-badge?')" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800/50 hover:bg-rose-100 dark:hover:bg-rose-900/50 transition cursor-pointer">
|
||||
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
|
||||
<span>Standaard Herstellen</span>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
<!-- Linker kolom: 24 Standaard Preset Avatars (8 cols) -->
|
||||
<div class="lg:col-span-8 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
|
||||
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||||
<span>Kies een Karakter (24 Mini Avatars)</span>
|
||||
</h3>
|
||||
<span class="text-[11px] text-slate-400 dark:text-slate-500">Klik om direct toe te passen</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-3 p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
||||
<?php
|
||||
for ($i = 1; $i <= 24; $i++):
|
||||
$numStr = sprintf("%02d", $i);
|
||||
$avatarPath = "/assets/avatars/avatar-{$numStr}.svg";
|
||||
$isSelected = ($userProfile['avatar_url'] === $avatarPath);
|
||||
?>
|
||||
<form method="POST" action="/profile.php" class="relative group">
|
||||
<input type="hidden" name="action" value="select_preset_avatar">
|
||||
<input type="hidden" name="avatar_preset" value="<?= $avatarPath ?>">
|
||||
<button type="submit" class="w-full aspect-square rounded-2xl p-1 transition transform hover:scale-105 active:scale-95 focus:outline-none cursor-pointer <?= $isSelected ? 'ring-2 ring-indigo-600 dark:ring-indigo-400 bg-indigo-50 dark:bg-indigo-950/40 shadow-sm' : 'hover:bg-white dark:hover:bg-slate-800 border border-transparent' ?>" title="Kies Avatar <?= $numStr ?>">
|
||||
<img src="<?= $avatarPath ?>" alt="Avatar <?= $numStr ?>" class="w-full h-full rounded-xl object-contain pointer-events-none">
|
||||
<?php if ($isSelected): ?>
|
||||
<span class="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-indigo-600 text-white flex items-center justify-center shadow-xs border-2 border-white dark:border-[#0c0f17]">
|
||||
<i data-lucide="check" class="w-3 h-3 stroke-[3]"></i>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechter kolom: Eigen Afbeelding Uploaden (4 cols) -->
|
||||
<div class="lg:col-span-4 space-y-3">
|
||||
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
|
||||
<i data-lucide="upload-cloud" class="w-3.5 h-3.5 text-sky-500"></i>
|
||||
<span>Eigen Foto Uploaden</span>
|
||||
</h3>
|
||||
|
||||
<form method="POST" action="/profile.php" enctype="multipart/form-data" class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-4">
|
||||
<input type="hidden" name="action" value="upload_custom_avatar">
|
||||
|
||||
<div class="text-center p-5 border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-xl hover:border-indigo-500 dark:hover:border-indigo-400 transition bg-white dark:bg-[#111622]">
|
||||
<div class="w-10 h-10 mx-auto mb-2 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||||
<i data-lucide="image" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<label for="customAvatarFile" class="block text-xs font-semibold text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer mb-1">
|
||||
Klik om bestand te kiezen
|
||||
</label>
|
||||
<p class="text-[10px] text-slate-500 dark:text-slate-400">JPG, PNG, WebP of GIF (max. 4MB)</p>
|
||||
<input type="file" id="customAvatarFile" name="avatar_file" accept="image/png, image/jpeg, image/webp, image/gif" required class="hidden" onchange="previewUploadAvatar(this)">
|
||||
<div id="uploadFileName" class="mt-2 text-[11px] font-mono text-slate-700 dark:text-slate-300 truncate hidden"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="uploadAvatarBtn" class="w-full py-2.5 px-4 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
|
||||
<i data-lucide="upload" class="w-4 h-4"></i>
|
||||
<span>Foto Uploaden & Opslaan</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sectie 1: Mijn Microsoft 365 Outlook Agenda -->
|
||||
<div class="fociz-card p-6 sm:p-7">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
|
||||
@@ -911,6 +1089,17 @@ function toggleJiraToken() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function previewUploadAvatar(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
const nameEl = document.getElementById('uploadFileName');
|
||||
if (nameEl) {
|
||||
nameEl.textContent = '📄 ' + file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
|
||||
nameEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync banner status met browser permissie & activeer actuele thema kaart
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialiseer geselecteerde thema-kaart
|
||||
|
||||
+5
-5
@@ -690,11 +690,11 @@ function renderTimeline() {
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 flex-shrink-0">
|
||||
${dateBadge}
|
||||
${card.assignee_name ? `
|
||||
<div class="w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center flex-shrink-0" title="${escapeHtml(card.assignee_name)}">
|
||||
${card.assignee_name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
` : ''}
|
||||
${card.assignee_name ? (
|
||||
card.assignee_avatar
|
||||
? `<img src="${escapeHtml(card.assignee_avatar)}" class="w-5 h-5 rounded-full object-cover flex-shrink-0 bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-800/60" title="${escapeHtml(card.assignee_name)}">`
|
||||
: `<div class="w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center flex-shrink-0" title="${escapeHtml(card.assignee_name)}">${card.assignee_name.charAt(0).toUpperCase()}</div>`
|
||||
) : ''}
|
||||
</div>
|
||||
`;
|
||||
workContainer.appendChild(leftItem);
|
||||
|
||||
@@ -205,8 +205,21 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<?php foreach ($users as $u): ?>
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition">
|
||||
<td class="p-3">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 overflow-hidden shadow-2xs">
|
||||
<?php if (!empty($u['avatar_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($u['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[10px] object-cover bg-white dark:bg-slate-900">
|
||||
<?php else: ?>
|
||||
<div class="w-full h-full rounded-[10px] bg-white dark:bg-slate-900 flex items-center justify-center font-bold text-xs text-indigo-600 dark:text-indigo-400 uppercase">
|
||||
<?= strtoupper(substr($u['name'], 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900 dark:text-white"><?= htmlspecialchars($u['name']) ?></div>
|
||||
<div class="text-[11px] text-slate-500 dark:text-slate-400 font-mono"><?= htmlspecialchars($u['email']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-bold <?= $u['role'] === 'ADMIN' ? 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300' ?>">
|
||||
|
||||
Reference in New Issue
Block a user