Ka-Note/rolling_agenda_proto.html

1135 lines
52 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rolling Agenda (Prototyp V14 Fix: Person Ratings)</title>
<style>
:root {
--bg-color: #1e1e1e;
--card-bg: #2d2d2d;
--text-color: #e0e0e0;
--accent: #007acc;
--danger: #d9534f;
--success: #5cb85c;
--warning: #f0ad4e;
--info: #5bc0de;
--muted: #888;
--border-color: #444;
--tag-bg: #44475a;
--project-tag-bg: #6c5ce7;
--person-ref-bg: #e17055;
--sidebar-bg: #252526;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
display: flex;
height: 100vh;
overflow: hidden;
}
/* SIDEBAR */
.sidebar {
width: 250px;
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.sidebar-title {
font-size: 1.2em;
font-weight: bold;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 2px solid var(--accent);
padding-bottom: 5px;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
}
.nav-item {
padding: 10px;
margin-bottom: 5px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
color: #ccc;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.nav-item.active {
background: var(--accent);
color: white;
font-weight: bold;
}
.nav-section-header {
font-size: 0.8em;
color: #888;
text-transform: uppercase;
margin-top: 15px;
margin-bottom: 5px;
padding-left: 5px;
}
/* MAIN CONTENT */
.main-content {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
position: relative;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
border-bottom: 2px solid var(--accent);
padding-bottom: 10px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.mode-switch {
background: #333;
padding: 5px;
border-radius: 20px;
display: flex;
gap: 5px;
}
.mode-btn {
padding: 8px 16px;
border-radius: 15px;
border: none;
background: transparent;
color: #aaa;
cursor: pointer;
font-weight: bold;
transition: 0.3s;
}
.mode-btn.active {
background: var(--accent);
color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
}
.mode-btn.active.meeting-mode {
background: var(--warning);
color: #222;
}
.tabs {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
margin-top: 10px;
flex-wrap: wrap;
}
.tab-btn {
background-color: #333;
color: #aaa;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
}
.tab-btn.active {
background-color: #444;
color: white;
border-bottom: 2px solid var(--accent);
}
.view-controls {
margin-left: auto;
}
.toggle-compact-btn {
background: transparent;
border: 1px solid #555;
color: #ccc;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.toggle-compact-btn:hover {
background: #333;
}
.input-group {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 30px;
background: #252526;
padding: 15px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
input[type="text"],
textarea {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #444;
background-color: #1e1e1e;
color: white;
font-family: Consolas, monospace;
box-sizing: border-box;
}
textarea {
resize: vertical;
min-height: 80px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: 0.2s;
}
.btn-primary {
background-color: var(--accent);
color: white;
}
.btn-success {
background-color: var(--success);
color: white;
}
.btn-danger {
background-color: var(--danger);
color: white;
}
.btn-warning {
background-color: var(--warning);
color: #222;
}
.btn-info {
background-color: var(--info);
color: #222;
}
.meta-editor {
background-color: #252526;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #444;
}
.meta-field {
margin-bottom: 10px;
display: flex;
flex-direction: column;
}
.meta-field label {
font-size: 0.9em;
color: #aaa;
margin-bottom: 5px;
}
.meta-field input {
background: #111;
color: #ddd;
border: 1px solid #555;
}
.topic-card {
background-color: var(--card-bg);
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border-left: 5px solid var(--accent);
transition: all 0.2s;
cursor: move;
}
.topic-card.dragging {
opacity: 0.4;
border: 2px dashed #aaa;
}
.topic-card.drag-over-top {
border-top: 4px solid var(--accent);
margin-top: 24px;
transition: margin 0.2s;
}
.topic-card.drag-over-bottom {
border-bottom: 4px solid var(--accent);
margin-bottom: 24px;
transition: margin 0.2s;
}
.topic-card.status-new {
border-left-color: var(--success);
}
.topic-card.status-snoozed {
opacity: 0.6;
border-left-color: var(--muted);
cursor: default;
}
.topic-card.status-processed {
opacity: 0.6;
border-left-color: var(--muted);
background: #222;
cursor: default;
}
.new-badge {
background: var(--success);
color: white;
font-size: 0.7em;
padding: 2px 6px;
border-radius: 4px;
margin-left: 10px;
text-transform: uppercase;
}
.processed-badge {
background: #444;
color: #aaa;
font-size: 0.7em;
padding: 2px 6px;
border-radius: 4px;
margin-left: 10px;
}
.topic-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
cursor: pointer;
user-select: none;
}
.topic-header:hover {
background: rgba(255, 255, 255, 0.05);
}
.topic-title {
font-size: 1.2em;
font-weight: bold;
display: flex;
align-items: center;
flex-grow: 1;
}
.title-edit-btn {
opacity: 0;
color: #aaa;
margin-left: 10px;
background: none;
border: none;
cursor: pointer;
font-size: 0.8em;
}
.topic-header:hover .title-edit-btn {
opacity: 1;
}
.collapse-icon {
margin-right: 10px;
font-size: 0.8em;
color: #aaa;
transition: transform 0.2s;
display: inline-block;
}
.topic-card.collapsed .collapse-icon {
transform: rotate(-90deg);
}
.topic-card.collapsed .topic-body {
display: none;
}
.topic-card.collapsed {
border-bottom: 1px solid #444;
}
.topic-body {
padding: 0 20px 20px 20px;
border-top: 1px solid #444;
cursor: default;
}
.meeting-controls {
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #444;
flex-wrap: wrap;
}
.meeting-controls button {
flex: 1;
font-size: 0.9em;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.history {
margin-bottom: 15px;
max-height: 300px;
overflow-y: auto;
background: #222;
padding: 10px;
border-radius: 4px;
border: 1px solid #333;
margin-top: 15px;
}
.history-item {
margin-bottom: 15px;
border-bottom: 1px solid #333;
padding-bottom: 10px;
position: relative;
}
.history-item:last-child {
border-bottom: none;
}
.history-item:hover {
background: #2a2a2a;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.history-date {
color: var(--accent);
font-size: 0.8em;
font-weight: bold;
}
.edit-history-btn {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 1.2em;
opacity: 0;
padding: 0 5px;
}
.history-item:hover .edit-history-btn {
opacity: 1;
}
.edit-history-btn:hover {
color: #fff;
}
.edit-area {
width: 100%;
background: #111;
color: #ddd;
border: 1px solid var(--accent);
padding: 10px;
font-family: Consolas, monospace;
min-height: 100px;
}
.edit-actions {
display: flex;
gap: 5px;
margin-top: 5px;
justify-content: flex-end;
}
ul.tree-list {
list-style-type: none;
padding-left: 20px;
margin: 0;
border-left: 1px solid #444;
}
ul.tree-list li {
position: relative;
margin-top: 4px;
}
.toggle-btn {
position: absolute;
left: -18px;
top: 2px;
width: 14px;
height: 14px;
line-height: 12px;
text-align: center;
font-size: 10px;
background: #444;
color: #fff;
cursor: pointer;
border-radius: 2px;
user-select: none;
}
.toggle-btn:hover {
background: #666;
}
.collapsed>ul {
display: none;
}
.collapsed-list>.toggle-btn::after {
content: "+";
}
.expanded-list>.toggle-btn::after {
content: "-";
}
.list-content {
color: #ddd;
}
.assignment-tag {
background-color: var(--tag-bg);
color: #fff;
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
font-size: 0.9em;
border: 1px solid #6272a4;
display: inline-block;
margin-left: 5px;
}
.project-tag {
background-color: var(--project-tag-bg);
color: #fff;
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
font-size: 0.9em;
display: inline-block;
margin-left: 5px;
}
.arrow-highlight {
color: var(--warning);
font-weight: bold;
}
/* PERSON REFERENCE (@Name) */
.person-ref-tag {
background-color: var(--person-ref-bg);
color: #fff;
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
font-size: 0.9em;
display: inline-block;
margin-left: 5px;
cursor: pointer;
border: 1px solid transparent;
position: relative;
}
.person-ref-tag:hover {
border-color: #fff;
}
/* RATING COLORS */
.rating-1 {
border-bottom: 3px solid #d63031;
}
/* Ganz schlecht */
.rating-2 {
border-bottom: 3px solid #fab1a0;
}
/* Schlecht */
.rating-3 {
border-bottom: 3px solid #00b894;
}
/* Gut */
.rating-4 {
border-bottom: 3px solid #00cec9;
box-shadow: 0 0 5px #00cec9;
}
/* Sehr gut */
/* HOVER DIALOG */
.rating-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #333;
border: 1px solid #555;
padding: 20px;
z-index: 1000;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
border-radius: 8px;
text-align: center;
}
.rating-dialog h3 {
color: #fff;
margin-top: 0;
}
.rating-btn {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
color: white;
font-weight: bold;
}
.rb-1 {
background-color: #d63031;
}
.rb-2 {
background-color: #e17055;
}
.rb-3 {
background-color: #00b894;
}
.rb-4 {
background-color: #0984e3;
}
.rating-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.view-section {
display: none;
}
.view-section.active {
display: block;
}
.journal-day {
margin-bottom: 30px;
border-left: 2px solid #555;
padding-left: 20px;
}
.journal-date-header {
font-size: 1.3em;
color: var(--accent);
margin-bottom: 15px;
font-weight: bold;
}
.journal-entry {
margin-bottom: 15px;
background: #2d2d2d;
padding: 10px;
border-radius: 4px;
}
.journal-topic-ref {
font-size: 0.8em;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
display: block;
}
.person-card,
.dashboard-view-card {
background-color: #2d2d2d;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
border-left: 5px solid var(--info);
}
.dashboard-view-card {
border-left-color: var(--project-tag-bg);
}
.person-header {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 10px;
color: var(--info);
border-bottom: 1px solid #444;
padding-bottom: 5px;
}
.dashboard-view-header {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 10px;
color: #a29bfe;
border-bottom: 1px solid #444;
padding-bottom: 5px;
}
.person-task {
margin-bottom: 10px;
padding-left: 10px;
border-left: 2px solid #444;
}
.person-task-ref {
font-size: 0.8em;
color: #aaa;
margin-bottom: 2px;
}
.agenda-divider {
color: #888;
font-size: 0.9em;
margin: 30px 0 10px 0;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid #444;
padding-bottom: 5px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-title">My Contexts</div>
<div class="nav-section-header">General</div>
<ul class="nav-list" id="nav-general"></ul>
<div class="nav-section-header">Jour Fixes</div>
<ul class="nav-list" id="nav-contexts"></ul>
<div class="nav-section-header">Projects</div>
<ul class="nav-list" id="nav-projects"></ul>
<div class="nav-section-header">People</div>
<ul class="nav-list" id="nav-people"></ul>
</div>
<div class="main-content">
<div class="container">
<h1 id="page-title">
<span id="context-title">Loading...</span>
<div class="mode-switch" id="mode-switch-container">
<button class="mode-btn active" id="btn-prep" onclick="setMode('prep')">🛠️ Vorbereitung</button>
<button class="mode-btn" id="btn-meet" onclick="setMode('meeting')">🎤 Meeting</button>
</div>
</h1>
<div class="tabs" id="tabs-container">
<button class="tab-btn active" onclick="switchView('agenda')" id="tab-agenda">🔥 Agenda /
Eingabe</button>
<button class="tab-btn" onclick="switchView('journal')" id="tab-journal">📜 Journal / Historie</button>
<button class="tab-btn" onclick="switchView('persons')" id="tab-persons">👥 Personen (All)</button>
<button class="tab-btn" onclick="switchView('snoozed')" id="tab-snoozed">💤 Verschoben</button>
<button class="tab-btn" onclick="switchView('dashboard')" id="tab-dashboard" style="display:none">📊
Dashboard</button>
<div class="view-controls">
<button class="toggle-compact-btn" onclick="toggleAllCompact()" id="btn-compact">🔽 Alle
einklappen</button>
</div>
</div>
<div id="view-agenda" class="view-section active">
<div class="input-group">
<label style="font-size:0.9em;color:#aaa;">Neues Thema / Log-Eintrag erstellen:</label>
<input type="text" id="newTopicInput" placeholder="Titel des Themas..."
onkeypress="handleTopicEnter(event)">
<button class="btn-primary" onclick="addTopic()">+ Thema anlegen</button>
</div>
<div id="agenda-open-header" class="agenda-divider" style="display:none;">Aktuell / Offen</div>
<div id="agendaList"></div>
<div id="agenda-processed-header" class="agenda-divider" style="display:none;">Bereits abgelegt heute
</div>
<div id="processedList"></div>
</div>
<div id="view-journal" class="view-section">
<div id="journalList"></div>
</div>
<div id="view-persons" class="view-section">
<div id="personsList"></div>
</div>
<div id="view-snoozed" class="view-section">
<div id="snoozedList"></div>
</div>
<div id="view-dashboard" class="view-section">
<div id="metaEditorContainer"></div>
<div id="dashboardContent"></div>
</div>
</div>
</div>
<!-- Rating Modal -->
<div id="rating-modal" style="display:none;">
<div class="rating-overlay" onclick="closeRating()"></div>
<div class="rating-dialog">
<h3 id="rating-title">Rate Interaction</h3>
<p style="color:#aaa; font-size:0.9em;">Wie war die Zusammenarbeit?</p>
<button class="rating-btn rb-1" onclick="submitRating(1)">😡 Ganz schlecht</button>
<button class="rating-btn rb-2" onclick="submitRating(2)">🙁 Schlecht</button>
<button class="rating-btn rb-3" onclick="submitRating(3)">🙂 Gut</button>
<button class="rating-btn rb-4" onclick="submitRating(4)">🤩 Sehr gut</button>
<button style="margin-top:10px; background:transparent; border:none; color:#aaa; cursor:pointer;"
onclick="closeRating()">Abbrechen</button>
</div>
</div>
<script>
// --- STATE & DATA MODEL ---
const today = new Date().toISOString().split('T')[0];
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
const dayBeforeYesterday = new Date(Date.now() - 172800000).toISOString().split('T')[0];
const lastWeek = new Date(Date.now() - 604800000).toISOString().split('T')[0];
// Templates
const tplProject = { status: "#stInPlanning", owner: "", links: "" };
const tplPerson = { fullName: "", email: "", phone: "", duSince: "" };
let appData = {
contexts: [
{ id: 'daily-log', name: '📝 Daily Log / Inbox', type: 'meeting' },
{ id: 'jf-sysadmins', name: 'JF Team Sysadmins', type: 'meeting' },
{ id: 'jf-devs', name: 'JF Developer', type: 'meeting' },
{ id: 'p-tisax', name: 'Project TISAX', type: 'project', meta: { status: "#stInArbeit", owner: "STEFE", links: "FileServer/Projects/TISAX" } },
{ id: 'p-cloud-migration', name: 'Project CLOUD-MIGRATION', type: 'project', meta: { status: "#stBlocked", owner: "CHFI", links: "Jira-1234" } },
{ id: 'u-stefe', name: 'Person STEFE', type: 'person', meta: { fullName: "Stefan E.", email: "stefe@example.com", phone: "+49 123 456", duSince: "2020" } },
{ id: 'u-chfi', name: 'Person CHFI', type: 'person', meta: { fullName: "Christoph F.", email: "chfi@example.com", phone: "98765", duSince: "2024" } },
{ id: 'u-vendor-x', name: 'Person VENDOR-X', type: 'person', meta: { fullName: "Hr. Müller (Vendor X)", email: "sales@vendor-x.com", phone: "", duSince: "" } },
{ id: 'u-chrkl', name: 'Person ChrKl', type: 'person', meta: { ...tplPerson, fullName: "Christian Kl." } }
],
ratings: {},
topics: [
{ id: 201, contextId: 'daily-log', title: "Anruf: Lizenzprobleme Vendor X", processedInCurrentSession: true, idCollapsed: true, snoozeUntil: null, history: [{ date: today, text: "Hr. Müller angerufen. Lizenzserver ist down.\n- Ersatzticket erstellt: #INC-999\n- Eskalation an -> VENDOR-X\n- Info an Team -> STEFE\n- @VENDOR-X war sehr hilfsbereit bei der Umgehung." }] },
{ id: 202, contextId: 'daily-log', title: "Idee: Cloud-Backup Optimierung", processedInCurrentSession: false, idCollapsed: false, snoozeUntil: null, history: [{ date: yesterday, text: "Kosten laufen aus dem Ruder.\n- Idee: S3 Glacier Deep Archive nutzen?\n- Prüfen -> CHFI @P:CLOUD-MIGRATION\n- @CHFI hat super Ideen geliefert." }] },
{ id: 203, contextId: 'daily-log', title: "Quick-Fix: Drucker Buchhaltung", processedInCurrentSession: true, idCollapsed: true, snoozeUntil: null, history: [{ date: today, text: "Papierstau behoben. Toner bestellt." }] },
{ id: 204, contextId: 'daily-log', title: "Personalgespräch Vorbereitung", processedInCurrentSession: false, idCollapsed: false, snoozeUntil: null, history: [{ date: lastWeek, text: "- Ziele für Q1 prüfen\n- Schulungsbedarf klären\n- -> CHFI" }] },
{ id: 1, contextId: 'jf-sysadmins', title: "TISAX: Sperren Produktionsrechner", isNew: false, processedInCurrentSession: false, idCollapsed: false, snoozeUntil: null, history: [{ date: today, text: "- Entscheidung: Keine Ausnahmen mehr.\n- Umsetzung startet nächste Woche @P:TISAX" }, { date: lastWeek, text: "- Maßnahmen definiert:\n - Benutzergruppe für kein Internet -> STEFE @P:TISAX\n - VLAN „shared“ nutzen -> PHILO\n - IP-Range prüfen" }] },
{ id: 2, contextId: 'jf-sysadmins', title: "Greenbone-Prozess", processedInCurrentSession: false, idCollapsed: true, snoozeUntil: null, history: [{ date: yesterday, text: "Priorisierung nötig -> ERAY @P:Security" }] },
{ id: 101, contextId: 'jf-devs', title: "API Refactoring", processedInCurrentSession: false, idCollapsed: false, snoozeUntil: null, history: [{ date: today, text: "Neues Schema prüfen @P:TISAX\n- @ChrKl hat das verbockt." }] }
]
};
let currentContextId = 'daily-log';
let currentMode = 'prep';
let viewMode = 'agenda';
let draggedId = null;
let activeRatingContext = null;
let allCollapsed = false;
// --- INITIALIZATION ---
function init() {
ensureContextsExist();
renderSidebar();
loadContext(currentContextId);
}
function ensureContextsExist() {
let foundProjects = new Set(), foundPersons = new Set();
appData.topics.forEach(t => {
t.history.forEach(h => {
extractProjects(h.text).forEach(p => foundProjects.add(p));
extractAssignments(h.text).forEach(p => foundPersons.add(p));
extractMentions(h.text).forEach(p => foundPersons.add(p));
});
});
foundProjects.forEach(pName => { if (!appData.contexts.find(c => c.type === 'project' && c.name === 'Project ' + pName)) appData.contexts.push({ id: 'p-' + pName.toLowerCase(), name: 'Project ' + pName, type: 'project', meta: { ...tplProject } }); });
foundPersons.forEach(pName => { if (!appData.contexts.find(c => c.type === 'person' && c.name === 'Person ' + pName)) appData.contexts.push({ id: 'u-' + pName.toLowerCase(), name: 'Person ' + pName, type: 'person', meta: { ...tplPerson, fullName: pName } }); });
}
// --- RATING LOGIC ---
function openRating(topicId, histIdx, personName) {
activeRatingContext = { topicId, histIdx, personName };
document.getElementById('rating-title').innerText = `Rate interaction with @${personName}`;
document.getElementById('rating-modal').style.display = 'block';
}
function closeRating() { document.getElementById('rating-modal').style.display = 'none'; activeRatingContext = null; }
function submitRating(val) {
if (!activeRatingContext) return;
const key = `${activeRatingContext.topicId}-${activeRatingContext.histIdx}-${activeRatingContext.personName}`;
appData.ratings[key] = val;
closeRating();
renderAgenda();
if (viewMode === 'dashboard') loadContext(currentContextId);
}
// --- PARSERS ---
function extractAssignments(text) { const regex = /->\s*([\w]+)/g; let a = []; let m; while ((m = regex.exec(text)) !== null) a.push(m[1]); return a; }
function extractProjects(text) { const regex = /@P:([\w-]+)/g; let p = []; let m; while ((m = regex.exec(text)) !== null) p.push(m[1]); return p; }
function extractMentions(text) { const regex = /@(?!(?:P:))([\w-]+)/g; let p = []; let m; while ((m = regex.exec(text)) !== null) p.push(m[1]); return p; }
function parseToNestedList(text, topicId, histIdx) {
if (!text) return '';
const l = text.split('\n');
const n = l.map(x => {
const s = x.search(/\S|$/);
return { l: s, c: highlightTags(escapeHtml(x.trim().replace(/^[-*]\s+/, '')), topicId, histIdx), is: /^\s*[-*]/.test(x), ch: [] };
});
if (!n.some(x => x.is)) { return `<div class="list-content">${highlightTags(escapeHtml(text).replace(/\n/g, '<br>'), topicId, histIdx)}</div>`; }
let r = { l: -1, ch: [] }; let p = [r];
n.forEach(x => { if (!x.c) return; while (p.length > 1 && p[p.length - 1].l >= x.l) p.pop(); p[p.length - 1].ch.push(x); p.push(x) });
return rTree(r.ch);
}
function rTree(n) { if (!n || !n.length) return ''; let h = '<ul class="tree-list">'; n.forEach(x => { const c = x.ch.length > 0; h += `<li class="${c ? 'expanded-list' : ''}">${c ? '<span class="toggle-btn" onclick="tList(this)"></span>' : ''}<span class="list-content">${x.c}</span>${rTree(x.ch)}</li>` }); h += '</ul>'; return h; }
function highlightTags(t, topicId, histIdx) {
t = t.replace(/-&gt;\s*([\w]+)/g, '<span class="arrow-highlight">&rarr;</span> <span class="assignment-tag">$1</span>');
t = t.replace(/@P:([\w-]+)/g, '<span class="project-tag">@P:$1</span>');
if (topicId !== undefined && histIdx !== undefined) {
t = t.replace(/@(?!(?:P:))([\w-]+)/g, (match, p1) => {
const key = `${topicId}-${histIdx}-${p1}`;
const rating = appData.ratings[key];
const ratingClass = rating ? `rating-${rating}` : '';
return `<span class="person-ref-tag ${ratingClass}" onclick="openRating(${topicId}, ${histIdx}, '${p1}')">@${p1}</span>`;
});
}
return t;
}
function escapeHtml(t) { return t ? t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") : ''; }
window.tList = function (b) { const l = b.parentElement; if (l.classList.contains('expanded-list')) { l.classList.remove('expanded-list'); l.classList.add('collapsed-list'); l.querySelector('ul').style.display = 'none'; } else { l.classList.remove('collapsed-list'); l.classList.add('expanded-list'); l.querySelector('ul').style.display = 'block'; } };
// --- VIEW LOGIC (Restored) ---
function setMode(m) { currentMode = m; document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active', 'meeting-mode')); const x = document.getElementById(m === 'prep' ? 'btn-prep' : 'btn-meet'); if (x) { x.classList.add('active'); if (m === 'meeting') x.classList.add('meeting-mode'); } renderAgenda(); }
function switchView(vm) {
viewMode = vm;
document.querySelectorAll('.view-section').forEach(e => e.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(e => e.classList.remove('active'));
document.getElementById(`view-${vm}`).classList.add('active');
const tb = document.getElementById(`tab-${vm.replace('dashboard', 'dashboard')}`);
if (tb) tb.classList.add('active');
const compBtn = document.getElementById('btn-compact');
if (compBtn) compBtn.style.display = (vm === 'agenda' ? 'block' : 'none');
if (vm === 'agenda') renderAgenda();
if (vm === 'journal') renderJournal();
if (vm === 'persons') renderPersons();
if (vm === 'snoozed') renderSnoozed();
}
// --- RENDERERS ---
function renderAgenda() {
if (viewMode !== 'agenda') return;
const c1 = document.getElementById('agendaList'); const c2 = document.getElementById('processedList');
if (!c1 || !c2) return;
c1.innerHTML = ''; c2.innerHTML = '';
const contextTopics = appData.topics.filter(t => t.contextId === currentContextId);
let openTopics = contextTopics.filter(t => !t.snoozeUntil && !t.processedInCurrentSession);
let processedTopics = contextTopics.filter(t => t.processedInCurrentSession);
let showOpen = currentMode === 'meeting' || currentContextId === 'daily-log';
let showProcessed = (currentMode === 'meeting' && processedTopics.length > 0);
document.getElementById('agenda-open-header').style.display = showOpen ? 'block' : 'none';
document.getElementById('agenda-processed-header').style.display = showProcessed ? 'block' : 'none';
if (currentMode !== 'meeting' && currentContextId !== 'daily-log') { if (processedTopics.length > 0) openTopics = [...openTopics, ...processedTopics]; processedTopics = []; }
openTopics.forEach(t => c1.appendChild(createTopicCard(t, false)));
processedTopics.forEach(t => c2.appendChild(createTopicCard(t, true)));
}
function createTopicCard(topic, isProcessed) {
const card = document.createElement('div');
const collapsedClass = topic.idCollapsed ? 'collapsed' : '';
card.className = `topic-card ${topic.isNew ? 'status-new' : ''} ${isProcessed ? 'status-processed' : ''} ${collapsedClass}`;
card.id = `card-${topic.id}`;
const isDraggable = !isProcessed && !topic.snoozeUntil;
if (isDraggable) { card.setAttribute('draggable', 'true'); card.ondragstart = (e) => handleDragStart(e, topic.id); card.ondragend = handleDragEnd; card.ondragover = handleDragOver; card.ondragleave = handleDragLeave; card.ondrop = (e) => handleDrop(e, topic.id); }
let badge = topic.isNew ? `<span class="new-badge">NEU</span>` : ''; if (isProcessed) badge += `<span class="processed-badge">BESPROCHEN/GEMERKT</span>`;
let controls = '';
if (currentContextId === 'daily-log') {
controls = `<div class="meeting-controls"><button class="btn-success" onclick="actionKeepOpen(${topic.id})">💬 Notieren</button><button class="btn-primary" onclick="actionDone(${topic.id})">✅ Erledigt / Archivieren</button><button class="btn-warning" onclick="actionSnooze(${topic.id})">zzz Später</button></div>`;
} else {
if (currentMode === 'meeting' && !isProcessed) { controls = `<div class="meeting-controls"><button class="btn-success" onclick="actionKeepOpen(${topic.id})">💬 Notieren & Offen</button><button class="btn-warning" onclick="actionSnooze(${topic.id})">zzz Verschieben</button><button class="btn-info" onclick="actionSkip(${topic.id})">⏭️ Überspringen</button><button class="btn-danger" onclick="actionDone(${topic.id})">✅ Erledigt</button></div>`; }
else if (isProcessed || currentMode === 'prep') { controls = `<div class="meeting-controls"><button class="btn-success" onclick="addNoteOnly(${topic.id})">💾 Speichern</button><button class="btn-danger" style="margin-left:auto" onclick="actionDone(${topic.id})">Archivieren</button></div>`; }
}
const historyHtml = topic.history.map((n, idx) => `<div class="history-item" id="hist-${topic.id}-${idx}"><div class="history-header"><span class="history-date">[${n.date}]</span><button class="edit-history-btn" onclick="editHistory(${topic.id}, ${idx})">✎</button></div><div class="history-content-viewer">${parseToNestedList(n.text, topic.id, idx)}</div></div>`).reverse().join('');
card.innerHTML = `<div class="topic-header" onclick="toggleTopicCard(${topic.id})" draggable="false"><div class="topic-title"><span class="collapse-icon">▼</span><span id="title-text-${topic.id}">${topic.title}</span><button class="title-edit-btn" onclick="event.stopPropagation(); editTitle(${topic.id})">✎</button>${badge}</div></div><div class="topic-body"><div class="history">${historyHtml}</div><div class="add-note-area" style="display:flex; flex-direction:column; gap:5px; margin-top:10px;"><textarea id="note-input-${topic.id}" placeholder="- Notiz... (z.B. '-> NAME' or '@P:PROJECT')" onclick="event.stopPropagation()"></textarea>${controls}</div></div>`;
return card;
}
function renderDashboardContent(name, type) {
const c = document.getElementById('dashboardContent'); c.innerHTML = '';
let re = [];
appData.topics.forEach(t => {
t.history.forEach((n, idx) => { // Use idx for rating key
let matches = false, isRef = false;
if (type === 'project') matches = extractProjects(n.text).includes(name);
else {
matches = extractAssignments(n.text).includes(name);
if (!matches) {
// Check for @Mention references
if (extractMentions(n.text).includes(name)) { matches = true; isRef = true; }
}
}
if (matches) {
// Retrieve Rating if person mention
let ratingInfo = '';
if (type === 'person') {
const key = `${t.id}-${idx}-${name}`;
const r = appData.ratings[key];
if (r) {
const labels = ["Ganz Schlecht", "Schlecht", "Gut", "Sehr Gut"];
const colors = ["#d63031", "#e17055", "#00b894", "#0984e3"];
ratingInfo = `<span style="float:right; font-size:0.8em; padding:2px 6px; border-radius:4px; margin-left:10px; background:${colors[r - 1]}; color:white;">Rating: ${labels[r - 1]}</span>`;
}
}
re.push({ tt: t.title, cn: appData.contexts.find(x => x.id === t.contextId)?.name || 'Unknown', d: n.date, x: n.text, tid: t.id, hidx: idx, ri: ratingInfo });
}
});
});
if (!re.length) { c.innerHTML = '<div style="color:#888;">No entries found.</div>'; return; }
re.sort((a, b) => new Date(b.d) - new Date(a.d));
let h = ''; re.forEach(e => { h += `<div class="person-task"><div class="person-task-ref">${e.d} in <span style="color:var(--accent)">${e.cn}</span>${e.ri}: ${e.tt}</div>${parseToNestedList(e.x, e.tid, e.hidx)}</div>`; });
c.innerHTML = `<div class="dashboard-view-card"><div class="dashboard-view-header">Activity Log for ${name}</div>${h}</div>`;
}
// --- METADATA EDITORS ---
function renderProjectMeta(ctx) { const c = document.getElementById('metaEditorContainer'); const m = ctx.meta || { ...tplProject }; c.innerHTML = `<div class="meta-editor"><h3 style="margin-top:0; color:#aaa; border-bottom:1px solid #444; padding-bottom:5px;">📋 Projekt Metadaten</h3><div class="meta-field"><label>Status:</label><input type="text" value="${m.status}" onchange="updMeta('${ctx.id}','status',this.value)"></div><div class="meta-field"><label>Verantwortlich:</label><input type="text" value="${m.owner}" onchange="updMeta('${ctx.id}','owner',this.value)"></div><div class="meta-field"><label>Links:</label><input type="text" value="${m.links}" onchange="updMeta('${ctx.id}','links',this.value)"></div></div>`; }
function renderPersonMeta(ctx) { const c = document.getElementById('metaEditorContainer'); const m = ctx.meta || { ...tplPerson }; c.innerHTML = `<div class="meta-editor"><h3 style="margin-top:0; color:#aaa; border-bottom:1px solid #444; padding-bottom:5px;">👤 Person Metadaten</h3><div class="meta-field"><label>Voller Name:</label><input type="text" value="${m.fullName}" onchange="updMeta('${ctx.id}','fullName',this.value)"></div><div class="meta-field"><label>Email (📨):</label><input type="text" value="${m.email}" onchange="updMeta('${ctx.id}','email',this.value)"></div><div class="meta-field"><label>Telefon (☎️):</label><input type="text" value="${m.phone}" onchange="updMeta('${ctx.id}','phone',this.value)"></div><div class="meta-field"><label>Per Du seit:</label><input type="text" value="${m.duSince}" onchange="updMeta('${ctx.id}','duSince',this.value)"></div></div>`; }
window.updMeta = function (cid, f, v) { const c = appData.contexts.find(x => x.id === cid); if (c) { if (!c.meta) c.meta = {}; c.meta[f] = v; } };
// --- HELPERS & ACTIONS ---
function renderSidebar() {
const genList = document.getElementById('nav-general'); const ctxList = document.getElementById('nav-contexts'); const prjList = document.getElementById('nav-projects'); const pplList = document.getElementById('nav-people');
genList.innerHTML = ''; ctxList.innerHTML = ''; prjList.innerHTML = ''; pplList.innerHTML = '';
const dailyLog = appData.contexts.find(c => c.id === 'daily-log');
if (dailyLog) { const li = document.createElement('li'); li.className = `nav-item ${dailyLog.id === currentContextId ? 'active' : ''}`; li.innerText = `${dailyLog.name}`; li.onclick = () => loadContext(dailyLog.id); genList.appendChild(li); }
appData.contexts.filter(c => c.type === 'meeting' && c.id !== 'daily-log').forEach(c => { const li = document.createElement('li'); li.className = `nav-item ${c.id === currentContextId ? 'active' : ''}`; li.innerText = `📅 ${c.name}`; li.onclick = () => loadContext(c.id); ctxList.appendChild(li); });
appData.contexts.filter(c => c.type === 'project').forEach(c => { const pName = c.name.replace('Project ', ''); const isActive = currentContextId === c.id; const li = document.createElement('li'); li.className = `nav-item ${isActive ? 'active' : ''}`; li.innerText = `🚀 ${pName}`; li.onclick = () => loadContext(c.id); prjList.appendChild(li); });
appData.contexts.filter(c => c.type === 'person').forEach(c => { const pName = c.name.replace('Person ', ''); const isActive = currentContextId === c.id; const li = document.createElement('li'); li.className = `nav-item ${isActive ? 'active' : ''}`; li.innerText = `👤 ${pName}`; li.onclick = () => loadContext(c.id); pplList.appendChild(li); });
}
function loadContext(contextId) {
currentContextId = contextId; const ctx = appData.contexts.find(c => c.id === contextId);
if (ctx.type === 'meeting') {
document.getElementById('context-title').innerText = ctx.name;
if (ctx.id === 'daily-log') { document.getElementById('mode-switch-container').style.display = 'none'; currentMode = 'meeting'; }
else { document.getElementById('mode-switch-container').style.display = 'flex'; }
document.getElementById('tab-agenda').style.display = 'inline-block'; document.getElementById('tab-journal').style.display = 'inline-block'; document.getElementById('tab-persons').style.display = 'inline-block'; document.getElementById('tab-dashboard').style.display = 'none'; switchView('agenda');
} else {
const pName = ctx.name.replace(ctx.type === 'project' ? 'Project ' : 'Person ', ''); document.getElementById('context-title').innerText = ctx.name; document.getElementById('mode-switch-container').style.display = 'none'; document.getElementById('tab-agenda').style.display = 'none'; document.getElementById('tab-dashboard').style.display = 'inline-block'; document.getElementById('tab-dashboard').innerText = `📊 Dashboard: ${pName}`; switchView('dashboard');
if (ctx.type === 'project') { renderProjectMeta(ctx); renderDashboardContent(pName, 'project'); } else { renderPersonMeta(ctx); renderDashboardContent(pName, 'person'); }
}
renderSidebar();
}
function renderJournal() { const c = document.getElementById('journalList'); c.innerHTML = ''; const ct = appData.topics.filter(t => t.contextId === currentContextId); let an = []; ct.forEach(t => { t.history.forEach((h, idx) => { an.push({ ...h, title: t.title, tid: t.id, hidx: idx }) }) }); an.sort((a, b) => new Date(b.date) - new Date(a.date)); let cd = null; let dc = null; an.forEach(n => { if (n.date !== cd) { cd = n.date; dc = document.createElement('div'); dc.className = 'journal-day'; dc.innerHTML = `<div class="journal-date-header">${cd}</div>`; c.appendChild(dc); } const e = document.createElement('div'); e.className = 'journal-entry'; e.innerHTML = `<span class="journal-topic-ref">${n.title}</span>${parseToNestedList(n.text, n.tid, n.hidx)}`; dc.appendChild(e); }); }
function renderPersons() { const c = document.getElementById('personsList'); c.innerHTML = ''; const ct = appData.topics.filter(t => t.contextId === currentContextId); let a = {}; ct.forEach(t => { t.history.forEach(n => { extractAssignments(n.text).forEach(p => { if (!a[p]) a[p] = []; a[p].push({ tt: t.title, d: n.date, l: n.text }) }) }) }); if (!Object.keys(a).length) { c.innerHTML = '<div style="color:#888;">Keine Aufgaben.</div>'; return; } Object.keys(a).sort().forEach(p => { const k = document.createElement('div'); k.className = 'person-card'; k.innerHTML = `<div class="person-header">👤 ${p}</div>` + a[p].map(t => `<div class="person-task"><div class="person-task-ref">${t.d} - ${t.tt}</div>${parseToNestedList(t.l)}</div>`).join(''); c.appendChild(k); }); }
function renderSnoozed() { const c = document.getElementById('snoozedList'); const s = appData.topics.filter(t => t.contextId === currentContextId && t.snoozeUntil); c.innerHTML = s.length ? '' : '<div style="color:#888;text-align:center">Leer.</div>'; s.forEach(t => { const k = document.createElement('div'); k.className = 'topic-card status-snoozed'; k.innerHTML = `<div class="topic-header"><span class="topic-title">${t.title}</span><button class="btn-success" onclick="unsnooze(${t.id})">Reaktivieren</button></div><div style="padding:15px;color:#aaa">Bis: ${t.snoozeUntil}</div>`; c.appendChild(k); }); }
function getNoteInput(id) { const el = document.getElementById(`note-input-${id}`); return el ? el.value.trim() : ""; }
function addTopic() { const input = document.getElementById('newTopicInput'); if (!input.value.trim()) return; appData.topics.unshift({ id: Date.now(), contextId: currentContextId, title: input.value, isNew: true, snoozeUntil: null, processedInCurrentSession: false, idCollapsed: false, history: [{ date: today, text: "Thema angelegt." }] }); input.value = ''; renderAgenda(); }
function saveNote(id) {
const text = getNoteInput(id);
const topic = appData.topics.find(t => t.id === id);
if (text && topic) {
topic.history.push({ date: today, text: text });
topic.isNew = false;
extractProjects(text).forEach(p => { if (!appData.contexts.find(c => c.type === 'project' && c.name === 'Project ' + p)) appData.contexts.push({ id: 'p-' + p.toLowerCase(), name: 'Project ' + p, type: 'project', meta: { ...tplProject } }); });
extractAssignments(text).forEach(p => { if (!appData.contexts.find(c => c.type === 'person' && c.name === 'Person ' + p)) appData.contexts.push({ id: 'u-' + p.toLowerCase(), name: 'Person ' + p, type: 'person', meta: { ...tplPerson, fullName: p } }); });
extractMentions(text).forEach(p => { if (!appData.contexts.find(c => c.type === 'person' && c.name === 'Person ' + p)) appData.contexts.push({ id: 'u-' + p.toLowerCase(), name: 'Person ' + p, type: 'person', meta: { ...tplPerson, fullName: p } }); });
renderSidebar();
return true;
}
return false;
}
function actionKeepOpen(id) { saveNote(id); const t = appData.topics.find(x => x.id === id); t.processedInCurrentSession = true; t.isNew = false; renderAgenda(); }
function actionDone(id) { if (confirm("Archivieren?")) { saveNote(id); appData.topics = appData.topics.filter(x => x.id !== id); renderAgenda(); } }
function actionSkip(id) { const t = appData.topics.find(x => x.id === id); t.processedInCurrentSession = true; renderAgenda(); }
function actionSnooze(id) { const i = prompt("Datum:", "2026-06-01"); if (i) { saveNote(id); const t = appData.topics.find(x => x.id === id); t.snoozeUntil = i; t.processedInCurrentSession = true; renderAgenda(); } }
function unsnooze(id) { const t = appData.topics.find(x => x.id === id); t.snoozeUntil = null; t.isNew = false; renderSnoozed(); renderAgenda(); }
function addNoteOnly(id) { if (saveNote(id)) renderAgenda(); }
function toggleAllCompact() { allCollapsed = !allCollapsed; appData.topics.forEach(t => t.idCollapsed = allCollapsed); renderAgenda(); }
function toggleTopicCard(id) { const t = appData.topics.find(x => x.id === id); t.idCollapsed = !t.idCollapsed; renderAgenda(); }
function editHistory(id, idx) { const c = document.getElementById(`hist-${id}-${idx}`); const t = appData.topics.find(x => x.id === id); if (c) c.innerHTML = `<div class="edit-mode"><textarea class="edit-area" id="edit-area-${id}-${idx}">${t.history[idx].text}</textarea><div class="edit-actions"><button class="btn-success" onclick="saveHistoryEdit(${id},${idx})">Speichern</button></div></div>`; }
function saveHistoryEdit(id, idx) { const t = appData.topics.find(x => x.id === id); const newText = document.getElementById(`edit-area-${id}-${idx}`).value; t.history[idx].text = newText; extractProjects(newText).forEach(p => { if (!appData.contexts.find(c => c.type === 'project' && c.name === 'Project ' + p)) appData.contexts.push({ id: 'p-' + p.toLowerCase(), name: 'Project ' + p, type: 'project', meta: { ...tplProject } }); }); extractAssignments(newText).forEach(p => { if (!appData.contexts.find(c => c.type === 'person' && c.name === 'Person ' + p)) appData.contexts.push({ id: 'u-' + p.toLowerCase(), name: 'Person ' + p, type: 'person', meta: { ...tplPerson, fullName: p } }); }); extractMentions(newText).forEach(p => { if (!appData.contexts.find(c => c.type === 'person' && c.name === 'Person ' + p)) appData.contexts.push({ id: 'u-' + p.toLowerCase(), name: 'Person ' + p, type: 'person', meta: { ...tplPerson, fullName: p } }); }); renderAgenda(); init(); }
function editTitle(id) { const t = appData.topics.find(x => x.id === id); const n = prompt("Titel", t.title); if (n) { t.title = n; renderAgenda(); } }
function handleTopicEnter(e) { if (e.key === 'Enter') addTopic(); }
function handleDragStart(e, id) { draggedId = id; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', id); setTimeout(() => e.target.classList.add('dragging'), 0); }
function handleDragEnd(e) { e.target.classList.remove('dragging'); document.querySelectorAll('.topic-card').forEach(el => el.classList.remove('drag-over-top', 'drag-over-bottom')); draggedId = null; }
function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const card = e.target.closest('.topic-card'); if (!card || card.classList.contains('dragging')) return; const rect = card.getBoundingClientRect(); const offset = e.clientY - rect.top; if (offset < rect.height / 2) { card.classList.add('drag-over-top'); card.classList.remove('drag-over-bottom'); } else { card.classList.add('drag-over-bottom'); card.classList.remove('drag-over-top'); } }
function handleDragLeave(e) { const card = e.target.closest('.topic-card'); if (card) card.classList.remove('drag-over-top', 'drag-over-bottom'); }
function handleDrop(e, targetId) { e.preventDefault(); const card = e.target.closest('.topic-card'); if (!card) return; const draggingBefore = card.classList.contains('drag-over-top'); card.classList.remove('drag-over-top', 'drag-over-bottom'); if (draggedId === targetId || !draggedId) return; const fromIndex = appData.topics.findIndex(t => t.id === draggedId); const toIndex = appData.topics.findIndex(t => t.id === targetId); if (fromIndex < 0 || toIndex < 0) return; const [movedItem] = appData.topics.splice(fromIndex, 1); let newToIndex = appData.topics.findIndex(t => t.id === targetId); if (draggingBefore) appData.topics.splice(newToIndex, 0, movedItem); else appData.topics.splice(newToIndex + 1, 0, movedItem); renderAgenda(); }
init();
</script>
</body>
</html>