Merge pull request 'Dev' (#34) from dev into main
Reviewed-on: #34
@@ -93,6 +93,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$messageType = 'success';
|
||||
}
|
||||
|
||||
// 2b. Jira Wachtrijfilter toevoegen of bewerken
|
||||
elseif ($action === 'save_jira_queue_filter') {
|
||||
$filterId = (int)($_POST['filter_id'] ?? 0);
|
||||
$filterCode = strtoupper(trim($_POST['filter_code'] ?? ''));
|
||||
$filterName = trim($_POST['filter_name'] ?? '');
|
||||
$projectKey = strtoupper(trim($_POST['project_key'] ?? ''));
|
||||
$jqlClause = trim($_POST['jql_clause'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
$sortOrder = (int)($_POST['sort_order'] ?? 0);
|
||||
|
||||
if (empty($filterCode) || empty($filterName) || empty($jqlClause)) {
|
||||
$message = "Filtercode, naam en JQL-clausule zijn verplicht.";
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
if ($filterId > 0) {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE jira_queue_filters
|
||||
SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $filterId]);
|
||||
$message = "Wachtrijfilter '{$filterName}' is succesvol bijgewerkt.";
|
||||
} else {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
filter_name = VALUES(filter_name),
|
||||
project_key = VALUES(project_key),
|
||||
jql_clause = VALUES(jql_clause),
|
||||
description = VALUES(description),
|
||||
is_active = VALUES(is_active),
|
||||
sort_order = VALUES(sort_order),
|
||||
updated_at = NOW()
|
||||
");
|
||||
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
|
||||
$message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd.";
|
||||
}
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
// 2c. Jira Wachtrijfilter verwijderen
|
||||
elseif ($action === 'delete_jira_queue_filter') {
|
||||
$filterId = (int)($_POST['filter_id'] ?? 0);
|
||||
if ($filterId > 0) {
|
||||
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
|
||||
$stmt->execute([$filterId]);
|
||||
$message = "Wachtrijfilter is succesvol verwijderd.";
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
// 2d. Teamleden toewijzen aan Jira Wachtrijfilter
|
||||
elseif ($action === 'save_user_jira_assignments') {
|
||||
$assignments = $_POST['user_filter'] ?? [];
|
||||
if (is_array($assignments)) {
|
||||
$upd = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?");
|
||||
foreach ($assignments as $uId => $fCode) {
|
||||
$fCode = trim($fCode);
|
||||
$upd->execute([$fCode !== '' ? $fCode : null, (int)$uId]);
|
||||
}
|
||||
$message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen.";
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Organisatie Werkdag Standaarden opslaan (Release v1.4)
|
||||
elseif ($action === 'save_workday_settings') {
|
||||
$workdayStart = trim($_POST['workday_start'] ?? '08:30');
|
||||
@@ -215,6 +283,15 @@ try {
|
||||
error_log("Fout bij ophalen recente gebruikers: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$allUsersForJira = [];
|
||||
$allJiraFilters = [];
|
||||
try {
|
||||
$allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code FROM users ORDER BY name ASC")->fetchAll() ?: [];
|
||||
$allJiraFilters = $db->query("SELECT * FROM jira_queue_filters ORDER BY sort_order ASC, id ASC")->fetchAll() ?: [];
|
||||
} catch (Throwable $e) {
|
||||
error_log("Fout bij ophalen jira filters/users: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$pageTitle = 'Systeembeheer & Integraties — Fociz';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
@@ -497,6 +574,286 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 2b. Atlassian Jira Wachtrijfilters & Gebruikerskoppelingen -->
|
||||
<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-xl bg-gradient-to-tr from-sky-500 to-indigo-600 text-white flex items-center justify-center font-bold shadow-md shadow-indigo-500/20 flex-shrink-0">
|
||||
<i data-lucide="sliders" class="w-6 h-6"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Jira Wachtrijfilters & Toewijzingen</h2>
|
||||
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-400 text-[10px] font-bold">
|
||||
ServiceDesk & Teams
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
Beheer aangepaste JQL-wachtrijfilters (zoals DX en SD) en wijs teamleden toe aan hun standaard wachtrij.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="openJiraFilterModal()" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer self-start sm:self-center">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
<span>Nieuw Wachtrijfilter</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Overzicht van actieve wachtrijfilters -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-1.5">
|
||||
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||||
<span>Geconfigureerde Wachtrijfilters (<?= count($allJiraFilters) ?>)</span>
|
||||
</h3>
|
||||
<span class="text-[11px] text-slate-400">Worden getoond als snelle filtertabs op de Jira pagina</span>
|
||||
</div>
|
||||
|
||||
<?php if (empty($allJiraFilters)): ?>
|
||||
<div class="p-6 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-center">
|
||||
<p class="text-xs text-slate-500">Nog geen wachtrijfilters ingesteld. Klik op "Nieuw Wachtrijfilter" om te beginnen.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$userNamesByFilter = [];
|
||||
foreach ($allUsersForJira as $u) {
|
||||
if (!empty($u['default_jira_filter_code'])) {
|
||||
$userNamesByFilter[$u['default_jira_filter_code']][] = $u['name'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="grid grid-cols-1 gap-3.5">
|
||||
<?php foreach ($allJiraFilters as $filter): ?>
|
||||
<?php
|
||||
$assignedColleagues = $userNamesByFilter[$filter['filter_code']] ?? [];
|
||||
$filterJsonSafe = htmlspecialchars(json_encode($filter), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 hover:border-indigo-500/40 transition flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="space-y-2 flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-xs font-black px-2.5 py-0.5 rounded-lg bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80">
|
||||
<?= htmlspecialchars($filter['filter_code']) ?>
|
||||
</span>
|
||||
<span class="text-xs font-bold text-slate-900 dark:text-white">
|
||||
<?= htmlspecialchars($filter['filter_name']) ?>
|
||||
</span>
|
||||
<?php if (!empty($filter['project_key'])): ?>
|
||||
<span class="text-[11px] font-medium px-2 py-0.5 rounded-md bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
||||
Project: <?= htmlspecialchars($filter['project_key']) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-[11px] font-medium px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800/60 text-slate-500">
|
||||
Alle projecten
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($filter['is_active'])): ?>
|
||||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-50 text-rose-700 dark:bg-rose-950/40 dark:text-rose-400">
|
||||
Inactief
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($filter['description'])): ?>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400">
|
||||
<?= htmlspecialchars($filter['description']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- JQL preview box -->
|
||||
<div class="p-2.5 rounded-xl bg-white dark:bg-[#111622] border border-slate-200/90 dark:border-slate-800 text-[11px] font-mono text-slate-700 dark:text-indigo-300 break-all select-all">
|
||||
<span class="text-slate-400 dark:text-slate-500 mr-1.5 font-sans font-bold">JQL:</span><?= htmlspecialchars($filter['jql_clause']) ?>
|
||||
</div>
|
||||
|
||||
<!-- Toegewezen collega's -->
|
||||
<div class="flex items-center space-x-2 text-[11px] text-slate-500 dark:text-slate-400 pt-0.5">
|
||||
<span class="font-semibold text-slate-700 dark:text-slate-300">Standaard gekoppeld aan:</span>
|
||||
<?php if (!empty($assignedColleagues)): ?>
|
||||
<span class="inline-flex items-center gap-1.5 flex-wrap">
|
||||
<?php foreach ($assignedColleagues as $cName): ?>
|
||||
<span class="px-2 py-0.5 rounded-md bg-sky-50 text-sky-700 dark:bg-sky-950/50 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60 font-medium">
|
||||
<?= htmlspecialchars($cName) ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="italic text-slate-400">Nog niemand specifiek toegewezen</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actieknoppen -->
|
||||
<div class="flex items-center space-x-2 self-start md:self-center flex-shrink-0 pt-2 md:pt-0 border-t md:border-t-0 border-slate-200 dark:border-slate-800 w-full md:w-auto justify-end">
|
||||
<button type="button" onclick="editJiraFilter(<?= $filterJsonSafe ?>)" class="px-3 py-1.5 rounded-xl bg-slate-200/80 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 text-xs font-semibold transition cursor-pointer">
|
||||
Bewerken
|
||||
</button>
|
||||
<form method="POST" action="/admin.php" onsubmit="return confirm('Weet u zeker dat u het filter \'<?= htmlspecialchars(addslashes($filter['filter_name'])) ?>\' wilt verwijderen?');" class="inline">
|
||||
<input type="hidden" name="action" value="delete_jira_queue_filter">
|
||||
<input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>">
|
||||
<button type="submit" class="p-2 rounded-xl text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer" title="Verwijderen">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Team Wachtrijmatrix: Standaard Wachtrij per Teamlid -->
|
||||
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/80 space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-1.5">
|
||||
<i data-lucide="users" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||||
<span>Team Wachtrijmatrix: Standaard Wachtrij per Collega</span>
|
||||
</h3>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
Wijs collega's toe aan hun primaire wachtrij (bijv. DX of SD). Wanneer zij de Jira pagina openen, wordt deze wachtrij direct als standaard getoond.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/admin.php" class="space-y-4">
|
||||
<input type="hidden" name="action" value="save_user_jira_assignments">
|
||||
|
||||
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100 dark:bg-[#0c0f17] text-slate-600 dark:text-slate-400 font-bold border-b border-slate-200 dark:border-slate-800">
|
||||
<tr>
|
||||
<th class="py-3 px-4">Teamlid</th>
|
||||
<th class="py-3 px-4">E-mailadres</th>
|
||||
<th class="py-3 px-4">Rol</th>
|
||||
<th class="py-3 px-4 w-72">Standaard Wachtrijfilter</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/80 bg-white dark:bg-[#111622]">
|
||||
<?php foreach ($allUsersForJira as $usr): ?>
|
||||
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition">
|
||||
<td class="py-3 px-4">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<?php if (!empty($usr['avatar_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($usr['avatar_url']) ?>" alt="Avatar" class="w-7 h-7 rounded-full object-cover border border-slate-200 dark:border-slate-700">
|
||||
<?php else: ?>
|
||||
<div class="w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-[10px]">
|
||||
<?= strtoupper(substr($usr['name'], 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<span class="font-bold text-slate-900 dark:text-white"><?= htmlspecialchars($usr['name']) ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 font-mono text-slate-600 dark:text-slate-400">
|
||||
<?= htmlspecialchars($usr['email']) ?>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $usr['role'] === 'ADMIN' ? 'bg-purple-100 text-purple-800 dark:bg-purple-950/60 dark:text-purple-300' : 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300' ?>">
|
||||
<?= $usr['role'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<select name="user_filter[<?= (int)$usr['id'] ?>]" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
|
||||
<option value="">Geen voorkeur (alles tonen)</option>
|
||||
<?php foreach ($allJiraFilters as $f): ?>
|
||||
<?php if ($f['is_active']): ?>
|
||||
<option value="<?= htmlspecialchars($f['filter_code']) ?>" <?= ($usr['default_jira_filter_code'] === $f['filter_code']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($f['filter_code']) ?> — <?= htmlspecialchars($f['filter_name']) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer">
|
||||
Wachtrijtoewijzingen Opslaan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Wachtrijfilter Toevoegen / Bewerken -->
|
||||
<div id="jiraFilterModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl p-6 transition transform">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-9 h-9 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
||||
<i data-lucide="filter" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 id="jiraFilterModalTitle" class="text-sm font-bold text-slate-900 dark:text-white">Wachtrijfilter Configureren</h3>
|
||||
<p class="text-[11px] text-slate-400">Beheer JQL zoekclausule en koppeling</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="closeJiraFilterModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/admin.php" class="mt-4 space-y-4">
|
||||
<input type="hidden" name="action" value="save_jira_queue_filter">
|
||||
<input type="hidden" id="filterModal_id" name="filter_id" value="0">
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
||||
Filter Code <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="filterModal_code" name="filter_code" required placeholder="bijv. DX of SD" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
||||
Doel Project (optioneel)
|
||||
</label>
|
||||
<input type="text" id="filterModal_project" name="project_key" placeholder="bijv. SD of SERVICEDESK" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
||||
Filter Naam <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="filterModal_name" name="filter_name" required placeholder="bijv. DX — Koppelingen, SSO & Migratie" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
||||
JQL Clausule <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea id="filterModal_jql" name="jql_clause" rows="3" required placeholder='bijv. "Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)' class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500"></textarea>
|
||||
<span class="text-[10px] text-slate-400">Voer de exacte JQL filterclausule in die in Jira op de issues wordt toegepast.</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
||||
Omschrijving / Toelichting
|
||||
</label>
|
||||
<input type="text" id="filterModal_desc" name="description" placeholder="Korte toelichting voor het team" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<label class="flex items-center space-x-2 text-xs text-slate-700 dark:text-slate-300 cursor-pointer">
|
||||
<input type="checkbox" id="filterModal_active" name="is_active" value="1" checked class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
||||
<span>Filter is actief</span>
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="button" onclick="closeJiraFilterModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
Annuleren
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition cursor-pointer shadow-xs">
|
||||
Filter Opslaan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Organisatie Werkdag Standaarden (Release v1.4) -->
|
||||
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800/80">
|
||||
@@ -860,6 +1217,49 @@ function toggleAdminGuide() {
|
||||
arrow.classList.toggle('rotate-180');
|
||||
}
|
||||
}
|
||||
|
||||
// Jira Wachtrijfilter Modalfuncties
|
||||
function openJiraFilterModal() {
|
||||
document.getElementById('jiraFilterModalTitle').innerText = 'Nieuw Wachtrijfilter Toevoegen';
|
||||
document.getElementById('filterModal_id').value = '0';
|
||||
document.getElementById('filterModal_code').value = '';
|
||||
document.getElementById('filterModal_name').value = '';
|
||||
document.getElementById('filterModal_project').value = 'SD';
|
||||
document.getElementById('filterModal_jql').value = '';
|
||||
document.getElementById('filterModal_desc').value = '';
|
||||
document.getElementById('filterModal_active').checked = true;
|
||||
|
||||
const modal = document.getElementById('jiraFilterModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function editJiraFilter(filter) {
|
||||
document.getElementById('jiraFilterModalTitle').innerText = `Wachtrijfilter '${filter.filter_name}' Bewerken`;
|
||||
document.getElementById('filterModal_id').value = filter.id || '0';
|
||||
document.getElementById('filterModal_code').value = filter.filter_code || '';
|
||||
document.getElementById('filterModal_name').value = filter.filter_name || '';
|
||||
document.getElementById('filterModal_project').value = filter.project_key || '';
|
||||
document.getElementById('filterModal_jql').value = filter.jql_clause || '';
|
||||
document.getElementById('filterModal_desc').value = filter.description || '';
|
||||
document.getElementById('filterModal_active').checked = (filter.is_active != 0);
|
||||
|
||||
const modal = document.getElementById('jiraFilterModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeJiraFilterModal() {
|
||||
const modal = document.getElementById('jiraFilterModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeJiraFilterModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
@@ -272,6 +272,107 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5a. Jira Wachtrijfilters Ophalen
|
||||
if ($action === 'get_jira_queue_filters') {
|
||||
$onlyActive = !isset($_GET['all']) && !isset($_POST['all']);
|
||||
$filters = jira_get_queue_filters($onlyActive);
|
||||
echo json_encode(['success' => true, 'filters' => $filters]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5b. Jira Wachtrijfilter Opslaan / Wijzigen (Alleen Beheerders)
|
||||
if ($action === 'save_jira_queue_filter') {
|
||||
if (($user['role'] ?? '') !== 'ADMIN') {
|
||||
echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters aanpassen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
$filterCode = strtoupper(trim($input['filter_code'] ?? ''));
|
||||
$filterName = trim($input['filter_name'] ?? '');
|
||||
$projectKey = strtoupper(trim($input['project_key'] ?? ''));
|
||||
$jqlClause = trim($input['jql_clause'] ?? '');
|
||||
$description = trim($input['description'] ?? '');
|
||||
$isActive = isset($input['is_active']) ? (int)$input['is_active'] : 1;
|
||||
$sortOrder = (int)($input['sort_order'] ?? 0);
|
||||
|
||||
if (empty($filterCode) || empty($filterName) || empty($jqlClause)) {
|
||||
echo json_encode(['error' => 'Code, naam en JQL-clausule zijn verplicht.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE jira_queue_filters
|
||||
SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $id]);
|
||||
} else {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
filter_name = VALUES(filter_name),
|
||||
project_key = VALUES(project_key),
|
||||
jql_clause = VALUES(jql_clause),
|
||||
description = VALUES(description),
|
||||
is_active = VALUES(is_active),
|
||||
sort_order = VALUES(sort_order),
|
||||
updated_at = NOW()
|
||||
");
|
||||
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => "Wachtrijfilter '{$filterName}' is succesvol opgeslagen."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5c. Jira Wachtrijfilter Verwijderen (Alleen Beheerders)
|
||||
if ($action === 'delete_jira_queue_filter') {
|
||||
if (($user['role'] ?? '') !== 'ADMIN') {
|
||||
echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters verwijderen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
$filterCode = trim($input['filter_code'] ?? '');
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
} elseif (!empty($filterCode)) {
|
||||
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE filter_code = ?");
|
||||
$stmt->execute([$filterCode]);
|
||||
} else {
|
||||
echo json_encode(['error' => 'Filter ID of code ontbreekt.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Wachtrijfilter is verwijderd.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5d. Standaard Wachtrijfilter Toewijzen aan Gebruiker (Beheerders of Gebruiker zelf)
|
||||
if ($action === 'set_user_jira_filter') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$targetUserId = (int)($input['user_id'] ?? $user['id']);
|
||||
$filterCode = trim($input['filter_code'] ?? '');
|
||||
|
||||
if ($targetUserId !== (int)$user['id'] && ($user['role'] ?? '') !== 'ADMIN') {
|
||||
echo json_encode(['error' => 'Geen toestemming om instellingen van andere gebruikers te wijzigen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?");
|
||||
$stmt->execute([$filterCode ?: null, $targetUserId]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Wachtrijvoorkeur is succesvol bijgewerkt.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['error' => 'Onbekende actie']);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Live Sync & Realtime Notificatie Hub
|
||||
* Controleert continu op nieuwe Outlook e-mails, relevante Jira tickets en agenda updates.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
require_once __DIR__ . '/../includes/integrations/jira.php';
|
||||
|
||||
$user = require_auth();
|
||||
$userId = (int)$user['id'];
|
||||
$db = get_db_connection();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$nowTs = time();
|
||||
$nowIso = date('Y-m-d H:i:s');
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$clientLastMailId = trim($_GET['last_mail_id'] ?? '');
|
||||
$clientLastJiraKey = trim($_GET['last_jira_key'] ?? '');
|
||||
$clientCalendarHash = trim($_GET['calendar_hash'] ?? '');
|
||||
|
||||
// 1. Outlook E-mail Controle
|
||||
$mailRes = microsoft_fetch_inbox_messages($userId, 5);
|
||||
$messages = $mailRes['messages'] ?? [];
|
||||
$unreadCount = (int)($mailRes['unread_count'] ?? 0);
|
||||
$latestMail = $messages[0] ?? null;
|
||||
|
||||
$hasNewMail = false;
|
||||
$newMailPayload = null;
|
||||
|
||||
if ($latestMail && !empty($latestMail['id'])) {
|
||||
// Als de client een ander ID heeft dan de allernieuwste mail en het bericht is ongelezen
|
||||
if (!empty($clientLastMailId) && $latestMail['id'] !== $clientLastMailId && empty($latestMail['is_read'])) {
|
||||
$hasNewMail = true;
|
||||
$newMailPayload = [
|
||||
'id' => $latestMail['id'],
|
||||
'subject' => $latestMail['subject'],
|
||||
'from_name' => $latestMail['from_name'],
|
||||
'from_email' => $latestMail['from_email'],
|
||||
'received_human' => $latestMail['received_human'],
|
||||
'web_link' => $latestMail['web_link'] ?? '/mail.php',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Jira Tickets Controle (rekening houdend met persoonlijke toewijzing & default queue filter zoals DX of SD)
|
||||
$userFilterCode = $user['default_jira_filter_code'] ?? '';
|
||||
$jiraData = jira_get_user_issues($userId);
|
||||
$assignedIssues = $jiraData['assigned_issues'] ?? ($jiraData['issues'] ?? []);
|
||||
$teamIssues = $jiraData['team_issues'] ?? [];
|
||||
|
||||
$dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
|
||||
$relevantIssues = [];
|
||||
|
||||
// Filter issues die specifiek relevant zijn voor deze medewerker
|
||||
foreach (array_merge($assignedIssues, $teamIssues) as $iss) {
|
||||
$isAssignedToMe = !empty($iss['assignee_name']) && (stripos($iss['assignee_name'], $user['name']) !== false || stripos($iss['assignee_name'], 'Niek') !== false);
|
||||
$ol = strtolower(trim($iss['onderwerp_logging'] ?? ''));
|
||||
|
||||
$isQueueMatch = false;
|
||||
if (!empty($userFilterCode)) {
|
||||
if ($userFilterCode === 'DX' && in_array($ol, $dxValues)) {
|
||||
$isQueueMatch = true;
|
||||
} elseif ($userFilterCode === 'SD' && ($ol === '' || $ol === 'empty' || !in_array($ol, $dxValues))) {
|
||||
$isQueueMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isAssignedToMe || $isQueueMatch) {
|
||||
$relevantIssues[$iss['key']] = $iss;
|
||||
}
|
||||
}
|
||||
|
||||
$hasNewJira = false;
|
||||
$newJiraPayload = [];
|
||||
$newestJiraKey = '';
|
||||
|
||||
if (!empty($relevantIssues)) {
|
||||
// Pak het nieuwste ticket
|
||||
$newest = reset($relevantIssues);
|
||||
$newestJiraKey = $newest['key'];
|
||||
|
||||
if (!empty($clientLastJiraKey) && $newestJiraKey !== $clientLastJiraKey) {
|
||||
$hasNewJira = true;
|
||||
$newJiraPayload = [
|
||||
'key' => $newest['key'],
|
||||
'summary' => $newest['summary'],
|
||||
'priority' => $newest['priority'] ?? 'Medium',
|
||||
'issue_type' => $newest['issue_type'] ?? 'Taak',
|
||||
'project' => $newest['project'] ?? 'SD',
|
||||
'url' => $newest['url'] ?? ('/jira.php?highlight=' . $newest['key']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Outlook Agenda Controle (Tijdlijn events van vandaag)
|
||||
$calendarChanged = false;
|
||||
$todayEvents = [];
|
||||
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||
$stmtInt->execute([$userId]);
|
||||
$outlookInt = $stmtInt->fetch();
|
||||
|
||||
if ($outlookInt && !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode'])) {
|
||||
$todayEvents = microsoft_fetch_today_events($userId, $today);
|
||||
}
|
||||
|
||||
$calHash = md5(json_encode(array_map(function($e) {
|
||||
return ($e['id'] ?? '') . ($e['start'] ?? '') . ($e['end'] ?? '') . ($e['subject'] ?? '');
|
||||
}, $todayEvents)));
|
||||
|
||||
if (!empty($clientCalendarHash) && $calHash !== $clientCalendarHash) {
|
||||
$calendarChanged = true;
|
||||
}
|
||||
|
||||
// 4. Update sync timestamps in database
|
||||
$stmtUpdate = $db->prepare("UPDATE users SET last_mail_check_at = NOW(), last_jira_check_at = NOW() WHERE id = ?");
|
||||
$stmtUpdate->execute([$userId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'server_time' => date('H:i:s'),
|
||||
'mail' => [
|
||||
'has_new' => $hasNewMail,
|
||||
'unread_count' => $unreadCount,
|
||||
'latest_id' => $latestMail['id'] ?? '',
|
||||
'new_mail' => $newMailPayload,
|
||||
],
|
||||
'jira' => [
|
||||
'has_new' => $hasNewJira,
|
||||
'newest_key' => $newestJiraKey,
|
||||
'new_issue' => $newJiraPayload,
|
||||
],
|
||||
'calendar' => [
|
||||
'has_changes' => $calendarChanged,
|
||||
'hash' => $calHash,
|
||||
'count' => count($todayEvents),
|
||||
],
|
||||
]);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Microsoft Outlook 365 E-mail API Endpoint
|
||||
* Verwerkt ophalen van inbox, detailberichten, beantwoorden, doorsturen en 1-klik inplannen.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||
|
||||
$user = require_auth();
|
||||
$userId = (int)$user['id'];
|
||||
$db = get_db_connection();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
|
||||
|
||||
try {
|
||||
// 1. Inbox berichten ophalen
|
||||
if ($action === 'get_inbox') {
|
||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 25;
|
||||
$unreadOnly = !empty($_GET['unread_only']) && $_GET['unread_only'] === '1';
|
||||
|
||||
$res = microsoft_fetch_inbox_messages($userId, $limit, $unreadOnly);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Enkel bericht detail ophalen
|
||||
if ($action === 'get_message') {
|
||||
$messageId = trim($_GET['id'] ?? ($_POST['id'] ?? ''));
|
||||
if (empty($messageId)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Geen bericht-ID opgegeven.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$detail = microsoft_fetch_message_detail($userId, $messageId);
|
||||
if (!$detail) {
|
||||
echo json_encode(['success' => false, 'error' => 'Bericht niet gevonden of geen toegang.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $detail]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Markeer als gelezen of ongelezen
|
||||
if ($action === 'mark_read') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$messageId = trim($input['id'] ?? '');
|
||||
$isRead = isset($input['is_read']) ? (bool)$input['is_read'] : true;
|
||||
|
||||
if (empty($messageId)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Geen bericht-ID opgegeven.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = microsoft_mark_message_read($userId, $messageId, $isRead);
|
||||
echo json_encode(['success' => $ok]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. E-mail beantwoorden
|
||||
if ($action === 'reply') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$messageId = trim($input['id'] ?? '');
|
||||
$comment = trim($input['comment'] ?? '');
|
||||
$replyAll = !empty($input['reply_all']);
|
||||
|
||||
if (empty($messageId) || empty($comment)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Bericht-ID en reactietekst zijn verplicht.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$res = microsoft_reply_message($userId, $messageId, $comment, $replyAll);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5. E-mail doorsturen
|
||||
if ($action === 'forward') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$messageId = trim($input['id'] ?? '');
|
||||
$toInput = $input['to'] ?? '';
|
||||
$comment = trim($input['comment'] ?? '');
|
||||
|
||||
if (empty($messageId) || empty($toInput)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Bericht-ID en minimaal één geadresseerde zijn verplicht.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$recipients = [];
|
||||
if (is_array($toInput)) {
|
||||
$recipients = $toInput;
|
||||
} else {
|
||||
$parts = explode(',', (string)$toInput);
|
||||
foreach ($parts as $p) {
|
||||
$p = trim($p);
|
||||
if (filter_var($p, FILTER_VALIDATE_EMAIL)) {
|
||||
$recipients[] = ['address' => $p, 'name' => $p];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($recipients)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Geen geldige e-mailadressen ingevoerd.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$res = microsoft_forward_message($userId, $messageId, $recipients, $comment);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 6. E-mail direct inplannen in Fociz dagplanning
|
||||
if ($action === 'plan_email') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
$messageId = trim($input['message_id'] ?? '');
|
||||
$subject = trim($input['subject'] ?? 'E-mail actiepunt');
|
||||
$planDate = trim($input['plan_date'] ?? date('Y-m-d'));
|
||||
$timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null;
|
||||
$durationMinutes = (int)($input['duration_minutes'] ?? 30);
|
||||
$priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2';
|
||||
$webLink = trim($input['web_link'] ?? 'https://outlook.office.com/mail/');
|
||||
$sender = trim($input['sender'] ?? '');
|
||||
|
||||
if (empty($subject)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Onderwerp is verplicht.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($durationMinutes <= 0) $durationMinutes = 30;
|
||||
|
||||
$timeBlockEnd = null;
|
||||
if (!empty($timeBlockStart)) {
|
||||
try {
|
||||
$dtEnd = new DateTime("{$planDate} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam'));
|
||||
$dtEnd->modify("+{$durationMinutes} minutes");
|
||||
$timeBlockEnd = $dtEnd->format('H:i');
|
||||
} catch (Throwable $t) {}
|
||||
}
|
||||
|
||||
// Zorg dat daily_plan bestaat
|
||||
$stmtPlan = $db->prepare("
|
||||
INSERT INTO daily_plans (user_id, plan_date)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)
|
||||
");
|
||||
$stmtPlan->execute([$userId, $planDate]);
|
||||
$planId = (int)$db->lastInsertId();
|
||||
|
||||
$taskTitle = "✉️ " . $subject;
|
||||
if (!empty($sender)) {
|
||||
$taskTitle .= " (van " . $sender . ")";
|
||||
}
|
||||
|
||||
$stmtTask = $db->prepare("
|
||||
INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, source, external_id, external_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'OUTLOOK_MAIL', ?, ?)
|
||||
");
|
||||
$stmtTask->execute([
|
||||
$userId,
|
||||
$planId,
|
||||
$taskTitle,
|
||||
$priority,
|
||||
$timeBlockStart,
|
||||
$timeBlockEnd,
|
||||
$durationMinutes,
|
||||
$messageId,
|
||||
$webLink
|
||||
]);
|
||||
$taskId = (int)$db->lastInsertId();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'plan_date' => $planDate,
|
||||
'message' => "E-mail succesvol ingepland voor {$planDate}."
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => false, 'error' => 'Onbekende e-mailactie.']);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Jira Webhook Receiver
|
||||
* Ontvangt webhook events van Atlassian Jira Cloud (issue_created, issue_updated, etc.)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/database.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rawPayload = file_get_contents('php://input');
|
||||
$data = json_decode($rawPayload, true);
|
||||
|
||||
if (!$data) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid JSON payload']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
$event = $data['webhookEvent'] ?? 'jira:unknown';
|
||||
$issueKey = $data['issue']['key'] ?? '';
|
||||
$summary = $data['issue']['fields']['summary'] ?? '';
|
||||
|
||||
// Sla event tijdelijk op of invalidate session cache
|
||||
error_log("Jira Webhook ontvangen: {$event} voor {$issueKey} - {$summary}");
|
||||
|
||||
echo json_encode([
|
||||
'received' => true,
|
||||
'event' => $event,
|
||||
'issue_key' => $issueKey,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
exit;
|
||||
} catch (Throwable $t) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $t->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* Microsoft Graph Webhook Receiver
|
||||
* Ontvangt subscription notifications voor /me/mailFolders('inbox')/messages en /me/events
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../config/database.php';
|
||||
|
||||
// Validatie bij aanmaken van Graph subscription
|
||||
if (isset($_GET['validationToken'])) {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
http_response_code(200);
|
||||
echo $_GET['validationToken'];
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
if ($data && isset($data['value'])) {
|
||||
foreach ($data['value'] as $change) {
|
||||
$resource = $change['resource'] ?? '';
|
||||
$changeType = $change['changeType'] ?? '';
|
||||
error_log("Microsoft Graph Webhook: {$changeType} op {$resource}");
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['received' => true]);
|
||||
|
||||
@@ -1,18 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Fun Emoji Set</dc:title><dc:creator>Davis Uche</dc:creator><dc:source xsi:type="dcterms:URI">https://www.figma.com/community/file/968125295144990435</dc:source><dcterms:license xsi:type="dcterms:URI">https://creativecommons.org/licenses/by/4.0/</dcterms:license><dc:rights>Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="200" height="200" rx="100" ry="100" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#6366f1" width="200" height="200" x="0" y="0" /><g transform="matrix(1.5625 0 0 1.5625 37.5 110.94)"><path fill-rule="evenodd" clip-rule="evenodd" d="M21 29a19.88 19.88 0 0 1 39 0c-9.72-7.8-30.4-7.44-39 0Z" fill="#2B1607"/><path d="M20.58 29.54c-.1 0-.2-.03-.3-.09a.58.58 0 0 1-.24-.27.56.56 0 0 1-.02-.36c2.48-9.9 10.54-16.22 20.51-16.22 9.98 0 18.01 6.41 20.45 16.28.03.11.02.24-.02.35a.58.58 0 0 1-.23.27.62.62 0 0 1-.7 0c-9.4-7.44-30.24-7.44-39.1 0a.6.6 0 0 1-.35.04Zm19.74-6.8c7.07 0 14.22 1.6 19.11 4.77-1.15-3.98-3.62-7.5-7.03-10a19.88 19.88 0 0 0-11.83-3.82 19.94 19.94 0 0 0-11.82 3.8 19.02 19.02 0 0 0-7.06 9.97c4.58-3.14 11.6-4.71 18.63-4.71Z" fill="#000"/><path d="M65 41.02a4.86 4.86 0 0 1-1.42 3.5 5.04 5.04 0 0 1-7.08.08 4.9 4.9 0 0 1-1.5-3.47c0-3.94 3.16-5.81 4.84-12.13 1.53 6.15 5.12 7.66 5.16 12.02Z" fill="#4ABCF4"/></g><g transform="matrix(1.5625 0 0 1.5625 31.25 59.38)"><path d="M74.38 30a13 13 0 0 0 13-13v-.66C87.38 5.9 75.83-.87 68.45.1c-6.54.84-7.07 5.02-7.07 11.24V17a13.01 13.01 0 0 0 13 13Z" fill="#000"/><path d="M70.2 9.67a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M74.44 18.48a5.32 5.32 0 1 0 0-10.64 5.32 5.32 0 0 0 0 10.64Z" fill="#fff"/><path d="M13.37 30a13.01 13.01 0 0 1-13-13v-.66C.38 5.9 11.93-.87 19.32.1c6.56.84 7.07 5.02 7.07 11.24V17a13 13 0 0 1-13 13Z" fill="#000"/><path d="M9.22 9.67a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M13.46 18.48a5.32 5.32 0 1 0 0-10.64 5.32 5.32 0 0 0 0 10.64Z" fill="#fff"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 2.5 KiB |
@@ -1,22 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Fun Emoji Set</dc:title><dc:creator>Davis Uche</dc:creator><dc:source xsi:type="dcterms:URI">https://www.figma.com/community/file/968125295144990435</dc:source><dcterms:license xsi:type="dcterms:URI">https://creativecommons.org/licenses/by/4.0/</dcterms:license><dc:rights>Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="200" height="200" rx="100" ry="100" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#ec4899" width="200" height="200" x="0" y="0" /><g transform="matrix(1.5625 0 0 1.5625 37.5 110.94)"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4c10.24 32.68 61.73 31.31 72 0A167.66 167.66 0 0 1 4 4Z" fill="#2B1607"/><path d="M39.68 29h-.64c-17.41-.24-31.54-9.95-36-24.74a1.02 1.02 0 0 1 .2-.95.98.98 0 0 1 .91-.28 165.35 165.35 0 0 0 71.72 0 .91.91 0 0 1 .9.3 1.04 1.04 0 0 1 .19.97C71.93 19.37 57.36 29 39.68 29ZM5.33 5.3c4.73 13.13 17.76 21.54 33.7 21.77C55.6 27.3 69.29 18.8 74.56 5.3a167.24 167.24 0 0 1-69.22 0Z" fill="#000"/><g style="mix-blend-mode:soft-light" opacity=".4"><path d="M22.02 35.23c5.08 5.6 28.25 5.96 33.85 0a79.58 79.58 0 0 1-33.85 0Z" fill="#422715"/><path d="M22.02 35.23c5.08 5.6 28.25 5.96 33.85 0a79.58 79.58 0 0 1-33.85 0Z" stroke="#000" stroke-width="1.58" stroke-miterlimit="10"/></g><path fill-rule="evenodd" clip-rule="evenodd" d="M40.83 45H38.1c-2.67 0-5.24-1.06-7.13-2.94a10 10 0 0 1-2.96-7.1V19.54c0-1.9 2.8-3.94 4.88-3.46a29.03 29.03 0 0 0 13.26 0C48 15.7 51 17.62 51 19.53v15.44a9.98 9.98 0 0 1-6.28 9.29c-1.23.5-2.55.75-3.89.74Z" fill="url(#mouthTongueOut-a)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M38.27 17c.95 3.75.33 13 1.13 13 .8 0 .53-9.01 1.37-13h-2.5Z" fill="#A00707"/><defs><linearGradient id="mouthTongueOut-a" x1="39.46" y1="45" x2="39.46" y2="16" gradientUnits="userSpaceOnUse"><stop stop-color="#EF0A0A"/><stop offset=".5" stop-color="#ED0A0A"/><stop offset=".67" stop-color="#E60A0A"/><stop offset=".8" stop-color="#DB0A0A"/><stop offset=".9" stop-color="#CA0A0A"/><stop offset=".99" stop-color="#B40A0A"/><stop offset="1" stop-color="#B10A0A"/></linearGradient></defs></g><g transform="matrix(1.5625 0 0 1.5625 31.25 59.38)"><path d="M74.46 30.06a13 13 0 0 1-13-13v-.66C61.46 5.95 73.01-.82 80.39.15c6.54.83 7.07 5.02 7.07 11.24v5.67a13.01 13.01 0 0 1-13 13Z" fill="#000"/><path d="M70.28 9.73a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M74.52 18.54a5.32 5.32 0 1 0 0-10.65 5.32 5.32 0 0 0 0 10.65Z" fill="#fff"/><path d="M13.47 30.06a13.01 13.01 0 0 0 12.99-13v-.66C26.46 5.95 14.92-.82 7.53.15.97.98.46 5.17.46 11.39v5.67a13 13 0 0 0 13 13Z" fill="#000"/><path d="M9.3 9.73a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M13.54 18.54a5.32 5.32 0 1 0 0-10.65 5.32 5.32 0 0 0 0 10.65Z" fill="#fff"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,22 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Fun Emoji Set</dc:title><dc:creator>Davis Uche</dc:creator><dc:source xsi:type="dcterms:URI">https://www.figma.com/community/file/968125295144990435</dc:source><dcterms:license xsi:type="dcterms:URI">https://creativecommons.org/licenses/by/4.0/</dcterms:license><dc:rights>Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="200" height="200" rx="100" ry="100" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#10b981" width="200" height="200" x="0" y="0" /><g transform="matrix(1.5625 0 0 1.5625 37.5 110.94)"><path d="M40.54 12h-.75c-9.7.22-20.8 5.3-23.7 16.15a1.36 1.36 0 0 0 .44 1.55 1.41 1.41 0 0 0 2.26-.86c2.55-9.46 12.42-13.89 21.06-14.08 8.24-.16 19.04 3.84 22.46 14.57a1.47 1.47 0 0 0 1.65.55A1.44 1.44 0 0 0 65 28.5C61.85 18.69 52.3 12 40.54 12Z" fill="#000"/></g><g transform="matrix(1.5625 0 0 1.5625 31.25 59.38)"><path d="M74.38 30a13 13 0 0 0 13-13v-.66C87.38 5.9 75.83-.87 68.45.1c-6.54.84-7.07 5.02-7.07 11.24V17a13.01 13.01 0 0 0 13 13Z" fill="#000"/><path d="M70.2 9.67a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M74.44 18.48a5.32 5.32 0 1 0 0-10.64 5.32 5.32 0 0 0 0 10.64Z" fill="#fff"/><path d="M13.37 30a13.01 13.01 0 0 1-13-13v-.66C.38 5.9 11.93-.87 19.32.1c6.56.84 7.07 5.02 7.07 11.24V17a13 13 0 0 1-13 13Z" fill="#000"/><path d="M9.22 9.67a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M13.46 18.48a5.32 5.32 0 1 0 0-10.64 5.32 5.32 0 0 0 0 10.64Z" fill="#fff"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,20 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Fun Emoji Set</dc:title><dc:creator>Davis Uche</dc:creator><dc:source xsi:type="dcterms:URI">https://www.figma.com/community/file/968125295144990435</dc:source><dcterms:license xsi:type="dcterms:URI">https://creativecommons.org/licenses/by/4.0/</dcterms:license><dc:rights>Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="200" height="200" rx="100" ry="100" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#8b5cf6" width="200" height="200" x="0" y="0" /><g transform="matrix(1.5625 0 0 1.5625 37.5 110.94)"><path d="M40.54 30h-.75c-9.7-.22-20.8-5.3-23.7-16.15a1.36 1.36 0 0 1 .44-1.55 1.41 1.41 0 0 1 2.26.86c2.55 9.46 12.42 13.89 21.06 14.08 8.24.16 19.04-3.84 22.46-14.57a1.47 1.47 0 0 1 1.65-.55A1.44 1.44 0 0 1 65 13.5C61.85 23.31 52.3 30 40.54 30Z" fill="#000"/></g><g transform="matrix(1.5625 0 0 1.5625 31.25 59.38)"><path d="M75.76 21.94c-2.9.04-5.72-.89-8.04-2.63a13.47 13.47 0 0 1-4.85-7.03 1.75 1.75 0 0 1 .49-1.92 1.7 1.7 0 0 1 2.76.9c.6 2.12 1.88 3.98 3.62 5.29a9.84 9.84 0 0 0 6.04 1.98c2.17.01 4.29-.68 6.03-2a10.17 10.17 0 0 0 3.65-5.26c.15-.42.46-.75.85-.95a1.68 1.68 0 0 1 2.24.7c.21.38.27.83.17 1.26a13.48 13.48 0 0 1-4.87 7.04 13.17 13.17 0 0 1-8.08 2.62ZM13.76 21.94c-2.9.03-5.73-.9-8.06-2.65a13.54 13.54 0 0 1-4.85-7.06 1.78 1.78 0 0 1 .51-1.88 1.67 1.67 0 0 1 2.4.22c.15.17.25.38.32.6.62 2.1 1.9 3.96 3.64 5.28a9.93 9.93 0 0 0 6.02 2c2.18.03 4.3-.67 6.06-1.99a10.22 10.22 0 0 0 3.66-5.3 1.68 1.68 0 0 1 3.08-.25c.21.4.27.85.17 1.27a13.57 13.57 0 0 1-4.87 7.1 13.19 13.19 0 0 1-8.08 2.65Z" fill="#000"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -1,18 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Fun Emoji Set</dc:title><dc:creator>Davis Uche</dc:creator><dc:source xsi:type="dcterms:URI">https://www.figma.com/community/file/968125295144990435</dc:source><dcterms:license xsi:type="dcterms:URI">https://creativecommons.org/licenses/by/4.0/</dcterms:license><dc:rights>Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="200" height="200" rx="100" ry="100" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#f59e0b" width="200" height="200" x="0" y="0" /><g transform="matrix(1.5625 0 0 1.5625 37.5 110.94)"><path fill-rule="evenodd" clip-rule="evenodd" d="M76 1v.49a35.13 35.13 0 0 1-.9 7.75 30.24 30.24 0 0 1-14.24 19.24C48.32 36 30.66 35.84 18.37 28A30.01 30.01 0 0 1 4.8 8.66 36.4 36.4 0 0 1 4 1c22.86 8.83 46.05 8.83 69.54.9L76 1Z" fill="#2B1607" stroke="#000" stroke-width="1.48" stroke-linecap="round" stroke-linejoin="round"/><path fill-rule="evenodd" clip-rule="evenodd" d="M76 1.48a33.97 33.97 0 0 1-.9 7.62C54.26 22.59 25.62 22.2 4.8 8.53 4.25 6.06 4 3.53 4 1c22.85 8.67 46.03 8.67 69.51.88l2.49-.4ZM62 28.49c-12.99 7.5-31.29 7.34-44-.49a134.2 134.2 0 0 0 44 .49Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M76.57.19c.27.19.43.5.43.83 0 11.3-4.64 19.83-11.62 25.51C58.42 32.2 49.18 35 40 35c-9.19 0-18.42-2.8-25.38-8.47C7.64 20.85 3 12.33 3 1.03A1.03 1.03 0 0 1 4.38.06c23.45 9.02 47.14 8.78 71.25 0 .32-.12.66-.08.94.12ZM5.07 2.5C5.45 12.44 9.7 19.9 15.9 24.94c6.54 5.32 15.3 8.01 24.1 8.01s17.56-2.69 24.1-8c6.2-5.06 10.45-12.53 10.83-22.46C51.36 10.77 28.1 11 5.07 2.5Z" fill="#000"/><path d="M24.01 36.05c4.5 5.16 25.03 5.5 30 0a68.03 68.03 0 0 1-30 0Z" fill="#000" style="mix-blend-mode:soft-light" opacity=".4"/></g><g transform="matrix(1.5625 0 0 1.5625 31.25 59.38)"><path d="M87.22 13.98c0-7.2-5.82-13.04-13-13.04s-13 5.84-13 13.04v3.92c0 7.2 5.82 13.04 13 13.04s13-5.83 13-13.04v-3.92Z" fill="#000"/><path d="M70 10.48a2.29 2.29 0 1 0 0-4.58 2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M74.24 19.3a5.32 5.32 0 1 0 0-10.66 5.32 5.32 0 0 0 0 10.65Z" fill="#fff"/><path d="M26.22 13.98c0-7.2-5.82-13.04-13-13.04s-13 5.84-13 13.04v3.92c0 7.2 5.82 13.04 13 13.04s13-5.83 13-13.04v-3.92Z" fill="#000"/><path d="M9 10.48A2.29 2.29 0 1 0 9 5.9a2.29 2.29 0 0 0 0 4.58Z" fill="#fff"/><path opacity=".1" d="M13.24 19.3a5.32 5.32 0 1 0 0-10.66 5.32 5.32 0 0 0 0 10.65Z" fill="#fff"/><path d="M84.33-5.7H65.45C58.63-5.7 53.1-.2 53.1 6.6v18.8c0 6.79 5.52 12.3 12.34 12.3h18.88c6.81 0 12.34-5.51 12.34-12.3V6.6c0-6.8-5.53-12.3-12.34-12.3Z" fill="url(#eyesGlasses-a)"/><path d="M21.3-5.7H2.42C-4.4-5.7-9.92-.2-9.92 6.6v18.8c0 6.79 5.52 12.3 12.34 12.3H21.3c6.81 0 12.34-5.51 12.34-12.3V6.6c0-6.8-5.53-12.3-12.34-12.3Z" fill="url(#eyesGlasses-b)"/><g fill="#000"><path d="M21.06 40.12H2.18A14.83 14.83 0 0 1-8.2 35.81a14.71 14.71 0 0 1-4.33-10.34V6.6c.02-3.87 1.58-7.59 4.34-10.34A14.85 14.85 0 0 1 2.18-8.06h18.88c3.92.02 7.66 1.58 10.42 4.35 2.76 2.76 4.31 6.5 4.31 10.4v18.7A14.62 14.62 0 0 1 26.71 39c-1.79.74-3.7 1.12-5.65 1.12ZM2.18-3.26A9.96 9.96 0 0 0-7 2.83a9.85 9.85 0 0 0-.76 3.78v18.8a9.85 9.85 0 0 0 9.9 9.86h18.92a9.93 9.93 0 0 0 9.9-9.86V6.6a9.87 9.87 0 0 0-9.9-9.86H2.18ZM84.33 40.12H65.46a14.83 14.83 0 0 1-10.39-4.31 14.71 14.71 0 0 1-4.33-10.34V6.6c.02-3.87 1.58-7.59 4.34-10.34a14.85 14.85 0 0 1 10.38-4.32h18.87c3.9.03 7.65 1.59 10.41 4.35s4.31 6.5 4.32 10.4v18.7c0 3.9-1.56 7.64-4.32 10.4a14.83 14.83 0 0 1-10.41 4.33ZM65.46-3.26a9.93 9.93 0 0 0-9.9 9.87v18.8a9.85 9.85 0 0 0 9.9 9.86h18.87a9.93 9.93 0 0 0 9.9-9.86V6.6a9.85 9.85 0 0 0-9.9-9.86H65.46Z"/><path d="M53.1 10.64H33.4v4.89h19.7v-4.89Z"/></g><defs><linearGradient id="eyesGlasses-a" x1="2332.67" y1="1561.82" x2="3621.21" y2="1561.82" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".3"/><stop offset=".5" stop-color="#969696" stop-opacity=".2"/><stop offset="1" stop-color="#fff" stop-opacity=".3"/></linearGradient><linearGradient id="eyesGlasses-b" x1="2269.64" y1="1561.82" x2="3558.18" y2="1561.82" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".3"/><stop offset=".5" stop-color="#969696" stop-opacity=".2"/><stop offset="1" stop-color="#fff" stop-opacity=".3"/></linearGradient></defs></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 973 B After Width: | Height: | Size: 4.6 KiB |
@@ -1,25 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Fun Emoji Set</dc:title><dc:creator>Davis Uche</dc:creator><dc:source xsi:type="dcterms:URI">https://www.figma.com/community/file/968125295144990435</dc:source><dcterms:license xsi:type="dcterms:URI">https://creativecommons.org/licenses/by/4.0/</dcterms:license><dc:rights>Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="200" height="200" rx="100" ry="100" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#f43f5e" width="200" height="200" x="0" y="0" /><g transform="matrix(1.5625 0 0 1.5625 37.5 110.94)"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4c10.24 32.68 61.73 31.31 72 0A167.66 167.66 0 0 1 4 4Z" fill="#2B1607"/><path d="M39.68 29h-.64c-17.41-.24-31.54-9.95-36-24.74a1.02 1.02 0 0 1 .2-.95.98.98 0 0 1 .91-.28 165.35 165.35 0 0 0 71.72 0 .91.91 0 0 1 .9.3 1.04 1.04 0 0 1 .19.97C71.93 19.37 57.36 29 39.68 29ZM5.33 5.3c4.73 13.13 17.76 21.54 33.7 21.77C55.6 27.3 69.29 18.8 74.56 5.3a167.24 167.24 0 0 1-69.22 0Z" fill="#000"/><g style="mix-blend-mode:soft-light" opacity=".4"><path d="M22.02 35.23c5.08 5.6 28.25 5.96 33.85 0a79.58 79.58 0 0 1-33.85 0Z" fill="#422715"/><path d="M22.02 35.23c5.08 5.6 28.25 5.96 33.85 0a79.58 79.58 0 0 1-33.85 0Z" stroke="#000" stroke-width="1.58" stroke-miterlimit="10"/></g><path fill-rule="evenodd" clip-rule="evenodd" d="M40.83 45H38.1c-2.67 0-5.24-1.06-7.13-2.94a10 10 0 0 1-2.96-7.1V19.54c0-1.9 2.8-3.94 4.88-3.46a29.03 29.03 0 0 0 13.26 0C48 15.7 51 17.62 51 19.53v15.44a9.98 9.98 0 0 1-6.28 9.29c-1.23.5-2.55.75-3.89.74Z" fill="url(#mouthTongueOut-a)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M38.27 17c.95 3.75.33 13 1.13 13 .8 0 .53-9.01 1.37-13h-2.5Z" fill="#A00707"/><defs><linearGradient id="mouthTongueOut-a" x1="39.46" y1="45" x2="39.46" y2="16" gradientUnits="userSpaceOnUse"><stop stop-color="#EF0A0A"/><stop offset=".5" stop-color="#ED0A0A"/><stop offset=".67" stop-color="#E60A0A"/><stop offset=".8" stop-color="#DB0A0A"/><stop offset=".9" stop-color="#CA0A0A"/><stop offset=".99" stop-color="#B40A0A"/><stop offset="1" stop-color="#B10A0A"/></linearGradient></defs></g><g transform="matrix(1.5625 0 0 1.5625 31.25 59.38)"><path d="M75.84 21.11c-2.9.04-5.72-.89-8.04-2.63a13.47 13.47 0 0 1-4.85-7.03 1.75 1.75 0 0 1 1.1-2.26 1.68 1.68 0 0 1 1.86.61c.14.2.24.4.3.64.6 2.11 1.87 3.97 3.61 5.28a9.84 9.84 0 0 0 6.04 1.98c2.17.01 4.29-.68 6.03-2a10.17 10.17 0 0 0 3.65-5.26c.15-.42.46-.75.85-.95a1.68 1.68 0 0 1 2.23.7c.21.38.27.83.17 1.26a13.48 13.48 0 0 1-4.87 7.04 13.17 13.17 0 0 1-8.08 2.62ZM13.84 21.11c-2.9.03-5.73-.9-8.06-2.65a13.54 13.54 0 0 1-4.85-7.05 1.78 1.78 0 0 1 .51-1.88 1.67 1.67 0 0 1 2.4.22c.15.17.25.38.32.6.62 2.1 1.9 3.96 3.64 5.28a9.93 9.93 0 0 0 6.02 2c2.18.03 4.3-.67 6.06-1.99a10.21 10.21 0 0 0 3.66-5.3 1.68 1.68 0 0 1 3.08-.25c.21.4.27.85.17 1.27a13.57 13.57 0 0 1-4.86 7.1 13.19 13.19 0 0 1-8.1 2.65Z" fill="#000"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,24 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Bottts</dc:title><dc:creator>Pablo Stanley</dc:creator><dc:source xsi:type="dcterms:URI">https://bottts.com/</dc:source><dcterms:license xsi:type="dcterms:URI">https://bottts.com/</dcterms:license><dc:rights>Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="180" height="180" rx="90" ry="90" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#6366f1" width="180" height="180" x="0" y="0" /><g transform="translate(0 66)"><mask id="sidesSquareAssymetric-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="10" y="8" width="165" height="60"><g fill="#fff"><rect x="10" y="31" width="36" height="30" rx="4"/><rect x="20" y="15" width="26" height="30" rx="4"/><rect x="139" y="23" width="36" height="30" rx="4"/><rect x="134" y="8" width="36" height="60" rx="4"/></g></mask><g mask="url(#sidesSquareAssymetric-a)"><path d="M0 0h180v76H0V0Z" fill="#d81b60"/><path d="M0 0h180v76H0V0Z" fill="#fff" fill-opacity=".3"/><path fill="#000" fill-opacity=".1" d="M0 47h180v29H0z"/><circle cx="161" cy="20" r="5" fill="#fff" fill-opacity=".6"/><circle cx="161" cy="36" r="5" fill="#fff" fill-opacity=".6"/></g></g><g transform="translate(41)"><mask id="topPyramid-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="18" y="8" width="64" height="44"><path fill-rule="evenodd" clip-rule="evenodd" d="m50 8 32 44H18L50 8Z" fill="#fff"/></mask><g mask="url(#topPyramid-a)"><path d="M0 0h100v52H0V0Z" fill="#d81b60"/><path d="M0 0h100v52H0V0Z" fill="#fff" fill-opacity=".3"/><path fill="#fff" fill-opacity=".8" d="M50 4h30v48H50z"/></g></g><g transform="translate(25 44)"><mask id="faceSquare04-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="130" height="120"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 102V68.85a40 40 0 0 1 2.28-13.31L19.76 12A18 18 0 0 1 36.74 0h56.52a18 18 0 0 1 16.98 12l17.48 43.54A40 40 0 0 1 130 68.85V102a18 18 0 0 1-18 18H18a18 18 0 0 1-18-18Z" fill="#fff"/></mask><g mask="url(#faceSquare04-a)"><path d="M-2-2h134v124H-2V-2Z" fill="#d81b60"/><g transform="translate(-1 -1)"></g></g></g><g transform="translate(52 124)"><path d="M27.05 8.44a2 2 0 1 1 3.9-.88C31.72 10.96 34.4 13 38 13c3.6 0 6.28-2.04 7.05-5.44a2 2 0 1 1 3.9.88C47.75 13.7 43.43 17 38 17s-9.76-3.3-10.95-8.56Z" fill="#000" fill-opacity=".6"/></g><g transform="translate(38 76)"><path d="m25 27.2 5.5 5.5c.5.4 1.2.4 1.6 0l1.6-1.6c.4-.5.4-1.2 0-1.6L28.2 24l5.5-5.5c.4-.5.4-1.2 0-1.6l-1.6-1.6c-.5-.4-1.2-.4-1.6 0L25 20.8l-5.5-5.5c-.5-.4-1.2-.4-1.6 0l-1.6 1.6c-.4.4-.4 1.1 0 1.6l5.5 5.5-5.5 5.5c-.4.5-.4 1.2 0 1.6l1.6 1.6c.5.4 1.2.4 1.6 0l5.5-5.5ZM79 27.2l5.5 5.5c.5.4 1.2.4 1.6 0l1.6-1.6c.4-.5.4-1.2 0-1.6L82.2 24l5.5-5.5c.4-.5.4-1.2 0-1.6l-1.6-1.6c-.5-.4-1.2-.4-1.6 0L79 20.8l-5.5-5.5c-.5-.4-1.2-.4-1.6 0l-1.6 1.6c-.4.4-.4 1.1 0 1.6l5.5 5.5-5.5 5.5c-.4.5-.4 1.2 0 1.6l1.6 1.6c.5.4 1.2.4 1.6 0l5.5-5.5Z" fill="#000" fill-opacity=".8"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -1,21 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Bottts</dc:title><dc:creator>Pablo Stanley</dc:creator><dc:source xsi:type="dcterms:URI">https://bottts.com/</dc:source><dcterms:license xsi:type="dcterms:URI">https://bottts.com/</dcterms:license><dc:rights>Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="180" height="180" rx="90" ry="90" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#10b981" width="180" height="180" x="0" y="0" /><g transform="translate(0 66)"><g opacity=".9" stroke="#2A3544"><ellipse cx="32.5" cy="23" rx="16.5" ry="18" stroke-width="6"/><path d="M29.51 36.76c-7.4 4.29-17 1.55-21.42-6.1" stroke-width="4"/><ellipse cx="28.5" cy="52.5" rx="16.5" ry="14.5" stroke-width="4"/></g><g opacity=".9" stroke="#2A3544"><path d="M168.6 60.42c-4.27-7.41-13.95-9.84-21.6-5.42" stroke-width="4"/><ellipse cx="148.5" cy="22.5" rx="16.5" ry="15.5" stroke-width="6"/></g><mask id="sidesCables02-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="21" y="0" width="138" height="72"><g fill="#fff"><rect x="21" y="27" width="16" height="22" rx="2"/><rect x="22" y="60" width="16" height="12" rx="2"/><rect x="143" y="42" width="16" height="22" rx="2"/><rect x="143" width="16" height="22" rx="2"/></g></mask><g mask="url(#sidesCables02-a)"><path d="M0 0h180v76H0V0Z" fill="#e53935"/><path d="M0 0h180v76H0V0Z" fill="#fff" fill-opacity=".3"/></g></g><g transform="translate(41)"><g filter="url(#topGlowingBulb01-a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M32 24A16 16 0 0 1 48 8h4a16 16 0 0 1 16 16v8a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-8Z" fill="#fff" fill-opacity=".3"/></g><path d="M49 11.5c4.93 0 9.37 2.13 12.44 5.52" stroke="#fff" stroke-width="2" stroke-linecap="round"/><path d="m49.83 29-9-9L38 22.83l10 10V40h4v-7.03l10.14-10.14L59.31 20l-9 9h-.48Z" fill="#fff" fill-opacity=".8"/><rect x="22" y="40" width="56" height="12" rx="1" fill="#48494B"/><defs><filter id="topGlowingBulb01-a" x="24" y="0" width="52" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset/><feGaussianBlur stdDeviation="4"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_617_621"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_617_621" result="shape"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0"/><feBlend in2="shape" result="effect2_innerShadow_617_621"/></filter></defs></g><g transform="translate(25 44)"><mask id="faceSquare02-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="130" height="120"><path d="M0 12A12 12 0 0 1 12 0h106a12 12 0 0 1 12 12v70a38 38 0 0 1-38 38H38A38 38 0 0 1 0 82V12Z" fill="#fff"/></mask><g mask="url(#faceSquare02-a)"><path d="M-2-2h134v124H-2V-2Z" fill="#e53935"/><g transform="translate(-1 -1)"></g></g></g><g transform="translate(52 124)"><g fill="#000" fill-opacity=".6"><rect x="28" y="10" width="6" height="14" rx="2"/><rect x="14" y="10" width="6" height="14" rx="2"/><rect x="42" y="10" width="6" height="14" rx="2"/><rect x="56" y="10" width="6" height="14" rx="2"/></g></g><g transform="translate(38 76)"><g fill-rule="evenodd" clip-rule="evenodd"><path d="M53 0c34.75 0 49 17.47 49 31 0 13.53-19.59 17-49 17-29.05 0-51-3.47-51-17S17.11 0 53 0Z" fill="#000" fill-opacity=".8"/><path d="M28.82 34.65c-6.53-1.35-11.24-6.34-10.52-11.14.72-4.79 6.6-7.58 13.12-6.23 6.53 1.36 11.24 6.35 10.52 11.15-.72 4.8-6.6 7.59-13.12 6.23ZM75.42 34.65c-6.52 1.36-12.4-1.43-13.12-6.23-.72-4.8 4-9.8 10.52-11.15 6.52-1.35 12.4 1.44 13.12 6.24.72 4.81-4 9.8-10.52 11.15Z" fill="#25A6F5"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,22 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Bottts</dc:title><dc:creator>Pablo Stanley</dc:creator><dc:source xsi:type="dcterms:URI">https://bottts.com/</dc:source><dcterms:license xsi:type="dcterms:URI">https://bottts.com/</dcterms:license><dc:rights>Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="180" height="180" rx="90" ry="90" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#8b5cf6" width="180" height="180" x="0" y="0" /><g transform="translate(0 66)"><mask id="sidesSquareAssymetric-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="10" y="8" width="165" height="60"><g fill="#fff"><rect x="10" y="31" width="36" height="30" rx="4"/><rect x="20" y="15" width="26" height="30" rx="4"/><rect x="139" y="23" width="36" height="30" rx="4"/><rect x="134" y="8" width="36" height="60" rx="4"/></g></mask><g mask="url(#sidesSquareAssymetric-a)"><path d="M0 0h180v76H0V0Z" fill="#fdd835"/><path d="M0 0h180v76H0V0Z" fill="#fff" fill-opacity=".3"/><path fill="#000" fill-opacity=".1" d="M0 47h180v29H0z"/><circle cx="161" cy="20" r="5" fill="#fff" fill-opacity=".6"/><circle cx="161" cy="36" r="5" fill="#fff" fill-opacity=".6"/></g></g><g transform="translate(41)"><mask id="topHorns-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="8" y="12" width="84" height="40"><g fill="#fff"><path d="M8 40h26v12H8z"/><path transform="matrix(-1 0 0 1 92 40)" d="M0 0h26v12H0z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M16.52 13.74c0 7.84 5.39 20.71 13.72 27.04H11.36S7.84 22.66 13.43 14.1c.9-1.38 3.1-1.42 3.1-.36ZM84 14c.66 7.04-5.77 20.62-14 27h19s3.14-18.26-2-27c-1-1.7-3.14-1.45-3 0Z"/></g></mask><g mask="url(#topHorns-a)"><path d="M0 0h100v52H0V0Z" fill="#fdd835"/><path d="M0 0h100v52H0V0Z" fill="#fff" fill-opacity=".3"/><path fill="#000" fill-opacity=".4" d="M0 40h100v12H0z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M15.46 13h16.1v27H20.83c-7.45-7.85-5.36-27-5.36-27ZM84.82 13h7.75v27H81.82c5.75-7.8 3-27 3-27Z" fill="#fff" fill-opacity=".4"/></g></g><g transform="translate(25 44)"><mask id="faceSquare01-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="130" height="120"><rect width="130" height="120" rx="18" fill="#fff"/></mask><g mask="url(#faceSquare01-a)"><path d="M-2-2h134v124H-2V-2Z" fill="#fdd835"/><g transform="translate(-1 -1)"></g></g></g><g transform="translate(52 124)"><rect x="4" y="4" width="68" height="24" rx="5" fill="#000" fill-opacity=".2"/><rect x="8" y="8" width="60" height="16" rx="2" fill="#000" fill-opacity=".8"/><path d="M9 17h11l2-4 3 7 4-8 2 9 3-11 3 10 3-3h15l3-4 2 7 3-3h4" stroke="#4EFAC9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g><g transform="translate(38 76)"><g fill="#fff"><circle cx="24" cy="30" r="6"/><circle cx="80" cy="30" r="6"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -1,19 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" fill="none" shape-rendering="auto"><metadata xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"><rdf:RDF><rdf:Description><dc:title>Bottts</dc:title><dc:creator>Pablo Stanley</dc:creator><dc:source xsi:type="dcterms:URI">https://bottts.com/</dc:source><dcterms:license xsi:type="dcterms:URI">https://bottts.com/</dcterms:license><dc:rights>Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/)</dc:rights></rdf:Description></rdf:RDF></metadata><mask id="viewboxMask"><rect width="180" height="180" rx="90" ry="90" x="0" y="0" fill="#fff" /></mask><g mask="url(#viewboxMask)"><rect fill="#06b6d4" width="180" height="180" x="0" y="0" /><g transform="translate(0 66)"><mask id="sidesSquareAssymetric-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="10" y="8" width="165" height="60"><g fill="#fff"><rect x="10" y="31" width="36" height="30" rx="4"/><rect x="20" y="15" width="26" height="30" rx="4"/><rect x="139" y="23" width="36" height="30" rx="4"/><rect x="134" y="8" width="36" height="60" rx="4"/></g></mask><g mask="url(#sidesSquareAssymetric-a)"><path d="M0 0h180v76H0V0Z" fill="#1e88e5"/><path d="M0 0h180v76H0V0Z" fill="#fff" fill-opacity=".3"/><path fill="#000" fill-opacity=".1" d="M0 47h180v29H0z"/><circle cx="161" cy="20" r="5" fill="#fff" fill-opacity=".6"/><circle cx="161" cy="36" r="5" fill="#fff" fill-opacity=".6"/></g></g><g transform="translate(41)"><mask id="topAntenna-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="38" y="5" width="24" height="47"><path d="M38 38c0-1.1.9-2 2-2h20a2 2 0 0 1 2 2v14H38V38ZM48 5h4v31h-4z" fill="#fff"/></mask><g mask="url(#topAntenna-a)"><path d="M0 0h100v52H0V0Z" fill="#1e88e5"/><path d="M0 3h100v52H0V3Z" fill="#fff" fill-opacity=".3"/><path fill="#fff" fill-opacity=".2" d="M38 36h24v16H38z"/></g><circle cx="50" cy="8" r="8" fill="#FFE65C"/><circle cx="53" cy="5" r="3" fill="#fff"/></g><g transform="translate(25 44)"><mask id="faceRound01-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="130" height="120"><path fill-rule="evenodd" clip-rule="evenodd" d="M66 0c58.35 0 64 40.69 64 78 0 33.31-25.47 42-64 42-37.46 0-66-8.69-66-42C0 40.69 7.65 0 66 0Z" fill="#fff"/></mask><g mask="url(#faceRound01-a)"><path d="M-4-2h138v124H-4V-2Z" fill="#1e88e5"/><g transform="translate(-1 -1)"></g></g></g><g transform="translate(52 124)"><g fill="#000" fill-opacity=".6"><rect x="28" y="10" width="6" height="14" rx="2"/><rect x="14" y="10" width="6" height="14" rx="2"/><rect x="42" y="10" width="6" height="14" rx="2"/><rect x="56" y="10" width="6" height="14" rx="2"/></g></g><g transform="translate(38 76)"><rect x="7" y="16" width="91" height="16" rx="4" fill="#000" fill-opacity=".8"/><mask id="eyesRobocop-a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="7" y="16" width="91" height="16"><rect x="7" y="16" width="91" height="16" rx="4" fill="#fff"/></mask><g mask="url(#eyesRobocop-a)" fill-rule="evenodd" clip-rule="evenodd" fill="#fff" fill-opacity=".8"><path d="M76 7h18L82 37H64L76 7ZM52 7h9L49 37h-9L52 7Z"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -147,8 +147,10 @@ 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 users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url",
|
||||
"ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)",
|
||||
"CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
"CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` 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) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
];
|
||||
foreach ($colUpgrades as $sql) {
|
||||
try {
|
||||
@@ -158,6 +160,35 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Zorg dat de standaard Jira Servicedesk wachtrijfilters (DX en SD) aanwezig zijn
|
||||
try {
|
||||
$filterCount = (int)$pdo->query("SELECT COUNT(*) FROM jira_queue_filters")->fetchColumn();
|
||||
if ($filterCount === 0) {
|
||||
$insertFilterStmt = $pdo->prepare("
|
||||
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?)
|
||||
");
|
||||
$insertFilterStmt->execute([
|
||||
'DX',
|
||||
'DX — Koppelingen, SSO & Migratie',
|
||||
'SD',
|
||||
'"Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)',
|
||||
'Servicedesk tickets met betrekking tot koppelingen, endpoints en datamigraties.',
|
||||
1
|
||||
]);
|
||||
$insertFilterStmt->execute([
|
||||
'SD',
|
||||
'SD — Regulier Servicedesk',
|
||||
'SD',
|
||||
'("Onderwerp logging[Dropdown]" = EMPTY OR "Onderwerp logging[Dropdown]" != Koppelingen AND "Onderwerp logging[Dropdown]" != Migratie AND "Onderwerp logging[Dropdown]" != SSO-koppeling AND "Onderwerp logging[Dropdown]" != UWV-koppeling AND "Onderwerp logging[Dropdown]" != Rest-endpoints AND "Onderwerp logging[Dropdown]" != Datadump)',
|
||||
'Reguliere servicedesk incidenten, algemene meldingen en gebruikersvragen.',
|
||||
2
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Stil negeren indien tabel nog in opbouw is
|
||||
}
|
||||
|
||||
// Zorg dat er ten minste één standaard bord met standaard lanes aanwezig is
|
||||
try {
|
||||
$boardCount = (int)$pdo->query("SELECT COUNT(*) FROM boards")->fetchColumn();
|
||||
|
||||
@@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
||||
`remember_token_expires_at` DATETIME NULL,
|
||||
`notification_settings` TEXT NULL,
|
||||
`avatar_url` VARCHAR(255) NULL,
|
||||
`default_jira_filter_code` VARCHAR(50) NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -218,3 +219,16 @@ CREATE TABLE IF NOT EXISTS `board_card_schedules` (
|
||||
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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `jira_queue_filters` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`filter_code` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`filter_name` VARCHAR(120) NOT NULL,
|
||||
`project_key` VARCHAR(50) NULL,
|
||||
`jql_clause` TEXT NOT NULL,
|
||||
`description` VARCHAR(255) NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` 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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -142,6 +142,76 @@ function jira_test_connection(string $domain, string $email, string $apiToken):
|
||||
/**
|
||||
* Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker
|
||||
*/
|
||||
/**
|
||||
* Converteert Atlassian Document Format (ADF) of tekst naar platte leesbare tekst
|
||||
*/
|
||||
function jira_adf_to_text($adf): string {
|
||||
if (empty($adf)) {
|
||||
return '';
|
||||
}
|
||||
if (is_string($adf)) {
|
||||
return trim($adf);
|
||||
}
|
||||
if (!is_array($adf)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = '';
|
||||
$type = $adf['type'] ?? '';
|
||||
|
||||
if ($type === 'text') {
|
||||
return $adf['text'] ?? '';
|
||||
}
|
||||
|
||||
if (!empty($adf['content']) && is_array($adf['content'])) {
|
||||
foreach ($adf['content'] as $child) {
|
||||
$childText = jira_adf_to_text($child);
|
||||
$childType = $child['type'] ?? '';
|
||||
|
||||
if ($childType === 'paragraph' || $childType === 'heading') {
|
||||
$text .= $childText . "\n\n";
|
||||
} elseif ($childType === 'listItem') {
|
||||
$text .= "• " . trim($childText) . "\n";
|
||||
} elseif ($childType === 'codeBlock') {
|
||||
$text .= "\n" . trim($childText) . "\n\n";
|
||||
} else {
|
||||
$text .= $childText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt alle geconfigureerde Jira wachtrijfilters op uit de database
|
||||
*/
|
||||
function jira_get_queue_filters(bool $onlyActive = true): array {
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
$sql = "SELECT * FROM jira_queue_filters " . ($onlyActive ? "WHERE is_active = 1 " : "") . "ORDER BY sort_order ASC, id ASC";
|
||||
return $db->query($sql)->fetchAll() ?: [];
|
||||
} catch (Throwable $e) {
|
||||
error_log("jira_get_queue_filters error: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de standaard wachtrijfilter voorkeur van een gebruiker op
|
||||
*/
|
||||
function jira_get_user_queue_preference(int $userId): ?string {
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("SELECT default_jira_filter_code FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$res = $stmt->fetchColumn();
|
||||
return $res ? (string)$res : null;
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt issues op via een flexibele JQL query via Atlassian REST API
|
||||
*/
|
||||
@@ -159,7 +229,9 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
|
||||
'assignee',
|
||||
'reporter',
|
||||
'duedate',
|
||||
'updated'
|
||||
'updated',
|
||||
'created',
|
||||
'description',
|
||||
];
|
||||
|
||||
// 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST
|
||||
@@ -242,26 +314,60 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
|
||||
$projectKey = $fields['project']['key'] ?? '';
|
||||
$dueDate = $fields['duedate'] ?? null;
|
||||
$updated = $fields['updated'] ?? null;
|
||||
$created = $fields['created'] ?? null;
|
||||
|
||||
$assigneeName = $fields['assignee']['displayName'] ?? 'Niet toegewezen';
|
||||
$assigneeAvatar = $fields['assignee']['avatarUrls']['48x48'] ?? ($fields['assignee']['avatarUrls']['32x32'] ?? '');
|
||||
|
||||
$reporterName = $fields['reporter']['displayName'] ?? 'Onbekend';
|
||||
$reporterAvatar = $fields['reporter']['avatarUrls']['48x48'] ?? ($fields['reporter']['avatarUrls']['32x32'] ?? '');
|
||||
|
||||
$descriptionText = jira_adf_to_text($fields['description'] ?? '');
|
||||
|
||||
// Detectie van custom field "Onderwerp logging"
|
||||
$onderwerpLogging = '';
|
||||
foreach ($fields as $fKey => $fVal) {
|
||||
if (str_starts_with($fKey, 'customfield_')) {
|
||||
if (is_array($fVal) && isset($fVal['value']) && is_string($fVal['value'])) {
|
||||
$valStr = trim($fVal['value']);
|
||||
$known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen'];
|
||||
if (in_array(strtolower($valStr), $known)) {
|
||||
$onderwerpLogging = $valStr;
|
||||
}
|
||||
} elseif (is_string($fVal) && !empty($fVal)) {
|
||||
$valStr = trim($fVal);
|
||||
$known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
|
||||
if (in_array(strtolower($valStr), $known)) {
|
||||
$onderwerpLogging = $valStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) {
|
||||
$onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? '');
|
||||
}
|
||||
|
||||
$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',
|
||||
'key' => $key,
|
||||
'summary' => $summary,
|
||||
'description' => $descriptionText,
|
||||
'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,
|
||||
'reporter_name' => $reporterName,
|
||||
'reporter_avatar' => $reporterAvatar,
|
||||
'onderwerp_logging' => $onderwerpLogging,
|
||||
'due_date' => $dueDate,
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'url' => "{$cleanDomain}/browse/{$key}",
|
||||
'source' => 'JIRA_LIVE',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -297,247 +403,407 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
||||
$stmt->execute([$userId]);
|
||||
$integration = $stmt->fetch();
|
||||
|
||||
// Ophalen van actieve wachtrijfilters en voorkeur van gebruiker
|
||||
$queueFilters = jira_get_queue_filters(true);
|
||||
$userQueuePref = jira_get_user_queue_preference($userId);
|
||||
|
||||
// Sandbox / Demo dataset indien niet gekoppeld of expliciet in demomodus
|
||||
if (!$integration || !empty($integration['is_demo_mode'])) {
|
||||
$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')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-101',
|
||||
'source' => 'SANDBOX',
|
||||
'key' => 'SD-401',
|
||||
'summary' => 'AFAS Profit SSO-koppeling geeft 401 fout bij token refresh',
|
||||
'description' => "Gebruikers die via AFAS SSO inloggen krijgen periodiek een HTTP 401 foutcode bij de geautomatiseerde token verversing.\n\nHet endpoint `/oauth/token` reageert met `invalid_grant` zodra de refresh token ouder is dan 24 uur. Er dient een retry-mechanisme met fallback authenticatie ingebouwd te worden.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Highest',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Kees van de Berg (AFAS Beheer)',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'SSO-koppeling',
|
||||
'due_date' => date('Y-m-d', strtotime('+1 day')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-45 minutes')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-401',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-102',
|
||||
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'key' => 'SD-405',
|
||||
'summary' => 'REST-endpoints voor medewerker synchronisatie beveiligen met rate limiting',
|
||||
'description' => "De openbare REST-endpoints voor medewerkerssynchronisatie dienen conform security richtlijnen te worden voorzien van rate limiting (max 120 calls per minuut per IP).\n\nBij overschrijding moet een duidelijke HTTP 429 Retry-After header worden geretourneerd.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Security Team',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Rest-endpoints',
|
||||
'due_date' => date('Y-m-d', strtotime('+3 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-405',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'SD-380',
|
||||
'summary' => 'Wachtwoord reset e-mail komt niet aan bij nieuwe medewerker',
|
||||
'description' => "Een nieuwe medewerker ontvangt geen welkomstmail en wachtwoord reset via het selfservice portaal.\n\nControleer de SMTP-verbinding van de mailserver mail.fociz.nl en controleer of het mailadres in de spamfilter is beland.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'HR Servicedesk',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'EMPTY',
|
||||
'due_date' => date('Y-m-d'),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-380',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'SD-385',
|
||||
'summary' => 'Rechten aanpassen voor rol Projectleider in urenregistratie',
|
||||
'description' => "Projectleiders hebben momenteel alleen leesrechten op ingediende urenstaten. Graag de rol uitbreiden zodat zij declaraties direct kunnen goedkeuren of afkeuren.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Marianne Willems',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Algemeen',
|
||||
'due_date' => date('Y-m-d', strtotime('+5 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-385',
|
||||
'source' => 'SANDBOX',
|
||||
],
|
||||
[
|
||||
'key' => 'FOC-101',
|
||||
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
|
||||
'description' => "Ontwikkeling van een complete Google Authenticator / Microsoft Authenticator TOTP verificatie flow inclusief 8 cijferige noodcodes en QR-code weergave in het gebruikersprofiel.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Fociz Platform',
|
||||
'project_key' => 'FOC',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Product Owner',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Beveiliging',
|
||||
'due_date' => date('Y-m-d', strtotime('+2 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'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',
|
||||
'description' => "Gebruikers moeten live post-its kunnen slepen, kleuren en stemmen tijdens kwartaal brainstormsessies.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Feature',
|
||||
'project' => 'Fociz Platform',
|
||||
'project_key' => 'FOC',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Scrum Master',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+4 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-5 days')),
|
||||
'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',
|
||||
'key' => 'FOC-103',
|
||||
'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API',
|
||||
'description' => "Synchronisatie van dagplanning blokken naar Microsoft 365 Exchange Online agenda met real-time conflictcontrole.",
|
||||
'status' => 'In Review',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Integrations Hub',
|
||||
'project_key' => 'INT',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Architect',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'SSO-koppeling',
|
||||
'due_date' => date('Y-m-d'),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-6 days')),
|
||||
'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')),
|
||||
'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',
|
||||
'description' => "Op Windows Firefox versies 128+ verspringt de horizontale scrollbalk bij het slepen van kaarten tussen kolommen.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Highest',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'UI/UX Redesign',
|
||||
'project_key' => 'UI',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'QA Lead',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+1 day')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-108',
|
||||
'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',
|
||||
'key' => 'FOC-112',
|
||||
'summary' => 'Avatar upload beveiliging en compressie optimalisatie',
|
||||
'description' => "Server-side image re-encoding en EXIF stripping voor geüploade gebruikersavatars.",
|
||||
'status' => 'Done',
|
||||
'status_category' => 'done',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Security',
|
||||
'project_key' => 'SEC',
|
||||
'assignee_name' => 'Mijzelf',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'SecOps',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('-1 day')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
'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' => 'SD-408',
|
||||
'summary' => 'UWV-koppeling polisadministratie import loopt vast op datumformaat',
|
||||
'description' => "De XML batch import vanuit het UWV portaal faalt op records met afwijkende geboortedatumnotatie.\n\nDe parser dient flexibel zowel ISO-8601 (YYYY-MM-DD) als traditioneel formaat (DD-MM-YYYY) te accepteren.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Lisa Bakker',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Servicedesk L2',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'UWV-koppeling',
|
||||
'due_date' => date('Y-m-d', strtotime('+2 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-408',
|
||||
'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' => 'SD-412',
|
||||
'summary' => 'Datadump archief export voor externe accountant audit 2025',
|
||||
'description' => "Genereer een beveiligde, versleutelde datadump van alle audit-logs en urenstaten voor de periodieke controle.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Daan van Dijk',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Compliance Officer',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Datadump',
|
||||
'due_date' => date('Y-m-d', strtotime('+6 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-412',
|
||||
'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' => 'SD-416',
|
||||
'summary' => 'Migratie van oude klantportaal database naar MariaDB 11 cluster',
|
||||
'description' => "Voorbereiden en uitvoeren van de dataschemamigratie van het oude portaal naar de nieuwe MariaDB 11 clusteromgeving inclusief regressietests.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Bram Meijer',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Lead Engineer',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Migratie',
|
||||
'due_date' => date('Y-m-d', strtotime('+8 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-416',
|
||||
'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' => 'SD-420',
|
||||
'summary' => 'Koppelingen API webhook notificaties afleveren met retry-mechanisme',
|
||||
'description' => "Webhooks die naar externe partnersystemen worden verzonden moeten bij een tijdelijke 5xx fout automatisch 3x opnieuw worden aangeboden (exponential backoff).",
|
||||
'status' => 'In Review',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Sophie de Vries',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Integratie Specialist',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Koppelingen',
|
||||
'due_date' => date('Y-m-d', strtotime('+4 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-5 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-420',
|
||||
'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' => 'SD-392',
|
||||
'summary' => 'Outlook Agenda synchronisatie toont dubbele afspraken op mobiel',
|
||||
'description' => "Bij enkele gebruikers met de iOS Outlook app worden terugkerende agenda-afspraken tweemaal weergegeven na het synchroniseren.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Medium',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Lisa Bakker',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Servicedesk Support',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Incident',
|
||||
'due_date' => date('Y-m-d', strtotime('+3 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-392',
|
||||
'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' => 'SD-399',
|
||||
'summary' => 'Vraag over exportmogelijkheden naar Excel rapportage',
|
||||
'description' => "Klant informeert of het mogelijk is om een wekelijkse CSV / Excel export van de urenstaten automatisch per mail te ontvangen.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'Low',
|
||||
'issue_type' => 'Task',
|
||||
'project' => 'Servicedesk',
|
||||
'project_key' => 'SD',
|
||||
'assignee_name' => 'Thomas Jansen',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Financieel Directeur',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => 'Vraag',
|
||||
'due_date' => date('Y-m-d', strtotime('+10 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-5 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
'url' => 'https://jira.atlassian.net/browse/SD-399',
|
||||
'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-95',
|
||||
'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab',
|
||||
'description' => "Wanneer een gebruiker de browser 2 uur niet aanraakt, vervalt de PHP sessie zonder dat het login-dialoogvenster netjes openspringt.",
|
||||
'status' => 'In Progress',
|
||||
'status_category' => 'indeterminate',
|
||||
'priority' => 'Highest',
|
||||
'issue_type' => 'Bug',
|
||||
'project' => 'Fociz Platform',
|
||||
'project_key' => 'FOC',
|
||||
'assignee_name' => 'Lisa Bakker',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Product QA',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+1 day')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-95',
|
||||
'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',
|
||||
'key' => 'FOC-98',
|
||||
'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen',
|
||||
'description' => "Cronjob instellen die 24 uur voor de deadline van een taak een herinneringsmail stuurt via SMTP.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Notifications',
|
||||
'project_key' => 'NOTIF',
|
||||
'assignee_name' => 'Daan van Dijk',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Operations',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+3 days')),
|
||||
'created' => date('Y-m-d H:i:s', 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-110',
|
||||
'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden',
|
||||
'description' => "Voorkom overschrijfconflicten wanneer meerdere collega's gelijktijdig kaarten verplaatsen op hetzelfde Kanban bord.",
|
||||
'status' => 'To Do',
|
||||
'status_category' => 'new',
|
||||
'priority' => 'High',
|
||||
'issue_type' => 'Story',
|
||||
'project' => 'Realtime Engine',
|
||||
'project_key' => 'RT',
|
||||
'assignee_name' => 'Bram Meijer',
|
||||
'assignee_avatar' => '',
|
||||
'reporter_name' => 'Architect',
|
||||
'reporter_avatar' => '',
|
||||
'onderwerp_logging' => '',
|
||||
'due_date' => date('Y-m-d', strtotime('+6 days')),
|
||||
'created' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')),
|
||||
'url' => 'https://jira.atlassian.net/browse/FOC-110',
|
||||
'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,
|
||||
'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,
|
||||
'queue_filters' => $queueFilters,
|
||||
'default_queue_filter' => $userQueuePref,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -587,14 +853,16 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
||||
$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,
|
||||
'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,
|
||||
'queue_filters' => $queueFilters,
|
||||
'default_queue_filter' => $userQueuePref,
|
||||
];
|
||||
|
||||
$_SESSION[$cacheKey] = [
|
||||
|
||||
@@ -56,16 +56,22 @@ foreach ($boards as &$b) {
|
||||
}
|
||||
unset($b);
|
||||
|
||||
// Verzamel unieke projecten en types voor filter dropdowns
|
||||
// Verzamel unieke projecten en types voor filter dropdowns en project switcher
|
||||
$allIssues = array_merge($assignedIssues, $teamIssues);
|
||||
$allProjects = [];
|
||||
$allTypes = [];
|
||||
$allPriorities = ['Highest', 'High', 'Medium', 'Low'];
|
||||
$allStatuses = [];
|
||||
$projectCounts = [];
|
||||
|
||||
foreach ($allIssues as $iss) {
|
||||
if (!empty($iss['project'])) {
|
||||
$allProjects[$iss['project']] = $iss['project'];
|
||||
$p = $iss['project'];
|
||||
$allProjects[$p] = [
|
||||
'name' => $p,
|
||||
'key' => $iss['project_key'] ?? '',
|
||||
];
|
||||
$projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1;
|
||||
}
|
||||
if (!empty($iss['issue_type'])) {
|
||||
$allTypes[$iss['issue_type']] = $iss['issue_type'];
|
||||
@@ -78,6 +84,9 @@ ksort($allProjects);
|
||||
ksort($allTypes);
|
||||
ksort($allStatuses);
|
||||
|
||||
$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true);
|
||||
$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId);
|
||||
|
||||
$jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
|
||||
$boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
|
||||
?>
|
||||
@@ -229,6 +238,97 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Snelle Project Switcher (Pill Tabs) -->
|
||||
<div class="fociz-card p-4 sm:p-5">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-3 mb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
||||
<i data-lucide="folder-kanban" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Jira Projecten</h2>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400">Klik op een project om direct te filteren</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-400 hidden sm:block">
|
||||
<span><?= count($allProjects) ?> actieve projecten gevonden</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 overflow-x-auto pb-1" id="projectPillsContainer">
|
||||
<!-- Alle Projecten Pill -->
|
||||
<button type="button" onclick="selectProject('')" data-project="" class="project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0">
|
||||
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
|
||||
<span>Alle Projecten</span>
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] bg-white/20 text-white font-black"><?= count($allIssues) ?></span>
|
||||
</button>
|
||||
|
||||
<!-- Per project pills -->
|
||||
<?php foreach ($allProjects as $pName => $pData): ?>
|
||||
<button type="button" onclick="selectProject('<?= htmlspecialchars(addslashes($pName)) ?>')" data-project="<?= htmlspecialchars($pName) ?>" class="project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0">
|
||||
<?php if (!empty($pData['key'])): ?>
|
||||
<span class="px-1.5 py-0.5 rounded text-[10px] font-mono font-black bg-indigo-100 dark:bg-indigo-950/80 text-indigo-700 dark:text-indigo-300"><?= htmlspecialchars($pData['key']) ?></span>
|
||||
<?php else: ?>
|
||||
<i data-lucide="folder" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<?php endif; ?>
|
||||
<span><?= htmlspecialchars($pName) ?></span>
|
||||
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300"><?= $projectCounts[$pName] ?? 0 ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Servicedesk Wachtrijfilter Sub-balk (DX / SD) -->
|
||||
<div id="queueFiltersBar" class="fociz-card p-4 bg-gradient-to-r from-slate-50 via-indigo-50/30 to-sky-50/20 dark:from-[#0c0f17] dark:via-indigo-950/20 dark:to-sky-950/20 border-indigo-200/70 dark:border-indigo-900/40">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 rounded-xl bg-indigo-500/15 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold flex-shrink-0">
|
||||
<i data-lucide="filter" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs font-bold text-slate-900 dark:text-white">Servicedesk Wachtrijfilters</span>
|
||||
<?php if (!empty($defaultQueueFilter)): ?>
|
||||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80">
|
||||
Jouw voorkeur: <?= htmlspecialchars($defaultQueueFilter) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400">
|
||||
Filtert op onderwerp logging routering (DX voor koppelingen & endpoints, SD voor reguliere support)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knoppen rij voor wachtrijen -->
|
||||
<div class="flex items-center gap-2 overflow-x-auto" id="queueButtonsContainer">
|
||||
<button type="button" onclick="selectQueueFilter('')" data-queue="" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300 flex-shrink-0">
|
||||
<i data-lucide="list" class="w-3.5 h-3.5"></i>
|
||||
<span>Alle Wachtrijen</span>
|
||||
<span id="queueCount_all" class="px-1.5 py-0.5 rounded-full text-[10px] bg-slate-300 dark:bg-slate-700 font-bold">0</span>
|
||||
</button>
|
||||
|
||||
<button type="button" onclick="selectQueueFilter('DX')" data-queue="DX" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0">
|
||||
<i data-lucide="zap" class="w-3.5 h-3.5 text-amber-500"></i>
|
||||
<span>DX Wachtrij (Koppelingen)</span>
|
||||
<span id="queueCount_DX" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-300 font-bold">0</span>
|
||||
</button>
|
||||
|
||||
<button type="button" onclick="selectQueueFilter('SD')" data-queue="SD" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0">
|
||||
<i data-lucide="wrench" class="w-3.5 h-3.5 text-sky-500"></i>
|
||||
<span>SD Wachtrij (Regulier)</span>
|
||||
<span id="queueCount_SD" class="px-1.5 py-0.5 rounded-full text-[10px] bg-sky-100 dark:bg-sky-950/60 text-sky-800 dark:text-sky-300 font-bold">0</span>
|
||||
</button>
|
||||
|
||||
<?php if ($isAdmin): ?>
|
||||
<a href="/admin.php#jira-wachtrijen" title="Wachtrijfilters beheren in Admin" class="p-1.5 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
|
||||
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filterbalk & Zoekveld -->
|
||||
<div class="fociz-card p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-3">
|
||||
@@ -250,10 +350,10 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
|
||||
<!-- Filter: Project -->
|
||||
<div class="md:col-span-2">
|
||||
<select id="filterProject" onchange="applyFilters()" class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
|
||||
<select id="filterProject" onchange="onProjectSelectChange(this.value)" class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
|
||||
<option value="">Alle projecten</option>
|
||||
<?php foreach ($allProjects as $p): ?>
|
||||
<option value="<?= htmlspecialchars($p) ?>"><?= htmlspecialchars($p) ?></option>
|
||||
<?php foreach ($allProjects as $pName => $pData): ?>
|
||||
<option value="<?= htmlspecialchars($pName) ?>"><?= htmlspecialchars($pName) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -573,7 +673,10 @@ const jiraData = <?= $jiraDataJson ?>;
|
||||
const availableBoards = <?= $boardsJson ?>;
|
||||
|
||||
let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done'
|
||||
let currentProject = '';
|
||||
let currentQueue = jiraData.default_queue_filter || '';
|
||||
let activeUrgentOnly = false;
|
||||
let expandedKeys = new Set();
|
||||
|
||||
// Bereken statussen en KPI statistieken
|
||||
function calculateKpis() {
|
||||
@@ -629,9 +732,97 @@ function switchTab(tab) {
|
||||
}
|
||||
});
|
||||
|
||||
updateQueueCounts();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Project selectie via pill tabs
|
||||
function selectProject(proj) {
|
||||
currentProject = proj;
|
||||
const pSelect = document.getElementById('filterProject');
|
||||
if (pSelect) {
|
||||
pSelect.value = proj;
|
||||
}
|
||||
updateProjectPills();
|
||||
updateQueueCounts();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Project selectie via dropdown
|
||||
function onProjectSelectChange(proj) {
|
||||
currentProject = proj;
|
||||
updateProjectPills();
|
||||
updateQueueCounts();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateProjectPills() {
|
||||
const pills = document.querySelectorAll('.project-pill');
|
||||
pills.forEach(p => {
|
||||
const pKey = p.getAttribute('data-project') || '';
|
||||
if (pKey === currentProject) {
|
||||
p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0";
|
||||
} else {
|
||||
p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wachtrijfilter selectie
|
||||
function selectQueueFilter(code) {
|
||||
currentQueue = code;
|
||||
updateQueuePills();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateQueuePills() {
|
||||
const pills = document.querySelectorAll('.queue-pill');
|
||||
pills.forEach(p => {
|
||||
const qCode = p.getAttribute('data-queue') || '';
|
||||
if (qCode === currentQueue) {
|
||||
p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0";
|
||||
} else {
|
||||
p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update de tellers op de DX / SD wachtrijfilter knoppen
|
||||
function updateQueueCounts() {
|
||||
const assigned = jiraData.assigned_issues || [];
|
||||
const team = jiraData.team_issues || [];
|
||||
const all = [...assigned, ...team];
|
||||
|
||||
let currentList = all;
|
||||
if (currentTab === 'assigned') currentList = assigned;
|
||||
else if (currentTab === 'team') currentList = team;
|
||||
|
||||
if (currentProject) {
|
||||
currentList = currentList.filter(iss => iss.project === currentProject);
|
||||
}
|
||||
|
||||
const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
|
||||
let dxCount = 0;
|
||||
let sdCount = 0;
|
||||
|
||||
currentList.forEach(iss => {
|
||||
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
|
||||
if (dxValues.includes(ol)) {
|
||||
dxCount++;
|
||||
} else {
|
||||
sdCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const elAll = document.getElementById('queueCount_all');
|
||||
const elDx = document.getElementById('queueCount_DX');
|
||||
const elSd = document.getElementById('queueCount_SD');
|
||||
|
||||
if (elAll) elAll.innerText = currentList.length;
|
||||
if (elDx) elDx.innerText = dxCount;
|
||||
if (elSd) elSd.innerText = sdCount;
|
||||
}
|
||||
|
||||
// Snelkoppeling KPI-kaart Urgent
|
||||
function filterByUrgent() {
|
||||
const pSelect = document.getElementById('filterPriority');
|
||||
@@ -653,6 +844,8 @@ function clearSearch() {
|
||||
|
||||
// Alle filters herstellen
|
||||
function resetAllFilters() {
|
||||
currentProject = '';
|
||||
currentQueue = '';
|
||||
const searchInput = document.getElementById('jiraSearchInput');
|
||||
const projectSelect = document.getElementById('filterProject');
|
||||
const typeSelect = document.getElementById('filterType');
|
||||
@@ -665,14 +858,17 @@ function resetAllFilters() {
|
||||
if (prioritySelect) prioritySelect.value = '';
|
||||
if (statusSelect) statusSelect.value = '';
|
||||
|
||||
document.getElementById('clearSearchBtn').classList.add('hidden');
|
||||
document.getElementById('clearSearchBtn')?.classList.add('hidden');
|
||||
updateProjectPills();
|
||||
updateQueuePills();
|
||||
updateQueueCounts();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Filter en render logic
|
||||
function applyFilters() {
|
||||
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
|
||||
const projectVal = document.getElementById('filterProject')?.value || '';
|
||||
const projectVal = currentProject || (document.getElementById('filterProject')?.value || '');
|
||||
const typeVal = document.getElementById('filterType')?.value || '';
|
||||
const priorityVal = document.getElementById('filterPriority')?.value || '';
|
||||
const statusVal = document.getElementById('filterStatus')?.value || '';
|
||||
@@ -706,19 +902,42 @@ function applyFilters() {
|
||||
});
|
||||
}
|
||||
|
||||
// Pas zoek- en selectiefilters toe
|
||||
const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
|
||||
|
||||
// Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters toe
|
||||
const filtered = sourceList.filter(iss => {
|
||||
if (searchVal) {
|
||||
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
|
||||
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
|
||||
const prjMatch = (iss.project || '').toLowerCase().includes(searchVal);
|
||||
if (!keyMatch && !sumMatch && !prjMatch) return false;
|
||||
const descMatch = (iss.description || '').toLowerCase().includes(searchVal);
|
||||
const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal);
|
||||
const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal);
|
||||
if (!keyMatch && !sumMatch && !prjMatch && !descMatch && !repMatch && !olMatch) return false;
|
||||
}
|
||||
|
||||
if (projectVal && iss.project !== projectVal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentQueue) {
|
||||
const qCode = currentQueue.toUpperCase();
|
||||
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
|
||||
|
||||
if (qCode === 'DX') {
|
||||
if (!dxValues.includes(ol)) return false;
|
||||
} else if (qCode === 'SD') {
|
||||
if (ol !== '' && ol !== 'empty' && dxValues.includes(ol)) return false;
|
||||
} else {
|
||||
const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === qCode);
|
||||
if (fObj) {
|
||||
if (ol && !ol.includes(qCode.toLowerCase()) && !fObj.jql_clause.toLowerCase().includes(ol)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeVal && iss.issue_type !== typeVal) {
|
||||
return false;
|
||||
}
|
||||
@@ -741,6 +960,36 @@ function applyFilters() {
|
||||
renderTickets(filtered);
|
||||
}
|
||||
|
||||
// Uitklappen en inklappen van ticket details
|
||||
function toggleTicketDetails(key, event) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
const details = document.getElementById(`details_${key}`);
|
||||
const chevron = document.getElementById(`chevron_${key}`);
|
||||
if (!details) return;
|
||||
|
||||
if (details.classList.contains('hidden')) {
|
||||
details.classList.remove('hidden');
|
||||
if (chevron) chevron.classList.add('rotate-180');
|
||||
expandedKeys.add(key);
|
||||
} else {
|
||||
details.classList.add('hidden');
|
||||
if (chevron) chevron.classList.remove('rotate-180');
|
||||
expandedKeys.delete(key);
|
||||
}
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function copyTicketKey(key) {
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
showToast({
|
||||
message: `Ticket ${key} gekopieerd!`,
|
||||
subMessage: 'De ticketsleutel is direct naar je klembord gekopieerd.'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Renders the list of tickets
|
||||
function renderTickets(tickets) {
|
||||
const container = document.getElementById('ticketsContainer');
|
||||
@@ -842,11 +1091,38 @@ function renderTickets(tickets) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Onderwerp logging badge
|
||||
let onderwerpBadge = '<span class="text-slate-400 italic font-normal">Niet ingevuld</span>';
|
||||
if (ticket.onderwerp_logging) {
|
||||
const olLower = ticket.onderwerp_logging.toLowerCase();
|
||||
const dxList = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
|
||||
const isDx = dxList.includes(olLower);
|
||||
if (isDx) {
|
||||
onderwerpBadge = `
|
||||
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold bg-amber-50 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80">
|
||||
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
|
||||
<span>DX: ${escapeHtml(ticket.onderwerp_logging)}</span>
|
||||
</span>
|
||||
`;
|
||||
} else {
|
||||
onderwerpBadge = `
|
||||
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold bg-sky-50 text-sky-800 dark:bg-sky-950/60 dark:text-sky-300 border border-sky-300 dark:border-sky-800/80">
|
||||
<i data-lucide="wrench" class="w-3 h-3 text-sky-500"></i>
|
||||
<span>SD: ${escapeHtml(ticket.onderwerp_logging)}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isExpanded = expandedKeys.has(ticket.key);
|
||||
const detailsClass = isExpanded ? '' : 'hidden';
|
||||
const chevronRotateClass = isExpanded ? 'rotate-180' : '';
|
||||
|
||||
// JSON stringified ticket data voor modalaanroep
|
||||
const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '"');
|
||||
|
||||
html += `
|
||||
<div class="fociz-card p-4 sm:p-5 transition hover:border-indigo-500/40 dark:hover:border-indigo-500/40">
|
||||
<div id="ticketCard_${escapeHtml(ticket.key)}" class="fociz-card p-4 sm:p-5 transition hover:border-indigo-500/40 dark:hover:border-indigo-500/40">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
|
||||
|
||||
<!-- Linkerkant: Metadata & Titel -->
|
||||
@@ -888,23 +1164,29 @@ function renderTickets(tickets) {
|
||||
${dueDateHtml}
|
||||
</div>
|
||||
|
||||
<!-- Ticket Titel / Summary -->
|
||||
<h3 class="text-sm sm:text-base font-bold text-slate-900 dark:text-white leading-snug">
|
||||
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition">
|
||||
<!-- Ticket Titel / Summary (Klikbaar voor accordion) -->
|
||||
<h3 class="text-sm sm:text-base font-bold text-slate-900 dark:text-white leading-snug cursor-pointer group" onclick="toggleTicketDetails('${escapeHtml(ticket.key)}', event)">
|
||||
<span class="group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">
|
||||
${escapeHtml(ticket.summary || '(Geen titel)')}
|
||||
</a>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<!-- Sub-info: Assignee & Update timestamp -->
|
||||
<!-- Sub-info: Assignee, Update timestamp & Inklap knop -->
|
||||
<div class="flex flex-wrap items-center gap-4 pt-1">
|
||||
${assigneeHtml}
|
||||
${ticket.updated ? `<span class="text-[11px] text-slate-400">Gewijzigd: ${escapeHtml(ticket.updated)}</span>` : ''}
|
||||
${ticket.onderwerp_logging ? onderwerpBadge : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rechterkant: Snelle Actieknoppen -->
|
||||
<!-- Rechterkant: Snelle Actieknoppen + Uitklapknop -->
|
||||
<div class="flex items-center space-x-2 pt-2 lg:pt-0 flex-shrink-0 border-t lg:border-t-0 border-slate-100 dark:border-slate-800">
|
||||
<button type="button" onclick="toggleTicketDetails('${escapeHtml(ticket.key)}', event)" class="flex items-center space-x-1 px-2.5 py-2 rounded-xl text-xs font-bold text-slate-600 dark:text-slate-300 hover:text-indigo-600 dark:hover:text-indigo-400 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700/80 transition cursor-pointer shadow-2xs">
|
||||
<span>Details</span>
|
||||
<i data-lucide="chevron-down" id="chevron_${escapeHtml(ticket.key)}" class="w-3.5 h-3.5 transition-transform duration-200 ${chevronRotateClass}"></i>
|
||||
</button>
|
||||
|
||||
<button onclick='openPlanModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/60 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-800/80 transition cursor-pointer shadow-2xs">
|
||||
<i data-lucide="calendar" class="w-3.5 h-3.5"></i>
|
||||
<span>Plan in Dagplanner</span>
|
||||
@@ -921,6 +1203,79 @@ function renderTickets(tickets) {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- UITKLAPBAAR PANEEL (Accordion Details) -->
|
||||
<div id="details_${escapeHtml(ticket.key)}" class="${detailsClass} mt-4 pt-4 border-t border-slate-100 dark:border-slate-800 space-y-4 text-xs">
|
||||
|
||||
<!-- Beschrijving -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center space-x-1.5 font-bold text-slate-700 dark:text-slate-300">
|
||||
<i data-lucide="align-left" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||||
<span>Volledige Ticket Omschrijving</span>
|
||||
</div>
|
||||
<div class="p-4 rounded-xl bg-slate-50 dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800 text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap select-text font-normal">
|
||||
${escapeHtml(ticket.description || 'Geen uitgebreide omschrijving beschikbaar in Jira.')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Grid (Reporter, Aanmaakdatum, Wijziging, Onderwerp) -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 p-3.5 rounded-xl bg-slate-50/60 dark:bg-slate-950/40 border border-slate-200/60 dark:border-slate-800/60 text-[11px]">
|
||||
<div>
|
||||
<span class="text-slate-400 block mb-1 font-medium">Aangemeld door (Reporter):</span>
|
||||
<div class="flex items-center space-x-1.5 font-semibold text-slate-800 dark:text-slate-200">
|
||||
<div class="w-4 h-4 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center font-bold text-[9px] flex-shrink-0">
|
||||
${escapeHtml((ticket.reporter_name || 'J').charAt(0).toUpperCase())}
|
||||
</div>
|
||||
<span class="truncate">${escapeHtml(ticket.reporter_name || 'Onbekend')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-400 block mb-1 font-medium">Aangemaakt op:</span>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">
|
||||
${ticket.created ? escapeHtml(ticket.created) : 'Niet opgegeven'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-400 block mb-1 font-medium">Laatst gewijzigd:</span>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">
|
||||
${ticket.updated ? escapeHtml(ticket.updated) : 'Recent'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-400 block mb-1 font-medium">Onderwerp logging:</span>
|
||||
${onderwerpBadge}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snelle acties onderaan detailvak -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="button" onclick='copyTicketKey("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
<span>Sleutel Kopiëren</span>
|
||||
</button>
|
||||
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-indigo-600 dark:text-indigo-400 text-xs font-semibold transition cursor-pointer">
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
<span>Open in Jira Cloud</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="button" onclick='openPlanModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-xs">
|
||||
<i data-lucide="calendar" class="w-3 h-3"></i>
|
||||
<span>Inplannen in Dagplanner</span>
|
||||
</button>
|
||||
<button type="button" onclick='openBoardModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-xs">
|
||||
<i data-lucide="kanban" class="w-3 h-3"></i>
|
||||
<span>Naar Kanban Bord</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -1191,6 +1546,11 @@ document.addEventListener('keydown', function(e) {
|
||||
// Initiële opstart
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
calculateKpis();
|
||||
updateProjectPills();
|
||||
updateQueueCounts();
|
||||
if (currentQueue) {
|
||||
updateQueuePills();
|
||||
}
|
||||
applyFilters();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
<?php
|
||||
/**
|
||||
* Fociz Microsoft Outlook 365 E-mail Hub
|
||||
* Volwaardige inbox, berichtdetail, beantwoorden, doorsturen en direct inplannen in Fociz.
|
||||
*/
|
||||
|
||||
$pageTitle = 'Fociz — Microsoft Outlook Mail';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
||||
|
||||
$userId = (int)($currentUser['id'] ?? 0);
|
||||
$db = get_db_connection();
|
||||
|
||||
// Haal initiële inbox data op
|
||||
$inboxData = microsoft_fetch_inbox_messages($userId, 30);
|
||||
$messages = $inboxData['messages'] ?? [];
|
||||
$isDemo = !empty($inboxData['is_demo']);
|
||||
$unreadCount = (int)($inboxData['unread_count'] ?? 0);
|
||||
|
||||
// Controleer status
|
||||
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||
$stmtInt->execute([$userId]);
|
||||
$outlookInt = $stmtInt->fetch();
|
||||
$isLiveOutlook = !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode']);
|
||||
|
||||
$jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
|
||||
?>
|
||||
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
|
||||
<!-- Top Banner: Status & Snelle Acties -->
|
||||
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 text-sky-600 dark:text-sky-400 text-xs font-bold uppercase tracking-wider mb-1">
|
||||
<i data-lucide="mail" class="w-4 h-4"></i>
|
||||
<span>Microsoft Outlook 365 • Inbox</span>
|
||||
<?php if ($isLiveOutlook): ?>
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 ring-1 ring-emerald-500/30">LIVE VERBONDEN</span>
|
||||
<?php else: ?>
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300 ring-1 ring-amber-500/30">DEMO INBOX</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
|
||||
E-mail & Communicatie
|
||||
</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||
Bekijk binnenkomende e-mails, beantwoord direct en plan belangrijke berichten in één klik in op jouw tijdlijn.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Knoppen rechts -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button onclick="refreshMailbox()" id="refreshMailBtn" class="flex items-center space-x-1.5 px-3.5 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer shadow-2xs">
|
||||
<i data-lucide="refresh-cw" id="refreshMailIcon" class="w-3.5 h-3.5"></i>
|
||||
<span>Vernieuwen</span>
|
||||
</button>
|
||||
|
||||
<?php if (!$isLiveOutlook): ?>
|
||||
<a href="/profile.php" class="flex items-center space-x-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-500 hover:to-indigo-500 text-white text-xs font-bold transition shadow-sm shadow-sky-600/20">
|
||||
<i data-lucide="link-2" class="w-3.5 h-3.5"></i>
|
||||
<span>Live Outlook Koppelen</span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="https://outlook.office.com/mail/" target="_blank" rel="noopener noreferrer" class="flex items-center space-x-1.5 px-3.5 py-2 rounded-xl bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60 text-xs font-bold transition hover:bg-sky-100">
|
||||
<span>Open in Outlook Web</span>
|
||||
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$isLiveOutlook): ?>
|
||||
<div class="mt-5 p-3.5 rounded-2xl bg-amber-50/80 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900/40 flex items-center justify-between gap-3 text-xs text-amber-900 dark:text-amber-200">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<i data-lucide="info" class="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0"></i>
|
||||
<span>Je bekijkt momenteel een interactieve demonstratieset. Koppel je zakelijke Microsoft 365 account via Profiel om al je live zakelijke e-mails in te laden.</span>
|
||||
</div>
|
||||
<a href="/auth/oauth_microsoft.php" class="font-bold underline hover:text-amber-700 dark:hover:text-amber-100 flex-shrink-0">Nu koppelen →</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Filterbalk & Zoeken -->
|
||||
<div class="fociz-card p-4 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<!-- Filter tabs: Alle / Ongelezen -->
|
||||
<div class="flex items-center space-x-1.5 bg-slate-100/90 dark:bg-slate-800/60 p-1 rounded-xl border border-slate-200/70 dark:border-slate-700/60 text-xs font-bold w-full sm:w-auto">
|
||||
<button onclick="setMailFilter('all')" id="filterBtn_all" class="flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition">
|
||||
<span>Alle Berichten</span>
|
||||
<span class="ml-1 px-1.5 py-0.2 rounded-full text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400" id="countAll"><?= count($messages) ?></span>
|
||||
</button>
|
||||
<button onclick="setMailFilter('unread')" id="filterBtn_unread" class="flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition">
|
||||
<span>Ongelezen</span>
|
||||
<span class="ml-1 px-1.5 py-0.2 rounded-full text-[10px] bg-sky-100 dark:bg-sky-900/60 text-sky-700 dark:text-sky-300" id="countUnread"><?= $unreadCount ?></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Zoekveld -->
|
||||
<div class="relative w-full sm:w-72">
|
||||
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-3 pointer-events-none"></i>
|
||||
<input type="text" id="mailSearchInput" oninput="applyMailSearch()" placeholder="Zoek op afzender, onderwerp..." class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Twee-kolommen Mail Interface -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-[600px]">
|
||||
|
||||
<!-- Linker kolom: Berichtenlijst (5 kolommen) -->
|
||||
<div id="mailListColumn" class="lg:col-span-5 fociz-card p-3 sm:p-4 flex flex-col h-[720px] overflow-hidden">
|
||||
<div class="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 px-2 py-1 mb-2 flex items-center justify-between">
|
||||
<span>Inbox Berichten (<span id="visibleMailCount"><?= count($messages) ?></span>)</span>
|
||||
<span class="text-[10px] text-slate-400 font-normal">Sorteer: Nieuwste eerst</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto space-y-1.5 pr-1" id="mailListContainer">
|
||||
<!-- Wordt dynamisch gerenderd via JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechter kolom: Bericht Detail & Acties (7 kolommen) -->
|
||||
<div id="mailDetailColumn" class="lg:col-span-7 fociz-card p-6 sm:p-7 flex flex-col h-[720px] overflow-hidden">
|
||||
<!-- Empty state wanneer geen mail geselecteerd is -->
|
||||
<div id="noMailSelectedState" class="flex-1 flex flex-col items-center justify-center text-center p-8 text-slate-400">
|
||||
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800/80 flex items-center justify-center mb-3 text-slate-400 dark:text-slate-500">
|
||||
<i data-lucide="mail-open" class="w-7 h-7"></i>
|
||||
</div>
|
||||
<h4 class="text-sm font-bold text-slate-700 dark:text-slate-300">Geen e-mail geselecteerd</h4>
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500 max-w-sm mt-1">
|
||||
Selecteer een bericht aan de linkerkant om de volledige inhoud te lezen, te beantwoorden of in te plannen in je dagagenda.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actief geselecteerd bericht -->
|
||||
<div id="mailActiveContainer" class="hidden flex-1 flex flex-col min-h-0">
|
||||
|
||||
<!-- Mobiel terug knop -->
|
||||
<button type="button" onclick="closeMobileDetail()" class="lg:hidden flex items-center space-x-1 text-xs font-bold text-slate-500 mb-3 hover:text-slate-800">
|
||||
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
|
||||
<span>Terug naar lijst</span>
|
||||
</button>
|
||||
|
||||
<!-- Detail Header -->
|
||||
<div class="pb-4 border-b border-slate-100 dark:border-slate-800/80 space-y-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<h2 id="detailSubject" class="text-lg sm:text-xl font-black text-slate-900 dark:text-white leading-snug"></h2>
|
||||
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
<!-- Plan in Fociz knop -->
|
||||
<button type="button" onclick="openPlanMailModal()" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-sm shadow-indigo-600/20 cursor-pointer">
|
||||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
|
||||
<span>Plan in Fociz</span>
|
||||
</button>
|
||||
|
||||
<!-- Beantwoorden knop -->
|
||||
<button type="button" onclick="openReplyModal()" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
|
||||
<i data-lucide="reply" class="w-3.5 h-3.5"></i>
|
||||
<span class="hidden sm:inline">Antwoord</span>
|
||||
</button>
|
||||
|
||||
<!-- Doorsturen knop -->
|
||||
<button type="button" onclick="openForwardModal()" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
|
||||
<i data-lucide="corner-up-right" class="w-3.5 h-3.5"></i>
|
||||
<span class="hidden sm:inline">Doorsturen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Afzender & Ontvangers info -->
|
||||
<div class="flex items-center justify-between text-xs pt-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div id="detailSenderAvatar" class="w-9 h-9 rounded-xl bg-sky-100 dark:bg-sky-950/60 text-sky-700 dark:text-sky-300 font-bold flex items-center justify-center text-xs">
|
||||
NL
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900 dark:text-white" id="detailSenderName"></div>
|
||||
<div class="text-[11px] text-slate-400 font-mono" id="detailSenderEmail"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<div class="text-xs font-semibold text-slate-600 dark:text-slate-300" id="detailReceivedHuman"></div>
|
||||
<div class="text-[10px] text-slate-400 font-mono" id="detailReceivedExact"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Body (Scrollable) -->
|
||||
<div class="flex-1 overflow-y-auto py-5 text-sm text-slate-800 dark:text-slate-200 leading-relaxed font-normal" id="detailBody">
|
||||
<!-- HTML body content -->
|
||||
</div>
|
||||
|
||||
<!-- Footer met web link -->
|
||||
<div class="pt-3 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between text-xs text-slate-400">
|
||||
<span id="detailFooterBadge">Outlook 365 Bericht</span>
|
||||
<a id="detailWebLink" href="#" target="_blank" rel="noopener noreferrer" class="text-sky-600 dark:text-sky-400 hover:underline flex items-center space-x-1 font-semibold">
|
||||
<span>Bekijk in Outlook Web App</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- MODAL 1: E-mail Inplannen in Fociz Dagplanning -->
|
||||
<div id="planMailModal" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-9 h-9 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||||
<i data-lucide="calendar-plus" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Plan E-mail in als Taak</h3>
|
||||
<p class="text-[11px] text-slate-400">Plaats dit actiepunt direct op jouw Fociz tijdlijn</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closePlanMailModal()" class="text-slate-400 hover:text-slate-600 p-1.5 rounded-lg transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onsubmit="submitPlanEmail(event)" class="space-y-4">
|
||||
<input type="hidden" id="planMailId" value="">
|
||||
<input type="hidden" id="planMailWebLink" value="">
|
||||
<input type="hidden" id="planMailSender" value="">
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
|
||||
<input type="text" id="planMailSubject" required class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white font-semibold focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
|
||||
<input type="date" id="planMailDate" value="<?= date('Y-m-d') ?>" required class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd (optioneel)</label>
|
||||
<input type="text" id="planMailStartTime" placeholder="bijv. 10:30" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs font-mono text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Tijdsduur</label>
|
||||
<select id="planMailDuration" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<option value="15">15 minuten</option>
|
||||
<option value="30" selected>30 minuten</option>
|
||||
<option value="45">45 minuten</option>
|
||||
<option value="60">60 minuten (1 uur)</option>
|
||||
<option value="90">90 minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
|
||||
<select id="planMailPriority" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<option value="P1">P1 - Urgent / Brandend</option>
|
||||
<option value="P2" selected>P2 - Belangrijk</option>
|
||||
<option value="P3">P3 - Regulier</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" onclick="closePlanMailModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
Annuleren
|
||||
</button>
|
||||
<button type="submit" id="planMailSubmitBtn" class="flex items-center space-x-1.5 px-5 py-2 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition shadow-sm shadow-indigo-600/30 cursor-pointer">
|
||||
<i data-lucide="check" class="w-3.5 h-3.5"></i>
|
||||
<span>Inplannen</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL 2: E-mail Beantwoorden -->
|
||||
<div id="replyModal" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-9 h-9 rounded-xl bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center">
|
||||
<i data-lucide="reply" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Beantwoord Bericht</h3>
|
||||
<p class="text-[11px] text-slate-400" id="replyToLabel"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeReplyModal()" class="text-slate-400 hover:text-slate-600 p-1.5 rounded-lg transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onsubmit="submitReply(event)" class="space-y-4">
|
||||
<input type="hidden" id="replyMailId" value="">
|
||||
|
||||
<div class="flex items-center space-x-3 text-xs">
|
||||
<label class="flex items-center space-x-2 cursor-pointer text-slate-700 dark:text-slate-300">
|
||||
<input type="checkbox" id="replyAllCheckbox" class="w-4 h-4 rounded text-sky-600 focus:ring-sky-500 border-slate-300 dark:border-slate-700">
|
||||
<span>Allen beantwoorden (Reply All)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Jouw Reactie</label>
|
||||
<textarea id="replyContent" rows="6" required placeholder="Typ hier jouw bericht..." class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl p-3 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" onclick="closeReplyModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
Annuleren
|
||||
</button>
|
||||
<button type="submit" id="replySubmitBtn" class="flex items-center space-x-1.5 px-5 py-2 rounded-xl text-xs font-bold bg-sky-600 hover:bg-sky-500 text-white transition shadow-sm shadow-sky-600/30 cursor-pointer">
|
||||
<i data-lucide="send" class="w-3.5 h-3.5"></i>
|
||||
<span>Versturen via Outlook</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL 3: E-mail Doorsturen -->
|
||||
<div id="forwardModal" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-9 h-9 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center">
|
||||
<i data-lucide="corner-up-right" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">E-mail Doorsturen</h3>
|
||||
<p class="text-[11px] text-slate-400" id="forwardSubjectLabel"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeForwardModal()" class="text-slate-400 hover:text-slate-600 p-1.5 rounded-lg transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onsubmit="submitForward(event)" class="space-y-4">
|
||||
<input type="hidden" id="forwardMailId" value="">
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Ontvanger(s) e-mailadres</label>
|
||||
<input type="text" id="forwardTo" required placeholder="collega@fociz.nl (meerdere scheiden met komma)" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-violet-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Toelichting (optioneel)</label>
|
||||
<textarea id="forwardComment" rows="4" placeholder="Eventuele toelichting voor de ontvanger..." class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl p-3 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-violet-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" onclick="closeForwardModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
Annuleren
|
||||
</button>
|
||||
<button type="submit" id="forwardSubmitBtn" class="flex items-center space-x-1.5 px-5 py-2 rounded-xl text-xs font-bold bg-violet-600 hover:bg-violet-500 text-white transition shadow-sm shadow-violet-600/30 cursor-pointer">
|
||||
<i data-lucide="send" class="w-3.5 h-3.5"></i>
|
||||
<span>Doorsturen via Outlook</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allMessages = <?= $jsonMessages ?>;
|
||||
let activeFilter = 'all'; // 'all' | 'unread'
|
||||
let selectedMessage = null;
|
||||
|
||||
function setMailFilter(filter) {
|
||||
activeFilter = filter;
|
||||
const btnAll = document.getElementById('filterBtn_all');
|
||||
const btnUnread = document.getElementById('filterBtn_unread');
|
||||
|
||||
if (filter === 'all') {
|
||||
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition";
|
||||
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition";
|
||||
} else {
|
||||
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition";
|
||||
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition";
|
||||
}
|
||||
|
||||
applyMailSearch();
|
||||
}
|
||||
|
||||
function applyMailSearch() {
|
||||
const q = (document.getElementById('mailSearchInput')?.value || '').toLowerCase().trim();
|
||||
let filtered = allMessages;
|
||||
|
||||
if (activeFilter === 'unread') {
|
||||
filtered = filtered.filter(m => !m.is_read);
|
||||
}
|
||||
|
||||
if (q) {
|
||||
filtered = filtered.filter(m => {
|
||||
const sub = (m.subject || '').toLowerCase();
|
||||
const from = (m.from_name || '').toLowerCase() + ' ' + (m.from_email || '').toLowerCase();
|
||||
const prev = (m.body_preview || '').toLowerCase();
|
||||
return sub.includes(q) || from.includes(q) || prev.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
renderMailList(filtered);
|
||||
}
|
||||
|
||||
function renderMailList(list) {
|
||||
const container = document.getElementById('mailListContainer');
|
||||
const countEl = document.getElementById('visibleMailCount');
|
||||
if (countEl) countEl.innerText = list.length;
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12 text-slate-400 text-xs">
|
||||
<i data-lucide="inbox" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||||
<p>Geen berichten gevonden.</p>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
list.forEach(m => {
|
||||
const isSelected = selectedMessage && selectedMessage.id === m.id;
|
||||
const isUnread = !m.is_read;
|
||||
const initials = (m.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||
|
||||
let cardClass = isSelected
|
||||
? 'bg-sky-50 dark:bg-sky-950/40 border-sky-400 dark:border-sky-700 shadow-xs'
|
||||
: (isUnread ? 'bg-white dark:bg-[#111624] border-slate-200 dark:border-slate-800 hover:border-slate-300 font-semibold' : 'bg-slate-50/60 dark:bg-[#0c0f17]/60 border-slate-100 dark:border-slate-800/60 text-slate-600 dark:text-slate-400');
|
||||
|
||||
html += `
|
||||
<div onclick="selectMail('${m.id}')" class="group relative p-3 rounded-2xl border transition cursor-pointer ${cardClass}">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center space-x-2 truncate">
|
||||
<div class="w-6 h-6 rounded-lg ${isUnread ? 'bg-sky-600 text-white' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400'} text-[10px] font-bold flex items-center justify-center flex-shrink-0">
|
||||
${initials}
|
||||
</div>
|
||||
<span class="text-xs ${isUnread ? 'font-black text-slate-900 dark:text-white' : 'font-medium text-slate-700 dark:text-slate-300'} truncate">
|
||||
${escapeHtml(m.from_name)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
${isUnread ? '<span class="w-2 h-2 rounded-full bg-sky-500"></span>' : ''}
|
||||
${m.has_attachments ? '<i data-lucide="paperclip" class="w-3 h-3 text-slate-400"></i>' : ''}
|
||||
<span class="text-[10px] text-slate-400 font-medium">${escapeHtml(m.received_human || '')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs ${isUnread ? 'font-bold text-slate-900 dark:text-white' : 'text-slate-800 dark:text-slate-200'} truncate">
|
||||
${escapeHtml(m.subject)}
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500 line-clamp-1 leading-snug">
|
||||
${escapeHtml(m.body_preview || '')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
async function selectMail(id) {
|
||||
const found = allMessages.find(m => m.id === id);
|
||||
if (!found) return;
|
||||
|
||||
selectedMessage = found;
|
||||
renderMailList(allMessages); // Update actieve styling
|
||||
|
||||
// Toon detail container
|
||||
document.getElementById('noMailSelectedState').classList.add('hidden');
|
||||
const activeCont = document.getElementById('mailActiveContainer');
|
||||
activeCont.classList.remove('hidden');
|
||||
|
||||
// Velden invullen met preview
|
||||
document.getElementById('detailSubject').innerText = found.subject;
|
||||
document.getElementById('detailSenderName').innerText = found.from_name;
|
||||
document.getElementById('detailSenderEmail').innerText = found.from_email;
|
||||
document.getElementById('detailReceivedHuman').innerText = found.received_human;
|
||||
document.getElementById('detailReceivedExact').innerText = found.received_at;
|
||||
document.getElementById('detailWebLink').href = found.web_link || 'https://outlook.office.com/mail/';
|
||||
|
||||
const initials = (found.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||
document.getElementById('detailSenderAvatar').innerText = initials;
|
||||
|
||||
// Body laden
|
||||
const bodyEl = document.getElementById('detailBody');
|
||||
bodyEl.innerHTML = `<div class="p-8 text-center text-slate-400"><i data-lucide="loader-2" class="w-5 h-5 animate-spin mx-auto mb-2"></i><span>Bericht laden...</span></div>`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/mail.php?action=get_message&id=${encodeURIComponent(id)}`);
|
||||
const data = await res.json();
|
||||
if (data.success && data.message) {
|
||||
bodyEl.innerHTML = data.message.body_html || `<pre class="font-sans whitespace-pre-wrap">${escapeHtml(data.message.body_text || found.body_preview)}</pre>`;
|
||||
found.is_read = true;
|
||||
updateUnreadCount();
|
||||
renderMailList(allMessages);
|
||||
} else {
|
||||
bodyEl.innerHTML = `<div class="p-4 rounded-xl bg-red-50 text-red-700 text-xs">${data.error || 'Kon bericht niet inladen.'}</div>`;
|
||||
}
|
||||
} catch(err) {
|
||||
bodyEl.innerHTML = `<p class="whitespace-pre-wrap">${escapeHtml(found.body_preview)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUnreadCount() {
|
||||
const unread = allMessages.filter(m => !m.is_read).length;
|
||||
const el = document.getElementById('countUnread');
|
||||
if (el) el.innerText = unread;
|
||||
}
|
||||
|
||||
function closeMobileDetail() {
|
||||
document.getElementById('mailActiveContainer').classList.add('hidden');
|
||||
document.getElementById('noMailSelectedState').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Modal open/close helpers
|
||||
function openPlanMailModal() {
|
||||
if (!selectedMessage) return;
|
||||
document.getElementById('planMailId').value = selectedMessage.id;
|
||||
document.getElementById('planMailSubject').value = selectedMessage.subject;
|
||||
document.getElementById('planMailWebLink').value = selectedMessage.web_link || '';
|
||||
document.getElementById('planMailSender').value = selectedMessage.from_name || '';
|
||||
|
||||
const modal = document.getElementById('planMailModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closePlanMailModal() {
|
||||
const modal = document.getElementById('planMailModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
async function submitPlanEmail(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('planMailSubmitBtn');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Inplannen...</span>`;
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mail.php?action=plan_email', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
message_id: document.getElementById('planMailId').value,
|
||||
subject: document.getElementById('planMailSubject').value,
|
||||
plan_date: document.getElementById('planMailDate').value,
|
||||
time_block_start: document.getElementById('planMailStartTime').value,
|
||||
duration_minutes: parseInt(document.getElementById('planMailDuration').value, 10),
|
||||
priority: document.getElementById('planMailPriority').value,
|
||||
web_link: document.getElementById('planMailWebLink').value,
|
||||
sender: document.getElementById('planMailSender').value
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closePlanMailModal();
|
||||
alert(`Succes! Het e-mailbericht is als taak ingepland voor ${data.plan_date}.`);
|
||||
} else {
|
||||
alert(data.error || 'Kon e-mail niet inplannen');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout: ' + err.message);
|
||||
} finally {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function openReplyModal() {
|
||||
if (!selectedMessage) return;
|
||||
document.getElementById('replyMailId').value = selectedMessage.id;
|
||||
document.getElementById('replyToLabel').innerText = `Aan: ${selectedMessage.from_name} (${selectedMessage.from_email})`;
|
||||
document.getElementById('replyContent').value = '';
|
||||
|
||||
const modal = document.getElementById('replyModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeReplyModal() {
|
||||
const modal = document.getElementById('replyModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
async function submitReply(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('replySubmitBtn');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Versturen...</span>`;
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mail.php?action=reply', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
id: document.getElementById('replyMailId').value,
|
||||
comment: document.getElementById('replyContent').value,
|
||||
reply_all: document.getElementById('replyAllCheckbox').checked
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeReplyModal();
|
||||
alert('Antwoord is succesvol verstuurd via Outlook!');
|
||||
} else {
|
||||
alert(data.error || 'Kon reactie niet versturen.');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout: ' + err.message);
|
||||
} finally {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function openForwardModal() {
|
||||
if (!selectedMessage) return;
|
||||
document.getElementById('forwardMailId').value = selectedMessage.id;
|
||||
document.getElementById('forwardSubjectLabel').innerText = `Fwd: ${selectedMessage.subject}`;
|
||||
document.getElementById('forwardTo').value = '';
|
||||
document.getElementById('forwardComment').value = '';
|
||||
|
||||
const modal = document.getElementById('forwardModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeForwardModal() {
|
||||
const modal = document.getElementById('forwardModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
async function submitForward(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('forwardSubmitBtn');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Doorsturen...</span>`;
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mail.php?action=forward', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
id: document.getElementById('forwardMailId').value,
|
||||
to: document.getElementById('forwardTo').value,
|
||||
comment: document.getElementById('forwardComment').value
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
closeForwardModal();
|
||||
alert('E-mail is succesvol doorgestuurd via Outlook!');
|
||||
} else {
|
||||
alert(data.error || 'Kon e-mail niet doorsturen.');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout: ' + err.message);
|
||||
} finally {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMailbox() {
|
||||
const icon = document.getElementById('refreshMailIcon');
|
||||
if (icon) icon.classList.add('animate-spin');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mail.php?action=get_inbox');
|
||||
const data = await res.json();
|
||||
if (data.success && data.messages) {
|
||||
allMessages = data.messages;
|
||||
applyMailSearch();
|
||||
updateUnreadCount();
|
||||
}
|
||||
} catch(err) {
|
||||
console.warn('Mail refresh error:', err);
|
||||
} finally {
|
||||
if (icon) icon.classList.remove('animate-spin');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
applyMailSearch();
|
||||
if (allMessages && allMessages.length > 0) {
|
||||
selectMail(allMessages[0].id);
|
||||
}
|
||||
|
||||
// Luister naar live websocket/polling event voor nieuwe inkomende e-mails
|
||||
window.addEventListener('fociz:mail-updated', () => {
|
||||
refreshMailbox();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -535,26 +535,71 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<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">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<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>
|
||||
<span>Kies een Karakter (24 Flitsende Avatars)</span>
|
||||
</h3>
|
||||
<span class="text-[11px] text-slate-400 dark:text-slate-500">Klik om direct toe te passen</span>
|
||||
</div>
|
||||
|
||||
<!-- Categorie Filter Pills -->
|
||||
<div class="flex flex-wrap items-center gap-1.5 pt-1">
|
||||
<button type="button" onclick="filterAvatarCategory('all')" id="avCatBtn-all" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs">
|
||||
Alle (24)
|
||||
</button>
|
||||
<button type="button" onclick="filterAvatarCategory('emoji')" id="avCatBtn-emoji" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
|
||||
⚡ Fun & Emojis (6)
|
||||
</button>
|
||||
<button type="button" onclick="filterAvatarCategory('bots')" id="avCatBtn-bots" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
|
||||
🤖 Cyber & Bots (6)
|
||||
</button>
|
||||
<button type="button" onclick="filterAvatarCategory('personas')" id="avCatBtn-personas" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
|
||||
✨ Moderne Personas (6)
|
||||
</button>
|
||||
<button type="button" onclick="filterAvatarCategory('heroes')" id="avCatBtn-heroes" class="av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300">
|
||||
🎯 Adventurer & Heroes (6)
|
||||
</button>
|
||||
</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";
|
||||
$avatarPresets = [
|
||||
['file' => 'avatar-01.svg', 'cat' => 'emoji', 'name' => 'Cool Star (Zonnebril)'],
|
||||
['file' => 'avatar-02.svg', 'cat' => 'emoji', 'name' => 'Party Champion'],
|
||||
['file' => 'avatar-03.svg', 'cat' => 'emoji', 'name' => 'Celebration Vibe'],
|
||||
['file' => 'avatar-04.svg', 'cat' => 'emoji', 'name' => 'Laser Eyes Energy'],
|
||||
['file' => 'avatar-05.svg', 'cat' => 'emoji', 'name' => 'Sunny Smiles'],
|
||||
['file' => 'avatar-06.svg', 'cat' => 'emoji', 'name' => 'Fire Heart'],
|
||||
['file' => 'avatar-07.svg', 'cat' => 'bots', 'name' => 'Cyber Tron'],
|
||||
['file' => 'avatar-08.svg', 'cat' => 'bots', 'name' => 'Aero Bot'],
|
||||
['file' => 'avatar-09.svg', 'cat' => 'bots', 'name' => 'Neon Volt'],
|
||||
['file' => 'avatar-10.svg', 'cat' => 'bots', 'name' => 'Pixel Pulse'],
|
||||
['file' => 'avatar-11.svg', 'cat' => 'bots', 'name' => 'Quantum AI'],
|
||||
['file' => 'avatar-12.svg', 'cat' => 'bots', 'name' => 'Robo Racer'],
|
||||
['file' => 'avatar-13.svg', 'cat' => 'personas', 'name' => 'Alex (Hipster)'],
|
||||
['file' => 'avatar-14.svg', 'cat' => 'personas', 'name' => 'Maya (Tech Lead)'],
|
||||
['file' => 'avatar-15.svg', 'cat' => 'personas', 'name' => 'Lucas (Designer)'],
|
||||
['file' => 'avatar-16.svg', 'cat' => 'personas', 'name' => 'Elena (Developer)'],
|
||||
['file' => 'avatar-17.svg', 'cat' => 'personas', 'name' => 'Sam (Creative)'],
|
||||
['file' => 'avatar-18.svg', 'cat' => 'personas', 'name' => 'Zoe (Founder)'],
|
||||
['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Jack (Adventurer)'],
|
||||
['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Luna (Heroine)'],
|
||||
['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Oliver (Explorer)'],
|
||||
['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Mia (Rebel)'],
|
||||
['file' => 'avatar-23.svg', 'cat' => 'heroes', 'name' => 'Leo (Cosmic)'],
|
||||
['file' => 'avatar-24.svg', 'cat' => 'heroes', 'name' => 'Nova (Cyberpunk)'],
|
||||
];
|
||||
|
||||
foreach ($avatarPresets as $av):
|
||||
$avatarPath = "/assets/avatars/{$av['file']}";
|
||||
$isSelected = ($userProfile['avatar_url'] === $avatarPath);
|
||||
?>
|
||||
<form method="POST" action="/profile.php" class="relative group">
|
||||
<form method="POST" action="/profile.php" class="relative group avatar-item" data-category="<?= $av['cat'] ?>">
|
||||
<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">
|
||||
<button type="submit" class="w-full aspect-square rounded-2xl p-1 transition-all duration-200 transform hover:scale-110 active:scale-95 hover:shadow-lg hover:shadow-indigo-500/25 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="<?= htmlspecialchars($av['name']) ?>">
|
||||
<img src="<?= $avatarPath ?>" alt="<?= htmlspecialchars($av['name']) ?>" class="w-full h-full rounded-xl object-contain pointer-events-none drop-shadow-xs">
|
||||
<?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>
|
||||
@@ -562,7 +607,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endfor; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1100,6 +1145,27 @@ function previewUploadAvatar(input) {
|
||||
}
|
||||
}
|
||||
|
||||
function filterAvatarCategory(category) {
|
||||
const items = document.querySelectorAll('.avatar-item');
|
||||
items.forEach(el => {
|
||||
const cat = el.getAttribute('data-category');
|
||||
if (category === 'all' || cat === category) {
|
||||
el.classList.remove('hidden');
|
||||
} else {
|
||||
el.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
const btns = document.querySelectorAll('.av-cat-btn');
|
||||
btns.forEach(btn => {
|
||||
btn.className = 'av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300';
|
||||
});
|
||||
const activeBtn = document.getElementById('avCatBtn-' + category);
|
||||
if (activeBtn) {
|
||||
activeBtn.className = 'av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs';
|
||||
}
|
||||
}
|
||||
|
||||
// Sync banner status met browser permissie & activeer actuele thema kaart
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialiseer geselecteerde thema-kaart
|
||||
|
||||