Merge pull request #12 from metacube2/claude/add-paypal-image-uploads-01XtWuh5yLHza7HPpMd7BKLq
Add PayPal integration and band image upload features
This commit is contained in:
@@ -0,0 +1,180 @@
|
|||||||
|
# Neue Features: PayPal-Integration & Bild-Upload
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die neu hinzugefügten Features für die GetYourBand-Plattform.
|
||||||
|
|
||||||
|
## 🖼️ Bild-Upload für Bands
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Upload-Funktionalität**: Bands können eigene Bilder hochladen
|
||||||
|
- **Galerie-Verwaltung**: Anzeige und Verwaltung aller hochgeladenen Bilder
|
||||||
|
- **Löschen**: Bilder können jederzeit gelöscht werden
|
||||||
|
- **Validierung**:
|
||||||
|
- Erlaubte Formate: JPG, PNG, GIF, WEBP
|
||||||
|
- Maximale Dateigröße: 5MB
|
||||||
|
- Automatische Dateinamens-Generierung
|
||||||
|
|
||||||
|
### Technische Details
|
||||||
|
- **Upload-Verzeichnis**: `/storage/uploads/bands/`
|
||||||
|
- **Handler**: `upload-handler.php`
|
||||||
|
- **Frontend**: AJAX-basierter Upload mit Fetch API
|
||||||
|
- **Dateinamensschema**: `band_{band_id}_{unique_id}.{extension}`
|
||||||
|
|
||||||
|
### Verwendung
|
||||||
|
1. Als Band-User einloggen
|
||||||
|
2. Zum Profil navigieren (`profil.php`)
|
||||||
|
3. Sektion "Band-Galerie" finden
|
||||||
|
4. Auf "+ Bild hochladen" klicken
|
||||||
|
5. Bild auswählen (wird automatisch hochgeladen)
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
- Nur authentifizierte Band-User können uploaden
|
||||||
|
- Strenge Dateitypprüfung (MIME-Type + Extension)
|
||||||
|
- Größenlimit verhindert DoS
|
||||||
|
- Sichere Dateinamen ohne User-Input
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 PayPal-Integration
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Zahlungsabwicklung**: Kunden können Buchungen direkt mit PayPal bezahlen
|
||||||
|
- **Service Fee**: Konfigurierbare Servicegebühr (in Admin-Settings)
|
||||||
|
- **Zahlungs-Tracking**: Alle Zahlungen werden in der Datenbank gespeichert
|
||||||
|
- **Status-Updates**: Anfragen werden automatisch auf "bestätigt" gesetzt
|
||||||
|
- **Email-Benachrichtigungen**: Kunde und Band erhalten Bestätigungen
|
||||||
|
|
||||||
|
### Komponenten
|
||||||
|
|
||||||
|
#### 1. Datenbank
|
||||||
|
Neue Tabelle `payments`:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE payments (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
request_id INTEGER NOT NULL,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
service_fee REAL NOT NULL,
|
||||||
|
total_amount REAL NOT NULL,
|
||||||
|
paypal_order_id TEXT,
|
||||||
|
paypal_payer_id TEXT,
|
||||||
|
status TEXT DEFAULT 'pending',
|
||||||
|
created_at TEXT,
|
||||||
|
completed_at TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Checkout-Seite
|
||||||
|
**Datei**: `paypal-checkout.php`
|
||||||
|
- Zeigt Buchungsdetails und Zahlungsübersicht
|
||||||
|
- Integriert PayPal JavaScript SDK
|
||||||
|
- Berechnet Gesamtbetrag (Band-Gage + Service Fee)
|
||||||
|
|
||||||
|
#### 3. Payment Processing
|
||||||
|
**Datei**: `paypal-process.php`
|
||||||
|
- Speichert erfolgreiche Zahlungen
|
||||||
|
- Aktualisiert Request-Status
|
||||||
|
- Sendet Bestätigungs-Emails
|
||||||
|
|
||||||
|
#### 4. Integration in Buchungsflow
|
||||||
|
**Änderungen in `anfrage.php`**:
|
||||||
|
- Nach erfolgreicher Anfrage wird PayPal-Button angezeigt (wenn aktiviert)
|
||||||
|
- Direkter Link zum Checkout
|
||||||
|
|
||||||
|
**Änderungen in `profil.php`**:
|
||||||
|
- Zahlungsstatus für jede Anfrage angezeigt
|
||||||
|
- "Jetzt bezahlen"-Button für ausstehende Zahlungen
|
||||||
|
|
||||||
|
### PayPal-Konfiguration
|
||||||
|
|
||||||
|
#### Admin-Einstellungen
|
||||||
|
Im Admin-Panel (`admin/settings.php`):
|
||||||
|
- `paypal_enabled`: 0/1 (aktiviert/deaktiviert)
|
||||||
|
- `service_fee`: Prozentsatz (z.B. 8 für 8%)
|
||||||
|
|
||||||
|
#### PayPal API Credentials
|
||||||
|
In `paypal-checkout.php` Zeile 80:
|
||||||
|
```javascript
|
||||||
|
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_PAYPAL_CLIENT_ID¤cy=CHF"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig**: `YOUR_PAYPAL_CLIENT_ID` durch echte Client-ID ersetzen!
|
||||||
|
|
||||||
|
#### PayPal Developer Setup
|
||||||
|
1. Gehen Sie zu https://developer.paypal.com
|
||||||
|
2. Erstellen Sie eine App in "My Apps & Credentials"
|
||||||
|
3. Kopieren Sie die Client-ID
|
||||||
|
4. Für Produktion: Aktivieren Sie Live-Modus und verwenden Sie Live-Credentials
|
||||||
|
|
||||||
|
### Zahlungsablauf
|
||||||
|
|
||||||
|
1. **Kunde erstellt Anfrage** → Request wird in DB gespeichert
|
||||||
|
2. **PayPal-Link erscheint** → Kunde klickt auf "Mit PayPal bezahlen"
|
||||||
|
3. **Checkout-Seite** → Übersicht und PayPal-Button
|
||||||
|
4. **PayPal-Zahlung** → Kunde loggt sich in PayPal ein und zahlt
|
||||||
|
5. **Payment Processing** → Zahlung wird in DB gespeichert
|
||||||
|
6. **Status-Update** → Request → "bestätigt", Emails versandt
|
||||||
|
7. **Rückkehr zum Profil** → Erfolgsmeldung
|
||||||
|
|
||||||
|
### Testmodus
|
||||||
|
|
||||||
|
Die aktuelle Implementation läuft im **Sandbox-Modus**:
|
||||||
|
- Verwenden Sie PayPal Sandbox-Accounts zum Testen
|
||||||
|
- Keine echten Transaktionen werden durchgeführt
|
||||||
|
- Für Produktion: Client-ID auf Live-Credentials umstellen
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
- Zahlung nur für eigene Requests möglich
|
||||||
|
- Doppelzahlungen werden verhindert
|
||||||
|
- Transaktions-IDs werden gespeichert
|
||||||
|
- Server-seitige Validierung aller Zahlungsdaten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Neue Dateien
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|--------------|
|
||||||
|
| `upload-handler.php` | REST-API für Bild-Uploads (POST/DELETE) |
|
||||||
|
| `paypal-checkout.php` | PayPal Checkout-Seite |
|
||||||
|
| `paypal-process.php` | PayPal Payment Processing Backend |
|
||||||
|
| `storage/uploads/bands/` | Upload-Verzeichnis für Band-Bilder |
|
||||||
|
| `PAYPAL_UPLOAD_FEATURES.md` | Diese Dokumentation |
|
||||||
|
|
||||||
|
## 🔄 Geänderte Dateien
|
||||||
|
|
||||||
|
| Datei | Änderungen |
|
||||||
|
|-------|------------|
|
||||||
|
| `database.sql` | + `payments` Tabelle |
|
||||||
|
| `profil.php` | + Galerie-Sektion, + Zahlungsstatus in Anfragen |
|
||||||
|
| `anfrage.php` | + PayPal-Button nach erfolgreicher Anfrage |
|
||||||
|
|
||||||
|
## 🚀 Deployment-Checklist
|
||||||
|
|
||||||
|
- [ ] `storage/uploads/` Verzeichnis erstellen mit Schreibrechten
|
||||||
|
- [ ] PayPal Developer Account erstellen
|
||||||
|
- [ ] Client-ID in `paypal-checkout.php` eintragen
|
||||||
|
- [ ] Admin-Panel: PayPal aktivieren und Service Fee setzen
|
||||||
|
- [ ] Für Produktion: Auf Live-Credentials umstellen
|
||||||
|
- [ ] SSL-Zertifikat für HTTPS (PayPal requirement)
|
||||||
|
|
||||||
|
## 🐛 Bekannte Einschränkungen
|
||||||
|
|
||||||
|
1. **PayPal Client-ID**: Muss manuell konfiguriert werden
|
||||||
|
2. **Keine Rückerstattungen**: Keine Admin-UI für Refunds
|
||||||
|
3. **Email-System**: Aktuell nur Logging, kein echtes SMTP
|
||||||
|
4. **Sandbox-Modus**: Standardmäßig aktiviert
|
||||||
|
|
||||||
|
## 📝 Nächste Schritte (Optional)
|
||||||
|
|
||||||
|
- Webhook-Integration für PayPal IPN (Instant Payment Notification)
|
||||||
|
- Admin-Dashboard für Zahlungsübersicht
|
||||||
|
- Automatische Rechnungserstellung (PDF)
|
||||||
|
- Stripe als alternative Zahlungsmethode
|
||||||
|
- Bulk-Upload für mehrere Bilder
|
||||||
|
- Bildkompression/Optimierung
|
||||||
|
- Thumbnail-Generierung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Entwickelt für**: GetYourBand Platform
|
||||||
|
**Datum**: 2025-12-02
|
||||||
|
**Version**: 1.0
|
||||||
+18
-1
@@ -15,6 +15,8 @@ $user = currentUser();
|
|||||||
$message = '';
|
$message = '';
|
||||||
$error = '';
|
$error = '';
|
||||||
|
|
||||||
|
$requestId = null;
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$data = [
|
$data = [
|
||||||
'band_id' => $bandId,
|
'band_id' => $bandId,
|
||||||
@@ -30,6 +32,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$error = 'Bitte Datum und Ort ausfüllen.';
|
$error = 'Bitte Datum und Ort ausfüllen.';
|
||||||
} else {
|
} else {
|
||||||
createRequest($data);
|
createRequest($data);
|
||||||
|
$requestId = (int) db()->lastInsertId();
|
||||||
$message = 'Anfrage gespeichert und an die Band gemeldet.';
|
$message = 'Anfrage gespeichert und an die Band gemeldet.';
|
||||||
sendEmail('info@' . preg_replace('/\s+/', '', strtolower($band['name'])) . '.ch', 'Neue Anfrage', 'Neue Anfrage für ' . $band['name']);
|
sendEmail('info@' . preg_replace('/\s+/', '', strtolower($band['name'])) . '.ch', 'Neue Anfrage', 'Neue Anfrage für ' . $band['name']);
|
||||||
}
|
}
|
||||||
@@ -52,8 +55,21 @@ $settings = settings();
|
|||||||
<p>PayPal Zahlungsabwicklung ist <?= $settings['paypal_enabled'] === '1' ? 'aktiviert' : 'optional' ?>, Service Fee: <?= htmlspecialchars($settings['service_fee']) ?>%.</p>
|
<p>PayPal Zahlungsabwicklung ist <?= $settings['paypal_enabled'] === '1' ? 'aktiviert' : 'optional' ?>, Service Fee: <?= htmlspecialchars($settings['service_fee']) ?>%.</p>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
|
<?php if ($message): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<?= htmlspecialchars($message) ?>
|
||||||
|
<?php if ($requestId && $settings['paypal_enabled'] === '1'): ?>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<a href="paypal-checkout.php?request_id=<?= $requestId ?>" class="btn-primary" style="display: inline-block; padding: 0.75rem 1.5rem; text-decoration: none;">
|
||||||
|
Jetzt mit PayPal bezahlen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!$message): ?>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label>Event-Datum
|
<label>Event-Datum
|
||||||
<input type="date" class="form-control" name="event_date" required>
|
<input type="date" class="form-control" name="event_date" required>
|
||||||
@@ -72,6 +88,7 @@ $settings = settings();
|
|||||||
</label>
|
</label>
|
||||||
<button class="btn-primary">Anfrage senden</button>
|
<button class="btn-primary">Anfrage senden</button>
|
||||||
</form>
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -74,3 +74,17 @@ CREATE TABLE IF NOT EXISTS settings (
|
|||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS payments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
request_id INTEGER NOT NULL,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
service_fee REAL NOT NULL,
|
||||||
|
total_amount REAL NOT NULL,
|
||||||
|
paypal_order_id TEXT,
|
||||||
|
paypal_payer_id TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
completed_at TEXT,
|
||||||
|
FOREIGN KEY(request_id) REFERENCES requests(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
|
||||||
|
$requestId = isset($_GET['request_id']) ? (int) $_GET['request_id'] : 0;
|
||||||
|
if (!$requestId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo 'Keine Anfrage-ID angegeben';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = currentUser();
|
||||||
|
|
||||||
|
// Get request details
|
||||||
|
$stmt = db()->prepare('SELECT r.*, b.name as band_name, b.price as band_price
|
||||||
|
FROM requests r
|
||||||
|
JOIN bands b ON b.id = r.band_id
|
||||||
|
WHERE r.id = :id AND r.user_id = :user_id');
|
||||||
|
$stmt->execute([':id' => $requestId, ':user_id' => $user['id']]);
|
||||||
|
$request = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$request) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo 'Anfrage nicht gefunden';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = settings();
|
||||||
|
if ($settings['paypal_enabled'] !== '1') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo 'PayPal-Zahlungen sind derzeit nicht aktiviert';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate amounts
|
||||||
|
$bandPrice = (int) $request['band_price'];
|
||||||
|
$serviceFeePercent = (float) $settings['service_fee'];
|
||||||
|
$serviceFee = $bandPrice * ($serviceFeePercent / 100);
|
||||||
|
$totalAmount = $bandPrice + $serviceFee;
|
||||||
|
|
||||||
|
// Check if already paid
|
||||||
|
$stmt = db()->prepare('SELECT * FROM payments WHERE request_id = :id AND status = "completed"');
|
||||||
|
$stmt->execute([':id' => $requestId]);
|
||||||
|
$existingPayment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existingPayment) {
|
||||||
|
$message = 'Diese Buchung wurde bereits bezahlt.';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PayPal Zahlung – <?= SITE_NAME ?></title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a class="badge" href="profil.php">← Zurück zum Profil</a>
|
||||||
|
<h1>Zahlung für Buchung</h1>
|
||||||
|
</header>
|
||||||
|
<main style="max-width: 600px; margin: 0 auto;">
|
||||||
|
<?php if (isset($message)): ?>
|
||||||
|
<div class="alert alert-success"><?= htmlspecialchars($message) ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<h2>Buchungsdetails</h2>
|
||||||
|
<table class="table" style="margin-bottom: 2rem;">
|
||||||
|
<tr><td><strong>Band:</strong></td><td><?= htmlspecialchars($request['band_name']) ?></td></tr>
|
||||||
|
<tr><td><strong>Event-Datum:</strong></td><td><?= htmlspecialchars($request['event_date']) ?></td></tr>
|
||||||
|
<tr><td><strong>Location:</strong></td><td><?= htmlspecialchars($request['location']) ?></td></tr>
|
||||||
|
<tr><td><strong>Event-Typ:</strong></td><td><?= htmlspecialchars($request['event_type']) ?></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Zahlungsübersicht</h2>
|
||||||
|
<table class="table" style="margin-bottom: 2rem;">
|
||||||
|
<tr><td><strong>Band-Gage:</strong></td><td><?= formatPrice($bandPrice) ?></td></tr>
|
||||||
|
<tr><td><strong>Service Fee (<?= htmlspecialchars($serviceFeePercent) ?>%):</strong></td><td><?= formatPrice((int) $serviceFee) ?></td></tr>
|
||||||
|
<tr style="border-top: 2px solid #ffb703;"><td><strong>Gesamtbetrag:</strong></td><td><strong><?= formatPrice((int) $totalAmount) ?></strong></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="payment-status" style="display:none; padding: 1rem; margin-bottom: 1rem; border-radius: 4px;"></div>
|
||||||
|
|
||||||
|
<!-- PayPal Button Container -->
|
||||||
|
<div id="paypal-button-container" style="margin: 2rem 0;"></div>
|
||||||
|
|
||||||
|
<p style="color: #666; font-size: 0.875rem; margin-top: 2rem;">
|
||||||
|
<strong>Hinweis:</strong> Dies ist eine Demo-Integration. Für die Produktivumgebung benötigen Sie echte PayPal API-Credentials.
|
||||||
|
Aktuell wird im Sandbox-Modus gearbeitet.
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php if (!isset($message)): ?>
|
||||||
|
<!-- PayPal SDK -->
|
||||||
|
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_PAYPAL_CLIENT_ID¤cy=CHF"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
paypal.Buttons({
|
||||||
|
createOrder: function(data, actions) {
|
||||||
|
return actions.order.create({
|
||||||
|
purchase_units: [{
|
||||||
|
amount: {
|
||||||
|
value: '<?= number_format($totalAmount, 2, '.', '') ?>',
|
||||||
|
currency_code: 'CHF',
|
||||||
|
breakdown: {
|
||||||
|
item_total: {
|
||||||
|
value: '<?= number_format($bandPrice, 2, '.', '') ?>',
|
||||||
|
currency_code: 'CHF'
|
||||||
|
},
|
||||||
|
tax_total: {
|
||||||
|
value: '<?= number_format($serviceFee, 2, '.', '') ?>',
|
||||||
|
currency_code: 'CHF'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: 'Buchung: <?= htmlspecialchars($request['band_name']) ?> - <?= htmlspecialchars($request['event_date']) ?>'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onApprove: function(data, actions) {
|
||||||
|
return actions.order.capture().then(function(details) {
|
||||||
|
// Save payment to database
|
||||||
|
const statusDiv = document.getElementById('payment-status');
|
||||||
|
statusDiv.style.display = 'block';
|
||||||
|
statusDiv.style.background = '#28a745';
|
||||||
|
statusDiv.style.color = 'white';
|
||||||
|
statusDiv.textContent = 'Zahlung erfolgreich! Verarbeite Transaktion...';
|
||||||
|
|
||||||
|
fetch('paypal-process.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
request_id: <?= $requestId ?>,
|
||||||
|
amount: <?= $bandPrice ?>,
|
||||||
|
service_fee: <?= number_format($serviceFee, 2, '.', '') ?>,
|
||||||
|
total_amount: <?= number_format($totalAmount, 2, '.', '') ?>,
|
||||||
|
paypal_order_id: data.orderID,
|
||||||
|
paypal_payer_id: details.payer.payer_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
statusDiv.textContent = 'Zahlung erfolgreich abgeschlossen! Sie werden weitergeleitet...';
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'profil.php?payment_success=1';
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
statusDiv.style.background = '#dc3545';
|
||||||
|
statusDiv.textContent = 'Fehler beim Speichern der Zahlung: ' + result.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: function(err) {
|
||||||
|
const statusDiv = document.getElementById('payment-status');
|
||||||
|
statusDiv.style.display = 'block';
|
||||||
|
statusDiv.style.background = '#dc3545';
|
||||||
|
statusDiv.style.color = 'white';
|
||||||
|
statusDiv.textContent = 'Fehler bei der Zahlung: ' + err;
|
||||||
|
}
|
||||||
|
}).render('#paypal-button-container');
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/email.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = currentUser();
|
||||||
|
|
||||||
|
// Get JSON input
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input || !isset($input['request_id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Ungültige Anfrage']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestId = (int) $input['request_id'];
|
||||||
|
$amount = (float) $input['amount'];
|
||||||
|
$serviceFee = (float) $input['service_fee'];
|
||||||
|
$totalAmount = (float) $input['total_amount'];
|
||||||
|
$paypalOrderId = $input['paypal_order_id'] ?? '';
|
||||||
|
$paypalPayerId = $input['paypal_payer_id'] ?? '';
|
||||||
|
|
||||||
|
// Verify request belongs to user
|
||||||
|
$stmt = db()->prepare('SELECT r.*, b.name as band_name, b.user_id as band_user_id
|
||||||
|
FROM requests r
|
||||||
|
JOIN bands b ON b.id = r.band_id
|
||||||
|
WHERE r.id = :id AND r.user_id = :user_id');
|
||||||
|
$stmt->execute([':id' => $requestId, ':user_id' => $user['id']]);
|
||||||
|
$request = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$request) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Anfrage nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already paid
|
||||||
|
$stmt = db()->prepare('SELECT * FROM payments WHERE request_id = :id AND status = "completed"');
|
||||||
|
$stmt->execute([':id' => $requestId]);
|
||||||
|
if ($stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Diese Buchung wurde bereits bezahlt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save payment
|
||||||
|
$stmt = db()->prepare('INSERT INTO payments (request_id, amount, service_fee, total_amount, paypal_order_id, paypal_payer_id, status, completed_at)
|
||||||
|
VALUES (:request_id, :amount, :service_fee, :total_amount, :paypal_order_id, :paypal_payer_id, :status, :completed_at)');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':request_id' => $requestId,
|
||||||
|
':amount' => $amount,
|
||||||
|
':service_fee' => $serviceFee,
|
||||||
|
':total_amount' => $totalAmount,
|
||||||
|
':paypal_order_id' => $paypalOrderId,
|
||||||
|
':paypal_payer_id' => $paypalPayerId,
|
||||||
|
':status' => 'completed',
|
||||||
|
':completed_at' => (new DateTimeImmutable())->format('c')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update request status to confirmed
|
||||||
|
$stmt = db()->prepare('UPDATE requests SET status = :status WHERE id = :id');
|
||||||
|
$stmt->execute([':status' => 'bestätigt', ':id' => $requestId]);
|
||||||
|
|
||||||
|
// Send confirmation emails
|
||||||
|
sendEmail($user['email'], 'Zahlungsbestätigung',
|
||||||
|
'Ihre Zahlung für die Buchung von ' . $request['band_name'] . ' wurde erfolgreich verarbeitet.');
|
||||||
|
|
||||||
|
// Notify band
|
||||||
|
if ($request['band_user_id']) {
|
||||||
|
$bandUserStmt = db()->prepare('SELECT email FROM users WHERE id = :id');
|
||||||
|
$bandUserStmt->execute([':id' => $request['band_user_id']]);
|
||||||
|
$bandUser = $bandUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($bandUser) {
|
||||||
|
sendEmail($bandUser['email'], 'Neue bezahlte Buchung',
|
||||||
|
'Sie haben eine neue bezahlte Buchung für ' . $request['event_date'] . ' erhalten.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Zahlung erfolgreich verarbeitet',
|
||||||
|
'payment_id' => (int) db()->lastInsertId()
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Fehler beim Speichern der Zahlung: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
+120
-2
@@ -66,20 +66,138 @@ if ($user['role'] === 'band') {
|
|||||||
</label>
|
</label>
|
||||||
<button class="btn-primary">Speichern</button>
|
<button class="btn-primary">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 2rem;">Band-Galerie</h2>
|
||||||
|
<div id="upload-status" style="display:none; padding: 1rem; margin-bottom: 1rem; background: #28a745; color: white; border-radius: 4px;"></div>
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<label class="btn-primary" style="display: inline-block; cursor: pointer;">
|
||||||
|
<input type="file" id="image-upload" accept="image/*" style="display: none;">
|
||||||
|
+ Bild hochladen
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-top: 0.5rem; color: #666;">Max 5MB (JPG, PNG, GIF, WEBP)</small>
|
||||||
|
</div>
|
||||||
|
<div id="gallery" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;">
|
||||||
|
<?php foreach (bandMedia((int) $band['id']) as $media): ?>
|
||||||
|
<div class="gallery-item" data-media-id="<?= $media['id'] ?>">
|
||||||
|
<img src="<?= htmlspecialchars($media['url']) ?>" alt="Band Foto" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;">
|
||||||
|
<button class="delete-image" data-id="<?= $media['id'] ?>" style="margin-top: 0.5rem; background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; width: 100%;">Löschen</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('image-upload').addEventListener('change', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', file);
|
||||||
|
|
||||||
|
const statusDiv = document.getElementById('upload-status');
|
||||||
|
statusDiv.style.display = 'block';
|
||||||
|
statusDiv.style.background = '#ffc107';
|
||||||
|
statusDiv.textContent = 'Uploading...';
|
||||||
|
|
||||||
|
fetch('upload-handler.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
statusDiv.style.background = '#28a745';
|
||||||
|
statusDiv.textContent = data.message;
|
||||||
|
|
||||||
|
// Add to gallery
|
||||||
|
const gallery = document.getElementById('gallery');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'gallery-item';
|
||||||
|
div.setAttribute('data-media-id', data.id);
|
||||||
|
div.innerHTML = `
|
||||||
|
<img src="${data.url}" alt="Band Foto" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;">
|
||||||
|
<button class="delete-image" data-id="${data.id}" style="margin-top: 0.5rem; background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; width: 100%;">Löschen</button>
|
||||||
|
`;
|
||||||
|
gallery.appendChild(div);
|
||||||
|
|
||||||
|
setTimeout(() => { statusDiv.style.display = 'none'; }, 3000);
|
||||||
|
} else {
|
||||||
|
statusDiv.style.background = '#dc3545';
|
||||||
|
statusDiv.textContent = data.error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
statusDiv.style.background = '#dc3545';
|
||||||
|
statusDiv.textContent = 'Upload fehlgeschlagen: ' + error.message;
|
||||||
|
});
|
||||||
|
|
||||||
|
e.target.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('delete-image')) {
|
||||||
|
if (!confirm('Bild wirklich löschen?')) return;
|
||||||
|
|
||||||
|
const mediaId = e.target.getAttribute('data-id');
|
||||||
|
const galleryItem = e.target.closest('.gallery-item');
|
||||||
|
|
||||||
|
fetch('upload-handler.php', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: 'media_id=' + mediaId
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
galleryItem.remove();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p>Du hast noch kein Bandprofil angelegt.</p>
|
<p>Du hast noch kein Bandprofil angelegt.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($user['role'] === 'kunde'): ?>
|
<?php if ($user['role'] === 'kunde'): ?>
|
||||||
|
<?php if (isset($_GET['payment_success'])): ?>
|
||||||
|
<div class="alert alert-success">Zahlung erfolgreich abgeschlossen! Vielen Dank für Ihre Buchung.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<h2>Meine Anfragen</h2>
|
<h2>Meine Anfragen</h2>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead><tr><th>Band</th><th>Datum</th><th>Status</th></tr></thead>
|
<thead><tr><th>Band</th><th>Datum</th><th>Status</th><th>Zahlung</th><th>Aktion</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach (userRequests((int) $user['id']) as $request): $bandName = findBand((int) $request['band_id']); ?>
|
<?php
|
||||||
|
$settings = settings();
|
||||||
|
foreach (userRequests((int) $user['id']) as $request):
|
||||||
|
$bandName = findBand((int) $request['band_id']);
|
||||||
|
|
||||||
|
// Check payment status
|
||||||
|
$stmt = db()->prepare('SELECT * FROM payments WHERE request_id = :id AND status = "completed"');
|
||||||
|
$stmt->execute([':id' => $request['id']]);
|
||||||
|
$payment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= htmlspecialchars($bandName['name'] ?? 'Band #' . $request['band_id']) ?></td>
|
<td><?= htmlspecialchars($bandName['name'] ?? 'Band #' . $request['band_id']) ?></td>
|
||||||
<td><?= htmlspecialchars($request['event_date']) ?></td>
|
<td><?= htmlspecialchars($request['event_date']) ?></td>
|
||||||
<td><?= htmlspecialchars($request['status']) ?></td>
|
<td><?= htmlspecialchars($request['status']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($payment): ?>
|
||||||
|
<span style="color: #28a745;">✓ Bezahlt</span><br>
|
||||||
|
<small style="color: #666;"><?= formatPrice((int) $payment['total_amount']) ?></small>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: #dc3545;">Ausstehend</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if (!$payment && $settings['paypal_enabled'] === '1'): ?>
|
||||||
|
<a href="paypal-checkout.php?request_id=<?= $request['id'] ?>" class="badge" style="background: #0070ba; color: white; text-decoration: none;">
|
||||||
|
PayPal bezahlen
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = currentUser();
|
||||||
|
if ($user['role'] !== 'band') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Nur Bands können Bilder hochladen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get band
|
||||||
|
$stmt = db()->prepare('SELECT * FROM bands WHERE user_id = :id');
|
||||||
|
$stmt->execute([':id' => $user['id']]);
|
||||||
|
$band = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$band) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Kein Bandprofil gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
|
||||||
|
$file = $_FILES['image'];
|
||||||
|
|
||||||
|
// Validate file
|
||||||
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||||
|
|
||||||
|
if (!in_array($file['type'], $allowedTypes)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Ungültiger Dateityp. Erlaubt sind: JPG, PNG, GIF, WEBP']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file['size'] > $maxSize) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Datei zu groß (max 5MB)']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Upload-Fehler']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = 'band_' . $band['id'] . '_' . uniqid() . '.' . $extension;
|
||||||
|
$uploadPath = __DIR__ . '/storage/uploads/bands/' . $filename;
|
||||||
|
|
||||||
|
// Move file
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Datei konnte nicht gespeichert werden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
$url = 'storage/uploads/bands/' . $filename;
|
||||||
|
$stmt = db()->prepare('INSERT INTO band_media (band_id, type, url) VALUES (:band_id, :type, :url)');
|
||||||
|
$stmt->execute([
|
||||||
|
':band_id' => $band['id'],
|
||||||
|
':type' => 'image',
|
||||||
|
':url' => $url
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mediaId = (int) db()->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'id' => $mediaId,
|
||||||
|
'url' => $url,
|
||||||
|
'message' => 'Bild erfolgreich hochgeladen'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete image
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
parse_str(file_get_contents('php://input'), $deleteData);
|
||||||
|
$mediaId = (int) ($deleteData['media_id'] ?? 0);
|
||||||
|
|
||||||
|
if (!$mediaId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Keine Media-ID angegeben']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
$stmt = db()->prepare('SELECT * FROM band_media WHERE id = :id AND band_id = :band_id');
|
||||||
|
$stmt->execute([':id' => $mediaId, ':band_id' => $band['id']]);
|
||||||
|
$media = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$media) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Bild nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete file
|
||||||
|
$filePath = __DIR__ . '/' . $media['url'];
|
||||||
|
if (file_exists($filePath) && strpos($media['url'], 'storage/uploads/') === 0) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from database
|
||||||
|
$stmt = db()->prepare('DELETE FROM band_media WHERE id = :id');
|
||||||
|
$stmt->execute([':id' => $mediaId]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Bild gelöscht']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Ungültige Anfrage']);
|
||||||
Reference in New Issue
Block a user