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:
@@ -0,0 +1,4 @@
|
||||
data/
|
||||
*.sqlite
|
||||
*.sqlite-wal
|
||||
*.sqlite-shm
|
||||
@@ -0,0 +1,10 @@
|
||||
Options -Indexes
|
||||
DirectoryIndex index.php
|
||||
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(sql|sqlite)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
+132
@@ -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'; ?>
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -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
@@ -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 & 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 & teilen</p>
|
||||
</div>
|
||||
</body></html>';
|
||||
|
||||
return sendMail($toEmail, $subject, $html);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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 |
@@ -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'; ?>
|
||||
@@ -0,0 +1,3 @@
|
||||
<script src="/Develop/PlanerGoogel/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
startSession();
|
||||
session_destroy();
|
||||
header('Location: /Develop/PlanerGoogel/login.php'); exit;
|
||||
@@ -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
@@ -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
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
Reference in New Issue
Block a user