forked from xziino/ff14-mitigator
Planner: Namen + Job Import aus beliebigem Report
- Neues api/players.php: playerDetails + maxHp aus DamageTaken in einem GQL-Query - Import-Modal übernimmt Jobs UND Namen (Heiler → DPS → Tank → Alphabet) - buildPlayerRoster/extractJobComp: einheitliche Sortierreihenfolge - Ref-Fight Export überträgt jetzt auch die Jobaufstellung - Job-Slots zeigen importierte Spielernamen an Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
565dedc568
commit
6024560e61
95
api/players.php
Normal file
95
api/players.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
session_start_safe();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed']); exit; }
|
||||||
|
if (empty($_SESSION['access_token'])) { echo json_encode(['reauth' => true]); exit; }
|
||||||
|
if (($_SESSION['token_expires'] ?? 0) <= time()) { echo json_encode(['reauth' => true]); exit; }
|
||||||
|
|
||||||
|
$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
|
||||||
|
$fightId = (int)($_POST['fight_id'] ?? 0);
|
||||||
|
$startTime = (float)($_POST['start_time'] ?? 0);
|
||||||
|
$endTime = (float)($_POST['end_time'] ?? 0);
|
||||||
|
|
||||||
|
if (!$reportCode || !$fightId || !$endTime) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Missing params']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $_SESSION['access_token'];
|
||||||
|
|
||||||
|
function pl_gql(string $query): array {
|
||||||
|
global $token;
|
||||||
|
$ch = curl_init(GRAPHQL_URI);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
|
||||||
|
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
||||||
|
]);
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($err) return ['_curl_error' => $err];
|
||||||
|
if ($code === 401) return ['_reauth' => true];
|
||||||
|
return json_decode($body, true) ?? ['_parse_error' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single query: playerDetails + first page of DamageTaken with resources to get maxHp
|
||||||
|
$result = pl_gql(<<<GQL
|
||||||
|
{
|
||||||
|
reportData {
|
||||||
|
report(code: "$reportCode") {
|
||||||
|
playerDetails(fightIDs: [$fightId])
|
||||||
|
events(
|
||||||
|
fightIDs: [$fightId],
|
||||||
|
dataType: DamageTaken,
|
||||||
|
startTime: $startTime,
|
||||||
|
endTime: $endTime,
|
||||||
|
includeResources: true
|
||||||
|
) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GQL);
|
||||||
|
|
||||||
|
if (isset($result['_reauth'])) { echo json_encode(['reauth' => true]); exit; }
|
||||||
|
if (isset($result['_curl_error'])) { http_response_code(502); echo json_encode(['error' => $result['_curl_error']]); exit; }
|
||||||
|
|
||||||
|
// Parse player details: id → {name, type, role, maxHp}
|
||||||
|
$pdRaw = $result['data']['reportData']['report']['playerDetails'] ?? null;
|
||||||
|
$pdParsed = is_string($pdRaw) ? json_decode($pdRaw, true) : $pdRaw;
|
||||||
|
$pdGroups = $pdParsed['data']['playerDetails'] ?? [];
|
||||||
|
|
||||||
|
$players = [];
|
||||||
|
$roleMap = ['tanks' => 'tank', 'healers' => 'healer', 'dps' => 'dps'];
|
||||||
|
foreach ($roleMap as $group => $role) {
|
||||||
|
foreach ($pdGroups[$group] ?? [] as $p) {
|
||||||
|
$players[(int)$p['id']] = [
|
||||||
|
'name' => $p['name'],
|
||||||
|
'type' => $p['type'] ?? '',
|
||||||
|
'role' => $role,
|
||||||
|
'maxHp' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract maxHp from first damage events (skip DoT ticks — they may lack resources)
|
||||||
|
foreach ($result['data']['reportData']['report']['events']['data'] ?? [] as $ev) {
|
||||||
|
if ($ev['tick'] ?? false) continue;
|
||||||
|
$tid = (int)($ev['targetID'] ?? 0);
|
||||||
|
$maxHp = (int)($ev['targetResources']['maxHitPoints'] ?? 0);
|
||||||
|
if (isset($players[$tid]) && $players[$tid]['maxHp'] === 0 && $maxHp > 0) {
|
||||||
|
$players[$tid]['maxHp'] = $maxHp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['players' => array_values($players)]);
|
||||||
@ -159,6 +159,63 @@
|
|||||||
.job-slot--healer select { border-left-color: var(--green); }
|
.job-slot--healer select { border-left-color: var(--green); }
|
||||||
.job-slot--dps select { border-left-color: rgba(200,168,75,.5); }
|
.job-slot--dps select { border-left-color: rgba(200,168,75,.5); }
|
||||||
|
|
||||||
|
/* ── Job Slot Player Names ───────────────────────────────────────────────────── */
|
||||||
|
.job-slot-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t2);
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Name Import Modal ───────────────────────────────────────────────────────── */
|
||||||
|
.name-import-input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.name-import-input-row input { flex: 1; }
|
||||||
|
|
||||||
|
.name-import-preview {
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-import-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.name-import-row:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.name-import-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--t1);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-import-name--none {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--t3);
|
||||||
|
font-style: italic;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-import-disambig {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */
|
/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */
|
||||||
.mechanic-card {
|
.mechanic-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -321,7 +321,7 @@
|
|||||||
if (!json.error && !json.reauth) {
|
if (!json.error && !json.reauth) {
|
||||||
refEvents = json.aoe_events ?? [];
|
refEvents = json.aoe_events ?? [];
|
||||||
refFightStart = json.fight_start ?? fight.startTime;
|
refFightStart = json.fight_start ?? fight.startTime;
|
||||||
refPlayers = [];
|
refPlayers = json.players ?? [];
|
||||||
window.App.setUrlState?.({
|
window.App.setUrlState?.({
|
||||||
compareReportCode: '',
|
compareReportCode: '',
|
||||||
compareFightId: refId,
|
compareFightId: refId,
|
||||||
|
|||||||
230
js/planner.js
230
js/planner.js
@ -325,7 +325,10 @@ function renderPlanDetail(plan) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card section-gap">
|
<div class="card section-gap">
|
||||||
|
<div class="card-title-row">
|
||||||
<div class="card-title">Jobaufstellung</div>
|
<div class="card-title">Jobaufstellung</div>
|
||||||
|
<button id="name-import-open-btn" class="btn btn-sm">Namen + Job Import</button>
|
||||||
|
</div>
|
||||||
<div class="job-slots-grid" id="job-slots-grid">
|
<div class="job-slots-grid" id="job-slots-grid">
|
||||||
${renderJobSlotsHtml(plan)}
|
${renderJobSlotsHtml(plan)}
|
||||||
</div>
|
</div>
|
||||||
@ -345,6 +348,9 @@ function renderPlanDetail(plan) {
|
|||||||
startRename(plan.id, plan.name);
|
startRename(plan.id, plan.name);
|
||||||
});
|
});
|
||||||
initJobSlots(plan.id);
|
initJobSlots(plan.id);
|
||||||
|
document.getElementById('name-import-open-btn')?.addEventListener('click', () => {
|
||||||
|
showNameImportModal(plan.id);
|
||||||
|
});
|
||||||
initMechanicClicks(plan.id);
|
initMechanicClicks(plan.id);
|
||||||
renderInfoPanel(plan);
|
renderInfoPanel(plan);
|
||||||
}
|
}
|
||||||
@ -417,9 +423,11 @@ function renderMechanicListHtml(plan) {
|
|||||||
// ── Job Slots ─────────────────────────────────────────────────────────────────
|
// ── Job Slots ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderJobSlotsHtml(plan) {
|
function renderJobSlotsHtml(plan) {
|
||||||
|
const roster = plan.playerRoster ?? [];
|
||||||
return Array.from({ length: 8 }, (_, i) => {
|
return Array.from({ length: 8 }, (_, i) => {
|
||||||
const job = plan.jobComposition[i] ?? '';
|
const job = plan.jobComposition[i] ?? '';
|
||||||
const role = JOB_ROLE[job] ?? '';
|
const role = JOB_ROLE[job] ?? '';
|
||||||
|
const playerName = roster[i]?.name ?? '';
|
||||||
return `
|
return `
|
||||||
<div class="job-slot${role ? ` job-slot--${role}` : ''}">
|
<div class="job-slot${role ? ` job-slot--${role}` : ''}">
|
||||||
<select class="job-slot-select" data-idx="${i}">
|
<select class="job-slot-select" data-idx="${i}">
|
||||||
@ -430,6 +438,7 @@ function renderJobSlotsHtml(plan) {
|
|||||||
).join('')}</optgroup>`
|
).join('')}</optgroup>`
|
||||||
).join('')}
|
).join('')}
|
||||||
</select>
|
</select>
|
||||||
|
${playerName ? `<div class="job-slot-name" title="${escHtml(playerName)}">${escHtml(playerName)}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@ -1009,12 +1018,36 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
|||||||
|
|
||||||
// ── Merge + Create plan from import ──────────────────────────────────────────
|
// ── Merge + Create plan from import ──────────────────────────────────────────
|
||||||
|
|
||||||
function extractJobComp(players) {
|
function buildPlayerRoster(players, aoeEvents) {
|
||||||
const order = { tank: 0, healer: 1, dps: 2 };
|
const maxHpByName = {};
|
||||||
|
for (const ev of aoeEvents ?? []) {
|
||||||
|
for (const t of ev.targets ?? []) {
|
||||||
|
if (t.name && t.maxHp > 0 && !(t.name in maxHpByName)) {
|
||||||
|
maxHpByName[t.name] = t.maxHp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const order = { healer: 0, dps: 1, tank: 2 };
|
||||||
const sorted = [...(players ?? [])]
|
const sorted = [...(players ?? [])]
|
||||||
.filter(p => JOB_FROM_TYPE[p.type])
|
.filter(p => JOB_FROM_TYPE[p.type])
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const roleCmp = (order[a.role] ?? 2) - (order[b.role] ?? 2);
|
const roleCmp = (order[a.role] ?? 1) - (order[b.role] ?? 1);
|
||||||
|
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
const roster = sorted.slice(0, 8).map(p => ({
|
||||||
|
name: p.name,
|
||||||
|
maxHp: maxHpByName[p.name] ?? 0,
|
||||||
|
}));
|
||||||
|
while (roster.length < 8) roster.push({ name: '', maxHp: 0 });
|
||||||
|
return roster;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractJobComp(players) {
|
||||||
|
const order = { healer: 0, dps: 1, tank: 2 };
|
||||||
|
const sorted = [...(players ?? [])]
|
||||||
|
.filter(p => JOB_FROM_TYPE[p.type])
|
||||||
|
.sort((a, b) => {
|
||||||
|
const roleCmp = (order[a.role] ?? 1) - (order[b.role] ?? 1);
|
||||||
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
const comp = sorted.map(p => JOB_FROM_TYPE[p.type] ?? '').slice(0, 8);
|
const comp = sorted.map(p => JOB_FROM_TYPE[p.type] ?? '').slice(0, 8);
|
||||||
@ -1034,6 +1067,7 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
|
|||||||
source,
|
source,
|
||||||
mitigationNames,
|
mitigationNames,
|
||||||
jobComposition: extractJobComp(players),
|
jobComposition: extractJobComp(players),
|
||||||
|
playerRoster: buildPlayerRoster(players, aoeEvents),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1334,6 +1368,195 @@ function initImportModal() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Name Import Modal ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let nameImportPlanId = null;
|
||||||
|
let nameImportFights = [];
|
||||||
|
let nameImportReportCode = '';
|
||||||
|
let nameImportMatchData = []; // length-8 array: null | {job, matched:[{name,type,role,maxHp}], selected:0}
|
||||||
|
|
||||||
|
function showNameImportModal(planId) {
|
||||||
|
nameImportPlanId = planId;
|
||||||
|
nameImportFights = [];
|
||||||
|
nameImportReportCode = '';
|
||||||
|
nameImportMatchData = [];
|
||||||
|
|
||||||
|
document.getElementById('name-import-report-input').value = '';
|
||||||
|
document.getElementById('name-import-fight-section').style.display = 'none';
|
||||||
|
document.getElementById('name-import-fight-select').innerHTML = '<option value="">— Fight auswählen —</option>';
|
||||||
|
document.getElementById('name-import-preview').style.display = 'none';
|
||||||
|
document.getElementById('name-import-preview').innerHTML = '';
|
||||||
|
document.getElementById('name-import-confirm-btn').style.display = 'none';
|
||||||
|
|
||||||
|
document.getElementById('planner-name-import-modal').style.display = 'flex';
|
||||||
|
document.getElementById('name-import-report-input').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNameImportModal() {
|
||||||
|
document.getElementById('planner-name-import-modal').style.display = 'none';
|
||||||
|
nameImportPlanId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshJobSlots(planId) {
|
||||||
|
const plan = getPlan(planId);
|
||||||
|
if (!plan) return;
|
||||||
|
const grid = document.getElementById('job-slots-grid');
|
||||||
|
if (grid) grid.innerHTML = renderJobSlotsHtml(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNameImportPreview(_plan, fetchedPlayers) {
|
||||||
|
const order = { healer: 0, dps: 1, tank: 2 };
|
||||||
|
const sorted = [...fetchedPlayers]
|
||||||
|
.filter(p => JOB_FROM_TYPE[p.type])
|
||||||
|
.sort((a, b) => {
|
||||||
|
const roleCmp = (order[a.role] ?? 1) - (order[b.role] ?? 1);
|
||||||
|
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
nameImportMatchData = sorted.slice(0, 8);
|
||||||
|
|
||||||
|
const preview = document.getElementById('name-import-preview');
|
||||||
|
|
||||||
|
const rows = Array.from({ length: 8 }, (_, i) => {
|
||||||
|
const p = sorted[i];
|
||||||
|
if (!p) {
|
||||||
|
return `
|
||||||
|
<div class="name-import-row">
|
||||||
|
<span class="aoe-target-job" style="opacity:0.25">—</span>
|
||||||
|
<span class="name-import-name--none">Leer</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const job = JOB_FROM_TYPE[p.type] ?? p.type;
|
||||||
|
const role = JOB_ROLE[job] ?? 'dps';
|
||||||
|
return `
|
||||||
|
<div class="name-import-row">
|
||||||
|
<span class="aoe-target-job role-${role}">${escHtml(job)}</span>
|
||||||
|
<span class="name-import-name">${escHtml(p.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
preview.innerHTML = rows;
|
||||||
|
preview.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function initNameImportModal() {
|
||||||
|
const modal = document.getElementById('planner-name-import-modal');
|
||||||
|
const reportInput = document.getElementById('name-import-report-input');
|
||||||
|
const loadBtn = document.getElementById('name-import-load-btn');
|
||||||
|
const fightSection = document.getElementById('name-import-fight-section');
|
||||||
|
const fightSelect = document.getElementById('name-import-fight-select');
|
||||||
|
const preview = document.getElementById('name-import-preview');
|
||||||
|
const confirmBtn = document.getElementById('name-import-confirm-btn');
|
||||||
|
const cancelBtn = document.getElementById('name-import-cancel-btn');
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
cancelBtn.addEventListener('click', hideNameImportModal);
|
||||||
|
modal.addEventListener('click', e => { if (e.target === modal) hideNameImportModal(); });
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape' && nameImportPlanId) hideNameImportModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
reportInput.addEventListener('input', () => {
|
||||||
|
const match = reportInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/);
|
||||||
|
if (match) reportInput.value = match[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBtn.addEventListener('click', async () => {
|
||||||
|
const code = reportInput.value.trim();
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
loadBtn.disabled = true;
|
||||||
|
loadBtn.textContent = 'Lädt…';
|
||||||
|
fightSection.style.display = 'none';
|
||||||
|
preview.style.display = 'none';
|
||||||
|
confirmBtn.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('api/fight.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ report_code: code, language: 'en' }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; }
|
||||||
|
|
||||||
|
nameImportFights = json?.data?.reportData?.report?.fights ?? [];
|
||||||
|
nameImportReportCode = code;
|
||||||
|
|
||||||
|
fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
||||||
|
nameImportFights.forEach(f => {
|
||||||
|
const ms = f.endTime - f.startTime;
|
||||||
|
const dur = `${Math.floor(ms / 60000)}:${String(Math.floor((ms % 60000) / 1000)).padStart(2, '0')}`;
|
||||||
|
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = f.id;
|
||||||
|
opt.textContent = `${f.name} — ${dur} — ${hp}`;
|
||||||
|
fightSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
fightSection.style.display = '';
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
loadBtn.disabled = false;
|
||||||
|
loadBtn.textContent = 'Laden';
|
||||||
|
});
|
||||||
|
|
||||||
|
fightSelect.addEventListener('change', async () => {
|
||||||
|
const fightId = parseInt(fightSelect.value, 10);
|
||||||
|
if (!fightId) {
|
||||||
|
preview.style.display = 'none';
|
||||||
|
confirmBtn.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fight = nameImportFights.find(f => f.id === fightId);
|
||||||
|
if (!fight) return;
|
||||||
|
|
||||||
|
fightSelect.disabled = true;
|
||||||
|
preview.innerHTML = '<div style="padding:10px 12px;color:var(--t3);font-size:13px">Lädt…</div>';
|
||||||
|
preview.style.display = '';
|
||||||
|
confirmBtn.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('api/players.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
report_code: nameImportReportCode,
|
||||||
|
fight_id: fightId,
|
||||||
|
start_time: fight.startTime,
|
||||||
|
end_time: fight.endTime,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; }
|
||||||
|
if (json.error) { preview.innerHTML = `<div style="padding:10px 12px;color:var(--red);font-size:13px">${escHtml(json.error)}</div>`; return; }
|
||||||
|
|
||||||
|
const plan = getPlan(nameImportPlanId);
|
||||||
|
if (!plan) return;
|
||||||
|
renderNameImportPreview(plan, json.players ?? []);
|
||||||
|
confirmBtn.style.display = '';
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
fightSelect.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmBtn.addEventListener('click', () => {
|
||||||
|
if (!nameImportPlanId) return;
|
||||||
|
|
||||||
|
const jobComposition = Array.from({ length: 8 }, (_, i) => {
|
||||||
|
const p = nameImportMatchData[i];
|
||||||
|
return p ? (JOB_FROM_TYPE[p.type] ?? '') : '';
|
||||||
|
});
|
||||||
|
const playerRoster = Array.from({ length: 8 }, (_, i) => {
|
||||||
|
const p = nameImportMatchData[i];
|
||||||
|
return p ? { name: p.name, maxHp: p.maxHp ?? 0 } : { name: '', maxHp: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
updatePlan(nameImportPlanId, { jobComposition, playerRoster });
|
||||||
|
openPlan(nameImportPlanId);
|
||||||
|
hideNameImportModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── window.plannerTab (hooks for other tabs) ──────────────────────────────────
|
// ── window.plannerTab (hooks for other tabs) ──────────────────────────────────
|
||||||
|
|
||||||
window.plannerTab = {
|
window.plannerTab = {
|
||||||
@ -1362,6 +1585,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
initNewFolderForm();
|
initNewFolderForm();
|
||||||
initImportModal();
|
initImportModal();
|
||||||
initAbilityModal();
|
initAbilityModal();
|
||||||
|
initNameImportModal();
|
||||||
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);
|
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);
|
||||||
renderPlanList();
|
renderPlanList();
|
||||||
if (activePlanId && getPlan(activePlanId)) {
|
if (activePlanId && getPlan(activePlanId)) {
|
||||||
|
|||||||
@ -80,6 +80,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Name Import Modal (Planner) -->
|
||||||
|
<div id="planner-name-import-modal" class="modal-overlay" style="display:none">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div class="modal-title">Namen importieren</div>
|
||||||
|
|
||||||
|
<div class="modal-section">
|
||||||
|
<div class="modal-label">Report-Code</div>
|
||||||
|
<div class="name-import-input-row">
|
||||||
|
<input type="text" id="name-import-report-input" placeholder="Report-Code oder URL…">
|
||||||
|
<button id="name-import-load-btn" class="btn btn-sm">Laden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-section" id="name-import-fight-section" style="display:none">
|
||||||
|
<div class="modal-label">Fight</div>
|
||||||
|
<select id="name-import-fight-select">
|
||||||
|
<option value="">— Fight auswählen —</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="name-import-preview" class="name-import-preview" style="display:none"></div>
|
||||||
|
|
||||||
|
<div class="modal-actions" style="margin-top:16px">
|
||||||
|
<button id="name-import-confirm-btn" class="btn btn-gold" style="display:none">Übernehmen</button>
|
||||||
|
<button id="name-import-cancel-btn" class="btn">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ability Assignment Modal -->
|
<!-- Ability Assignment Modal -->
|
||||||
<div id="planner-ability-modal" class="modal-overlay" style="display:none">
|
<div id="planner-ability-modal" class="modal-overlay" style="display:none">
|
||||||
<div class="modal-box ability-modal-box">
|
<div class="modal-box ability-modal-box">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user