forked from xziino/ff14-mitigator
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 <noreply@anthropic.com>
334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
// ── 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, '>')
|
|
.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 = '<div class="plan-list-empty">Noch keine Pläne vorhanden</div>';
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = plans.map(p => `
|
|
<div class="plan-item${p.id === activePlanId ? ' active' : ''}" data-id="${escHtml(p.id)}">
|
|
<div class="plan-item-info">
|
|
<div class="plan-item-name">${escHtml(p.name)}</div>
|
|
<div class="plan-item-meta">${p.mechanics.length} Mechaniken · ${fmtDate(p.updatedAt)}</div>
|
|
</div>
|
|
<div class="plan-item-actions">
|
|
<button class="plan-btn plan-btn-copy" data-id="${escHtml(p.id)}" title="Kopieren">⎘</button>
|
|
<button class="plan-btn plan-btn-danger plan-btn-delete" data-id="${escHtml(p.id)}" title="Löschen">✕</button>
|
|
</div>
|
|
</div>
|
|
`).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 = `
|
|
<div class="card section-gap">
|
|
<div class="plan-detail-header">
|
|
<div class="plan-name-wrap">
|
|
<span id="plan-name-display" class="plan-name-text">${escHtml(plan.name)}</span>
|
|
<button id="plan-name-edit-btn" class="plan-btn" title="Umbenennen">✎</button>
|
|
</div>
|
|
<div class="plan-detail-meta">Erstellt ${fmtDate(plan.createdAt)} · ${plan.mechanics.length} Mechaniken</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card section-gap">
|
|
<div class="card-title">Jobaufstellung</div>
|
|
<div class="job-slots-placeholder">
|
|
Jobaufstellung wird in einem späteren Schritt konfigurierbar
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title-row">
|
|
<div class="card-title">Mechaniken</div>
|
|
</div>
|
|
${renderMechanicList(plan)}
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('plan-name-edit-btn')?.addEventListener('click', () => {
|
|
startRename(plan.id, plan.name);
|
|
});
|
|
}
|
|
|
|
function renderMechanicList(plan) {
|
|
if (plan.mechanics.length === 0) {
|
|
return `
|
|
<div class="empty" style="padding:30px 0">
|
|
<div class="empty-icon" style="font-size:26px">📋</div>
|
|
<h3>Noch keine Mechaniken</h3>
|
|
<p style="font-size:13px;color:var(--t3);margin-top:6px">
|
|
Importiere einen Log aus dem Analyse-Tab
|
|
</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return plan.mechanics.map(m => `
|
|
<div class="mechanic-card">
|
|
<div class="mechanic-time">${escHtml(fmtTimestamp(m.timestamp))}</div>
|
|
<div class="mechanic-body">
|
|
${m.phase ? `<div class="mechanic-phase">${escHtml(m.phase)}</div>` : ''}
|
|
<div class="mechanic-name">${escHtml(m.name)}</div>
|
|
${m.unmitigatedDamage
|
|
? `<div class="mechanic-dmg">${fmtNumber(m.unmitigatedDamage)} unmitigiert</div>`
|
|
: ''
|
|
}
|
|
<div class="mechanic-assignments">
|
|
${m.assignments.length === 0
|
|
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
|
: m.assignments.map(a =>
|
|
`<span class="badge badge-assign">${escHtml(a.job)} · ${escHtml(a.ability)}</span>`
|
|
).join('')
|
|
}
|
|
</div>
|
|
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).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 = `<input id="plan-name-input" class="plan-name-input" type="text" value="${escHtml(currentName)}">`;
|
|
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 = `<span class="plan-name-text">${escHtml(currentName)}</span>`;
|
|
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);
|
|
});
|