396 lines
24 KiB
PHP
396 lines
24 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../includes/bootstrap.php';
|
|
require_once __DIR__ . '/../includes/osticket_import_passes.php';
|
|
$staff = require_admin();
|
|
$pdo = get_db();
|
|
|
|
if (!empty($_POST['action'])) {
|
|
ob_start();
|
|
error_reporting(0);
|
|
@ini_set('display_errors', '0');
|
|
register_shutdown_function(function () {
|
|
$err = error_get_last();
|
|
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
|
while (ob_get_level() > 0) ob_end_clean();
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['ok' => false, 'error' => $err['message'] . ' in ' . basename($err['file']) . ':' . $err['line']]);
|
|
}
|
|
});
|
|
}
|
|
|
|
@ini_set('memory_limit', '1024M');
|
|
@set_time_limit(300);
|
|
|
|
function import_json(array $data): never
|
|
{
|
|
while (ob_get_level() > 0) ob_end_clean();
|
|
header('Content-Type: application/json');
|
|
echo json_encode($data, JSON_INVALID_UTF8_SUBSTITUTE);
|
|
exit;
|
|
}
|
|
function import_ajax_guard(): void
|
|
{
|
|
header('Content-Type: application/json');
|
|
if (!hash_equals($_SESSION['csrf_token'] ?? '', (string) ($_POST['csrf_token'] ?? ''))) {
|
|
// Ist eine Anfrage größer als post_max_size, verwirft PHP alle Formulardaten: dann fehlt auch das Token
|
|
import_json(['ok' => false, 'error' => 'Die Sitzung ist abgelaufen oder der Upload-Teil war größer als der Server erlaubt (post_max_size ' . ini_get('post_max_size') . '). Bitte die Seite neu laden und erneut versuchen.']);
|
|
}
|
|
}
|
|
|
|
$stateDir = osticket_state_dir();
|
|
|
|
/** PHP-Größenangabe ("2M", "128K", "1G") in Bytes. */
|
|
function import_ini_bytes(string $v): int
|
|
{
|
|
$v = trim($v);
|
|
if ($v === '' || $v === '-1') return PHP_INT_MAX;
|
|
$n = (float) $v;
|
|
return (int) match (strtolower(substr($v, -1))) { 'g' => $n * 1073741824, 'm' => $n * 1048576, 'k' => $n * 1024, default => $n };
|
|
}
|
|
|
|
/** Größe eines Upload-Teils: so groß wie der Server erlaubt (höchstens 5 MB, mit Reserve), damit der Upload auch bei kleinen Limits (z. B. 2 MB) klappt. */
|
|
function import_chunk_size(): int
|
|
{
|
|
$limit = min(import_ini_bytes((string) ini_get('upload_max_filesize')), import_ini_bytes((string) ini_get('post_max_size')));
|
|
$size = (int) ($limit * 0.8);
|
|
$size = intdiv($size, 65536) * 65536;
|
|
return max(131072, min(5 * 1048576, $size));
|
|
}
|
|
|
|
/** Verständliche Meldung zu einem PHP-Upload-Fehlercode. */
|
|
function import_upload_error(?array $file): string
|
|
{
|
|
$code = $file['error'] ?? UPLOAD_ERR_NO_FILE;
|
|
$limit = ini_get('upload_max_filesize') . ' / post_max_size ' . ini_get('post_max_size');
|
|
return match ($code) {
|
|
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Der Server lehnt den Upload-Teil ab, er ist größer als erlaubt (upload_max_filesize ' . $limit . '). Laden Sie die Seite neu und versuchen Sie es erneut, oder legen Sie die Datei per FTP in den DeskHub-Ordner (Option 1).',
|
|
UPLOAD_ERR_PARTIAL => 'Der Upload-Teil kam nur unvollständig an (Verbindung unterbrochen). Bitte erneut versuchen.',
|
|
UPLOAD_ERR_NO_FILE => 'Es kam kein Upload-Teil an.',
|
|
UPLOAD_ERR_NO_TMP_DIR => 'Auf dem Server fehlt der temporäre Ordner für Uploads (upload_tmp_dir). Bitte den Hoster fragen oder die Datei per FTP hochladen (Option 1).',
|
|
UPLOAD_ERR_CANT_WRITE => 'Der Server konnte den Upload-Teil nicht speichern (Speicherplatz oder Schreibrechte). Bitte die Datei per FTP hochladen (Option 1).',
|
|
default => 'Upload fehlgeschlagen (PHP-Fehlercode ' . $code . ').',
|
|
};
|
|
}
|
|
|
|
// ── Chunked Upload ─────────────────────────────────────────────────────────
|
|
if (($_POST['action'] ?? '') === 'upload_chunk') {
|
|
import_ajax_guard();
|
|
$chunk = $_FILES['chunk'] ?? null;
|
|
$idx = (int) ($_POST['chunk_index'] ?? 0);
|
|
$total = (int) ($_POST['total_chunks'] ?? 1);
|
|
$tmpFile = $stateDir . '/upload.sql';
|
|
if (!$chunk || $chunk['error'] !== UPLOAD_ERR_OK) import_json(['ok' => false, 'error' => import_upload_error($chunk)]);
|
|
$fh = @fopen($tmpFile, $idx === 0 ? 'wb' : 'ab');
|
|
if (!$fh) import_json(['ok' => false, 'error' => 'Die Datei konnte nicht gespeichert werden: Der Import-Ordner ist nicht beschreibbar (' . $stateDir . ').']);
|
|
$written = fwrite($fh, (string) file_get_contents($chunk['tmp_name']));
|
|
fclose($fh);
|
|
if ($written === false || $written < (int) filesize($chunk['tmp_name'])) import_json(['ok' => false, 'error' => 'Die Datei konnte nicht vollständig gespeichert werden (Speicherplatz voll?).']);
|
|
$done = ($idx + 1) >= $total;
|
|
if ($done) {
|
|
$_SESSION['import_sql_path'] = $tmpFile;
|
|
$_SESSION['import_filename'] = basename((string) ($_POST['filename'] ?? 'import.sql'));
|
|
@unlink($stateDir . '/state.json');
|
|
@unlink($stateDir . '/entries.jsonl');
|
|
@unlink($stateDir . '/entry_map.jsonl');
|
|
}
|
|
import_json(['ok' => true, 'done' => $done, 'chunk' => $idx]);
|
|
}
|
|
|
|
// ── Dateipfad ──────────────────────────────────────────────────────────────
|
|
if (($_POST['action'] ?? '') === 'use_path') {
|
|
verify_csrf();
|
|
$filename = basename(trim($_POST['sql_filename'] ?? ''));
|
|
$fullPath = realpath(__DIR__ . '/../' . $filename);
|
|
$rootDir = realpath(__DIR__ . '/..');
|
|
if ($filename === '') {
|
|
flash('error', 'Bitte den Dateinamen der SQL-Datei eintragen (das graue Beispiel im Feld ist nur ein Platzhalter).'); redirect('/staff/import_osticket.php');
|
|
}
|
|
if (!$fullPath || strpos($fullPath, $rootDir) !== 0 || !is_file($fullPath)) {
|
|
flash('error', 'Datei nicht gefunden: ' . $filename . '. Sie muss im DeskHub-Hauptordner auf dem Server liegen (dort, wo auch index.php liegt).'); redirect('/staff/import_osticket.php');
|
|
}
|
|
$_SESSION['import_sql_path'] = $fullPath;
|
|
$_SESSION['import_filename'] = $filename;
|
|
foreach (['state.json', 'entries.jsonl', 'entry_map.jsonl'] as $f) @unlink($stateDir . '/' . $f);
|
|
redirect('/staff/import_osticket.php?step=analyze');
|
|
}
|
|
|
|
// ── Analyse ────────────────────────────────────────────────────────────────
|
|
if (($_POST['action'] ?? '') === 'analyze_chunk') {
|
|
import_ajax_guard();
|
|
$tmp = $_SESSION['import_sql_path'] ?? '';
|
|
if (!$tmp || !file_exists($tmp)) import_json(['ok' => false, 'error' => 'Datei fehlt']);
|
|
try {
|
|
$res = osticket_analyze_step($tmp, $stateDir, (int) ($_POST['byte_offset'] ?? 0));
|
|
} catch (Throwable $e) {
|
|
import_json(['ok' => false, 'error' => $e->getMessage()]);
|
|
}
|
|
import_json(['ok' => true, 'done' => $res['done'], 'byte_offset' => $res['offset'], 'file_size' => filesize($tmp), 'counts' => $res['counts']]);
|
|
}
|
|
|
|
// ── Import starten ─────────────────────────────────────────────────────────
|
|
if (($_POST['action'] ?? '') === 'start') {
|
|
verify_csrf();
|
|
$allowed = ['users', 'structure', 'tickets', 'threads', 'attachments'];
|
|
$_SESSION['import_what'] = array_values(array_intersect($allowed, (array) ($_POST['import'] ?? [])));
|
|
redirect('/staff/import_osticket.php?step=run');
|
|
}
|
|
|
|
// ── Reset ──────────────────────────────────────────────────────────────────
|
|
if (($_POST['action'] ?? '') === 'reset_import') {
|
|
import_ajax_guard();
|
|
$type = $_POST['reset_type'] ?? 'all';
|
|
if ($type === 'threads_only') {
|
|
$pdo->exec('DELETE FROM ticket_attachments');
|
|
$pdo->exec('DELETE FROM ticket_threads');
|
|
} else {
|
|
$pdo->exec('DELETE FROM ticket_attachments');
|
|
$pdo->exec('DELETE FROM ticket_threads');
|
|
$pdo->exec('DELETE FROM ticket_ratings');
|
|
$pdo->exec('DELETE FROM tickets');
|
|
$pdo->exec('DELETE FROM users WHERE id NOT IN (SELECT DISTINCT user_id FROM tickets WHERE user_id IS NOT NULL)');
|
|
}
|
|
@unlink($stateDir . '/entry_map.jsonl');
|
|
audit_log($staff['id'], 'Import-Reset: ' . $type);
|
|
import_json(['ok' => true]);
|
|
}
|
|
|
|
// ── Import-Schritt ─────────────────────────────────────────────────────────
|
|
if (($_POST['action'] ?? '') === 'import_chunk') {
|
|
import_ajax_guard();
|
|
$pass = (string) ($_POST['pass'] ?? '');
|
|
$offset = (int) ($_POST['offset'] ?? 0);
|
|
if (!in_array($pass, ['users', 'structure', 'tickets', 'threads', 'attachments'], true)) import_json(['ok' => false, 'error' => 'Unbekannter Durchgang']);
|
|
if ($pass === 'threads' && $offset === 0) @unlink($stateDir . '/entry_map.jsonl');
|
|
try {
|
|
$res = osticket_import_step($pdo, $stateDir, $pass, $offset, (string) ($_SESSION['import_sql_path'] ?? ''));
|
|
} catch (Throwable $e) {
|
|
import_json(['ok' => false, 'error' => $e->getMessage()]);
|
|
}
|
|
$importWhat = $_SESSION['import_what'] ?? [];
|
|
if ($res['done'] && $pass === end($importWhat)) {
|
|
$tmp = $_SESSION['import_sql_path'] ?? '';
|
|
if ($tmp && file_exists($tmp) && str_starts_with($tmp, $stateDir)) @unlink($tmp);
|
|
osticket_state_clear($stateDir);
|
|
unset($_SESSION['import_sql_path'], $_SESSION['import_what'], $_SESSION['import_filename']);
|
|
audit_log($staff['id'], 'osTicket Import abgeschlossen');
|
|
}
|
|
import_json(['ok' => true] + $res);
|
|
}
|
|
|
|
// ── Seite ──────────────────────────────────────────────────────────────────
|
|
$step = $_GET['step'] ?? '1';
|
|
$sqlPath = $_SESSION['import_sql_path'] ?? '';
|
|
$filename = $_SESSION['import_filename'] ?? '';
|
|
$fileSize = $sqlPath && file_exists($sqlPath) ? filesize($sqlPath) : 0;
|
|
|
|
$pageTitle = 'osTicket Import';
|
|
$activePage = 'settings';
|
|
require __DIR__ . '/_header.php';
|
|
?>
|
|
|
|
<div class="topbar">
|
|
<h1>osTicket Import</h1>
|
|
<a href="/staff/settings.php" class="btn secondary">← Einstellungen</a>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:0;margin-bottom:24px;border-bottom:2px solid var(--border)">
|
|
<?php foreach(['1'=>'Hochladen','analyze'=>'Analysieren','2'=>'Optionen','run'=>'Import'] as $s=>$l): ?>
|
|
<div style="padding:10px 18px;font-size:.84rem;font-weight:<?=$step===$s?'700':'400'?>;color:<?=$step===$s?'var(--primary)':'var(--muted)'?>;border-bottom:2px solid <?=$step===$s?'var(--primary)':'transparent'?>;margin-bottom:-2px"><?=$l?></div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<?php if ($step === '1'): ?>
|
|
<div class="card" style="max-width:620px">
|
|
<h3>SQL-Datei hochladen oder Pfad angeben</h3>
|
|
<p class="muted" style="font-size:.85rem">Unterstützt werden SQL-Dumps von osTicket 1.10 bis 1.18 (phpMyAdmin-Export oder <code>mysqldump</code>, auch mit anderem Tabellenpräfix als <code>ost_</code>). Importiert werden Kunden, Abteilungen, Hilfethemen, Tickets, Nachrichten und Anhänge.</p>
|
|
<p style="font-weight:600;font-size:.88rem;margin-bottom:8px">Option 1: Datei in den DeskHub-Ordner legen (für große Dateien empfohlen)</p>
|
|
<form method="post" style="display:flex;gap:8px;margin-bottom:20px">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="action" value="use_path">
|
|
<input type="text" name="sql_filename" placeholder="helpdesk.sql" style="flex:1">
|
|
<button type="submit" class="btn">Verwenden →</button>
|
|
</form>
|
|
<hr style="border:none;border-top:1px solid var(--border);margin-bottom:20px">
|
|
<p style="font-weight:600;font-size:.88rem;margin-bottom:8px">Option 2: Direkt hochladen (in Teilen von <?= round(import_chunk_size() / 1048576, 1) ?> MB, passend zum Limit dieses Servers)</p>
|
|
<input type="file" id="sql-file-input" accept=".sql" style="margin-bottom:10px">
|
|
<div id="upload-progress" style="display:none;margin-bottom:10px">
|
|
<div style="background:var(--border);border-radius:20px;height:10px;overflow:hidden;margin-bottom:5px">
|
|
<div id="upload-bar" style="background:var(--primary);height:10px;width:0%;border-radius:20px;transition:width .2s"></div>
|
|
</div>
|
|
<div id="upload-status" class="muted" style="font-size:.82rem"></div>
|
|
</div>
|
|
<button onclick="startChunkedUpload()" class="btn secondary" id="upload-btn">Hochladen →</button>
|
|
<script>
|
|
var csrf = <?= json_encode(csrf_token()) ?>;
|
|
function startChunkedUpload() {
|
|
var file = document.getElementById('sql-file-input').files[0];
|
|
if (!file) { alert('Bitte eine Datei auswaehlen.'); return; }
|
|
var btn = document.getElementById('upload-btn');
|
|
btn.disabled = true;
|
|
document.getElementById('upload-progress').style.display = 'block';
|
|
var chunkSize = <?= import_chunk_size() ?>, totalChunks = Math.ceil(file.size/chunkSize), idx = 0;
|
|
function next() {
|
|
var blob = file.slice(idx*chunkSize, (idx+1)*chunkSize);
|
|
var fd = new FormData();
|
|
fd.append('action','upload_chunk'); fd.append('csrf_token',csrf);
|
|
fd.append('chunk',blob,file.name); fd.append('chunk_index',idx);
|
|
fd.append('total_chunks',totalChunks); fd.append('filename',file.name);
|
|
fetch('/staff/import_osticket.php',{method:'POST',body:fd})
|
|
.then(r=>r.text()).then(function(t){
|
|
var d; try { d = JSON.parse(t); } catch(e) { d = {ok:false,error:'Der Server hat den Upload-Teil nicht angenommen (vermutlich ist er größer als das Limit post_max_size ' + <?= json_encode((string) ini_get('post_max_size')) ?> + '). Laden Sie die Seite neu und versuchen Sie es erneut, oder legen Sie die Datei per FTP in den DeskHub-Ordner (Option 1).'}; }
|
|
if(!d.ok){alert('Fehler: '+d.error);btn.disabled=false;document.getElementById('upload-bar').style.background='#dc2626';return;}
|
|
idx++;
|
|
var pct=Math.round(idx/totalChunks*100);
|
|
document.getElementById('upload-bar').style.width=pct+'%';
|
|
document.getElementById('upload-status').textContent=pct+'% - '+Math.round(Math.min(idx*chunkSize,file.size)/1048576)+' von '+Math.round(file.size/1048576)+' MB';
|
|
if(d.done){
|
|
document.getElementById('upload-bar').style.background='#22c55e';
|
|
setTimeout(()=>{window.location.href='/staff/import_osticket.php?step=analyze';},400);
|
|
} else { next(); }
|
|
}).catch(e=>{alert('Fehler: '+e);btn.disabled=false;});
|
|
}
|
|
next();
|
|
}
|
|
</script>
|
|
</div>
|
|
|
|
<?php elseif ($step === 'analyze'): ?>
|
|
<div class="card" style="max-width:620px">
|
|
<h3>Analysiere: <?= e($filename) ?> (<?= round($fileSize/1024/1024,1) ?> MB)</h3>
|
|
<p class="muted" style="font-size:.85rem;margin-bottom:14px">Liest die Datei in 4-MB-Blöcken. Bei sehr großen Dateien (Anhänge, Sitzungsdaten) kann das einige Minuten dauern.</p>
|
|
<div style="background:var(--border);border-radius:20px;height:12px;overflow:hidden;margin-bottom:10px">
|
|
<div id="analyze-bar" style="background:var(--primary);height:12px;width:0%;border-radius:20px;transition:width .2s"></div>
|
|
</div>
|
|
<div id="analyze-status" class="muted" style="font-size:.82rem;margin-bottom:14px">Starte...</div>
|
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px">
|
|
<?php foreach(['users'=>'Kunden','tickets'=>'Tickets','threads'=>'Nachrichten','departments'=>'Abteilungen','topics'=>'Hilfethemen','attachments'=>'Anhänge'] as $k=>$l): ?>
|
|
<div style="text-align:center;background:var(--bg-soft);border-radius:8px;padding:10px">
|
|
<div id="cnt-<?=$k?>" style="font-size:1.5rem;font-weight:800;color:var(--primary)">0</div>
|
|
<div class="muted" style="font-size:.75rem"><?=$l?></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<a id="next-btn" href="/staff/import_osticket.php?step=2" class="btn" style="display:none">Weiter →</a>
|
|
</div>
|
|
<script>
|
|
var fileSize=<?=(int)$fileSize?>,byteOffset=0,csrf=<?=json_encode(csrf_token())?>;
|
|
function analyzeChunk(){
|
|
var fd=new FormData();
|
|
fd.append('action','analyze_chunk');fd.append('csrf_token',csrf);fd.append('byte_offset',byteOffset);
|
|
fetch('/staff/import_osticket.php',{method:'POST',body:fd})
|
|
.then(r=>r.json()).then(d=>{
|
|
if(!d.ok){alert('Fehler: '+d.error);return;}
|
|
byteOffset=d.byte_offset;
|
|
var pct=fileSize>0?Math.min(100,Math.round(d.byte_offset/fileSize*100)):100;
|
|
document.getElementById('analyze-bar').style.width=pct+'%';
|
|
document.getElementById('analyze-status').textContent=pct+'% - '+Math.round(d.byte_offset/1024/1024)+' MB';
|
|
for(var k in d.counts){var el=document.getElementById('cnt-'+k);if(el)el.textContent=Number(d.counts[k]).toLocaleString();}
|
|
if(d.done){
|
|
document.getElementById('analyze-bar').style.background='#22c55e';
|
|
document.getElementById('analyze-status').textContent='Analyse abgeschlossen!';
|
|
document.getElementById('next-btn').style.display='inline-flex';
|
|
} else { setTimeout(analyzeChunk,10); }
|
|
}).catch(e=>{console.error(e);alert('Netzwerkfehler: '+e);});
|
|
}
|
|
analyzeChunk();
|
|
</script>
|
|
|
|
<?php elseif ($step === '2'): ?>
|
|
<?php $ad = osticket_state_load($stateDir); $counts = osticket_counts($ad); ?>
|
|
<div class="card" style="max-width:620px">
|
|
<h3>Was soll importiert werden?</h3>
|
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px">
|
|
<?php foreach(['users'=>'Kunden','tickets'=>'Tickets','threads'=>'Nachrichten','departments'=>'Abteilungen','topics'=>'Hilfethemen','attachments'=>'Anhänge'] as $k=>$l): ?>
|
|
<div style="text-align:center;background:var(--bg-soft);border-radius:8px;padding:12px">
|
|
<div style="font-size:1.5rem;font-weight:800;color:var(--primary)"><?= number_format((int) $counts[$k], 0, ',', '.') ?></div>
|
|
<div class="muted" style="font-size:.75rem"><?=$l?></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<form method="post">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="action" value="start">
|
|
<?php foreach([
|
|
'users' => ['Kunden', 'Name und E-Mail-Adresse'],
|
|
'structure' => ['Abteilungen und Hilfethemen', 'Fehlende werden angelegt, gleichnamige vorhandene wiederverwendet. Tickets landen damit in der richtigen Abteilung.'],
|
|
'tickets' => ['Tickets', 'Betreff, Nummer, Status, Priorität, Abteilung, Hilfethema, zugewiesener Mitarbeiter (über gleiche E-Mail-Adresse), Zeitstempel. Gelöschte Tickets werden ausgelassen.'],
|
|
'threads' => ['Nachrichten', 'Kundennachrichten, Antworten, interne Notizen und Mitlesende'],
|
|
'attachments' => ['Anhänge (' . round($counts['attachment_bytes'] / 1048576, 1) . ' MB)', 'Dateien zu den Nachrichten, bis ' . round(OSTICKET_ATTACHMENT_MAX_BYTES / 1048576) . ' MB je Datei. Setzt die Nachrichten voraus.'],
|
|
] as $v => [$lb, $desc]): ?>
|
|
<label style="display:flex;align-items:flex-start;gap:10px;margin-bottom:10px;cursor:pointer;font-weight:400">
|
|
<input type="checkbox" name="import[]" value="<?=$v?>" <?= $v === 'attachments' && !$counts['attachments'] ? '' : 'checked' ?> style="width:auto;margin-top:2px">
|
|
<div><strong><?=$lb?></strong><div class="muted" style="font-size:.78rem"><?=$desc?></div></div>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
<p class="muted" style="font-size:.78rem">Der Import kann gefahrlos wiederholt werden: Vorhandene Kunden und Ticketnummern werden übersprungen, bereits importierte Nachrichten nicht doppelt angelegt. Vorher ein Backup erstellen ist trotzdem empfehlenswert.</p>
|
|
<div class="btn-row" style="margin-top:16px;flex-wrap:wrap;gap:8px">
|
|
<button type="submit" class="btn">Import starten →</button>
|
|
<a href="/staff/import_osticket.php" class="btn secondary">Neu hochladen</a>
|
|
<button type="button" onclick="resetImport('threads_only')" class="btn secondary" style="border-color:#f59e0b;color:#92400e">Nur Nachrichten löschen</button>
|
|
<button type="button" onclick="resetImport('all')" class="btn secondary" style="border-color:var(--danger);color:var(--danger)">Alle Tickets löschen</button>
|
|
</div>
|
|
</form>
|
|
<script>
|
|
function resetImport(type){
|
|
if(!confirm(type==='threads_only'?'Alle Nachrichten und Anhänge löschen?':'ALLE Tickets, Nachrichten und Kunden ohne Ticket in DeskHub löschen?')) return;
|
|
var fd=new FormData();
|
|
fd.append('action','reset_import');fd.append('reset_type',type);fd.append('csrf_token',<?=json_encode(csrf_token())?>);
|
|
fetch('/staff/import_osticket.php',{method:'POST',body:fd}).then(r=>r.json()).then(d=>{if(d.ok){alert('Gelöscht.');location.reload();}});
|
|
}
|
|
</script>
|
|
</div>
|
|
|
|
<?php elseif ($step === 'run'): ?>
|
|
<?php $importWhat = $_SESSION['import_what'] ?? []; ?>
|
|
<div class="card" style="max-width:620px">
|
|
<h3>Import läuft...</h3>
|
|
<p class="muted" style="font-size:.85rem;margin-bottom:18px">Seite nicht schließen.</p>
|
|
<?php foreach(['users'=>'Kunden','structure'=>'Abteilungen und Hilfethemen','tickets'=>'Tickets','threads'=>'Nachrichten','attachments'=>'Anhänge'] as $p=>$l): if(!in_array($p,$importWhat)) continue; ?>
|
|
<div style="margin-bottom:14px">
|
|
<div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:.84rem"><span><?=$l?></span><span id="lbl-<?=$p?>" class="muted">Wartend...</span></div>
|
|
<div style="background:var(--border);border-radius:20px;height:10px;overflow:hidden">
|
|
<div id="bar-<?=$p?>" style="background:var(--primary);height:10px;width:0%;border-radius:20px;transition:width .3s"></div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<div id="result" style="display:none;margin-top:20px"></div>
|
|
</div>
|
|
<script>
|
|
var passes=<?=json_encode(array_values($importWhat))?>,csrf=<?=json_encode(csrf_token())?>;
|
|
var totals={},skipped=0,passIdx=0,offset=0,notes={};
|
|
function runChunk(){
|
|
if(passIdx>=passes.length){showResult();return;}
|
|
var pass=passes[passIdx];
|
|
var fd=new FormData();
|
|
fd.append('action','import_chunk');fd.append('csrf_token',csrf);fd.append('pass',pass);fd.append('offset',offset);
|
|
fetch('/staff/import_osticket.php',{method:'POST',body:fd})
|
|
.then(r=>r.json()).then(d=>{
|
|
if(!d.ok){alert('Fehler: '+d.error);return;}
|
|
totals[pass]=(totals[pass]||0)+d.inserted;
|
|
skipped+=d.skipped||0;
|
|
offset=d.next_offset;
|
|
for(var k in (d.notes||{})) notes[pass+k]=d.notes[k];
|
|
var bar=document.getElementById('bar-'+pass),lbl=document.getElementById('lbl-'+pass);
|
|
if(bar)bar.style.width=d.done?'100%':Math.min(98,Math.max(5,(bar.dataset.p=(Number(bar.dataset.p||0)+1))))+'%';
|
|
if(lbl)lbl.textContent=(totals[pass]||0).toLocaleString()+' importiert';
|
|
if(d.done){
|
|
if(bar)bar.style.background='#22c55e';
|
|
if(lbl)lbl.textContent='✅ '+(totals[pass]||0).toLocaleString();
|
|
passIdx++;offset=0;setTimeout(runChunk,100);
|
|
} else { setTimeout(runChunk,30); }
|
|
}).catch(e=>alert('Netzwerkfehler: '+e));
|
|
}
|
|
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
|
function showResult(){
|
|
var r=document.getElementById('result');r.style.display='block';
|
|
var n=Object.keys(notes).map(function(k){return '<li>'+esc(notes[k])+'</li>';}).join('');
|
|
r.innerHTML='<div class="flash success">Fertig! Kunden: <strong>'+(totals.users||0).toLocaleString()+'</strong>, Tickets: <strong>'+(totals.tickets||0).toLocaleString()+'</strong>, Nachrichten: <strong>'+(totals.threads||0).toLocaleString()+'</strong>, Anhänge: <strong>'+(totals.attachments||0).toLocaleString()+'</strong>, Übersprungen: <strong>'+skipped.toLocaleString()+'</strong></div>'
|
|
+(n?'<ul style="font-size:.84rem;color:var(--muted)">'+n+'</ul>':'')
|
|
+'<div class="btn-row"><a href="/staff/tickets.php" class="btn">Tickets ansehen</a><a href="/staff/import_osticket.php" class="btn secondary">Erneut importieren</a></div>';
|
|
}
|
|
runChunk();
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<?php require __DIR__ . '/_footer.php'; ?>
|