Initial Release: Lütte Planer mit Google-Login-Vorbereitung

- Login-System (Mock für Tests, Google OAuth vorbereitet)
- Reisen erstellen mit Emoji, Ziel und Datum
- Ablauf (Timeline), Packliste, Ideen, Notizen, Dokumente
- Team-System: Personen einladen per E-Mail + Einladungslinks
- Berechtigungen: Nur lesen / Bearbeiten / Admin
- E-Mail-Benachrichtigungen beim Einladen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 20:56:30 +02:00
co-authored by Claude Sonnet 4.6
commit bd8a322fc0
21 changed files with 1647 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
data/
*.sqlite
*.sqlite-wal
*.sqlite-shm
+10
View File
@@ -0,0 +1,10 @@
Options -Indexes
DirectoryIndex index.php
<FilesMatch "^\.">
Require all denied
</FilesMatch>
<FilesMatch "\.(sql|sqlite)$">
Require all denied
</FilesMatch>
+132
View File
@@ -0,0 +1,132 @@
<?php
require_once __DIR__ . '/config.php';
$tripId = $_GET['trip'] ?? '';
[$trip, $user] = requireTripAccess($tripId);
$canEdit = userCanEdit($tripId, $user['id']);
// Eintrag hinzufügen
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
dbInsert('ablauf', [
'trip_id' => $tripId,
'datum' => $_POST['datum'] ?? '',
'zeit' => $_POST['zeit'] ?? '',
'titel' => trim($_POST['titel'] ?? ''),
'beschreibung'=> trim($_POST['beschreibung'] ?? ''),
'ort' => trim($_POST['ort'] ?? ''),
'kategorie' => $_POST['kategorie'] ?? 'allgemein',
'created_by' => $user['id'],
]);
header('Location: /Develop/PlanerGoogel/ablauf.php?trip=' . $tripId); exit;
}
// Eintrag löschen
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
dbExec('DELETE FROM ablauf WHERE id = ? AND trip_id = ?', [(int)$_POST['delete'], $tripId]);
header('Location: /Develop/PlanerGoogel/ablauf.php?trip=' . $tripId); exit;
}
$eintraege = dbAll(
'SELECT a.*, u.name as user_name, u.farbe as user_farbe FROM ablauf a
LEFT JOIN users u ON u.id = a.created_by
WHERE a.trip_id = ?
ORDER BY a.datum ASC, a.zeit ASC, a.sort_order ASC, a.id ASC',
[$tripId]
);
// Nach Datum gruppieren
$grouped = [];
foreach ($eintraege as $e) {
$key = $e['datum'] ?: '_ohne';
$grouped[$key][] = $e;
}
$kategorien = [
'allgemein' => ['icon' => '📌', 'label' => 'Allgemein'],
'transport' => ['icon' => '🚗', 'label' => 'Transport'],
'hotel' => ['icon' => '🏨', 'label' => 'Hotel'],
'essen' => ['icon' => '🍽️', 'label' => 'Essen'],
'aktivität' => ['icon' => '🎯', 'label' => 'Aktivität'],
'sehenswürdigkeit' => ['icon' => '🏛️', 'label' => 'Sehenswürdigkeit'],
'einkaufen' => ['icon' => '🛍️', 'label' => 'Einkaufen'],
'sonstiges' => ['icon' => '📝', 'label' => 'Sonstiges'],
];
$pageTitle = 'Ablauf';
$activeTab = 'ablauf';
include __DIR__ . '/includes/header.php';
$countdown = null;
if ($trip['start_datum']) {
$days = daysUntil($trip['start_datum']);
if ($days > 0) $countdown = "Noch <strong>{$days}</strong> Tage bis zur Reise!";
elseif ($days === 0) $countdown = "🎉 Heute geht's los!";
}
?>
<main class="main">
<?php if ($countdown): ?>
<div class="countdown-bar"><?= $countdown ?></div>
<?php endif; ?>
<div class="page-header">
<h1 class="section-head">Ablauf</h1>
<?php if ($canEdit): ?>
<button class="btn btn-primary" onclick="toggleForm('add-form')">+ Eintrag</button>
<?php endif; ?>
</div>
<?php if ($canEdit): ?>
<div id="add-form" class="add-form hidden">
<form method="POST" style="display:contents;">
<input type="date" name="datum" style="max-width:150px;">
<input type="time" name="zeit" style="max-width:110px;">
<input type="text" name="titel" placeholder="Titel *" required style="min-width:180px;">
<input type="text" name="ort" placeholder="Ort / Adresse">
<select name="kategorie">
<?php foreach ($kategorien as $k => $v): ?>
<option value="<?= $k ?>"><?= $v['icon'] ?> <?= $v['label'] ?></option>
<?php endforeach; ?>
</select>
<input type="text" name="beschreibung" placeholder="Beschreibung / Notiz">
<button type="submit" name="add" class="btn btn-primary">Hinzufügen</button>
</form>
</div>
<?php endif; ?>
<?php if (empty($eintraege)): ?>
<div class="empty"><div class="empty-icon">🗓️</div><p>Noch keine Einträge im Ablauf.</p></div>
<?php else: ?>
<div class="timeline">
<?php foreach ($grouped as $datum => $items): ?>
<div class="tl-group">
<div class="tl-group-date">
<?= $datum === '_ohne' ? 'Ohne Datum' : germanDate($datum) ?>
</div>
<?php foreach ($items as $e): ?>
<div class="tl-item">
<div class="tl-dot"></div>
<div class="tl-item-inner">
<div class="tl-meta">
<?php if ($e['zeit']): ?><span class="tl-time"><?= h($e['zeit']) ?></span><?php endif; ?>
<span class="tl-kat"><?= $kategorien[$e['kategorie']]['icon'] ?? '📌' ?></span>
</div>
<div class="tl-title"><?= h($e['titel']) ?></div>
<?php if ($e['ort']): ?><div class="tl-ort">📍 <?= h($e['ort']) ?></div><?php endif; ?>
<?php if ($e['beschreibung']): ?><div class="tl-desc"><?= h($e['beschreibung']) ?></div><?php endif; ?>
<?php if ($canEdit): ?>
<form method="POST" style="display:inline;" onsubmit="return confirm('Eintrag löschen?')">
<button type="submit" name="delete" value="<?= $e['id'] ?>" class="btn-delete" style="margin-top:8px;">Löschen</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
+157
View File
@@ -0,0 +1,157 @@
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
:root{
--bg:#f0f6ff;--white:#fff;--blue:#2563eb;--blue-lt:#3b82f6;--blue-dk:#1d4ed8;
--sky:#e8f0fe;--sky2:#dbeafe;--ink:#1e293b;--muted:#64748b;--border:#bfdbfe;
--radius:10px;--shadow:0 2px 14px rgba(37,99,235,.09);--shadow-lg:0 6px 28px rgba(37,99,235,.14);
}
body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);min-height:100vh;overflow-x:hidden;}
/* ── Header ─────────────────────────────────────────────────────────────────── */
.site-header{background:var(--blue-dk);padding:10px 16px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:50;box-shadow:0 2px 12px rgba(29,78,216,.3);gap:10px;}
.header-home{display:flex;align-items:center;gap:8px;text-decoration:none;min-width:0;}
.header-title{font-family:'Playfair Display',serif;font-size:1.1rem;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.header-right{display:flex;align-items:center;gap:10px;flex-shrink:0;}
.header-logout{color:#93c5fd;font-size:1.1rem;text-decoration:none;padding:4px;}
.header-logout:hover{color:#fff;}
/* ── Nav ────────────────────────────────────────────────────────────────────── */
.nav-bar{display:flex;align-items:center;padding:0 4px;background:var(--white);border-bottom:2px solid var(--border);position:sticky;top:48px;z-index:40;box-shadow:0 2px 8px rgba(37,99,235,.06);overflow-x:auto;scrollbar-width:none;}
.nav-bar::-webkit-scrollbar{display:none;}
.nav-bar a{display:block;text-decoration:none;font-size:.68rem;font-weight:500;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);padding:12px 12px;border-bottom:2px solid transparent;margin-bottom:-2px;transition:color .18s,border-color .18s;white-space:nowrap;}
.nav-bar a:hover{color:var(--blue);}
.nav-bar a.active{color:var(--blue);border-bottom:2.5px solid var(--blue);font-weight:600;}
/* ── Main / Layout ──────────────────────────────────────────────────────────── */
.main{padding:22px 16px 40px;max-width:960px;margin:0 auto;width:100%;}
.page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;flex-wrap:wrap;gap:10px;}
.section-head{font-family:'Playfair Display',serif;font-size:1.6rem;font-weight:400;color:var(--blue-dk);}
.col-title{font-family:'Playfair Display',serif;font-size:1.1rem;color:var(--blue-dk);margin-bottom:12px;padding-bottom:8px;border-bottom:2px solid var(--sky2);}
/* ── Buttons ────────────────────────────────────────────────────────────────── */
.btn{padding:8px 20px;border:none;border-radius:8px;cursor:pointer;font-family:'Inter',sans-serif;font-size:.83rem;font-weight:500;transition:background .18s,transform .1s;white-space:nowrap;text-decoration:none;display:inline-block;}
.btn-primary{background:var(--blue);color:#fff;}
.btn-primary:hover{background:var(--blue-dk);}
.btn-secondary{padding:7px 14px;background:none;border:1.5px solid var(--border);border-radius:8px;color:var(--muted);font-size:.82rem;font-family:'Inter',sans-serif;font-weight:500;cursor:pointer;text-decoration:none;display:inline-block;transition:border-color .15s,color .15s;}
.btn-secondary:hover{border-color:var(--blue);color:var(--blue);}
.btn-edit{background:none;border:1.5px solid var(--border);cursor:pointer;font-size:.8rem;color:var(--blue);padding:5px 10px;border-radius:7px;transition:background .14s;display:inline-flex;align-items:center;text-decoration:none;}
.btn-edit:hover{background:var(--sky);}
.btn-delete{background:none;border:1.5px solid #fca5a5;cursor:pointer;font-size:.8rem;color:#ef4444;padding:5px 10px;border-radius:7px;display:inline-flex;align-items:center;}
.btn-delete:hover{background:#fef2f2;}
/* ── Add Form ───────────────────────────────────────────────────────────────── */
.add-form{display:flex;gap:9px;flex-wrap:wrap;margin-bottom:22px;padding:16px 14px;background:var(--sky);border:1.5px solid var(--border);border-radius:var(--radius);}
.add-form input,.add-form select,.add-form textarea{flex:1;min-width:0;padding:8px 12px;background:var(--white);border:1.5px solid var(--border);border-radius:7px;font-family:'Inter',sans-serif;font-size:.83rem;color:var(--ink);outline:none;transition:border-color .18s;}
.add-form input:focus,.add-form select:focus,.add-form textarea:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(37,99,235,.1);}
.hidden{display:none!important;}
/* ── Alerts ─────────────────────────────────────────────────────────────────── */
.alert-success{background:#f0fdf4;border:1.5px solid #86efac;border-radius:10px;padding:12px 16px;font-size:.84rem;color:#15803d;margin-bottom:20px;}
.countdown-bar{background:linear-gradient(135deg,var(--blue-dk),var(--blue-lt));color:#fff;border-radius:10px;padding:12px 18px;margin-bottom:18px;font-size:.88rem;text-align:center;}
/* ── Stats ──────────────────────────────────────────────────────────────────── */
.stats-bar{display:flex;gap:8px;margin-bottom:18px;flex-wrap:wrap;align-items:center;}
.stat-pill{background:var(--white);border:1.5px solid var(--border);border-radius:20px;padding:5px 13px;font-size:.76rem;color:var(--muted);}
.stat-pill strong{color:var(--blue);}
.empty{text-align:center;padding:40px 16px;color:var(--muted);font-size:.84rem;}
.empty-icon{font-size:2rem;margin-bottom:8px;}
/* ── Trip Grid (Startseite) ─────────────────────────────────────────────────── */
.trip-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;}
.trip-card{background:var(--white);border:1.5px solid var(--border);border-radius:14px;padding:20px;text-decoration:none;color:var(--ink);display:flex;gap:14px;align-items:flex-start;box-shadow:var(--shadow);transition:transform .15s,box-shadow .15s,border-color .15s;position:relative;}
.trip-card:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg);border-color:var(--blue-lt);}
.trip-card-emoji{font-size:2rem;flex-shrink:0;line-height:1;}
.trip-card-body{flex:1;min-width:0;}
.trip-card-name{font-family:'Playfair Display',serif;font-size:1.1rem;color:var(--blue-dk);margin-bottom:4px;}
.trip-card-ziel{font-size:.78rem;color:var(--muted);margin-bottom:3px;}
.trip-card-date{font-size:.75rem;color:var(--muted);margin-bottom:8px;}
.trip-card-members{display:flex;gap:4px;flex-wrap:wrap;align-items:center;}
.members-more{font-size:.72rem;color:var(--muted);padding:0 6px;}
.countdown-badge{display:inline-block;background:var(--sky2);color:var(--blue-dk);border-radius:20px;padding:1px 8px;font-size:.68rem;font-weight:500;margin-left:6px;}
.countdown-badge.active{background:#fef9c3;color:#854d0e;}
.trip-card-badge{position:absolute;top:12px;right:12px;background:var(--sky2);color:var(--blue-dk);border-radius:20px;padding:2px 8px;font-size:.65rem;font-weight:600;}
/* ── Timeline (Ablauf) ──────────────────────────────────────────────────────── */
.timeline{display:flex;flex-direction:column;gap:20px;}
.tl-group-date{font-size:.72rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--blue);margin-bottom:10px;padding-left:32px;}
.tl-item{position:relative;padding-left:32px;margin-bottom:12px;}
.tl-dot{position:absolute;left:5px;top:14px;width:12px;height:12px;background:var(--blue);border-radius:50%;border:3px solid var(--bg);box-shadow:0 0 0 2px var(--blue-lt);}
.tl-item-inner{background:var(--white);border:1.5px solid var(--border);border-radius:var(--radius);padding:14px 16px;box-shadow:var(--shadow);}
.tl-item-inner:hover{border-color:var(--blue-lt);}
.tl-meta{display:flex;align-items:center;gap:8px;margin-bottom:4px;}
.tl-time{font-size:.72rem;font-weight:600;color:var(--blue);letter-spacing:.05em;}
.tl-kat{font-size:.9rem;}
.tl-title{font-family:'Playfair Display',serif;font-size:1.08rem;color:var(--ink);margin-bottom:3px;}
.tl-ort{font-size:.76rem;color:var(--muted);margin-bottom:3px;}
.tl-desc{font-size:.8rem;color:var(--muted);line-height:1.5;}
/* ── Packliste ──────────────────────────────────────────────────────────────── */
.pack-progress{height:8px;background:var(--sky2);border-radius:10px;margin-bottom:12px;overflow:hidden;}
.pack-progress-bar{height:100%;background:linear-gradient(90deg,var(--blue),var(--blue-lt));border-radius:10px;transition:width .4s;}
.pack-group{margin-bottom:22px;}
.pack-group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;font-size:.8rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
.pack-group-count{font-size:.75rem;color:var(--blue);}
.todo-list{display:flex;flex-direction:column;gap:7px;}
.todo-item{display:flex;align-items:center;gap:9px;background:var(--white);border:1.5px solid var(--border);border-radius:8px;padding:10px 12px;box-shadow:var(--shadow);}
.todo-item.done{opacity:.5;}
.todo-check-form{display:contents;}
.todo-check{width:22px;height:22px;border-radius:50%;border:2px solid var(--blue);background:none;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:background .14s;padding:0;}
.todo-check.checked{background:var(--blue);}
.todo-check.checked::after{content:'✓';color:#fff;font-size:.65rem;font-weight:700;}
.todo-content{flex:1;min-width:0;}
.todo-row1{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
.todo-text{flex:1;min-width:0;font-size:.88rem;}
.todo-item.done .todo-text{text-decoration:line-through;color:var(--muted);}
.badge-menge{font-size:.68rem;background:var(--sky2);color:var(--blue-dk);border-radius:10px;padding:1px 7px;}
.who-badge{font-size:.68rem;padding:2px 8px;border-radius:20px;font-weight:500;}
/* ── Ideen ──────────────────────────────────────────────────────────────────── */
.ideen-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;}
.idee-card{background:var(--white);border:1.5px solid var(--border);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow);}
.idee-header{display:flex;align-items:flex-start;gap:8px;margin-bottom:8px;}
.idee-icon{font-size:1.2rem;flex-shrink:0;}
.idee-title{flex:1;font-weight:500;font-size:.92rem;}
.idee-desc{font-size:.8rem;color:var(--muted);margin-bottom:8px;line-height:1.5;}
.idee-link{font-size:.76rem;color:var(--blue);text-decoration:none;display:block;margin-bottom:10px;word-break:break-all;}
.idee-link:hover{text-decoration:underline;}
.idee-actions{display:flex;gap:6px;flex-wrap:wrap;}
.tl-badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:.68rem;font-weight:500;white-space:nowrap;}
.badge-plan{background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}
.badge-active{background:#fefce8;color:#b45309;border:1px solid #fde68a;}
.badge-done{background:#f0fdf4;color:#16a34a;border:1px solid #bbf7d0;}
/* ── Notizen ────────────────────────────────────────────────────────────────── */
.notizen-list{display:flex;flex-direction:column;gap:16px;}
.notiz-card{background:var(--white);border:1.5px solid var(--border);border-radius:var(--radius);padding:18px;box-shadow:var(--shadow);}
.notiz-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;}
.notiz-title{font-family:'Playfair Display',serif;font-size:1.05rem;color:var(--blue-dk);}
.notiz-inhalt{font-size:.85rem;color:var(--ink);line-height:1.6;white-space:pre-wrap;}
/* ── Dokumente ──────────────────────────────────────────────────────────────── */
.doc-list{display:flex;flex-direction:column;gap:10px;}
.doc-item{background:var(--white);border:1.5px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;align-items:center;gap:12px;box-shadow:var(--shadow);}
.doc-icon{font-size:1.5rem;flex-shrink:0;}
.doc-info{flex:1;min-width:0;}
.doc-name{font-weight:500;font-size:.88rem;margin-bottom:2px;}
.doc-meta{font-size:.73rem;color:var(--muted);}
.doc-actions{display:flex;gap:6px;flex-shrink:0;}
/* ── Team ───────────────────────────────────────────────────────────────────── */
.settings-section{background:var(--white);border:1.5px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:20px;box-shadow:var(--shadow);}
.member-list{display:flex;flex-direction:column;gap:10px;margin-bottom:20px;}
.member-item{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.member-info{flex:1;min-width:0;}
.member-name{font-weight:500;font-size:.88rem;}
.member-email{font-size:.73rem;color:var(--muted);}
.access-badge{font-size:.68rem;padding:2px 9px;border-radius:20px;font-weight:600;}
.access-admin{background:#fef3c7;color:#92400e;border:1px solid #fde68a;}
.access-edit{background:#dbeafe;color:#1e40af;border:1px solid #bfdbfe;}
.access-see{background:#f1f5f9;color:#475569;border:1px solid #cbd5e1;}
.invite-form{background:var(--sky);border-radius:10px;padding:14px 16px;}
@media(max-width:600px){
.trip-grid{grid-template-columns:1fr;}
.ideen-grid{grid-template-columns:1fr;}
.add-form{flex-direction:column;}
.doc-actions{flex-direction:column;}
.member-item{flex-wrap:wrap;}
}
+10
View File
@@ -0,0 +1,10 @@
function toggleForm(id) {
const el = document.getElementById(id);
if (el) el.classList.toggle('hidden');
}
// Auto-close flash messages
document.addEventListener('DOMContentLoaded', () => {
const alerts = document.querySelectorAll('.alert-success');
alerts.forEach(a => setTimeout(() => { a.style.opacity = '0'; a.style.transition = 'opacity .4s'; setTimeout(() => a.remove(), 400); }, 4000));
});
+236
View File
@@ -0,0 +1,236 @@
<?php
date_default_timezone_set('Europe/Berlin');
define('SITE_ICON', '📑');
define('SITE_TITLE', 'Lütte Planer');
define('MAIL_FROM', 'jason@illg.de');
define('MAIL_FROM_NAME', 'Lütte Planer');
define('DATA_DIR', __DIR__ . '/data/');
define('UPLOAD_DIR', DATA_DIR . 'uploads/');
define('UPLOAD_MAX_MB', 10);
define('UPLOAD_ACCEPT', '.pdf,.jpg,.jpeg,.png,.gif,.webp,.doc,.docx');
// Google OAuth — wird später befüllt
define('GOOGLE_CLIENT_ID', '');
define('GOOGLE_CLIENT_SECRET', '');
define('GOOGLE_REDIRECT_URI', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/Develop/PlanerGoogel/auth/callback.php');
require_once __DIR__ . '/db.php';
// ── Session ───────────────────────────────────────────────────────────────────
function startSession(): void {
if (session_status() === PHP_SESSION_NONE) session_start();
}
function requireLogin(): void {
startSession();
if (empty($_SESSION['user_id'])) {
header('Location: /Develop/PlanerGoogel/login.php'); exit;
}
}
function currentUser(): ?array {
startSession();
if (empty($_SESSION['user_id'])) return null;
return dbOne('SELECT * FROM users WHERE id = ?', [$_SESSION['user_id']]);
}
function loginUser(array $user): void {
startSession();
$_SESSION['user_id'] = $user['id'];
}
// ── Reisen-Helfer ─────────────────────────────────────────────────────────────
function getTrip(string $id): ?array {
return dbOne('SELECT * FROM reisen WHERE id = ?', [$id]);
}
function getTripMembers(string $tripId): array {
return dbAll(
'SELECT u.*, rz.access FROM users u
JOIN reise_zugriff rz ON rz.user_id = u.id
WHERE rz.trip_id = ?
ORDER BY rz.access DESC, u.name ASC',
[$tripId]
);
}
function userCanSee(string $tripId, int $userId): bool {
return dbOne('SELECT 1 FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]) !== null;
}
function userCanEdit(string $tripId, int $userId): bool {
$row = dbOne('SELECT access FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]);
return $row && in_array($row['access'], ['edit', 'admin']);
}
function userIsAdmin(string $tripId, int $userId): bool {
$row = dbOne('SELECT access FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]);
return $row && $row['access'] === 'admin';
}
function requireTripAccess(string $tripId, bool $editRequired = false): array {
requireLogin();
$user = currentUser();
$trip = getTrip($tripId);
if (!$trip) { http_response_code(404); die('Reise nicht gefunden.'); }
if (!userCanSee($tripId, $user['id'])) { http_response_code(403); die('Kein Zugriff.'); }
if ($editRequired && !userCanEdit($tripId, $user['id'])) { http_response_code(403); die('Keine Berechtigung zum Bearbeiten.'); }
return [$trip, $user];
}
function newUuid(): string {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
);
}
// ── Datum-Helfer ──────────────────────────────────────────────────────────────
function germanDate(string $date): string {
if (!$date) return '';
$ts = strtotime($date);
$monate = ['Januar','Februar','März','April','Mai','Juni',
'Juli','August','September','Oktober','November','Dezember'];
return date('j', $ts) . '. ' . $monate[date('n', $ts) - 1] . ' ' . date('Y', $ts);
}
function daysUntil(string $date): int {
return (int) ceil((strtotime($date) - time()) / 86400);
}
// ── Avatar-Helfer ─────────────────────────────────────────────────────────────
function userInitials(string $name): string {
$parts = explode(' ', trim($name));
if (count($parts) >= 2) return strtoupper(mb_substr($parts[0], 0, 1) . mb_substr($parts[1], 0, 1));
return strtoupper(mb_substr($name, 0, 2));
}
function avatarHtml(array $user, string $size = '32px'): string {
$initials = userInitials($user['name']);
$farbe = htmlspecialchars($user['farbe'] ?? '#2563eb');
$name = htmlspecialchars($user['name']);
if (!empty($user['avatar_url'])) {
return "<img src=\"{$user['avatar_url']}\" alt=\"{$name}\" style=\"width:{$size};height:{$size};border-radius:50%;object-fit:cover;\" title=\"{$name}\">";
}
return "<span style=\"display:inline-flex;align-items:center;justify-content:center;width:{$size};height:{$size};border-radius:50%;background:{$farbe};color:#fff;font-size:calc({$size} * 0.38);font-weight:600;\" title=\"{$name}\">{$initials}</span>";
}
// ── XSS-Schutz ────────────────────────────────────────────────────────────────
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// ── Packliste-Kategorien ──────────────────────────────────────────────────────
$PACKEN_KATEGORIEN = [
'kleidung' => ['label' => 'Kleidung', 'icon' => '👕'],
'dokumente' => ['label' => 'Dokumente', 'icon' => '📄'],
'elektronik' => ['label' => 'Elektronik', 'icon' => '🔌'],
'medizin' => ['label' => 'Medizin', 'icon' => '💊'],
'toilette' => ['label' => 'Toilette', 'icon' => '🧴'],
'essen' => ['label' => 'Essen & Trinken', 'icon' => '🍿'],
'strand' => ['label' => 'Strand', 'icon' => '🏖'],
'sport' => ['label' => 'Sport', 'icon' => '⚽'],
'sonstiges' => ['label' => 'Sonstiges', 'icon' => '📦'],
];
$UPLOAD_TYPEN = [
'application/pdf' => 'PDF',
'image/jpeg' => 'JPG',
'image/png' => 'PNG',
'image/gif' => 'GIF',
'image/webp' => 'WEBP',
'application/msword' => 'Word',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'Word',
];
// ── E-Mail-Versand ────────────────────────────────────────────────────────────
function sendMail(string $to, string $subject, string $htmlBody): bool {
$fromName = '=?UTF-8?B?' . base64_encode(MAIL_FROM_NAME) . '?=';
$subjectEncoded = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$boundary = md5(uniqid());
$headers = "From: {$fromName} <" . MAIL_FROM . ">\r\n";
$headers .= "Reply-To: " . MAIL_FROM . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$plain = strip_tags(str_replace(['<br>', '<br/>', '<br />', '</p>', '</div>'], "\n", $htmlBody));
$plain = preg_replace('/\n{3,}/', "\n\n", trim($plain));
$body = "--{$boundary}\r\n";
$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n\r\n";
$body .= chunk_split(base64_encode($plain)) . "\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Type: text/html; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n\r\n";
$body .= chunk_split(base64_encode($htmlBody)) . "\r\n";
$body .= "--{$boundary}--";
return mail($to, $subjectEncoded, $body, $headers);
}
function sendInviteMail(string $toEmail, string $tripName, string $inviteLink, string $inviterName): bool {
$subject = $inviterName . ' laedt dich zu "' . $tripName . '" ein';
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Inter,sans-serif;background:#f0f6ff;padding:30px;">
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 4px 20px rgba(37,99,235,.1);">
<div style="text-align:center;margin-bottom:24px;">
<div style="font-size:2.5rem;">📑</div>
<h1 style="font-family:Georgia,serif;font-size:1.4rem;color:#1d4ed8;margin:8px 0 4px;">Lütte Planer</h1>
</div>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:12px;">Hallo!</p>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:20px;">
<strong>' . htmlspecialchars($inviterName) . '</strong> hat dich zur Reise
<strong>„' . htmlspecialchars($tripName) . '"</strong> eingeladen.
</p>
<div style="text-align:center;margin:28px 0;">
<a href="' . htmlspecialchars($inviteLink) . '" style="background:#2563eb;color:#fff;padding:13px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.95rem;">
Einladung annehmen
</a>
</div>
<p style="color:#64748b;font-size:.78rem;margin-top:24px;">
Oder kopiere diesen Link in deinen Browser:<br>
<span style="color:#2563eb;word-break:break-all;">' . htmlspecialchars($inviteLink) . '</span>
</p>
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
<p style="color:#94a3b8;font-size:.72rem;text-align:center;">Lütte Planer · Reisen planen &amp; teilen</p>
</div>
</body></html>';
return sendMail($toEmail, $subject, $html);
}
function sendAddedToTripMail(string $toEmail, string $toName, string $tripName, string $tripUrl, string $inviterName): bool {
$subject = 'Du wurdest zu "' . $tripName . '" hinzugefuegt';
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Inter,sans-serif;background:#f0f6ff;padding:30px;">
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 4px 20px rgba(37,99,235,.1);">
<div style="text-align:center;margin-bottom:24px;">
<div style="font-size:2.5rem;">📑</div>
<h1 style="font-family:Georgia,serif;font-size:1.4rem;color:#1d4ed8;margin:8px 0 4px;">Lütte Planer</h1>
</div>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:12px;">Hallo ' . htmlspecialchars($toName) . '!</p>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:20px;">
<strong>' . htmlspecialchars($inviterName) . '</strong> hat dich zur Reise
<strong>„' . htmlspecialchars($tripName) . '"</strong> hinzugefügt.
</p>
<div style="text-align:center;margin:28px 0;">
<a href="' . htmlspecialchars($tripUrl) . '" style="background:#2563eb;color:#fff;padding:13px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.95rem;">
Zur Reise
</a>
</div>
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
<p style="color:#94a3b8;font-size:.72rem;text-align:center;">Lütte Planer · Reisen planen &amp; teilen</p>
</div>
</body></html>';
return sendMail($toEmail, $subject, $html);
}
+48
View File
@@ -0,0 +1,48 @@
<?php
define('DB_FILE', DATA_DIR . 'planer.sqlite');
function db(): PDO {
static $pdo = null;
if ($pdo === null) {
$isNew = !file_exists(DB_FILE);
$pdo = new PDO('sqlite:' . DB_FILE);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA foreign_keys = ON');
$pdo->exec('PRAGMA journal_mode = WAL');
if ($isNew) {
$pdo->exec(file_get_contents(__DIR__ . '/db.sql'));
}
}
return $pdo;
}
function dbAll(string $sql, array $params = []): array {
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
function dbOne(string $sql, array $params = []): ?array {
$stmt = db()->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row === false ? null : $row;
}
function dbExec(string $sql, array $params = []): void {
$stmt = db()->prepare($sql);
$stmt->execute($params);
}
function dbInsert(string $table, array $data): int {
$cols = array_keys($data);
$sql = 'INSERT INTO ' . $table . ' (' . implode(',', $cols) . ') VALUES (' .
implode(',', array_map(fn($c) => ':' . $c, $cols)) . ')';
dbExec($sql, $data);
return (int) db()->lastInsertId();
}
function dbLastId(): int {
return (int) db()->lastInsertId();
}
+103
View File
@@ -0,0 +1,103 @@
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
google_id TEXT UNIQUE,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar_url TEXT,
farbe TEXT DEFAULT '#2563eb',
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS reisen (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
ziel TEXT,
start_datum TEXT,
end_datum TEXT,
cover_emoji TEXT DEFAULT '✈️',
beschreibung TEXT,
created_by INTEGER REFERENCES users(id),
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS reise_zugriff (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
access TEXT NOT NULL DEFAULT 'see',
invited_by INTEGER REFERENCES users(id),
joined_at INTEGER DEFAULT (strftime('%s','now')),
UNIQUE(trip_id, user_id)
);
CREATE TABLE IF NOT EXISTS reise_einladungen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
email TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
access TEXT DEFAULT 'see',
invited_by INTEGER REFERENCES users(id),
created_at INTEGER DEFAULT (strftime('%s','now')),
used_at INTEGER
);
CREATE TABLE IF NOT EXISTS ablauf (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
datum TEXT,
zeit TEXT,
titel TEXT NOT NULL,
beschreibung TEXT,
ort TEXT,
kategorie TEXT DEFAULT 'allgemein',
sort_order INTEGER DEFAULT 0,
created_by INTEGER REFERENCES users(id),
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS packliste (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
name TEXT NOT NULL,
menge TEXT,
kat TEXT DEFAULT 'sonstiges',
packed INTEGER DEFAULT 0,
assigned_to INTEGER REFERENCES users(id),
created_by INTEGER REFERENCES users(id),
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS ideen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
titel TEXT NOT NULL,
beschreibung TEXT,
link TEXT,
status TEXT DEFAULT 'offen',
created_by INTEGER REFERENCES users(id),
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS notizen (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
titel TEXT NOT NULL,
inhalt TEXT,
created_by INTEGER REFERENCES users(id),
updated_at INTEGER DEFAULT (strftime('%s','now')),
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS dokumente (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id TEXT NOT NULL REFERENCES reisen(id) ON DELETE CASCADE,
name TEXT NOT NULL,
dateiname TEXT NOT NULL,
mime_type TEXT,
groesse INTEGER,
created_by INTEGER REFERENCES users(id),
created_at INTEGER DEFAULT (strftime('%s','now'))
);
+108
View File
@@ -0,0 +1,108 @@
<?php
require_once __DIR__ . '/config.php';
$tripId = $_GET['trip'] ?? '';
[$trip, $user] = requireTripAccess($tripId);
$canEdit = userCanEdit($tripId, $user['id']);
global $UPLOAD_TYPEN;
$uploadDir = UPLOAD_DIR . $tripId . '/';
// Upload
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload'])) {
$file = $_FILES['datei'] ?? null;
$name = trim($_POST['name'] ?? '') ?: ($file['name'] ?? '');
if ($file && $file['error'] === UPLOAD_ERR_OK && $name) {
$mime = mime_content_type($file['tmp_name']);
if (isset($UPLOAD_TYPEN[$mime]) && $file['size'] <= UPLOAD_MAX_MB * 1024 * 1024) {
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid('doc_') . '.' . $ext;
if (move_uploaded_file($file['tmp_name'], $uploadDir . $filename)) {
dbInsert('dokumente', ['trip_id' => $tripId, 'name' => $name, 'dateiname' => $filename, 'mime_type' => $mime, 'groesse' => $file['size'], 'created_by' => $user['id']]);
}
}
}
header('Location: /Develop/PlanerGoogel/dokumente.php?trip=' . $tripId); exit;
}
// Löschen
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
$doc = dbOne('SELECT * FROM dokumente WHERE id = ? AND trip_id = ?', [(int)$_POST['delete'], $tripId]);
if ($doc) {
@unlink($uploadDir . $doc['dateiname']);
dbExec('DELETE FROM dokumente WHERE id = ?', [$doc['id']]);
}
header('Location: /Develop/PlanerGoogel/dokumente.php?trip=' . $tripId); exit;
}
// Download
if (isset($_GET['dl'])) {
$doc = dbOne('SELECT * FROM dokumente WHERE id = ? AND trip_id = ?', [(int)$_GET['dl'], $tripId]);
if ($doc && file_exists($uploadDir . $doc['dateiname'])) {
header('Content-Type: ' . $doc['mime_type']);
header('Content-Disposition: attachment; filename="' . addslashes($doc['name']) . '"');
readfile($uploadDir . $doc['dateiname']); exit;
}
http_response_code(404); exit;
}
$docs = dbAll('SELECT d.*, u.name as user_name FROM dokumente d LEFT JOIN users u ON u.id = d.created_by WHERE d.trip_id = ? ORDER BY d.created_at DESC', [$tripId]);
function formatSize(int $bytes): string {
if ($bytes < 1024) return $bytes . ' B';
if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB';
return round($bytes / 1048576, 1) . ' MB';
}
$pageTitle = 'Dokumente';
$activeTab = 'dokumente';
include __DIR__ . '/includes/header.php';
?>
<main class="main">
<div class="page-header">
<h1 class="section-head">Dokumente</h1>
<?php if ($canEdit): ?>
<button class="btn btn-primary" onclick="toggleForm('add-form')">+ Upload</button>
<?php endif; ?>
</div>
<?php if ($canEdit): ?>
<div id="add-form" class="add-form hidden">
<form method="POST" enctype="multipart/form-data" style="display:contents;">
<input type="text" name="name" placeholder="Bezeichnung (optional)">
<input type="file" name="datei" accept="<?= UPLOAD_ACCEPT ?>" required style="flex:1;">
<button type="submit" name="upload" class="btn btn-primary">Hochladen</button>
</form>
<div style="width:100%;font-size:.72rem;color:var(--muted);margin-top:-8px;">Max. <?= UPLOAD_MAX_MB ?> MB · PDF, JPG, PNG, Word</div>
</div>
<?php endif; ?>
<?php if (empty($docs)): ?>
<div class="empty"><div class="empty-icon">📁</div><p>Noch keine Dokumente hochgeladen.</p></div>
<?php else: ?>
<div class="doc-list">
<?php foreach ($docs as $doc):
$icon = str_contains($doc['mime_type'], 'pdf') ? '📄' : (str_contains($doc['mime_type'], 'image') ? '🖼️' : '📎');
?>
<div class="doc-item">
<span class="doc-icon"><?= $icon ?></span>
<div class="doc-info">
<div class="doc-name"><?= h($doc['name']) ?></div>
<div class="doc-meta"><?= formatSize((int)$doc['groesse']) ?> · <?= date('d.m.Y', $doc['created_at']) ?><?= $doc['user_name'] ? ' · ' . h($doc['user_name']) : '' ?></div>
</div>
<div class="doc-actions">
<a href="/Develop/PlanerGoogel/dokumente.php?trip=<?= $tripId ?>&dl=<?= $doc['id'] ?>" class="btn-edit">⬇ Download</a>
<?php if ($canEdit): ?>
<form method="POST" onsubmit="return confirm('Dokument löschen?')" style="display:inline;">
<button type="submit" name="delete" value="<?= $doc['id'] ?>" class="btn-delete">✕</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+107
View File
@@ -0,0 +1,107 @@
<?php
require_once __DIR__ . '/config.php';
$tripId = $_GET['trip'] ?? '';
[$trip, $user] = requireTripAccess($tripId);
$canEdit = userCanEdit($tripId, $user['id']);
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
dbInsert('ideen', [
'trip_id' => $tripId,
'titel' => trim($_POST['titel'] ?? ''),
'beschreibung'=> trim($_POST['beschreibung'] ?? ''),
'link' => trim($_POST['link'] ?? ''),
'status' => 'offen',
'created_by' => $user['id'],
]);
header('Location: /Develop/PlanerGoogel/ideen.php?trip=' . $tripId); exit;
}
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['status'])) {
$statuses = ['offen', 'geplant', 'erledigt'];
$newStatus = $_POST['status'];
if (in_array($newStatus, $statuses)) {
dbExec('UPDATE ideen SET status = ? WHERE id = ? AND trip_id = ?', [$newStatus, (int)$_POST['id'], $tripId]);
}
header('Location: /Develop/PlanerGoogel/ideen.php?trip=' . $tripId); exit;
}
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
dbExec('DELETE FROM ideen WHERE id = ? AND trip_id = ?', [(int)$_POST['delete'], $tripId]);
header('Location: /Develop/PlanerGoogel/ideen.php?trip=' . $tripId); exit;
}
$ideen = dbAll(
'SELECT i.*, u.name as user_name FROM ideen i
LEFT JOIN users u ON u.id = i.created_by
WHERE i.trip_id = ?
ORDER BY CASE i.status WHEN "geplant" THEN 0 WHEN "offen" THEN 1 ELSE 2 END, i.id DESC',
[$tripId]
);
$statusMap = [
'offen' => ['label' => 'Offen', 'class' => 'badge-plan', 'icon' => '💡'],
'geplant' => ['label' => 'Geplant', 'class' => 'badge-active', 'icon' => '📅'],
'erledigt' => ['label' => 'Erledigt', 'class' => 'badge-done', 'icon' => '✅'],
];
$pageTitle = 'Ideen';
$activeTab = 'ideen';
include __DIR__ . '/includes/header.php';
?>
<main class="main">
<div class="page-header">
<h1 class="section-head">Ideen</h1>
<?php if ($canEdit): ?>
<button class="btn btn-primary" onclick="toggleForm('add-form')">+ Idee</button>
<?php endif; ?>
</div>
<?php if ($canEdit): ?>
<div id="add-form" class="add-form hidden">
<form method="POST" style="display:contents;">
<input type="text" name="titel" placeholder="Idee *" required style="min-width:200px;">
<input type="text" name="beschreibung" placeholder="Beschreibung">
<input type="url" name="link" placeholder="Link (optional)">
<button type="submit" name="add" class="btn btn-primary">Hinzufügen</button>
</form>
</div>
<?php endif; ?>
<?php if (empty($ideen)): ?>
<div class="empty"><div class="empty-icon">💡</div><p>Noch keine Ideen gesammelt.</p></div>
<?php else: ?>
<div class="ideen-grid">
<?php foreach ($ideen as $ide): $s = $statusMap[$ide['status']] ?? $statusMap['offen']; ?>
<div class="idee-card">
<div class="idee-header">
<span class="idee-icon"><?= $s['icon'] ?></span>
<div class="idee-title"><?= h($ide['titel']) ?></div>
<span class="tl-badge <?= $s['class'] ?>"><?= $s['label'] ?></span>
</div>
<?php if ($ide['beschreibung']): ?>
<div class="idee-desc"><?= h($ide['beschreibung']) ?></div>
<?php endif; ?>
<?php if ($ide['link']): ?>
<a href="<?= h($ide['link']) ?>" target="_blank" rel="noopener" class="idee-link">🔗 <?= h(parse_url($ide['link'], PHP_URL_HOST) ?: $ide['link']) ?></a>
<?php endif; ?>
<?php if ($canEdit): ?>
<div class="idee-actions">
<?php foreach (['offen', 'geplant', 'erledigt'] as $st): if ($st === $ide['status']) continue; ?>
<form method="POST" style="display:inline;">
<input type="hidden" name="id" value="<?= $ide['id'] ?>">
<button type="submit" name="status" value="<?= $st ?>" class="btn-secondary" style="font-size:.72rem;padding:4px 10px;"><?= $statusMap[$st]['label'] ?></button>
</form>
<?php endforeach; ?>
<form method="POST" style="display:inline;" onsubmit="return confirm('Idee löschen?')">
<button type="submit" name="delete" value="<?= $ide['id'] ?>" class="btn-delete">✕</button>
</form>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
+3
View File
@@ -0,0 +1,3 @@
<script src="/Develop/PlanerGoogel/assets/js/app.js"></script>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<?php
// Erwartet: $pageTitle, $trip (optional), $activeTab (optional)
$user = currentUser();
$tripId = $trip['id'] ?? '';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($pageTitle ?? SITE_TITLE) ?> <?= SITE_TITLE ?></title>
<link rel="manifest" href="/Develop/PlanerGoogel/manifest.json">
<meta name="theme-color" content="#1d4ed8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/Develop/PlanerGoogel/assets/css/style.css">
</head>
<body>
<header class="site-header">
<a href="/Develop/PlanerGoogel/" class="header-home" title="Alle Reisen">
<span><?= SITE_ICON ?></span>
<span class="header-title"><?= $trip ? h($trip['cover_emoji'] . ' ' . $trip['name']) : SITE_TITLE ?></span>
</a>
<div class="header-right">
<span class="header-user">
<?= avatarHtml($user, '28px') ?>
</span>
<a href="/Develop/PlanerGoogel/logout.php" class="header-logout" title="Abmelden">↩</a>
</div>
</header>
<?php if ($trip): ?>
<nav class="nav-bar">
<?php
$tabs = [
'ablauf' => ['label' => 'Ablauf', 'url' => '/Develop/PlanerGoogel/ablauf.php?trip=' . $tripId],
'packen' => ['label' => 'Packen', 'url' => '/Develop/PlanerGoogel/packen.php?trip=' . $tripId],
'ideen' => ['label' => 'Ideen', 'url' => '/Develop/PlanerGoogel/ideen.php?trip=' . $tripId],
'notizen' => ['label' => 'Notizen', 'url' => '/Develop/PlanerGoogel/notizen.php?trip=' . $tripId],
'dokumente' => ['label' => 'Dokumente', 'url' => '/Develop/PlanerGoogel/dokumente.php?trip=' . $tripId],
'team' => ['label' => 'Team', 'url' => '/Develop/PlanerGoogel/team.php?trip=' . $tripId],
];
foreach ($tabs as $key => $tab):
$active = ($activeTab ?? '') === $key ? ' active' : '';
?>
<a href="<?= $tab['url'] ?>" class="<?= $active ?>"><?= $tab['label'] ?></a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
+102
View File
@@ -0,0 +1,102 @@
<?php
require_once __DIR__ . '/config.php';
requireLogin();
$user = currentUser();
// Neue Reise erstellen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['neue_reise'])) {
$name = trim($_POST['name'] ?? '');
$ziel = trim($_POST['ziel'] ?? '');
$start = $_POST['start_datum'] ?? '';
$end = $_POST['end_datum'] ?? '';
$emoji = trim($_POST['cover_emoji'] ?? '✈️');
if ($name) {
$id = newUuid();
dbInsert('reisen', ['id' => $id, 'name' => $name, 'ziel' => $ziel, 'start_datum' => $start, 'end_datum' => $end, 'cover_emoji' => $emoji ?: '✈️', 'created_by' => $user['id']]);
dbInsert('reise_zugriff', ['trip_id' => $id, 'user_id' => $user['id'], 'access' => 'admin']);
header('Location: /Develop/PlanerGoogel/ablauf.php?trip=' . $id); exit;
}
}
// Reisen des Users laden
$reisen = dbAll(
'SELECT r.*, rz.access FROM reisen r
JOIN reise_zugriff rz ON rz.trip_id = r.id
WHERE rz.user_id = ?
ORDER BY r.start_datum DESC, r.created_at DESC',
[$user['id']]
);
$pageTitle = 'Meine Reisen';
$trip = null;
include __DIR__ . '/includes/header.php';
?>
<main class="main">
<div class="page-header">
<h1 class="section-head">Meine Reisen</h1>
<button class="btn btn-primary" onclick="document.getElementById('neue-reise-form').classList.toggle('hidden')">+ Neue Reise</button>
</div>
<!-- Neue Reise Form -->
<div id="neue-reise-form" class="add-form hidden" style="margin-bottom:28px;">
<form method="POST" style="display:contents;">
<input type="text" name="name" placeholder="Reisename *" required style="min-width:160px;">
<input type="text" name="ziel" placeholder="Ziel / Ort">
<input type="date" name="start_datum" title="Startdatum">
<input type="date" name="end_datum" title="Enddatum">
<input type="text" name="cover_emoji" placeholder="Emoji" value="✈️" style="max-width:80px;text-align:center;">
<button type="submit" name="neue_reise" class="btn btn-primary">Erstellen</button>
<button type="button" class="btn-secondary" onclick="document.getElementById('neue-reise-form').classList.add('hidden')">Abbrechen</button>
</form>
</div>
<?php if (empty($reisen)): ?>
<div class="empty">
<div class="empty-icon">🗺️</div>
<p>Noch keine Reisen geplant.</p>
<p>Klick auf <strong>+ Neue Reise</strong> um loszulegen!</p>
</div>
<?php else: ?>
<div class="trip-grid">
<?php foreach ($reisen as $r):
$members = getTripMembers($r['id']);
$days = $r['start_datum'] ? daysUntil($r['start_datum']) : null;
?>
<a href="/Develop/PlanerGoogel/ablauf.php?trip=<?= h($r['id']) ?>" class="trip-card">
<div class="trip-card-emoji"><?= h($r['cover_emoji']) ?></div>
<div class="trip-card-body">
<div class="trip-card-name"><?= h($r['name']) ?></div>
<?php if ($r['ziel']): ?>
<div class="trip-card-ziel">📍 <?= h($r['ziel']) ?></div>
<?php endif; ?>
<?php if ($r['start_datum']): ?>
<div class="trip-card-date">
<?= germanDate($r['start_datum']) ?>
<?php if ($r['end_datum']): ?> <?= germanDate($r['end_datum']) ?><?php endif; ?>
<?php if ($days !== null && $days > 0): ?>
<span class="countdown-badge">in <?= $days ?> Tagen</span>
<?php elseif ($days !== null && $days <= 0 && $days > -30): ?>
<span class="countdown-badge active">Läuft!</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="trip-card-members">
<?php foreach (array_slice($members, 0, 5) as $m): ?>
<?= avatarHtml($m, '24px') ?>
<?php endforeach; ?>
<?php if (count($members) > 5): ?>
<span class="members-more">+<?= count($members) - 5 ?></span>
<?php endif; ?>
</div>
</div>
<?php if ($r['access'] === 'admin'): ?>
<span class="trip-card-badge">Admin</span>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
+124
View File
@@ -0,0 +1,124 @@
<?php
require_once __DIR__ . '/config.php';
startSession();
// Einladungs-Token prüfen
$inviteToken = $_GET['invite'] ?? '';
$inviteTrip = null;
if ($inviteToken) {
$invite = dbOne('SELECT * FROM reise_einladungen WHERE token = ? AND used_at IS NULL', [$inviteToken]);
if ($invite) $inviteTrip = getTrip($invite['trip_id']);
}
// Mock-Login verarbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mock_login'])) {
$name = trim($_POST['name'] ?? '');
$email = strtolower(trim($_POST['email'] ?? ''));
if ($name && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$user = dbOne('SELECT * FROM users WHERE email = ?', [$email]);
if (!$user) {
$farben = ['#2563eb','#7c3aed','#db2777','#059669','#d97706','#dc2626'];
$farbe = $farben[array_rand($farben)];
$id = dbInsert('users', ['email' => $email, 'name' => $name, 'farbe' => $farbe]);
$user = dbOne('SELECT * FROM users WHERE id = ?', [$id]);
}
loginUser($user);
// Einladung einlösen
if ($inviteToken && $invite) {
$exists = dbOne('SELECT 1 FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$invite['trip_id'], $user['id']]);
if (!$exists) {
dbInsert('reise_zugriff', ['trip_id' => $invite['trip_id'], 'user_id' => $user['id'], 'access' => $invite['access'], 'invited_by' => $invite['invited_by']]);
}
dbExec('UPDATE reise_einladungen SET used_at = ? WHERE token = ?', [time(), $inviteToken]);
header('Location: /Develop/PlanerGoogel/ablauf.php?trip=' . urlencode($invite['trip_id'])); exit;
}
header('Location: /Develop/PlanerGoogel/'); exit;
}
$error = 'Bitte Name und gültige E-Mail-Adresse eingeben.';
}
if (!empty($_SESSION['user_id'])) {
header('Location: /Develop/PlanerGoogel/'); exit;
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Anmelden <?= SITE_TITLE ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
:root{--blue:#2563eb;--blue-dk:#1d4ed8;--bg:#f0f6ff;--white:#fff;--border:#bfdbfe;--ink:#1e293b;--muted:#64748b;--radius:12px;}
body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;}
.card{background:var(--white);border-radius:var(--radius);box-shadow:0 8px 40px rgba(37,99,235,.13);padding:40px 36px;width:100%;max-width:420px;}
.logo{text-align:center;margin-bottom:28px;}
.logo-icon{font-size:2.5rem;display:block;margin-bottom:8px;}
.logo-title{font-family:'Playfair Display',serif;font-size:1.6rem;color:var(--blue-dk);}
.logo-sub{font-size:.8rem;color:var(--muted);margin-top:4px;}
.invite-box{background:#f0fdf4;border:1.5px solid #86efac;border-radius:10px;padding:12px 16px;margin-bottom:20px;font-size:.83rem;color:#15803d;}
.invite-box strong{display:block;margin-bottom:2px;}
/* Google Button (Platzhalter) */
.btn-google{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:11px 16px;background:#fff;border:1.5px solid #dadce0;border-radius:8px;font-family:'Inter',sans-serif;font-size:.9rem;font-weight:500;color:#3c4043;cursor:not-allowed;opacity:.6;position:relative;}
.btn-google svg{width:18px;height:18px;flex-shrink:0;}
.btn-google-badge{position:absolute;right:12px;font-size:.65rem;background:#fef9c3;color:#854d0e;border-radius:4px;padding:2px 6px;border:1px solid #fde68a;}
.divider{display:flex;align-items:center;gap:10px;margin:20px 0;color:var(--muted);font-size:.78rem;}
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--border);}
label{display:block;font-size:.78rem;font-weight:500;color:var(--muted);margin-bottom:4px;margin-top:14px;}
label:first-of-type{margin-top:0;}
input{width:100%;padding:10px 13px;border:1.5px solid var(--border);border-radius:8px;font-family:'Inter',sans-serif;font-size:.88rem;color:var(--ink);outline:none;transition:border-color .18s;}
input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(37,99,235,.1);}
.btn-login{width:100%;margin-top:18px;padding:11px;background:var(--blue);color:#fff;border:none;border-radius:8px;font-family:'Inter',sans-serif;font-size:.9rem;font-weight:500;cursor:pointer;transition:background .18s;}
.btn-login:hover{background:var(--blue-dk);}
.error{background:#fef2f2;border:1.5px solid #fca5a5;border-radius:8px;padding:10px 14px;font-size:.82rem;color:#dc2626;margin-bottom:16px;}
.note{font-size:.73rem;color:var(--muted);text-align:center;margin-top:16px;line-height:1.5;}
</style>
</head>
<body>
<div class="card">
<div class="logo">
<span class="logo-icon"><?= SITE_ICON ?></span>
<div class="logo-title"><?= SITE_TITLE ?></div>
<div class="logo-sub">Reisen planen, teilen und erleben</div>
</div>
<?php if ($inviteTrip): ?>
<div class="invite-box">
<strong>Du wurdest eingeladen!</strong>
Melde dich an, um der Reise <em><?= h($inviteTrip['name']) ?></em> beizutreten.
</div>
<?php endif; ?>
<!-- Google Login (kommt später) -->
<button class="btn-google" disabled title="Kommt bald">
<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.18 1.48-4.97 2.31-8.16 2.31-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>
Mit Google anmelden
<span class="btn-google-badge">Kommt bald</span>
</button>
<div class="divider">oder zum Testen</div>
<?php if (!empty($error)): ?>
<div class="error"><?= h($error) ?></div>
<?php endif; ?>
<form method="POST">
<?php if ($inviteToken): ?><input type="hidden" name="invite" value="<?= h($inviteToken) ?>"><?php endif; ?>
<label for="name">Dein Name</label>
<input type="text" id="name" name="name" placeholder="z.B. Jason" required value="<?= h($_POST['name'] ?? '') ?>">
<label for="email">E-Mail-Adresse</label>
<input type="email" id="email" name="email" placeholder="deine@email.de" required value="<?= h($_POST['email'] ?? '') ?>">
<button type="submit" name="mock_login" class="btn-login">Anmelden / Registrieren</button>
</form>
<p class="note">Diese Anmeldung ist nur zum Testen.<br>Google Login wird später aktiviert.</p>
</div>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/config.php';
startSession();
session_destroy();
header('Location: /Develop/PlanerGoogel/login.php'); exit;
+12
View File
@@ -0,0 +1,12 @@
{
"name": "Lütte Planer",
"short_name": "Planer",
"start_url": "/Develop/PlanerGoogel/",
"display": "standalone",
"theme_color": "#1d4ed8",
"background_color": "#f0f6ff",
"icons": [
{ "src": "/Develop/PlanerGoogel/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/Develop/PlanerGoogel/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
+80
View File
@@ -0,0 +1,80 @@
<?php
require_once __DIR__ . '/config.php';
$tripId = $_GET['trip'] ?? '';
[$trip, $user] = requireTripAccess($tripId);
$canEdit = userCanEdit($tripId, $user['id']);
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
$titel = trim($_POST['titel'] ?? '');
if ($titel) {
dbInsert('notizen', ['trip_id' => $tripId, 'titel' => $titel, 'inhalt' => trim($_POST['inhalt'] ?? ''), 'created_by' => $user['id']]);
}
header('Location: /Develop/PlanerGoogel/notizen.php?trip=' . $tripId); exit;
}
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) {
dbExec('UPDATE notizen SET inhalt = ?, updated_at = ? WHERE id = ? AND trip_id = ?',
[trim($_POST['inhalt'] ?? ''), time(), (int)$_POST['id'], $tripId]);
header('Location: /Develop/PlanerGoogel/notizen.php?trip=' . $tripId . '#notiz-' . (int)$_POST['id']); exit;
}
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
dbExec('DELETE FROM notizen WHERE id = ? AND trip_id = ?', [(int)$_POST['delete'], $tripId]);
header('Location: /Develop/PlanerGoogel/notizen.php?trip=' . $tripId); exit;
}
$notizen = dbAll('SELECT * FROM notizen WHERE trip_id = ? ORDER BY updated_at DESC', [$tripId]);
$pageTitle = 'Notizen';
$activeTab = 'notizen';
include __DIR__ . '/includes/header.php';
?>
<main class="main">
<div class="page-header">
<h1 class="section-head">Notizen</h1>
<?php if ($canEdit): ?>
<button class="btn btn-primary" onclick="toggleForm('add-form')">+ Notiz</button>
<?php endif; ?>
</div>
<?php if ($canEdit): ?>
<div id="add-form" class="add-form hidden">
<form method="POST" style="display:contents;flex-direction:column;width:100%;">
<input type="text" name="titel" placeholder="Titel *" required>
<textarea name="inhalt" placeholder="Inhalt…" rows="3" style="flex:1;min-width:0;padding:8px 13px;background:#fff;border:1.5px solid var(--border);border-radius:7px;font-family:'Inter',sans-serif;font-size:.83rem;resize:vertical;"></textarea>
<button type="submit" name="add" class="btn btn-primary">Erstellen</button>
</form>
</div>
<?php endif; ?>
<?php if (empty($notizen)): ?>
<div class="empty"><div class="empty-icon">📝</div><p>Noch keine Notizen.</p></div>
<?php else: ?>
<div class="notizen-list">
<?php foreach ($notizen as $n): ?>
<div class="notiz-card" id="notiz-<?= $n['id'] ?>">
<div class="notiz-header">
<div class="notiz-title"><?= h($n['titel']) ?></div>
<?php if ($canEdit): ?>
<form method="POST" onsubmit="return confirm('Notiz löschen?')" style="display:inline;">
<button type="submit" name="delete" value="<?= $n['id'] ?>" class="btn-delete">✕</button>
</form>
<?php endif; ?>
</div>
<?php if ($canEdit): ?>
<form method="POST">
<input type="hidden" name="id" value="<?= $n['id'] ?>">
<textarea name="inhalt" rows="6" style="width:100%;padding:10px;border:1.5px solid var(--border);border-radius:8px;font-family:'Inter',sans-serif;font-size:.85rem;resize:vertical;margin-bottom:8px;"><?= h($n['inhalt'] ?? '') ?></textarea>
<button type="submit" name="save" class="btn btn-primary" style="font-size:.8rem;padding:6px 16px;">Speichern</button>
</form>
<?php else: ?>
<div class="notiz-inhalt"><?= nl2br(h($n['inhalt'] ?? '')) ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
+156
View File
@@ -0,0 +1,156 @@
<?php
require_once __DIR__ . '/config.php';
$tripId = $_GET['trip'] ?? '';
[$trip, $user] = requireTripAccess($tripId);
$canEdit = userCanEdit($tripId, $user['id']);
$members = getTripMembers($tripId);
global $PACKEN_KATEGORIEN;
// Item hinzufügen
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
dbInsert('packliste', [
'trip_id' => $tripId,
'name' => trim($_POST['name'] ?? ''),
'menge' => trim($_POST['menge'] ?? ''),
'kat' => $_POST['kat'] ?? 'sonstiges',
'assigned_to' => ($_POST['assigned_to'] ?? '') ?: null,
'created_by' => $user['id'],
]);
header('Location: /Develop/PlanerGoogel/packen.php?trip=' . $tripId); exit;
}
// Packed toggling
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle'])) {
$item = dbOne('SELECT * FROM packliste WHERE id = ? AND trip_id = ?', [(int)$_POST['toggle'], $tripId]);
if ($item) {
dbExec('UPDATE packliste SET packed = ? WHERE id = ?', [$item['packed'] ? 0 : 1, $item['id']]);
}
header('Location: /Develop/PlanerGoogel/packen.php?trip=' . $tripId); exit;
}
// Löschen
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
dbExec('DELETE FROM packliste WHERE id = ? AND trip_id = ?', [(int)$_POST['delete'], $tripId]);
header('Location: /Develop/PlanerGoogel/packen.php?trip=' . $tripId); exit;
}
// Alle zurücksetzen
if ($canEdit && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['reset_all'])) {
dbExec('UPDATE packliste SET packed = 0 WHERE trip_id = ?', [$tripId]);
header('Location: /Develop/PlanerGoogel/packen.php?trip=' . $tripId); exit;
}
$items = dbAll(
'SELECT p.*, u.name as assigned_name, u.farbe as assigned_farbe FROM packliste p
LEFT JOIN users u ON u.id = p.assigned_to
WHERE p.trip_id = ?
ORDER BY p.kat ASC, p.id ASC',
[$tripId]
);
// Nach Kategorie gruppieren
$grouped = [];
foreach ($items as $item) {
$grouped[$item['kat']][] = $item;
}
$total = count($items);
$packed = count(array_filter($items, fn($i) => $i['packed']));
$pct = $total > 0 ? round($packed / $total * 100) : 0;
$pageTitle = 'Packliste';
$activeTab = 'packen';
include __DIR__ . '/includes/header.php';
?>
<main class="main">
<div class="page-header">
<h1 class="section-head">Packliste</h1>
<div style="display:flex;gap:8px;align-items:center;">
<?php if ($canEdit): ?>
<button class="btn btn-primary" onclick="toggleForm('add-form')">+ Item</button>
<?php endif; ?>
</div>
</div>
<?php if ($total > 0): ?>
<div class="pack-progress">
<div class="pack-progress-bar" style="width:<?= $pct ?>%"></div>
</div>
<div class="stats-bar">
<span class="stat-pill"><strong><?= $packed ?></strong> / <?= $total ?> gepackt</span>
<span class="stat-pill"><strong><?= $pct ?>%</strong></span>
<?php if ($canEdit && $packed > 0): ?>
<form method="POST" style="margin:0;">
<button type="submit" name="reset_all" class="btn-secondary" style="font-size:.73rem;padding:4px 10px;">↺ Zurücksetzen</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($canEdit): ?>
<div id="add-form" class="add-form hidden">
<form method="POST" style="display:contents;">
<input type="text" name="name" placeholder="Was packen? *" required style="min-width:180px;">
<input type="text" name="menge" placeholder="Menge (z.B. 2x)" style="max-width:100px;">
<select name="kat">
<?php foreach ($PACKEN_KATEGORIEN as $k => $v): ?>
<option value="<?= $k ?>"><?= $v['icon'] ?> <?= $v['label'] ?></option>
<?php endforeach; ?>
</select>
<select name="assigned_to">
<option value="">Für wen?</option>
<?php foreach ($members as $m): ?>
<option value="<?= $m['id'] ?>"><?= h($m['name']) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" name="add" class="btn btn-primary">Hinzufügen</button>
</form>
</div>
<?php endif; ?>
<?php if (empty($items)): ?>
<div class="empty"><div class="empty-icon">🧳</div><p>Noch nichts auf der Packliste.</p></div>
<?php else: ?>
<?php foreach ($grouped as $kat => $katItems):
$katInfo = $PACKEN_KATEGORIEN[$kat] ?? ['icon' => '📦', 'label' => ucfirst($kat)];
$katPacked = count(array_filter($katItems, fn($i) => $i['packed']));
?>
<div class="pack-group">
<div class="pack-group-header">
<span><?= $katInfo['icon'] ?> <?= $katInfo['label'] ?></span>
<span class="pack-group-count"><?= $katPacked ?>/<?= count($katItems) ?></span>
</div>
<div class="todo-list">
<?php foreach ($katItems as $item): ?>
<div class="todo-item <?= $item['packed'] ? 'done' : '' ?>">
<form method="POST" class="todo-check-form">
<button type="submit" name="toggle" value="<?= $item['id'] ?>" class="todo-check <?= $item['packed'] ? 'checked' : '' ?>"></button>
</form>
<div class="todo-content">
<div class="todo-row1">
<span class="todo-text"><?= h($item['name']) ?></span>
<?php if ($item['menge']): ?>
<span class="badge-menge"><?= h($item['menge']) ?></span>
<?php endif; ?>
<?php if ($item['assigned_name']): ?>
<span class="who-badge" style="background:<?= h($item['assigned_farbe']) ?>22;color:<?= h($item['assigned_farbe']) ?>;border:1.5px solid <?= h($item['assigned_farbe']) ?>55;"><?= h($item['assigned_name']) ?></span>
<?php endif; ?>
</div>
</div>
<?php if ($canEdit): ?>
<form method="POST">
<button type="submit" name="delete" value="<?= $item['id'] ?>" class="btn-delete" onclick="return confirm('Löschen?')">✕</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
+200
View File
@@ -0,0 +1,200 @@
<?php
require_once __DIR__ . '/config.php';
$tripId = $_GET['trip'] ?? '';
[$trip, $user] = requireTripAccess($tripId);
$isAdmin = userIsAdmin($tripId, $user['id']);
// Person einladen
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['einladen'])) {
$email = strtolower(trim($_POST['email'] ?? ''));
$access = in_array($_POST['access'] ?? '', ['see','edit']) ? $_POST['access'] : 'see';
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Prüfen ob schon Mitglied
$existing = dbOne('SELECT u.id FROM users u JOIN reise_zugriff rz ON rz.user_id = u.id WHERE u.email = ? AND rz.trip_id = ?', [$email, $tripId]);
if (!$existing) {
// Prüfen ob User schon existiert
$invitedUser = dbOne('SELECT * FROM users WHERE email = ?', [$email]);
if ($invitedUser) {
// Direkt hinzufügen
$alreadyZugriff = dbOne('SELECT 1 FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $invitedUser['id']]);
if (!$alreadyZugriff) {
dbInsert('reise_zugriff', ['trip_id' => $tripId, 'user_id' => $invitedUser['id'], 'access' => $access, 'invited_by' => $user['id']]);
$tripUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . '/Develop/PlanerGoogel/ablauf.php?trip=' . $tripId;
sendAddedToTripMail($invitedUser['email'], $invitedUser['name'], $trip['name'], $tripUrl, $user['name']);
}
$msg = h($invitedUser['name']) . ' wurde zur Reise hinzugefügt und per E-Mail informiert.';
} else {
// Einladungslink erstellen
$token = bin2hex(random_bytes(16));
// Alte offene Einladung für diese E-Mail löschen
dbExec('DELETE FROM reise_einladungen WHERE trip_id = ? AND email = ? AND used_at IS NULL', [$tripId, $email]);
dbInsert('reise_einladungen', ['trip_id' => $tripId, 'email' => $email, 'token' => $token, 'access' => $access, 'invited_by' => $user['id']]);
$inviteLink = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . '/Develop/PlanerGoogel/login.php?invite=' . $token;
$mailSent = sendInviteMail($email, $trip['name'], $inviteLink, $user['name']);
$msg = $mailSent
? 'Einladungsmail an <strong>' . h($email) . '</strong> wurde verschickt!'
: 'Einladungslink erstellt (E-Mail konnte nicht gesendet werden).<br><code style="font-size:.75rem;word-break:break-all;">' . h($inviteLink) . '</code>';
}
} else {
$msg = 'Diese Person ist bereits Mitglied.';
}
}
}
// Zugriff ändern
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_access'])) {
$memberId = (int)$_POST['member_id'];
$newAccess = $_POST['new_access'] ?? '';
if ($memberId !== $user['id'] && in_array($newAccess, ['see','edit','admin'])) {
dbExec('UPDATE reise_zugriff SET access = ? WHERE trip_id = ? AND user_id = ?', [$newAccess, $tripId, $memberId]);
}
header('Location: /Develop/PlanerGoogel/team.php?trip=' . $tripId); exit;
}
// Person entfernen
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove'])) {
$memberId = (int)$_POST['remove'];
if ($memberId !== $user['id']) {
dbExec('DELETE FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $memberId]);
}
header('Location: /Develop/PlanerGoogel/team.php?trip=' . $tripId); exit;
}
// Reise-Einstellungen speichern
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_trip'])) {
dbExec('UPDATE reisen SET name = ?, ziel = ?, start_datum = ?, end_datum = ?, cover_emoji = ?, beschreibung = ? WHERE id = ?', [
trim($_POST['name'] ?? $trip['name']),
trim($_POST['ziel'] ?? ''),
$_POST['start_datum'] ?? '',
$_POST['end_datum'] ?? '',
trim($_POST['cover_emoji'] ?? '✈️') ?: '✈️',
trim($_POST['beschreibung'] ?? ''),
$tripId,
]);
header('Location: /Develop/PlanerGoogel/team.php?trip=' . $tripId); exit;
}
// Reise löschen
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_trip'])) {
dbExec('DELETE FROM reisen WHERE id = ?', [$tripId]);
header('Location: /Develop/PlanerGoogel/'); exit;
}
$members = getTripMembers($tripId);
$invites = dbAll('SELECT * FROM reise_einladungen WHERE trip_id = ? AND used_at IS NULL ORDER BY created_at DESC', [$tripId]);
$baseUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
$pageTitle = 'Team & Einstellungen';
$activeTab = 'team';
include __DIR__ . '/includes/header.php';
?>
<main class="main">
<h1 class="section-head">Team & Einstellungen</h1>
<?php if (!empty($msg)): ?>
<div class="alert-success"><?= $msg ?></div>
<?php endif; ?>
<!-- Mitglieder -->
<div class="settings-section">
<h2 class="col-title">Mitglieder</h2>
<div class="member-list">
<?php foreach ($members as $m): ?>
<div class="member-item">
<?= avatarHtml($m, '38px') ?>
<div class="member-info">
<div class="member-name"><?= h($m['name']) ?></div>
<div class="member-email"><?= h($m['email']) ?></div>
</div>
<span class="access-badge access-<?= $m['access'] ?>"><?= ucfirst($m['access']) ?></span>
<?php if ($isAdmin && $m['id'] != $user['id']): ?>
<form method="POST" style="display:flex;gap:6px;align-items:center;">
<input type="hidden" name="member_id" value="<?= $m['id'] ?>">
<select name="new_access" style="font-size:.78rem;padding:4px 8px;border:1.5px solid var(--border);border-radius:6px;">
<option value="see" <?= $m['access'] === 'see' ? 'selected' : '' ?>>Nur lesen</option>
<option value="edit" <?= $m['access'] === 'edit' ? 'selected' : '' ?>>Bearbeiten</option>
<option value="admin" <?= $m['access'] === 'admin' ? 'selected' : '' ?>>Admin</option>
</select>
<button type="submit" name="change_access" class="btn-edit">Ändern</button>
</form>
<form method="POST" onsubmit="return confirm('Person entfernen?')">
<button type="submit" name="remove" value="<?= $m['id'] ?>" class="btn-delete">✕</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if ($isAdmin): ?>
<div class="invite-form">
<h3 style="font-size:.88rem;font-weight:600;margin-bottom:10px;color:var(--ink);">Person einladen</h3>
<form method="POST" style="display:flex;gap:8px;flex-wrap:wrap;">
<input type="email" name="email" placeholder="E-Mail-Adresse" required style="flex:1;min-width:200px;padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.85rem;">
<select name="access" style="padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.85rem;">
<option value="see">Nur lesen</option>
<option value="edit">Bearbeiten</option>
</select>
<button type="submit" name="einladen" class="btn btn-primary">Einladen</button>
</form>
</div>
<?php if (!empty($invites)): ?>
<div style="margin-top:16px;">
<h3 style="font-size:.82rem;font-weight:600;color:var(--muted);margin-bottom:8px;">Offene Einladungen</h3>
<?php foreach ($invites as $inv): ?>
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--sky);border-radius:8px;margin-bottom:6px;font-size:.8rem;">
<span>✉️ <?= h($inv['email']) ?></span>
<span class="access-badge access-<?= $inv['access'] ?>"><?= ucfirst($inv['access']) ?></span>
<code style="flex:1;font-size:.68rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= $baseUrl ?>/Develop/PlanerGoogel/login.php?invite=<?= $inv['token'] ?></code>
<button onclick="navigator.clipboard.writeText('<?= $baseUrl ?>/Develop/PlanerGoogel/login.php?invite=<?= $inv['token'] ?>').then(()=>alert('Link kopiert!'))" class="btn-edit" type="button">📋 Kopieren</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<!-- Reise-Einstellungen -->
<?php if ($isAdmin): ?>
<div class="settings-section">
<h2 class="col-title">Reise bearbeiten</h2>
<form method="POST" style="display:flex;flex-direction:column;gap:12px;max-width:500px;">
<div>
<label style="font-size:.78rem;font-weight:500;color:var(--muted);display:block;margin-bottom:4px;">Name</label>
<input type="text" name="name" value="<?= h($trip['name']) ?>" required style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.88rem;">
</div>
<div style="display:flex;gap:8px;">
<div style="flex:1;">
<label style="font-size:.78rem;font-weight:500;color:var(--muted);display:block;margin-bottom:4px;">Ziel</label>
<input type="text" name="ziel" value="<?= h($trip['ziel'] ?? '') ?>" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.88rem;">
</div>
<div style="max-width:80px;">
<label style="font-size:.78rem;font-weight:500;color:var(--muted);display:block;margin-bottom:4px;">Emoji</label>
<input type="text" name="cover_emoji" value="<?= h($trip['cover_emoji']) ?>" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.88rem;text-align:center;">
</div>
</div>
<div style="display:flex;gap:8px;">
<div style="flex:1;">
<label style="font-size:.78rem;font-weight:500;color:var(--muted);display:block;margin-bottom:4px;">Start</label>
<input type="date" name="start_datum" value="<?= h($trip['start_datum'] ?? '') ?>" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.88rem;">
</div>
<div style="flex:1;">
<label style="font-size:.78rem;font-weight:500;color:var(--muted);display:block;margin-bottom:4px;">Ende</label>
<input type="date" name="end_datum" value="<?= h($trip['end_datum'] ?? '') ?>" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.88rem;">
</div>
</div>
<button type="submit" name="save_trip" class="btn btn-primary">Speichern</button>
</form>
<div style="margin-top:32px;padding-top:20px;border-top:1.5px solid #fca5a5;">
<h3 style="font-size:.88rem;font-weight:600;color:#dc2626;margin-bottom:8px;">Gefahrenzone</h3>
<form method="POST" onsubmit="return confirm('Reise wirklich löschen? Alle Daten gehen verloren!')">
<button type="submit" name="delete_trip" class="btn" style="background:#dc2626;color:#fff;padding:8px 20px;">Reise löschen</button>
</form>
</div>
</div>
<?php endif; ?>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>