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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 20:56:30 +02:00
co-authored by Claude Sonnet 4.6
commit bd8a322fc0
21 changed files with 1647 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
<?php
date_default_timezone_set('Europe/Berlin');
define('SITE_ICON', '📑');
define('SITE_TITLE', 'Lütte Planer');
define('MAIL_FROM', 'jason@illg.de');
define('MAIL_FROM_NAME', 'Lütte Planer');
define('DATA_DIR', __DIR__ . '/data/');
define('UPLOAD_DIR', DATA_DIR . 'uploads/');
define('UPLOAD_MAX_MB', 10);
define('UPLOAD_ACCEPT', '.pdf,.jpg,.jpeg,.png,.gif,.webp,.doc,.docx');
// Google OAuth — wird später befüllt
define('GOOGLE_CLIENT_ID', '');
define('GOOGLE_CLIENT_SECRET', '');
define('GOOGLE_REDIRECT_URI', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/Develop/PlanerGoogel/auth/callback.php');
require_once __DIR__ . '/db.php';
// ── Session ───────────────────────────────────────────────────────────────────
function startSession(): void {
if (session_status() === PHP_SESSION_NONE) session_start();
}
function requireLogin(): void {
startSession();
if (empty($_SESSION['user_id'])) {
header('Location: /Develop/PlanerGoogel/login.php'); exit;
}
}
function currentUser(): ?array {
startSession();
if (empty($_SESSION['user_id'])) return null;
return dbOne('SELECT * FROM users WHERE id = ?', [$_SESSION['user_id']]);
}
function loginUser(array $user): void {
startSession();
$_SESSION['user_id'] = $user['id'];
}
// ── Reisen-Helfer ─────────────────────────────────────────────────────────────
function getTrip(string $id): ?array {
return dbOne('SELECT * FROM reisen WHERE id = ?', [$id]);
}
function getTripMembers(string $tripId): array {
return dbAll(
'SELECT u.*, rz.access FROM users u
JOIN reise_zugriff rz ON rz.user_id = u.id
WHERE rz.trip_id = ?
ORDER BY rz.access DESC, u.name ASC',
[$tripId]
);
}
function userCanSee(string $tripId, int $userId): bool {
return dbOne('SELECT 1 FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]) !== null;
}
function userCanEdit(string $tripId, int $userId): bool {
$row = dbOne('SELECT access FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]);
return $row && in_array($row['access'], ['edit', 'admin']);
}
function userIsAdmin(string $tripId, int $userId): bool {
$row = dbOne('SELECT access FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]);
return $row && $row['access'] === 'admin';
}
function requireTripAccess(string $tripId, bool $editRequired = false): array {
requireLogin();
$user = currentUser();
$trip = getTrip($tripId);
if (!$trip) { http_response_code(404); die('Reise nicht gefunden.'); }
if (!userCanSee($tripId, $user['id'])) { http_response_code(403); die('Kein Zugriff.'); }
if ($editRequired && !userCanEdit($tripId, $user['id'])) { http_response_code(403); die('Keine Berechtigung zum Bearbeiten.'); }
return [$trip, $user];
}
function newUuid(): string {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
);
}
// ── Datum-Helfer ──────────────────────────────────────────────────────────────
function germanDate(string $date): string {
if (!$date) return '';
$ts = strtotime($date);
$monate = ['Januar','Februar','März','April','Mai','Juni',
'Juli','August','September','Oktober','November','Dezember'];
return date('j', $ts) . '. ' . $monate[date('n', $ts) - 1] . ' ' . date('Y', $ts);
}
function daysUntil(string $date): int {
return (int) ceil((strtotime($date) - time()) / 86400);
}
// ── Avatar-Helfer ─────────────────────────────────────────────────────────────
function userInitials(string $name): string {
$parts = explode(' ', trim($name));
if (count($parts) >= 2) return strtoupper(mb_substr($parts[0], 0, 1) . mb_substr($parts[1], 0, 1));
return strtoupper(mb_substr($name, 0, 2));
}
function avatarHtml(array $user, string $size = '32px'): string {
$initials = userInitials($user['name']);
$farbe = htmlspecialchars($user['farbe'] ?? '#2563eb');
$name = htmlspecialchars($user['name']);
if (!empty($user['avatar_url'])) {
return "<img src=\"{$user['avatar_url']}\" alt=\"{$name}\" style=\"width:{$size};height:{$size};border-radius:50%;object-fit:cover;\" title=\"{$name}\">";
}
return "<span style=\"display:inline-flex;align-items:center;justify-content:center;width:{$size};height:{$size};border-radius:50%;background:{$farbe};color:#fff;font-size:calc({$size} * 0.38);font-weight:600;\" title=\"{$name}\">{$initials}</span>";
}
// ── XSS-Schutz ────────────────────────────────────────────────────────────────
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// ── Packliste-Kategorien ──────────────────────────────────────────────────────
$PACKEN_KATEGORIEN = [
'kleidung' => ['label' => 'Kleidung', 'icon' => '👕'],
'dokumente' => ['label' => 'Dokumente', 'icon' => '📄'],
'elektronik' => ['label' => 'Elektronik', 'icon' => '🔌'],
'medizin' => ['label' => 'Medizin', 'icon' => '💊'],
'toilette' => ['label' => 'Toilette', 'icon' => '🧴'],
'essen' => ['label' => 'Essen & Trinken', 'icon' => '🍿'],
'strand' => ['label' => 'Strand', 'icon' => '🏖'],
'sport' => ['label' => 'Sport', 'icon' => '⚽'],
'sonstiges' => ['label' => 'Sonstiges', 'icon' => '📦'],
];
$UPLOAD_TYPEN = [
'application/pdf' => 'PDF',
'image/jpeg' => 'JPG',
'image/png' => 'PNG',
'image/gif' => 'GIF',
'image/webp' => 'WEBP',
'application/msword' => 'Word',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'Word',
];
// ── E-Mail-Versand ────────────────────────────────────────────────────────────
function sendMail(string $to, string $subject, string $htmlBody): bool {
$fromName = '=?UTF-8?B?' . base64_encode(MAIL_FROM_NAME) . '?=';
$subjectEncoded = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$boundary = md5(uniqid());
$headers = "From: {$fromName} <" . MAIL_FROM . ">\r\n";
$headers .= "Reply-To: " . MAIL_FROM . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$plain = strip_tags(str_replace(['<br>', '<br/>', '<br />', '</p>', '</div>'], "\n", $htmlBody));
$plain = preg_replace('/\n{3,}/', "\n\n", trim($plain));
$body = "--{$boundary}\r\n";
$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n\r\n";
$body .= chunk_split(base64_encode($plain)) . "\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Type: text/html; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n\r\n";
$body .= chunk_split(base64_encode($htmlBody)) . "\r\n";
$body .= "--{$boundary}--";
return mail($to, $subjectEncoded, $body, $headers);
}
function sendInviteMail(string $toEmail, string $tripName, string $inviteLink, string $inviterName): bool {
$subject = $inviterName . ' laedt dich zu "' . $tripName . '" ein';
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Inter,sans-serif;background:#f0f6ff;padding:30px;">
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 4px 20px rgba(37,99,235,.1);">
<div style="text-align:center;margin-bottom:24px;">
<div style="font-size:2.5rem;">📑</div>
<h1 style="font-family:Georgia,serif;font-size:1.4rem;color:#1d4ed8;margin:8px 0 4px;">Lütte Planer</h1>
</div>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:12px;">Hallo!</p>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:20px;">
<strong>' . htmlspecialchars($inviterName) . '</strong> hat dich zur Reise
<strong>„' . htmlspecialchars($tripName) . '"</strong> eingeladen.
</p>
<div style="text-align:center;margin:28px 0;">
<a href="' . htmlspecialchars($inviteLink) . '" style="background:#2563eb;color:#fff;padding:13px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.95rem;">
Einladung annehmen
</a>
</div>
<p style="color:#64748b;font-size:.78rem;margin-top:24px;">
Oder kopiere diesen Link in deinen Browser:<br>
<span style="color:#2563eb;word-break:break-all;">' . htmlspecialchars($inviteLink) . '</span>
</p>
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
<p style="color:#94a3b8;font-size:.72rem;text-align:center;">Lütte Planer · Reisen planen &amp; teilen</p>
</div>
</body></html>';
return sendMail($toEmail, $subject, $html);
}
function sendAddedToTripMail(string $toEmail, string $toName, string $tripName, string $tripUrl, string $inviterName): bool {
$subject = 'Du wurdest zu "' . $tripName . '" hinzugefuegt';
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Inter,sans-serif;background:#f0f6ff;padding:30px;">
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 4px 20px rgba(37,99,235,.1);">
<div style="text-align:center;margin-bottom:24px;">
<div style="font-size:2.5rem;">📑</div>
<h1 style="font-family:Georgia,serif;font-size:1.4rem;color:#1d4ed8;margin:8px 0 4px;">Lütte Planer</h1>
</div>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:12px;">Hallo ' . htmlspecialchars($toName) . '!</p>
<p style="color:#1e293b;font-size:.95rem;margin-bottom:20px;">
<strong>' . htmlspecialchars($inviterName) . '</strong> hat dich zur Reise
<strong>„' . htmlspecialchars($tripName) . '"</strong> hinzugefügt.
</p>
<div style="text-align:center;margin:28px 0;">
<a href="' . htmlspecialchars($tripUrl) . '" style="background:#2563eb;color:#fff;padding:13px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.95rem;">
Zur Reise
</a>
</div>
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
<p style="color:#94a3b8;font-size:.72rem;text-align:center;">Lütte Planer · Reisen planen &amp; teilen</p>
</div>
</body></html>';
return sendMail($toEmail, $subject, $html);
}