diff --git a/css/layout.css b/css/layout.css
index 866a347..f5c485d 100644
--- a/css/layout.css
+++ b/css/layout.css
@@ -98,6 +98,43 @@
color: var(--t2);
}
+.report-load-row {
+ flex-wrap: nowrap;
+}
+
+.report-code-field {
+ flex: 1 1 360px;
+ min-width: 240px;
+}
+
+.report-language-field {
+ flex: 0 0 120px;
+ min-width: 100px;
+}
+
+.report-fight-field {
+ flex: 1 1 360px;
+ min-width: 260px;
+}
+
+.report-fight-label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.report-fight-label a {
+ color: var(--gold);
+ font-size: 12px;
+}
+
+@media (max-width: 1100px) {
+ .report-load-row {
+ flex-wrap: wrap;
+ }
+}
+
/* ── Topbar user badge ───────────────────────────────────────────────────────── */
.topbar-user {
font-size: 13px;
diff --git a/css/planner.css b/css/planner.css
index f55d9ac..bc40f7c 100644
--- a/css/planner.css
+++ b/css/planner.css
@@ -1762,11 +1762,6 @@
/* ── View Toggle ─────────────────────────────────────────────────────────── */
-.view-toggle-btns {
- display: flex;
- gap: 4px;
-}
-
.planner-card-actions {
margin-left: auto;
display: flex;
@@ -1775,6 +1770,31 @@
flex-wrap: wrap;
}
+.planner-class-filter-wrap {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--t3);
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+.planner-class-filter {
+ width: auto;
+ min-width: 88px;
+ padding: 4px 9px;
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ background: var(--bg2);
+ color: var(--t1);
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.planner-class-filter:hover {
+ border-color: var(--t3);
+}
+
.cactbot-export-modal-box {
max-width: 720px;
max-height: min(86vh, 780px);
@@ -1971,7 +1991,7 @@
font-weight: 600;
}
-/* ── Meine Spells ────────────────────────────────────────────────────────── */
+/* ── Simple View Rows ────────────────────────────────────────────────────── */
.myspells-controls {
display: flex;
@@ -2040,6 +2060,18 @@
white-space: nowrap;
}
+.myspells-ability .badge-remove {
+ opacity: .45;
+}
+
+.myspells-ability:hover .badge-remove {
+ opacity: .8;
+}
+
+.myspells-ability .badge-remove:hover {
+ opacity: 1;
+}
+
.myspells-ability.myspells-type--debuff { color: var(--orange); border-color: rgba(255,140,0,0.3); }
.myspells-ability.myspells-type--shield { color: var(--blue); border-color: rgba(88,166,255,0.3); }
.myspells-ability.myspells-type--personal { color: #dbc7ff; border-color: rgba(177,112,255,0.4); }
diff --git a/js/planner.js b/js/planner.js
index 63bb773..276cfa5 100644
--- a/js/planner.js
+++ b/js/planner.js
@@ -3,6 +3,8 @@
const PLANNER_KEY = 'ff14-planner-plans';
const PLANNER_ACTIVE_KEY = 'ff14-planner-active-plan';
const FOLDERS_KEY = 'ff14-planner-folders';
+const PLANNER_SIMPLE_VIEW_KEY = 'ff14-planner-simple-view';
+const PLANNER_CLASS_FILTER_KEY = 'ff14-planner-class-filter';
function loadPlans() {
try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); }
@@ -255,6 +257,33 @@ function visiblePlanMechanics(plan) {
.sort((a, b) => a.timestamp - b.timestamp);
}
+function plannerClassFilterJobs(plan) {
+ return [...new Set((plan?.jobComposition ?? []).filter(Boolean))];
+}
+
+function plannerViewOptions(plan = null) {
+ const savedJob = localStorage.getItem(PLANNER_CLASS_FILTER_KEY) ?? '';
+ const jobs = plan ? plannerClassFilterJobs(plan) : [];
+ const job = plan && savedJob && !jobs.includes(savedJob) ? '' : savedJob;
+ if (savedJob && savedJob !== job) localStorage.setItem(PLANNER_CLASS_FILTER_KEY, job);
+ return {
+ simple: localStorage.getItem(PLANNER_SIMPLE_VIEW_KEY) === '1',
+ job,
+ };
+}
+
+function renderClassFilterOptions(plan, selectedJob = '') {
+ const jobs = plannerClassFilterJobs(plan);
+ return [
+ ``,
+ ...jobs.map(job => ``),
+ ].join('');
+}
+
+function assignmentMatchesClassFilter(assignment, job) {
+ return !job || assignment.job === job;
+}
+
// ── Rendering: Plan List ──────────────────────────────────────────────────────
function planItemHtml(p) {
@@ -431,6 +460,7 @@ function renderPlanDetail(plan) {
content.style.display = '';
const visibleMechanics = visiblePlanMechanics(plan);
+ const viewOptions = plannerViewOptions(plan);
content.innerHTML = `
@@ -485,28 +515,19 @@ function renderPlanDetail(plan) {
Mechaniken
-
-
-
-
+
+
${renderMechanicListHtml(plan)}
-
-
-
-
-
-
-
`;
@@ -523,68 +544,12 @@ function renderPlanDetail(plan) {
initTimelineOptions(plan.id);
initTimeline(plan.id);
initMechanicClicks(plan.id);
- initMySpells(plan.id);
+ initPlannerViewControls(plan.id);
initCactbotExport(plan.id);
renderInfoPanel(plan);
ensureActionMetaLoaded().then(() => refreshMechanicList(plan.id));
}
-// ── Meine Spells ─────────────────────────────────────────────────────────────
-
-function renderMySpellsHtml(plan, job) {
- if (!job) return 'Job auswählen um Assignments zu sehen.
';
-
- const mechanics = visiblePlanMechanics(plan);
- const rows = [];
- for (const mechanic of mechanics) {
- const mine = (mechanic.assignments ?? []).filter(a => a.job === job);
- if (!mine.length) continue;
- rows.push({ mechanic, assignments: mine });
- }
-
- if (!rows.length) {
- return `Keine Assignments für ${escHtml(job)} in diesem Plan.
`;
- }
-
- return rows.map(({ mechanic, assignments }) => {
- const time = escHtml(fmtTimestamp(mechanic.timestamp));
- const mechName = escHtml(mechanic.name);
- const abilities = assignments.map(a => {
- const iconSrc = abilityIcon(a.ability);
- const icon = iconSrc
- ? `
`
- : '';
- const name = escHtml(assignmentAbilityName(a, plan));
- const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(a.ability);
- const typeClass = a.buffType === 'debuff' ? 'myspells-type--debuff'
- : a.buffType === 'shield' ? 'myspells-type--shield'
- : isPersonal ? 'myspells-type--personal'
- : 'myspells-type--buff';
- return `${icon}${name}`;
- }).join('');
- return `
-
-
${time}
-
${mechName}
-
${abilities}
-
`;
- }).join('');
-}
-
-function mySpellsPlainText(plan, job) {
- if (!job) return '';
- const mechanics = visiblePlanMechanics(plan);
- const lines = [`Meine Spells — ${job} (${plan.name})`, '─'.repeat(36)];
- for (const mechanic of mechanics) {
- const mine = (mechanic.assignments ?? []).filter(a => a.job === job);
- if (!mine.length) continue;
- const time = fmtTimestamp(mechanic.timestamp);
- const names = mine.map(a => assignmentAbilityName(a, plan)).join(', ');
- lines.push(`${time.padEnd(6)} ${mechanic.name.padEnd(22)} → ${names}`);
- }
- return lines.join('\n');
-}
-
function cactbotEscape(text) {
return String(text ?? '')
.replace(/\\/g, '\\\\')
@@ -900,55 +865,30 @@ function initCactbotExport(planId) {
});
}
-function initMySpells(planId) {
- const viewBtns = document.querySelectorAll('.view-toggle-btn');
- const mechList = document.getElementById('mechanic-list');
- const myPanel = document.getElementById('myspells-panel');
- const jobSelect = document.getElementById('myspells-job-select');
- const myList = document.getElementById('myspells-list');
- const copyBtn = document.getElementById('myspells-copy-btn');
- if (!mechList || !myPanel || !jobSelect || !myList) return;
+function initPlannerViewControls(planId) {
+ const simpleBtn = document.getElementById('planner-simple-view-btn');
+ const classFilter = document.getElementById('planner-class-filter');
- viewBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- viewBtns.forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
- const view = btn.dataset.view;
- mechList.style.display = view === 'mechanics' ? '' : 'none';
- myPanel.style.display = view === 'myspells' ? '' : 'none';
- if (view === 'myspells') {
- const plan = loadPlans().find(p => p.id === planId);
- if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value || '');
- }
- });
+ simpleBtn?.addEventListener('click', () => {
+ const enabled = !simpleBtn.classList.contains('active');
+ simpleBtn.classList.toggle('active', enabled);
+ localStorage.setItem(PLANNER_SIMPLE_VIEW_KEY, enabled ? '1' : '0');
+ refreshMechanicList(planId, false);
});
- // Gespeicherten Job wiederherstellen
- const savedJob = localStorage.getItem('ff14-planner-myspells-job') ?? '';
- if (savedJob && [...jobSelect.options].some(o => o.value === savedJob)) {
- jobSelect.value = savedJob;
- const plan = loadPlans().find(p => p.id === planId);
- if (plan) myList.innerHTML = renderMySpellsHtml(plan, savedJob);
- }
-
- jobSelect.addEventListener('change', () => {
- localStorage.setItem('ff14-planner-myspells-job', jobSelect.value);
- const plan = loadPlans().find(p => p.id === planId);
- if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value);
+ classFilter?.addEventListener('change', () => {
+ localStorage.setItem(PLANNER_CLASS_FILTER_KEY, classFilter.value);
+ refreshMechanicList(planId, false);
});
+}
- copyBtn?.addEventListener('click', () => {
- const plan = loadPlans().find(p => p.id === planId);
- if (!plan) return;
- const text = mySpellsPlainText(plan, jobSelect.value);
- navigator.clipboard.writeText(text).then(() => {
- copyBtn.textContent = '✓ Kopiert';
- setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800);
- }).catch(() => {
- copyBtn.textContent = '✗ Fehler';
- setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800);
- });
- });
+function refreshPlannerClassFilter(planId) {
+ const classFilter = document.getElementById('planner-class-filter');
+ const plan = getPlan(planId);
+ if (!classFilter || !plan) return;
+ const selected = plannerViewOptions(plan).job;
+ classFilter.innerHTML = renderClassFilterOptions(plan, selected);
+ classFilter.value = selected;
}
function avgNonTankMaxHp(plan) {
@@ -1037,6 +977,46 @@ function plannedAssignmentsForMechanic(plan, targetMechanic) {
return result;
}
+function renderSimpleMechanicListHtml(plan, mechanics, selectedJob) {
+ const rows = [];
+ for (const mechanic of mechanics) {
+ const assignments = sortedAssignments(plannedAssignmentsForMechanic(plan, mechanic))
+ .filter(a => assignmentMatchesClassFilter(a, selectedJob));
+ if (!assignments.length) continue;
+ rows.push({ mechanic, assignments });
+ }
+
+ if (!rows.length) {
+ const scope = selectedJob ? ` für ${escHtml(selectedJob)}` : '';
+ return `Keine Assignments${scope} in diesem Plan.
`;
+ }
+
+ return rows.map(({ mechanic, assignments }) => {
+ const abilities = assignments.map(a => {
+ const iconSrc = abilityIcon(a.ability);
+ const icon = iconSrc ? `
` : '';
+ const name = escHtml(assignmentAbilityName(a, plan));
+ const label = selectedJob || !a.job ? name : `${escHtml(a.job)} · ${name}`;
+ const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(a.ability);
+ const typeClass = a.buffType === 'debuff' ? 'myspells-type--debuff'
+ : a.buffType === 'shield' ? 'myspells-type--shield'
+ : isPersonal ? 'myspells-type--personal'
+ : 'myspells-type--buff';
+ return `
+ ${icon}${label}
+
+ `;
+ }).join('');
+
+ return `
+
+
${escHtml(fmtTimestamp(mechanic.timestamp))}
+
${escHtml(mechanic.name)}
+
${abilities}
+
`;
+ }).join('');
+}
+
function renderMechanicListHtml(plan) {
const mechanics = visiblePlanMechanics(plan);
if (mechanics.length === 0) {
@@ -1054,14 +1034,21 @@ function renderMechanicListHtml(plan) {
const activeJobSet = new Set(plan.jobComposition.filter(j => j));
const nonTankAvgHp = avgNonTankMaxHp(plan);
const tankAvgHp = avgTankMaxHp(plan);
+ const viewOptions = plannerViewOptions(plan);
- return mechanics.map(m => {
+ if (viewOptions.simple) {
+ return renderSimpleMechanicListHtml(plan, mechanics, viewOptions.job);
+ }
+
+ const rows = mechanics.map(m => {
// Tankbuster: gespeicherter maxHP des getroffenen Tanks hat Vorrang (präziser als Roster-Durchschnitt)
const avgHp = m.isHeavyTankbuster
? ((m.tankMaxHp ?? 0) > 0 ? m.tankMaxHp : tankAvgHp)
: nonTankAvgHp;
const planned = plannedAssignmentsForMechanic(plan, m);
- const sorted = sortedAssignments(planned);
+ const visiblePlanned = planned.filter(a => assignmentMatchesClassFilter(a, viewOptions.job));
+ const sorted = sortedAssignments(visiblePlanned);
+ if (viewOptions.job && !sorted.length) return '';
const assignHtml = sorted.length === 0
? 'Keine Zuweisung'
: sorted.map(a => {
@@ -1120,7 +1107,12 @@ function renderMechanicListHtml(plan) {
`;
- }).join('');
+ }).filter(Boolean);
+
+ if (!rows.length) {
+ return `Keine Assignments für ${escHtml(viewOptions.job)} in diesem Plan.
`;
+ }
+ return rows.join('');
}
// ── Job Slots ─────────────────────────────────────────────────────────────────
@@ -1163,6 +1155,7 @@ function initJobSlots(planId) {
slot.className = 'job-slot' + (role ? ` job-slot--${role}` : '');
}
refreshMechanicList(planId);
+ refreshPlannerClassFilter(planId);
});
}
diff --git a/templates/fight-select.php b/templates/fight-select.php
deleted file mode 100644
index 37db7ae..0000000
--- a/templates/fight-select.php
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/templates/report-form.php b/templates/report-form.php
index 9f0c275..5e2893b 100644
--- a/templates/report-form.php
+++ b/templates/report-form.php
@@ -1,8 +1,8 @@