From 9ff8139c81b6822b72e63158c3afd086640d0e13 Mon Sep 17 00:00:00 2001 From: xziino Date: Fri, 22 May 2026 08:11:44 +0200 Subject: [PATCH 1/2] Update CLAUDE.md: Planer-Tab concept and implementation roadmap Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 93 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f7893d1..1df4d29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,10 @@ window.App = { reportCode, fightId, fightStart, fightEnd, phases: [], fights: [] - `onFightsLoaded(fights)` — wird von `app.js` aufgerufen nach Report-Load (befüllt Vergleichs-Dropdown) - `reset()` — wird von `app.js` aufgerufen wenn ein neuer Report geladen wird +`window.plannerTab` (definiert in `planner.js`) stellt Hooks bereit: +- `onTabOpen()` — wird von `tabs.js` aufgerufen wenn der Planer-Tab geöffnet wird +- `importFromAnalysis(aoeEvents, refEvents, options)` — wird vom Analyse-Tab aufgerufen beim Export + ## Dateistruktur ``` index.php — PHP-Logik: Auth-Check, Variablen, require page.php @@ -41,6 +45,7 @@ templates/ topbar.php — Topbar mit Logo + Tab-Navigation + Token-Ablaufzeit tab-report.php — Report-Tab: includes report-form, fight-select, event-explorer, output-card tab-analysis.php — Analyse-Tab: Spieler-Grid + Pull-Vergleich + AoE-Timeline HTML + tab-planner.php — Planer-Tab: Plan-Liste, Mechanik-Timeline, Job-Aufstellung (in Entwicklung) report-form.php — Report-Code-Eingabe Card fight-select.php — Fight-Auswahl Dropdown Card event-explorer.php — Event Explorer Card (Ability/DataType/EventType/Spieler-Filter) @@ -50,10 +55,12 @@ css/ layout.css — App-Shell, Topbar, Tabs, Login-Overlay, Form-Helpers components.css — Cards, Inputs, Buttons, Badges, Terminal analysis.css — Spieler-Grid, AoE-Timeline, Mitigation-Icons, HP-Bar, Ref-Row + planner.css — Planer-Tab: Plan-Liste, Mechanik-Cards, Job-Slots, Ability-Modal (in Entwicklung) js/ app.js — Formular, Fight-Dropdown, Fetch, window.App State, Event Explorer tabs.js — Tab-Switching, ruft window.analysisTab.onTabOpen() auf analysis.js — Analyse-Tab: Daten laden, Spieler rendern, Timeline rendern, Pull-Vergleich + planner.js — Planer-Tab: localStorage CRUD, Plan-Rendering, Ability-Zuweisung (in Entwicklung) auth/ start.php — PKCE generieren, Session speichern, Redirect zu FFLogs callback.php — Code gegen Token tauschen, Token in Session speichern @@ -64,6 +71,9 @@ api/ debug-events.php — POST-Endpunkt: Raw Events für Event Explorer (mit Filterung) assets/ icons/mitigation/ — Lokal gespeicherte Ability-Icons (PNG, von XIVAPI) +data/ + recast-times.json — Recast-Zeiten pro Ability (in Entwicklung) + ability-equivalents.json — Funktionale Äquivalente über Jobs hinweg (in Entwicklung) debug/ schema.php — Einmaliges Schema-Explorer Tool (nicht produktiv deployen) ``` @@ -220,7 +230,7 @@ Vollständiges Schema: siehe `debug/schema.php` oder `fflogs-schema.json` ## Planer-Tab — Konzept & Roadmap ### Ziel -Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik eingesetzt? Basierend auf Log-Daten oder manuell aufgebaut. Überlebt Browser-Neustarts via localStorage. +Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik eingesetzt? Basierend auf Log-Daten oder manuell aufgebaut. Überlebt Browser-Neustarts via localStorage. Kein Server-State — alles im Browser. ### Datenmodell (Plan) ```json @@ -236,7 +246,9 @@ Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik einges "id": "uuid", "name": "Fourth-Wall Fusion", "timestamp": 83000, + "phase": "Phase 1", "unmitigatedDamage": 280000, + "notes": "", "assignments": [ { "ability": "Reprisal", "job": "PLD" }, { "ability": "Shield Samba", "job": "BRD" } @@ -248,31 +260,42 @@ Raid-Cooldown-Planer: Welche Mitigation-Ability wird für welche Mechanik einges Mehrere Pläne gespeichert in `localStorage` unter `ff14-planner-plans` als Array. -### Import-Flow (erster Meilenstein) -**Ziel:** Einen bestehenden Log als saubere Mechanik-Vorlage laden — ohne vorhandene Mechaniken zu überschreiben. +### Primärer Import-Flow: Export aus dem Analyse-Tab +Der Haupteinstieg ist der Analyse-Tab — der Nutzer hat die Daten bereits geladen und sieht die Timeline. -1. Benutzer wählt Report-Code + Kampf (gleicher Flow wie im Report-Tab, eigenes Mini-Formular im Planer) -2. `api/analysis.php` wird aufgerufen → liefert `aoe_events` mit Name, Timestamp, `unmitigatedAmount` -3. Jedes AoE-Event wird als Mechanik-Kandidat angezeigt (Name + Timestamp + Rohschaden) -4. Benutzer kann einzelne Events auswählen oder alle übernehmen -5. **Merge-Logik:** Beim Import in einen bestehenden Plan werden nur Mechaniken hinzugefügt die noch nicht vorhanden sind — Matching per `abilityName` + Timestamp-Nähe (± 5s). Bestehende Assignments bleiben erhalten. -6. Neue Mechaniken werden an der richtigen Timestamp-Position eingefügt (Timeline bleibt sortiert) +1. Button **"In Planer exportieren"** erscheint im Analyse-Tab sobald Daten geladen sind (auch für den Ref-Log separat) +2. Dialog mit zwei Entscheidungen: + - **Was importieren?** "Nur Mechaniken" vs. "Mechaniken + erkannte Mitigations als Startpunkt" + - **Wohin?** Neuen Plan anlegen (Name eingeben) vs. bestehenden Plan überschreiben/mergen (Dropdown) +3. Bei Merge: explizite Bestätigung — niemals implizit überschreiben +4. AoE-Events werden zu Mechaniken; Phase-Information aus `phaseTransitions` wird mitübernommen +5. Weiterarbeiten im Planer-Tab -**Warum Merge statt Überschreiben:** Progress-Szenario — erster Import enthält Phase 1, späterer Import (weiter im Fight) fügt Phase 2 hinzu ohne Phase-1-Planung zu verlieren. +**Merge-Logik:** Mechaniken gelten als identisch wenn `abilityName` gleich und `|timestamp_a - timestamp_b| < 5000ms`. Nur neue Mechaniken werden hinzugefügt, bestehende Assignments bleiben erhalten. Neue Mechaniken werden timestamp-sortiert eingefügt. -### Geplante Features (Übersicht) +**Warum Merge statt Überschreiben:** Progress-Szenario — erster Import enthält Phase 1, späterer Import fügt Phase 2 hinzu ohne Phase-1-Planung zu verlieren. -| Prio | Feature | Beschreibung | +### Implementierungs-Reihenfolge + +| Schritt | Feature | Beschreibung | |---|---|---| -| 1 | **Import-Flow** | Log → Mechanik-Timeline, Merge bei Teilimporten | -| 2 | **Jobaufstellung** | 8 Slots (2 Tank, 2 Healer, 4 DPS), Auswahl bestimmt verfügbare Spells | -| 3 | **Cooldown-Zuweisung** | Pro Mechanik Abilities zuweisen/entfernen per Klick | -| 4 | **DR-Simulation** | `simuliert = unmitigated × ∏(1 − dr_i)` live berechnet beim Toggle | -| 5 | **Recast-Tracking** | Recast-Datenbank pro Ability; Konflikt-Warnung wenn CD noch läuft | -| 6 | **Coverage-Ansicht** | Gantt-Chart: Mechaniken auf X-Achse, Buff-Dauer als Balken | -| 7 | **Analyse-Overlay** | Planer-Tab: Vergleich geplanter vs. tatsächlich genutzter CDs (Job-basiertes Matching, nicht Spielername) | -| 8 | **Shield-Schätzung** | Empirisch aus Log-Durchschnitt (`absorbed`-Werte), nicht exakt | -| 9 | **JSON-Export/Import** | Plan als Datei teilen mit Raidkollegen | +| 1 | **Datenfundament** | Plan-Datenmodell + localStorage CRUD (create, read, update, delete, copy) | +| 2 | **Tab-Grundgerüst** | Leere Tab-Hülle wie Analyse-Tab; Plan-Liste; Mechanik-Timeline (read-only) | +| 3 | **Import aus Analyse-Tab** | Export-Button + Dialog (s. oben); `window.plannerTab.importFromAnalysis()` | +| 4 | **Jobaufstellung** | 8 Slots mit Job-Dropdown; bestimmt verfügbare Abilities in Schritt 5 | +| 5 | **Ability-Zuweisung** | Pro Mechanik Abilities per Modal-Picker hinzufügen/entfernen | +| 6 | **Recast-Konflikt** | `data/recast-times.json`; rote Warnung wenn CD zwischen zwei Mechaniken noch läuft | +| 7 | **DR-Simulation** | `simuliert = unmitigated × ∏(1 − dr_i)`; grün/rot ob Spieler laut Simulation überlebt | +| 8 | **Job-Äquivalente** | `data/ability-equivalents.json`; auto-substituieren beim Job-Wechsel | +| 9 | **Analyse-Overlay** | Vergleich geplanter vs. tatsächlich genutzter CDs im Analyse-Tab | + +Schritte 1–3 = nutzbarer MVP. Schritte 4–6 = praktisch einsetzbar. 7–9 = Power-Features. + +### UI-Paradigma +- Visuell dem Analyse-Tab ähneln (Cards, gleiche CSS-Variablen, einheitliches Look & Feel) +- Mechaniken als vertikale Timeline-Cards +- Ability-Picker als **Modal** (kein Inline-Dropdown) +- Nicht für mobile Geräte ausgelegt ### Spell-Verfügbarkeit nach Job Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`): @@ -301,17 +324,31 @@ Jobaufstellung → verfügbare Abilities (Subset von `MITIGATION_ABILITIES`): | RDM | Addle, Magick Barrier | | PCT | Addle, Tempera Coat, Tempera Grassa | -### Recast-Zeiten (geplante Datenbank) -Wird benötigt für Konflikt-Erkennung. Beispiele: +### Job-Äquivalente (`data/ability-equivalents.json`) +Abilities die funktional gleich sind aber unterschiedliche Namen haben — relevant beim Job-Wechsel im Slot: + +| Gruppe | Abilities | +|---|---| +| 15% Party-Mitigation | Troubadour, Tactician, Shield Samba | +| 10% Ground-Barrier | Sacred Soil, Kerachole | + +Reprisal, Feint und Addle sind identische Ability-Namen über Jobs hinweg — kein Mapping nötig, die gleiche Ability bleibt einfach bestehen. + +**Verhalten beim Job-Wechsel:** Assignment wird auto-substituiert wenn Äquivalent für neuen Job existiert (mit Hinweis "automatisch gemappt"). Kein Äquivalent → Assignment ausgegraut (nicht gelöscht, Nutzer entscheidet). + +### Recast-Zeiten (`data/recast-times.json`) +Wird für Konflikt-Erkennung benötigt (Schritt 6). Vollständige Liste wird beim Implementieren vervollständigt, Beispiele: - Reprisal: 60s - Feint / Addle: 90s +- Dark Missionary / Heart of Light: 90s - Troubadour / Tactician / Shield Samba: 120s - Temperance: 120s -- Dark Missionary / Heart of Light: 90s ### Technische Entscheidungen -- **Persistenz:** `localStorage` — kein Backend nötig, mehrere Pläne möglich +- **Persistenz:** `localStorage` unter `ff14-planner-plans` — kein Backend nötig - **IDs:** `crypto.randomUUID()` für Plan- und Mechanik-IDs -- **Merge-Matching:** Mechaniken gelten als identisch wenn `abilityName` gleich und `|timestamp_a - timestamp_b| < 5000ms` -- **Keine Spielernamen im Planer:** Assignments sind Job-basiert (`{ ability, job }`), damit Pläne übertragbar sind -- **Analyse-Tab Overlay (Prio 7):** Job aus tatsächlichem Pull → lookup welche Ability dieser Job im Plan hatte → Soll/Ist-Vergleich +- **Keine Spielernamen:** Assignments sind Job-basiert (`{ ability, job }`), damit Pläne übertragbar sind +- **Kein Ability-Stacking:** FFXIV erlaubt keine doppelte Anwendung derselben Ability — jede Ability kommt pro Mechanik maximal einmal vor, doppelte Instanzen desselben Jobs im Static sind daher kein Sonderfall +- **Analyse-Overlay (Schritt 9):** Job aus tatsächlichem Pull → lookup welche Ability dieser Job im Plan hatte → Soll/Ist-Vergleich (kein Spielername-Matching nötig) +- **Shield-Attribution:** Aktuell nicht lösbar — `absorbed` im `damage`-Event ist ein Gesamtwert ohne Aufschlüsselung per Shield. Zu untersuchen: ob `calculatedheal`-Events die Shield-Kapazität beim Auftragen mitliefern. Vorerst zurückgestellt. +- **Plan kopieren:** Duplicate-Funktion für Plan-Varianten ("Week 3 v2") ohne Original zu verlieren From ea00268227e3b7fb9ef549e7da9b602cada8ff5b Mon Sep 17 00:00:00 2001 From: xziino Date: Fri, 22 May 2026 08:26:33 +0200 Subject: [PATCH 2/2] Add Planner tab: localStorage plan CRUD and basic UI shell Steps 1+2 of the planner roadmap: data model, create/rename/copy/delete plans, read-only mechanic timeline, two-column layout mirroring the analysis tab style. Co-Authored-By: Claude Sonnet 4.6 --- css/planner.css | 220 +++++++++++++++++++++++++ js/planner.js | 333 ++++++++++++++++++++++++++++++++++++++ js/tabs.js | 1 + templates/page.php | 6 + templates/tab-planner.php | 31 ++++ templates/topbar.php | 1 + 6 files changed, 592 insertions(+) create mode 100644 css/planner.css create mode 100644 js/planner.js create mode 100644 templates/tab-planner.php diff --git a/css/planner.css b/css/planner.css new file mode 100644 index 0000000..33f27bb --- /dev/null +++ b/css/planner.css @@ -0,0 +1,220 @@ +/* ── Planner Layout ──────────────────────────────────────────────────────────── */ +.planner-layout { + display: grid; + grid-template-columns: 280px 1fr; + gap: 16px; + align-items: start; +} + +/* ── Plan Sidebar ────────────────────────────────────────────────────────────── */ +.plan-sidebar { + background: var(--bgcard); + border: 1px solid var(--border); + border-radius: var(--rl); + padding: 18px; + position: sticky; + top: 74px; +} + +.plan-sidebar-header { + display: flex; + align-items: center; + margin-bottom: 14px; +} +.plan-sidebar-header .card-title { margin-bottom: 0; flex: 1; } + +.plan-new-form { + background: var(--bg2); + border: 1px solid var(--borderem); + border-radius: var(--r); + padding: 10px; + margin-bottom: 12px; +} +.plan-new-form input { + margin-bottom: 8px; + font-size: 14px; + padding: 6px 10px; +} +.plan-new-actions { + display: flex; + gap: 6px; +} + +.plan-list-empty { + font-size: 13px; + color: var(--t3); + text-align: center; + padding: 24px 0; +} + +/* ── Plan Item ───────────────────────────────────────────────────────────────── */ +.plan-item { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 10px; + border-radius: var(--r); + border: 1px solid transparent; + cursor: pointer; + transition: all 0.12s; + margin-bottom: 4px; +} +.plan-item:hover { background: var(--bg2); border-color: var(--border); } +.plan-item.active { background: var(--bg2); border-color: var(--gold); } + +.plan-item-info { flex: 1; min-width: 0; } + +.plan-item-name { + font-size: 14px; + color: var(--t1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.plan-item.active .plan-item-name { color: var(--gold); } + +.plan-item-meta { + font-size: 12px; + color: var(--t3); + margin-top: 2px; +} + +.plan-item-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.12s; +} +.plan-item:hover .plan-item-actions { opacity: 1; } + +.plan-btn { + background: none; + border: 1px solid var(--borderem); + border-radius: var(--r); + color: var(--t2); + font-size: 13px; + padding: 2px 7px; + cursor: pointer; + transition: all 0.12s; + line-height: 1.6; +} +.plan-btn:hover { background: var(--bg3); color: var(--t1); } +.plan-btn.plan-btn-danger { color: var(--red); border-color: rgba(224,92,92,0.3); } +.plan-btn.plan-btn-danger:hover { background: var(--redbg); } + +/* ── Plan Detail ─────────────────────────────────────────────────────────────── */ +.plan-detail-header { + display: flex; + flex-direction: column; + gap: 5px; +} + +.plan-name-wrap { + display: flex; + align-items: center; + gap: 8px; +} + +.plan-name-text { + font-family: var(--font-d); + font-size: 18px; + color: var(--gold); + letter-spacing: 0.04em; +} + +.plan-name-input { + font-size: 18px !important; + font-family: var(--font-d) !important; + padding: 2px 8px !important; + width: auto !important; + min-width: 200px; + color: var(--gold) !important; +} + +.plan-detail-meta { + font-size: 13px; + color: var(--t3); +} + +/* ── Job Slots Placeholder ───────────────────────────────────────────────────── */ +.job-slots-placeholder { + padding: 20px; + text-align: center; + color: var(--t3); + font-size: 13px; + background: var(--bg2); + border: 1px dashed var(--border); + border-radius: var(--r); +} + +/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */ +.mechanic-card { + display: grid; + grid-template-columns: 52px 1fr; + gap: 14px; + padding: 12px 0; + border-bottom: 1px solid var(--border); + align-items: start; +} +.mechanic-card:last-child { border-bottom: none; } + +.mechanic-time { + font-family: var(--font-d); + font-size: 14px; + color: var(--gold); + letter-spacing: 0.03em; + padding-top: 3px; +} + +.mechanic-body { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mechanic-phase { + font-size: 11px; + color: var(--blue); + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.mechanic-name { + font-size: 15px; + color: var(--t1); + font-weight: 500; +} + +.mechanic-dmg { + font-size: 13px; + color: var(--t2); +} + +.mechanic-assignments { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 2px; +} + +.mechanic-no-assign { + font-size: 12px; + color: var(--t3); +} + +.badge-assign { + background: var(--bg2); + border: 1px solid var(--borderem); + border-radius: var(--r); + padding: 2px 8px; + font-size: 12px; + color: var(--t2); +} + +.mechanic-notes { + font-size: 12px; + color: var(--t3); + font-style: italic; + margin-top: 2px; +} diff --git a/js/planner.js b/js/planner.js new file mode 100644 index 0000000..c7549e5 --- /dev/null +++ b/js/planner.js @@ -0,0 +1,333 @@ +// ── Storage ─────────────────────────────────────────────────────────────────── + +const PLANNER_KEY = 'ff14-planner-plans'; + +function loadPlans() { + try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); } + catch { return []; } +} + +function savePlans(plans) { + localStorage.setItem(PLANNER_KEY, JSON.stringify(plans)); +} + +// ── CRUD ────────────────────────────────────────────────────────────────────── + +function createPlan(name) { + const plan = { + id: crypto.randomUUID(), + name: name.trim() || 'Unbenannter Plan', + createdAt: Date.now(), + updatedAt: Date.now(), + source: null, + jobComposition: Array(8).fill(''), + mechanics: [] + }; + const all = loadPlans(); + all.push(plan); + savePlans(all); + return plan; +} + +function getPlan(id) { + return loadPlans().find(p => p.id === id) ?? null; +} + +function updatePlan(id, changes) { + const all = loadPlans(); + const idx = all.findIndex(p => p.id === id); + if (idx === -1) return null; + all[idx] = { ...all[idx], ...changes, updatedAt: Date.now() }; + savePlans(all); + return all[idx]; +} + +function deletePlan(id) { + savePlans(loadPlans().filter(p => p.id !== id)); +} + +function copyPlan(id) { + const all = loadPlans(); + const orig = all.find(p => p.id === id); + if (!orig) return null; + const copy = { + ...JSON.parse(JSON.stringify(orig)), + id: crypto.randomUUID(), + name: orig.name + ' (Kopie)', + createdAt: Date.now(), + updatedAt: Date.now() + }; + all.push(copy); + savePlans(all); + return copy; +} + +// ── State ───────────────────────────────────────────────────────────────────── + +let activePlanId = null; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function escHtml(str) { + return String(str ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function fmtDate(ts) { + return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function fmtTimestamp(ms) { + const s = Math.floor(ms / 1000); + return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; +} + +function fmtNumber(n) { + return Number(n).toLocaleString('de-DE'); +} + +// ── Rendering: Plan List ────────────────────────────────────────────────────── + +function renderPlanList() { + const el = document.getElementById('plan-list'); + if (!el) return; + + const plans = loadPlans(); + + if (plans.length === 0) { + el.innerHTML = '
Noch keine Pläne vorhanden
'; + return; + } + + el.innerHTML = plans.map(p => ` +
+
+
${escHtml(p.name)}
+
${p.mechanics.length} Mechaniken · ${fmtDate(p.updatedAt)}
+
+
+ + +
+
+ `).join(''); + + el.querySelectorAll('.plan-item').forEach(row => { + row.addEventListener('click', e => { + if (e.target.closest('.plan-item-actions')) return; + openPlan(row.dataset.id); + }); + }); + + el.querySelectorAll('.plan-btn-copy').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const copy = copyPlan(btn.dataset.id); + if (copy) { renderPlanList(); openPlan(copy.id); } + }); + }); + + el.querySelectorAll('.plan-btn-delete').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const plan = getPlan(btn.dataset.id); + if (!plan) return; + if (!confirm(`Plan "${plan.name}" löschen?`)) return; + deletePlan(btn.dataset.id); + if (activePlanId === btn.dataset.id) { + activePlanId = null; + renderPlanDetail(null); + } + renderPlanList(); + }); + }); +} + +// ── Rendering: Plan Detail ──────────────────────────────────────────────────── + +function renderPlanDetail(plan) { + const noplan = document.getElementById('planner-no-plan'); + const content = document.getElementById('plan-content'); + if (!noplan || !content) return; + + if (!plan) { + noplan.style.display = ''; + content.style.display = 'none'; + return; + } + + noplan.style.display = 'none'; + content.style.display = ''; + + content.innerHTML = ` +
+
+
+ ${escHtml(plan.name)} + +
+
Erstellt ${fmtDate(plan.createdAt)} · ${plan.mechanics.length} Mechaniken
+
+
+ +
+
Jobaufstellung
+
+ Jobaufstellung wird in einem späteren Schritt konfigurierbar +
+
+ +
+
+
Mechaniken
+
+ ${renderMechanicList(plan)} +
+ `; + + document.getElementById('plan-name-edit-btn')?.addEventListener('click', () => { + startRename(plan.id, plan.name); + }); +} + +function renderMechanicList(plan) { + if (plan.mechanics.length === 0) { + return ` +
+
📋
+

Noch keine Mechaniken

+

+ Importiere einen Log aus dem Analyse-Tab +

+
+ `; + } + + return plan.mechanics.map(m => ` +
+
${escHtml(fmtTimestamp(m.timestamp))}
+
+ ${m.phase ? `
${escHtml(m.phase)}
` : ''} +
${escHtml(m.name)}
+ ${m.unmitigatedDamage + ? `
${fmtNumber(m.unmitigatedDamage)} unmitigiert
` + : '' + } +
+ ${m.assignments.length === 0 + ? 'Keine Zuweisung' + : m.assignments.map(a => + `${escHtml(a.job)} · ${escHtml(a.ability)}` + ).join('') + } +
+ ${m.notes ? `
${escHtml(m.notes)}
` : ''} +
+
+ `).join(''); +} + +// ── Rename ──────────────────────────────────────────────────────────────────── + +function startRename(id, currentName) { + const display = document.getElementById('plan-name-display'); + const editBtn = document.getElementById('plan-name-edit-btn'); + if (!display) return; + + display.innerHTML = ``; + const input = document.getElementById('plan-name-input'); + input.focus(); + input.select(); + if (editBtn) editBtn.style.display = 'none'; + + let saved = false; + const save = () => { + if (saved) return; + saved = true; + const newName = input.value.trim() || currentName; + updatePlan(id, { name: newName }); + renderPlanList(); + openPlan(id); + }; + + input.addEventListener('keydown', e => { + if (e.key === 'Enter') save(); + if (e.key === 'Escape') { + saved = true; + display.innerHTML = `${escHtml(currentName)}`; + if (editBtn) editBtn.style.display = ''; + } + }); + input.addEventListener('blur', save); +} + +// ── Open plan ───────────────────────────────────────────────────────────────── + +function openPlan(id) { + activePlanId = id; + renderPlanList(); + renderPlanDetail(getPlan(id)); +} + +// ── New plan form ───────────────────────────────────────────────────────────── + +function initNewPlanForm() { + const btn = document.getElementById('planner-new-btn'); + const form = document.getElementById('planner-new-form'); + const input = document.getElementById('planner-new-name'); + const save = document.getElementById('planner-new-save'); + const cancel = document.getElementById('planner-new-cancel'); + if (!btn) return; + + btn.addEventListener('click', () => { + form.style.display = ''; + input.value = ''; + input.focus(); + }); + + cancel.addEventListener('click', () => { form.style.display = 'none'; }); + + const doCreate = () => { + const name = input.value.trim(); + if (!name) { input.focus(); return; } + const plan = createPlan(name); + form.style.display = 'none'; + renderPlanList(); + openPlan(plan.id); + }; + + save.addEventListener('click', doCreate); + input.addEventListener('keydown', e => { + if (e.key === 'Enter') doCreate(); + if (e.key === 'Escape') form.style.display = 'none'; + }); +} + +// ── window.plannerTab (hooks for other tabs) ────────────────────────────────── + +window.plannerTab = { + onTabOpen() { + renderPlanList(); + if (activePlanId) { + openPlan(activePlanId); + } else { + renderPlanDetail(null); + } + }, + + importFromAnalysis(aoeEvents, _refEvents, _options) { + // Schritt 3 — noch nicht implementiert + console.log('[Planner] importFromAnalysis — not yet implemented', aoeEvents); + } +}; + +// ── Init ────────────────────────────────────────────────────────────────────── + +document.addEventListener('DOMContentLoaded', () => { + initNewPlanForm(); + renderPlanList(); + renderPlanDetail(null); +}); diff --git a/js/tabs.js b/js/tabs.js index 0f889ce..1f0dd88 100644 --- a/js/tabs.js +++ b/js/tabs.js @@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => { if (btn) btn.classList.add('active'); if (name === 'analysis') window.analysisTab?.onTabOpen?.(); + if (name === 'planner') window.plannerTab?.onTabOpen?.(); } tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab))); diff --git a/templates/page.php b/templates/page.php index 883cf8e..5210e42 100644 --- a/templates/page.php +++ b/templates/page.php @@ -8,6 +8,7 @@ + @@ -28,11 +29,16 @@ + + + diff --git a/templates/tab-planner.php b/templates/tab-planner.php new file mode 100644 index 0000000..98109ca --- /dev/null +++ b/templates/tab-planner.php @@ -0,0 +1,31 @@ +
+ + +
+
+
Pläne
+ +
+ + + +
+
+ + +
+
+
📋
+

Kein Plan ausgewählt

+

Erstelle einen neuen Plan oder wähle einen bestehenden aus

+
+ +
+ +
diff --git a/templates/topbar.php b/templates/topbar.php index b81cb22..0457f39 100644 --- a/templates/topbar.php +++ b/templates/topbar.php @@ -3,6 +3,7 @@
Token gültig bis: