From bd8a322fc0ebb45b7a577b7a39a0ecc815d2182b Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 26 Jun 2026 20:56:30 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20Release:=20L=C3=BCtte=20Planer=20mit?= =?UTF-8?q?=20Google-Login-Vorbereitung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 4 + .htaccess | 10 ++ ablauf.php | 132 ++++++++++++++++++++++++ assets/css/style.css | 157 ++++++++++++++++++++++++++++ assets/js/app.js | 10 ++ config.php | 236 +++++++++++++++++++++++++++++++++++++++++++ db.php | 48 +++++++++ db.sql | 103 +++++++++++++++++++ dokumente.php | 108 ++++++++++++++++++++ icons/icon-192.png | Bin 0 -> 1110 bytes icons/icon-512.png | Bin 0 -> 4096 bytes ideen.php | 107 ++++++++++++++++++++ includes/footer.php | 3 + includes/header.php | 50 +++++++++ index.php | 102 +++++++++++++++++++ login.php | 124 +++++++++++++++++++++++ logout.php | 5 + manifest.json | 12 +++ notizen.php | 80 +++++++++++++++ packen.php | 156 ++++++++++++++++++++++++++++ team.php | 200 ++++++++++++++++++++++++++++++++++++ 21 files changed, 1647 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 ablauf.php create mode 100644 assets/css/style.css create mode 100644 assets/js/app.js create mode 100644 config.php create mode 100644 db.php create mode 100644 db.sql create mode 100644 dokumente.php create mode 100644 icons/icon-192.png create mode 100644 icons/icon-512.png create mode 100644 ideen.php create mode 100644 includes/footer.php create mode 100644 includes/header.php create mode 100644 index.php create mode 100644 login.php create mode 100644 logout.php create mode 100644 manifest.json create mode 100644 notizen.php create mode 100644 packen.php create mode 100644 team.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fa0f53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data/ +*.sqlite +*.sqlite-wal +*.sqlite-shm diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..2c1c184 --- /dev/null +++ b/.htaccess @@ -0,0 +1,10 @@ +Options -Indexes +DirectoryIndex index.php + + + Require all denied + + + + Require all denied + diff --git a/ablauf.php b/ablauf.php new file mode 100644 index 0000000..5220563 --- /dev/null +++ b/ablauf.php @@ -0,0 +1,132 @@ + $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 {$days} Tage bis zur Reise!"; + elseif ($days === 0) $countdown = "πŸŽ‰ Heute geht's los!"; +} +?> + +
+ +
+ + + + + + + + + +
πŸ—“οΈ

Noch keine EintrΓ€ge im Ablauf.

+ + +
+ $items): ?> +
+
+ +
+ +
+
+
+
+ + +
+
+
πŸ“
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+ + diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..0ba7eb8 --- /dev/null +++ b/assets/css/style.css @@ -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;} +} diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..218966f --- /dev/null +++ b/assets/js/app.js @@ -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)); +}); diff --git a/config.php b/config.php new file mode 100644 index 0000000..cfdb8df --- /dev/null +++ b/config.php @@ -0,0 +1,236 @@ += 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 "\"{$name}\""; + } + return "{$initials}"; +} + +// ── 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(['
', '
', '
', '

', ''], "\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 = ' +
+
+
πŸ“‘
+

LΓΌtte Planer

+
+

Hallo!

+

+ ' . htmlspecialchars($inviterName) . ' hat dich zur Reise + β€ž' . htmlspecialchars($tripName) . '" eingeladen. +

+
+ + Einladung annehmen + +
+

+ Oder kopiere diesen Link in deinen Browser:
+ ' . htmlspecialchars($inviteLink) . ' +

+
+

LΓΌtte Planer Β· Reisen planen & teilen

+
+ '; + + 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 = ' +
+
+
πŸ“‘
+

LΓΌtte Planer

+
+

Hallo ' . htmlspecialchars($toName) . '!

+

+ ' . htmlspecialchars($inviterName) . ' hat dich zur Reise + β€ž' . htmlspecialchars($tripName) . '" hinzugefΓΌgt. +

+
+ + Zur Reise + +
+
+

LΓΌtte Planer Β· Reisen planen & teilen

+
+ '; + + return sendMail($toEmail, $subject, $html); +} diff --git a/db.php b/db.php new file mode 100644 index 0000000..7a0bdc1 --- /dev/null +++ b/db.php @@ -0,0 +1,48 @@ +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(); +} diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..bd1a3b2 --- /dev/null +++ b/db.sql @@ -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')) +); diff --git a/dokumente.php b/dokumente.php new file mode 100644 index 0000000..9c1e633 --- /dev/null +++ b/dokumente.php @@ -0,0 +1,108 @@ + $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'; +?> + +
+ + + + + + + +
πŸ“

Noch keine Dokumente hochgeladen.

+ +
+ +
+ +
+
+
Β·
+
+
+ ⬇ Download + +
+ +
+ +
+
+ +
+ +
+ + diff --git a/icons/icon-192.png b/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3a73eb2e2487b8518a1b14c55a8761bcbc7c7b GIT binary patch literal 1110 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 $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'; +?> + +
+ + + + + + + +
πŸ’‘

Noch keine Ideen gesammelt.

+ +
+ +
+
+ +
+ +
+ +
+ + + πŸ”— + + +
+ +
+ + +
+ +
+ +
+
+ +
+ +
+ +
+ + diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..2c72665 --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,3 @@ + + + diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..915bf59 --- /dev/null +++ b/includes/header.php @@ -0,0 +1,50 @@ + + + + + + +<?= h($pageTitle ?? SITE_TITLE) ?> – <?= SITE_TITLE ?> + + + + + + + + + + + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..dcfc92c --- /dev/null +++ b/index.php @@ -0,0 +1,102 @@ + $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'; +?> + +
+ + + + + + +
+
πŸ—ΊοΈ
+

Noch keine Reisen geplant.

+

Klick auf + Neue Reise um loszulegen!

+
+ + + +
+ + diff --git a/login.php b/login.php new file mode 100644 index 0000000..4ad528b --- /dev/null +++ b/login.php @@ -0,0 +1,124 @@ + $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; +} +?> + + + + + +Anmelden – <?= SITE_TITLE ?> + + + + + +
+ + + +
+ Du wurdest eingeladen! + Melde dich an, um der Reise beizutreten. +
+ + + + + +
oder zum Testen
+ + +
+ + +
+ + + + + + +
+ +

Diese Anmeldung ist nur zum Testen.
Google Login wird spΓ€ter aktiviert.

+
+ + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..09d4e21 --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ + $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'; +?> + +
+ + + + + + + +
πŸ“

Noch keine Notizen.

+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ + diff --git a/packen.php b/packen.php new file mode 100644 index 0000000..f1148a6 --- /dev/null +++ b/packen.php @@ -0,0 +1,156 @@ + $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'; +?> + +
+ + + 0): ?> +
+
+
+
+ / gepackt + % + 0): ?> +
+ +
+ +
+ + + + + + + +
🧳

Noch nichts auf der Packliste.

+ + + $katItems): + $katInfo = $PACKEN_KATEGORIEN[$kat] ?? ['icon' => 'πŸ“¦', 'label' => ucfirst($kat)]; + $katPacked = count(array_filter($katItems, fn($i) => $i['packed'])); + ?> +
+
+ + / +
+
+ +
+
+ +
+
+
+ + + + + + + +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ + diff --git a/team.php b/team.php new file mode 100644 index 0000000..c3ae398 --- /dev/null +++ b/team.php @@ -0,0 +1,200 @@ + $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 ' . h($email) . ' wurde verschickt!' + : 'Einladungslink erstellt (E-Mail konnte nicht gesendet werden).
' . h($inviteLink) . ''; + } + } 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'; +?> + +
+

Team & Einstellungen

+ + +
+ + + +
+

Mitglieder

+
+ +
+ +
+
+
+
+ + +
+ + + +
+
+ +
+ +
+ +
+ + +
+

Person einladen

+
+ + + +
+
+ + +
+

Offene Einladungen

+ +
+ βœ‰οΈ + + /Develop/PlanerGoogel/login.php?invite= + +
+ +
+ + +
+ + + +
+

Reise bearbeiten

+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+

Gefahrenzone

+
+ +
+
+
+ +
+ +