OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

team_show.php

41 KB
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<style>
a, a:hover { text-decoration: none; }
.vault-wrap { display:flex; height:calc(100vh - 60px); overflow:hidden; flex-direction:column; }
/* ---- Redesign-Hilfsklassen (KPI, Toolbar, Tabelle) ---- */
.v-toolbar { display:flex; align-items:center; gap:0.75rem; padding:0.55rem 1.25rem;
    border-bottom:1px solid var(--gray-200); background:var(--color-bg); flex-shrink:0; }
.v-toolbar-title { font-size:0.85rem; font-weight:600; color:var(--gray-700); flex:1; display:flex; align-items:center; gap:0.4rem; }
.v-toolbar-tools { display:flex; align-items:center; gap:0.55rem; flex-shrink:0; }
.v-head-search { position:relative; display:inline-block; }
.v-head-search > i.fa-search,
.v-head-search > .emoji-icon:first-child { position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--gray-400); font-size:0.75rem; line-height:1; pointer-events:none; }
.v-head-search input { width:200px; padding-left:1.7rem; }
.v-head-search input::-webkit-search-cancel-button { -webkit-appearance:none; appearance:none; }
.v-table-scroll { flex:1; overflow-y:auto; padding:0; }
.v-table-scroll .table-wrapper { border:0; border-radius:0; margin:0; box-shadow:none; }
.v-table-scroll .data-table { margin:0; }
/* Import/Export-Aufklappmenü (JS-frei, robust) */
.v-io-menu { position:relative; display:inline-block; }
.v-io-menu > summary { list-style:none; cursor:pointer; }
.v-io-menu > summary::-webkit-details-marker { display:none; }
.v-io-list { position:absolute; right:0; top:calc(100% + 4px); z-index:1100; min-width:200px;
    background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.45rem; box-shadow:0 10px 30px rgba(0,0,0,0.14); padding:0.25rem 0; }
.v-io-list a { display:flex; align-items:center; gap:0.55rem; padding:0.45rem 0.9rem; color:var(--gray-700); font-size:0.85rem; white-space:nowrap; text-decoration:none; }
.v-io-list a:hover { background:var(--gray-100); color:var(--gray-900); }
.content-header, .content-header-actions { overflow:visible; }
.v-table-scroll .data-table thead th { position:sticky; top:0; z-index:3; background:var(--slate-50); box-shadow:inset 0 -1px 0 var(--gray-200); }
.v-cell { display:inline-flex; align-items:center; gap:0.35rem; max-width:100%; }
.v-cell-val { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.v-inline-copy { border:none; background:none; color:var(--color-gray-400); cursor:pointer; padding:0 0.15rem; font-size:0.78rem; opacity:0; transition:opacity 0.1s, color 0.1s; }
tr:hover .v-inline-copy { opacity:1; }
.v-inline-copy:hover { color:var(--blue-600); }
.v-inline-copy.copied { color:var(--green-600); opacity:1; }
.v-body.v-sidebar-collapsed .vault-sidebar { display:none; }
.v-sidebar-toggle { border:1px solid var(--gray-200); background:var(--gray-50); color:var(--gray-500); border-radius:0.35rem; width:30px; height:26px; cursor:pointer; flex-shrink:0; }
.v-sidebar-toggle:hover { background:var(--slate-100); color:var(--gray-900); }
/* Mobile: Karten-Liste statt Tabelle */
@media(max-width:640px){
    .v-head-search input { width:130px; }
    .v-table-scroll { overflow:visible; padding:0.65rem 0.75rem; }
    .v-table-scroll .data-table thead th { position:static; box-shadow:none; }
    .v-table-scroll .table-wrapper { border:none; background:none; box-shadow:none; overflow:visible; }
    /* app.css erzwingt mobil min-width:640px für .data-table – Karten brauchen volle Breite */
    #vTable, #vTable tbody { display:block; width:100%; min-width:0 !important; }
    #vTable thead { display:none; }
    #vTable tr.v-row { display:flex; flex-wrap:wrap; align-items:center; gap:0.35rem 0.6rem;
        background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.65rem; margin-bottom:0.5rem;
        padding:0.65rem 0.75rem; box-shadow:0 1px 2px rgba(0,0,0,0.04); }
    #vTable td { display:block; border:none; padding:0; background:none; }
    #vTable td:first-child { flex:1 1 100%; min-width:0; }
    #vTable td:nth-child(3), #vTable td:nth-child(4) { display:none; }
    #vTable td:nth-child(2) { flex:1 1 auto; min-width:0; font-size:0.8rem; }
    #vTable td.v-actions-cell { margin-left:auto; display:flex; align-items:center; gap:0.4rem; }
    #vTable td.v-actions-cell .btn { width:36px; height:34px; padding:0; display:inline-flex;
        align-items:center; justify-content:center; font-size:0.85rem; }
    .v-inline-copy { opacity:1; padding:0.35rem 0.45rem; color:var(--gray-400); }
}
.v-load-more { padding:0.7rem; }
.v-ttitle { display:flex; align-items:center; gap:0.55rem; min-width:0; }
.v-entry-title { font-weight:600; font-size:0.875rem; color:var(--gray-900);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.v-tsub { color:var(--gray-500); }
.v-entry-badge { display:inline-block; font-size:0.6rem; font-weight:700; padding:0.1rem 0.3rem; border-radius:3px; }
.v-badge-totp { background:var(--amber-100); color:var(--amber-800); }
.v-badge-team { background:var(--violet-100); color:var(--violet-700); }
.data-table.vlt-dense td, .data-table.vlt-dense th { padding-top:0.28rem; padding-bottom:0.28rem; }
.data-table td.v-actions-cell { white-space:nowrap; text-align:right; }
.vlt-hidden-row { display:none !important; }
.v-load-more { text-align:center; padding:0.6rem 0 0.2rem; }
.vault-sidebar { width:220px; min-width:180px; background:var(--slate-50); border-right:1px solid var(--gray-200);
    display:flex; flex-direction:column; overflow-y:auto; flex-shrink:0; }
.vault-sidebar-header { padding:1rem 1rem 0.5rem; font-size:0.7rem; font-weight:700;
    text-transform:uppercase; letter-spacing:0.07em; color:var(--gray-400); }
.vault-sidebar-item { display:flex; align-items:center; gap:0.5rem; padding:0.45rem 1rem;
    font-size:0.875rem; color:var(--gray-700); text-decoration:none; cursor:pointer;
    border:none; background:none; width:100%; text-align:left; }
.vault-sidebar-item:hover { background:var(--gray-200); }
.vault-sidebar-item.active { background:var(--blue-100); color:var(--blue-700); font-weight:600; }
.vault-sidebar-icon { width:16px; height:16px; flex-shrink:0; color:var(--gray-400); }
.vault-sidebar-add { font-size:0.78rem; color:var(--gray-500); padding:0.25rem 1rem 0.75rem;
    display:flex; align-items:center; gap:0.35rem; cursor:pointer; text-decoration:none; }
.vault-sidebar-add:hover { color:var(--blue-600); }
.vault-sidebar-divider { height:1px; background:var(--gray-200); margin:0.5rem 0; }
.vault-main { flex:1; display:flex; flex-direction:column; overflow:hidden; }
.vault-toolbar { display:flex; align-items:center; gap:0.75rem; padding:0.875rem 1.25rem;
    border-bottom:1px solid var(--gray-200); background:var(--color-bg); flex-wrap:wrap; }
.vault-entries { flex:1; overflow-y:auto; padding:1rem 1.25rem; }
.vault-entry-card { display:flex; align-items:center; gap:0.875rem; padding:0.75rem 1rem;
    background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.625rem; margin-bottom:0.5rem;
    cursor:pointer; transition:box-shadow 0.15s; }
.vault-entry-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.07); }
.vault-favicon { width:28px; height:28px; border-radius:6px; background:var(--gray-100);
    display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; }
.vault-favicon img { width:20px; height:20px; object-fit:contain; }
.vault-favicon-placeholder { font-size:12px; color:var(--gray-400); font-weight:700; }
.vault-entry-info { flex:1; min-width:0; }
.vault-entry-title { font-size:0.9rem; font-weight:600; color:var(--gray-900);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vault-entry-sub { font-size:0.78rem; color:var(--gray-400); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vault-entry-actions { display:flex; gap:0.35rem; }
/* Modals: Basis aus app.css (.modal-overlay/.modal-card); hier nur Zustands-Toggle und Breite */
.vault-modal-overlay { display:none; }
.vault-modal-overlay.open { display:flex; }
.vault-modal-overlay .modal-card { max-width:480px; }
.folder-item-wrap { display:flex; align-items:center; width:100%; }
.folder-item-link { flex:1; display:flex; align-items:center; gap:0.5rem; padding:0.45rem 0.5rem 0.45rem 1rem;
    font-size:0.875rem; color:var(--gray-700); text-decoration:none; }
.folder-item-link:hover { background:var(--gray-200); }
.folder-item-link.active { background:var(--blue-100); color:var(--blue-700); }
.folder-actions { display:none; gap:0.1rem; padding-right:0.4rem; flex-shrink:0; }
.vault-sidebar .folder-item-wrap:hover .folder-actions { display:flex; }
.folder-action-btn { width:22px; height:22px; border:none; background:none; cursor:pointer;
    color:var(--gray-400); font-size:11px; border-radius:3px; display:flex; align-items:center; justify-content:center; }
.folder-action-btn:hover { background:var(--gray-200); color:var(--gray-700); }
.vault-mobile-row { display:none; }
@media(max-width:768px) {
    .vault-wrap { flex-direction:column; height:auto; min-height:calc(100vh - 60px); }
    .vault-sidebar { display:none; }
    .vault-main { height:auto; }
    .vault-entries { overflow-y:visible; }
    .vault-mobile-row { display:flex; align-items:center; gap:0.4rem; overflow-x:auto;
        padding:0.5rem 0.875rem; background:var(--color-bg); border-bottom:1px solid var(--gray-200);
        flex-shrink:0; scrollbar-width:none; }
    .vault-mobile-row::-webkit-scrollbar { display:none; }
    .vault-mobile-pill { display:inline-flex; align-items:center; gap:0.3rem; white-space:nowrap;
        padding:0.3rem 0.7rem; border-radius:9999px; font-size:0.78rem; font-weight:600;
        border:1px solid var(--gray-200); background:var(--color-bg); color:var(--gray-700); text-decoration:none; flex-shrink:0; }
    .vault-mobile-pill.active { background:var(--color-primary-deep); color:var(--color-on-accent); border-color:var(--color-primary-deep); }
}
</style>
<script<?= $this->scriptNonce() ?>>
</script>

<?php
$moveTeams = [];
foreach ($all_teams ?? [] as $t) {
    $tid = (int)$t['id'];
    if (!isset($all_tvks[$tid])) continue;
    $tfolders = [];
    foreach ($all_team_folders[$tid] ?? [] as $f) {
        $tfolders[] = ['id' => (int)$f['id'], 'name' => $f['path_label'] ?? $f['name']];
    }
    $moveTeams[] = ['id' => $tid, 'name' => $t['name'], 'folders' => $tfolders];
}
$personalFoldersJson = [];
foreach ($personal_folders ?? [] as $f) {
    $personalFoldersJson[] = ['id' => (int)$f['id'], 'name' => $f['path_label'] ?? $f['name']];
}
?>

<div class="vault-wrap">
    <!-- Content Header -->
    <div class="content-header" style="flex-shrink:0;">
        <div>
            <h1><?= $this->e($team['name']) ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item"><a href="/vault"><?= $this->te('Vault') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Team-Tresor') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <div class="v-head-search">
                <i class="fas fa-search"></i>
                <input type="search" id="vSearch" class="form-control form-control-sm" placeholder="<?= $this->te('Filtern…') ?>" autocomplete="off" data-act-input="vaultApplyTable">
            </div>
            <a href="/vault" class="btn btn-default btn-sm"><i class="fas fa-arrow-left"></i> <?= $this->te('Mein Tresor') ?></a>
            <?php if ($can_write ?? true): ?>
            <a href="/vault/teams/<?= (int)$team['id'] ?>/entries/create<?= $active_folder_id ? '?folder=' . $active_folder_id : '' ?>" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> <?= $this->te('Neuer Eintrag') ?></a>
            <?php endif; ?>
            <details class="v-io-menu">
                <summary class="btn btn-default btn-sm"><i class="fas fa-exchange-alt"></i> Import/Export <i class="fas fa-caret-down"></i></summary>
                <div class="v-io-list">
                    <a href="/vault/teams/<?= (int)$team['id'] ?>/import"><i class="fas fa-file-import"></i> <?= $this->te('Importieren (CSV/KDBX)') ?></a>
                    <a href="/vault/teams/<?= (int)$team['id'] ?>/export"><i class="fas fa-file-export"></i> <?= $this->te('Exportieren (CSV)') ?></a>
                </div>
            </details>
        </div>
    </div>

    <div class="vault-mobile-row">
        <a href="/vault" class="vault-mobile-pill"><?= $this->te('← Tresor') ?></a>
        <a href="/vault/teams/<?= (int)$team['id'] ?>"
           class="vault-mobile-pill <?= $active_folder_id === null ? 'active' : '' ?>"><?= $this->te('Alle') ?></a>
        <?php foreach ($folders as $folder): ?>
            <a href="/vault/teams/<?= (int)$team['id'] ?>?folder=<?= (int)$folder['id'] ?>"
               class="vault-mobile-pill <?= $active_folder_id === (int)$folder['id'] ? 'active' : '' ?>">
                <?= $this->e($folder['name']) ?>
            </a>
        <?php endforeach; ?>
        <?php if ($can_write ?? true): ?>
        <a href="#" class="vault-mobile-pill"
           data-open="folderModal"><?= $this->te('+ Ordner') ?></a>
        <?php endif; ?>
    </div>

    <div class="v-body" style="display:flex;flex:1;overflow:hidden;">
    <aside class="vault-sidebar">
        <a href="/vault" class="vault-sidebar-item" style="border-bottom:1px solid var(--gray-200);">
            <svg class="vault-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
            <?= $this->te('Mein Tresor') ?>
        </a>
        <div class="vault-sidebar-header"><?= $this->e($team['name']) ?></div>
        <a href="/vault/teams/<?= (int)$team['id'] ?>"
           class="vault-sidebar-item <?= $active_folder_id === null ? 'active' : '' ?>">
            <svg class="vault-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/>
            </svg>
            <?= $this->te('Alle Einträge') ?>
        </a>
        <?php foreach ($folders as $folder): ?>
        <div class="folder-item-wrap">
            <a href="/vault/teams/<?= (int)$team['id'] ?>?folder=<?= (int)$folder['id'] ?>"
               class="folder-item-link <?= $active_folder_id === (int)$folder['id'] ? 'active' : '' ?>"
               style="padding-left:<?= 1 + 0.9 * (int)($folder['depth'] ?? 0) ?>rem;"
               title="<?= $this->e($folder['path_label'] ?? $folder['name']) ?>">
                <svg class="vault-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
                </svg>
                <?= $this->e($folder['name']) ?>
            </a>
            <?php if ($can_write ?? true): ?>
            <div class="folder-actions">
                <button class="folder-action-btn" title="<?= $this->te('Umbenennen') ?>"
                        data-act="openFolderRename" data-args='[<?= (int)$folder['id'] ?>,<?= $this->e(json_encode($folder['name'])) ?>]'>&#9998;</button>
                <form method="POST" action="/vault/teams/<?= (int)$team['id'] ?>/folders/<?= (int)$folder['id'] ?>/delete"
                      data-confirm="Ordner löschen? Einträge bleiben erhalten." style="margin:0;">
                    <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                    <button type="submit" class="folder-action-btn" title="<?= $this->te('Löschen') ?>" style="color:var(--red-500);">&#128465;</button>
                </form>
            </div>
            <?php endif; ?>
        </div>
        <?php endforeach; ?>
        <?php if ($can_write ?? true): ?>
        <a href="#" class="vault-sidebar-add"
           data-open="folderModal">
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
            <?= $this->te('Ordner anlegen') ?>
        </a>
        <?php endif; ?>
        <div class="vault-sidebar-divider"></div>
        <div class="vault-sidebar-header"><?= $this->t('Mitglieder') ?> (<?= count($members) ?>)</div>
        <?php foreach ($members as $member): ?>
            <div class="vault-sidebar-item" style="cursor:default;">
                <svg class="vault-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
                </svg>
                <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= $this->e($member['display_name']) ?></span>
                <?php if ($member['role'] === 'admin'): ?>
                    <span style="font-size:0.62rem;background:var(--blue-100);color:var(--blue-800);padding:0.1rem 0.3rem;border-radius:3px;">A</span>
                <?php endif; ?>
            </div>
        <?php endforeach; ?>
    </aside>

    <div class="vault-main">
        <div class="v-toolbar">
            <button type="button" class="v-sidebar-toggle" title="<?= $this->te('Seitenleiste ein-/ausblenden') ?>" data-act="vaultToggleSidebar"><i class="fas fa-bars"></i></button>
            <span class="v-toolbar-title">
                <i class="fas fa-users" style="color:var(--gray-400);"></i> <?= $this->e($team['name']) ?>
                <?php if (!($can_write ?? true)): ?>
                    <span class="status-pill status-pill--info" style="margin-left:0.4rem;"><?= $this->te('Nur Lesen') ?></span>
                <?php endif; ?>
                <span id="vCount" style="font-weight:400;color:var(--gray-500);margin-left:0.3rem;">(<?= count($entries) ?>)</span>
            </span>
            <div class="v-toolbar-tools">
                <label class="mb-0" style="display:inline-flex;align-items:center;gap:0.3rem;font-size:0.78rem;white-space:nowrap;color:var(--gray-500);">
                    <input type="checkbox" id="vTotpOnly" data-act-change="vaultApplyTable"> <?= $this->te('nur 2FA') ?>
                </label>
                <select id="vSort" class="form-control form-control-sm" style="width:auto;" title="<?= $this->te('Sortierung') ?>" data-act-change="vaultApplyTable">
                    <option value="title-asc"><?= $this->te('Titel A–Z') ?></option>
                    <option value="title-desc"><?= $this->te('Titel Z–A') ?></option>
                    <option value="user-asc"><?= $this->te('Benutzer A–Z') ?></option>
                </select>
                <button type="button" id="vDensity" class="btn btn-default btn-xs" title="<?= $this->te('Kompakte Ansicht umschalten') ?>" data-act="vaultToggleDensity"><i class="fas fa-compress"></i></button>
            </div>
        </div>

        <div class="v-table-scroll">
            <?php if (empty($entries)): ?>
                <div class="empty-state">
                    <div class="empty-icon"><i class="fas fa-lock"></i></div>
                    <div><?= $this->te('Keine Einträge in diesem Team-Tresor') ?></div>
                    <?php if ($can_write ?? true): ?>
                    <a href="/vault/teams/<?= (int)$team['id'] ?>/entries/create" class="btn btn-primary btn-sm" style="margin-top:0.6rem;"><i class="fas fa-plus"></i> <?= $this->te('Ersten Eintrag anlegen') ?></a>
                    <?php endif; ?>
                </div>
            <?php else: ?>
                <div class="table-wrapper">
                    <table class="data-table" id="vTable">
                        <thead>
                            <tr><th scope="col"><?= $this->te('Titel') ?></th><th scope="col"><?= $this->te('Benutzername') ?></th><th scope="col"><?= $this->te('Adresse') ?></th><th scope="col"><?= $this->te('Geändert') ?></th><th scope="col" class="text-right"><?= $this->te('Aktionen') ?></th></tr>
                        </thead>
                        <tbody id="vTbody">
                        <?php foreach ($entries as $entry):
                            $showUrl   = '/vault/teams/' . (int)$team['id'] . '/entries/' . (int)$entry['id'];
                            $editUrl   = $showUrl . '/edit';
                            $deleteUrl = $showUrl . '/delete';
                            $_avTitle  = $entry['title'] ?? '?';
                            $_avHue    = crc32((string)$_avTitle) % 360;
                            $_user     = (string)($entry['username'] ?? '');
                            $_url      = (string)($entry['url'] ?? '');
                            $_favDom   = strtolower(trim((string)($entry['favicon_domain'] ?? '')));
                            $_favUrl   = ($favicon_urls ?? [])[$_favDom] ?? null;
                        ?>
                            <tr class="clickable v-row"
                                data-title="<?= $this->e(mb_strtolower((string)$_avTitle)) ?>"
                                data-user="<?= $this->e(mb_strtolower($_user)) ?>"
                                data-totp="<?= !empty($entry['totp']) ? '1' : '0' ?>"
                                data-href="<?= $this->e($showUrl) ?>">
                                <td>
                                    <div class="v-ttitle">
                                        <?php if ($_favDom !== ''): ?>
                                        <span class="vault-favicon">
                                            <img src="/vault/entries/<?= (int)$entry['id'] ?>/favicon" alt="" loading="lazy"
                                                 data-favicon-fallback>
                                            <span class="vault-favicon-placeholder" hidden data-bg="hsl(<?= $_avHue ?>,52%,90%)"
                                                  style="color:hsl(<?= $_avHue ?>,55%,38%);"><?= $this->e(mb_strtoupper(mb_substr((string)$_avTitle, 0, 1))) ?></span>
                                        </span>
                                        <?php else: ?>
                                        <span class="vault-favicon" style="background:hsl(<?= $_avHue ?>,52%,90%);">
                                            <span class="vault-favicon-placeholder" style="color:hsl(<?= $_avHue ?>,55%,38%);"><?= $this->e(mb_strtoupper(mb_substr((string)$_avTitle, 0, 1))) ?></span>
                                        </span>
                                        <?php endif; ?>
                                        <span class="v-entry-title"><?= $this->e($entry['title']) ?></span>
                                        <?php if (!empty($entry['totp'])): ?>
                                            <span class="v-entry-badge v-badge-totp">2FA</span>
                                        <?php endif; ?>
                                    </div>
                                </td>
                                <td class="v-tsub"><?php if ($_user !== ''): ?><span class="v-cell"><span class="v-cell-val"><?= $this->e($_user) ?></span><button type="button" class="v-inline-copy" data-copy="<?= $this->e($_user) ?>" title="<?= $this->te('Benutzername kopieren') ?>" ><i class="fas fa-copy"></i></button></span><?php else: ?>—<?php endif; ?></td>
                                <td class="v-tsub" style="max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="<?= $this->e($_url) ?>"><?= $_url !== '' ? $this->e($_url) : '—' ?></td>
                                <td class="v-tsub" style="white-space:nowrap;"><?= !empty($entry['updated_at']) ? date('d.m.Y', strtotime((string)$entry['updated_at'])) : '—' ?></td>
                                <td class="v-actions-cell" >
                                    <?php if ($can_write ?? true): ?>
                                    <button type="button" class="btn btn-xs btn-default" title="<?= $this->te('Aktionen') ?>"
                                            data-show="<?= $this->e($showUrl) ?>" data-edit="<?= $this->e($editUrl) ?>"
                                            data-del="<?= $this->e($deleteUrl) ?>" data-id="<?= (int)$entry['id'] ?>"
                                            data-folder="<?= isset($entry['folder_id']) && $entry['folder_id'] ? (int)$entry['folder_id'] : '' ?>"
                                            data-act="vaultRowMenu"><i class="fas fa-ellipsis-v"></i></button>
                                    <?php else: ?>
                                    <a href="<?= $showUrl ?>" class="btn btn-xs btn-default" title="<?= $this->te('Anzeigen') ?>"><i class="fas fa-eye"></i></a>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>
                <div class="v-load-more" id="vLoadMore" style="display:none;">
                    <button type="button" class="btn btn-default btn-sm" data-act="vaultShowMore"><?= $this->te('Mehr anzeigen') ?></button>
                </div>
            <?php endif; ?>
        </div>
    </div>
    </div>
</div>

<!-- Ordner anlegen Modal -->
<div class="modal-overlay vault-modal-overlay" id="folderModal" data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Ordner anlegen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-close="folderModal">&times;</button>
        </div>
        <form method="POST" action="/vault/teams/<?= (int)$team['id'] ?>/folders">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <div class="modal-body">
            <div class="form-group"><input type="text" class="form-control" name="name" placeholder="<?= $this->te('Ordner-Name') ?>" required autofocus></div>
            <?php if (!empty($folders)): ?>
            <select name="parent_id" class="form-control" style="margin-bottom:1rem;">
                <option value=""><?= $this->te('— Kein übergeordneter Ordner —') ?></option>
                <?php foreach ($folders as $pf): ?>
                    <option value="<?= (int)$pf['id'] ?>" <?= $active_folder_id === (int)$pf['id'] ? 'selected' : '' ?>>
                        <?= $this->e($pf['path_label'] ?? $pf['name']) ?>
                    </option>
                <?php endforeach; ?>
            </select>
            <?php endif; ?>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default"
                        data-close="folderModal"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Anlegen') ?></button>
            </div>
        </form>
    </div>
</div>

<!-- Ordner umbenennen Modal -->
<div class="modal-overlay vault-modal-overlay" id="folderRenameModal" data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Ordner umbenennen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-close="folderRenameModal">&times;</button>
        </div>
        <form id="folderRenameForm" method="POST">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <div class="modal-body">
                <div class="form-group"><input type="text" class="form-control" id="folderRenameInput" name="name" placeholder="<?= $this->te('Neuer Name') ?>" required></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default"
                        data-close="folderRenameModal"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Umbenennen') ?></button>
            </div>
        </form>
    </div>
</div>

<!-- Move Entry Modal -->
<div class="modal-overlay vault-modal-overlay" id="moveModal" data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Eintrag verschieben') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-act="closeMoveModal">&times;</button>
        </div>
        <div class="modal-body">
        <div class="form-group">
        <label for="moveTargetContext"><?= $this->te('Zielbereich') ?></label>
        <select class="form-control" id="moveTargetContext">
            <option value=""><?= $this->te('Persönlicher Tresor') ?></option>
            <?php foreach ($all_teams ?? [] as $t): ?>
                <?php if (isset($all_tvks[(int)$t['id']])): ?>
                <option value="<?= (int)$t['id'] ?>"><?= $this->e($t['name']) ?></option>
                <?php endif; ?>
            <?php endforeach; ?>
        </select>
        </div>
        <div class="form-group">
        <label for="moveTargetFolder"><?= $this->te('Zielordner') ?></label>
        <select class="form-control" id="moveTargetFolder">
            <option value=""><?= $this->te('Kein Ordner') ?></option>
        </select>
        </div>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-act="closeMoveModal"><?= $this->te('Abbrechen') ?></button>
            <button type="button" class="btn btn-primary" id="moveSaveBtn" data-act="submitMove"><?= $this->te('Verschieben') ?></button>
        </div>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
var _moveEntryId = null;
var _moveTeams   = <?= json_encode($moveTeams, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var _persfolders = <?= json_encode($personalFoldersJson, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var _csrf        = <?= json_encode($this->csrf(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;

var CLIP_CLEAR_MS = 45000;

// Der Ereignisverteiler kopiert über diesen Namen.
window.vaultCopyText = function (text, btn) { copyText(text, btn); };

function copyText(text, btn) {
    if (!text) return;
    navigator.clipboard.writeText(text).then(function() {
        if (btn) {
            var orig = btn.innerHTML;
            btn.textContent = '✓';
            setTimeout(function() { btn.innerHTML = orig; }, 1500);
        }
        scheduleClipboardClear(text);
    });
}

function scheduleClipboardClear(value) {
    setTimeout(function() {
        if (!navigator.clipboard.readText) { navigator.clipboard.writeText('').catch(function(){}); return; }
        navigator.clipboard.readText()
            .then(function(cur) { if (cur === value) navigator.clipboard.writeText('').catch(function(){}); })
            .catch(function(){});
    }, CLIP_CLEAR_MS);
}

// Kopier-Buttons mit data-copy (sicheres Escaping statt Inline-JS)
document.addEventListener('click', function (e) {
    var b = e.target.closest('[data-copy]');
    if (b) copyText(b.getAttribute('data-copy'), b);
});

function openFolderRename(folderId, currentName) {
    var form  = document.getElementById('folderRenameForm');
    var input = document.getElementById('folderRenameInput');
    form.action = '/vault/teams/<?= (int)$team['id'] ?>/folders/' + folderId;
    input.value = currentName;
    document.getElementById('folderRenameModal').classList.add('open');
    setTimeout(function() { input.select(); }, 50);
}

function openMoveModal(entryId, srcTeamId, srcFolderId) {
    _moveEntryId = entryId;
    var ctx = document.getElementById('moveTargetContext');
    ctx.value = srcTeamId ? String(srcTeamId) : '';
    fillMoveFolders();
    if (srcFolderId !== null) {
        document.getElementById('moveTargetFolder').value = String(srcFolderId);
    }
    document.getElementById('moveModal').classList.add('open');
}

function closeMoveModal() {
    document.getElementById('moveModal').classList.remove('open');
    _moveEntryId = null;
}

function fillMoveFolders() {
    var ctx     = document.getElementById('moveTargetContext').value;
    var sel     = document.getElementById('moveTargetFolder');
    sel.innerHTML = '<option value="">Kein Ordner</option>';
    var folders = [];
    if (ctx === '') {
        folders = _persfolders;
    } else {
        var tid = parseInt(ctx, 10);
        for (var i = 0; i < _moveTeams.length; i++) {
            if (_moveTeams[i].id === tid) { folders = _moveTeams[i].folders; break; }
        }
    }
    folders.forEach(function(f) {
        var opt = document.createElement('option');
        opt.value = f.id; opt.textContent = f.name;
        sel.appendChild(opt);
    });
}

document.getElementById('moveTargetContext').addEventListener('change', fillMoveFolders);

function submitMove() {
    if (_moveEntryId === null) return;
    var btn = document.getElementById('moveSaveBtn');
    btn.textContent = '...'; btn.disabled = true;
    var form = new FormData();
    form.append('_csrf', _csrf);
    // Beide Felder immer mitsenden: leer heißt persönlicher Tresor bzw. kein Ordner.
    form.append('target_team_id', document.getElementById('moveTargetContext').value);
    form.append('target_folder_id', document.getElementById('moveTargetFolder').value);
    fetch('/vault/entries/' + _moveEntryId + '/move', {
        method: 'POST',
        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
        body: form
    })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (data.success) { closeMoveModal(); window.location.reload(); }
            else { btn.textContent = 'Verschieben'; btn.disabled = false; alert(data.message || 'Fehler'); }
        })
        .catch(function() { btn.textContent = 'Verschieben'; btn.disabled = false; });
}
</script>
<script<?= $this->scriptNonce() ?>>
/* Tresor-Tabelle: Textfilter, Sortierung, 2FA-Filter, Dichte, Paginierung (clientseitig) */
(function () {
    var PAGE = 50;
    var vPageLimit = PAGE;
    window.vaultApplyTable = function () {
        var tbody = document.getElementById('vTbody');
        if (!tbody) return;
        var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr.v-row'));
        var sortEl = document.getElementById('vSort');
        var sort = sortEl ? sortEl.value : 'title-asc';
        var key = sort.indexOf('user') === 0 ? 'user' : 'title';
        var desc = sort.indexOf('desc') !== -1;
        rows.sort(function (a, b) {
            var av = a.getAttribute('data-' + key) || '', bv = b.getAttribute('data-' + key) || '';
            var cmp = av.localeCompare(bv, 'de');
            return desc ? -cmp : cmp;
        });
        rows.forEach(function (r) { tbody.appendChild(r); });
        var q = (document.getElementById('vSearch') && document.getElementById('vSearch').value || '').toLowerCase().trim();
        var totpOnly = document.getElementById('vTotpOnly') && document.getElementById('vTotpOnly').checked;
        var shown = 0, matched = 0;
        rows.forEach(function (r) {
            var ok = (!totpOnly || r.getAttribute('data-totp') === '1');
            if (ok && q !== '') {
                ok = (r.getAttribute('data-title') || '').indexOf(q) !== -1 || (r.getAttribute('data-user') || '').indexOf(q) !== -1;
            }
            if (!ok) { r.classList.add('vlt-hidden-row'); return; }
            matched++;
            if (shown < vPageLimit) { r.classList.remove('vlt-hidden-row'); shown++; }
            else { r.classList.add('vlt-hidden-row'); }
        });
        var cnt = document.getElementById('vCount'); if (cnt) cnt.textContent = '(' + matched + ')';
        var lm = document.getElementById('vLoadMore'); if (lm) lm.style.display = (matched > shown) ? '' : 'none';
    };
    window.vaultShowMore = function () { vPageLimit += PAGE; vaultApplyTable(); };
    window.vaultToggleDensity = function () {
        var t = document.getElementById('vTable'); if (!t) return;
        t.classList.toggle('vlt-dense');
        try { localStorage.setItem('vaultDense', t.classList.contains('vlt-dense') ? '1' : '0'); } catch (e) {}
    };
    window.vaultToggleSidebar = function () {
        var body = document.querySelector('.v-body');
        if (!body) return;
        body.classList.toggle('v-sidebar-collapsed');
        try { localStorage.setItem('vaultSidebar', body.classList.contains('v-sidebar-collapsed') ? '0' : '1'); } catch (e) {}
    };
    document.addEventListener('click', function (ev) {
        document.querySelectorAll('details.v-io-menu[open]').forEach(function (d) {
            if (!d.contains(ev.target)) d.removeAttribute('open');
        });
    });
    document.addEventListener('DOMContentLoaded', function () {
        try { if (localStorage.getItem('vaultDense') !== '0') { var t = document.getElementById('vTable'); if (t) t.classList.add('vlt-dense'); } } catch (e) {}
        try { if (localStorage.getItem('vaultSidebar') === '0') { var b = document.querySelector('.v-body'); if (b) b.classList.add('v-sidebar-collapsed'); } } catch (e) {}
        if (document.getElementById('vTbody')) vaultApplyTable();

        // Endlos-Laden: nächste Einträge automatisch nachladen (Button als Fallback)
        var lm = document.getElementById('vLoadMore');
        if (lm && 'IntersectionObserver' in window) {
            new IntersectionObserver(function (entries) {
                entries.forEach(function (e) {
                    if (e.isIntersecting && lm.style.display !== 'none') vaultShowMore();
                });
            }, { rootMargin: '300px' }).observe(lm);
        }
    });
})();
</script>

<!-- Aktionen-Menü (3 Punkte) -->
<div id="vRowMenu" class="v-row-menu" style="display:none;">
    <a href="#" data-act="vRmAction" data-args='["show"]'><i class="fas fa-eye"></i> <?= $this->te('Anzeigen') ?></a>
    <a href="#" data-act="vRmAction" data-args='["edit"]'><i class="fas fa-pen"></i> <?= $this->te('Bearbeiten') ?></a>
    <a href="#" data-act="vRmAction" data-args='["move"]'><i class="fas fa-exchange-alt"></i> <?= $this->te('Verschieben') ?></a>
    <a href="#" class="v-rm-danger" data-act="vRmAction" data-args='["delete"]'><i class="fas fa-trash"></i> <?= $this->te('Löschen') ?></a>
</div>
<style>
.v-row-menu { position:fixed; z-index:4000; min-width:180px; background:var(--color-bg); border:1px solid var(--gray-200);
    border-radius:0.45rem; box-shadow:0 10px 30px rgba(0,0,0,0.16); padding:0.25rem 0; }
.v-row-menu a { display:flex; align-items:center; gap:0.55rem; padding:0.5rem 0.9rem; color:var(--gray-700);
    font-size:0.85rem; white-space:nowrap; text-decoration:none; }
.v-row-menu a:hover { background:var(--gray-100); color:var(--gray-900); }
.v-row-menu a.v-rm-danger { color:var(--red-600); }
.v-row-menu a.v-rm-danger:hover { background:var(--red-100); }
.v-row-menu a i { width:15px; text-align:center; color:var(--gray-400); }
.v-row-menu a.v-rm-danger i { color:var(--red-600); }
</style>
<script<?= $this->scriptNonce() ?>>
var _rmData = null;
var _rmCsrf = <?= json_encode($this->csrf(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;

window.vaultRowMenu = function (btn) {
    var m = document.getElementById('vRowMenu');
    _rmData = {
        show:   btn.getAttribute('data-show'),
        edit:   btn.getAttribute('data-edit'),
        del:    btn.getAttribute('data-del'),
        id:     parseInt(btn.getAttribute('data-id'), 10),
        folder: btn.getAttribute('data-folder') !== '' ? parseInt(btn.getAttribute('data-folder'), 10) : null
    };
    m.style.display = 'block';
    var r  = btn.getBoundingClientRect();
    var mw = m.offsetWidth, mh = m.offsetHeight;
    var left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
    var top  = r.bottom + 4;
    if (top + mh > window.innerHeight - 8) top = Math.max(8, r.top - mh - 4);
    m.style.left = left + 'px';
    m.style.top  = top + 'px';
};

function vRmClose() {
    document.getElementById('vRowMenu').style.display = 'none';
}

window.vRmAction = function (action) {
    if (!_rmData) { vRmClose(); return false; }
    var d = _rmData;
    vRmClose();
    if (action === 'show') {
        window.location.href = d.show;
    } else if (action === 'edit') {
        window.location.href = d.edit;
    } else if (action === 'move') {
        openMoveModal(d.id, <?= (int)$team['id'] ?>, d.folder);
    } else if (action === 'delete') {
        if (confirm('Eintrag in den Papierkorb verschieben?')) {
            var f = document.createElement('form');
            f.method = 'POST';
            f.action = d.del;
            var i = document.createElement('input');
            i.type = 'hidden'; i.name = '_csrf'; i.value = _rmCsrf;
            f.appendChild(i);
            document.body.appendChild(f);
            f.submit();
        }
    }
    return false;
};

document.addEventListener('click', function (ev) {
    var m = document.getElementById('vRowMenu');
    if (m && m.style.display !== 'none' && !m.contains(ev.target) && !ev.target.closest('[data-show]')) vRmClose();
});
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') vRmClose(); });
window.addEventListener('scroll', vRmClose, true);
</script>

<?= $this->render('vault/_autolock', ['vault_idle_ttl' => $vault_idle_ttl ?? null]) ?>