Add Planner import flow (Schritt 3): export from Analysis tab
- Export button in AoE Timeline card title row (replaces bottom bar) - Import modal with mechanic-only vs with-mitigations choice, new plan vs merge into existing plan - Merge logic: match by abilityName + timestamp ±5s, keep existing assignments - Color-coded assignment badges: blue=buff, red=debuff, gold=shield - buffType stored in assignments for color rendering - Modal radio button layout fix (override global input width:100%) - Auto-switch to Planner tab after import - window.showTab exposed from tabs.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ea00268227
commit
4107779e2a
@ -211,6 +211,9 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--t2);
|
color: var(--t2);
|
||||||
}
|
}
|
||||||
|
.badge-assign-buff { background: rgba(74,158,255,.08); border-color: rgba(74,158,255,.4); color: var(--blue); }
|
||||||
|
.badge-assign-debuff { background: rgba(224,92,92,.08); border-color: rgba(224,92,92,.4); color: var(--red); }
|
||||||
|
.badge-assign-shield { background: rgba(200,168,75,.08); border-color: rgba(200,168,75,.4); color: var(--gold); }
|
||||||
|
|
||||||
.mechanic-notes {
|
.mechanic-notes {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -218,3 +221,91 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Import Modal ────────────────────────────────────────────────────────────── */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 200;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-box {
|
||||||
|
background: var(--bgcard);
|
||||||
|
border: 1px solid var(--borderem);
|
||||||
|
border-radius: var(--rl);
|
||||||
|
padding: 28px 32px;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-family: var(--font-d);
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--gold);
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-radio-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--t1);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
/* Override global input styles that break radio button layout */
|
||||||
|
.modal-radio-label input[type="radio"] {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subsection {
|
||||||
|
margin-left: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.modal-subsection input,
|
||||||
|
.modal-subsection select {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|||||||
@ -654,6 +654,9 @@
|
|||||||
|
|
||||||
document.getElementById('analysis-loading').style.display = 'none';
|
document.getElementById('analysis-loading').style.display = 'none';
|
||||||
document.getElementById('analysis-content').style.display = 'block';
|
document.getElementById('analysis-content').style.display = 'block';
|
||||||
|
|
||||||
|
const exportBtn = document.getElementById('export-to-planner-btn');
|
||||||
|
if (exportBtn) exportBtn.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.analysisTab = {
|
window.analysisTab = {
|
||||||
@ -672,6 +675,17 @@
|
|||||||
refFightSelect.dispatchEvent(new Event('change'));
|
refFightSelect.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
exportForPlanner() {
|
||||||
|
const fight = (window.App?.fights ?? []).find(f => f.id === window.App?.fightId);
|
||||||
|
return {
|
||||||
|
aoeEvents: lastEvents,
|
||||||
|
fightStart: lastFightStart,
|
||||||
|
phases: window.App?.phases ?? [],
|
||||||
|
players: currentPlayers,
|
||||||
|
fightName: fight?.name ?? `Fight ${window.App?.fightId ?? '?'}`,
|
||||||
|
reportCode: window.App?.reportCode ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
lastFightId = null;
|
lastFightId = null;
|
||||||
refEvents = [];
|
refEvents = [];
|
||||||
@ -685,6 +699,12 @@
|
|||||||
refExtFightSelect.value = '';
|
refExtFightSelect.value = '';
|
||||||
refExtFightSelect.style.display = 'none';
|
refExtFightSelect.style.display = 'none';
|
||||||
refExtPanel.style.display = 'none';
|
refExtPanel.style.display = 'none';
|
||||||
|
const exportBtn = document.getElementById('export-to-planner-btn');
|
||||||
|
if (exportBtn) exportBtn.style.display = 'none';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById('export-to-planner-btn')?.addEventListener('click', () => {
|
||||||
|
window.plannerTab?.showImportModal(window.analysisTab.exportForPlanner());
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
235
js/planner.js
235
js/planner.js
@ -219,9 +219,14 @@ function renderMechanicList(plan) {
|
|||||||
<div class="mechanic-assignments">
|
<div class="mechanic-assignments">
|
||||||
${m.assignments.length === 0
|
${m.assignments.length === 0
|
||||||
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
? '<span class="mechanic-no-assign">Keine Zuweisung</span>'
|
||||||
: m.assignments.map(a =>
|
: m.assignments.map(a => {
|
||||||
`<span class="badge badge-assign">${escHtml(a.job)} · ${escHtml(a.ability)}</span>`
|
const cls = a.buffType === 'debuff' ? 'badge-assign-debuff'
|
||||||
).join('')
|
: a.buffType === 'shield' ? 'badge-assign-shield'
|
||||||
|
: a.buffType === 'buff' ? 'badge-assign-buff'
|
||||||
|
: '';
|
||||||
|
const label = a.job ? `${escHtml(a.job)} · ${escHtml(a.ability)}` : escHtml(a.ability);
|
||||||
|
return `<span class="badge badge-assign ${cls}">${label}</span>`;
|
||||||
|
}).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
|
${m.notes ? `<div class="mechanic-notes">${escHtml(m.notes)}</div>` : ''}
|
||||||
@ -306,6 +311,220 @@ function initNewPlanForm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Ability → Job mapping ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ABILITY_JOB_MAP = {
|
||||||
|
'Passage of Arms': 'PLD', 'Divine Veil': 'PLD', 'Guardian': 'PLD',
|
||||||
|
'Shake It Off': 'WAR', 'Bloodwhetting': 'WAR',
|
||||||
|
'Dark Missionary': 'DRK',
|
||||||
|
'Heart of Light': 'GNB',
|
||||||
|
'Temperance': 'WHM', 'Divine Benison': 'WHM', 'Divine Caress': 'WHM',
|
||||||
|
'Sacred Soil': 'SCH', 'Expedient': 'SCH', 'Fey Illumination': 'SCH',
|
||||||
|
'Galvanize': 'SCH', 'Seraphic Veil': 'SCH', 'Catalyze': 'SCH',
|
||||||
|
'Collective Unconscious': 'AST', 'Neutral Sect': 'AST',
|
||||||
|
'Intersection': 'AST', 'the Spire': 'AST',
|
||||||
|
'Kerachole': 'SGE', 'Holos': 'SGE', 'Holosakos': 'SGE',
|
||||||
|
'Panhaima': 'SGE', 'Haima': 'SGE',
|
||||||
|
'Eukrasian Prognosis': 'SGE', 'Eukrasian Prognosis II': 'SGE',
|
||||||
|
'Eukrasian Diagnosis': 'SGE', 'Differential Diagnosis': 'SGE',
|
||||||
|
'Troubadour': 'BRD',
|
||||||
|
'Tactician': 'MCH',
|
||||||
|
'Shield Samba': 'DNC', 'Improvised Finish': 'DNC',
|
||||||
|
'Radiant Aegis': 'SMN',
|
||||||
|
'Magick Barrier': 'RDM',
|
||||||
|
'Tempera Coat': 'PCT', 'Tempera Grassa': 'PCT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const JOB_FROM_TYPE = {
|
||||||
|
'Paladin': 'PLD', 'Warrior': 'WAR', 'DarkKnight': 'DRK', 'Gunbreaker': 'GNB',
|
||||||
|
'WhiteMage': 'WHM', 'Scholar': 'SCH', 'Astrologian': 'AST', 'Sage': 'SGE',
|
||||||
|
'Monk': 'MNK', 'Dragoon': 'DRG', 'Ninja': 'NIN', 'Samurai': 'SAM',
|
||||||
|
'Reaper': 'RPR', 'Viper': 'VPR', 'Bard': 'BRD', 'Machinist': 'MCH',
|
||||||
|
'Dancer': 'DNC', 'BlackMage': 'BLM', 'Summoner': 'SMN', 'RedMage': 'RDM',
|
||||||
|
'Pictomancer': 'PCT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TANK_JOBS = new Set(['PLD', 'WAR', 'DRK', 'GNB']);
|
||||||
|
const MELEE_JOBS = new Set(['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR']);
|
||||||
|
const CASTER_JOBS = new Set(['BLM', 'SMN', 'RDM', 'PCT']);
|
||||||
|
|
||||||
|
function guessJob(abilityName, players) {
|
||||||
|
if (ABILITY_JOB_MAP[abilityName]) return ABILITY_JOB_MAP[abilityName];
|
||||||
|
const jobs = (players ?? []).map(p => JOB_FROM_TYPE[p.type] ?? '');
|
||||||
|
if (abilityName === 'Reprisal') {
|
||||||
|
const tanks = jobs.filter(j => TANK_JOBS.has(j));
|
||||||
|
return tanks.length === 1 ? tanks[0] : '';
|
||||||
|
}
|
||||||
|
if (abilityName === 'Feint') {
|
||||||
|
const melees = jobs.filter(j => MELEE_JOBS.has(j));
|
||||||
|
return melees.length === 1 ? melees[0] : '';
|
||||||
|
}
|
||||||
|
if (abilityName === 'Addle') {
|
||||||
|
const casters = jobs.filter(j => CASTER_JOBS.has(j));
|
||||||
|
return casters.length === 1 ? casters[0] : '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── AoE Events → Plan Mechanics ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations) {
|
||||||
|
return aoeEvents.map(ev => {
|
||||||
|
const relTs = ev.timestamp - fightStart;
|
||||||
|
const phase = (phases ?? []).filter(p => p.id !== 0).find(p =>
|
||||||
|
ev.timestamp >= p.startTime && ev.timestamp < p.endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetsWithData = ev.targets.filter(t => (t.unmitigatedAmount ?? 0) > 0);
|
||||||
|
const avgUnmit = targetsWithData.length > 0
|
||||||
|
? Math.round(targetsWithData.reduce((s, t) => s + t.unmitigatedAmount, 0) / targetsWithData.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
let assignments = [];
|
||||||
|
if (withMitigations) {
|
||||||
|
const seen = new Set();
|
||||||
|
for (const t of ev.targets) {
|
||||||
|
for (const m of (t.mitigations ?? [])) {
|
||||||
|
const key = m.key ?? m.name;
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
assignments.push({ ability: key, job: guessJob(key, players), buffType: m.buffType ?? '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: ev.abilityName,
|
||||||
|
timestamp: relTs,
|
||||||
|
phase: phase?.name ?? '',
|
||||||
|
unmitigatedDamage: avgUnmit,
|
||||||
|
notes: '',
|
||||||
|
assignments,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Merge + Create plan from import ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function doImport(data, withMitigations, whereMode, mergeId, newName) {
|
||||||
|
const { aoeEvents, fightStart, phases, players, fightName, reportCode } = data;
|
||||||
|
const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations);
|
||||||
|
|
||||||
|
if (whereMode === 'new') {
|
||||||
|
const plan = createPlan(newName || fightName || 'Importierter Plan');
|
||||||
|
return updatePlan(plan.id, {
|
||||||
|
mechanics,
|
||||||
|
source: { reportCode, fightName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge into existing plan
|
||||||
|
const plan = getPlan(mergeId);
|
||||||
|
if (!plan) return null;
|
||||||
|
|
||||||
|
const merged = [...plan.mechanics];
|
||||||
|
for (const newM of mechanics) {
|
||||||
|
const exists = plan.mechanics.some(m =>
|
||||||
|
m.name === newM.name && Math.abs(m.timestamp - newM.timestamp) < 5000
|
||||||
|
);
|
||||||
|
if (!exists) merged.push(newM);
|
||||||
|
}
|
||||||
|
merged.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
||||||
|
return updatePlan(mergeId, { mechanics: merged });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Import Modal ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let pendingImportData = null;
|
||||||
|
|
||||||
|
function showImportModal(data) {
|
||||||
|
pendingImportData = data;
|
||||||
|
|
||||||
|
// Pre-fill name
|
||||||
|
const nameInput = document.getElementById('import-plan-name');
|
||||||
|
if (nameInput) { nameInput.value = data.fightName || ''; }
|
||||||
|
|
||||||
|
// Populate merge dropdown
|
||||||
|
const planSelect = document.getElementById('import-plan-select');
|
||||||
|
if (planSelect) {
|
||||||
|
const plans = loadPlans();
|
||||||
|
planSelect.innerHTML = '<option value="">— Plan auswählen —</option>';
|
||||||
|
plans.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = p.name;
|
||||||
|
planSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
const mergeLabel = document.getElementById('import-merge-label');
|
||||||
|
if (mergeLabel) mergeLabel.style.opacity = plans.length === 0 ? '0.4' : '';
|
||||||
|
const mergeRadio = document.querySelector('input[name="import-where"][value="merge"]');
|
||||||
|
if (mergeRadio) mergeRadio.disabled = plans.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to defaults
|
||||||
|
document.querySelectorAll('input[name="import-what"]').forEach(r => { r.checked = r.value === 'mechanics'; });
|
||||||
|
document.querySelectorAll('input[name="import-where"]').forEach(r => { r.checked = r.value === 'new'; });
|
||||||
|
document.getElementById('import-new-section').style.display = '';
|
||||||
|
document.getElementById('import-merge-section').style.display = 'none';
|
||||||
|
|
||||||
|
document.getElementById('planner-import-modal').style.display = 'flex';
|
||||||
|
nameInput?.focus();
|
||||||
|
nameInput?.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideImportModal() {
|
||||||
|
document.getElementById('planner-import-modal').style.display = 'none';
|
||||||
|
pendingImportData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initImportModal() {
|
||||||
|
document.querySelectorAll('input[name="import-where"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', () => {
|
||||||
|
document.getElementById('import-new-section').style.display = radio.value === 'new' ? '' : 'none';
|
||||||
|
document.getElementById('import-merge-section').style.display = radio.value === 'merge' ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('import-cancel-btn')?.addEventListener('click', hideImportModal);
|
||||||
|
|
||||||
|
document.getElementById('planner-import-modal')?.addEventListener('click', e => {
|
||||||
|
if (e.target === e.currentTarget) hideImportModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape' && pendingImportData) hideImportModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('import-confirm-btn')?.addEventListener('click', () => {
|
||||||
|
if (!pendingImportData) return;
|
||||||
|
|
||||||
|
const withMitigations = document.querySelector('input[name="import-what"]:checked')?.value === 'with-mitigations';
|
||||||
|
const whereMode = document.querySelector('input[name="import-where"]:checked')?.value ?? 'new';
|
||||||
|
const newName = document.getElementById('import-plan-name')?.value.trim();
|
||||||
|
const mergeId = document.getElementById('import-plan-select')?.value;
|
||||||
|
|
||||||
|
if (whereMode === 'new' && !newName) {
|
||||||
|
document.getElementById('import-plan-name')?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (whereMode === 'merge' && !mergeId) {
|
||||||
|
document.getElementById('import-plan-select')?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = doImport(pendingImportData, withMitigations, whereMode, mergeId, newName);
|
||||||
|
if (!plan) return;
|
||||||
|
|
||||||
|
hideImportModal();
|
||||||
|
renderPlanList();
|
||||||
|
openPlan(plan.id);
|
||||||
|
window.showTab?.('planner');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── window.plannerTab (hooks for other tabs) ──────────────────────────────────
|
// ── window.plannerTab (hooks for other tabs) ──────────────────────────────────
|
||||||
|
|
||||||
window.plannerTab = {
|
window.plannerTab = {
|
||||||
@ -318,16 +537,20 @@ window.plannerTab = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showImportModal(data) {
|
||||||
|
showImportModal(data);
|
||||||
|
},
|
||||||
|
|
||||||
importFromAnalysis(aoeEvents, _refEvents, _options) {
|
importFromAnalysis(aoeEvents, _refEvents, _options) {
|
||||||
// Schritt 3 — noch nicht implementiert
|
console.log('[Planner] importFromAnalysis — use showImportModal instead', aoeEvents);
|
||||||
console.log('[Planner] importFromAnalysis — not yet implemented', aoeEvents);
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initNewPlanForm();
|
initNewPlanForm();
|
||||||
|
initImportModal();
|
||||||
renderPlanList();
|
renderPlanList();
|
||||||
renderPlanDetail(null);
|
renderPlanDetail(null);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,4 +17,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab)));
|
tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab)));
|
||||||
|
window.showTab = showTab;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -35,6 +35,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Planner Import Modal -->
|
||||||
|
<div id="planner-import-modal" class="modal-overlay" style="display:none">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div class="modal-title">In Planer exportieren</div>
|
||||||
|
|
||||||
|
<div class="modal-section">
|
||||||
|
<div class="modal-label">Was importieren?</div>
|
||||||
|
<label class="modal-radio-label">
|
||||||
|
<input type="radio" name="import-what" value="mechanics" checked>
|
||||||
|
<span>Nur Mechaniken</span>
|
||||||
|
</label>
|
||||||
|
<label class="modal-radio-label">
|
||||||
|
<input type="radio" name="import-what" value="with-mitigations">
|
||||||
|
<span>Mechaniken + erkannte Mitigations als Startpunkt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-section">
|
||||||
|
<div class="modal-label">Wohin?</div>
|
||||||
|
<label class="modal-radio-label">
|
||||||
|
<input type="radio" name="import-where" value="new" checked>
|
||||||
|
<span>Neuer Plan</span>
|
||||||
|
</label>
|
||||||
|
<div id="import-new-section" class="modal-subsection">
|
||||||
|
<input type="text" id="import-plan-name" placeholder="Plan-Name…">
|
||||||
|
</div>
|
||||||
|
<label class="modal-radio-label" id="import-merge-label">
|
||||||
|
<input type="radio" name="import-where" value="merge">
|
||||||
|
<span>In bestehenden Plan mergen</span>
|
||||||
|
</label>
|
||||||
|
<div id="import-merge-section" class="modal-subsection" style="display:none">
|
||||||
|
<select id="import-plan-select">
|
||||||
|
<option value="">— Plan auswählen —</option>
|
||||||
|
</select>
|
||||||
|
<div class="modal-hint">Neue Mechaniken werden hinzugefügt, bestehende Assignments bleiben erhalten.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="import-confirm-btn" class="btn btn-gold">Importieren</button>
|
||||||
|
<button id="import-cancel-btn" class="btn">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
<script src="js/tabs.js"></script>
|
<script src="js/tabs.js"></script>
|
||||||
<script src="js/analysis.js"></script>
|
<script src="js/analysis.js"></script>
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
<div class="card-title">AoE Timeline</div>
|
<div class="card-title">AoE Timeline</div>
|
||||||
<select id="phase-select" class="filter-input" style="display:none"></select>
|
<select id="phase-select" class="filter-input" style="display:none"></select>
|
||||||
<input type="text" id="player-filter" class="filter-input" placeholder="Spieler filtern…">
|
<input type="text" id="player-filter" class="filter-input" placeholder="Spieler filtern…">
|
||||||
|
<button id="export-to-planner-btn" class="btn btn-sm btn-gold" style="display:none">📋 Exportieren</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="aoe-timeline"></div>
|
<div id="aoe-timeline"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user