Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => 'Telefonbuch',
'current_route' => 'phonebook',
'full_width' => true,
]);
use OpenNIT\Controllers\PhonebookController;
// Personen alphabetisch sortieren
usort($persons, fn($a, $b) => strcasecmp($a['display_name'] ?? '', $b['display_name'] ?? ''));
$canManage = $this->hasCapability('manage_phonebook');
?>
<div class="cb-fullwidth">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('Telefonbuch') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Telefonbuch') ?></li>
</ol>
</div>
<?php if ($this->hasCapability('manage_phonebook')): ?>
<div class="content-header-actions">
<button type="button" onclick="openPersonModal()" class="btn btn-primary">
<i class="fas fa-plus"></i> <?= $this->te('Neuer Eintrag') ?>
</button>
</div>
<?php endif; ?>
</div>
<!-- Filterleiste (Styleguide § 8) -->
<div class="card card-outline card-primary pb-filter-card">
<div class="filter-card-body">
<form method="GET" action="/phonebook" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;width:100%;">
<div class="filter-input-wrap" style="flex:1;min-width:180px;max-width:600px;">
<svg class="filter-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15" aria-hidden="true" focusable="false">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="search" name="search" class="form-control form-control-sm"
placeholder="<?= $this->te('Name, Abteilung, Telefon…') ?>"
value="<?= $this->e($search) ?>" autocomplete="off" autofocus>
</div>
<?php if (!empty($departments)): ?>
<select name="department" class="form-control form-control-sm" style="width:auto;min-width:140px;flex-shrink:0;">
<option value=""><?= $this->te('Alle Abteilungen') ?></option>
<?php foreach ($departments as $dept): ?>
<option value="<?= $this->e($dept) ?>" <?= $department === $dept ? 'selected' : '' ?>>
<?= $this->e($dept) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Suchen') ?></button>
<?php if ($search !== '' || $department !== ''): ?>
<a href="/phonebook" class="btn btn-secondary btn-sm"><?= $this->te('Zurücksetzen') ?></a>
<?php endif; ?>
</form>
</div>
</div>
<!-- Ergebnisanzahl -->
<?php if ($search !== '' || $department !== ''): ?>
<p class="pb-result-count">
<?= count($persons) ?> Ergebnis<?= count($persons) !== 1 ? 'se' : '' ?> gefunden
</p>
<?php endif; ?>
<div class="cb-content">
<?php if (empty($persons)): ?>
<div class="cb-empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48" height="48">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<p><?= $this->te('Keine Personen gefunden.') ?></p>
<?php if ($search !== '' || $department !== ''): ?>
<a href="/phonebook" class="cb-btn cb-btn-secondary"><?= $this->te('Filter zurücksetzen') ?></a>
<?php else: ?>
<p style="font-size:.85rem;color:var(--gray-400);margin-top:.5rem;">
<?= $this->t('Personen können unter <a href="/admin/directory">Verzeichnis (Admin)</a> angelegt werden.') ?>
</p>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Alphabet-Schnellnavigation (nur wenn kein Filter aktiv) -->
<?php if ($search === '' && $department === ''): ?>
<?php
$letters = [];
foreach ($persons as $p) {
$l = strtoupper(mb_substr($p['display_name'] ?? '', 0, 1));
if ($l !== '' && !isset($letters[$l])) $letters[$l] = true;
}
$letters = array_keys($letters);
sort($letters);
?>
<?php if (count($letters) > 5): ?>
<nav class="pb-alpha" aria-label="<?= $this->te('Alphabet-Schnellnavigation') ?>">
<?php foreach ($letters as $l): ?>
<a href="#pb-letter-<?= $l ?>" class="pb-alpha-link"><?= $l ?></a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
<?php endif; ?>
<!-- Personen-Grid alphabetisch gruppiert -->
<?php
// Alphabetische Buchstaben-Gruppen
if ($search === '' && $department === '') {
$grouped = [];
foreach ($persons as $p) {
$l = strtoupper(mb_substr($p['display_name'] ?? '', 0, 1));
if ($l === '') $l = '#';
$grouped[$l][] = $p;
}
ksort($grouped);
} else {
$grouped = ['' => $persons]; // Kein Gruppenheader bei Suche
}
?>
<?php foreach ($grouped as $letter => $groupPersons): ?>
<?php if ($letter !== ''): ?>
<div id="pb-letter-<?= $letter ?>" class="pb-group-header">
<?= $letter ?>
<span class="pb-group-count"><?= count($groupPersons) ?></span>
</div>
<?php endif; ?>
<div class="cb-contacts-grid pb-grid">
<?php foreach ($groupPersons as $p):
$initials = PhonebookController::getInitials($p['display_name']);
$color = PhonebookController::getAvatarColor($p['display_name']);
?>
<div class="cb-person-card<?= $canManage && ($p['source'] ?? 'manual') === 'manual' ? ' pb-has-actions' : '' ?>" style="cursor:default;">
<?php if ($canManage && ($p['source'] ?? 'manual') === 'manual'): ?>
<div class="pb-card-actions">
<button type="button" class="pb-card-btn pb-edit-btn" title="<?= $this->te('Bearbeiten') ?>"
data-id="<?= (int)$p['id'] ?>"
data-display="<?= $this->e($p['display_name'] ?? '') ?>"
data-first="<?= $this->e($p['first_name'] ?? '') ?>"
data-last="<?= $this->e($p['last_name'] ?? '') ?>"
data-email="<?= $this->e($p['email'] ?? '') ?>"
data-department="<?= $this->e($p['department'] ?? '') ?>"
data-job="<?= $this->e($p['job_title'] ?? '') ?>"
data-company="<?= $this->e($p['company'] ?? '') ?>"
data-office="<?= $this->e($p['office_location'] ?? '') ?>"
data-phone="<?= $this->e($p['phone'] ?? '') ?>"
data-mobile="<?= $this->e($p['mobile'] ?? '') ?>">
<i class="fas fa-pen"></i>
</button>
<form method="POST" action="/phonebook/<?= (int)$p['id'] ?>" style="display:inline;margin:0;"
onsubmit="return confirm('Diesen Eintrag aus dem Telefonbuch entfernen?');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="pb-card-btn pb-card-btn-danger" title="<?= $this->te('Löschen') ?>">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
<?php endif; ?>
<?php if (!empty($p['image_path'])): ?>
<img src="<?= $this->e($p['image_path']) ?>"
alt="<?= $this->e($p['display_name']) ?>"
class="cb-person-avatar-img"
loading="lazy">
<?php else: ?>
<span class="cb-person-avatar"
style="background:<?= $this->e($color) ?>;color:var(--color-on-accent);font-size:.85rem;font-weight:700;">
<?= $this->e($initials) ?>
</span>
<?php endif; ?>
<div class="cb-person-info">
<div class="cb-person-name"><?= $this->e($p['display_name']) ?></div>
<?php if (!empty($p['job_title'])): ?>
<div class="cb-person-position" title="<?= $this->e($p['job_title']) ?>"><?= $this->e($p['job_title']) ?></div>
<?php endif; ?>
<?php if (!empty($p['department'])): ?>
<div class="cb-person-company">
<span style="font-size:.7rem;color:var(--gray-400);background:var(--gray-100);border-radius:.25rem;padding:.1em .4em;">
<?= $this->e($p['department']) ?>
</span>
</div>
<?php endif; ?>
<?php if (!empty($p['company'])): ?>
<div class="pb-person-firma" title="<?= $this->te('Firma') ?>">
🏢 <?= $this->e($p['company']) ?>
</div>
<?php endif; ?>
<?php if (!empty($p['office_location'])): ?>
<div class="pb-person-office" title="<?= $this->te('Büro / Raum') ?>">
📍 <?= $this->e($p['office_location']) ?>
</div>
<?php endif; ?>
</div>
<div class="cb-person-contacts">
<?php if (!empty($p['phone'])): ?>
<a href="tel:<?= $this->e(preg_replace('/[^\d+]/', '', $p['phone'])) ?>"
class="cb-person-detail"
style="color:var(--blue-700);text-decoration:none;white-space:nowrap;"
title="Telefon: <?= $this->e($p['phone']) ?>">
📞 <?= $this->e($p['phone']) ?>
</a>
<?php endif; ?>
<?php if (!empty($p['mobile'])): ?>
<a href="tel:<?= $this->e(preg_replace('/[^\d+]/', '', $p['mobile'])) ?>"
class="cb-person-detail"
style="color:var(--emerald-600);text-decoration:none;white-space:nowrap;"
title="Mobil: <?= $this->e($p['mobile']) ?>">
📱 <?= $this->e($p['mobile']) ?>
</a>
<?php endif; ?>
<?php if (!empty($p['email'])): ?>
<a href="mailto:<?= $this->e($p['email']) ?>"
class="cb-person-detail"
style="color:var(--gray-500);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;"
title="<?= $this->e($p['email']) ?>">
✉ <?= $this->e($p['email']) ?>
</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<style>
/* Einheitlicher horizontaler Einzug (1rem) wie .cb-content – damit Filter-
karte, Ergebniszeile, Alphabet, Gruppen-Header und Karten bündig sitzen. */
.cb-fullwidth .pb-filter-card { margin: 1rem 1rem 0.5rem; }
.cb-fullwidth .pb-result-count { font-size:.85rem; color:var(--gray-500); margin:.25rem 0 .75rem 1rem; }
/* Firma / Büro – dezente Zusatzzeilen unter Beruf/Abteilung */
.cb-fullwidth .pb-person-firma,
.cb-fullwidth .pb-person-office {
font-size:.72rem; color:var(--gray-500); margin-top:.15rem;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%;
}
/* Diese Blöcke liegen INNERHALB von .cb-content (hat bereits 1rem padding),
daher kein zusätzlicher horizontaler Einzug. */
.cb-fullwidth .pb-group-header {
font-weight:700; color:var(--gray-700); font-size:1rem;
margin:1rem 0 .25rem; padding-bottom:.25rem;
border-bottom:2px solid var(--gray-200); letter-spacing:.05em;
}
.cb-fullwidth .pb-group-count { font-weight:400; font-size:.75rem; color:var(--gray-400); margin-left:.4rem; }
.cb-fullwidth .pb-grid { padding:.25rem 0; }
/* Alphabet-Schnellnavigation – dezente Leiste, Hover per CSS (kein Inline-JS) */
.cb-fullwidth .pb-alpha {
display:flex; flex-wrap:wrap; gap:.3rem;
margin:.25rem 0 1rem; padding:.5rem;
background:var(--gray-50); border:1px solid var(--gray-200); border-radius:.5rem;
}
.cb-fullwidth .pb-alpha-link {
display:inline-flex; align-items:center; justify-content:center;
width:1.75rem; height:1.75rem; border-radius:.35rem;
background:var(--color-bg); border:1px solid var(--gray-300);
font-weight:600; font-size:.8rem; color:var(--gray-700); text-decoration:none;
transition:background .1s, border-color .1s, color .1s;
}
.cb-fullwidth .pb-alpha-link:hover {
background:var(--color-primary); border-color:var(--color-primary); color:var(--color-on-accent);
}
/* E-Mail-Adresse: moderate Obergrenze als Basis (auch mobil sicher),
wird bei Überlänge sauber gekürzt (voller Text als Tooltip). */
.cb-fullwidth .cb-person-contacts a[href^="mailto:"] { max-width: 240px; }
/* Bearbeiten/Löschen je Karte – nur bei manuellen Einträgen sichtbar.
Erscheinen dezent oben rechts, beim Hover der Karte deutlicher. */
.cb-fullwidth .pb-has-actions { position: relative; }
.cb-fullwidth .pb-card-actions {
position: absolute; top: .4rem; right: .4rem;
display: flex; gap: .25rem; opacity: .35; transition: opacity .12s;
}
.cb-fullwidth .pb-has-actions:hover .pb-card-actions,
.cb-fullwidth .pb-card-actions:focus-within { opacity: 1; }
.cb-fullwidth .pb-card-actions form { line-height: 0; }
.cb-fullwidth .pb-card-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 1.6rem; height: 1.6rem; padding: 0;
border: 1px solid var(--gray-300); border-radius: .35rem; background: var(--color-bg);
color: var(--gray-500); font-size: .72rem; cursor: pointer; transition: all .1s;
}
.cb-fullwidth .pb-card-btn:hover { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-accent); }
.cb-fullwidth .pb-card-btn-danger:hover { background: var(--red-600); border-color: var(--red-600); color: var(--color-on-accent); }
/* Telefonbuch: Desktop genau 2 Spalten (breitere Karten, mehr Luft).
Greift erst ab Tablet/Desktop – die mobile 1-Spalten-Ansicht aus app.css
(max-width:768px) bleibt unberührt. */
@media (min-width: 769px) {
.cb-fullwidth .cb-contacts-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Beruf & Abteilung sauber abschneiden, damit nichts in die Rufnummern läuft */
.cb-fullwidth .cb-person-position {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.cb-fullwidth .cb-person-company > span {
display: inline-block; max-width: 100%; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
}
/* Kontaktspalte mit etwas Abstand und eigener Mindestbreite */
.cb-fullwidth .cb-person-contacts { min-width: 150px; padding-left: 1rem; }
/* Breitere Karten = mehr Platz: E-Mail darf deutlich länger sein,
bevor gekürzt wird. */
.cb-fullwidth .cb-person-contacts a[href^="mailto:"] { max-width: 360px; }
}
/* Responsive Phonebook */
@media (max-width: 640px) {
.content-header { padding: 0.75rem 0.75rem 0; }
.content-header h1 { font-size: 1.2rem; }
/* Search area */
.filter-card-body form { flex-direction: column; align-items: stretch !important; }
.filter-card-body form > div,
.filter-card-body form > select,
.filter-card-body form > button,
.filter-card-body form > a { width: 100% !important; max-width: none !important; min-width: 0 !important; }
/* Person cards: stack contact info below name */
.cb-person-card { flex-wrap: wrap; gap: 0.5rem; }
.cb-person-contacts {
flex-direction: row !important;
flex-wrap: wrap;
gap: 0.5rem;
text-align: left !important;
width: 100%;
padding-left: calc(60px + 0.75rem);
margin-top: -0.25rem;
}
.cb-person-contacts a { white-space: nowrap; }
/* Alphabet nav */
#pb-letter-A, #pb-letter-B { scroll-margin-top: 4rem; }
}
@media (max-width: 400px) {
.cb-contacts-grid { gap: 0.5rem; }
.cb-person-avatar, .cb-person-avatar-img { width: 48px; height: 48px; min-width: 48px; }
.cb-person-contacts { padding-left: calc(48px + 0.5rem); }
}
</style>
<?php if ($this->hasCapability('manage_phonebook')): ?>
<!-- Modal: Person anlegen -->
<div id="modal-create-person" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-create-person-title" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h3 id="modal-create-person-title"><?= $this->te('Eintrag anlegen') ?></h3>
<button type="button" class="modal-close" onclick="closePersonModal()" aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<form method="POST" action="/phonebook" id="form-create-person">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="modal-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label for="f-9d9a-first_name"><?= $this->te('Vorname') ?></label>
<input id="f-9d9a-first_name" type="text" name="first_name" class="form-control form-control-sm">
</div>
<div class="form-group">
<label for="f-9d9a-last_name"><?= $this->te('Nachname') ?></label>
<input id="f-9d9a-last_name" type="text" name="last_name" class="form-control form-control-sm">
</div>
</div>
<div class="form-group">
<label for="f-9d9a-display_name"><?= $this->te('Anzeigename') ?> <span class="text-danger">*</span></label>
<input id="f-9d9a-display_name" type="text" name="display_name" class="form-control form-control-sm" required>
</div>
<div class="form-group">
<label for="f-9d9a-email"><?= $this->t('E-Mail') ?></label>
<input id="f-9d9a-email" type="email" name="email" class="form-control form-control-sm">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label for="f-9d9a-department"><?= $this->te('Abteilung') ?></label>
<input id="f-9d9a-department" type="text" name="department" class="form-control form-control-sm">
</div>
<div class="form-group">
<label for="f-9d9a-job_title"><?= $this->te('Jobtitel') ?></label>
<input id="f-9d9a-job_title" type="text" name="job_title" class="form-control form-control-sm">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label for="f-9d9a-company"><?= $this->te('Firma') ?></label>
<input id="f-9d9a-company" type="text" name="company" class="form-control form-control-sm">
</div>
<div class="form-group">
<label for="f-9d9a-office_location"><?= $this->te('Büro / Raum') ?></label>
<input id="f-9d9a-office_location" type="text" name="office_location" class="form-control form-control-sm">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label><?= $this->te('Telefon') ?></label>
<input type="text" name="phone" class="form-control form-control-sm" placeholder="+49 211 …">
</div>
<div class="form-group">
<label><?= $this->te('Mobilnummer') ?></label>
<input type="text" name="mobile" class="form-control form-control-sm" placeholder="+49 170 …">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closePersonModal()"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
</div>
</form>
</div>
</div>
<!-- Modal: Person bearbeiten -->
<div id="modal-edit-person" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-edit-person-title" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h3 id="modal-edit-person-title"><?= $this->te('Eintrag bearbeiten') ?></h3>
<button type="button" class="modal-close" onclick="closeEditPersonModal()" aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<form method="POST" action="/phonebook" id="form-edit-person">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="PUT">
<div class="modal-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label><?= $this->te('Vorname') ?></label>
<input type="text" name="first_name" class="form-control form-control-sm">
</div>
<div class="form-group">
<label><?= $this->te('Nachname') ?></label>
<input type="text" name="last_name" class="form-control form-control-sm">
</div>
</div>
<div class="form-group">
<label><?= $this->te('Anzeigename') ?> <span class="text-danger">*</span></label>
<input type="text" name="display_name" class="form-control form-control-sm" required>
</div>
<div class="form-group">
<label><?= $this->t('E-Mail') ?></label>
<input type="email" name="email" class="form-control form-control-sm">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label><?= $this->te('Abteilung') ?></label>
<input type="text" name="department" class="form-control form-control-sm">
</div>
<div class="form-group">
<label><?= $this->te('Jobtitel') ?></label>
<input type="text" name="job_title" class="form-control form-control-sm">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label><?= $this->te('Firma') ?></label>
<input type="text" name="company" class="form-control form-control-sm">
</div>
<div class="form-group">
<label><?= $this->te('Büro / Raum') ?></label>
<input type="text" name="office_location" class="form-control form-control-sm">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label><?= $this->te('Telefon') ?></label>
<input type="text" name="phone" class="form-control form-control-sm" placeholder="+49 211 …">
</div>
<div class="form-group">
<label><?= $this->te('Mobilnummer') ?></label>
<input type="text" name="mobile" class="form-control form-control-sm" placeholder="+49 170 …">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeEditPersonModal()"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
</div>
</form>
</div>
</div>
<script>
function openPersonModal() {
document.getElementById('modal-create-person').style.display = 'flex';
}
function closePersonModal() {
var m = document.getElementById('modal-create-person');
m.style.display = 'none';
document.getElementById('form-create-person').reset();
}
function closeEditPersonModal() {
document.getElementById('modal-edit-person').style.display = 'none';
}
(function () {
var cm = document.getElementById('modal-create-person');
if (cm) cm.addEventListener('click', function (e) { if (e.target === this) closePersonModal(); });
var em = document.getElementById('modal-edit-person');
var ef = document.getElementById('form-edit-person');
// Edit-Buttons der Karten verdrahten – Formular füllen und Modal öffnen.
document.querySelectorAll('.pb-edit-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var d = btn.dataset;
ef.action = '/phonebook/' + d.id;
ef.first_name.value = d.first || '';
ef.last_name.value = d.last || '';
ef.display_name.value = d.display || '';
ef.email.value = d.email || '';
ef.department.value = d.department || '';
ef.job_title.value = d.job || '';
ef.company.value = d.company || '';
ef.office_location.value = d.office || '';
ef.phone.value = d.phone || '';
ef.mobile.value = d.mobile || '';
em.style.display = 'flex';
});
});
if (em) em.addEventListener('click', function (e) { if (e.target === this) closeEditPersonModal(); });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { closePersonModal(); closeEditPersonModal(); }
});
})();
</script>
<?php endif; ?>
<?= $this->partial('image_lightbox') ?>