Quelltext des aktuellen Standes
<?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'])) ?>]'>✎</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);">🗑</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">×</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">×</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">×</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]) ?>