Dataform Block

Anwendungen für Webseiten. Künstliche Intelligenz verwenden.
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Dataform Block

Ungelesener Beitrag von Volker »

Huhu :D

Also hier mein neuster Block:



Testseite: https://www.niederastroth.de/dataform/

STRG&Doppelklick - Passwort: volker04
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Interessant :tu:

... ich gucke mir das morgen mal genauer an.
Benutzeravatar
Bushisan
Mitglied (Level 10)
Mitglied (Level 10)
Beiträge: 389
Registriert: So 13. Dez 2020, 21:23
Wohnort: Neukirchen bei Sulzbach-Rosenberg
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Bushisan »

Geile Geschichte :tu: :tu: :tu: :tu:
Grüße aus der schönen Oberpfalz
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Ist noch ein kleiner Bug drin ;)

Pagenierung auf Seite 2 wird nicht angezeigt. Melde mich wenn das gefixt ist ;)
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

So,

Pagenierung ist nun OK und noch ein paar aufhübsch Dinge eingebaut ;)
Ist jetzt im Download zu finden.

Ich warte auf Fehlermeldung dann von Tommy :D

Ich find es schick ;)

https://www.niederastroth.de/dataform/

PS: Und ja ich hab bewusst auf X und Pfeile in der Lightbox verzichtet. Man soll nur schnell das Bild in Groß sehen können und Klick dann ist wieder zu ;) Also kein BUG :cool:

PPS: Ich hatte vergessen zu erwähnen das man die Cards auch frei in der Position ändern kann. Nach logout wird automatisch gespeichert. Inn der Ansichz kann man verschieben :
cardposition.png
Alles Läuft nun ( hoffe ich ) Tommy findet bestimmt wieder was :D
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Moin,

Ich habe mir das angesehen und werde diese Anwendung diesmal überspringen, da ich kaum Einsatzmöglichkeiten sehe — obwohl die Idee, individuelle Einträge hinzuzufügen (oder eben nicht), prima ist, sodass jede Karte ihre eigene Ansicht erhält, sich jedoch gleichzeitig immer an die Höhe der anderen Karten anpasst. An alles gedacht :)

Die Technik ähnelt außerdem stark der der VN‑Tabelle, die ich ja gerade erst eingebaut habe und sehr intensiv getestet habe, was ich nun nicht mit jeder dieser Anwendung machen will oder kann, denn das macht ja auch einen Haufen Arbeit.

Ich persönlich würde solche Daten ohnehin niemals online eingeben, sondern immer in Ruhe am Computer. Das ist hier aber unerheblich.

Vielleicht sehe ich sogar mehr Einsatzmöglichkeiten als manch andere, allerdings nutzt kaum jemand mal diese Online‑Anwendungen. Jahrelang wurden CMS oder ähnliche Lösungen gefordert; inzwischen gibt es viele vollwertige CMS und auch Mini‑CMS, etwa in Form solcher Tabellen. Ich glaube jedoch, dass das Interesse daran sehr begrenzt ist.

Ich habe CMS schon vor 20 Jahren zusammen mit Werner Zenk in NOF eingebaut und niemand hat sich dafür interessiert. Inzwischen habe ich sicherlich 20 - 30 vollwertige CMS verwendet, die alle niemanden interessiert haben. Einzig der Event-Kalender vom Werner war mit ca. 5.000 Downloads ein riesen Erfolg, der dann auch oft kopiert wurde.

Ich denke, wir sollten uns wieder stärker auf ganz normale Offline‑Blöcke konzentrieren, denn die meisten Nutzer stellen ihre Webseiten offline zusammen und nicht erst am Server. Etwas abweichende Blöcke — insbesondere solche ohne PHP‑Komponenten, die sich direkt aus der VN‑Block‑Galerie in Mobirise einfügen lassen, ohne als Erweiterung installiert zu werden — halte ich für interessanter für die Mehrheit der Nutzer, vorausgesetzt, sie unterscheiden sich tatsächlich von den üblichen Mobirise‑Blöcken, was angesichts der Vielzahl vorhandener Blöcke zunehmend schwierig wird.

Solche „Karten“ gibt es bei Mobirise bereits in Hülle und Fülle — etwa 150 Card-Blöcke in unterschiedlichen Ausführungen, teilweise auch als Slider. Hier mal nur ein Beispiel, das im Grunde denselben Zweck erfüllt:

https://www.mobirise-tutorials.com/Tuto ... tbox-Block

Ich sehe das so: Kunden, die eine Webseite in Auftrag geben, tun das meist ungern — ihnen fehlt die Zeit und/oder die Lust, sich einzuarbeiten. Deshalb lassen sie Änderungen vom Webdesigner vornehmen oder sollten zumindest erkennen, dass das der richtige Weg ist. Andere Kunden kennen sich genügend aus, um kleinere Änderungen selbst vorzunehmen; das ist in jedem Fall einfacher, als sich mit komplexen Systemen wie WordPress zu beschäftigen.

Wieder andere Kunden - typische - lassen die Webseite vom Profi möglichst kostenlos erstellen, möchten dann aber ohne Software und ohne Kosten jede Änderung auch selbst machen können.

Wenn ich Webdesigner wäre, würde ich all das ablehnen. Kunden, für die ich eine Webseite erstelle, würde ich auch deren Pflege übernehmen — entweder nach Bedarf gegen Stundenlohn oder gegen eine jährliche Pauschale.


Dennoch - tolle Anwendung mit reichlich Potenzial :tu:
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Habe da doch noch eine Idee, was ich damit angfangen könnte und probiere das mal ...
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Moin,

also - ich komme schon mal gleich nicht weiter, weil der Pfad zum Bild falsch ist.

Ein hochgeladenes Bild erhält diesen falschen Pfad:

Code: Alles auswählen

https://www.mobirise-tutorials.com/homepages/12/d22935777/htdocs/Mobirise-Tutorials/
VN-Block-Galerie/assets/vn-dataform/uploads/form-1/img_6a5b20516bce7.png

Richtig wäre aber dieser Pfad:

Code: Alles auswählen

https://www.mobirise-tutorials.com/VN-Block-Galerie/assets/vn-dataform/
uploads/form-1/img_6a5b20516bce7.png
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Hier ist die korrigierte api.php - bitte testen:

Code: Alles auswählen

<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');
date_default_timezone_set('Europe/Berlin');

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

/*
 * Datenverzeichnis
 */
$dataDir = __DIR__ . '/data';

if (!is_dir($dataDir)) {
    mkdir($dataDir, 0755, true);
}

file_put_contents(
    $dataDir . '/.htaccess',
    "Deny from all\n",
    LOCK_EX
);

/*
 * Galerie-/Formular-ID bereinigen
 */
$gid = preg_replace(
    '/[^a-z0-9_-]/i',
    '',
    $_GET['gid'] ?? 'default'
);

if ($gid === '') {
    $gid = 'default';
}

$dbFile = $dataDir . '/' . $gid . '.sqlite';
$pwFile = $dataDir . '/' . $gid . '.hash';

/*
 * JSON-Antwort ausgeben
 */
function respond(bool $ok, array $extra = []): void
{
    echo json_encode(
        array_merge(['success' => $ok], $extra),
        JSON_UNESCAPED_UNICODE
    );
    exit;
}

/*
 * Adminstatus prüfen
 */
function isAdmin(string $gid): bool
{
    return !empty($_SESSION['vndf_admin_' . $gid]);
}

/*
 * Öffentlichen URL-Pfad des aktuellen PHP-Verzeichnisses ermitteln.
 *
 * Beispiel:
 * SCRIPT_NAME:
 * /VN-Block-Galerie/assets/vn-dataform/dataform.php
 *
 * Ergebnis:
 * /VN-Block-Galerie/assets/vn-dataform
 */
function getPublicScriptDirectory(): string
{
    $scriptName = $_SERVER['SCRIPT_NAME']
        ?? $_SERVER['PHP_SELF']
        ?? '';

    $scriptName = str_replace('\\', '/', $scriptName);

    /*
     * Querystrings vorsorglich entfernen.
     */
    $scriptPath = parse_url($scriptName, PHP_URL_PATH);

    if (!is_string($scriptPath) || $scriptPath === '') {
        return '';
    }

    $directory = dirname($scriptPath);
    $directory = str_replace('\\', '/', $directory);

    /*
     * dirname('/') kann "/" ergeben.
     */
    if ($directory === '/' || $directory === '.') {
        return '';
    }

    return '/' . trim($directory, '/');
}

/*
 * Datenbank initialisieren
 */
$db = new PDO('sqlite:' . $dbFile);

$db->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION
);

$db->exec(
    "CREATE TABLE IF NOT EXISTS vndf_fields (
        id INTEGER PRIMARY KEY,
        sort_order INTEGER DEFAULT 0,
        field_key TEXT UNIQUE,
        label TEXT,
        type TEXT DEFAULT 'text',
        required INTEGER DEFAULT 0,
        searchable INTEGER DEFAULT 1,
        show_in_list INTEGER DEFAULT 1
    )"
);

$db->exec(
    "CREATE TABLE IF NOT EXISTS vndf_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        sort_order INTEGER DEFAULT 0,
        created_at TEXT DEFAULT (datetime('now','localtime')),
        updated_at TEXT DEFAULT (datetime('now','localtime')),
        data TEXT
    )"
);

/*
 * sort_order-Spalte bei älteren Datenbanken ergänzen.
 */
try {
    $db->exec(
        "ALTER TABLE vndf_records
         ADD COLUMN sort_order INTEGER DEFAULT 0"
    );
} catch (Exception $e) {
    /*
     * Die Spalte existiert bereits.
     */
}

/*
 * JSON-Eingabe lesen
 */
$rawInput = file_get_contents('php://input');
$input    = json_decode($rawInput ?: '', true);

if (!is_array($input)) {
    $input = [];
}

$action = $_GET['action']
    ?? $input['action']
    ?? '';

/*
 * ─────────────────────────────────────────────
 * Authentifizierung
 * ─────────────────────────────────────────────
 */

if ($action === 'check') {
    respond(
        true,
        [
            'loggedIn' => isAdmin($gid),
            'firstRun' => !file_exists($pwFile)
        ]
    );
}

if ($action === 'login') {
    $pw = (string)($input['password'] ?? '');

    /*
     * Beim ersten Aufruf Passwort anlegen.
     */
    if (!file_exists($pwFile)) {
        if (strlen($pw) < 4) {
            respond(
                false,
                ['error' => 'Min. 4 Zeichen.']
            );
        }

        $passwordHash = password_hash(
            $pw,
            PASSWORD_BCRYPT
        );

        if ($passwordHash === false) {
            respond(
                false,
                ['error' => 'Passwort konnte nicht gespeichert werden.']
            );
        }

        if (
            file_put_contents(
                $pwFile,
                $passwordHash,
                LOCK_EX
            ) === false
        ) {
            respond(
                false,
                ['error' => 'Passwortdatei konnte nicht geschrieben werden.']
            );
        }

        $_SESSION['vndf_admin_' . $gid] = true;

        respond(
            true,
            ['firstRun' => true]
        );
    }

    $storedHash = trim(
        (string)file_get_contents($pwFile)
    );

    if (
        $storedHash !== ''
        && password_verify($pw, $storedHash)
    ) {
        $_SESSION['vndf_admin_' . $gid] = true;
        respond(true);
    }

    respond(
        false,
        ['error' => 'Falsches Passwort.']
    );
}

if ($action === 'logout') {
    unset($_SESSION['vndf_admin_' . $gid]);
    respond(true);
}

/*
 * ─────────────────────────────────────────────
 * Felder
 * ─────────────────────────────────────────────
 */

if ($action === 'get_fields') {
    $rows = $db
        ->query(
            "SELECT *
             FROM vndf_fields
             ORDER BY sort_order, id"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    respond(
        true,
        ['fields' => $rows]
    );
}

if ($action === 'save_fields') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $fields = $input['fields'] ?? [];

    if (!is_array($fields)) {
        $fields = [];
    }

    $db->beginTransaction();

    try {
        $db->exec(
            "DELETE FROM vndf_fields"
        );

        $stmt = $db->prepare(
            "INSERT INTO vndf_fields (
                sort_order,
                field_key,
                label,
                type,
                required,
                searchable,
                show_in_list
            ) VALUES (?, ?, ?, ?, ?, ?, ?)"
        );

        foreach ($fields as $i => $f) {
            if (!is_array($f)) {
                continue;
            }

            $label = trim(
                (string)($f['label'] ?? '')
            );

            if ($label === '') {
                $label = 'Feld ' . ($i + 1);
            }

            $key = preg_replace(
                '/[^a-z0-9_]/i',
                '_',
                strtolower($label)
            );

            if ($key === null || $key === '') {
                $key = 'feld_' . $i;
            }

            $stmt->execute(
                [
                    $i,
                    $key,
                    $label,
                    $f['type'] ?? 'text',
                    (int)($f['required'] ?? 0),
                    (int)($f['searchable'] ?? 1),
                    (int)($f['show_in_list'] ?? 1)
                ]
            );
        }

        $db->commit();
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }

        respond(
            false,
            ['error' => 'Felder konnten nicht gespeichert werden.']
        );
    }

    respond(
        true,
        ['message' => '✅ Felder gespeichert!']
    );
}

/*
 * ─────────────────────────────────────────────
 * Datensätze
 * ─────────────────────────────────────────────
 */

if ($action === 'get_records') {
    $search = trim(
        (string)($input['search'] ?? '')
    );

    $page = max(
        0,
        (int)($input['page'] ?? 0)
    );

    $limit = max(
        5,
        min(
            100,
            (int)($input['limit'] ?? 20)
        )
    );

    $all = $db
        ->query(
            "SELECT *
             FROM vndf_records
             ORDER BY sort_order ASC, id ASC"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    $rows = [];

    foreach ($all as $row) {
        $data = json_decode(
            $row['data'] ?? '{}',
            true
        );

        if (!is_array($data)) {
            $data = [];
        }

        if ($search !== '') {
            $found = false;

            foreach ($data as $value) {
                if (
                    stripos(
                        (string)$value,
                        $search
                    ) !== false
                ) {
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                continue;
            }
        }

        $rows[] = [
            'id'         => $row['id'],
            'created_at' => $row['created_at'],
            'data'       => $data
        ];
    }

    $total = count($rows);

    $slice = array_slice(
        $rows,
        $page * $limit,
        $limit
    );

    respond(
        true,
        [
            'records' => $slice,
            'total'   => $total,
            'page'    => $page,
            'limit'   => $limit
        ]
    );
}

if ($action === 'save_record') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $data = $input['data'] ?? [];

    if (!is_array($data)) {
        $data = [];
    }

    $id = (int)($input['id'] ?? 0);

    $json = json_encode(
        $data,
        JSON_UNESCAPED_UNICODE
    );

    if ($json === false) {
        respond(
            false,
            ['error' => 'Datensatz konnte nicht verarbeitet werden.']
        );
    }

    if ($id > 0) {
        $stmt = $db->prepare(
            "UPDATE vndf_records
             SET data = ?,
                 updated_at = datetime('now','localtime')
             WHERE id = ?"
        );

        $stmt->execute(
            [$json, $id]
        );
    } else {
        $stmt = $db->prepare(
            "INSERT INTO vndf_records (data)
             VALUES (?)"
        );

        $stmt->execute(
            [$json]
        );

        $id = (int)$db->lastInsertId();
    }

    respond(
        true,
        [
            'message' => '✅ Gespeichert!',
            'id'      => $id
        ]
    );
}

if ($action === 'delete_record') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $id = (int)($input['id'] ?? 0);

    if ($id > 0) {
        $stmt = $db->prepare(
            "DELETE FROM vndf_records
             WHERE id = ?"
        );

        $stmt->execute(
            [$id]
        );
    }

    respond(
        true,
        ['message' => '🗑 Gelöscht!']
    );
}

/*
 * ─────────────────────────────────────────────
 * CSV-Export
 * ─────────────────────────────────────────────
 */

if ($action === 'export_csv') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $fields = $db
        ->query(
            "SELECT *
             FROM vndf_fields
             ORDER BY sort_order, id"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    $records = $db
        ->query(
            "SELECT *
             FROM vndf_records
             ORDER BY sort_order ASC, id ASC"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    header(
        'Content-Type: text/csv; charset=utf-8'
    );

    header(
        'Content-Disposition: attachment; filename="' .
        $gid .
        '_export.csv"'
    );

    $out = fopen(
        'php://output',
        'w'
    );

    if ($out === false) {
        respond(
            false,
            ['error' => 'CSV-Datei konnte nicht erstellt werden.']
        );
    }

    /*
     * UTF-8-BOM für Excel.
     */
    fprintf(
        $out,
        chr(0xEF) .
        chr(0xBB) .
        chr(0xBF)
    );

    fputcsv(
        $out,
        array_column($fields, 'label'),
        ';'
    );

    foreach ($records as $record) {
        $data = json_decode(
            $record['data'] ?? '{}',
            true
        );

        if (!is_array($data)) {
            $data = [];
        }

        $csvRow = array_map(
            static function (array $field) use ($data): string {
                return (string)(
                    $data[$field['field_key']] ?? ''
                );
            },
            $fields
        );

        fputcsv(
            $out,
            $csvRow,
            ';'
        );
    }

    fclose($out);
    exit;
}

/*
 * ─────────────────────────────────────────────
 * Bild hochladen
 * ─────────────────────────────────────────────
 */

if ($action === 'upload_image') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    if (
        !isset($_FILES['img'])
        || !is_array($_FILES['img'])
        || empty($_FILES['img']['tmp_name'])
    ) {
        respond(
            false,
            ['error' => 'Keine Datei.']
        );
    }

    $uploadError = (int)(
        $_FILES['img']['error']
        ?? UPLOAD_ERR_NO_FILE
    );

    if ($uploadError !== UPLOAD_ERR_OK) {
        respond(
            false,
            ['error' => 'Fehler beim Hochladen der Datei. Fehlercode: ' . $uploadError]
        );
    }

    $tmpFile = (string)$_FILES['img']['tmp_name'];

    if (!is_uploaded_file($tmpFile)) {
        respond(
            false,
            ['error' => 'Ungültige Upload-Datei.']
        );
    }

    /*
     * Dateityp anhand des tatsächlichen MIME-Typs prüfen.
     */
    $allowedTypes = [
        'image/jpeg' => 'jpg',
        'image/png'  => 'png',
        'image/gif'  => 'gif',
        'image/webp' => 'webp'
    ];

    $mimeType = '';

    if (function_exists('finfo_open')) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);

        if ($finfo !== false) {
            $detectedType = finfo_file(
                $finfo,
                $tmpFile
            );

            if (is_string($detectedType)) {
                $mimeType = $detectedType;
            }

            finfo_close($finfo);
        }
    }

    /*
     * Fallback, falls Fileinfo nicht verfügbar ist.
     */
    if ($mimeType === '') {
        $imageInfo = @getimagesize($tmpFile);

        if (
            is_array($imageInfo)
            && isset($imageInfo['mime'])
        ) {
            $mimeType = (string)$imageInfo['mime'];
        }
    }

    if (!isset($allowedTypes[$mimeType])) {
        respond(
            false,
            ['error' => 'Ungültiges Bildformat.']
        );
    }

    $extension = $allowedTypes[$mimeType];

    /*
     * Physischer Upload-Ordner:
     * assets/vn-dataform/uploads/<gid>/
     */
    $uploadDir = __DIR__
        . '/uploads/'
        . $gid;

    if (
        !is_dir($uploadDir)
        && !mkdir($uploadDir, 0755, true)
        && !is_dir($uploadDir)
    ) {
        respond(
            false,
            ['error' => 'Upload-Ordner konnte nicht angelegt werden.']
        );
    }

    /*
     * Sicheren, eindeutigen Dateinamen erzeugen.
     */
    try {
        $randomName = bin2hex(
            random_bytes(8)
        );
    } catch (Throwable $e) {
        $randomName = uniqid('', true);
        $randomName = preg_replace(
            '/[^a-z0-9]/i',
            '',
            $randomName
        );
    }

    $fileName = 'img_'
        . $randomName
        . '.'
        . $extension;

    $target = $uploadDir
        . '/'
        . $fileName;

    if (
        !move_uploaded_file(
            $tmpFile,
            $target
        )
    ) {
        respond(
            false,
            ['error' => 'Upload fehlgeschlagen.']
        );
    }

    /*
     * Öffentliche URL aus dem URL-Pfad des PHP-Scripts erzeugen.
     *
     * Es wird ausdrücklich NICHT mehr mit DOCUMENT_ROOT
     * oder realpath() gearbeitet.
     */
    $scriptDirectory = getPublicScriptDirectory();

    $publicUrl = $scriptDirectory
        . '/uploads/'
        . rawurlencode($gid)
        . '/'
        . rawurlencode($fileName);

    /*
     * Sicherstellen, dass die URL mit genau einem Slash beginnt.
     */
    $publicUrl = '/'
        . ltrim($publicUrl, '/');

    respond(
        true,
        [
            'url'     => $publicUrl,
            'message' => '✅ Hochgeladen!'
        ]
    );
}

/*
 * ─────────────────────────────────────────────
 * Passwort ändern
 * ─────────────────────────────────────────────
 */

if ($action === 'change_password') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $oldPw = (string)(
        $input['old_password'] ?? ''
    );

    $newPw = (string)(
        $input['new_password'] ?? ''
    );

    $hash = file_exists($pwFile)
        ? trim((string)file_get_contents($pwFile))
        : '';

    if (
        $hash !== ''
        && !password_verify($oldPw, $hash)
    ) {
        respond(
            false,
            ['error' => 'Altes Passwort falsch.']
        );
    }

    if (strlen($newPw) < 4) {
        respond(
            false,
            ['error' => 'Min. 4 Zeichen.']
        );
    }

    $newHash = password_hash(
        $newPw,
        PASSWORD_BCRYPT
    );

    if ($newHash === false) {
        respond(
            false,
            ['error' => 'Neues Passwort konnte nicht erzeugt werden.']
        );
    }

    if (
        file_put_contents(
            $pwFile,
            $newHash,
            LOCK_EX
        ) === false
    ) {
        respond(
            false,
            ['error' => 'Neues Passwort konnte nicht gespeichert werden.']
        );
    }

    respond(
        true,
        ['message' => '✅ Passwort geändert!']
    );
}

/*
 * ─────────────────────────────────────────────
 * Reihenfolge speichern
 * ─────────────────────────────────────────────
 */

if ($action === 'reorder') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $ids = $input['ids'] ?? [];

    if (!is_array($ids)) {
        $ids = [];
    }

    $stmt = $db->prepare(
        "UPDATE vndf_records
         SET sort_order = ?
         WHERE id = ?"
    );

    $db->beginTransaction();

    try {
        foreach ($ids as $i => $id) {
            $stmt->execute(
                [
                    $i,
                    (int)$id
                ]
            );
        }

        $db->commit();
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }

        respond(
            false,
            ['error' => 'Reihenfolge konnte nicht gespeichert werden.']
        );
    }

    respond(
        true,
        ['message' => '✅ Reihenfolge gespeichert!']
    );
}

/*
 * Keine bekannte Aktion gefunden.
 */
respond(
    false,
    ['error' => 'Unbekannte Aktion: ' . $action]
);
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Moin Tommy,

ich sag doch ohne deine Tests gehts nicht :D

Server Pfad geändert. Bitte neu downloaden und testen.

Sollte jetzt gehen. Ich kann eben nur auf meinem Server testen. Hab vergessen das Hoster da andere Pfade haben ;)
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Du hast jetzt nur die api.php geändert. Das brauche ich nicht testen - sondern Du musst es testen, ob es bei Dir auch noch so geht.

Es sollte bei allen jetzt so funktionieren.

Ich finde sicher noch andere Fehler, dann downloade ich nochmals neu.
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Hab deinen api code genommen - ist sauberer als meiner :D
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Ich habe die HTML etwas umgeschrieben, damit auch wieder ein Textlink zum Admin möglich ist. Es wäre schön, wenn Du das beibehalten würdest. Viele Benutzer werden einen solchen Textlink zum Adminbereich bevorzugen.

Hier die erweiterte HTML um das hier:

Code: Alles auswählen

window.dfOpenLogin = openLogin;

Bitte hier kopieren, denn ich habe die HTML hier korrekt eingerückt :aufsmaul:

Code: Alles auswählen

<section data-bs-version="5.1" class="vn-dataform" data-showsearch="true" group="Content" plugins="vn-dataform" data-gid="{{formId}}" data-mobiletrigger="{{mobileTrigger}}" data-viewmode="{{viewMode}}" data-perpage="{{perPage}}" data-imgsize="{{imgSize}}" data-imgpos="{{imgPos}}" data-shadowsize="{{shadowSize}}" data-shadowcolor="{{shadowColor}}">
    <mbr-parameters>
        <header>Formular</header>
        <input type="text" title="Formular-ID (eindeutig)" name="formId" value="form-1">
          <input type="checkbox" title="Suche anzeigen" name="showSearch" checked>
        <select title="Ansicht" name="viewMode">
            <option value="cards" selected>Karten</option>
            <option value="table">Tabelle</option>
        </select>
        <input type="range" inline title="Einträge/Seite" name="perPage" min="5" max="50" step="1" value="10">
       

        <header>Handy Admin-Trigger</header>
        <select title="Handy Admin-Trigger" name="mobileTrigger">
            <option value="shake" selected>Schütteln</option>
            <option value="tap4">4x Tippen</option>
            <option value="button">⚙️ Button</option>
        </select>

        <header>Abstände</header>
        <input type="range" inline title="Oben" name="paddingTop" min="2" max="12" step="1" value="7">
        <input type="range" inline title="Unten" name="paddingBottom" min="2" max="12" step="1" value="2">
        <input type="range" inline title="Ecken" name="cardRadius" min="0" max="20" step="2" value="10">

        <header>Schatten</header>
        <input type="range" inline title="Schatten Stärke" name="shadowSize" min="0" max="40" step="4" value="8">
        <input type="color" title="Schatten Farbe" name="shadowColor" value="#ffffff" selected>

        <header>Bilder</header>
        <input type="range" inline title="Bildgröße (px)" name="imgSize" min="60" max="300" step="10" value="110">
        <select title="Bild-Position" name="imgPos">
            <option value="top" selected>Oben in Karte</option>
            <option value="row">Als Zeile</option>
        </select>

        <header>Schriftgrößen</header>
        <input type="range" inline title="Label (px)" name="labelSize" min="10" max="22" step="1" value="18">
        <input type="range" inline title="Wert (px)" name="valueSize" min="10" max="22" step="1" value="21">
        <input type="range" inline title="Header (px)" name="headerSize" min="12" max="30" step="1" value="21">

        <header>Farben</header>
        <input type="color" title="Hintergrund" name="bgColor" value="#0a0a0a" selected>
        <input type="color" title="Karte" name="cardColor" value="#272727" selected>
        <input type="color" title="Kopf Hintergrund" name="headBg" value="#08378e" selected>
        <input type="color" title="Kopf Text" name="headText" value="#ffffff" selected>
        <input type="color" title="Label-Farbe" name="labelColor" value="#bed3f9" selected>
        <input type="color" title="Wert-Farbe" name="valueColor" value="#e0e0e0" selected>
        <input type="color" title="Rahmen" name="borderColor" value="#222222" selected>
        <input type="color" title="Akzent" name="accentColor" value="#0d41a0" selected>
        <input type="color" title="Suche Hintergrund" name="searchBg" value="#001a47" selected>
    </mbr-parameters>

    <div class="df-wrap">
        <!-- Suchbereich -->
        <div class="df-search-wrap" id="df-search-wrap">
            <input type="text" id="df-search" class="df-search-inp" placeholder="🔍 Suche…" autocomplete="off" spellcheck="false">
        </div>

        <!-- Ergebnisse -->
        <div id="df-results"></div>


        <!-- Admin Panel -->
        <div id="df-admin" style="display:none;">

            <!-- Tabs -->
            <div class="df-tabs">
                <button class="df-tab df-tab-active" data-tab="records" onclick="dfTab('records')">📋 Einträge</button>
                <button class="df-tab" data-tab="fields" onclick="dfTab('fields')">🔧 Felder</button>
                <button class="df-tab" data-tab="new" onclick="dfTab('new')">➕ Neu</button>
            </div>

            <!-- Tab: Einträge -->
            <div id="df-tab-records" class="df-tab-content">
                <div id="df-admin-list"></div>
            </div>

            <!-- Tab: Felder definieren -->
            <div id="df-tab-fields" class="df-tab-content" style="display:none;">
                <div style="margin-bottom:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
                    <button class="df-btn-accent" onclick="dfAddField()">+ Feld</button>
                    <button class="df-btn-primary" onclick="dfSaveFields()">💾 Felder speichern</button>
                </div>
                <div id="df-field-list"></div>
            </div>

            <!-- Tab: Neuer Eintrag -->
            <div id="df-tab-new" class="df-tab-content" style="display:none;max-height:60vh;overflow-y:auto;">
                <div id="df-new-form"></div>
                <button class="df-btn-primary" style="margin-top:1rem;" onclick="dfSaveRecord()">💾 Speichern</button>
            </div>

            <div style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
                <a id="df-export-link" href="#" class="df-btn-sm" onclick="dfExport(event)">⬇ CSV</a>
                <button class="df-btn-ghost" onclick="document.getElementById('df-pw-modal').style.display='flex'">🔑 Passwort</button>
                <button class="df-btn-ghost" onclick="dfLogout()">Ausloggen</button>
            </div>
        </div>

        <!-- Dummy im Editor -->
        <div id="df-dummy">
            <div class="df-card">
                <div class="df-card-head">Max Mustermann</div>
                <div class="df-card-body">
                    <div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">max@beispiel.de</span></div>
                    <div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 123 456789</span></div>
                    <div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">Stuttgart</span></div>
                </div>
            </div>
            <div class="df-card">
                <div class="df-card-head">Erika Musterfrau</div>
                <div class="df-card-body">
                    <div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">erika@beispiel.de</span></div>
                    <div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 987 654321</span></div>
                    <div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">München</span></div>
                </div>
            </div>
            <p class="df-hint">📊 Vorschau – echte Daten nach Publish</p>
        </div>
    </div>

    <!-- Login Modal -->
    <div id="df-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
        <div class="df-modal-box">
            <h3 id="df-modal-title" style="margin:0 0 0.75rem;font-size:1rem;">🔐 Admin-Login</h3>
            <input type="password" id="df-pw" class="df-inp" placeholder="Passwort">
            <p id="df-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button id="df-login-btn" class="df-btn-primary" style="flex:1;">Anmelden</button>
                <button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-modal').style.display='none'">Abbrechen</button>
            </div>
        </div>
    </div>
    <!-- Passwort-Modal -->
    <div id="df-pw-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
        <div class="df-modal-box">
            <h3 style="margin:0 0 0.75rem;font-size:1rem;">🔑 Passwort ändern</h3>
            <input type="password" id="df-old-pw" class="df-inp" placeholder="Altes Passwort" style="margin-bottom:0.5rem;">
            <input type="password" id="df-new-pw" class="df-inp" placeholder="Neues Passwort (min. 4 Zeichen)" style="margin-bottom:0.5rem;">
            <input type="password" id="df-new-pw2" class="df-inp" placeholder="Neues Passwort bestätigen">
            <p id="df-pw-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button onclick="dfChangePw()" class="df-btn-primary" style="flex:1;">Speichern</button>
                <button onclick="document.getElementById('df-pw-modal').style.display='none'" class="df-btn-ghost" style="flex:1;">Abbrechen</button>
            </div>
        </div>
    </div>
    <div id="df-toast" style="display:none;position:fixed;bottom:1.5rem;right:1.5rem;padding:0.75rem 1.25rem;border-radius:8px;z-index:99999;font-size:0.88rem;font-weight:600;color:#fff;"></div>

    <!-- Edit Modal -->
    <div id="df-edit-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99998;align-items:center;justify-content:center;">
        <div class="df-modal-box" style="max-width:500px;max-height:85vh;overflow-y:auto;">
            <h3 style="margin:0 0 0.75rem;font-size:1rem;">✏️ Eintrag bearbeiten</h3>
            <div id="df-edit-form"></div>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button class="df-btn-primary" style="flex:1;" onclick="dfSaveEdit()">💾 Speichern</button>
                <button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-edit-modal').style.display='none'">Abbrechen</button>
            </div>
        </div>
    </div>

    <script>
    (function(){
        var sec = document.currentScript ? document.currentScript.closest('section.vn-dataform') : document.querySelector('section.vn-dataform');
        if (!sec) return;

        var inBuilder = (function(){
            if (document.querySelector('html').classList.contains('is-builder')) return true;
            if (window.location.port === '409' || window.location.protocol === 'file:') return true;
            var all = document.querySelectorAll('link[href],script[src]');
            for (var i=0;i<all.length;i++){
                var h=all[i].getAttribute('href')||all[i].getAttribute('src')||'';
                if (h.indexOf('file:')===0||h.indexOf('AppData')!==-1||h.indexOf('Mobirise')!==-1) return true;
            }
            return false;
        })();
        if (inBuilder) return;

        // Live: Dummy verstecken
        var dummy = document.getElementById('df-dummy');
        if (dummy) dummy.style.display = 'none';

        // Loading-Div dynamisch einfügen
        var ld = document.createElement('div');
        ld.id = 'df-loading';
        ld.className = 'df-loading';
        ld.textContent = '⟳ Lade…';
        document.getElementById('df-results').before(ld);

        var GID      = sec.dataset.gid || 'form-1';
        var VIEWMODE = sec.dataset.viewmode || 'cards';
        var PERPAGE  = parseInt(sec.dataset.perpage) || 10;
        var SEARCH   = sec.dataset.showsearch === 'true' || sec.dataset.showsearch === '1';
        var IMGSIZE  = parseInt(sec.dataset.imgsize) || 120;
        var IMGPOS   = sec.dataset.imgpos || 'top';
        var isAdmin  = false;
        var fields   = [];
        var curPage  = 0;
        var editId   = 0;

        var ep = (function(){
            var links = document.querySelectorAll('link[href]');
            for (var i=0;i<links.length;i++){
                var h=links[i].getAttribute('href')||'';
                if (h.indexOf('http')===0 && h.indexOf('assets/')!==-1)
                    return h.split('assets/')[0]+'assets/vn-dataform/api.php';
            }
            return 'assets/vn-dataform/api.php';
        })();

        function api(payload, cb) {
            fetch(ep + '?action=' + payload.action + '&gid=' + encodeURIComponent(GID), {
                method:'POST', headers:{'Content-Type':'application/json'},
                body: JSON.stringify(payload)
            }).then(function(r){ return r.json(); }).then(cb).catch(function(e){ cb({success:false,error:String(e)}); });
        }

        function toast(msg, err) {
            var t = document.getElementById('df-toast');
            t.textContent = msg; t.style.display = 'block';
            t.style.background = err ? '#e74c3c' : '#1d9e75';
            setTimeout(function(){ t.style.display='none'; }, 3000);
        }

        function esc(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

        function fmtDate(val) {
            if (!val) return '';
            // YYYY-MM-DD → TT.MM.JJJJ
            var m = val.match(/^(\d{4})-(\d{2})-(\d{2})$/);
            if (m) return m[3] + '.' + m[2] + '.' + m[1];
            return val;
        }

        // ── Felder laden ──────────────────────────────────
        function loadFields(cb) {
            api({action:'get_fields'}, function(d){
                fields = d.fields || [];
                if (cb) cb();
            });
        }

      // ── Suchefeld ─────────────────────────────────────
    function initSearch() {

        var srch = document.getElementById('df-search');
        if (!srch) return;

        // Suchfeld immer zurücksetzen
        srch.value = "";
        srch.setAttribute("autocomplete", "off");
        srch.setAttribute("autocorrect", "off");
        srch.setAttribute("autocapitalize", "off");
        srch.setAttribute("spellcheck", "false");

        // Browser-Autofill zusätzlich verhindern
        setTimeout(function () {
            srch.value = "";
        }, 50);

        // Suche verstecken wenn Checkbox aus
        if (!SEARCH) {
            var wrap = document.getElementById('df-search-wrap');
            if (wrap) wrap.style.display = 'none';
            return;
        }

        srch.oninput = function () {
            curPage = 0;
            loadRecords();
        };
    }

        // ── Datensätze laden und anzeigen ─────────────────
        function loadRecords() {
            var search = (document.getElementById('df-search')||{value:''}).value;
            api({action:'get_records', search:search, page:parseInt(curPage), limit:parseInt(PERPAGE)}, function(d){
                renderResults(d.records||[], d.total||0);
            });
        }

        function renderResults(records, total) {
            console.log("Seite:", curPage, "Records:", records.length, "Total:", total);
            var ldEl = document.getElementById('df-loading');
            if (ldEl) ldEl.style.display = 'none';
            var div = document.getElementById('df-results');
            if (!records.length) {
                if (total > 0 && curPage > 0) {
                    // Seite existiert nicht mehr → zurück zu Seite 0
                    curPage = 0;
                    loadRecords();
                    return;
                }
                div.innerHTML = '<p class="df-hint">Keine Einträge gefunden.</p>';
                document.getElementById('df-pager').innerHTML = '';
                return;
            }
            var listFields = fields.filter(function(f){ return f.show_in_list == 1; });
            var html = '';

            if (VIEWMODE === 'table') {
                var tableFields = fields.length ? fields : listFields;
                html = '<div class="df-table-wrap"><table class="df-table"><thead><tr>';
                tableFields.forEach(function(f){ html += '<th>'+esc(f.label)+'</th>'; });
                if (isAdmin) html += '<th></th>';
                html += '</tr></thead><tbody>';
                records.forEach(function(r){
                    html += '<tr>';
                    tableFields.forEach(function(f){
                        var tdval = r.data[f.field_key]||'';
                        var tdhtml = f.type==='image' && tdval
                            ? '<img src="'+tdval+'" style="height:'+Math.round(IMGSIZE*0.5)+'px;max-width:120px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+tdval+'">' 
                            : (f.type==='date' ? fmtDate(tdval) : esc(tdval));
                        html += '<td>'+tdhtml+'</td>';
                    });
                    if (isAdmin) html += '<td><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></td>';
                    html += '</tr>';
                });
                html += '</tbody></table></div>';
            } else {
                html = '<div class="df-cards">';
                records.forEach(function(r){
                    var firstField = listFields[0];
                    var title = firstField ? esc(r.data[firstField.field_key]||'—') : '#'+r.id;
                    // Bild-Felder für diese Karte
                    var imgField = fields.find(function(f){ return f.type==='image' && r.data[f.field_key]; });
                    var imgUrl   = imgField ? r.data[imgField.field_key] : '';

                    html += '<div class="df-card">';

                    // Bild oben
                    if (imgUrl && IMGPOS==='top') {
                        html += '<div style="overflow:hidden;max-height:'+IMGSIZE+'px;cursor:zoom-in;" onclick="dfLightbox(this.dataset.lbsrc)" data-lbsrc="'+imgUrl+'">'
                            + '<img src="'+imgUrl+'" class="df-card-img" style="width:100%;height:'+IMGSIZE+'px;object-fit:cover;display:block;">'
                            + '</div>';
                    }

                    html += '<div class="df-card-head">'+title;
                    if (isAdmin) html += '<span style="float:right;"><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></span>';
                    html += '</div><div class="df-card-body">';
                    listFields.slice(1).forEach(function(f){
                        var val = r.data[f.field_key]||'';
                        if (!val) return;
                        var link = '';
                        if (f.type==='email') link='<a href="mailto:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='tel') link='<a href="tel:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='url') link='<a href="'+esc(val)+'" target="_blank" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='image') {
                            if (IMGPOS==='top') return; // schon oben gezeigt
                            link='<img src="'+val+'" style="max-width:100%;max-height:'+IMGSIZE+'px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+val+'">';
                        }
                        else if (f.type==='date') link=fmtDate(val);
                        else link=esc(val);
                        html += '<div class="df-row"><span class="df-lbl">'+esc(f.label)+'</span><span class="df-val">'+link+'</span></div>';
                    });
                    html += '</div></div>';
                });
                html += '</div>';
            }
    // ---------------- Pagination ----------------

    var pages = Math.ceil(total / PERPAGE);

    if (pages > 1) {

        var phtml = '<div class="df-pager"><div class="df-pager-inner">';

        // Erste
        phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage(0)')+'">« Erste</button>';

        // Zurück
        phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage('+(curPage-1)+')')+'">‹ Zurück</button>';

        // Seitenbereich
        var start = Math.max(0, curPage-2);
        var end   = Math.min(pages-1, curPage+2);

        if (start > 0) {
            phtml += '<button class="df-page-btn" onclick="dfGoPage(0)">1</button>';
            if (start > 1)
                phtml += '<span class="df-page-dots">…</span>';
        }

        for (var i=start;i<=end;i++) {
            phtml += '<button class="df-page-btn'+(i==curPage?' df-page-active':'')+'" onclick="dfGoPage('+i+')">'+(i+1)+'</button>';
        }

        if (end < pages-1) {

            if (end < pages-2)
                phtml += '<span class="df-page-dots">…</span>';

            phtml += '<button class="df-page-btn" onclick="dfGoPage('+(pages-1)+')">'+pages+'</button>';
        }

        // Weiter
        phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(curPage+1)+')')+'">Weiter ›</button>';

        // Letzte
        phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(pages-1)+')')+'">Letzte »</button>';

        phtml += '</div></div>';

        html += phtml;
    }
    console.log(html);
            div.innerHTML = html;

            // Lightbox-Delegation
            div.querySelectorAll('img.df-lb-img').forEach(function(img){
                img.style.cursor = 'zoom-in';
                img.onclick = function(){ dfLightbox(this.dataset.lbsrc); };
            });
        }

       window.dfGoPage = function(p){

        if (p < 0) p = 0;

        curPage = p;

        loadRecords();

        var el = document.getElementById("df-results");
        if(el){
            el.scrollIntoView({
                behavior:"smooth",
                block:"start"
            });
        }
    };

        // ── Admin: Datensatz bearbeiten ───────────────────
        window.dfEditRecord = function(id) {
            editId = id;
            api({action:'get_records', page:0, limit:9999}, function(d){
                var rec = (d.records||[]).find(function(r){ return r.id===id; });
                if (!rec) return;
                var html = '';
                fields.forEach(function(f){
                    var v = esc(rec.data[f.field_key]||'');
                    var einp;
                    if (f.type==='textarea') {
                        einp = '<textarea class="df-inp" id="dfe_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;">'+v+'</textarea>';
                    } else if (f.type==='image') {
                        einp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
                            + (v ? '<img src="'+v+'" style="max-height:60px;border-radius:4px;object-fit:cover;margin-bottom:4px;">' : '')
                            + '<input type="file" accept="image/*" class="df-inp" id="dfe_file_'+f.field_key+'" style="padding:4px;">'
                            + '<input type="hidden" id="dfe_'+f.field_key+'" value="'+v+'">'
                            + '</div>';
                    } else {
                        einp = '<input type="'+f.type+'" class="df-inp" id="dfe_'+f.field_key+'" value="'+v+'">';
                    }
                    html += '<label class="df-field-lbl">'+esc(f.label)+einp+'</label>';
                });
                document.getElementById('df-edit-form').innerHTML = html;
                document.getElementById('df-edit-modal').style.display = 'flex';
            });
        };

        window.dfSaveEdit = function() {
            uploadImages('dfe_', function(){
                var data = {};
                fields.forEach(function(f){
                    var el = document.getElementById('dfe_'+f.field_key);
                    if (el) data[f.field_key] = el.value;
                });
                api({action:'save_record', id:editId, data:data}, function(d){
                    if (d.success) {
                        toast(d.message||'✅ Gespeichert!');
                        document.getElementById('df-edit-modal').style.display='none';
                        loadRecords();
                        renderAdminList();
                    } else toast(d.error||'Fehler', true);
                });
            });
        };

        window.dfDeleteRecord = function(id) {
            if (!confirm('Eintrag löschen?')) return;
            api({action:'delete_record', id:id}, function(d){
                if (d.success) { toast(d.message||'Gelöscht!'); loadRecords(); renderAdminList(); }
                else toast(d.error||'Fehler', true);
            });
        };

        // ── Admin: Eintrags-Liste ─────────────────────────
        function renderAdminList() {
            api({action:'get_records', page:0, limit:999}, function(d){
                var records = d.records||[];
                var div = document.getElementById('df-admin-list');
                if (!records.length) { div.innerHTML='<p class="df-hint">Noch keine Einträge.</p>'; return; }
                var html = '<table class="df-table"><thead><tr>';
                fields.slice(0,3).forEach(function(f){ html+='<th>'+esc(f.label)+'</th>'; });
                html += '<th></th></tr></thead><tbody>';
                records.forEach(function(r){
                    html += '<tr data-id="'+r.id+'">';
                    fields.slice(0,3).forEach(function(f){ html+='<td>'+esc(r.data[f.field_key]||'')+'</td>'; });
                    html += '<td style="white-space:nowrap;">'
                        +'<button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> '
                        +'<button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button>'
                        +'</td></tr>';
                });
                html += '</tbody></table>';
                div.innerHTML = html;
                // Drag & Drop Zeilen
                var dragSrcRow = null;
                div.querySelectorAll('tbody tr').forEach(function(tr){
                    tr.draggable = true;
                    tr.style.cursor = 'grab';
                    tr.addEventListener('dragstart',function(){ dragSrcRow=tr; setTimeout(function(){tr.style.opacity='0.4';},0); });
                    tr.addEventListener('dragend',function(){ tr.style.opacity='1'; div.querySelectorAll('tbody tr').forEach(function(r){r.style.outline='';}); });
                    tr.addEventListener('dragover',function(e){ e.preventDefault(); tr.style.outline='2px solid #1d9e75'; });
                    tr.addEventListener('dragleave',function(){ tr.style.outline=''; });
                    tr.addEventListener('drop',function(e){
                        e.preventDefault(); tr.style.outline='';
                        if (!dragSrcRow || dragSrcRow===tr) return;
                        var tbody = tr.closest('tbody');
                        var rows = Array.from(tbody.querySelectorAll('tr'));
                        var si = rows.indexOf(dragSrcRow), ti = rows.indexOf(tr);
                        if (si < ti) tbody.insertBefore(dragSrcRow, tr.nextSibling);
                        else tbody.insertBefore(dragSrcRow, tr);
                        dragSrcRow = null;
                        // Neue Reihenfolge speichern
                        var newOrder = Array.from(tbody.querySelectorAll('tr')).map(function(r){
                            return parseInt(r.dataset.id);
                        }).filter(function(id){ return !isNaN(id); });
                        if (newOrder.length) {
                            api({action:'reorder', ids:newOrder}, function(d){
                                if (d.success) toast('✅ Reihenfolge gespeichert!');
                            });
                        }
                    });
                });
            });
        }

        // ── Admin: Felder ─────────────────────────────────
        window.dfTab = function(tab) {
            ['records','fields','new'].forEach(function(t){
                document.getElementById('df-tab-'+t).style.display = t===tab ? 'block' : 'none';
                var btn = document.querySelector('[data-tab="'+t+'"]');
                if (btn) btn.classList.toggle('df-tab-active', t===tab);
            });
            if (tab==='records') renderAdminList();
            if (tab==='fields')  renderFieldEditor();
            if (tab==='new')     renderNewForm();
        };

        function renderFieldEditor() {
            var html = '';
            fields.forEach(function(f,i){
                html += '<div class="df-field-row" data-idx="'+i+'">'
                    + '<span class="df-drag-handle">⠿</span>'
                    + '<input type="text" class="df-inp df-fl-name" value="'+esc(f.label)+'" placeholder="Feldname">'
                    + '<select class="df-inp df-fl-type">'
                    + ['text','email','tel','url','number','date','textarea','image'].map(function(t){
                        return '<option value="'+t+'"'+(f.type===t?' selected':'')+'>'+t+'</option>';
                      }).join('')
                    + '</select>'
                    + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
                    + '<input type="checkbox" class="df-fl-req"'+(f.required==1?' checked':'')+'> Pflicht</label>'
                    + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
                    + '<input type="checkbox" class="df-fl-show"'+(f.show_in_list==1?' checked':'')+'> Anzeigen</label>'
                    + '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>'
                    + '</div>';
            });
            document.getElementById('df-field-list').innerHTML = html;
        }

        window.dfAddField = function() {
            var li = document.getElementById('df-field-list');
            var div = document.createElement('div');
            div.className = 'df-field-row';
            div.innerHTML = '<span class="df-drag-handle">⠿</span>'
                + '<input type="text" class="df-inp df-fl-name" placeholder="Feldname">'
                + '<select class="df-inp df-fl-type">'
                + ['text','email','tel','url','number','date','textarea','image'].map(function(t){
                    return '<option value="'+t+'">'+t+'</option>';
                  }).join('')
                + '</select>'
                + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-req"> Pflicht</label>'
                + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-show" checked> Anzeigen</label>'
                + '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>';
            li.appendChild(div);
        };

        window.dfSaveFields = function() {
            var rows = document.querySelectorAll('#df-field-list .df-field-row');
            var newFields = [];
            rows.forEach(function(row){
                var name = row.querySelector('.df-fl-name').value.trim();
                if (!name) return;
                newFields.push({
                    label:       name,
                    type:        row.querySelector('.df-fl-type').value,
                    required:    row.querySelector('.df-fl-req').checked ? 1 : 0,
                    show_in_list:row.querySelector('.df-fl-show').checked ? 1 : 0,
                });
            });
            api({action:'save_fields', fields:newFields}, function(d){
                if (d.success) {
                    toast(d.message||'✅ Felder gespeichert!');
                    loadFields(function(){ loadRecords(); renderNewForm(); });
                } else toast(d.error||'Fehler', true);
            });
        };

        // ── Admin: Neuer Eintrag ──────────────────────────
        function renderNewForm() {
            editId = 0;
            var html = '';
            fields.forEach(function(f){
                var inp;
                if (f.type==='textarea') {
                    inp = '<textarea class="df-inp" id="dfn_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;"></textarea>';
                } else if (f.type==='image') {
                    inp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
                        + '<input type="file" accept="image/*" class="df-inp" id="dfn_file_'+f.field_key+'" style="padding:4px;">'
                        + '<input type="hidden" id="dfn_'+f.field_key+'">'
                        + '<img id="dfn_prev_'+f.field_key+'" src style="display:none;max-height:80px;border-radius:4px;margin-top:4px;object-fit:cover;">'
                        + '</div>';
                } else {
                    inp = '<input type="'+f.type+'" class="df-inp" id="dfn_'+f.field_key+'">';
                }
                html += '<label class="df-field-lbl">'+esc(f.label)+(f.required==1?' <span style="color:#e74c3c;">*</span>':'')+inp+'</label>';
            });
            if (!html) html = '<p class="df-hint">Zuerst Felder unter 🔧 Felder anlegen!</p>';
            document.getElementById('df-new-form').innerHTML = html;
        }

        window.dfSaveRecord = function() {
            uploadImages('dfn_', function(){
                var data = {};
                fields.forEach(function(f){
                    var el = document.getElementById('dfn_'+f.field_key);
                    if (el) data[f.field_key] = el.value;
                });
                api({action:'save_record', id:0, data:data}, function(d){
                    if (d.success) {
                        toast(d.message||'✅ Gespeichert!');
                        renderNewForm();
                        loadRecords();
                    } else toast(d.error||'Fehler', true);
                });
            });
        };

        // Alle Bild-Felder vor dem Speichern hochladen
        function uploadImages(prefix, cb) {
            var imgFields = fields.filter(function(f){ return f.type==='image'; });
            var pending = 0;
            if (!imgFields.length) { cb(); return; }
            imgFields.forEach(function(f){
                var fileInp = document.getElementById(prefix+'file_'+f.field_key);
                if (!fileInp || !fileInp.files || !fileInp.files[0]) return;
                pending++;
                var fd = new FormData();
                fd.append('img', fileInp.files[0]);
                fetch(ep + '?action=upload_image&gid=' + encodeURIComponent(GID), {method:'POST', body:fd})
                    .then(function(r){ return r.json(); })
                    .then(function(d){
                        if (d.success) {
                            var hidden = document.getElementById(prefix+f.field_key);
                            if (hidden) hidden.value = d.url;
                        }
                        pending--;
                        if (pending===0) cb();
                    })
                    .catch(function(){ pending--; if(pending===0) cb(); });
            });
            if (pending===0) cb();
        }

        window.dfLightbox = function(url) {
            var lb  = document.getElementById('df-lightbox');
            var img = document.getElementById('df-lightbox-img');
            if (!lb || !img) return;
            img.src = url;
            lb.style.display = 'flex';
        };
        document.addEventListener('keydown', function(e){
            if (e.key==='Escape') {
                var lb = document.getElementById('df-lightbox');
                if (lb) lb.style.display = 'none';
            }
        });

        window.dfExport = function(e) {
            e.preventDefault();
            window.open(ep.replace('api.php','') + '?action=export_csv&gid='+encodeURIComponent(GID), '_blank');
        };

        window.dfChangePw = function() {
            var old  = document.getElementById('df-old-pw').value;
            var nw   = document.getElementById('df-new-pw').value;
            var nw2  = document.getElementById('df-new-pw2').value;
            var err  = document.getElementById('df-pw-err');
            if (nw !== nw2) { err.textContent='Passwörter stimmen nicht überein.'; err.style.display='block'; return; }
            api({action:'change_password', old_password:old, new_password:nw}, function(d){
                if (d.success) {
                    toast(d.message||'✅ Passwort geändert!');
                    document.getElementById('df-pw-modal').style.display='none';
                    ['df-old-pw','df-new-pw','df-new-pw2'].forEach(function(id){
                        var el=document.getElementById(id); if(el) el.value='';
                    });
                } else {
                    err.textContent = d.error||'Fehler';
                    err.style.display = 'block';
                }
            });
        };

        window.dfLogout = function() {
            api({action:'logout'}, function(){
                isAdmin=false;
                document.getElementById('df-admin').style.display='none';
                loadRecords();
            });
        };

        // ── Login ─────────────────────────────────────────
        function openLogin() {
            api({action:'check'}, function(d){
                if (d.loggedIn) { isAdmin=true; showAdmin(); return; }
                var modal = document.getElementById('df-modal');
                var title = document.getElementById('df-modal-title');
                var err   = document.getElementById('df-err');
                var pw    = document.getElementById('df-pw');
                if (!modal) return;
                if (title) title.textContent = d.firstRun ? '⚙️ Erst-Einrichtung' : '🔐 Admin-Login';
                if (err) err.style.display = 'none';
                if (pw) pw.value = '';
                modal.style.display = 'flex';
                setTimeout(function(){ if(pw) pw.focus(); }, 100);
            });
        }
        
        /* EDIT Tommy - Login auch über externen Textlink aufrufbar machen */
        window.dfOpenLogin = openLogin;

        function showAdmin() {
            document.getElementById('df-admin').style.display = 'block';
            dfTab('records');
        }

        document.getElementById('df-login-btn').onclick = function(){
            var pw  = document.getElementById('df-pw');
            var err = document.getElementById('df-err');
            api({action:'login', password: pw ? pw.value : ''}, function(d){
                if (d.success) {
                    isAdmin = true;
                    document.getElementById('df-modal').style.display = 'none';
                    showAdmin();
                } else {
                    if (err) { err.textContent = d.error||'Falsches Passwort.'; err.style.display='block'; }
                }
            });
        };

        var pwEl = document.getElementById('df-pw');
        if (pwEl) pwEl.addEventListener('keypress', function(e){ if(e.key==='Enter') document.getElementById('df-login-btn').click(); });

        document.addEventListener('dblclick', function(e){ if(e.ctrlKey){ e.preventDefault(); openLogin(); }});
        document.addEventListener('keydown',  function(e){ if(e.ctrlKey&&e.shiftKey&&(e.key==='A'||e.key==='a')){ e.preventDefault(); openLogin(); }});

        // Handy-Trigger
        var mt = sec.dataset.mobiletrigger || 'shake';
        var isTouch = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
        if (mt==='button' && isTouch) {
            var fab = document.createElement('button');
            fab.innerHTML='⚙️';
            fab.style.cssText='position:fixed;bottom:1.5rem;left:1rem;width:44px;height:44px;border-radius:50%;background:#1d9e75;border:none;color:#fff;font-size:1.2rem;cursor:pointer;z-index:9998;';
            fab.onclick=openLogin;
            document.body.appendChild(fab);
        } else if (mt==='tap4') {
            var _tc=0,_tt=null;
            sec.addEventListener('touchend',function(){
                _tc++;clearTimeout(_tt);
                _tt=setTimeout(function(){_tc=0;},1500);
                if(_tc>=4){_tc=0;openLogin();}
            });
        } else if (mt==='shake') {
            var _sx=0,_sy=0,_sz=0,_st=0;
            if (window.DeviceMotionEvent) {
                window.addEventListener('devicemotion',function(e){
                    var a=e.accelerationIncludingGravity;if(!a)return;
                    var dv=Math.abs((a.x||0)-_sx)+Math.abs((a.y||0)-_sy)+Math.abs((a.z||0)-_sz);
                    _sx=a.x||0;_sy=a.y||0;_sz=a.z||0;
                    if(dv>25){var n=Date.now();if(n-_st>1500){_st=n;openLogin();}}
                });
            }
        }

        // Start
        loadFields(function(){
            initSearch();
            loadRecords();
        });
    })();
    </script>
    
    <!-- Lightbox -->
    <div id="df-lightbox" style="display:none;position:fixed;inset:0;background:#000000ee;z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;" onclick="document.getElementById('df-lightbox').style.display='none'">
        <img id="df-lightbox-img" src style="max-width:92%;max-height:92%;border-radius:10px;box-shadow:0 0 40px #000;object-fit:contain;">
    </div>

</section>
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Habe fertisch …

Keine anderen Fehler gefunden :tu:

↗️ https://www.mobirise-tutorials.com/VN-B ... Cards.html
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Ich sag doch Rezepte oder so ... ;)
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Doch noch Fehler :(

Der CSV-Export hat nicht funktioniert. Daher musste die Block-HTML und auch die api.php neu geschrieben werden.

Bitte füge diese nochmals neu ein und erstelle eine neue .mbrext-Datei.

Falls Du andere Farben haben willst - musst Du die noch wieder anpassen, denn das sind jetzt (natürlich) meine.

Ich muss eine ZIP erstellen, da der Forum-Editor diese Dateien von der Größe nicht mehr annimmt ...
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Wozu eigentlich der CSV-Export wenn es keinen CSV-Import gibt :confused:

Hier die ZIP:
Tommy-VN-DataForm-Dateien.zip
(16.03 KiB) 3-mal heruntergeladen

Wenn Du getestet und eine neue .mbrext-Datei erstellt hast, gib bitte Bescheid, damit ich die auch nochmals neu importieren kann.
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Mach ich heute Abend - bin noch bis 16 uhr im Bus ;)

CSV muss ich noich mal prüfen
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Warte mal - ich muss das selbst nochmals prüfen. Wieso hat die HTML jetzt 3 Mal soviel Zeilen :eek:
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Hier die aktuelle Block-HTML. ChatGPT hat für eine 2-zeilige Änderung aus 808 Zeilen Code ca. 2600 Zeilen gemacht :eek:

Ich habe das jetzt selbst geändert.

Der CSV-Export funktioniert jetzt korrekt.

Bitte diesen Code für die Block-HTML verwenden und nicht den im ZIP-Archiv.

Code: Alles auswählen

<section data-bs-version="5.1" class="vn-dataform" data-showsearch="true" group="Content" plugins="vn-dataform" data-gid="{{formId}}" data-mobiletrigger="{{mobileTrigger}}" data-viewmode="{{viewMode}}" data-perpage="{{perPage}}" data-imgsize="{{imgSize}}" data-imgpos="{{imgPos}}" data-shadowsize="{{shadowSize}}" data-shadowcolor="{{shadowColor}}">
    <mbr-parameters>
        <header>Formular</header>
        <input type="text" title="Formular-ID (eindeutig)" name="formId" value="form-1">
          <input type="checkbox" title="Suche anzeigen" name="showSearch" checked>
        <select title="Ansicht" name="viewMode">
            <option value="cards" selected>Karten</option>
            <option value="table">Tabelle</option>
        </select>
        <input type="range" inline title="Einträge/Seite" name="perPage" min="4" max="20" step="1" value="6">
       

        <header>Handy Admin-Trigger</header>
        <select title="Handy Admin-Trigger" name="mobileTrigger">
            <option value="shake" selected>Schütteln</option>
            <option value="tap4">4x Tippen</option>
            <option value="button">⚙️ Button</option>
        </select>

        <header>Abstände</header>
        <input type="range" inline title="Oben" name="paddingTop" min="2" max="12" step="1" value="7">
        <input type="range" inline title="Unten" name="paddingBottom" min="2" max="12" step="1" value="2">
        <input type="range" inline title="Ecken" name="cardRadius" min="0" max="20" step="2" value="10">

        <header>Schatten</header>
        <input type="range" inline title="Schatten Stärke" name="shadowSize" min="0" max="40" step="4" value="20">
        <input type="color" title="Schatten Farbe" name="shadowColor" value="#bbbbbb" selected>

        <header>Bilder</header>
        <input type="range" inline title="Bildgröße (px)" name="imgSize" min="60" max="300" step="10" value="120">
        <select title="Bild-Position" name="imgPos">
            <option value="top" selected>Oben in Karte</option>
            <option value="row">Als Zeile</option>
        </select>

        <header>Schriftgrößen</header>
        <input type="range" inline title="Label (px)" name="labelSize" min="10" max="22" step="1" value="18">
        <input type="range" inline title="Wert (px)" name="valueSize" min="10" max="22" step="1" value="21">
        <input type="range" inline title="Header (px)" name="headerSize" min="12" max="30" step="1" value="21">

        <header>Farben</header>
        <input type="color" title="Hintergrund" name="bgColor" value="#0a0a0a" selected>
        <input type="color" title="Karte" name="cardColor" value="#272727" selected>
        <input type="color" title="Kopf Hintergrund" name="headBg" value="#08378e" selected>
        <input type="color" title="Kopf Text" name="headText" value="#ffffff" selected>
        <input type="color" title="Label-Farbe" name="labelColor" value="#bed3f9" selected>
        <input type="color" title="Wert-Farbe" name="valueColor" value="#e0e0e0" selected>
        <input type="color" title="Rahmen" name="borderColor" value="#222222" selected>
        <input type="color" title="Akzent" name="accentColor" value="#0d41a0" selected>
        <input type="color" title="Suche Hintergrund" name="searchBg" value="#001a47" selected>
    </mbr-parameters>

    <div class="df-wrap">
        <!-- Suchbereich -->
        <div class="df-search-wrap" id="df-search-wrap">
            <input type="text" id="df-search" class="df-search-inp" placeholder="🔍 Suche…" autocomplete="off" spellcheck="false">
        </div>

        <!-- Ergebnisse -->
        <div id="df-results"></div>


        <!-- Admin Panel -->
        <div id="df-admin" style="display:none;">

            <!-- Tabs -->
            <div class="df-tabs">
                <button class="df-tab df-tab-active" data-tab="records" onclick="dfTab('records')">📋 Einträge</button>
                <button class="df-tab" data-tab="fields" onclick="dfTab('fields')">🔧 Felder</button>
                <button class="df-tab" data-tab="new" onclick="dfTab('new')">➕ Neu</button>
            </div>

            <!-- Tab: Einträge -->
            <div id="df-tab-records" class="df-tab-content">
                <div id="df-admin-list"></div>
            </div>

            <!-- Tab: Felder definieren -->
            <div id="df-tab-fields" class="df-tab-content" style="display:none;">
                <div style="margin-bottom:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
                    <button class="df-btn-accent" onclick="dfAddField()">+ Feld</button>
                    <button class="df-btn-primary" onclick="dfSaveFields()">💾 Felder speichern</button>
                </div>
                <div id="df-field-list"></div>
            </div>

            <!-- Tab: Neuer Eintrag -->
            <div id="df-tab-new" class="df-tab-content" style="display:none;max-height:60vh;overflow-y:auto;">
                <div id="df-new-form"></div>
                <button class="df-btn-primary" style="margin-top:1rem;" onclick="dfSaveRecord()">💾 Speichern</button>
            </div>

            <div style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
                <a id="df-export-link" href="#" class="df-btn-sm" onclick="dfExport(event)">⬇ CSV</a>
                <button class="df-btn-ghost" onclick="document.getElementById('df-pw-modal').style.display='flex'">🔑 Passwort</button>
                <button class="df-btn-ghost" onclick="dfLogout()">Ausloggen</button>
            </div>
        </div>

        <!-- Dummy im Editor -->
        <div id="df-dummy">
            <div class="df-card">
                <div class="df-card-head">Max Mustermann</div>
                <div class="df-card-body">
                    <div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">max@beispiel.de</span></div>
                    <div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 123 456789</span></div>
                    <div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">Stuttgart</span></div>
                </div>
            </div>
            <div class="df-card">
                <div class="df-card-head">Erika Musterfrau</div>
                <div class="df-card-body">
                    <div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">erika@beispiel.de</span></div>
                    <div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 987 654321</span></div>
                    <div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">München</span></div>
                </div>
            </div>
            <p class="df-hint">📊 Vorschau – echte Daten nach Publish</p>
        </div>
    </div>

    <!-- Login Modal -->
    <div id="df-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
        <div class="df-modal-box">
            <h3 id="df-modal-title" style="margin:0 0 0.75rem;font-size:1rem;">🔐 Admin-Login</h3>
            <input type="password" id="df-pw" class="df-inp" placeholder="Passwort">
            <p id="df-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button id="df-login-btn" class="df-btn-primary" style="flex:1;">Anmelden</button>
                <button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-modal').style.display='none'">Abbrechen</button>
            </div>
        </div>
    </div>
    <!-- Passwort-Modal -->
    <div id="df-pw-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
        <div class="df-modal-box">
            <h3 style="margin:0 0 0.75rem;font-size:1rem;">🔑 Passwort ändern</h3>
            <input type="password" id="df-old-pw" class="df-inp" placeholder="Altes Passwort" style="margin-bottom:0.5rem;">
            <input type="password" id="df-new-pw" class="df-inp" placeholder="Neues Passwort (min. 4 Zeichen)" style="margin-bottom:0.5rem;">
            <input type="password" id="df-new-pw2" class="df-inp" placeholder="Neues Passwort bestätigen">
            <p id="df-pw-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button onclick="dfChangePw()" class="df-btn-primary" style="flex:1;">Speichern</button>
                <button onclick="document.getElementById('df-pw-modal').style.display='none'" class="df-btn-ghost" style="flex:1;">Abbrechen</button>
            </div>
        </div>
    </div>
    <div id="df-toast" style="display:none;position:fixed;bottom:1.5rem;right:1.5rem;padding:0.75rem 1.25rem;border-radius:8px;z-index:99999;font-size:0.88rem;font-weight:600;color:#fff;"></div>

    <!-- Edit Modal -->
    <div id="df-edit-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99998;align-items:center;justify-content:center;">
        <div class="df-modal-box" style="max-width:500px;max-height:85vh;overflow-y:auto;">
            <h3 style="margin:0 0 0.75rem;font-size:1rem;">✏️ Eintrag bearbeiten</h3>
            <div id="df-edit-form"></div>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button class="df-btn-primary" style="flex:1;" onclick="dfSaveEdit()">💾 Speichern</button>
                <button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-edit-modal').style.display='none'">Abbrechen</button>
            </div>
        </div>
    </div>

    <script>
    (function(){
        var sec = document.currentScript ? document.currentScript.closest('section.vn-dataform') : document.querySelector('section.vn-dataform');
        if (!sec) return;

        var inBuilder = (function(){
            if (document.querySelector('html').classList.contains('is-builder')) return true;
            if (window.location.port === '409' || window.location.protocol === 'file:') return true;
            var all = document.querySelectorAll('link[href],script[src]');
            for (var i=0;i<all.length;i++){
                var h=all[i].getAttribute('href')||all[i].getAttribute('src')||'';
                if (h.indexOf('file:')===0||h.indexOf('AppData')!==-1||h.indexOf('Mobirise')!==-1) return true;
            }
            return false;
        })();
        if (inBuilder) return;

        // Live: Dummy verstecken
        var dummy = document.getElementById('df-dummy');
        if (dummy) dummy.style.display = 'none';

        // Loading-Div dynamisch einfügen
        var ld = document.createElement('div');
        ld.id = 'df-loading';
        ld.className = 'df-loading';
        ld.textContent = '⟳ Lade…';
        document.getElementById('df-results').before(ld);

        var GID      = sec.dataset.gid || 'form-1';
        var VIEWMODE = sec.dataset.viewmode || 'cards';
        var PERPAGE  = parseInt(sec.dataset.perpage) || 10;
        var SEARCH   = sec.dataset.showsearch === 'true' || sec.dataset.showsearch === '1';
        var IMGSIZE  = parseInt(sec.dataset.imgsize) || 120;
        var IMGPOS   = sec.dataset.imgpos || 'top';
        var isAdmin  = false;
        var fields   = [];
        var curPage  = 0;
        var editId   = 0;

        var ep = (function(){
            var links = document.querySelectorAll('link[href]');
            for (var i=0;i<links.length;i++){
                var h=links[i].getAttribute('href')||'';
                if (h.indexOf('http')===0 && h.indexOf('assets/')!==-1)
                    return h.split('assets/')[0]+'assets/vn-dataform/api.php';
            }
            return 'assets/vn-dataform/api.php';
        })();

        function api(payload, cb) {
            fetch(ep + '?action=' + payload.action + '&gid=' + encodeURIComponent(GID), {
                method:'POST', headers:{'Content-Type':'application/json'},
                body: JSON.stringify(payload)
            }).then(function(r){ return r.json(); }).then(cb).catch(function(e){ cb({success:false,error:String(e)}); });
        }

        function toast(msg, err) {
            var t = document.getElementById('df-toast');
            t.textContent = msg; t.style.display = 'block';
            t.style.background = err ? '#e74c3c' : '#1d9e75';
            setTimeout(function(){ t.style.display='none'; }, 3000);
        }

        function esc(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

        function fmtDate(val) {
            if (!val) return '';
            // YYYY-MM-DD → TT.MM.JJJJ
            var m = val.match(/^(\d{4})-(\d{2})-(\d{2})$/);
            if (m) return m[3] + '.' + m[2] + '.' + m[1];
            return val;
        }

        // ── Felder laden ──────────────────────────────────
        function loadFields(cb) {
            api({action:'get_fields'}, function(d){
                fields = d.fields || [];
                if (cb) cb();
            });
        }

      // ── Suchefeld ─────────────────────────────────────
    function initSearch() {

        var srch = document.getElementById('df-search');
        if (!srch) return;

        // Suchfeld immer zurücksetzen
        srch.value = "";
        srch.setAttribute("autocomplete", "off");
        srch.setAttribute("autocorrect", "off");
        srch.setAttribute("autocapitalize", "off");
        srch.setAttribute("spellcheck", "false");

        // Browser-Autofill zusätzlich verhindern
        setTimeout(function () {
            srch.value = "";
        }, 50);

        // Suche verstecken wenn Checkbox aus
        if (!SEARCH) {
            var wrap = document.getElementById('df-search-wrap');
            if (wrap) wrap.style.display = 'none';
            return;
        }

        srch.oninput = function () {
            curPage = 0;
            loadRecords();
        };
    }

        // ── Datensätze laden und anzeigen ─────────────────
        function loadRecords() {
            var search = (document.getElementById('df-search')||{value:''}).value;
            api({action:'get_records', search:search, page:parseInt(curPage), limit:parseInt(PERPAGE)}, function(d){
                renderResults(d.records||[], d.total||0);
            });
        }

        function renderResults(records, total) {
            console.log("Seite:", curPage, "Records:", records.length, "Total:", total);
            var ldEl = document.getElementById('df-loading');
            if (ldEl) ldEl.style.display = 'none';
            var div = document.getElementById('df-results');
            if (!records.length) {
                if (total > 0 && curPage > 0) {
                    // Seite existiert nicht mehr → zurück zu Seite 0
                    curPage = 0;
                    loadRecords();
                    return;
                }
                div.innerHTML = '<p class="df-hint">Keine Einträge gefunden.</p>';
                document.getElementById('df-pager').innerHTML = '';
                return;
            }
            var listFields = fields.filter(function(f){ return f.show_in_list == 1; });
            var html = '';

            if (VIEWMODE === 'table') {
                var tableFields = fields.length ? fields : listFields;
                html = '<div class="df-table-wrap"><table class="df-table"><thead><tr>';
                tableFields.forEach(function(f){ html += '<th>'+esc(f.label)+'</th>'; });
                if (isAdmin) html += '<th></th>';
                html += '</tr></thead><tbody>';
                records.forEach(function(r){
                    html += '<tr>';
                    tableFields.forEach(function(f){
                        var tdval = r.data[f.field_key]||'';
                        var tdhtml = f.type==='image' && tdval
                            ? '<img src="'+tdval+'" style="height:'+Math.round(IMGSIZE*0.5)+'px;max-width:120px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+tdval+'">' 
                            : (f.type==='date' ? fmtDate(tdval) : esc(tdval));
                        html += '<td>'+tdhtml+'</td>';
                    });
                    if (isAdmin) html += '<td><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></td>';
                    html += '</tr>';
                });
                html += '</tbody></table></div>';
            } else {
                html = '<div class="df-cards">';
                records.forEach(function(r){
                    var firstField = listFields[0];
                    var title = firstField ? esc(r.data[firstField.field_key]||'—') : '#'+r.id;
                    // Bild-Felder für diese Karte
                    var imgField = fields.find(function(f){ return f.type==='image' && r.data[f.field_key]; });
                    var imgUrl   = imgField ? r.data[imgField.field_key] : '';

                    html += '<div class="df-card">';

                    // Bild oben
                    if (imgUrl && IMGPOS==='top') {
                        html += '<div style="overflow:hidden;max-height:'+IMGSIZE+'px;cursor:zoom-in;" onclick="dfLightbox(this.dataset.lbsrc)" data-lbsrc="'+imgUrl+'">'
                            + '<img src="'+imgUrl+'" class="df-card-img" style="width:100%;height:'+IMGSIZE+'px;object-fit:cover;display:block;">'
                            + '</div>';
                    }

                    html += '<div class="df-card-head">'+title;
                    if (isAdmin) html += '<span style="float:right;"><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></span>';
                    html += '</div><div class="df-card-body">';
                    listFields.slice(1).forEach(function(f){
                        var val = r.data[f.field_key]||'';
                        if (!val) return;
                        var link = '';
                        if (f.type==='email') link='<a href="mailto:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='tel') link='<a href="tel:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='url') link='<a href="'+esc(val)+'" target="_blank" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='image') {
                            if (IMGPOS==='top') return; // schon oben gezeigt
                            link='<img src="'+val+'" style="max-width:100%;max-height:'+IMGSIZE+'px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+val+'">';
                        }
                        else if (f.type==='date') link=fmtDate(val);
                        else link=esc(val);
                        html += '<div class="df-row"><span class="df-lbl">'+esc(f.label)+'</span><span class="df-val">'+link+'</span></div>';
                    });
                    html += '</div></div>';
                });
                html += '</div>';
            }
    // ---------------- Pagination ----------------

    var pages = Math.ceil(total / PERPAGE);

    if (pages > 1) {

        var phtml = '<div class="df-pager"><div class="df-pager-inner">';

        // Erste
        phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage(0)')+'">« Erste</button>';

        // Zurück
        phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage('+(curPage-1)+')')+'">‹ Zurück</button>';

        // Seitenbereich
        var start = Math.max(0, curPage-2);
        var end   = Math.min(pages-1, curPage+2);

        if (start > 0) {
            phtml += '<button class="df-page-btn" onclick="dfGoPage(0)">1</button>';
            if (start > 1)
                phtml += '<span class="df-page-dots">…</span>';
        }

        for (var i=start;i<=end;i++) {
            phtml += '<button class="df-page-btn'+(i==curPage?' df-page-active':'')+'" onclick="dfGoPage('+i+')">'+(i+1)+'</button>';
        }

        if (end < pages-1) {

            if (end < pages-2)
                phtml += '<span class="df-page-dots">…</span>';

            phtml += '<button class="df-page-btn" onclick="dfGoPage('+(pages-1)+')">'+pages+'</button>';
        }

        // Weiter
        phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(curPage+1)+')')+'">Weiter ›</button>';

        // Letzte
        phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(pages-1)+')')+'">Letzte »</button>';

        phtml += '</div></div>';

        html += phtml;
    }
    console.log(html);
            div.innerHTML = html;

            // Lightbox-Delegation
            div.querySelectorAll('img.df-lb-img').forEach(function(img){
                img.style.cursor = 'zoom-in';
                img.onclick = function(){ dfLightbox(this.dataset.lbsrc); };
            });
        }

       window.dfGoPage = function(p){

        if (p < 0) p = 0;

        curPage = p;

        loadRecords();

        var el = document.getElementById("df-results");
        if(el){
            el.scrollIntoView({
                behavior:"smooth",
                block:"start"
            });
        }
    };

        // ── Admin: Datensatz bearbeiten ───────────────────
        window.dfEditRecord = function(id) {
            editId = id;
            api({action:'get_records', page:0, limit:9999}, function(d){
                var rec = (d.records||[]).find(function(r){ return r.id===id; });
                if (!rec) return;
                var html = '';
                fields.forEach(function(f){
                    var v = esc(rec.data[f.field_key]||'');
                    var einp;
                    if (f.type==='textarea') {
                        einp = '<textarea class="df-inp" id="dfe_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;">'+v+'</textarea>';
                    } else if (f.type==='image') {
                        einp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
                            + (v ? '<img src="'+v+'" style="max-height:60px;border-radius:4px;object-fit:cover;margin-bottom:4px;">' : '')
                            + '<input type="file" accept="image/*" class="df-inp" id="dfe_file_'+f.field_key+'" style="padding:4px;">'
                            + '<input type="hidden" id="dfe_'+f.field_key+'" value="'+v+'">'
                            + '</div>';
                    } else {
                        einp = '<input type="'+f.type+'" class="df-inp" id="dfe_'+f.field_key+'" value="'+v+'">';
                    }
                    html += '<label class="df-field-lbl">'+esc(f.label)+einp+'</label>';
                });
                document.getElementById('df-edit-form').innerHTML = html;
                document.getElementById('df-edit-modal').style.display = 'flex';
            });
        };

        window.dfSaveEdit = function() {
            uploadImages('dfe_', function(){
                var data = {};
                fields.forEach(function(f){
                    var el = document.getElementById('dfe_'+f.field_key);
                    if (el) data[f.field_key] = el.value;
                });
                api({action:'save_record', id:editId, data:data}, function(d){
                    if (d.success) {
                        toast(d.message||'✅ Gespeichert!');
                        document.getElementById('df-edit-modal').style.display='none';
                        loadRecords();
                        renderAdminList();
                    } else toast(d.error||'Fehler', true);
                });
            });
        };

        window.dfDeleteRecord = function(id) {
            if (!confirm('Eintrag löschen?')) return;
            api({action:'delete_record', id:id}, function(d){
                if (d.success) { toast(d.message||'Gelöscht!'); loadRecords(); renderAdminList(); }
                else toast(d.error||'Fehler', true);
            });
        };

        // ── Admin: Eintrags-Liste ─────────────────────────
        function renderAdminList() {
            api({action:'get_records', page:0, limit:999}, function(d){
                var records = d.records||[];
                var div = document.getElementById('df-admin-list');
                if (!records.length) { div.innerHTML='<p class="df-hint">Noch keine Einträge.</p>'; return; }
                var html = '<table class="df-table"><thead><tr>';
                fields.slice(0,3).forEach(function(f){ html+='<th>'+esc(f.label)+'</th>'; });
                html += '<th></th></tr></thead><tbody>';
                records.forEach(function(r){
                    html += '<tr data-id="'+r.id+'">';
                    fields.slice(0,3).forEach(function(f){ html+='<td>'+esc(r.data[f.field_key]||'')+'</td>'; });
                    html += '<td style="white-space:nowrap;">'
                        +'<button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> '
                        +'<button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button>'
                        +'</td></tr>';
                });
                html += '</tbody></table>';
                div.innerHTML = html;
                // Drag & Drop Zeilen
                var dragSrcRow = null;
                div.querySelectorAll('tbody tr').forEach(function(tr){
                    tr.draggable = true;
                    tr.style.cursor = 'grab';
                    tr.addEventListener('dragstart',function(){ dragSrcRow=tr; setTimeout(function(){tr.style.opacity='0.4';},0); });
                    tr.addEventListener('dragend',function(){ tr.style.opacity='1'; div.querySelectorAll('tbody tr').forEach(function(r){r.style.outline='';}); });
                    tr.addEventListener('dragover',function(e){ e.preventDefault(); tr.style.outline='2px solid #1d9e75'; });
                    tr.addEventListener('dragleave',function(){ tr.style.outline=''; });
                    tr.addEventListener('drop',function(e){
                        e.preventDefault(); tr.style.outline='';
                        if (!dragSrcRow || dragSrcRow===tr) return;
                        var tbody = tr.closest('tbody');
                        var rows = Array.from(tbody.querySelectorAll('tr'));
                        var si = rows.indexOf(dragSrcRow), ti = rows.indexOf(tr);
                        if (si < ti) tbody.insertBefore(dragSrcRow, tr.nextSibling);
                        else tbody.insertBefore(dragSrcRow, tr);
                        dragSrcRow = null;
                        // Neue Reihenfolge speichern
                        var newOrder = Array.from(tbody.querySelectorAll('tr')).map(function(r){
                            return parseInt(r.dataset.id);
                        }).filter(function(id){ return !isNaN(id); });
                        if (newOrder.length) {
                            api({action:'reorder', ids:newOrder}, function(d){
                                if (d.success) toast('✅ Reihenfolge gespeichert!');
                            });
                        }
                    });
                });
            });
        }

        // ── Admin: Felder ─────────────────────────────────
        window.dfTab = function(tab) {
            ['records','fields','new'].forEach(function(t){
                document.getElementById('df-tab-'+t).style.display = t===tab ? 'block' : 'none';
                var btn = document.querySelector('[data-tab="'+t+'"]');
                if (btn) btn.classList.toggle('df-tab-active', t===tab);
            });
            if (tab==='records') renderAdminList();
            if (tab==='fields')  renderFieldEditor();
            if (tab==='new')     renderNewForm();
        };

        function renderFieldEditor() {
            var html = '';
            fields.forEach(function(f,i){
                html += '<div class="df-field-row" data-idx="'+i+'">'
                    + '<span class="df-drag-handle">⠿</span>'
                    + '<input type="text" class="df-inp df-fl-name" value="'+esc(f.label)+'" placeholder="Feldname">'
                    + '<select class="df-inp df-fl-type">'
                    + ['text','email','tel','url','number','date','textarea','image'].map(function(t){
                        return '<option value="'+t+'"'+(f.type===t?' selected':'')+'>'+t+'</option>';
                      }).join('')
                    + '</select>'
                    + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
                    + '<input type="checkbox" class="df-fl-req"'+(f.required==1?' checked':'')+'> Pflicht</label>'
                    + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
                    + '<input type="checkbox" class="df-fl-show"'+(f.show_in_list==1?' checked':'')+'> Anzeigen</label>'
                    + '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>'
                    + '</div>';
            });
            document.getElementById('df-field-list').innerHTML = html;
        }

        window.dfAddField = function() {
            var li = document.getElementById('df-field-list');
            var div = document.createElement('div');
            div.className = 'df-field-row';
            div.innerHTML = '<span class="df-drag-handle">⠿</span>'
                + '<input type="text" class="df-inp df-fl-name" placeholder="Feldname">'
                + '<select class="df-inp df-fl-type">'
                + ['text','email','tel','url','number','date','textarea','image'].map(function(t){
                    return '<option value="'+t+'">'+t+'</option>';
                  }).join('')
                + '</select>'
                + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-req"> Pflicht</label>'
                + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-show" checked> Anzeigen</label>'
                + '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>';
            li.appendChild(div);
        };

        window.dfSaveFields = function() {
            var rows = document.querySelectorAll('#df-field-list .df-field-row');
            var newFields = [];
            rows.forEach(function(row){
                var name = row.querySelector('.df-fl-name').value.trim();
                if (!name) return;
                newFields.push({
                    label:       name,
                    type:        row.querySelector('.df-fl-type').value,
                    required:    row.querySelector('.df-fl-req').checked ? 1 : 0,
                    show_in_list:row.querySelector('.df-fl-show').checked ? 1 : 0,
                });
            });
            api({action:'save_fields', fields:newFields}, function(d){
                if (d.success) {
                    toast(d.message||'✅ Felder gespeichert!');
                    loadFields(function(){ loadRecords(); renderNewForm(); });
                } else toast(d.error||'Fehler', true);
            });
        };

        // ── Admin: Neuer Eintrag ──────────────────────────
        function renderNewForm() {
            editId = 0;
            var html = '';
            fields.forEach(function(f){
                var inp;
                if (f.type==='textarea') {
                    inp = '<textarea class="df-inp" id="dfn_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;"></textarea>';
                } else if (f.type==='image') {
                    inp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
                        + '<input type="file" accept="image/*" class="df-inp" id="dfn_file_'+f.field_key+'" style="padding:4px;">'
                        + '<input type="hidden" id="dfn_'+f.field_key+'">'
                        + '<img id="dfn_prev_'+f.field_key+'" src style="display:none;max-height:80px;border-radius:4px;margin-top:4px;object-fit:cover;">'
                        + '</div>';
                } else {
                    inp = '<input type="'+f.type+'" class="df-inp" id="dfn_'+f.field_key+'">';
                }
                html += '<label class="df-field-lbl">'+esc(f.label)+(f.required==1?' <span style="color:#e74c3c;">*</span>':'')+inp+'</label>';
            });
            if (!html) html = '<p class="df-hint">Zuerst Felder unter 🔧 Felder anlegen!</p>';
            document.getElementById('df-new-form').innerHTML = html;
        }

        window.dfSaveRecord = function() {
            uploadImages('dfn_', function(){
                var data = {};
                fields.forEach(function(f){
                    var el = document.getElementById('dfn_'+f.field_key);
                    if (el) data[f.field_key] = el.value;
                });
                api({action:'save_record', id:0, data:data}, function(d){
                    if (d.success) {
                        toast(d.message||'✅ Gespeichert!');
                        renderNewForm();
                        loadRecords();
                    } else toast(d.error||'Fehler', true);
                });
            });
        };

        // Alle Bild-Felder vor dem Speichern hochladen
        function uploadImages(prefix, cb) {
            var imgFields = fields.filter(function(f){ return f.type==='image'; });
            var pending = 0;
            if (!imgFields.length) { cb(); return; }
            imgFields.forEach(function(f){
                var fileInp = document.getElementById(prefix+'file_'+f.field_key);
                if (!fileInp || !fileInp.files || !fileInp.files[0]) return;
                pending++;
                var fd = new FormData();
                fd.append('img', fileInp.files[0]);
                fetch(ep + '?action=upload_image&gid=' + encodeURIComponent(GID), {method:'POST', body:fd})
                    .then(function(r){ return r.json(); })
                    .then(function(d){
                        if (d.success) {
                            var hidden = document.getElementById(prefix+f.field_key);
                            if (hidden) hidden.value = d.url;
                        }
                        pending--;
                        if (pending===0) cb();
                    })
                    .catch(function(){ pending--; if(pending===0) cb(); });
            });
            if (pending===0) cb();
        }

        window.dfLightbox = function(url) {
            var lb  = document.getElementById('df-lightbox');
            var img = document.getElementById('df-lightbox-img');
            if (!lb || !img) return;
            img.src = url;
            lb.style.display = 'flex';
        };
        document.addEventListener('keydown', function(e){
            if (e.key==='Escape') {
                var lb = document.getElementById('df-lightbox');
                if (lb) lb.style.display = 'none';
            }
        });

        window.dfExport = function(e) {
            e.preventDefault();

            var exportUrl = ep
                + '?action=export_csv'
                + '&gid=' + encodeURIComponent(GID)
                + '&_=' + Date.now();

            window.location.href = exportUrl;
        };        

        window.dfChangePw = function() {
            var old  = document.getElementById('df-old-pw').value;
            var nw   = document.getElementById('df-new-pw').value;
            var nw2  = document.getElementById('df-new-pw2').value;
            var err  = document.getElementById('df-pw-err');
            if (nw !== nw2) { err.textContent='Passwörter stimmen nicht überein.'; err.style.display='block'; return; }
            api({action:'change_password', old_password:old, new_password:nw}, function(d){
                if (d.success) {
                    toast(d.message||'✅ Passwort geändert!');
                    document.getElementById('df-pw-modal').style.display='none';
                    ['df-old-pw','df-new-pw','df-new-pw2'].forEach(function(id){
                        var el=document.getElementById(id); if(el) el.value='';
                    });
                } else {
                    err.textContent = d.error||'Fehler';
                    err.style.display = 'block';
                }
            });
        };

        window.dfLogout = function() {
            api({action:'logout'}, function(){
                isAdmin=false;
                document.getElementById('df-admin').style.display='none';
                loadRecords();
            });
        };

        // ── Login ─────────────────────────────────────────
        function openLogin() {
            api({action:'check'}, function(d){
                if (d.loggedIn) { isAdmin=true; showAdmin(); return; }
                var modal = document.getElementById('df-modal');
                var title = document.getElementById('df-modal-title');
                var err   = document.getElementById('df-err');
                var pw    = document.getElementById('df-pw');
                if (!modal) return;
                if (title) title.textContent = d.firstRun ? '⚙️ Erst-Einrichtung' : '🔐 Admin-Login';
                if (err) err.style.display = 'none';
                if (pw) pw.value = '';
                modal.style.display = 'flex';
                setTimeout(function(){ if(pw) pw.focus(); }, 100);
            });
        }
        
        /* Login auch über externen Textlink aufrufbar machen */
        window.dfOpenLogin = openLogin;

        function showAdmin() {
            document.getElementById('df-admin').style.display = 'block';
            dfTab('records');
        }

        document.getElementById('df-login-btn').onclick = function(){
            var pw  = document.getElementById('df-pw');
            var err = document.getElementById('df-err');
            api({action:'login', password: pw ? pw.value : ''}, function(d){
                if (d.success) {
                    isAdmin = true;
                    document.getElementById('df-modal').style.display = 'none';
                    showAdmin();
                } else {
                    if (err) { err.textContent = d.error||'Falsches Passwort.'; err.style.display='block'; }
                }
            });
        };

        var pwEl = document.getElementById('df-pw');
        if (pwEl) pwEl.addEventListener('keypress', function(e){ if(e.key==='Enter') document.getElementById('df-login-btn').click(); });

        document.addEventListener('dblclick', function(e){ if(e.ctrlKey){ e.preventDefault(); openLogin(); }});
        document.addEventListener('keydown',  function(e){ if(e.ctrlKey&&e.shiftKey&&(e.key==='A'||e.key==='a')){ e.preventDefault(); openLogin(); }});

        // Handy-Trigger
        var mt = sec.dataset.mobiletrigger || 'shake';
        var isTouch = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
        if (mt==='button' && isTouch) {
            var fab = document.createElement('button');
            fab.innerHTML='⚙️';
            fab.style.cssText='position:fixed;bottom:1.5rem;left:1rem;width:44px;height:44px;border-radius:50%;background:#1d9e75;border:none;color:#fff;font-size:1.2rem;cursor:pointer;z-index:9998;';
            fab.onclick=openLogin;
            document.body.appendChild(fab);
        } else if (mt==='tap4') {
            var _tc=0,_tt=null;
            sec.addEventListener('touchend',function(){
                _tc++;clearTimeout(_tt);
                _tt=setTimeout(function(){_tc=0;},1500);
                if(_tc>=4){_tc=0;openLogin();}
            });
        } else if (mt==='shake') {
            var _sx=0,_sy=0,_sz=0,_st=0;
            if (window.DeviceMotionEvent) {
                window.addEventListener('devicemotion',function(e){
                    var a=e.accelerationIncludingGravity;if(!a)return;
                    var dv=Math.abs((a.x||0)-_sx)+Math.abs((a.y||0)-_sy)+Math.abs((a.z||0)-_sz);
                    _sx=a.x||0;_sy=a.y||0;_sz=a.z||0;
                    if(dv>25){var n=Date.now();if(n-_st>1500){_st=n;openLogin();}}
                });
            }
        }

        // Start
        loadFields(function(){
            initSearch();
            loadRecords();
        });
    })();
    </script>
    
    <!-- Lightbox -->
    <div id="df-lightbox" style="display:none;position:fixed;inset:0;background:#000000ee;z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;" onclick="document.getElementById('df-lightbox').style.display='none'">
        <img id="df-lightbox-img" src style="max-width:92%;max-height:92%;border-radius:10px;box-shadow:0 0 40px #000;object-fit:contain;">
    </div>

</section>
Antworten

Wer ist online?

Mitglieder in diesem Forum: Amazon [Bot] und 3 Gäste