initial upload
This commit is contained in:
97
change_password.php
Normal file
97
change_password.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/qdb/change_password.php
|
||||||
|
require_once __DIR__ . '/common.php';
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
// dieselben Defaults wie in login.php
|
||||||
|
$USERS_DEFAULT = [
|
||||||
|
'user01' => 'pw1',
|
||||||
|
'user02' => 'pw2',
|
||||||
|
'Tmp_Daniel' => 'dummy4',
|
||||||
|
'Tmp_Johanna' => 'dummy1',
|
||||||
|
'Tmp_Anke' => 'dummy2',
|
||||||
|
'Tmp_Liliana' => 'dummy3',
|
||||||
|
'Amy_tmp' => 'dummy4',
|
||||||
|
'Brigitte_tmp' => 'dummy5',
|
||||||
|
'Extra1_tmp' => 'dummy6',
|
||||||
|
'Extra2_tmp' => 'dummy7',
|
||||||
|
'Danny' => 'pw1',
|
||||||
|
'User1' => 'pw1',
|
||||||
|
'User2' => 'pw2',
|
||||||
|
'User3' => 'pw3',
|
||||||
|
'User4' => 'pw4',
|
||||||
|
'User5' => 'pw5',
|
||||||
|
'User6' => 'pw6',
|
||||||
|
'User7' => 'pw7',
|
||||||
|
'User8' => 'pw8',
|
||||||
|
'User9' => 'pw9',
|
||||||
|
'User10' => 'pw10',
|
||||||
|
'User11' => 'pw11'
|
||||||
|
];
|
||||||
|
|
||||||
|
$STORE_FILE = __DIR__ . '/users.json';
|
||||||
|
|
||||||
|
function load_store($path) {
|
||||||
|
if (!file_exists($path)) return [];
|
||||||
|
$txt = file_get_contents($path);
|
||||||
|
$arr = json_decode($txt, true);
|
||||||
|
return is_array($arr) ? $arr : [];
|
||||||
|
}
|
||||||
|
function save_store($path, $data) {
|
||||||
|
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents("php://input");
|
||||||
|
$in = json_decode($raw, true) ?: [];
|
||||||
|
|
||||||
|
$username = trim($in['username'] ?? '');
|
||||||
|
$oldPassword = (string)($in['old_password'] ?? '');
|
||||||
|
$newPassword = (string)($in['new_password'] ?? '');
|
||||||
|
|
||||||
|
if ($username === '' || $oldPassword === '' || $newPassword === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["success" => false, "message" => "Felder fehlen"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (strlen($newPassword) < 6) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["success" => false, "message" => "Neues Passwort zu kurz (min. 6 Zeichen)."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$store = load_store($STORE_FILE);
|
||||||
|
|
||||||
|
// Fall A: User hat schon Hash -> altes Passwort gegen Hash prüfen
|
||||||
|
if (array_key_exists($username, $store) && isset($store[$username]['hash'])) {
|
||||||
|
$hash = (string)$store[$username]['hash'];
|
||||||
|
if (!password_verify($oldPassword, $hash)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(["success" => false, "message" => "Altes Passwort falsch."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall B: User hat noch keinen Hash -> gegen Default prüfen
|
||||||
|
if (!array_key_exists($username, $USERS_DEFAULT) || $USERS_DEFAULT[$username] !== $oldPassword) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(["success" => false, "message" => "Altes Passwort falsch."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// neuen Hash setzen (bcrypt)
|
||||||
|
$store[$username] = [
|
||||||
|
"hash" => password_hash($newPassword, PASSWORD_DEFAULT),
|
||||||
|
"changedAt" => time()
|
||||||
|
];
|
||||||
|
save_store($STORE_FILE, $store);
|
||||||
|
|
||||||
|
// Nach erfolgreichem Wechsel direkt einloggen (Token erstellen)
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
token_add($token, 30 * 24 * 60 * 60);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
"success" => true,
|
||||||
|
"message" => "Passwort geändert.",
|
||||||
|
"token" => $token,
|
||||||
|
"user" => $username
|
||||||
|
]);
|
||||||
10
checkDatabaseExists.php
Executable file
10
checkDatabaseExists.php
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$dbPath = '/var/www/html/uploads/questionnaire_database';
|
||||||
|
|
||||||
|
if (file_exists($dbPath)) {
|
||||||
|
echo json_encode(["exists" => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(["exists" => false]);
|
||||||
|
}
|
||||||
91
common.php
Normal file
91
common.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/common.php
|
||||||
|
// Gemeinsame Helfer für Token-Validierung, Key-Derivation (HKDF) und AES (CBC, IV vorangestellt).
|
||||||
|
|
||||||
|
// --- MASTER-KEY (Server-seitig zum Speichern der DB) ---
|
||||||
|
// Per ENV 'QDB_MASTER_KEY' (Base64- oder ASCII-String) oder Fallback auf bisherigen Wert.
|
||||||
|
function get_master_key_bytes(): string {
|
||||||
|
$env = getenv('QDB_MASTER_KEY');
|
||||||
|
if ($env && $env !== '') {
|
||||||
|
// Versuche Base64 zu dekodieren, sonst als ASCII nehmen
|
||||||
|
$b = base64_decode($env, true);
|
||||||
|
if ($b !== false && strlen($b) > 0) {
|
||||||
|
return str_pad(substr($b, 0, 32), 32, "\0");
|
||||||
|
}
|
||||||
|
return str_pad(substr($env, 0, 32), 32, "\0");
|
||||||
|
}
|
||||||
|
// Fallback: alter Key aus bestehender Installation (32 ASCII-Bytes)
|
||||||
|
return "12345678901234567890123456789012";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HKDF (SHA-256) über Token -> Session-Key (32 Byte) ---
|
||||||
|
// Kompatibel zu Android-Implementierung: salt = leer, info = "qdb-aes"
|
||||||
|
function hkdf_session_key_from_token(string $tokenHex, string $info = 'qdb-aes', int $len = 32): string {
|
||||||
|
$ikm = @hex2bin(trim($tokenHex));
|
||||||
|
if ($ikm === false) $ikm = $tokenHex; // Falls bereits binär
|
||||||
|
if (function_exists('hash_hkdf')) {
|
||||||
|
return hash_hkdf('sha256', $ikm, $len, $info, '');
|
||||||
|
}
|
||||||
|
// Fallback: eigene HKDF-Implementierung
|
||||||
|
$hashLen = 32;
|
||||||
|
$salt = str_repeat("\0", $hashLen);
|
||||||
|
$prk = hash_hmac('sha256', $ikm, $salt, true);
|
||||||
|
$okm = '';
|
||||||
|
$t = '';
|
||||||
|
$n = (int)ceil($len / $hashLen);
|
||||||
|
for ($i = 1; $i <= $n; $i++) {
|
||||||
|
$t = hash_hmac('sha256', $t . $info . chr($i), $prk, true);
|
||||||
|
$okm .= $t;
|
||||||
|
}
|
||||||
|
return substr($okm, 0, $len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Token-Storage (mit Ablauf) ---
|
||||||
|
function tokens_file_path(): string {
|
||||||
|
return __DIR__ . '/tokens.jsonl';
|
||||||
|
}
|
||||||
|
function token_add(string $token, int $ttlSeconds = 86400): void {
|
||||||
|
$rec = [
|
||||||
|
'token' => $token,
|
||||||
|
'created' => time(),
|
||||||
|
'exp' => time() + $ttlSeconds,
|
||||||
|
];
|
||||||
|
file_put_contents(tokens_file_path(), json_encode($rec) . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||||
|
// optional weiterhin für Alt-Skripte:
|
||||||
|
file_put_contents(__DIR__ . '/valid_tokens.txt', $token . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
function token_is_valid(string $token): bool {
|
||||||
|
$f = tokens_file_path();
|
||||||
|
if (!file_exists($f)) return false;
|
||||||
|
$h = fopen($f, 'r');
|
||||||
|
if (!$h) return false;
|
||||||
|
$now = time();
|
||||||
|
$valid = false;
|
||||||
|
while (($line = fgets($h)) !== false) {
|
||||||
|
$j = json_decode($line, true);
|
||||||
|
if (!is_array($j)) continue;
|
||||||
|
if (($j['token'] ?? '') === $token && ($j['exp'] ?? 0) >= $now) {
|
||||||
|
$valid = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($h);
|
||||||
|
return $valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AES-256-CBC: IV(16) + CIPHERTEXT ---
|
||||||
|
function aes256_cbc_encrypt_bytes(string $plain, string $key): string {
|
||||||
|
$key = str_pad(substr($key, 0, 32), 32, "\0");
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$cipher = openssl_encrypt($plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($cipher === false) throw new Exception('openssl_encrypt failed');
|
||||||
|
return $iv . $cipher;
|
||||||
|
}
|
||||||
|
function aes256_cbc_decrypt_bytes(string $data, string $key): string {
|
||||||
|
if (strlen($data) < 16) throw new Exception('cipher too short');
|
||||||
|
$key = str_pad(substr($key, 0, 32), 32, "\0");
|
||||||
|
$iv = substr($data, 0, 16);
|
||||||
|
$ct = substr($data, 16);
|
||||||
|
$plain = openssl_decrypt($ct, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($plain === false) throw new Exception('openssl_decrypt failed');
|
||||||
|
return $plain;
|
||||||
|
}
|
||||||
27
db_download.php
Normal file
27
db_download.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/db_download.php
|
||||||
|
require_once __DIR__ . '/common.php';
|
||||||
|
|
||||||
|
// Token prüfen
|
||||||
|
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
|
||||||
|
if (stripos($auth, 'Bearer ') !== 0) { http_response_code(401); echo "Fehlender Bearer-Token"; exit; }
|
||||||
|
$token = substr($auth, 7);
|
||||||
|
if (!token_is_valid($token)) { http_response_code(403); echo "Ungültiger oder abgelaufener Token"; exit; }
|
||||||
|
|
||||||
|
// DB entschlüsseln
|
||||||
|
$path = __DIR__ . '/uploads/questionnaire_database';
|
||||||
|
if (!file_exists($path)) { http_response_code(404); echo "Datenbank nicht gefunden"; exit; }
|
||||||
|
$enc = file_get_contents($path);
|
||||||
|
if ($enc === false) { http_response_code(500); echo "Lesefehler"; exit; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$plain = aes256_cbc_decrypt_bytes($enc, get_master_key_bytes());
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500); echo "Entschlüsselung fehlgeschlagen"; exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ausliefern
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Disposition: attachment; filename="questionnaire.sqlite"');
|
||||||
|
header('Content-Length: ' . strlen($plain));
|
||||||
|
echo $plain;
|
||||||
78
db_view.php
Normal file
78
db_view.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/db_view.php
|
||||||
|
require_once __DIR__ . '/common.php';
|
||||||
|
header('Content-Type: text/html; charset=UTF-8');
|
||||||
|
|
||||||
|
// ---- 1) Token prüfen (aus Authorization: Bearer <token>) ----
|
||||||
|
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
|
||||||
|
if (stripos($auth, 'Bearer ') !== 0) {
|
||||||
|
http_response_code(401); echo "Fehlender Bearer-Token"; exit;
|
||||||
|
}
|
||||||
|
$token = substr($auth, 7);
|
||||||
|
if (!token_is_valid($token)) {
|
||||||
|
http_response_code(403); echo "Ungültiger oder abgelaufener Token"; exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 2) Master-verschlüsselte DB laden & entschlüsseln ----
|
||||||
|
$dbPath = __DIR__ . '/uploads/questionnaire_database';
|
||||||
|
if (!file_exists($dbPath)) { http_response_code(404); echo "Datenbank nicht gefunden"; exit; }
|
||||||
|
$enc = file_get_contents($dbPath);
|
||||||
|
if ($enc === false) { http_response_code(500); echo "Lesefehler"; exit; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$plain = aes256_cbc_decrypt_bytes($enc, get_master_key_bytes());
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500); echo "Entschlüsselung fehlgeschlagen"; exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 3) In temporäre SQLite-Datei schreiben ----
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'qdb_');
|
||||||
|
file_put_contents($tmp, $plain);
|
||||||
|
|
||||||
|
// ---- 4) Tabellen lesen & als HTML ausgeben ----
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:$tmp");
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
// Tabellenliste
|
||||||
|
$tables = [];
|
||||||
|
$rs = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
||||||
|
foreach ($rs as $row) $tables[] = $row['name'];
|
||||||
|
|
||||||
|
// Ausgabe
|
||||||
|
echo "<style>h2{margin:24px 0 8px} table{border-collapse:collapse;width:100%} th,td{border:1px solid #ddd;padding:6px 8px;text-align:left} th{background:#f7f7f7}</style>";
|
||||||
|
echo "<p><b>Tabellen:</b> " . htmlspecialchars(implode(', ', $tables)) . "</p>";
|
||||||
|
|
||||||
|
foreach ($tables as $t) {
|
||||||
|
echo "<h2>" . htmlspecialchars($t) . "</h2>";
|
||||||
|
|
||||||
|
// Spaltenüberschriften via PRAGMA
|
||||||
|
$cols = [];
|
||||||
|
$cr = $pdo->query("PRAGMA table_info(" . $pdo->quote($t) . ")");
|
||||||
|
// PRAGMA table_info akzeptiert kein quote(..) direkt, also besser plain escapen:
|
||||||
|
// Workaround:
|
||||||
|
$t_esc = preg_replace('/[^A-Za-z0-9_]/', '', $t);
|
||||||
|
$cr = $pdo->query("PRAGMA table_info($t_esc)");
|
||||||
|
foreach ($cr as $c) $cols[] = $c['name'];
|
||||||
|
|
||||||
|
// Daten holen (ohne Limit; bei sehr großen Tabellen ggf. LIMIT einbauen)
|
||||||
|
$stmt = $pdo->query("SELECT * FROM $t_esc");
|
||||||
|
echo "<table><thead><tr>";
|
||||||
|
foreach ($cols as $c) echo "<th>" . htmlspecialchars($c) . "</th>";
|
||||||
|
echo "</tr></thead><tbody>";
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
echo "<tr>";
|
||||||
|
foreach ($cols as $c) {
|
||||||
|
$val = $row[$c];
|
||||||
|
if ($val === null) { echo "<td><em>NULL</em></td>"; }
|
||||||
|
else { echo "<td>" . htmlspecialchars((string)$val) . "</td>"; }
|
||||||
|
}
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
echo "</tbody></table>";
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500); echo "SQLite-Fehler: " . htmlspecialchars($e->getMessage());
|
||||||
|
} finally {
|
||||||
|
@unlink($tmp);
|
||||||
|
}
|
||||||
132
db_view_ordered.php
Normal file
132
db_view_ordered.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/db_view_ordered.php
|
||||||
|
require_once __DIR__ . '/common.php';
|
||||||
|
header('Content-Type: text/html; charset=UTF-8');
|
||||||
|
|
||||||
|
// ---- Auth ----
|
||||||
|
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
|
||||||
|
if (stripos($auth, 'Bearer ') !== 0) { http_response_code(401); echo "Missing Bearer token"; exit; }
|
||||||
|
$token = substr($auth, 7);
|
||||||
|
if (!token_is_valid($token)) { http_response_code(403); echo "Invalid or expired token"; exit; }
|
||||||
|
|
||||||
|
// ---- Paths for order, header labels and value translations ----
|
||||||
|
$ORDER_JSON = __DIR__ . '/header_order.json';
|
||||||
|
$LABELS_JSON = __DIR__ . '/questions_en.json'; // Spaltenüberschriften (2. Kopfzeile)
|
||||||
|
$VALS_JSON = __DIR__ . '/translations_en.json'; // NEU: Werte-Übersetzungen
|
||||||
|
|
||||||
|
$order = @json_decode(@file_get_contents($ORDER_JSON), true);
|
||||||
|
$labels = @json_decode(@file_get_contents($LABELS_JSON), true);
|
||||||
|
$vals = @json_decode(@file_get_contents($VALS_JSON), true);
|
||||||
|
if (!is_array($order) || empty($order)) { http_response_code(500); echo "header_order.json missing or empty."; exit; }
|
||||||
|
if (!is_array($labels)) $labels = [];
|
||||||
|
if (!is_array($vals)) $vals = [];
|
||||||
|
|
||||||
|
function t_val(string $id, string $raw, array $vals): string {
|
||||||
|
// feste Wörter direkt durchlassen
|
||||||
|
if ($raw === '' || $raw === 'None' || $raw === 'Done' || $raw === 'Not Done') return $raw;
|
||||||
|
|
||||||
|
// Kandidaten: exakt, normalisiert, id+value
|
||||||
|
$norm = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $raw));
|
||||||
|
$cands = [$raw];
|
||||||
|
if ($norm && $norm !== $raw) $cands[] = $norm;
|
||||||
|
$cands[] = "{$id}_{$raw}";
|
||||||
|
$cands[] = "{$id}-{$raw}";
|
||||||
|
$cands[] = "{$id}_{$norm}";
|
||||||
|
$cands[] = "{$id}-{$norm}";
|
||||||
|
|
||||||
|
foreach ($cands as $k) {
|
||||||
|
if (isset($vals[$k]) && is_string($vals[$k]) && $vals[$k] !== '') return $vals[$k];
|
||||||
|
}
|
||||||
|
// Fallback: Versuch mit einfachen globalen Keys (z. B. "yes","no","consent_signed")
|
||||||
|
if (isset($vals[$norm])) return (string)$vals[$norm];
|
||||||
|
if (isset($vals[$raw])) return (string)$vals[$raw];
|
||||||
|
|
||||||
|
return $raw; // nichts gefunden → Original anzeigen
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- load & decrypt DB ----
|
||||||
|
$dbPath = __DIR__ . '/uploads/questionnaire_database';
|
||||||
|
if (!file_exists($dbPath)) { http_response_code(404); echo "Database not found"; exit; }
|
||||||
|
$enc = file_get_contents($dbPath);
|
||||||
|
if ($enc === false) { http_response_code(500); echo "Read error"; exit; }
|
||||||
|
|
||||||
|
try { $plain = aes256_cbc_decrypt_bytes($enc, get_master_key_bytes()); }
|
||||||
|
catch (Throwable $e) { http_response_code(500); echo "Decryption failed"; exit; }
|
||||||
|
|
||||||
|
// ---- temp SQLite file ----
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'qdb_');
|
||||||
|
file_put_contents($tmp, $plain);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:$tmp");
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$clients = $pdo->query("SELECT clientCode FROM clients ORDER BY clientCode")->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||||
|
$questionnaireIds = $pdo->query("SELECT id FROM questionnaires")->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||||
|
$qidSet = array_fill_keys($questionnaireIds, true);
|
||||||
|
|
||||||
|
$completed = [];
|
||||||
|
$stmt = $pdo->query("SELECT clientCode, questionnaireId, isDone FROM completed_questionnaires");
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$c = $row['clientCode'] ?? 'undefined';
|
||||||
|
$q = $row['questionnaireId'] ?? 'undefined';
|
||||||
|
$completed[$c][$q] = ((int)($row['isDone'] ?? 0)) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$answers = [];
|
||||||
|
$stmt = $pdo->query("SELECT clientCode, questionId, answerValue FROM answers");
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$c = $row['clientCode'] ?? 'undefined';
|
||||||
|
$q = $row['questionId'] ?? 'undefined';
|
||||||
|
$answers[$c][$q] = (string)($row['answerValue'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- HTML ----
|
||||||
|
echo '<style>
|
||||||
|
table{border-collapse:collapse;width:100%;}
|
||||||
|
th,td{border:1px solid #ddd;padding:6px 8px;text-align:left;vertical-align:top;}
|
||||||
|
thead th{background:#f7f7f7;position:sticky;top:0;z-index:1;}
|
||||||
|
thead tr:nth-child(2) th{background:#fafafa;}
|
||||||
|
.nowrap{white-space:nowrap;}
|
||||||
|
.chkcol{width:36px;text-align:center;}
|
||||||
|
</style>';
|
||||||
|
|
||||||
|
echo '<table id="qdb-table">';
|
||||||
|
echo '<thead>';
|
||||||
|
// Row 1: checkbox + IDs (bleiben als technische Schlüssel im Kopf – Werte darunter sind übersetzt)
|
||||||
|
echo '<tr><th class="chkcol"><input type="checkbox" id="chkAll" title="select all"></th>';
|
||||||
|
foreach ($order as $id) echo '<th data-id="'.htmlspecialchars($id).'">'.htmlspecialchars($id).'</th>';
|
||||||
|
echo '</tr>';
|
||||||
|
|
||||||
|
// Row 2: blank + English labels
|
||||||
|
echo '<tr><th class="chkcol"></th>';
|
||||||
|
foreach ($order as $id) {
|
||||||
|
$lbl = $labels[$id] ?? '';
|
||||||
|
echo '<th>'.htmlspecialchars($lbl).'</th>';
|
||||||
|
}
|
||||||
|
echo '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
echo '<tr data-client="'.htmlspecialchars($client).'">';
|
||||||
|
echo '<td class="chkcol"><input type="checkbox" class="rowchk"></td>';
|
||||||
|
foreach ($order as $id) {
|
||||||
|
if ($id === 'client_code') {
|
||||||
|
$val = $client;
|
||||||
|
} elseif (isset($qidSet[$id]) && strpos($id, '-') === false) {
|
||||||
|
$val = ($completed[$client][$id] ?? false) ? 'Done' : 'Not Done';
|
||||||
|
} else {
|
||||||
|
$raw = $answers[$client][$id] ?? '';
|
||||||
|
$val = (strlen(trim($raw)) > 0) ? t_val($id, $raw, $vals) : 'None';
|
||||||
|
}
|
||||||
|
echo '<td>'.htmlspecialchars($val).'</td>';
|
||||||
|
}
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Error: ".htmlspecialchars($e->getMessage());
|
||||||
|
} finally {
|
||||||
|
@unlink($tmp);
|
||||||
|
}
|
||||||
38
downloadFull.php
Executable file
38
downloadFull.php
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/downloadFull.php
|
||||||
|
require_once __DIR__ . '/common.php';
|
||||||
|
|
||||||
|
// Bearer Token
|
||||||
|
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||||
|
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
|
||||||
|
if (stripos($auth, 'Bearer ') !== 0) {
|
||||||
|
http_response_code(403); echo "Kein Token übergeben"; exit;
|
||||||
|
}
|
||||||
|
$token = substr($auth, 7);
|
||||||
|
if (!token_is_valid($token)) {
|
||||||
|
http_response_code(403); echo "Ungültiger Token"; exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master-verschlüsselte DB laden
|
||||||
|
$dbPath = __DIR__ . '/uploads/questionnaire_database';
|
||||||
|
if (!file_exists($dbPath)) { http_response_code(404); echo "Datenbank nicht gefunden"; exit; }
|
||||||
|
$encMaster = file_get_contents($dbPath);
|
||||||
|
if ($encMaster === false) { http_response_code(500); echo "Lesefehler"; exit; }
|
||||||
|
|
||||||
|
// Mit Master-Key entschlüsseln
|
||||||
|
$masterKey = get_master_key_bytes();
|
||||||
|
try {
|
||||||
|
$plain = aes256_cbc_decrypt_bytes($encMaster, $masterKey);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500); echo "Entschlüsselung fehlgeschlagen"; exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für diese Session neu verschlüsseln (Key aus Token via HKDF)
|
||||||
|
$sessionKey = hkdf_session_key_from_token($token);
|
||||||
|
$out = aes256_cbc_encrypt_bytes($plain, $sessionKey);
|
||||||
|
|
||||||
|
// Ausliefern
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Disposition: attachment; filename="questionnaire_database"');
|
||||||
|
header('Content-Length: ' . strlen($out));
|
||||||
|
echo $out;
|
||||||
106
header_order.json
Normal file
106
header_order.json
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
[
|
||||||
|
"client_code",
|
||||||
|
"questionnaire_1_demographic_information",
|
||||||
|
"questionnaire_1_demographic_information-coach_code",
|
||||||
|
"questionnaire_1_demographic_information-consent_instruction",
|
||||||
|
"questionnaire_1_demographic_information-no_consent_entered",
|
||||||
|
"questionnaire_1_demographic_information-accommodation",
|
||||||
|
"questionnaire_1_demographic_information-other_accommodation",
|
||||||
|
"questionnaire_1_demographic_information-client_code_entry_question",
|
||||||
|
"questionnaire_1_demographic_information-age",
|
||||||
|
"questionnaire_1_demographic_information-gender",
|
||||||
|
"questionnaire_1_demographic_information-country_of_origin",
|
||||||
|
"questionnaire_1_demographic_information-departure_country",
|
||||||
|
"questionnaire_1_demographic_information-since_in_germany",
|
||||||
|
"questionnaire_1_demographic_information-living_situation",
|
||||||
|
"questionnaire_1_demographic_information-number_family_members",
|
||||||
|
"questionnaire_1_demographic_information-languages_spoken",
|
||||||
|
"questionnaire_1_demographic_information-german_skills",
|
||||||
|
"questionnaire_1_demographic_information-school_years_total",
|
||||||
|
"questionnaire_1_demographic_information-school_years_origin",
|
||||||
|
"questionnaire_1_demographic_information-school_years_transit",
|
||||||
|
"questionnaire_1_demographic_information-school_years_germany",
|
||||||
|
"questionnaire_1_demographic_information-vocational_training",
|
||||||
|
"questionnaire_1_demographic_information-provisional_accommodation_since",
|
||||||
|
"questionnaire_2_rhs",
|
||||||
|
"questionnaire_2_rhs-coach_code",
|
||||||
|
"questionnaire_2_rhs-glass_explanation",
|
||||||
|
"questionnaire_2_rhs-q1_symptom",
|
||||||
|
"questionnaire_2_rhs-q2_symptom",
|
||||||
|
"questionnaire_2_rhs-q3_symptom",
|
||||||
|
"questionnaire_2_rhs-q4_symptom",
|
||||||
|
"questionnaire_2_rhs-q5_symptom",
|
||||||
|
"questionnaire_2_rhs-q6_symptom",
|
||||||
|
"questionnaire_2_rhs-q7_symptom",
|
||||||
|
"questionnaire_2_rhs-q8_symptom",
|
||||||
|
"questionnaire_2_rhs-q9_symptom",
|
||||||
|
"questionnaire_2_rhs-q10_reexperience_trauma",
|
||||||
|
"questionnaire_2_rhs-q11_physical_reaction",
|
||||||
|
"questionnaire_2_rhs-q12_emotional_numbness",
|
||||||
|
"questionnaire_2_rhs-q13_easily_startled",
|
||||||
|
"questionnaire_2_rhs-q14_intro",
|
||||||
|
"questionnaire_2_rhs-pain_rating_instruction",
|
||||||
|
"questionnaire_2_rhs-violence_question_1",
|
||||||
|
"questionnaire_2_rhs-times_happend",
|
||||||
|
"questionnaire_2_rhs-age_at_incident",
|
||||||
|
"questionnaire_2_rhs-conflict_since_arrival",
|
||||||
|
"questionnaire_2_rhs-times_happend2",
|
||||||
|
"questionnaire_2_rhs-age_at_incident2",
|
||||||
|
"questionnaire_2_rhs-asylum_procedure_since",
|
||||||
|
"questionnaire_3_integration_index",
|
||||||
|
"questionnaire_3_integration_index-coach_code",
|
||||||
|
"questionnaire_3_integration_index-feeling_connected_to_germany_question",
|
||||||
|
"questionnaire_3_integration_index-understanding_political_issues",
|
||||||
|
"questionnaire_3_integration_index-unexpected_expense_question",
|
||||||
|
"questionnaire_3_integration_index-dining_with_germans_question",
|
||||||
|
"questionnaire_3_integration_index-reading_german_articles_question",
|
||||||
|
"questionnaire_3_integration_index-visiting_doctor_question",
|
||||||
|
"questionnaire_3_integration_index-feeling_as_outsider_question",
|
||||||
|
"questionnaire_3_integration_index-recent_occupation_question",
|
||||||
|
"questionnaire_3_integration_index-discussing_politics_question",
|
||||||
|
"questionnaire_3_integration_index-contact_with_germans_question",
|
||||||
|
"questionnaire_3_integration_index-speaking_german_opinion_question",
|
||||||
|
"questionnaire_3_integration_index-job_search_question",
|
||||||
|
"questionnaire_4_consultation_results",
|
||||||
|
"questionnaire_4_consultation_results-coach_code",
|
||||||
|
"questionnaire_4_consultation_results-date_consultation_health_interview_result",
|
||||||
|
"questionnaire_4_consultation_results-consultation_decision",
|
||||||
|
"questionnaire_4_consultation_results-consent_conversation_in_6_months",
|
||||||
|
"questionnaire_4_consultation_results-participation_in_coaching",
|
||||||
|
"questionnaire_4_consultation_results-consent_coaching_given",
|
||||||
|
"questionnaire_4_consultation_results-consent_conversation_in_6_months",
|
||||||
|
"questionnaire_4_consultation_results-decision_after_reflection_period",
|
||||||
|
"questionnaire_4_consultation_results-professional_referral",
|
||||||
|
"questionnaire_4_consultation_results-confidentiality_agreement",
|
||||||
|
"questionnaire_4_consultation_results-health_insurance_card",
|
||||||
|
"questionnaire_4_consultation_results-consent_conversation_in_6_months",
|
||||||
|
"questionnaire_5_final_interview",
|
||||||
|
"questionnaire_5_final_interview-coach_code",
|
||||||
|
"questionnaire_5_final_interview-consent_followup_6_months",
|
||||||
|
"questionnaire_5_final_interview-date_final_interview",
|
||||||
|
"questionnaire_5_final_interview-amount_nat_appointments",
|
||||||
|
"questionnaire_5_final_interview-amount_session_flowers",
|
||||||
|
"questionnaire_5_final_interview-amount_session_stones",
|
||||||
|
"questionnaire_5_final_interview-termination_nat_coaching",
|
||||||
|
"questionnaire_5_final_interview-client_canceled_NAT",
|
||||||
|
"questionnaire_6_follow_up_survey",
|
||||||
|
"questionnaire_6_follow_up_survey-coach_code",
|
||||||
|
"questionnaire_6_follow_up_survey-follow_up",
|
||||||
|
"questionnaire_6_follow_up_survey-special_burden_question",
|
||||||
|
"questionnaire_6_follow_up_survey-glass_explanation",
|
||||||
|
"questionnaire_6_follow_up_survey-how_strong_past_month",
|
||||||
|
"questionnaire_6_follow_up_survey-q14_intro",
|
||||||
|
"questionnaire_6_follow_up_survey-pain_rating_instruction",
|
||||||
|
"questionnaire_6_follow_up_survey-feeling_connected_to_germany_question",
|
||||||
|
"questionnaire_6_follow_up_survey-understanding_political_issues",
|
||||||
|
"questionnaire_6_follow_up_survey-unexpected_expense_question",
|
||||||
|
"questionnaire_6_follow_up_survey-dining_with_germans_question",
|
||||||
|
"questionnaire_6_follow_up_survey-reading_german_articles_question",
|
||||||
|
"questionnaire_6_follow_up_survey-visiting_doctor_question",
|
||||||
|
"questionnaire_6_follow_up_survey-feeling_as_outsider_question",
|
||||||
|
"questionnaire_6_follow_up_survey-recent_occupation_question",
|
||||||
|
"questionnaire_6_follow_up_survey-discussing_politics_question",
|
||||||
|
"questionnaire_6_follow_up_survey-contact_with_germans_question",
|
||||||
|
"questionnaire_6_follow_up_survey-speaking_german_opinion_question",
|
||||||
|
"questionnaire_6_follow_up_survey-job_search_question"
|
||||||
|
]
|
||||||
330
index.html
Normal file
330
index.html
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Questionnaire Database</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 20px; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 12px; padding: 16px; margin-bottom: 16px; width: 100%; box-sizing: border-box; }
|
||||||
|
.hidden { display: none; }
|
||||||
|
.actions { margin: 12px 0; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||||
|
button, a.button { padding:8px 12px; border:1px solid #ccc; border-radius:8px; background:#fff; cursor:pointer; text-decoration:none; }
|
||||||
|
button:hover, a.button:hover { background:#f7f7f7; }
|
||||||
|
.muted { color:#666; }
|
||||||
|
|
||||||
|
/* Nur der Tabellenbereich scrollt vertikal */
|
||||||
|
.table-wrap { overflow-x: auto; border-radius: 8px; }
|
||||||
|
|
||||||
|
table { border-collapse: collapse; width: 100%; margin: 8px 0 24px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
|
||||||
|
|
||||||
|
/* BEIDE Kopfzeilen fixieren */
|
||||||
|
#qdb-table thead tr:nth-child(1) th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
#qdb-table thead tr:nth-child(2) th {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--head1h, 34px); /* wird per JS exakt gesetzt */
|
||||||
|
z-index: 2;
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-highlight { animation: hi 1.2s ease-in-out 0s 1; }
|
||||||
|
@keyframes hi { 0% { background:#fff9c4; } 100% { background:transparent; } }
|
||||||
|
|
||||||
|
.find-wrap { margin-left:auto; display:flex; gap:6px; align-items:center; }
|
||||||
|
.find-wrap input[type="text"] { padding:6px 8px; border:1px solid #ccc; border-radius:8px; min-width:160px; }
|
||||||
|
|
||||||
|
/* kompaktere „lange“ Spalte (2. Daten-Spalte nach Checkbox) */
|
||||||
|
#qdb-table th:nth-child(3),
|
||||||
|
#qdb-table td:nth-child(3) {
|
||||||
|
max-width: 260px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Questionnaire Database</h1>
|
||||||
|
|
||||||
|
<div id="loginCard" class="card">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form id="loginForm" autocomplete="on">
|
||||||
|
<label>Username<br><input id="username" required autocomplete="username" /></label><br><br>
|
||||||
|
<label>Password<br><input id="password" type="password" required autocomplete="current-password" /></label><br><br>
|
||||||
|
<button>Sign in</button>
|
||||||
|
</form>
|
||||||
|
<p id="loginMsg"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="appCard" class="card hidden">
|
||||||
|
<div class="actions">
|
||||||
|
<span>Signed in as <b id="who"></b>.</span>
|
||||||
|
<button id="logoutBtn">Logout</button>
|
||||||
|
<button id="exportSelectedBtn">Download</button>
|
||||||
|
<span id="selInfo" class="muted">(0 selected)</span>
|
||||||
|
|
||||||
|
<div class="find-wrap">
|
||||||
|
<label for="findClient">Find client:</label>
|
||||||
|
<input id="findClient" type="text" list="clientList" placeholder="Client code" />
|
||||||
|
<datalist id="clientList"></datalist>
|
||||||
|
<button id="findBtn" type="button">Go</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap" id="tableWrap">
|
||||||
|
<div id="dbContainer"><em>Loading…</em></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = sel => document.querySelector(sel);
|
||||||
|
|
||||||
|
function setLoggedIn(user, token) {
|
||||||
|
localStorage.setItem('qdb_user', user);
|
||||||
|
localStorage.setItem('qdb_token', token);
|
||||||
|
$('#who').textContent = user;
|
||||||
|
$('#loginCard').classList.add('hidden');
|
||||||
|
$('#appCard').classList.remove('hidden');
|
||||||
|
loadDbView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoggedOut() {
|
||||||
|
localStorage.removeItem('qdb_user');
|
||||||
|
localStorage.removeItem('qdb_token');
|
||||||
|
$('#dbContainer').innerHTML = '';
|
||||||
|
$('#appCard').classList.add('hidden');
|
||||||
|
$('#loginCard').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
$('#loginMsg').textContent = 'Signing in…';
|
||||||
|
try {
|
||||||
|
const res = await fetch('login.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: $('#username').value,
|
||||||
|
password: $('#password').value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || !data.success) {
|
||||||
|
$('#loginMsg').textContent = data.message || 'Login failed';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#loginMsg').textContent = '';
|
||||||
|
setLoggedIn(data.user, data.token);
|
||||||
|
} catch (e) {
|
||||||
|
$('#loginMsg').textContent = 'Error: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeTableArea() {
|
||||||
|
const wrap = document.getElementById('tableWrap');
|
||||||
|
if (!wrap) return;
|
||||||
|
const rect = wrap.getBoundingClientRect();
|
||||||
|
const top = rect.top;
|
||||||
|
const avail = window.innerHeight - top - 16;
|
||||||
|
wrap.style.maxHeight = (avail > 200 ? avail : 200) + 'px';
|
||||||
|
wrap.style.overflowY = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDbView() {
|
||||||
|
const token = localStorage.getItem('qdb_token');
|
||||||
|
if (!token) return setLoggedOut();
|
||||||
|
$('#dbContainer').innerHTML = '<em>Loading…</em>';
|
||||||
|
try {
|
||||||
|
const res = await fetch('db_view_ordered.php', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
if (res.status === 401 || res.status === 403) {
|
||||||
|
setLoggedOut();
|
||||||
|
alert('Session expired or invalid. Please sign in again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = await res.text();
|
||||||
|
$('#dbContainer').innerHTML = html;
|
||||||
|
|
||||||
|
const table = document.querySelector('#qdb-table');
|
||||||
|
|
||||||
|
const selInfo = $('#selInfo');
|
||||||
|
function updateCount(){
|
||||||
|
const n = table.querySelectorAll('tbody .rowchk:checked').length;
|
||||||
|
selInfo.textContent = `(${n} selected)`;
|
||||||
|
}
|
||||||
|
const chkAll = table.querySelector('#chkAll');
|
||||||
|
if (chkAll) {
|
||||||
|
chkAll.addEventListener('change', () => {
|
||||||
|
table.querySelectorAll('tbody .rowchk').forEach(ch => ch.checked = chkAll.checked);
|
||||||
|
updateCount();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
table.querySelectorAll('tbody .rowchk').forEach(ch => ch.addEventListener('change', updateCount));
|
||||||
|
updateCount();
|
||||||
|
|
||||||
|
$('#exportSelectedBtn').onclick = () => exportSelectedToXLSX();
|
||||||
|
|
||||||
|
buildClientSearchList();
|
||||||
|
const findInput = $('#findClient');
|
||||||
|
const findBtn = $('#findBtn');
|
||||||
|
findBtn.onclick = () => gotoClientRow(findInput.value.trim());
|
||||||
|
findInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); findBtn.click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Höhe der 1. Kopfzeile messen und als CSS-Variable setzen,
|
||||||
|
damit die 2. Kopfzeile darunter sticky stehen bleibt */
|
||||||
|
if (table && table.tHead && table.tHead.rows.length >= 1) {
|
||||||
|
const h1 = table.tHead.rows[0].getBoundingClientRect().height || 34;
|
||||||
|
table.style.setProperty('--head1h', `${Math.round(h1)}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeTableArea();
|
||||||
|
} catch (e) {
|
||||||
|
$('#dbContainer').innerHTML = '<b>Error:</b> ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', sizeTableArea);
|
||||||
|
|
||||||
|
function exportSelectedToXLSX() {
|
||||||
|
const table = document.querySelector('#qdb-table');
|
||||||
|
if (!table) return alert('Table not found.');
|
||||||
|
const headRows = table.tHead.rows;
|
||||||
|
if (headRows.length < 2) return alert('Header rows missing.');
|
||||||
|
|
||||||
|
const ids = [];
|
||||||
|
for (let i=1; i<headRows[0].cells.length; i++) {
|
||||||
|
const th = headRows[0].cells[i];
|
||||||
|
const id = th.getAttribute('data-id') || th.textContent.trim();
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
const labels = [];
|
||||||
|
for (let i=1; i<headRows[1].cells.length; i++) {
|
||||||
|
labels.push(headRows[1].cells[i].textContent.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = Array.from(table.tBodies[0].rows)
|
||||||
|
.filter(tr => tr.querySelector('.rowchk')?.checked);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
alert('Please select at least one row.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aoa = [];
|
||||||
|
aoa.push([...ids]);
|
||||||
|
aoa.push([...labels]);
|
||||||
|
|
||||||
|
rows.forEach(tr => {
|
||||||
|
const tds = tr.cells;
|
||||||
|
const arr = [];
|
||||||
|
for (let i=1; i<tds.length; i++) arr.push(tds[i].textContent.trim());
|
||||||
|
aoa.push(arr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet(aoa);
|
||||||
|
ws['!cols'] = ids.map(()=>({wch:36}));
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, 'Headers');
|
||||||
|
XLSX.writeFile(wb, 'SelectedClients.xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======= Suche (nur Client-Code) ======= */
|
||||||
|
function buildClientSearchList() {
|
||||||
|
const table = document.querySelector('#qdb-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
let clientCol = -1;
|
||||||
|
const thead = table.tHead;
|
||||||
|
const rows = thead ? thead.rows : [];
|
||||||
|
if (rows.length >= 2) {
|
||||||
|
const cells2 = Array.from(rows[1].cells).map(c => c.textContent.trim().toLowerCase());
|
||||||
|
clientCol = cells2.findIndex(t => t === 'client code' || t === 'client_code');
|
||||||
|
}
|
||||||
|
if (clientCol < 0 && rows.length >= 1) {
|
||||||
|
const cells1 = Array.from(rows[0].cells).map(c => (c.getAttribute('data-id') || c.textContent).trim().toLowerCase());
|
||||||
|
clientCol = cells1.findIndex(t => t === 'client code' || t === 'client_code');
|
||||||
|
}
|
||||||
|
if (clientCol < 0) return;
|
||||||
|
|
||||||
|
const dl = document.querySelector('#clientList');
|
||||||
|
dl.innerHTML = '';
|
||||||
|
|
||||||
|
const codes = new Set();
|
||||||
|
table.tBodies[0].querySelectorAll('tr').forEach(tr => {
|
||||||
|
const td = tr.cells[clientCol];
|
||||||
|
if (!td) return;
|
||||||
|
const val = td.textContent.trim();
|
||||||
|
if (val) codes.add(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.from(codes).sort((a,b)=> (''+a).localeCompare(''+b, undefined, {numeric:true}))
|
||||||
|
.forEach(code => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = code;
|
||||||
|
dl.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoClientRow(query) {
|
||||||
|
if (!query) return;
|
||||||
|
const table = document.querySelector('#qdb-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
let clientCol = -1;
|
||||||
|
const thead = table.tHead;
|
||||||
|
const rows = thead ? thead.rows : [];
|
||||||
|
if (rows.length >= 2) {
|
||||||
|
const cells2 = Array.from(rows[1].cells).map(c => c.textContent.trim().toLowerCase());
|
||||||
|
clientCol = cells2.findIndex(t => t === 'client code' || t === 'client_code');
|
||||||
|
}
|
||||||
|
if (clientCol < 0 && rows.length >= 1) {
|
||||||
|
const cells1 = Array.from(rows[0].cells).map(c => (c.getAttribute('data-id') || c.textContent).trim().toLowerCase());
|
||||||
|
clientCol = cells1.findIndex(t => t === 'client code' || t === 'client_code');
|
||||||
|
}
|
||||||
|
if (clientCol < 0) return;
|
||||||
|
|
||||||
|
const tryCols = [clientCol, clientCol + 1, Math.max(0, clientCol - 1)];
|
||||||
|
|
||||||
|
const bodyRows = Array.from(table.tBodies[0].rows);
|
||||||
|
let target = null;
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for (const tr of bodyRows) {
|
||||||
|
for (const col of tryCols) {
|
||||||
|
const txt = (tr.cells[col]?.textContent || '').trim();
|
||||||
|
if (!txt) continue;
|
||||||
|
if (txt === query || txt.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
target = tr;
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
target.classList.add('row-highlight');
|
||||||
|
setTimeout(() => target.classList.remove('row-highlight'), 1800);
|
||||||
|
} else {
|
||||||
|
alert('No matching client found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ======= Ende Suche ======= */
|
||||||
|
|
||||||
|
$('#loginForm').addEventListener('submit', login);
|
||||||
|
$('#logoutBtn').addEventListener('click', setLoggedOut);
|
||||||
|
|
||||||
|
const t = localStorage.getItem('qdb_token');
|
||||||
|
const u = localStorage.getItem('qdb_user');
|
||||||
|
if (t && u) setLoggedIn(u, t);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
91
login.php
Normal file
91
login.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/login.php
|
||||||
|
require_once __DIR__ . '/common.php';
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
// Default-Logins (Startzustand)
|
||||||
|
$USERS_DEFAULT = [
|
||||||
|
'user01' => 'pw1',
|
||||||
|
'user02' => 'pw2',
|
||||||
|
'Tmp_Daniel' => 'dummy4',
|
||||||
|
'Tmp_Johanna' => 'dummy1',
|
||||||
|
'Tmp_Anke' => 'dummy2',
|
||||||
|
'Tmp_Liliana' => 'dummy3',
|
||||||
|
'Amy_tmp' => 'dummy4',
|
||||||
|
'Brigitte_tmp' => 'dummy5',
|
||||||
|
'Extra1_tmp' => 'dummy6',
|
||||||
|
'Extra2_tmp' => 'dummy7',
|
||||||
|
'Danny' => 'pw1',
|
||||||
|
'User1' => 'pw1',
|
||||||
|
'User2' => 'pw2',
|
||||||
|
'User3' => 'pw3',
|
||||||
|
'User4' => 'pw4',
|
||||||
|
'User5' => 'pw5',
|
||||||
|
'User6' => 'pw6',
|
||||||
|
'User7' => 'pw7',
|
||||||
|
'User8' => 'pw8',
|
||||||
|
'User9' => 'pw9',
|
||||||
|
'User10' => 'pw10',
|
||||||
|
'User11' => 'pw11'
|
||||||
|
];
|
||||||
|
|
||||||
|
$STORE_FILE = __DIR__ . '/users.json'; // persistenter Speicher für gehashte Passwörter
|
||||||
|
|
||||||
|
function load_store($path) {
|
||||||
|
if (!file_exists($path)) return [];
|
||||||
|
$txt = file_get_contents($path);
|
||||||
|
$arr = json_decode($txt, true);
|
||||||
|
return is_array($arr) ? $arr : [];
|
||||||
|
}
|
||||||
|
function save_store($path, $data) {
|
||||||
|
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents("php://input");
|
||||||
|
$data = json_decode($raw, true) ?: [];
|
||||||
|
$username = trim($data['username'] ?? '');
|
||||||
|
$password = (string)($data['password'] ?? '');
|
||||||
|
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["success" => false, "message" => "Username/Passwort fehlt"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Prüfe, ob Nutzer bereits ein *eigenes*, gehashtes Passwort hat
|
||||||
|
$store = load_store($STORE_FILE); // Struktur: ["user01" => ["hash" => "...", "changedAt" => 1234567890]]
|
||||||
|
if (array_key_exists($username, $store)) {
|
||||||
|
$hash = (string)($store[$username]['hash'] ?? '');
|
||||||
|
if ($hash !== '' && password_verify($password, $hash)) {
|
||||||
|
// ok -> normaler Login
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
token_add($token, 30 * 24 * 60 * 60);
|
||||||
|
echo json_encode(["success" => true, "token" => $token, "user" => $username]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// hat schon Hash, aber PW falsch
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(["success" => false, "message" => "Ungültige Zugangsdaten"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Nutzer hat *noch* keinen Hash -> nur Default-Zugang erlaubt
|
||||||
|
if (!array_key_exists($username, $USERS_DEFAULT)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(["success" => false, "message" => "Ungültige Zugangsdaten"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($USERS_DEFAULT[$username] !== $password) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(["success" => false, "message" => "Ungültige Zugangsdaten"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Default-Passwort korrekt -> Passwortwechsel erzwingen
|
||||||
|
echo json_encode([
|
||||||
|
"success" => true,
|
||||||
|
"user" => $username,
|
||||||
|
"must_change_password" => true,
|
||||||
|
"message" => "Bitte Passwort ändern."
|
||||||
|
]);
|
||||||
104
questions_en.json
Normal file
104
questions_en.json
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"client_code": "Client code",
|
||||||
|
"questionnaire_1_demographic_information": "Questionnaire status",
|
||||||
|
"questionnaire_1_demographic_information-coach_code": "Coach Code",
|
||||||
|
"questionnaire_1_demographic_information-consent_instruction": "Obtain client consent (separate document)",
|
||||||
|
"questionnaire_1_demographic_information-no_consent_entered": "You have indicated that the client has not signed the consent form.",
|
||||||
|
"questionnaire_1_demographic_information-accommodation": "Accommodation",
|
||||||
|
"questionnaire_1_demographic_information-other_accommodation": "Other accommodation (name)",
|
||||||
|
"questionnaire_1_demographic_information-client_code_entry_question": "Please enter client code and coach code.",
|
||||||
|
"questionnaire_1_demographic_information-age": "Age: How old are you?",
|
||||||
|
"questionnaire_1_demographic_information-gender": "Gender",
|
||||||
|
"questionnaire_1_demographic_information-country_of_origin": "Country of origin: where are you from?",
|
||||||
|
"questionnaire_1_demographic_information-departure_country": "When did you leave your country of origin?",
|
||||||
|
"questionnaire_1_demographic_information-since_in_germany": "Since when have you been in Germany?",
|
||||||
|
"questionnaire_1_demographic_information-living_situation": "How are you living here?",
|
||||||
|
"questionnaire_1_demographic_information-number_family_members": "How many family members are you living with?",
|
||||||
|
"questionnaire_1_demographic_information-languages_spoken": "Which languages do you speak?",
|
||||||
|
"questionnaire_1_demographic_information-german_skills": "How would you rate your German language skills?",
|
||||||
|
"questionnaire_1_demographic_information-school_years_total": "How many years did you attend school?",
|
||||||
|
"questionnaire_1_demographic_information-school_years_origin": "How many years did you attend school? (in your home country)",
|
||||||
|
"questionnaire_1_demographic_information-school_years_transit": "How many years did you attend school? (while in transit)",
|
||||||
|
"questionnaire_1_demographic_information-school_years_germany": "How many years did you attend school? (in Germany)",
|
||||||
|
"questionnaire_1_demographic_information-vocational_training": "Have you completed vocational training?",
|
||||||
|
"questionnaire_1_demographic_information-provisional_accommodation_since": "Since when have you been in provisional accommodation?",
|
||||||
|
"questionnaire_2_rhs": "Questionnaire status",
|
||||||
|
"questionnaire_2_rhs-coach_code": "Coach Code",
|
||||||
|
"questionnaire_2_rhs-glass_explanation": "The response options are shown in words and as a picture of a glass container: if something does not apply at all, the glass is empty; if it applies somewhat or fairly, the glass fills up; if it applies extremely, the glass is full.",
|
||||||
|
"questionnaire_2_rhs-q1_symptom": "1. Muscle, bone, or joint pain",
|
||||||
|
"questionnaire_2_rhs-q2_symptom": "2. Feeling unhappy, sad, or depressed most of the time",
|
||||||
|
"questionnaire_2_rhs-q3_symptom": "3. Overthinking or worrying too much",
|
||||||
|
"questionnaire_2_rhs-q4_symptom": "4. Feeling helpless",
|
||||||
|
"questionnaire_2_rhs-q5_symptom": "5. Being easily startled for no reason",
|
||||||
|
"questionnaire_2_rhs-q6_symptom": "6. Fatigue, dizziness, or feeling weak",
|
||||||
|
"questionnaire_2_rhs-q7_symptom": "7. Feeling nervous or insecure",
|
||||||
|
"questionnaire_2_rhs-q8_symptom": "8. Feeling restless, unable to sit still",
|
||||||
|
"questionnaire_2_rhs-q9_symptom": "9. Feeling like crying easily",
|
||||||
|
"questionnaire_2_rhs-q10_reexperience_trauma": "10. ... re-experiencing the trauma; behaving or feeling as if it were happening again?",
|
||||||
|
"questionnaire_2_rhs-q11_physical_reaction": "11. ... experiencing physical reactions (e.g., sweating, rapid heartbeat) when reminded of the trauma?",
|
||||||
|
"questionnaire_2_rhs-q12_emotional_numbness": "12. ... feeling emotionally numb (e.g., feeling sad but unable to cry, unable to feel loving emotions)?",
|
||||||
|
"questionnaire_2_rhs-q13_easily_startled": "13. ... being more easily startled (e.g., when someone approaches from behind)?",
|
||||||
|
"questionnaire_2_rhs-q14_intro": "14. Thinking about your life in the past four weeks, to what extent do you feel that you:",
|
||||||
|
"questionnaire_2_rhs-pain_rating_instruction": "Choose the number (0–10) that best describes how much suffering the respondent experienced in the past week, including today.",
|
||||||
|
"questionnaire_2_rhs-violence_question_1": "Have you ever been injured by others so seriously that you needed medical attention? (doctor, hospital)",
|
||||||
|
"questionnaire_2_rhs-times_happend": "Has this ... happened?",
|
||||||
|
"questionnaire_2_rhs-age_at_incident": "At what age: at ... years?",
|
||||||
|
"questionnaire_2_rhs-conflict_since_arrival": "Since arriving in Germany, have you ever been involved in violent conflicts (physical fights, physical or psychological conflicts)?",
|
||||||
|
"questionnaire_2_rhs-times_happend2": "Has this ... happened?",
|
||||||
|
"questionnaire_2_rhs-age_at_incident2": "At what age: at ... years?",
|
||||||
|
"questionnaire_2_rhs-asylum_procedure_since": "Since when has your asylum procedure been ongoing?",
|
||||||
|
"questionnaire_3_integration_index": "Questionnaire status",
|
||||||
|
"questionnaire_3_integration_index-coach_code": "Coach Code",
|
||||||
|
"questionnaire_3_integration_index-feeling_connected_to_germany_question": "How connected do you feel to Germany (how well have you settled in Germany)?",
|
||||||
|
"questionnaire_3_integration_index-understanding_political_issues": "How well do you understand the most important political issues in Germany?",
|
||||||
|
"questionnaire_3_integration_index-unexpected_expense_question": "Imagine you had to make an unexpected but necessary payment. Which of the following amounts could you pay or afford (if you had free access to money)?",
|
||||||
|
"questionnaire_3_integration_index-dining_with_germans_question": "How often have you dined (e.g. dinner) with Germans in the past 12 months?",
|
||||||
|
"questionnaire_3_integration_index-reading_german_articles_question": "How well can you read simple newspaper articles in German about familiar topics (health, home, family, etc.) and understand the main points?",
|
||||||
|
"questionnaire_3_integration_index-visiting_doctor_question": "How easy or difficult would it be for you to see a doctor in this country? (making an appointment, etc. - not: 'you can't get an appointment', 'I would never go to a doctor here')",
|
||||||
|
"questionnaire_3_integration_index-feeling_as_outsider_question": "How often do you feel like an outsider in Germany?",
|
||||||
|
"questionnaire_3_integration_index-recent_occupation_question": "How have you been occupied in the last four weeks? (Please select only one answer)",
|
||||||
|
"questionnaire_3_integration_index-discussing_politics_question": "Since arriving in Germany, how often have you discussed important political topics in Germany with other people?",
|
||||||
|
"questionnaire_3_integration_index-contact_with_germans_question": "With how many Germans have you exchanged messages (phone, messenger chat (WhatsApp, Facebook...), or SMS) in the last 4 weeks?",
|
||||||
|
"questionnaire_3_integration_index-speaking_german_opinion_question": "How well can you participate in a conversation in German on familiar topics (children, family, school, home, etc.) and express your opinion?",
|
||||||
|
"questionnaire_3_integration_index-job_search_question": "How easy or difficult would it be for you to search for and find a job in this country (even if you may not be allowed to work: what would be your assumption if there were no restrictions)?",
|
||||||
|
"questionnaire_4_consultation_results": "Questionnaire status",
|
||||||
|
"questionnaire_4_consultation_results-coach_code": "Coach Code",
|
||||||
|
"questionnaire_4_consultation_results-date_consultation_health_interview_result": "Date of counseling interview (health interview result green/yellow/red)",
|
||||||
|
"questionnaire_4_consultation_results-consultation_decision": "Counseling decision (<b><font color='#9E9E9E'>0</font></b>)",
|
||||||
|
"questionnaire_4_consultation_results-consent_conversation_in_6_months": "Consent for conversation in 6 months:",
|
||||||
|
"questionnaire_4_consultation_results-participation_in_coaching": "Participation In Coaching",
|
||||||
|
"questionnaire_4_consultation_results-consent_coaching_given": "Consent for coaching is given",
|
||||||
|
"questionnaire_4_consultation_results-decision_after_reflection_period": "Decision on .............. (date) after reflection period",
|
||||||
|
"questionnaire_4_consultation_results-professional_referral": "Professional Referral",
|
||||||
|
"questionnaire_4_consultation_results-confidentiality_agreement": "Confidentiality Agreement",
|
||||||
|
"questionnaire_4_consultation_results-health_insurance_card": "Health Insurance Card",
|
||||||
|
"questionnaire_5_final_interview": "Final Interview",
|
||||||
|
"questionnaire_5_final_interview-coach_code": "Coach Code",
|
||||||
|
"questionnaire_5_final_interview-consent_followup_6_months": "Consent for conversation in 6 months",
|
||||||
|
"questionnaire_5_final_interview-date_final_interview": "Date of final interview",
|
||||||
|
"questionnaire_5_final_interview-amount_nat_appointments": "Number of NAT appointments (after the counseling interview)",
|
||||||
|
"questionnaire_5_final_interview-amount_session_flowers": "Number of storytelling sessions with flowers",
|
||||||
|
"questionnaire_5_final_interview-amount_session_stones": "Number of storytelling sessions with stones",
|
||||||
|
"questionnaire_5_final_interview-termination_nat_coaching": "Termination of NAT coaching",
|
||||||
|
"questionnaire_5_final_interview-client_canceled_NAT": "Client discontinued NAT",
|
||||||
|
"questionnaire_6_follow_up_survey": "Questionnaire status",
|
||||||
|
"questionnaire_6_follow_up_survey-coach_code": "Coach Code",
|
||||||
|
"questionnaire_6_follow_up_survey-follow_up": "Follow-up survey",
|
||||||
|
"questionnaire_6_follow_up_survey-special_burden_question": "Was there any special burden?",
|
||||||
|
"questionnaire_6_follow_up_survey-glass_explanation": "The response options are shown in words and as a picture of a glass container: if something does not apply at all, the glass is empty; if it applies somewhat or fairly, the glass fills up; if it applies extremely, the glass is full.",
|
||||||
|
"questionnaire_6_follow_up_survey-how_strong_past_month": "How strongly did you experience the following in the past month...",
|
||||||
|
"questionnaire_6_follow_up_survey-q14_intro": "14. Thinking about your life in the past four weeks, to what extent do you feel that you:",
|
||||||
|
"questionnaire_6_follow_up_survey-pain_rating_instruction": "Choose the number (0–10) that best describes how much suffering the respondent experienced in the past week, including today.",
|
||||||
|
"questionnaire_6_follow_up_survey-feeling_connected_to_germany_question": "How connected do you feel to Germany (how well have you settled in Germany)?",
|
||||||
|
"questionnaire_6_follow_up_survey-understanding_political_issues": "How well do you understand the most important political issues in Germany?",
|
||||||
|
"questionnaire_6_follow_up_survey-unexpected_expense_question": "Imagine you had to make an unexpected but necessary payment. Which of the following amounts could you pay or afford (if you had free access to money)?",
|
||||||
|
"questionnaire_6_follow_up_survey-dining_with_germans_question": "How often have you dined (e.g. dinner) with Germans in the past 12 months?",
|
||||||
|
"questionnaire_6_follow_up_survey-reading_german_articles_question": "How well can you read simple newspaper articles in German about familiar topics (health, home, family, etc.) and understand the main points?",
|
||||||
|
"questionnaire_6_follow_up_survey-visiting_doctor_question": "How easy or difficult would it be for you to see a doctor in this country? (making an appointment, etc. - not: 'you can't get an appointment', 'I would never go to a doctor here')",
|
||||||
|
"questionnaire_6_follow_up_survey-feeling_as_outsider_question": "How often do you feel like an outsider in Germany?",
|
||||||
|
"questionnaire_6_follow_up_survey-recent_occupation_question": "How have you been occupied in the last four weeks? (Please select only one answer)",
|
||||||
|
"questionnaire_6_follow_up_survey-discussing_politics_question": "Since arriving in Germany, how often have you discussed important political topics in Germany with other people?",
|
||||||
|
"questionnaire_6_follow_up_survey-contact_with_germans_question": "With how many Germans have you exchanged messages (phone, messenger chat (WhatsApp, Facebook...), or SMS) in the last 4 weeks?",
|
||||||
|
"questionnaire_6_follow_up_survey-speaking_german_opinion_question": "How well can you participate in a conversation in German on familiar topics (children, family, school, home, etc.) and express your opinion?",
|
||||||
|
"questionnaire_6_follow_up_survey-job_search_question": "How easy or difficult would it be for you to search for and find a job in this country (even if you may not be allowed to work: what would be your assumption if there were no restrictions)?"
|
||||||
|
}
|
||||||
192
tokens.jsonl
Normal file
192
tokens.jsonl
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
{"token":"731c101342f056ba57da7f958ba683fb41b0381d42de69090bcabee8c1b3a389","created":1756809382,"exp":1756895782}
|
||||||
|
{"token":"a1ba2845684ab849d364a84027670c3c6357028b3d1afb5f5b4868e904805235","created":1756809404,"exp":1756895804}
|
||||||
|
{"token":"6d2a7a1686cddb29441dfbe2e261d853ec55a1d46e1bbb8db3e4f8d0ce953937","created":1756809519,"exp":1756895919}
|
||||||
|
{"token":"1a0bc695ff5854e16b76dc60d5cbf51529942430b2d5315a36e0266136b9ab43","created":1756809545,"exp":1756895945}
|
||||||
|
{"token":"375ace02859b27ca837bfa5a130e6237b7c7b39720ff5d880b3519e2c8bbff57","created":1756809577,"exp":1756895977}
|
||||||
|
{"token":"95c8f8cd5a300c2357fb237c55932dd15079d777c69ac9c523f347c98e6a196c","created":1756809590,"exp":1756895990}
|
||||||
|
{"token":"66048cfaf8ba428c18125328d3d92ee5ef85fb5da4b57ca10e30ed49e65cb7a3","created":1756990520,"exp":1757076920}
|
||||||
|
{"token":"74b33d1ec4314def382db73b25a68bb835b847399587b9e1c753b5fc6d7ac526","created":1756990626,"exp":1757077026}
|
||||||
|
{"token":"cf64baa593827d9d213f65da58f68e4be4fede9d1d089f667247985244218b79","created":1757055382,"exp":1757141782}
|
||||||
|
{"token":"91161cab2fad81cc2fbd5bec8990bc763169b9151bd5686d5643a7df119ddbeb","created":1757057047,"exp":1757143447}
|
||||||
|
{"token":"01390c7efae09a04917042c7a33ee669f1f399aea1ad6e38fdf0ea128cb56293","created":1757057152,"exp":1757143552}
|
||||||
|
{"token":"76da7ea81a475703f54e628d34349cf58825f5c69a83cd7ac28864d46a070f3e","created":1757057205,"exp":1757143605}
|
||||||
|
{"token":"f39965dc4374d6e6a4ff1d7f3cfcebf8a46591496778de606d5e8be6583fc32b","created":1757057967,"exp":1757144367}
|
||||||
|
{"token":"91d056c9e6c74d6cfb3a7dfc490e3fb74e0fa1107a644fa9ebc770046ad10108","created":1757058317,"exp":1757144717}
|
||||||
|
{"token":"a50ca166c451e4eb1a6ecfbd8e8947ab33adc41638cfe490844d4f216df1b2b7","created":1757058371,"exp":1757144771}
|
||||||
|
{"token":"4fa698c244a21802e1001cd9e2ca95e3836f8a40b87130ba0bbf56e4409a5258","created":1757058387,"exp":1757144787}
|
||||||
|
{"token":"a0c28eaaf54cc6c85b0aa28abaf3579f7c730127f84782223b549eb69a4fbda2","created":1757315000,"exp":1757401400}
|
||||||
|
{"token":"e03f93c020e7a1e63ca7bd35e8996effad1f164e771747248086e6e1062361a9","created":1757318539,"exp":1757404939}
|
||||||
|
{"token":"a2b06b8b6d4c4f13c0e1764b444c9d54b6391a44df11d76e07b388f010cef535","created":1757318594,"exp":1757404994}
|
||||||
|
{"token":"df8b9dc5ac814565c4fc7543d7dca8385103de68c3463c4a6bf751ec935a930e","created":1757319230,"exp":1757405630}
|
||||||
|
{"token":"ea183dbe572c64451825d0383554ec325494f79a342de8be082ca356bd634861","created":1757326842,"exp":1757413242}
|
||||||
|
{"token":"f221237385a68cae4487815580ffde5dba191ab26e530629fbb9d25cfbd78b9b","created":1757328356,"exp":1757414756}
|
||||||
|
{"token":"1f1fbe3e4e61c93bf0dda1ecd1be15613d33a2065458789465ae6390df55bb6a","created":1758107596,"exp":1758193996}
|
||||||
|
{"token":"9b36e1cd9064f7a0d0b6932d7e2540c8167b5f0217d25ecb3366890cbe45ad0b","created":1758107713,"exp":1758194113}
|
||||||
|
{"token":"c5a33030ac64f9695c8afde31dd5c11846e9047a2dea117f49a133f90ce76924","created":1758107738,"exp":1758194138}
|
||||||
|
{"token":"f2077115a4969f4f17d14b1245d93afee249b9dc224472ca9fb3626a88c767df","created":1758477721,"exp":1758564121}
|
||||||
|
{"token":"fb6e30161a17fcf4d26fff6b6fdf9390c516cda64dcb3df9d7785f22ff18b1d1","created":1758477745,"exp":1758564145}
|
||||||
|
{"token":"767d37875a3d3e26eed32a0856ab36e81b500155a2a86b0349b185ec7fb3a090","created":1758477764,"exp":1758564164}
|
||||||
|
{"token":"606140c6c1c72ee6ed237b9bcd1d06f8e0706d693be2a04eeae4e90830a5bd18","created":1758477789,"exp":1758564189}
|
||||||
|
{"token":"5dca3d7add783b6448d863809bcc4389578c4b8512fe3f6f24c7cdbef4e1b908","created":1758523992,"exp":1758610392}
|
||||||
|
{"token":"ecb9246a30d8c24398351fe69b40730d5fa3b1ccc237f822cc6ec568702bdc61","created":1758524058,"exp":1758610458}
|
||||||
|
{"token":"971f7fc5b21463b1180ce20152e8e9412e6582638f02d0d3f0ab5b87e831c92c","created":1758524093,"exp":1758610493}
|
||||||
|
{"token":"02ec7c0520bb2e9992012a276fac309770aead2d1e21e201142c88e85dd77938","created":1758524109,"exp":1758610509}
|
||||||
|
{"token":"b6844c5a99952786846d065777bb33647e24dd59371c6e16b37aa6fde8d08635","created":1758524126,"exp":1758610526}
|
||||||
|
{"token":"e07239755729bbd43f13112d25a3fd7ca652de6705e24fa49bfe9fbe67a94a43","created":1758524433,"exp":1758610833}
|
||||||
|
{"token":"0e0e946dbef1255217b9418adf577d79d195db8e2e1060932cc824ba6efaed11","created":1758524480,"exp":1758610880}
|
||||||
|
{"token":"6d2a941987d0753b8f58d3058039ebc091286030be9f0d6be08a666c6212f325","created":1758631441,"exp":1758717841}
|
||||||
|
{"token":"a2cc4bedf2d3a070546a795cd2799e0e6ba7a5640c31acc458909d7467fbacfd","created":1758631465,"exp":1758717865}
|
||||||
|
{"token":"5e5d72eb15330fc4dbfb3acf262d43c730307b04206234f22ddf7efa784857f4","created":1758631622,"exp":1758718022}
|
||||||
|
{"token":"83e5311d921f2d77a13b3bffc3173e3f1ab402de5a0983da046dac3735d50669","created":1758631641,"exp":1758718041}
|
||||||
|
{"token":"51835ba874245fb5fb31bf795ab418173a1ac842c50d0555464c61b452d0759b","created":1758631699,"exp":1758718099}
|
||||||
|
{"token":"f7d1a8f2625a4dc23caf983f78ca6e6cc09fb5a1c9e622ffc47d194bcb1eb2fc","created":1758631715,"exp":1758718115}
|
||||||
|
{"token":"f262efa480d0d98034e6139072f672a8c287454a415327472b8d8b76fc431bb5","created":1758631821,"exp":1758718221}
|
||||||
|
{"token":"b3fff94205e321bca2bfa80e5edfafa51a42e1e94f2f5813dc1c226d98e9a21f","created":1758631831,"exp":1758718231}
|
||||||
|
{"token":"10ba334c91ab6af3f8cb768cd6e139b3fecaf180a7c4a44ac8d6f25712e4a5a9","created":1758632063,"exp":1758718463}
|
||||||
|
{"token":"53b7527c88487d67a77e110b5274c72d5620088aae58debfdd68fd1b9583438a","created":1758632075,"exp":1758718475}
|
||||||
|
{"token":"a8e92c5cfcce9f4e4a3402a6b275230d759dd2f0c4de0577d237e5bdebdd7a39","created":1758632214,"exp":1758718614}
|
||||||
|
{"token":"93c49902b428c4073dd61e93fbd3b3267dc99710e0eace885362c1d36d1bff11","created":1758632231,"exp":1758718631}
|
||||||
|
{"token":"9bbbfbb833576b33140add4a3a251d5d5d4d130b82c95aa37e8d5f67b6b3fa47","created":1758634360,"exp":1758720760}
|
||||||
|
{"token":"3f6d20603b463bbff6369b75ed20c482c088d194604438af636a065cc00d38cc","created":1758634417,"exp":1758720817}
|
||||||
|
{"token":"16f4c983c3b186d282b5b68534e1d581e6aa2922c9d8fcecda9c4de9481d85ee","created":1758634669,"exp":1758721069}
|
||||||
|
{"token":"ac0e1352a82161ec19577823712b93f62c6eecc645248f8213d80ee5d056aef8","created":1758634694,"exp":1758721094}
|
||||||
|
{"token":"c44e10696b9de668527ee7e645cb44c7aba983a5890b71dd3fc923ebe93807b2","created":1758634748,"exp":1758721148}
|
||||||
|
{"token":"7547fd0a6593b28c4f4778ba93a00fa93e905b4d99aef38d4cd3215254659d1e","created":1758634779,"exp":1758721179}
|
||||||
|
{"token":"03c005ba089d5ca9db37dec87abcc87eda453a11c88cc9e0129c9c4420c66cd7","created":1758634837,"exp":1758721237}
|
||||||
|
{"token":"17a40e3d0752d7b40b1ecdff2fef13f1a670f027d2152eaf60146ce988cf8418","created":1758634850,"exp":1758721250}
|
||||||
|
{"token":"404a6ce228679470fd88b9926777238bbc4c3487dda478f5e9ba68cff9be24ba","created":1758637923,"exp":1758724323}
|
||||||
|
{"token":"9306963145c2beac257849cd41d8a8994899c04eb1661bb891b765d3eac4d202","created":1758638114,"exp":1758724514}
|
||||||
|
{"token":"02f79fd4f0d88863f6277552aa73a58f44d3706e2bc916f9bfa155b51047c124","created":1758638334,"exp":1758724734}
|
||||||
|
{"token":"d7722fecb0fcc0f4788fdbfd7c9d72dca05ee9417992426c3a0289dd605ea4cd","created":1758638870,"exp":1758725270}
|
||||||
|
{"token":"e8f527f42d8a27af0a8dfc1ab94811c9654327542ee6fbf56ed762677d06d24e","created":1758638916,"exp":1758725316}
|
||||||
|
{"token":"b8b66d92eba410abf7cde0d47528dca87526d0e6ebff12ec6d872ff56a5dfbda","created":1758638951,"exp":1758725351}
|
||||||
|
{"token":"f7c5cf139714a7d161fef9ff8de76a68131b1bebfc44ccbe2540fe9bd078f0ca","created":1758713063,"exp":1758799463}
|
||||||
|
{"token":"7b969f437896d654492db1f012bd15454f7d9a54547372a00d500e8575c18ebc","created":1758880572,"exp":1758966972}
|
||||||
|
{"token":"217f9fc39604e187023233900ea85c3cd6b0500b9a2e31f96b61b26b2b03757e","created":1758880587,"exp":1758966987}
|
||||||
|
{"token":"54fc17d45268b6653d8354d52a0a538eb62d1dd602e874aed5527f3d70ae9bc4","created":1758880618,"exp":1758967018}
|
||||||
|
{"token":"2f3f7fd350188548cbbd76959a91120bf67c871d8b0ce0d372a7394a6c958b49","created":1758880849,"exp":1758967249}
|
||||||
|
{"token":"76758ca57e42ed78504d148e243d9c333a83431b0072f59807e3ba906576c714","created":1758881595,"exp":1758967995}
|
||||||
|
{"token":"7543060413d38da66b7b0b80a444e8e37d3317c0be32585682365007ff6ce176","created":1758881763,"exp":1758968163}
|
||||||
|
{"token":"8ceff5d37ee6e6bd58b349fce81fd2d7814280799897e1f3ec94dc2c02b5044b","created":1758882150,"exp":1758968550}
|
||||||
|
{"token":"f813faa3aced8e2bb4ee7f815a0e5cd7c15cd8a314d178a9bd4adaa1d65ad7a8","created":1758963705,"exp":1759050105}
|
||||||
|
{"token":"30172769015f9d95aca3cff58dcc8e82dd1c34ff34e5fa421b93f852c2efc995","created":1758963970,"exp":1759050370}
|
||||||
|
{"token":"286f6448269afb14ad3579b2ec20edf5a5215efc7b8c61a4da42a81f670eea5a","created":1758964604,"exp":1759051004}
|
||||||
|
{"token":"602ba60c2b42590fd6b81fa0374769c3fa5419e6431ad936556c53f858e0dd3d","created":1758964964,"exp":1759051364}
|
||||||
|
{"token":"60d72e4dd79c595caa31a363a18fb65d53b408de657be558428486e0f95c1624","created":1759130219,"exp":1759216619}
|
||||||
|
{"token":"7ecf40a37b1860287ab9749039dd06b1aa41fc968daee96abb86b1f5640e9a3b","created":1759131060,"exp":1759217460}
|
||||||
|
{"token":"9f7caf0c8b690935664c8458297becfe0f01d4c636cc27739b6a4bb422dce023","created":1759131973,"exp":1759218373}
|
||||||
|
{"token":"49c9c4105eaae364566eb87e8ade6f76ee501fe304bc34a435ce5f59ba6d0d61","created":1759132236,"exp":1759218636}
|
||||||
|
{"token":"0bf4c2a9b0635fd0a9e9e6729f958c0c521ba03763740ffe35423d006382f3d2","created":1759132261,"exp":1759218661}
|
||||||
|
{"token":"4a227bacb48a669ed6f767cd5355bbb2b7ebdb8413439cd880debf8be6f05854","created":1759132276,"exp":1759218676}
|
||||||
|
{"token":"9f835e179b993f1ce748f3a8deffb5e2b6c420ba0ed256d76b1008bccb09592b","created":1759132305,"exp":1759218705}
|
||||||
|
{"token":"97726da2f384f90cc9772113d25b05f270d83bd95d8e7174217e0f1b90b6c0d8","created":1759132645,"exp":1759219045}
|
||||||
|
{"token":"12602dd163016ff3f4cc44e2024af559ab1d3ee4da03b1dd80b9c89bde3b7dac","created":1759132832,"exp":1759219232}
|
||||||
|
{"token":"555ce8248ee91024d3d78a29a001411643739d68bbd1b845331423cb03a2b686","created":1759132975,"exp":1759219375}
|
||||||
|
{"token":"23920ccf38cd1d89b8600be7634adf5b568dd65c55683d1aa9ae0065f07d9dd0","created":1759133185,"exp":1759219585}
|
||||||
|
{"token":"05223b7302ff2ce1920395b840dbaf901b03a19e74e586c89e557c7ecd5c045a","created":1759133317,"exp":1759219717}
|
||||||
|
{"token":"4bb6e11294e7c1231420ed75a9bb6cdd65c29b1af3a83e33663d41cb64b0db2e","created":1759133349,"exp":1759219749}
|
||||||
|
{"token":"c23f83b74435194855203c703a4c763b69fe3de6161e64528855887551edd777","created":1759133401,"exp":1759219801}
|
||||||
|
{"token":"48ca9bc7937f8ec478f97b8d77680a507f671ce02bae4c354ac66a924b4d9a63","created":1759133422,"exp":1759219822}
|
||||||
|
{"token":"e6952e1cb227a985cd8546ec8008f7d606ab329cf1d19bb148957a2bb794fa8f","created":1759133445,"exp":1759219845}
|
||||||
|
{"token":"fe1dfd9aa8bdc9717fd2dfbd3867771c979b5e49fab8f45342b8405e5dddfb8d","created":1759133762,"exp":1759220162}
|
||||||
|
{"token":"d62b8043df3bbdbdb787806c5b492ceaa505402d099ba38c78a46a409d0c33f0","created":1759133809,"exp":1759220209}
|
||||||
|
{"token":"6c78c2e05d9021ca8a050954104c9e0c31f76e944fdac5d56c7227dbba048f72","created":1759133839,"exp":1759220239}
|
||||||
|
{"token":"13ccd9f6051c1a11e6950fed92cd0c4e60cf595e47a07cbb25f283ceac7bbf43","created":1759133903,"exp":1759220303}
|
||||||
|
{"token":"0d9e3dc9e5aef1fcf579e6dc55bb989d84af7e27999295cb60900206a2b043e7","created":1759134295,"exp":1759220695}
|
||||||
|
{"token":"5248a0c841ae09db2166f1cd3254b1672031f321c71212e775b07794ee2efb99","created":1759136855,"exp":1759223255}
|
||||||
|
{"token":"dd9941e9b616fe179e33c0a884a3bfc3ada56c2f626afcb9da63e3e585ab9226","created":1759137413,"exp":1759223813}
|
||||||
|
{"token":"ecced21404122b68765272c3a1c5707be9ae230ab0cf193d661fc4b09bafe78d","created":1759137429,"exp":1759223829}
|
||||||
|
{"token":"8e013fbd30ee8278356f08e0aa0d3f6e4e709ae5c5246a731a8034070eda7079","created":1759137677,"exp":1759224077}
|
||||||
|
{"token":"b35841e30ea56d0bbf1274a7606294d1b85d149aebca4389675cb47731a25e1d","created":1759138841,"exp":1759225241}
|
||||||
|
{"token":"a82c1f1b051f51b33f1f3e676511a07f4914395c1fddce1aeee41aa0c398fa27","created":1759138863,"exp":1759225263}
|
||||||
|
{"token":"6647b9b2900320da05769955bdc172dabab24c6a24d258659593978d7b8d8be9","created":1759138920,"exp":1759225320}
|
||||||
|
{"token":"68b87e342ee484d6b9d278913ed8ca7864780e2672c520941eca7adcdabc90d0","created":1759138985,"exp":1759225385}
|
||||||
|
{"token":"41584f24f999a2b08bca7a26716b7bb82e27df6f3bf22efd925a05c1e49e7f4e","created":1759138997,"exp":1759225397}
|
||||||
|
{"token":"ee10e4e87b7c80c875e622d53fc1f3658b4d7e6d4cfb86a8b1af8a00537d76af","created":1759139042,"exp":1759225442}
|
||||||
|
{"token":"4a9fc9ad92631aa019e2f4a6c829bc47277bfad191ef7a0cff744b30949e5002","created":1759139881,"exp":1759226281}
|
||||||
|
{"token":"a3888918ee647f213e7408e6fff916e99f6fb3e757fe44a02f279b508da2016f","created":1759139945,"exp":1759226345}
|
||||||
|
{"token":"9b574f805ec4ab45a99aadf98246cc3cac3839cc25cc9f1f40ffc4a314cf5390","created":1759141376,"exp":1759227776}
|
||||||
|
{"token":"ef0d5340827ffc436c88863199d820cf7d8143f260c4f3f89a9438cbbae9e9ea","created":1759141871,"exp":1759228271}
|
||||||
|
{"token":"2d51a5ead5266958a15313b8dd92109d90ae0069c476328af7e764e3284e6563","created":1759141889,"exp":1759228289}
|
||||||
|
{"token":"01ce0e40ecd1253e53eaba374b09e363535fe511cea5c11a9cc7e2d50b5a5d70","created":1759141930,"exp":1759228330}
|
||||||
|
{"token":"d2a967c7ef1cfd6444f9f98865b71b7a50eee6fb8746b33f753bec23fe8e134c","created":1759141955,"exp":1759228355}
|
||||||
|
{"token":"31b442f6c1787dfc4090e8687d697bf3b9947acee107595054014892688b69fc","created":1759143187,"exp":1759229587}
|
||||||
|
{"token":"2cb95a73cd824886d5a8ef8f78e4ea50bec48545399b4ec8a97569e11ad1fae7","created":1759143242,"exp":1759229642}
|
||||||
|
{"token":"1270a387b5ed061801df25c587a32c16d19e01705bad9b33b5c8d081a8a6e405","created":1759665689,"exp":1759752089}
|
||||||
|
{"token":"cf99f7b82cf9d5929e171c4a10bfe2f804f8dbcfa8d4615032d116975273a20a","created":1759670447,"exp":1759756847}
|
||||||
|
{"token":"99a2398703836d3b4ad35122f1eceaaf752404a61df5932684f1bde2983065b0","created":1759671255,"exp":1759757655}
|
||||||
|
{"token":"492feb4f9c7cbdc9ff81003cf0ea821584ac9018d556ce64303e39f402644151","created":1759672995,"exp":1759759395}
|
||||||
|
{"token":"4950d4b18494285205bd5a1925d89c74a9a27d8bf61fea546a60c3ff77434c8a","created":1759769234,"exp":1759855634}
|
||||||
|
{"token":"d15b65be6c96d98d273bf75542da813d8bdf9a3e67b21b5d6e6ffc9578d825f6","created":1759769252,"exp":1759855652}
|
||||||
|
{"token":"7455244c86cfa13f6a35bb350669fb118ed559eec1b04615af6e61df24b15860","created":1759770329,"exp":1759856729}
|
||||||
|
{"token":"de71102f6b394bf1c9fefdabb3f81063947971d9c0b3039044f8a5ccb06fe0b9","created":1760019733,"exp":1760106133}
|
||||||
|
{"token":"33dfc2f32a92c33add844c063b6441435fbd81b099687f562f91bb844cce51fd","created":1760019746,"exp":1760106146}
|
||||||
|
{"token":"af9b3962b69dd2ed8101a6c82a4d2b3c16e6dcfe9bd2f704dad5ad89e5f4ec78","created":1760020288,"exp":1760106688}
|
||||||
|
{"token":"60476805d087f95b73bfc85906f3da2d564f0da823be997bff56840b9a3cbf83","created":1760020419,"exp":1760106819}
|
||||||
|
{"token":"e2865b9b9e93ab3a6824bbfc7b8a538d9504c5e742d8311ac8cff1cd5b1be537","created":1760020537,"exp":1760106937}
|
||||||
|
{"token":"e9525d496c03eab391ff7a33bebcf894e27739dbe72dbfd78561bd3f0abeeb04","created":1760020598,"exp":1760106998}
|
||||||
|
{"token":"3dab956d4162eafcafbc909774e5c72b8f705bd0118526ec64fc9aad779ffbd1","created":1760100224,"exp":1760186624}
|
||||||
|
{"token":"46048543edd7e1164f09e15ad7b270d0cbe902a8e9cf83c74b336126e9812919","created":1760100259,"exp":1760186659}
|
||||||
|
{"token":"c6769ca917c5b7bc3a8de449d4c1ee432a48af2816106d45151da78b6df5f6fe","created":1760102088,"exp":1760188488}
|
||||||
|
{"token":"1d661c5092d3f5cb49d9040eca309d899d76aab6853f730e10e11251aeb0e3e6","created":1760102101,"exp":1760188501}
|
||||||
|
{"token":"18678f9d4b37859266ce5f94974b50b8b79c6b2f8ba95b1127e21e55584242a1","created":1760102109,"exp":1760188509}
|
||||||
|
{"token":"4d0242faef13d10b3a601a221193e541eafaa4bc3ee6bd803c1ffb7a50839809","created":1760102145,"exp":1760188545}
|
||||||
|
{"token":"7b70d5aa0f414c658968462359345b2a3a14a9d4a5975b6e11120b474cda67ea","created":1760102318,"exp":1760188718}
|
||||||
|
{"token":"5cc22039581e43ee6ca998bddef9f38406074365ebab0f19f78a3770d129c80a","created":1760102851,"exp":1760189251}
|
||||||
|
{"token":"ca738bb4509efb56d9ed890202fe610e086e6c060a937fe24f0950d5e1ad0e91","created":1760102867,"exp":1760189267}
|
||||||
|
{"token":"63a52bf36dfdbdf183bc3645c0499c7b9d7407ff4e0b68d0ba4a7c4d21b3b2bd","created":1760373199,"exp":1760459599}
|
||||||
|
{"token":"540025ab075bd0b0d5b5f32136a2d728d33940428f1e03cb1cbefa2a4031cb22","created":1760373303,"exp":1760459703}
|
||||||
|
{"token":"698c35a7a528eff612691bd1e5b8d87606282e602946f4f3e780ad07882ccf39","created":1760376043,"exp":1760462443}
|
||||||
|
{"token":"03014ddfae73da00708075e584b7acc7453bd6fb09336ea4d0f311dea048cf74","created":1760423624,"exp":1760510024}
|
||||||
|
{"token":"59fe7551c92538ba16a8cb037cb4841e804955e541a711b93ce872c0cbfa4822","created":1760428837,"exp":1760515237}
|
||||||
|
{"token":"4e721a7c891ce14f50525807fefa7f30935fb646abcbe797a57967638d3153b1","created":1760429061,"exp":1760515461}
|
||||||
|
{"token":"df53f6ee07a698402ae87f2b61d0f1449eb9e8947379f8b5e49416577f44466e","created":1760429937,"exp":1760516337}
|
||||||
|
{"token":"72d697bda7b5b1b6c433f77d4974d056101cbfe59ed4a3b797b43e501e7315ae","created":1760602832,"exp":1760689232}
|
||||||
|
{"token":"f0f8bf32768a2741cc503b990d18123c5d4baf492e9a8d842eaaa1010ee5bb88","created":1760608588,"exp":1760694988}
|
||||||
|
{"token":"b7840ea72c124152e09992cc55bb8ea4239ec90fb5970caa2142344be49cd4c5","created":1760608677,"exp":1760695077}
|
||||||
|
{"token":"08aa5a790cc9fd8cef3b9a75b5b65286544c008dc2a9ea8f0d43b9e25fbe19a7","created":1760610683,"exp":1760697083}
|
||||||
|
{"token":"2cbc45e37c995d2673eb5b5d4f5dbc470e69b30b634972bf358c92afe841da06","created":1760610695,"exp":1760697095}
|
||||||
|
{"token":"bc6cf1156147f85d1b20982dd9f8730bf0fbf50b5740f62cf52bea5d5dd28c82","created":1760611508,"exp":1760697908}
|
||||||
|
{"token":"f1ff89caf2b5b3a68df5b091a9f158db2174f8e865f7d632cabe156c96847099","created":1760612940,"exp":1760699340}
|
||||||
|
{"token":"5cd4cc46d876fb7f6ac0435dd35f7b1f10637ef6aea0084cddcf701ee385747d","created":1760612996,"exp":1760699396}
|
||||||
|
{"token":"7d88c0fa36dc13981eacf5b57f36d666727cc516ef546a84e5708b0c47998175","created":1760613447,"exp":1760699847}
|
||||||
|
{"token":"627765059218d2e0f829887025a3c2c3319dbbfa052698ef4bbfa9932da34cd7","created":1760971357,"exp":1761057757}
|
||||||
|
{"token":"be417f82627bee9786ea5f8b99ce64a4dc855114f3e90e81192243bd7f9ee53c","created":1760972135,"exp":1761058535}
|
||||||
|
{"token":"4c4a6345cdd1a3a5556e44d2d306224eaf00ae580c46c4e0a305f9bf83ae1f1b","created":1761039024,"exp":1761125424}
|
||||||
|
{"token":"7b8def5d4ba5727c1332601b9652440c4850cfaa87cd93b38b5575b6ee2b93cc","created":1761064340,"exp":1761150740}
|
||||||
|
{"token":"33b38bbae91754d7459ea880a76f348765bc8c9a348ea32217efc40b487bcceb","created":1763290822,"exp":1763377222}
|
||||||
|
{"token":"9dde0839b583c875468530ee6fa2a92b85f4689bee30f59dd4e321fc2893cccd","created":1771865346,"exp":1771951746}
|
||||||
|
{"token":"cd7ef79a5664faf211e7508aa3e1da38a9295d3d7f30852abf87f657c4545ef3","created":1771865413,"exp":1771951813}
|
||||||
|
{"token":"3f71f2fde72c9fa670e4e17ac67da022952e5526fe3b4ffd70820046bab7dc7d","created":1771865429,"exp":1771951829}
|
||||||
|
{"token":"4eed29d1c55d2bcfc9fb218177062cd55f502ebca01915f121c4f725ab3886f3","created":1771865524,"exp":1771951924}
|
||||||
|
{"token":"508fdc8268cdbf8e58acbc6d0a2113f4f266f562ec2e7bbd2b4586226930bc1c","created":1771865748,"exp":1771952148}
|
||||||
|
{"token":"ea7c5c53ae3add1263435239a377f55a8edb3c3db0bb9d6ca8ca9755f40c952c","created":1771866052,"exp":1771952452}
|
||||||
|
{"token":"16db690623237f5f1e748c335ff72beb34be1ddf8117937705fc3f956d5854d4","created":1771866144,"exp":1771952544}
|
||||||
|
{"token":"7c079d53022f27743a8f41c2c16ba241622dde305d07df3da142feb5ce109a7a","created":1771866278,"exp":1771952678}
|
||||||
|
{"token":"afd3d79a917c910596bffd65ddd3d806f2c8c99f0ab9e398080989e0c352a3a0","created":1771866586,"exp":1771952986}
|
||||||
|
{"token":"e320bc456c9ab043f6259d687ff8074f0cd10ba63618f89671d584243b7be9e1","created":1771866962,"exp":1771953362}
|
||||||
|
{"token":"b37345318d18290039246d9473d0dcd64794ee2e9c024ecf6ec2c80a2d81820a","created":1771937505,"exp":1772023905}
|
||||||
|
{"token":"275398f85a005c7e58bdac6c5b001e544aee3cec9f5d323c4f98fe8578f39ae7","created":1771937569,"exp":1772023969}
|
||||||
|
{"token":"0476b9a7134403ab3fb28e0da8b00897a18bd1f20c58b3f66e49427f4e4fab17","created":1771939864,"exp":1772026264}
|
||||||
|
{"token":"6e4372fb37e0abd54d5a559978b1742bfbead965376df39439c3b809857dead9","created":1771940580,"exp":1772026980}
|
||||||
|
{"token":"dc58bb4fde7a94f169998ce31a1ac52433572b52f696d8af298dfe4191b3864c","created":1771940628,"exp":1772027028}
|
||||||
|
{"token":"e11133b3ab5c850ecc75a6496a882a7e7b348d4c449eba7d6192f598defdb229","created":1771940848,"exp":1772027248}
|
||||||
|
{"token":"44e8ac2a418d7b759c061d7f6187a0023736d2a0104b7ea70c1459ec8dab4700","created":1771941296,"exp":1772027696}
|
||||||
|
{"token":"9d39a349397f05b07b11fb1b07fe04d703030ed322d5f61bdf9b3dd81e244b59","created":1772000804,"exp":1772087204}
|
||||||
|
{"token":"b3e448c188ec718cf7aec6601fbd800cb087d08475ee95adc753eaab1a6ea800","created":1772001151,"exp":1772087551}
|
||||||
|
{"token":"9714b678caa1dded7222bb00ac466c8163bf1003dbaaf290d694be764e4a5c46","created":1772001535,"exp":1772087935}
|
||||||
|
{"token":"48721aaa13f78d571a28275e39fec81586eabda937fee765720ec72e42d82aa1","created":1772002283,"exp":1772088683}
|
||||||
|
{"token":"36242c20ed90459c72d12d7541311d803da304e3a50848a06ee47b0896543e9c","created":1772002329,"exp":1772088729}
|
||||||
|
{"token":"af278b2e4b05d94ff422dc4391e4aa1a5c09d43f3b34f1e3e522696ab631e07a","created":1772101322,"exp":1772187722}
|
||||||
|
{"token":"7abe6b6ffb9f168bcce197ee0e2287d9ee5055551d73aea70e479a8e4ef6ef01","created":1772106567,"exp":1772192967}
|
||||||
|
{"token":"88052fc72bf9ad9f99b70c4197322aa270dfef966e50a2ad268f62cbbae9e8d9","created":1772110997,"exp":1772197397}
|
||||||
|
{"token":"af85e2994c1d9e844d9179171e57ff3542de845967f1d1f4f90107029d814f20","created":1772111983,"exp":1772198383}
|
||||||
|
{"token":"3119ce71143809f78316ce131bc2e93bbf113b7b2a103fa20d584f233efc7a43","created":1772112290,"exp":1772198690}
|
||||||
|
{"token":"0474561405352eb731e66ed121a827c951673b7ff1b7032ba6195f9d19abde59","created":1772114237,"exp":1772200637}
|
||||||
|
{"token":"12719b1ce7992358d537eacb1d39397292acbc76fa12bde72c9d4314ae6f80dd","created":1772114779,"exp":1772201179}
|
||||||
|
{"token":"173d5f63ae00a092ebd135f0c09efe959bd23e426fb13159f8dea14f1dc4b924","created":1772283082,"exp":1772369482}
|
||||||
|
{"token":"8527a42eedef65315ed02f22f0a489279b5f8fe9f90a3a3e657fe4db3fdc5f48","created":1772452494,"exp":1772538894}
|
||||||
|
{"token":"8b9a0f52b37fa7dd28fc52ea8f6b6d40d60240dd978a1b78d1c295594836b954","created":1772698644,"exp":1772785044}
|
||||||
|
{"token":"d0bd7435c514c3f090d06ab92bc208b5d8eabd7cddd333427bd329257521a124","created":1772698738,"exp":1772785138}
|
||||||
|
{"token":"8f0959185b3c22aa86310192e8e81096c503305e211780a0e9a446711e762639","created":1772699719,"exp":1775291719}
|
||||||
|
{"token":"9bd42a9a3d9364f3087dd6e15b991daf764b4510cec728369e5b870f08ca91ad","created":1772703629,"exp":1775295629}
|
||||||
348
translations_en.json
Normal file
348
translations_en.json
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
{
|
||||||
|
"client_code_entry_question": "Please enter client code and coach code.",
|
||||||
|
"all_fields_between_0_and_15": "Please enter a number between 0 and 15 in all fields!",
|
||||||
|
"fill_both_fields": "Please fill in both fields!",
|
||||||
|
"please_answer_all": "Please answer all questions!",
|
||||||
|
"select_accommodation": "Please select an accommodation!",
|
||||||
|
"enter_coach_code": "Please enter your coach code to continue!",
|
||||||
|
"enter_country": "Please enter a country to continue!",
|
||||||
|
"only_letters_allowed": "Please enter letters only!",
|
||||||
|
"enter_valid_number": "Please enter a valid number!",
|
||||||
|
"enter_valid_year": "Please enter a valid year!",
|
||||||
|
"enter_text_to_continue": "Please enter text to continue!",
|
||||||
|
"select_at_least_one_language": "Please select at least one language!",
|
||||||
|
"select_country": "Please select a country",
|
||||||
|
"select_gender": "Please select a gender!",
|
||||||
|
"select_one_answer": "Please select one answer!",
|
||||||
|
"select_one_option": "Please select one option!",
|
||||||
|
"coach_code": "Coach Code",
|
||||||
|
"client_code": "Client Code",
|
||||||
|
"select_month": "Select month!",
|
||||||
|
"select_year": "Select year!",
|
||||||
|
"choose_answer": "Choose answer",
|
||||||
|
"choose_option": "Choose option!",
|
||||||
|
"consent_not_signed": "Consent not signed",
|
||||||
|
"consent_signed": "Consent signed",
|
||||||
|
"error_file": "Error processing file!",
|
||||||
|
"no_school_attended": "I did not attend school!",
|
||||||
|
"once": "once",
|
||||||
|
"year_after_2000": "The year must be after 2000!",
|
||||||
|
"year_after_departure": "The year must be after leaving the country of origin!",
|
||||||
|
"year_max": "The year must be less than or equal to $MAX_VALUE_YEAR!",
|
||||||
|
"data_final_warning": "<b><font color='#FF0000'>Important:</font></b> The data cannot be changed or edited after completion!",
|
||||||
|
"multiple_times": "multiple times",
|
||||||
|
"more_than_15_years": "more than 15 years",
|
||||||
|
"no": "No",
|
||||||
|
"no_answer": "No answer",
|
||||||
|
"other_country": "Other country",
|
||||||
|
"value_must_be_less_equal_max": "The value must be less than or equal to $MAX_VALUE_AGE!",
|
||||||
|
"value_between_1_and_15": "The value must be between 1 and 15!",
|
||||||
|
"invalid_month": "Invalid month!",
|
||||||
|
"invalid_year": "Invalid year!",
|
||||||
|
"yes": "Yes",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Back",
|
||||||
|
"finish": "Complete data entry",
|
||||||
|
"thank_you_participation": "Thank you for participating in this survey.",
|
||||||
|
"response_recorded": "Your response has been recorded.",
|
||||||
|
"january": "January",
|
||||||
|
"february": "February",
|
||||||
|
"march": "March",
|
||||||
|
"april": "April",
|
||||||
|
"may": "May",
|
||||||
|
"june": "June",
|
||||||
|
"july": "July",
|
||||||
|
"august": "August",
|
||||||
|
"september": "September",
|
||||||
|
"october": "October",
|
||||||
|
"november": "November",
|
||||||
|
"december": "December",
|
||||||
|
"consent_instruction": "Obtain client consent (separate document)",
|
||||||
|
"no_consent_entered": "You have indicated that the client has not signed the consent form.",
|
||||||
|
"no_consent_note": "This is important information for the evaluation of 'BW schützt!'.",
|
||||||
|
"coach_code_request": "Please enter your coach code and the name of the accommodation below. Then upload the data as usual.",
|
||||||
|
"other_accommodation": "Other accommodation (name)",
|
||||||
|
"age": "Age: How old are you?",
|
||||||
|
"gender": "Gender",
|
||||||
|
"gender_male": "Male",
|
||||||
|
"gender_female": "Female",
|
||||||
|
"gender_diverse": "Diverse",
|
||||||
|
"country_of_origin": "Country of origin: where are you from?",
|
||||||
|
"country_text_entry": "Country of origin (text entry)",
|
||||||
|
"departure_country": "When did you leave your country of origin?",
|
||||||
|
"year": "Year",
|
||||||
|
"since_in_germany": "Since when have you been in Germany?",
|
||||||
|
"living_situation": "How are you living here?",
|
||||||
|
"alone": "Alone",
|
||||||
|
"with_family": "With family members",
|
||||||
|
"number_family_members": "How many family members are you living with?",
|
||||||
|
"number_label": "Number of family members",
|
||||||
|
"languages_spoken": "Which languages do you speak?",
|
||||||
|
"language_albanian": "Albanian",
|
||||||
|
"language_pashto": "Pashto",
|
||||||
|
"language_russian": "Russian",
|
||||||
|
"language_serbo": "Serbo-Croatian",
|
||||||
|
"language_somali": "Somali",
|
||||||
|
"language_tamil": "Tamil",
|
||||||
|
"language_tigrinya": "Tigrinya",
|
||||||
|
"language_turkish": "Turkish",
|
||||||
|
"language_ukrainian": "Ukrainian",
|
||||||
|
"language_urdu": "Urdu",
|
||||||
|
"language_other": "Other",
|
||||||
|
"language_arabic": "Arabic",
|
||||||
|
"language_dari_farsi": "Dari/Farsi",
|
||||||
|
"language_chinese": "Chinese",
|
||||||
|
"language_english": "English",
|
||||||
|
"language_macedonian": "Macedonian",
|
||||||
|
"language_kurmanji": "Kurdish-Kurmanji",
|
||||||
|
"language_hindi": "Hindi",
|
||||||
|
"language_french": "French",
|
||||||
|
"german_skills": "How would you rate your German language skills?",
|
||||||
|
"skill_none": "None",
|
||||||
|
"skill_basic": "Basic knowledge",
|
||||||
|
"skill_a1": "A1",
|
||||||
|
"skill_a2": "A2",
|
||||||
|
"skill_b1": "B1",
|
||||||
|
"skill_b2": "B2",
|
||||||
|
"skill_c1": "C1",
|
||||||
|
"skill_c2": "C2",
|
||||||
|
"school_years_origin": "How many years did you attend school? (in your home country)",
|
||||||
|
"school_years_transit": "How many years did you attend school? (while in transit)",
|
||||||
|
"school_years_germany": "How many years did you attend school? (in Germany)",
|
||||||
|
"label_years": "Years",
|
||||||
|
"vocational_training": "Have you completed vocational training?",
|
||||||
|
"answer_no": "No",
|
||||||
|
"answer_started": "Started",
|
||||||
|
"answer_completed": "Completed",
|
||||||
|
"provisional_accommodation_since": "Since when have you been in provisional accommodation?",
|
||||||
|
"asylum_procedure_since": "Since when has your asylum procedure been ongoing?",
|
||||||
|
"accommodation": "Accommodation",
|
||||||
|
"steinstrasse": "Steinstraße",
|
||||||
|
"doerfle": "Dörfle",
|
||||||
|
"zoll_emmishofer": "Zoll (Emmishofer Straße)",
|
||||||
|
"other": "Other",
|
||||||
|
"school_years_total": "How many years did you attend school?",
|
||||||
|
"glass_explanation": "The response options are shown in words and as a picture of a glass container: if something does not apply at all, the glass is empty; if it applies somewhat or fairly, the glass fills up; if it applies extremely, the glass is full.",
|
||||||
|
"symptoms": "Symptoms",
|
||||||
|
"never_glass": "Never",
|
||||||
|
"little_glass": "A little",
|
||||||
|
"moderate_glass": "Moderately",
|
||||||
|
"much_glass": "Much",
|
||||||
|
"extreme_glass": "Extremely",
|
||||||
|
"q1_symptom": "1. Muscle, bone, or joint pain",
|
||||||
|
"q2_symptom": "2. Feeling unhappy, sad, or depressed most of the time",
|
||||||
|
"q3_symptom": "3. Overthinking or worrying too much",
|
||||||
|
"q4_symptom": "4. Feeling helpless",
|
||||||
|
"q5_symptom": "5. Being easily startled for no reason",
|
||||||
|
"q6_symptom": "6. Fatigue, dizziness, or feeling weak",
|
||||||
|
"q7_symptom": "7. Feeling nervous or insecure",
|
||||||
|
"q8_symptom": "8. Feeling restless, unable to sit still",
|
||||||
|
"q9_symptom": "9. Feeling like crying easily",
|
||||||
|
"how_strong_past_month": "How strongly did you experience the following in the past month...",
|
||||||
|
"q10_reexperience_trauma": "10. ... re-experiencing the trauma; behaving or feeling as if it were happening again?",
|
||||||
|
"q11_physical_reaction": "11. ... experiencing physical reactions (e.g., sweating, rapid heartbeat) when reminded of the trauma?",
|
||||||
|
"q12_emotional_numbness": "12. ... feeling emotionally numb (e.g., feeling sad but unable to cry, unable to feel loving emotions)?",
|
||||||
|
"q13_easily_startled": "13. ... being more easily startled (e.g., when someone approaches from behind)?",
|
||||||
|
"instruction_listen_statements": "I will now read 5 statements to you. Please listen carefully and then tell me which one best applies to you:",
|
||||||
|
"q14_intro": "14. Thinking about your life in the past four weeks, to what extent do you feel that you:",
|
||||||
|
"q14_option_1": "Are able to handle everything that comes your way",
|
||||||
|
"q14_option_2": "Are able to handle most things that come your way",
|
||||||
|
"q14_option_3": "Are able to handle some things but not others",
|
||||||
|
"q14_option_4": "Are unable to handle most things",
|
||||||
|
"q14_option_5": "Are unable to handle anything",
|
||||||
|
"pain_rating_instruction": "Choose the number (0–10) that best describes how much suffering the respondent experienced in the past week, including today.",
|
||||||
|
"violence_intro": "Finally, I have 2 questions about experiences of violence:",
|
||||||
|
"violence_question_1": "Have you ever been injured by others so seriously that you needed medical attention? (doctor, hospital)",
|
||||||
|
"times_happend": "Has this ... happened?",
|
||||||
|
"age_at_incident": "At what age: at ... years?",
|
||||||
|
"times_happend2": "Has this ... happened?",
|
||||||
|
"age_at_incident2": "At what age: at ... years?",
|
||||||
|
"conflict_since_arrival": "Since arriving in Germany, have you ever been involved in violent conflicts (physical fights, physical or psychological conflicts)?",
|
||||||
|
"finish_data_entry": "When you have finished entering the data, click \"Save\".",
|
||||||
|
"feeling_connected_to_germany_question": "How connected do you feel to Germany (how well have you settled in Germany)?",
|
||||||
|
"intro_life_in_germany": "Now we would like to ask you some questions about your life in Germany:",
|
||||||
|
"not_connected_at_all": "not connected at all",
|
||||||
|
"somewhat_loose": "somewhat loose",
|
||||||
|
"moderately_connected": "moderately connected",
|
||||||
|
"quite_connected": "quite connected",
|
||||||
|
"very_connected": "very connected",
|
||||||
|
"understanding_political_issues": "How well do you understand the most important political issues in Germany?",
|
||||||
|
"very_good": "very good",
|
||||||
|
"good": "good",
|
||||||
|
"fairly_good": "fairly good",
|
||||||
|
"somewhat": "somewhat",
|
||||||
|
"not_at_all": "not at all",
|
||||||
|
"unexpected_expense_question": "Imagine you had to make an unexpected but necessary payment. Which of the following amounts could you pay or afford (if you had free access to money)?",
|
||||||
|
"expense_50": "up to €50",
|
||||||
|
"expense_100": "€100",
|
||||||
|
"expense_300": "€300",
|
||||||
|
"expense_500": "€500",
|
||||||
|
"expense_800": "€800",
|
||||||
|
"dining_with_germans_question": "How often have you dined (e.g. dinner) with Germans in the past 12 months?",
|
||||||
|
"never": "never",
|
||||||
|
"once_a_year": "once a year",
|
||||||
|
"once_a_month": "once a month",
|
||||||
|
"once_a_week": "once a week",
|
||||||
|
"almost_every_day": "almost every day",
|
||||||
|
"reading_german_articles_question": "How well can you read simple newspaper articles in German about familiar topics (health, home, family, etc.) and understand the main points?",
|
||||||
|
"not_good": "not well",
|
||||||
|
"visiting_doctor_question": "How easy or difficult would it be for you to see a doctor in this country? (making an appointment, etc. - not: 'you can't get an appointment', 'I would never go to a doctor here')",
|
||||||
|
"very_difficult": "very difficult",
|
||||||
|
"rather_difficult": "rather difficult",
|
||||||
|
"neither_nor": "neither difficult nor easy",
|
||||||
|
"rather_easy": "rather easy",
|
||||||
|
"very_easy": "very easy",
|
||||||
|
"feeling_as_outsider_question": "How often do you feel like an outsider in Germany?",
|
||||||
|
"rarely": "rarely",
|
||||||
|
"sometimes": "sometimes",
|
||||||
|
"often": "often",
|
||||||
|
"always": "always",
|
||||||
|
"recent_occupation_question": "How have you been occupied in the last four weeks? (Please select only one answer)",
|
||||||
|
"employed": "in paid employment, even if only temporarily (employed, self-employed, minor jobs)",
|
||||||
|
"education": "in education (school, vocational training, university)",
|
||||||
|
"internship": "internship",
|
||||||
|
"unemployed_searching": "unemployed and actively looking for work",
|
||||||
|
"unemployed_not_searching": "unemployed and not actively looking for work",
|
||||||
|
"sick_or_disabled": "ill or unable to work",
|
||||||
|
"unpaid_housework": "in unpaid housework, childcare / caring for others",
|
||||||
|
"other_activity": "Other (e.g., language course)",
|
||||||
|
"discussing_politics_question": "Since arriving in Germany, how often have you discussed important political topics in Germany with other people?",
|
||||||
|
"contact_with_germans_question": "With how many Germans have you exchanged messages (phone, messenger chat (WhatsApp, Facebook...), or SMS) in the last 4 weeks?",
|
||||||
|
"zero": "0",
|
||||||
|
"one_to_three": "1 to 3",
|
||||||
|
"three_to_six": "3 to 6",
|
||||||
|
"seven_to_fourteen": "7 to 14",
|
||||||
|
"fifteen_or_more": "15 or more",
|
||||||
|
"speaking_german_opinion_question": "How well can you participate in a conversation in German on familiar topics (children, family, school, home, etc.) and express your opinion?",
|
||||||
|
"moderately_good": "moderately good",
|
||||||
|
"job_search_question": "How easy or difficult would it be for you to search for and find a job in this country (even if you may not be allowed to work: what would be your assumption if there were no restrictions)?",
|
||||||
|
"integration_index": "Integration Index: %d",
|
||||||
|
"consent_followup_6_months": "Consent for conversation in 6 months",
|
||||||
|
"day": "Day",
|
||||||
|
"month": "Month",
|
||||||
|
"select_day": "Please select a day.",
|
||||||
|
"select_month": "Please select a month.",
|
||||||
|
"select_year": "Please select a year.",
|
||||||
|
"date_final_interview": "Date of final interview",
|
||||||
|
"amount_nat_appointments": "Number of NAT appointments (after the counseling interview)",
|
||||||
|
"amount_session_flowers": "Number of storytelling sessions with flowers",
|
||||||
|
"amount_session_stones": "Number of storytelling sessions with stones",
|
||||||
|
"termination_nat_coaching": "Termination of NAT coaching",
|
||||||
|
"termination_nat_regular": "Regular (mutual decision to terminate after discussing all important events)",
|
||||||
|
"termination_nat_referral": "Referral to a specialized professional (doctor, psychologist, ...)",
|
||||||
|
"termination_nat_dropout": "Client discontinued NAT",
|
||||||
|
"follow_up": "Follow-up survey",
|
||||||
|
"follow_up_completed": "was successfully conducted",
|
||||||
|
"follow_up_failed_contact": "could not be conducted: repeated failed contact attempts",
|
||||||
|
"follow_up_consent_withdrawn": "could not be conducted: consent withdrawn",
|
||||||
|
"special_burden_question": "Was there any special burden?",
|
||||||
|
"resilience_fully_capable": "Able to handle everything that comes your way",
|
||||||
|
"resilience_mostly_capable": "Able to handle most things that come your way",
|
||||||
|
"resilience_partially_capable": "Able to handle some things but not others",
|
||||||
|
"resilience_mostly_not_capable": "Unable to handle most things",
|
||||||
|
"resilience_not_capable": "Unable to handle anything",
|
||||||
|
"intro_resilience_questions": "I will now read 5 statements to you. Please listen carefully and then tell me which statement best applies to you:",
|
||||||
|
"resilience_reflection_prompt": "Thinking about your life in the past four weeks, to what extent do you feel that you:",
|
||||||
|
"resilience_some_capable_some_not": "Able to handle most things but unable to handle some others",
|
||||||
|
"follow_up_survey_score": "The RHS total score is: %d",
|
||||||
|
"select_one_answer_per_row": "Please select one answer per row!",
|
||||||
|
"no_next_question_defined": "No forwarding page defined",
|
||||||
|
"date_consultation_health_interview_result": "Date of counseling interview (health interview result green/yellow/red)",
|
||||||
|
"consultation_decision": "Counseling decision (RHS_POINTS)",
|
||||||
|
"consent_conversation_in_6_months": "Consent for conversation in 6 months:",
|
||||||
|
"participation_in_coaching": "Participation in coaching",
|
||||||
|
"decision_after_reflection_period": "Decision on .............. (date) after reflection period",
|
||||||
|
"participation_coaching": "Coaching participation",
|
||||||
|
"consent_coaching_given": "Consent for coaching is given",
|
||||||
|
"consent_form_must_be_available": "Consent form must be available!",
|
||||||
|
"professional_referral": "Professional referral",
|
||||||
|
"confidentiality_agreement": "Confidentiality agreement",
|
||||||
|
"health_insurance_card": "Health insurance card",
|
||||||
|
"green": "green",
|
||||||
|
"yellow": "yellow",
|
||||||
|
"red": "red",
|
||||||
|
"time_to_think_about_it": "Reflection period",
|
||||||
|
"consent_yes": "yes, consent is given",
|
||||||
|
"consent_no": "no, consent is not given",
|
||||||
|
"available_yes": "yes, available",
|
||||||
|
"available_no": "no, not available",
|
||||||
|
"client_canceled_NAT": "Client discontinued NAT",
|
||||||
|
"after_meeting": "after session",
|
||||||
|
"without_giving_reasons": "without giving reasons",
|
||||||
|
"with_reasons_given": "with reasons given",
|
||||||
|
"select_at_least_one_answer": "Please select at least one answer.",
|
||||||
|
"select_at_least_minimum": "Please select at least %d answers.",
|
||||||
|
"demographic_information": "Demographic information",
|
||||||
|
"rhs": "RHS",
|
||||||
|
"integration_index_label": "Integration index",
|
||||||
|
"consultation_results": "Counseling results",
|
||||||
|
"final_interview": "Final interview",
|
||||||
|
"follow_up_survey": "Follow-up survey",
|
||||||
|
"example_text": "Example text",
|
||||||
|
"client_code_exists": "This client code already exists.",
|
||||||
|
"load": "Load",
|
||||||
|
"date_after": "The date must be after",
|
||||||
|
"date_before": "The date must be before",
|
||||||
|
"lay": "it.",
|
||||||
|
"choose_more_elements": "More elements must be selected.",
|
||||||
|
"please_client_code": "Please enter client code",
|
||||||
|
"no_profile": "This client is not yet part of the database",
|
||||||
|
"questionnaires_finished": "All questionnaires have been completed for this client!",
|
||||||
|
"edit": "Edit",
|
||||||
|
"save": "Save",
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
|
"database": "Database",
|
||||||
|
"clients": "Clients",
|
||||||
|
"client": "Client",
|
||||||
|
"client_code_label": "Client Code",
|
||||||
|
"questionnaires": "Questionnaires",
|
||||||
|
"questionnaire": "Questionnaire",
|
||||||
|
"questionnaire_id": "Questionnaire ID",
|
||||||
|
"status": "Status",
|
||||||
|
"header_label": "Header",
|
||||||
|
"id": "ID",
|
||||||
|
"value": "Value",
|
||||||
|
"no_clients": "No clients available.",
|
||||||
|
"no_questionnaires": "No questionnaires available.",
|
||||||
|
"no_questions": "No questions available.",
|
||||||
|
"question": "Question",
|
||||||
|
"answer": "Answer",
|
||||||
|
"download_header": "Download header",
|
||||||
|
"back": "Back",
|
||||||
|
"export_success_downloads_headers": "Export successful: Downloads/ClientHeaders.xlsx",
|
||||||
|
"export_failed": "Export failed.",
|
||||||
|
"error_generic": "Error",
|
||||||
|
"not_done": "Not Done",
|
||||||
|
"none": "None",
|
||||||
|
"done": "Done",
|
||||||
|
"locked": "Locked",
|
||||||
|
"start": "Start",
|
||||||
|
"points": "Points",
|
||||||
|
"saved_pdf_csv": "PDF and CSV were saved in the \"Downloads\" folder.",
|
||||||
|
"no_pdf_viewer": "No PDF viewer installed.",
|
||||||
|
"save_error": "Error while saving: {message}",
|
||||||
|
"login_required_title": "Login required",
|
||||||
|
"username_hint": "Username",
|
||||||
|
"password_hint": "Password",
|
||||||
|
"login_btn": "Login",
|
||||||
|
"exit_btn": "Exit",
|
||||||
|
"please_username_password": "Please enter username and password.",
|
||||||
|
"download_failed_no_local_db": "Download failed – no local database available",
|
||||||
|
"download_failed_use_offline": "Download failed – working offline with existing database",
|
||||||
|
"login_failed_with_reason": "Login failed: {reason}",
|
||||||
|
"no_header_template_found": "No header template found",
|
||||||
|
"login_required": "Please log in first",
|
||||||
|
"session_label": "Session",
|
||||||
|
"session_dash": "Session: —",
|
||||||
|
"hours_short": "h",
|
||||||
|
"minutes_short": "min",
|
||||||
|
"online": "Online",
|
||||||
|
"offline": "Offline",
|
||||||
|
"open_client_via_load": "Please open the client via \"Load\".",
|
||||||
|
"database_clients_title": "Database – Clients",
|
||||||
|
"no_clients_available": "No clients available.",
|
||||||
|
"headers": "Headers",
|
||||||
|
"no_questions_available": "No questions available.",
|
||||||
|
"view_missing": "Missing view: %s"
|
||||||
|
}
|
||||||
BIN
uploadDelta.log
Executable file
BIN
uploadDelta.log
Executable file
Binary file not shown.
466
uploadDeltaTest5.php
Normal file
466
uploadDeltaTest5.php
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/uploadDeltaTest5.php
|
||||||
|
// Vollständige, eigenständige Version – einfach kopieren & einfügen.
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Logging & PHP-Settings
|
||||||
|
// -----------------------------
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
ini_set('log_errors', '1');
|
||||||
|
$LOG_FILE = __DIR__ . '/uploadDelta.log';
|
||||||
|
function log_msg($msg) {
|
||||||
|
global $LOG_FILE;
|
||||||
|
error_log(date('[Y-m-d H:i:s] ') . $msg . PHP_EOL, 3, $LOG_FILE);
|
||||||
|
}
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Krypto-/Token-Helfer
|
||||||
|
// -----------------------------
|
||||||
|
function b64_or_ascii_to_32_bytes(string $s): string {
|
||||||
|
// Versuche Base64, sonst ASCII; pad/truncate auf 32
|
||||||
|
$b = base64_decode($s, true);
|
||||||
|
if ($b !== false && $b !== '') {
|
||||||
|
return str_pad(substr($b, 0, 32), 32, "\0");
|
||||||
|
}
|
||||||
|
return str_pad(substr($s, 0, 32), 32, "\0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_master_key_bytes(): string {
|
||||||
|
// MASTER-KEY nur serverseitig (für gespeicherte DB)
|
||||||
|
// ENV: QDB_MASTER_KEY (Base64 oder ASCII). Fallback ist alter Key.
|
||||||
|
$env = getenv('QDB_MASTER_KEY');
|
||||||
|
if ($env && $env !== '') return b64_or_ascii_to_32_bytes($env);
|
||||||
|
return "12345678901234567890123456789012"; // Fallback (bitte in PROD durch ENV ersetzen!)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hkdf_session_key_from_token(string $tokenHex, string $info = 'qdb-aes', int $len = 32): string {
|
||||||
|
// HKDF-SHA256: salt = 32x0x00, info = "qdb-aes"
|
||||||
|
$ikm = @hex2bin(trim($tokenHex));
|
||||||
|
if ($ikm === false) $ikm = $tokenHex; // falls bereits binär
|
||||||
|
$hashLen = 32;
|
||||||
|
$salt = str_repeat("\0", $hashLen);
|
||||||
|
$prk = hash_hmac('sha256', $ikm, $salt, true);
|
||||||
|
$okm = '';
|
||||||
|
$t = '';
|
||||||
|
$n = (int)ceil($len / $hashLen);
|
||||||
|
for ($i = 1; $i <= $n; $i++) {
|
||||||
|
$t = hash_hmac('sha256', $t . $info . chr($i), $prk, true);
|
||||||
|
$okm .= $t;
|
||||||
|
}
|
||||||
|
return substr($okm, 0, $len);
|
||||||
|
}
|
||||||
|
|
||||||
|
function aes256_cbc_encrypt_bytes(string $plain, string $key32): string {
|
||||||
|
$key = str_pad(substr($key32, 0, 32), 32, "\0");
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$cipher = openssl_encrypt($plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($cipher === false) throw new Exception('openssl_encrypt failed');
|
||||||
|
return $iv . $cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aes256_cbc_decrypt_bytes(string $data, string $key32): string {
|
||||||
|
if (strlen($data) < 16) throw new Exception('cipher too short');
|
||||||
|
$key = str_pad(substr($key32, 0, 32), 32, "\0");
|
||||||
|
$iv = substr($data, 0, 16);
|
||||||
|
$ct = substr($data, 16);
|
||||||
|
$plain = openssl_decrypt($ct, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
if ($plain === false) throw new Exception('openssl_decrypt failed');
|
||||||
|
return $plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token-Validierung: bevorzugt tokens.jsonl mit exp, Fallback valid_tokens.txt
|
||||||
|
function token_is_valid(string $token): bool {
|
||||||
|
$now = time();
|
||||||
|
$jsonl = __DIR__ . '/tokens.jsonl';
|
||||||
|
if (file_exists($jsonl)) {
|
||||||
|
$h = fopen($jsonl, 'r');
|
||||||
|
if ($h) {
|
||||||
|
while (($line = fgets($h)) !== false) {
|
||||||
|
$j = json_decode($line, true);
|
||||||
|
if (!is_array($j)) continue;
|
||||||
|
if (($j['token'] ?? '') === $token) {
|
||||||
|
$exp = $j['exp'] ?? ($now + 1); // falls kein exp, akzeptieren
|
||||||
|
fclose($h);
|
||||||
|
return $exp >= $now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$txt = __DIR__ . '/valid_tokens.txt';
|
||||||
|
if (file_exists($txt)) {
|
||||||
|
$arr = file($txt, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if ($arr && in_array($token, $arr, true)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Hauptlogik
|
||||||
|
// -----------------------------
|
||||||
|
try {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(["error" => "Nur POST erlaubt"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token aus multipart oder Authorization: Bearer
|
||||||
|
$token = $_POST['token'] ?? null;
|
||||||
|
if (!$token) {
|
||||||
|
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
|
||||||
|
if (stripos($auth, 'Bearer ') === 0) {
|
||||||
|
$token = substr($auth, 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$token || !token_is_valid($token)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(["error" => "Ungültiger Token"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload entgegennehmen (multipart 'file' oder raw body)
|
||||||
|
$uploadedFilePath = null;
|
||||||
|
if (!empty($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
|
||||||
|
$uploadedFilePath = $_FILES['file']['tmp_name'];
|
||||||
|
} else {
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
if ($raw !== false && strlen($raw) > 0) {
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'upl_');
|
||||||
|
file_put_contents($tmp, $raw);
|
||||||
|
$uploadedFilePath = $tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$uploadedFilePath) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["error" => "Keine Datei gesendet"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload mit SESSION-Key (aus Token via HKDF) entschlüsseln
|
||||||
|
$encData = file_get_contents($uploadedFilePath);
|
||||||
|
if ($encData === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(["error" => "Fehler beim Lesen der hochgeladenen Datei"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$sessionKey = hkdf_session_key_from_token($token);
|
||||||
|
$jsonData = null;
|
||||||
|
try {
|
||||||
|
$jsonData = aes256_cbc_decrypt_bytes($encData, $sessionKey);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Fallback: akzeptiere Plain-JSON (Kompatibilität)
|
||||||
|
$maybeJson = trim($encData);
|
||||||
|
if ($maybeJson === '' || json_decode($maybeJson, true) === null) {
|
||||||
|
log_msg("Entschlüsselung fehlgeschlagen und kein valides JSON. raw len=" . strlen($encData));
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["error" => "Ungültige verschlüsselte Datei oder kein JSON"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$jsonData = $maybeJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($jsonData, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["error" => "Ungültiges JSON"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ziel: MASTER-verschlüsselte DB speichern
|
||||||
|
$dbPath = __DIR__ . '/uploads/questionnaire_database';
|
||||||
|
if (!is_dir(dirname($dbPath))) {
|
||||||
|
if (!mkdir(dirname($dbPath), 0755, true) && !is_dir(dirname($dbPath))) {
|
||||||
|
throw new Exception("Konnte Upload-Ordner nicht erstellen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock, damit keine parallelen Writes kollidieren
|
||||||
|
$lockFile = __DIR__ . '/uploads/.qdb_lock';
|
||||||
|
$lockFp = fopen($lockFile, 'c');
|
||||||
|
if ($lockFp === false) throw new Exception("Konnte Lock-Datei nicht öffnen");
|
||||||
|
if (!flock($lockFp, LOCK_EX)) { fclose($lockFp); throw new Exception("Konnte Lock nicht setzen"); }
|
||||||
|
|
||||||
|
$tmpDb = null;
|
||||||
|
$tmpEncrypted = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tmpDb = tempnam(sys_get_temp_dir(), 'qdb_');
|
||||||
|
$masterKey = get_master_key_bytes();
|
||||||
|
|
||||||
|
// Bestehende MASTER-verschlüsselte DB entschlüsseln oder neue DB mit Schema anlegen
|
||||||
|
if (file_exists($dbPath) && is_file($dbPath)) {
|
||||||
|
$storedEnc = @file_get_contents($dbPath);
|
||||||
|
if ($storedEnc === false) throw new Exception("Konnte gespeicherte DB nicht lesen");
|
||||||
|
$decrypted = aes256_cbc_decrypt_bytes($storedEnc, $masterKey);
|
||||||
|
if (file_put_contents($tmpDb, $decrypted) === false) throw new Exception("Konnte temporäre DB nicht schreiben");
|
||||||
|
} else {
|
||||||
|
$pdo = new PDO("sqlite:$tmpDb");
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$pdo->exec("PRAGMA foreign_keys = ON;");
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS clients (
|
||||||
|
clientCode TEXT NOT NULL DEFAULT 'undefined' PRIMARY KEY
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS questionnaires (
|
||||||
|
id TEXT NOT NULL DEFAULT 'undefined' PRIMARY KEY
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS questions (
|
||||||
|
questionId TEXT NOT NULL DEFAULT 'undefined' PRIMARY KEY,
|
||||||
|
questionnaireId TEXT NOT NULL,
|
||||||
|
question TEXT NOT NULL DEFAULT '',
|
||||||
|
FOREIGN KEY(questionnaireId) REFERENCES questionnaires(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS answers (
|
||||||
|
clientCode TEXT NOT NULL,
|
||||||
|
questionId TEXT NOT NULL,
|
||||||
|
answerValue TEXT NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (clientCode, questionId),
|
||||||
|
FOREIGN KEY(clientCode) REFERENCES clients(clientCode) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(questionId) REFERENCES questions(questionId) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS completed_questionnaires (
|
||||||
|
clientCode TEXT NOT NULL,
|
||||||
|
questionnaireId TEXT NOT NULL,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
isDone INTEGER NOT NULL,
|
||||||
|
sumPoints INTEGER,
|
||||||
|
PRIMARY KEY (clientCode, questionnaireId),
|
||||||
|
FOREIGN KEY(clientCode) REFERENCES clients(clientCode) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(questionnaireId) REFERENCES questionnaires(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
");
|
||||||
|
$pdo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einspielen der Daten
|
||||||
|
$db = new PDO("sqlite:$tmpDb");
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$db->exec("PRAGMA foreign_keys = ON;");
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// --- Sammeln, welche IDs/Paarungen betroffen sind ---
|
||||||
|
$questionnaireIds = []; // set[string] questionnaireId => true
|
||||||
|
$clientQuestionPairs = []; // set["client|qid"] => true
|
||||||
|
|
||||||
|
if (!empty($data['questionnaires'])) {
|
||||||
|
foreach ($data['questionnaires'] as $q) {
|
||||||
|
if (isset($q['id']) && $q['id'] !== null && $q['id'] !== '') {
|
||||||
|
$questionnaireIds[$q['id']] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($data['questions'])) {
|
||||||
|
foreach ($data['questions'] as $q) {
|
||||||
|
if (isset($q['questionnaireId']) && $q['questionnaireId'] !== null && $q['questionnaireId'] !== '') {
|
||||||
|
$questionnaireIds[$q['questionnaireId']] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($data['completed_questionnaires'])) {
|
||||||
|
foreach ($data['completed_questionnaires'] as $c) {
|
||||||
|
if (isset($c['questionnaireId']) && $c['questionnaireId'] !== null && $c['questionnaireId'] !== '') {
|
||||||
|
$questionnaireIds[$c['questionnaireId']] = true;
|
||||||
|
}
|
||||||
|
if (isset($c['clientCode']) && isset($c['questionnaireId'])) {
|
||||||
|
$client = ($c['clientCode'] === null || $c['clientCode'] === '') ? 'undefined' : $c['clientCode'];
|
||||||
|
$qid = ($c['questionnaireId'] === null || $c['questionnaireId'] === '') ? 'undefined' : $c['questionnaireId'];
|
||||||
|
$clientQuestionPairs["$client|$qid"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($data['answers'])) {
|
||||||
|
foreach ($data['answers'] as $a) {
|
||||||
|
if (isset($a['clientCode'])) {
|
||||||
|
$client = ($a['clientCode'] === null || $a['clientCode'] === '') ? 'undefined' : $a['clientCode'];
|
||||||
|
$qid = null;
|
||||||
|
if (isset($a['questionId']) && is_string($a['questionId'])) {
|
||||||
|
$parts = explode('-', $a['questionId'], 2);
|
||||||
|
if (count($parts) >= 1) {
|
||||||
|
$potential = $parts[0];
|
||||||
|
if ($potential !== '') $qid = $potential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($qid !== null) {
|
||||||
|
$questionnaireIds[$qid] = true;
|
||||||
|
$clientQuestionPairs["$client|$qid"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($data['clients']) && !empty($data['questionnaires'])) {
|
||||||
|
foreach ($data['clients'] as $c) {
|
||||||
|
if (!isset($c['clientCode'])) continue;
|
||||||
|
$client = ($c['clientCode'] === null || $c['clientCode'] === '') ? 'undefined' : $c['clientCode'];
|
||||||
|
foreach ($data['questionnaires'] as $q) {
|
||||||
|
if (!isset($q['id'])) continue;
|
||||||
|
$qid = ($q['id'] === null || $q['id'] === '') ? 'undefined' : $q['id'];
|
||||||
|
$clientQuestionPairs["$client|$qid"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Löschungen (zuerst abhängige Tabellen) ---
|
||||||
|
$delAnswersByPairStmt = $db->prepare("
|
||||||
|
DELETE FROM answers
|
||||||
|
WHERE clientCode = :clientCode
|
||||||
|
AND questionId IN (SELECT questionId FROM questions WHERE questionnaireId = :questionnaireId)
|
||||||
|
");
|
||||||
|
$delCompletedByPairStmt = $db->prepare("
|
||||||
|
DELETE FROM completed_questionnaires
|
||||||
|
WHERE clientCode = :clientCode
|
||||||
|
AND questionnaireId = :questionnaireId
|
||||||
|
");
|
||||||
|
foreach ($clientQuestionPairs as $pair => $_) {
|
||||||
|
list($clientCode, $qid) = explode('|', $pair, 2);
|
||||||
|
try {
|
||||||
|
$delAnswersByPairStmt->execute([
|
||||||
|
':clientCode' => $clientCode,
|
||||||
|
':questionnaireId' => $qid
|
||||||
|
]);
|
||||||
|
$delCompletedByPairStmt->execute([
|
||||||
|
':clientCode' => $clientCode,
|
||||||
|
':questionnaireId' => $qid
|
||||||
|
]);
|
||||||
|
log_msg("Deleted answers & completed for client='$clientCode' questionnaire='$qid'");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
log_msg("Delete error for pair $pair: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($questionnaireIds)) {
|
||||||
|
$delQuestionsStmt = $db->prepare("DELETE FROM questions WHERE questionnaireId = :questionnaireId");
|
||||||
|
foreach ($questionnaireIds as $qid => $_) {
|
||||||
|
try {
|
||||||
|
$delQuestionsStmt->execute([':questionnaireId' => $qid]);
|
||||||
|
log_msg("Deleted questions for questionnaireId='$qid'");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
log_msg("Delete questions error for $qid: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Inserts/Upserts ---
|
||||||
|
if (!empty($data['clients'])) {
|
||||||
|
$stmt = $db->prepare("INSERT OR IGNORE INTO clients (clientCode) VALUES (:clientCode)");
|
||||||
|
foreach ($data['clients'] as $client) {
|
||||||
|
if (!isset($client['clientCode'])) continue;
|
||||||
|
$val = $client['clientCode'] === null || $client['clientCode'] === '' ? 'undefined' : $client['clientCode'];
|
||||||
|
$stmt->execute([':clientCode' => $val]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['questionnaires'])) {
|
||||||
|
$stmt = $db->prepare("INSERT OR IGNORE INTO questionnaires (id) VALUES (:id)");
|
||||||
|
foreach ($data['questionnaires'] as $q) {
|
||||||
|
if (!isset($q['id'])) continue;
|
||||||
|
$val = $q['id'] === null || $q['id'] === '' ? 'undefined' : $q['id'];
|
||||||
|
$stmt->execute([':id' => $val]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['questions'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT OR IGNORE INTO questions (questionId, questionnaireId, question)
|
||||||
|
VALUES (:questionId, :questionnaireId, :question)
|
||||||
|
");
|
||||||
|
foreach ($data['questions'] as $q) {
|
||||||
|
if (!isset($q['questionId'])) continue;
|
||||||
|
$questionId = $q['questionId'] === null || $q['questionId'] === '' ? 'undefined' : $q['questionId'];
|
||||||
|
$questionnaireId = $q['questionnaireId'] === null || $q['questionnaireId'] === '' ? 'undefined' : $q['questionnaireId'];
|
||||||
|
$questionText = $q['question'] ?? '';
|
||||||
|
$stmt->execute([
|
||||||
|
':questionId' => $questionId,
|
||||||
|
':questionnaireId' => $questionnaireId,
|
||||||
|
':question' => $questionText
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['answers'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT OR IGNORE INTO answers (clientCode, questionId, answerValue)
|
||||||
|
VALUES (:clientCode, :questionId, :answerValue)
|
||||||
|
");
|
||||||
|
foreach ($data['answers'] as $a) {
|
||||||
|
if (!isset($a['clientCode']) || !isset($a['questionId'])) continue;
|
||||||
|
$clientCode = $a['clientCode'] === null || $a['clientCode'] === '' ? 'undefined' : $a['clientCode'];
|
||||||
|
$questionId = $a['questionId'] === null || $a['questionId'] === '' ? 'undefined' : $a['questionId'];
|
||||||
|
$answerValue = array_key_exists('answerValue', $a) && $a['answerValue'] !== null ? $a['answerValue'] : '';
|
||||||
|
$stmt->execute([
|
||||||
|
':clientCode' => $clientCode,
|
||||||
|
':questionId' => $questionId,
|
||||||
|
':answerValue' => $answerValue
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['completed_questionnaires'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT OR REPLACE INTO completed_questionnaires (clientCode, questionnaireId, timestamp, isDone, sumPoints)
|
||||||
|
VALUES (:clientCode, :questionnaireId, :timestamp, :isDone, :sumPoints)
|
||||||
|
");
|
||||||
|
foreach ($data['completed_questionnaires'] as $c) {
|
||||||
|
if (!isset($c['clientCode']) || !isset($c['questionnaireId'])) continue;
|
||||||
|
$clientCode = $c['clientCode'] === null || $c['clientCode'] === '' ? 'undefined' : $c['clientCode'];
|
||||||
|
$questionnaireId = $c['questionnaireId'] === null || $c['questionnaireId'] === '' ? 'undefined' : $c['questionnaireId'];
|
||||||
|
$timestamp = isset($c['timestamp']) ? (int)$c['timestamp'] : time();
|
||||||
|
$isDone = !empty($c['isDone']) ? 1 : 0;
|
||||||
|
$sumPoints = array_key_exists('sumPoints', $c) ? ($c['sumPoints'] === null ? null : (int)$c['sumPoints']) : null;
|
||||||
|
$stmt->execute([
|
||||||
|
':clientCode' => $clientCode,
|
||||||
|
':questionnaireId' => $questionnaireId,
|
||||||
|
':timestamp' => $timestamp,
|
||||||
|
':isDone' => $isDone,
|
||||||
|
':sumPoints' => $sumPoints
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
$db = null;
|
||||||
|
|
||||||
|
// MASTER-verschlüsselt speichern (atomar via temp + rename)
|
||||||
|
$plainDb = file_get_contents($tmpDb);
|
||||||
|
if ($plainDb === false) throw new Exception("Konnte tmp DB nicht lesen");
|
||||||
|
$enc = aes256_cbc_encrypt_bytes($plainDb, $masterKey);
|
||||||
|
|
||||||
|
$tmpEncrypted = tempnam(dirname($dbPath), 'enc_qdb_');
|
||||||
|
if (file_put_contents($tmpEncrypted, $enc) === false) throw new Exception("Konnte verschlüsselte DB nicht schreiben");
|
||||||
|
if (!@rename($tmpEncrypted, $dbPath)) {
|
||||||
|
if (!@copy($tmpEncrypted, $dbPath) || !@unlink($tmpEncrypted)) {
|
||||||
|
throw new Exception("Konnte verschlüsselte DB nicht speichern (rename/copy fehlgeschlagen)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@chmod($dbPath, 0644);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
@unlink($uploadedFilePath);
|
||||||
|
@unlink($tmpDb);
|
||||||
|
flock($lockFp, LOCK_UN);
|
||||||
|
fclose($lockFp);
|
||||||
|
|
||||||
|
echo json_encode(["success" => true, "message" => "Delta erfolgreich eingespielt und DB gespeichert"]);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Throwable $inner) {
|
||||||
|
// Cleanup bei Fehler
|
||||||
|
@flock($lockFp, LOCK_UN);
|
||||||
|
@fclose($lockFp);
|
||||||
|
@unlink($tmpDb ?? '');
|
||||||
|
@unlink($tmpEncrypted ?? '');
|
||||||
|
@unlink($uploadedFilePath ?? '');
|
||||||
|
log_msg("Inner exception: " . $inner->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(["error" => "Fehler beim Speichern", "message" => $inner->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
log_msg("Top-level exception: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(["error" => "Fehler", "message" => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
0
uploads/.qdb_lock
Normal file
0
uploads/.qdb_lock
Normal file
BIN
uploads/questionnaire_database
Normal file
BIN
uploads/questionnaire_database
Normal file
Binary file not shown.
50
users.json
Normal file
50
users.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"user01": {
|
||||||
|
"hash": "$2y$10$fcB5mBc.RDeCHyY69RScZOpFL/IjY.wFzx1DNzKI7F6kJhd9rwQfu",
|
||||||
|
"changedAt": 1760610683
|
||||||
|
},
|
||||||
|
"user02": {
|
||||||
|
"hash": "$2y$10$fGDcFLBgL3oypyjkwVna/OhQ5Be6UFH6hNGdupeNfJMASgrFK7h0K",
|
||||||
|
"changedAt": 1760611508
|
||||||
|
},
|
||||||
|
"Tmp_Daniel": {
|
||||||
|
"hash": "$2y$10$NRdP4/vkDQEM4fD70xGqdO1dgujOBATcA00Zj2aIaH0hoQV5Rp8ri",
|
||||||
|
"changedAt": 1760612940
|
||||||
|
},
|
||||||
|
"Tmp_Liliana": {
|
||||||
|
"hash": "$2y$10$4Gui323PXRnQ9Ix4WSrefOEr2HQwfbr6TNVaa7qVE2q59ZwBpnisa",
|
||||||
|
"changedAt": 1760971357
|
||||||
|
},
|
||||||
|
"Tmp_Johanna": {
|
||||||
|
"hash": "$2y$10$B4eNyer1anHYMNjpNliIW.54sK8z1cYyQEHjmHloatUnmeilchjva",
|
||||||
|
"changedAt": 1760972135
|
||||||
|
},
|
||||||
|
"Danny": {
|
||||||
|
"hash": "$2y$10$JreuWqvh1VO3fH2YhSpqveDpSSgVPUeNW91ySSzxeQ0o/OgNe1gGS",
|
||||||
|
"changedAt": 1771865346
|
||||||
|
},
|
||||||
|
"Extra1_tmp": {
|
||||||
|
"hash": "$2y$10$zUtdsv1N2Uv/7XZT4wr4lOTnJEWFF74EJp2/kgq6GHpO8dguMQify",
|
||||||
|
"changedAt": 1771865748
|
||||||
|
},
|
||||||
|
"User11": {
|
||||||
|
"hash": "$2y$10$ql0aI9C.1nzkE33Oxy8T3.dE0jF0y42fe4x8YWyqFm.SwlUBoqv22",
|
||||||
|
"changedAt": 1772001151
|
||||||
|
},
|
||||||
|
"User10": {
|
||||||
|
"hash": "$2y$10$5HoG6T6UA9CFd2q1VGLiE.3EbwNyyVR8kSuTZWxJ5run8KvHgrhhK",
|
||||||
|
"changedAt": 1772110997
|
||||||
|
},
|
||||||
|
"User2": {
|
||||||
|
"hash": "$2y$10$/KxLlL9ZdHnGYL1y4IEIfOnWL.bd2HgPVfk.n5V0Sc.3LzDwmeWl6",
|
||||||
|
"changedAt": 1772111983
|
||||||
|
},
|
||||||
|
"User3": {
|
||||||
|
"hash": "$2y$10$GMtUWbW.bczjc43dpf539OE7rFgwL08QV1qnjFlyDh5mF/Ss4atom",
|
||||||
|
"changedAt": 1772112290
|
||||||
|
},
|
||||||
|
"User7": {
|
||||||
|
"hash": "$2y$10$AfJp8go8Vt5J0.46SqYmtOPrrp1CqkWw1pmkOGI6xZ5MgqHTmwIga",
|
||||||
|
"changedAt": 1772283082
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
valid_tokens.txt
Normal file
BIN
valid_tokens.txt
Normal file
Binary file not shown.
Reference in New Issue
Block a user