Initial commit
This commit is contained in:
11
skrift-configurator/.vscode/sftp.json
vendored
Normal file
11
skrift-configurator/.vscode/sftp.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "skrift",
|
||||
"host": "ae975.netcup.net",
|
||||
"protocol": "ftp",
|
||||
"port": 21,
|
||||
"username": "skrift",
|
||||
"remotePath": "/",
|
||||
"uploadOnSave": true,
|
||||
"useTempFile": false,
|
||||
"openSsh": false
|
||||
}
|
||||
388
skrift-configurator/BACKEND_INTEGRATION.md
Normal file
388
skrift-configurator/BACKEND_INTEGRATION.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# Backend Integration - WordPress Plugin
|
||||
|
||||
Anleitung zur Integration des Node.js Backends mit dem WordPress Konfigurator-Plugin.
|
||||
|
||||
## Überblick
|
||||
|
||||
Das WordPress Plugin kommuniziert mit dem Node.js Backend über eine REST API. Das Backend generiert die handgeschriebenen SVG-Dateien und speichert die finalen Bestellungen.
|
||||
|
||||
## WordPress Admin-Einstellungen
|
||||
|
||||
Nach der Installation des Plugins in WordPress:
|
||||
|
||||
1. Gehe zu **Einstellungen → Skrift Konfigurator**
|
||||
2. Scrolle nach unten zum Abschnitt **"Backend-Verbindung"**
|
||||
|
||||
### Erforderliche Einstellungen
|
||||
|
||||
#### 1. API URL / Domain
|
||||
**Beispiel:** `https://backend.deine-domain.de`
|
||||
|
||||
Die vollständige URL zu deinem Backend-Server (ohne trailing slash).
|
||||
|
||||
- ✅ `https://backend.example.com`
|
||||
- ✅ `http://localhost:4000` (nur für lokale Tests)
|
||||
- ❌ `https://backend.example.com/` (kein Slash am Ende!)
|
||||
|
||||
#### 2. API Token / Authentifizierung
|
||||
**Optional** - Aktuell nicht implementiert, für zukünftige Erweiterungen vorbereitet.
|
||||
|
||||
Lasse dieses Feld erstmal leer.
|
||||
|
||||
#### 3. Order Webhook URL
|
||||
**Beispiel:** `https://n8n.deine-domain.de/webhook/order`
|
||||
|
||||
URL die aufgerufen wird, nachdem eine Bestellung abgeschickt wurde.
|
||||
|
||||
**Wird aufgerufen:**
|
||||
- **Business-Kunden:** Sofort nach Klick auf "Jetzt kostenpflichtig bestellen"
|
||||
- **Privat-Kunden:** Nach erfolgreicher PayPal-Zahlung (später implementiert)
|
||||
|
||||
**Webhook-Payload:**
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-03-001",
|
||||
"customer_type": "business",
|
||||
"product": "businessbriefe",
|
||||
"quantity": 100,
|
||||
"format": "A4",
|
||||
"shipping_mode": "direct",
|
||||
"envelope": "yes",
|
||||
"customer_data": {
|
||||
"firstName": "Max",
|
||||
"lastName": "Mustermann",
|
||||
"company": "Beispiel GmbH",
|
||||
"email": "max@example.com",
|
||||
...
|
||||
},
|
||||
"quote": {
|
||||
"total": 250.00,
|
||||
...
|
||||
},
|
||||
"backend_result": {
|
||||
"path": "/var/skrift-output/SK-2026-01-03-001",
|
||||
"files": [...],
|
||||
"summary": {...}
|
||||
},
|
||||
"timestamp": "2026-01-03T..."
|
||||
}
|
||||
```
|
||||
|
||||
**Verwendung:**
|
||||
- N8N Workflow triggern
|
||||
- CRM-System benachrichtigen
|
||||
- Interne Benachrichtigungen versenden
|
||||
|
||||
#### 4. Redirect URL Geschäftskunden
|
||||
**Beispiel:** `https://deine-domain.de/danke-business`
|
||||
|
||||
Wohin Business-Kunden nach dem Klick auf "Jetzt kostenpflichtig bestellen" weitergeleitet werden.
|
||||
|
||||
**Query-Parameter:**
|
||||
- `orderNumber` - Die generierte Bestellnummer
|
||||
|
||||
**Beispiel-Redirect:**
|
||||
```
|
||||
https://deine-domain.de/danke-business?orderNumber=SK-2026-01-03-001
|
||||
```
|
||||
|
||||
#### 5. Redirect URL Privatkunden
|
||||
**Beispiel:** `https://deine-domain.de/danke-privat`
|
||||
|
||||
Wohin Privat-Kunden nach erfolgreicher PayPal-Zahlung weitergeleitet werden.
|
||||
|
||||
**Query-Parameter:**
|
||||
- `orderNumber` - Die generierte Bestellnummer
|
||||
|
||||
## Backend-API Endpunkte
|
||||
|
||||
Das Plugin nutzt folgende Backend-Endpunkte:
|
||||
|
||||
### 1. Health Check
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"timestamp": "2026-01-03T..."
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Preview Batch
|
||||
```
|
||||
POST /api/preview/batch
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "session-1234567890-abc",
|
||||
"letters": [
|
||||
{
|
||||
"text": "Liebe Oma, ...",
|
||||
"font": "tilda",
|
||||
"format": "A4",
|
||||
"placeholders": {},
|
||||
"envelope": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "session-1234567890-abc",
|
||||
"previews": [
|
||||
{
|
||||
"index": 0,
|
||||
"url": "/api/preview/session-1234567890-abc/0"
|
||||
}
|
||||
],
|
||||
"batchInfo": {
|
||||
"totalLetters": 1,
|
||||
"batchSize": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Generate Order
|
||||
```
|
||||
POST /api/order/generate
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-03-001",
|
||||
"letters": [...],
|
||||
"envelopes": [...],
|
||||
"metadata": {
|
||||
"customer": {...},
|
||||
"orderDate": "2026-01-03T..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-03-001",
|
||||
"path": "/var/skrift-output/SK-2026-01-03-001",
|
||||
"files": [
|
||||
"letter_000.svg",
|
||||
"envelope_000.svg",
|
||||
"order-metadata.json",
|
||||
"placeholders.csv"
|
||||
],
|
||||
"summary": {
|
||||
"totalLetters": 100,
|
||||
"totalEnvelopes": 100,
|
||||
"fonts": ["tilda"],
|
||||
"formats": ["A4"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Für Business-Kunden
|
||||
|
||||
```
|
||||
1. Kunde füllt Konfigurator aus
|
||||
2. Kunde klickt "Jetzt kostenpflichtig bestellen"
|
||||
3. WordPress Plugin → Backend API (generateOrder)
|
||||
4. Backend generiert SVG-Dateien
|
||||
5. Backend speichert in /var/skrift-output/SK-...
|
||||
6. WordPress Plugin → Webhook aufrufen
|
||||
7. WordPress Plugin → Redirect zu Business-Danke-Seite
|
||||
```
|
||||
|
||||
### Für Privat-Kunden (später)
|
||||
|
||||
```
|
||||
1. Kunde füllt Konfigurator aus
|
||||
2. Kunde klickt "Jetzt kostenpflichtig bestellen"
|
||||
3. WordPress Plugin → PayPal Checkout
|
||||
4. PayPal → Zahlung erfolgreich
|
||||
5. PayPal Webhook → WordPress
|
||||
6. WordPress Plugin → Backend API (generateOrder)
|
||||
7. Backend generiert SVG-Dateien
|
||||
8. WordPress Plugin → Webhook aufrufen
|
||||
9. WordPress Plugin → Redirect zu Privat-Danke-Seite
|
||||
```
|
||||
|
||||
## Datenmapping
|
||||
|
||||
### Fonts
|
||||
|
||||
| Frontend | Backend |
|
||||
|----------|---------|
|
||||
| tilda | tilda (PremiumUltra79) |
|
||||
| alva | alva (PremiumUltra23) |
|
||||
| ellie | ellie (PremiumUltra39) |
|
||||
|
||||
### Formate
|
||||
|
||||
| Frontend | Backend |
|
||||
|----------|---------|
|
||||
| a4 | A4 |
|
||||
| a6p | A6_PORTRAIT |
|
||||
| a6l | A6_LANDSCAPE |
|
||||
|
||||
### Envelope-Formate
|
||||
|
||||
| Brief-Format | Envelope-Format |
|
||||
|--------------|-----------------|
|
||||
| A4 | DIN_LANG |
|
||||
| A6 | C6 |
|
||||
|
||||
## JavaScript Integration
|
||||
|
||||
### API Client laden
|
||||
|
||||
Das Plugin lädt automatisch:
|
||||
- `configurator-api.js` - Backend API Client
|
||||
- `configurator-backend-integration.js` - Integration Logic
|
||||
|
||||
### Globale Instanz
|
||||
|
||||
```javascript
|
||||
// Verfügbar in allen Konfigurator-Scripts
|
||||
const api = window.SkriftBackendAPI;
|
||||
|
||||
// Health-Check
|
||||
const isHealthy = await api.healthCheck();
|
||||
|
||||
// Preview generieren
|
||||
const result = await api.generatePreviewBatch(letters);
|
||||
|
||||
// Order generieren
|
||||
const order = await api.generateOrder(orderNumber, letters, envelopes, metadata);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Backend Health-Check
|
||||
|
||||
```javascript
|
||||
// In Browser-Console auf Konfigurator-Seite:
|
||||
const api = window.SkriftBackendAPI;
|
||||
const healthy = await api.healthCheck();
|
||||
console.log('Backend healthy:', healthy);
|
||||
```
|
||||
|
||||
### 2. Test-Order generieren
|
||||
|
||||
```javascript
|
||||
const api = window.SkriftBackendAPI;
|
||||
|
||||
const result = await api.generateOrder(
|
||||
api.generateOrderNumber(),
|
||||
[
|
||||
{
|
||||
text: 'Test Brief',
|
||||
font: 'tilda',
|
||||
format: 'A4',
|
||||
placeholders: {}
|
||||
}
|
||||
],
|
||||
[],
|
||||
{
|
||||
customer: {
|
||||
type: 'business',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
email: 'test@example.com'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Order result:', result);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Fehler: "Backend ist nicht erreichbar"
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe Backend-URL in WordPress-Einstellungen
|
||||
2. Teste Health-Check: `curl https://backend.deine-domain.de/health`
|
||||
3. Prüfe Nginx Proxy Manager Konfiguration
|
||||
4. Prüfe Backend-Container: `docker compose logs -f`
|
||||
|
||||
### Fehler: "CORS Error"
|
||||
|
||||
**Lösung:**
|
||||
Nginx Proxy Manager muss CORS-Header setzen:
|
||||
|
||||
```nginx
|
||||
# In Custom Nginx Configuration (Advanced Tab)
|
||||
add_header Access-Control-Allow-Origin "https://deine-wordpress-domain.de" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
```
|
||||
|
||||
### Fehler: "Order generation failed"
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe Backend-Logs: `docker compose logs -f`
|
||||
2. Prüfe `/var/skrift-output` Verzeichnis existiert
|
||||
3. Prüfe Fonts sind vorhanden in `/app/fonts`
|
||||
4. Prüfe `.env` hat `SCRIPTALIZER_ERR_FREQUENCY=0`
|
||||
|
||||
### Webhook wird nicht aufgerufen
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe Webhook-URL in WordPress-Einstellungen
|
||||
2. Teste Webhook manuell mit curl
|
||||
3. Prüfe N8N/Webhook-Service Logs
|
||||
4. Webhook-Fehler werden ignoriert (soft fail) - Order wird trotzdem erstellt
|
||||
|
||||
## Weitere Entwicklung
|
||||
|
||||
### Preview-System (TODO)
|
||||
|
||||
Aktuell wird direkt die finale Order generiert. Zukünftig:
|
||||
1. Kunde füllt Schritt 1-4 aus
|
||||
2. Preview generieren mit `/api/preview/batch`
|
||||
3. Kunde sieht Vorschau der Briefe
|
||||
4. Kunde bestätigt → Order finalisieren mit `/api/order/finalize`
|
||||
|
||||
### PayPal-Integration (TODO)
|
||||
|
||||
Für Privatkunden:
|
||||
1. PayPal SDK laden
|
||||
2. Order-ID an PayPal übergeben
|
||||
3. Nach Zahlung Webhook empfangen
|
||||
4. Backend-Order generieren
|
||||
5. Weiterleitung
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen:
|
||||
1. Browser-Console öffnen (F12)
|
||||
2. Logs prüfen (nach `[API]` oder `[Backend Integration]` filtern)
|
||||
3. Backend-Logs prüfen: `docker compose logs -f`
|
||||
4. WordPress Debug-Log prüfen: `wp-content/debug.log`
|
||||
|
||||
## Checkliste nach Installation
|
||||
|
||||
- [ ] Backend deployed und erreichbar
|
||||
- [ ] Health-Check erfolgreich: `https://backend.domain.de/health`
|
||||
- [ ] WordPress Admin-Einstellungen konfiguriert
|
||||
- [ ] API URL gesetzt
|
||||
- [ ] Order Webhook URL gesetzt (optional)
|
||||
- [ ] Redirect URLs gesetzt
|
||||
- [ ] Test-Bestellung generiert
|
||||
- [ ] Dateien in `/var/skrift-output` erstellt
|
||||
- [ ] Webhook wurde aufgerufen (falls konfiguriert)
|
||||
- [ ] Keine durchgestrichenen Wörter in SVGs
|
||||
- [ ] Handschrift-Variationen sichtbar (Wortabstände, Rotation)
|
||||
318
skrift-configurator/README.md
Normal file
318
skrift-configurator/README.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Skrift Konfigurator - WordPress Plugin
|
||||
|
||||
Interaktiver Konfigurator für handgeschriebene Briefe, Postkarten und Einladungen.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Multi-Step Konfigurator mit 6 Schritten
|
||||
- ✅ B2B und B2C Workflows
|
||||
- ✅ Dynamische Preisberechnung
|
||||
- ✅ Gutschein-System
|
||||
- ✅ Backend-Integration für SVG-Generierung
|
||||
- ✅ Preview-System (in Entwicklung)
|
||||
- ✅ Responsive Design
|
||||
- ✅ Vollständig anpassbare Preise und Produkte
|
||||
|
||||
## Installation
|
||||
|
||||
1. Plugin-Ordner nach `wp-content/plugins/skrift-konfigurator/` kopieren
|
||||
2. In WordPress: **Plugins → Installierte Plugins**
|
||||
3. "Skrift Konfigurator" aktivieren
|
||||
4. Zu **Einstellungen → Skrift Konfigurator** gehen
|
||||
5. Einstellungen konfigurieren
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### 1. Produkte
|
||||
|
||||
Verwalte Namen, Beschreibungen und Basispreise für alle 5 Produkte:
|
||||
- Businessbriefe
|
||||
- Business Postkarten
|
||||
- Follow-ups
|
||||
- Einladungen
|
||||
- Private Briefe
|
||||
|
||||
### 2. Preise
|
||||
|
||||
Konfiguriere Aufpreise für:
|
||||
- Formate (A4 Upgrade)
|
||||
- Versand (Direkt vs. Bulk)
|
||||
- Umschläge (mit Adresse vs. Custom Text)
|
||||
- Zusatzleistungen (Motiv-Upload, Design-Service, etc.)
|
||||
- Schriftarten (Tilda, Alva, Ellie)
|
||||
|
||||
### 3. Dynamische Preisformeln
|
||||
|
||||
Erstelle Mengenrabatt-Formeln für Business und Privatkunden:
|
||||
|
||||
**Beispiel Business:**
|
||||
```
|
||||
if (q < 50) return 2.50;
|
||||
if (q < 100) return 2.30;
|
||||
if (q < 200) return 2.10;
|
||||
return 1.90;
|
||||
```
|
||||
|
||||
**Beispiel Privat:**
|
||||
```
|
||||
if (q < 10) return 3.00;
|
||||
if (q < 25) return 2.80;
|
||||
return 2.50;
|
||||
```
|
||||
|
||||
### 4. Backend-Verbindung
|
||||
|
||||
**WICHTIG:** Für die SVG-Generierung!
|
||||
|
||||
- **API URL:** `https://backend.deine-domain.de`
|
||||
- **Order Webhook URL:** `https://n8n.deine-domain.de/webhook/order` (optional)
|
||||
- **Redirect URLs:** Wohin nach Bestellung weitergeleitet wird
|
||||
|
||||
Siehe [BACKEND_INTEGRATION.md](./BACKEND_INTEGRATION.md) für Details.
|
||||
|
||||
### 5. Gutscheine
|
||||
|
||||
Erstelle Gutschein-Codes für Rabatte oder Testbestellungen.
|
||||
|
||||
**Arten:**
|
||||
- **Prozentual:** 10%, 20%, 50%
|
||||
- **Festbetrag:** 5€, 10€, 20€
|
||||
- **Gratis:** 100% Rabatt
|
||||
|
||||
**Einstellungen:**
|
||||
- Einmalverwendung oder Mehrfachnutzung
|
||||
- Aktiv/Inaktiv Toggle
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Shortcode einfügen
|
||||
|
||||
```
|
||||
[skrift_konfigurator]
|
||||
```
|
||||
|
||||
Füge diesen Shortcode auf jeder Seite oder jedem Beitrag ein.
|
||||
|
||||
### Mit URL-Parametern
|
||||
|
||||
Direktlink zu einem Produkt:
|
||||
|
||||
```
|
||||
https://deine-domain.de/konfigurator/?businessbriefe
|
||||
https://deine-domain.de/konfigurator/?private-briefe
|
||||
https://deine-domain.de/konfigurator/?einladungen
|
||||
```
|
||||
|
||||
## Datei-Struktur
|
||||
|
||||
```
|
||||
skrift-konfigurator/
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ └── configurator.css # Styling
|
||||
│ └── js/
|
||||
│ ├── configurator-app.js # Main App
|
||||
│ ├── configurator-state.js # State Management
|
||||
│ ├── configurator-ui.js # UI Rendering
|
||||
│ ├── configurator-pricing.js # Price Calculation
|
||||
│ ├── configurator-api.js # Backend API Client
|
||||
│ └── configurator-backend-integration.js # Backend Integration
|
||||
├── includes/
|
||||
│ ├── admin-settings.php # Admin Settings Page
|
||||
│ └── admin-vouchers.php # Voucher Management
|
||||
├── skrift-konfigurator.php # Main Plugin File
|
||||
├── BACKEND_INTEGRATION.md # Backend Integration Guide
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
Das Plugin kommuniziert mit dem Node.js Backend über REST API.
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Endpoint | Method | Beschreibung |
|
||||
|----------|--------|--------------|
|
||||
| `/health` | GET | Health-Check |
|
||||
| `/api/preview/batch` | POST | Preview generieren |
|
||||
| `/api/order/generate` | POST | Order erstellen |
|
||||
| `/api/order/finalize` | POST | Order aus Preview finalisieren |
|
||||
|
||||
Siehe [BACKEND_INTEGRATION.md](./BACKEND_INTEGRATION.md) für vollständige API-Dokumentation.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Business-Kunde
|
||||
|
||||
```
|
||||
1. Produkt auswählen (Businessbriefe, Business Postkarten, Follow-ups)
|
||||
2. Menge eingeben
|
||||
3. Format wählen
|
||||
4. Versand & Umschlag konfigurieren
|
||||
5. Inhalt eingeben (Text)
|
||||
6. Kundendaten eingeben
|
||||
7. Prüfen & Bestellen
|
||||
→ Backend generiert SVG-Dateien
|
||||
→ Webhook wird aufgerufen
|
||||
→ Weiterleitung zu Danke-Seite
|
||||
```
|
||||
|
||||
### Privat-Kunde
|
||||
|
||||
```
|
||||
1. Produkt auswählen (Private Briefe, Einladungen)
|
||||
2. Menge eingeben
|
||||
3. Format wählen
|
||||
4. Versand & Umschlag konfigurieren
|
||||
5. Inhalt eingeben (Text + optional Motiv)
|
||||
6. Kundendaten eingeben
|
||||
7. Prüfen & Bestellen
|
||||
→ PayPal-Checkout (später)
|
||||
→ Backend generiert SVG-Dateien
|
||||
→ Webhook wird aufgerufen
|
||||
→ Weiterleitung zu Danke-Seite
|
||||
```
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.4+
|
||||
- WordPress 5.8+
|
||||
- Modern Browser mit ES6+ Support
|
||||
|
||||
### JavaScript
|
||||
|
||||
Das Plugin nutzt ES6 Modules und läuft ohne Build-Step.
|
||||
|
||||
**State Management:**
|
||||
- Reducer-Pattern (ähnlich Redux)
|
||||
- Immutable State Updates
|
||||
- Uni-directional Data Flow
|
||||
|
||||
**UI Rendering:**
|
||||
- Virtual DOM mit `h()` Helper
|
||||
- Deklaratives Rendering
|
||||
- Event-Delegation
|
||||
|
||||
### Debugging
|
||||
|
||||
Browser-Console öffnen (F12):
|
||||
|
||||
```javascript
|
||||
// State prüfen
|
||||
console.log(window.currentState);
|
||||
|
||||
// Backend API testen
|
||||
const api = window.SkriftBackendAPI;
|
||||
await api.healthCheck();
|
||||
|
||||
// Test-Order erstellen
|
||||
await api.generateOrder(
|
||||
api.generateOrderNumber(),
|
||||
[{ text: 'Test', font: 'tilda', format: 'A4', placeholders: {} }],
|
||||
[],
|
||||
{ customer: { type: 'business', firstName: 'Test' } }
|
||||
);
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### 1. Plugin auf Server hochladen
|
||||
|
||||
Via FTP, SSH oder WordPress Dashboard:
|
||||
```
|
||||
wp-content/plugins/skrift-konfigurator/
|
||||
```
|
||||
|
||||
### 2. Backend deployen
|
||||
|
||||
Siehe `Docker Backend/DEPLOYMENT.md`
|
||||
|
||||
### 3. WordPress konfigurieren
|
||||
|
||||
- Einstellungen → Skrift Konfigurator
|
||||
- Backend-URL setzen
|
||||
- Preise anpassen
|
||||
- Gutscheine erstellen (optional)
|
||||
|
||||
### 4. Seite erstellen
|
||||
|
||||
- Neue Seite: "Konfigurator"
|
||||
- Shortcode einfügen: `[skrift_konfigurator]`
|
||||
- Veröffentlichen
|
||||
|
||||
### 5. Testen
|
||||
|
||||
- Produkt durchklicken
|
||||
- Test-Bestellung aufgeben
|
||||
- Prüfen ob Backend-Order erstellt wurde
|
||||
- Prüfen ob Dateien in `/var/skrift-output/` erstellt wurden
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Konfigurator wird nicht angezeigt
|
||||
|
||||
**Lösung:**
|
||||
- Shortcode korrekt? `[skrift_konfigurator]`
|
||||
- JavaScript-Fehler in Console? (F12)
|
||||
- Plugin aktiviert?
|
||||
|
||||
### Backend-Verbindung fehlgeschlagen
|
||||
|
||||
**Lösung:**
|
||||
- Backend URL korrekt in Einstellungen?
|
||||
- Backend erreichbar? `curl https://backend.domain.de/health`
|
||||
- CORS konfiguriert? (Nginx Proxy Manager)
|
||||
|
||||
### Preise werden falsch berechnet
|
||||
|
||||
**Lösung:**
|
||||
- Preise in Einstellungen prüfen
|
||||
- Dynamische Formeln prüfen (Syntax)
|
||||
- Console-Logs prüfen: `window.currentState.quote`
|
||||
|
||||
### Gutschein funktioniert nicht
|
||||
|
||||
**Lösung:**
|
||||
- Gutschein ist aktiv?
|
||||
- Gutschein noch nicht verwendet? (bei Einmalverwendung)
|
||||
- Code korrekt geschrieben? (Case-sensitive!)
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 0.3.0
|
||||
- ✅ Backend-Integration implementiert
|
||||
- ✅ API Client für Preview & Order
|
||||
- ✅ Webhook-Support
|
||||
- ✅ Redirect-URLs konfigurierbar
|
||||
|
||||
### Version 0.2.0
|
||||
- ✅ Gutschein-System
|
||||
- ✅ Dynamische Preisformeln
|
||||
- ✅ Admin-Einstellungen erweitert
|
||||
|
||||
### Version 0.1.0
|
||||
- ✅ Basis-Konfigurator
|
||||
- ✅ 6 Schritte
|
||||
- ✅ B2B und B2C Workflows
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Preview-System vollständig integrieren
|
||||
- [ ] PayPal-Integration für Privatkunden
|
||||
- [ ] Email-Benachrichtigungen
|
||||
- [ ] PDF-Export der Bestellung
|
||||
- [ ] Admin-Dashboard für Bestellungen
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
1. [BACKEND_INTEGRATION.md](./BACKEND_INTEGRATION.md) lesen
|
||||
2. Browser-Console prüfen (F12)
|
||||
3. Backend-Logs prüfen: `docker compose logs -f`
|
||||
4. WordPress Debug-Log: `wp-content/debug.log`
|
||||
|
||||
## Lizenz
|
||||
|
||||
Proprietär - Alle Rechte vorbehalten
|
||||
1463
skrift-configurator/assets/css/configurator.css
Normal file
1463
skrift-configurator/assets/css/configurator.css
Normal file
File diff suppressed because it is too large
Load Diff
392
skrift-configurator/assets/js/configurator-api.js
Normal file
392
skrift-configurator/assets/js/configurator-api.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Skrift Backend API Client
|
||||
* Kommunikation mit dem Node.js Backend über WordPress Proxy
|
||||
* Der API-Token wird serverseitig gehandhabt und ist nicht im Frontend exponiert
|
||||
*/
|
||||
|
||||
class SkriftBackendAPI {
|
||||
constructor() {
|
||||
// WordPress REST API URL für den Proxy
|
||||
this.restUrl = window.SkriftConfigurator?.restUrl || '/wp-json/';
|
||||
// API Key für WordPress REST API Authentifizierung
|
||||
this.apiKey = window.SkriftConfigurator?.apiKey || '';
|
||||
// WordPress Nonce für CSRF-Schutz
|
||||
this.nonce = window.SkriftConfigurator?.nonce || '';
|
||||
// Direkte Backend-URL nur für Preview-Bilder (read-only)
|
||||
this.backendUrl = window.SkriftConfigurator?.settings?.backend_connection?.api_url || '';
|
||||
// Alias für Kompatibilität mit PreviewManager
|
||||
this.baseURL = this.backendUrl;
|
||||
this.sessionId = null;
|
||||
this.previewCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Standard-Headers für WordPress REST API zurück
|
||||
*/
|
||||
getHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': this.nonce,
|
||||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['X-Skrift-API-Key'] = this.apiKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine eindeutige Session-ID für Preview-Caching
|
||||
*/
|
||||
generateSessionId() {
|
||||
return `session-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health-Check: Prüft ob Backend erreichbar ist (über WordPress Proxy)
|
||||
*/
|
||||
async healthCheck() {
|
||||
try {
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/health`, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Health check failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.status === 'ok';
|
||||
} catch (error) {
|
||||
console.error('[API] Health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Batch: Generiert eine Vorschau von Briefen (über WordPress Proxy)
|
||||
* Briefe und Umschläge werden in derselben Session gespeichert
|
||||
*/
|
||||
async generatePreviewBatch(letters, options = {}) {
|
||||
try {
|
||||
// SessionId nur generieren wenn noch keine existiert oder explizit angefordert
|
||||
// So bleiben Briefe und Umschläge in derselben Session
|
||||
if (!this.sessionId || options.newSession) {
|
||||
this.sessionId = this.generateSessionId();
|
||||
console.log('[API] New session created:', this.sessionId);
|
||||
} else {
|
||||
console.log('[API] Reusing existing session:', this.sessionId);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
sessionId: this.sessionId,
|
||||
letters: letters.map(letter => ({
|
||||
index: letter.index,
|
||||
text: letter.text,
|
||||
font: letter.font || 'tilda',
|
||||
format: letter.format || 'A4',
|
||||
placeholders: letter.placeholders || {},
|
||||
type: letter.type || 'letter',
|
||||
envelopeType: letter.envelopeType || 'recipient',
|
||||
envelope: letter.envelope || null,
|
||||
})),
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/preview/batch`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
|
||||
if (response.status === 429 && error.retryAfter) {
|
||||
const err = new Error(error.error || 'Rate limit exceeded');
|
||||
err.retryAfter = error.retryAfter;
|
||||
err.statusCode = 429;
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error(error.error || error.message || `Preview generation failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Session-ID vom Backend übernehmen (falls anders als gesendet)
|
||||
if (data.sessionId) {
|
||||
this.sessionId = data.sessionId;
|
||||
}
|
||||
|
||||
const previews = data.files ? data.files.map((file, index) => ({
|
||||
index: file.index !== undefined ? file.index : index,
|
||||
url: file.url || file.path,
|
||||
format: file.format,
|
||||
pages: file.pages || 1,
|
||||
lineCount: file.lineCount,
|
||||
lineLimit: file.lineLimit,
|
||||
overflow: file.overflow,
|
||||
recipientName: file.recipientName,
|
||||
})) : [];
|
||||
|
||||
previews.forEach((preview, index) => {
|
||||
this.previewCache.set(`${this.sessionId}-${index}`, preview);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId: this.sessionId,
|
||||
previews: previews,
|
||||
batchInfo: data.batchInfo,
|
||||
hasOverflow: data.hasOverflow || false,
|
||||
overflowFiles: data.overflowFiles || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Preview batch error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Preview: Ruft eine einzelne Preview-URL ab
|
||||
* Hinweis: Diese Methode wird aktuell nicht verwendet, da Preview-URLs direkt vom Backend kommen
|
||||
*/
|
||||
async getPreviewUrl(sessionId, index) {
|
||||
try {
|
||||
const cacheKey = `${sessionId}-${index}`;
|
||||
|
||||
if (this.previewCache.has(cacheKey)) {
|
||||
return this.previewCache.get(cacheKey).url;
|
||||
}
|
||||
|
||||
// Über WordPress Proxy abrufen
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/preview/${sessionId}/${index}`, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Preview not found: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const svgText = await response.text();
|
||||
// Sicheres Base64-Encoding für Unicode
|
||||
const dataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgText)))}`;
|
||||
|
||||
return dataUrl;
|
||||
} catch (error) {
|
||||
console.error('[API] Get preview URL error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize Order: Finalisiert eine Bestellung aus dem Preview-Cache (über WordPress Proxy)
|
||||
*/
|
||||
async finalizeOrder(sessionId, orderNumber, metadata = {}) {
|
||||
try {
|
||||
const requestBody = {
|
||||
sessionId: sessionId,
|
||||
orderNumber: orderNumber,
|
||||
metadata: {
|
||||
customer: metadata.customer || {},
|
||||
orderDate: metadata.orderDate || new Date().toISOString(),
|
||||
...metadata,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/order/finalize`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `Order finalization failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
orderNumber: data.orderNumber,
|
||||
path: data.path,
|
||||
files: data.files,
|
||||
envelopesGenerated: data.envelopesGenerated || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Finalize order error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Order: Generiert eine Bestellung ohne Preview (direkt, über WordPress Proxy)
|
||||
* Backend erwartet alle Dokumente (Briefe + Umschläge) im letters-Array mit type-Property
|
||||
*/
|
||||
async generateOrder(orderNumber, letters, envelopes = [], metadata = {}) {
|
||||
try {
|
||||
// Letters vorbereiten
|
||||
const preparedLetters = letters.map((letter, index) => ({
|
||||
index: letter.index !== undefined ? letter.index : index,
|
||||
text: letter.text,
|
||||
font: letter.font || 'tilda',
|
||||
format: letter.format || 'A4',
|
||||
placeholders: letter.placeholders || {},
|
||||
type: 'letter',
|
||||
}));
|
||||
|
||||
// Envelopes vorbereiten und anhängen
|
||||
const preparedEnvelopes = envelopes.map((envelope, index) => ({
|
||||
index: envelope.index !== undefined ? envelope.index : index,
|
||||
text: envelope.text || '',
|
||||
font: envelope.font || 'tilda',
|
||||
format: envelope.format || 'DIN_LANG',
|
||||
placeholders: envelope.placeholders || {},
|
||||
type: 'envelope',
|
||||
envelopeType: envelope.envelopeType || 'recipient',
|
||||
}));
|
||||
|
||||
// Alle Dokumente in einem Array für Backend
|
||||
const allDocuments = [...preparedLetters, ...preparedEnvelopes];
|
||||
|
||||
const requestBody = {
|
||||
orderNumber: orderNumber,
|
||||
letters: allDocuments,
|
||||
metadata: {
|
||||
customer: metadata.customer || {},
|
||||
orderDate: metadata.orderDate || new Date().toISOString(),
|
||||
...metadata,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/order/generate`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `Order generation failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
orderNumber: data.orderNumber,
|
||||
path: data.path,
|
||||
files: data.files,
|
||||
summary: data.summary,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Generate order error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Order Number: Holt fortlaufende Bestellnummer vom WordPress-Backend
|
||||
* Schema: S-JAHR-MONAT-TAG-fortlaufendeNummer (z.B. S-2026-01-12-001)
|
||||
*/
|
||||
async generateOrderNumber() {
|
||||
try {
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/order/generate-number`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to generate order number: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.orderNumber;
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to generate order number from WP:', error);
|
||||
// Fallback: Lokale Generierung (sollte nicht passieren)
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const random = String(Math.floor(Math.random() * 1000)).padStart(3, '0');
|
||||
return `S-${year}-${month}-${day}-${random}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload Motif: Lädt ein Motiv-Bild hoch (über WordPress Proxy)
|
||||
* @param {File} file - Die hochzuladende Datei
|
||||
* @param {string} orderNumber - Die Bestellnummer für die Dateinamenszuordnung
|
||||
* @returns {Promise<{success: boolean, filename?: string, url?: string, error?: string}>}
|
||||
*/
|
||||
async uploadMotif(file, orderNumber) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('motif', file);
|
||||
formData.append('orderNumber', orderNumber || '');
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/motif/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
...(this.apiKey ? { 'X-Skrift-API-Key': this.apiKey } : {}),
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `Motif upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
filename: data.filename,
|
||||
url: data.url,
|
||||
path: data.path,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Motif upload error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Preview Cache: Löscht den Preview-Cache und setzt Session zurück
|
||||
*/
|
||||
clearPreviewCache() {
|
||||
this.previewCache.clear();
|
||||
this.sessionId = null; // Wird beim nächsten Preview-Aufruf neu generiert
|
||||
}
|
||||
|
||||
/**
|
||||
* Start New Session: Erzwingt eine neue Session für den nächsten Preview-Aufruf
|
||||
*/
|
||||
startNewSession() {
|
||||
this.sessionId = null;
|
||||
this.previewCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz exportieren
|
||||
window.SkriftBackendAPI = new SkriftBackendAPI();
|
||||
|
||||
export default SkriftBackendAPI;
|
||||
160
skrift-configurator/assets/js/configurator-app.js
Normal file
160
skrift-configurator/assets/js/configurator-app.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/* global SkriftConfigurator */
|
||||
import {
|
||||
createInitialState,
|
||||
deriveContextFromUrl,
|
||||
reducer,
|
||||
STEPS,
|
||||
} from "./configurator-state.js?ver=0.3.0";
|
||||
import { render, showValidationOverlay, hideValidationOverlay, showOverflowWarning, showValidationError, flushAllTables } from "./configurator-ui.js?ver=0.3.0";
|
||||
import './configurator-api.js'; // Backend API initialisieren
|
||||
import PreviewManager from './configurator-preview-manager.js'; // Preview Management
|
||||
|
||||
(function boot() {
|
||||
const root = document.querySelector('[data-skrift-konfigurator="1"]');
|
||||
if (!root) return;
|
||||
|
||||
// Cleanup bei Navigation (SPA) oder Seiten-Unload
|
||||
const cleanupHandlers = [];
|
||||
|
||||
const cleanup = () => {
|
||||
flushAllTables(); // Tabellen-Daten speichern vor Cleanup/Seiten-Unload
|
||||
cleanupHandlers.forEach(handler => handler());
|
||||
cleanupHandlers.length = 0;
|
||||
if (window.envelopePreviewManager) {
|
||||
window.envelopePreviewManager.destroy();
|
||||
}
|
||||
if (window.contentPreviewManager) {
|
||||
window.contentPreviewManager.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup bei Seiten-Unload
|
||||
window.addEventListener('beforeunload', cleanup);
|
||||
cleanupHandlers.push(() => window.removeEventListener('beforeunload', cleanup));
|
||||
|
||||
const ctx = deriveContextFromUrl(window.location.search);
|
||||
let state = createInitialState(ctx);
|
||||
|
||||
const dom = {
|
||||
topbar: document.getElementById("sk-topbar"),
|
||||
stepper: document.getElementById("sk-stepper"),
|
||||
form: document.getElementById("sk-form"),
|
||||
prev: document.getElementById("sk-prev"),
|
||||
next: document.getElementById("sk-next"),
|
||||
preview: document.getElementById("sk-preview"),
|
||||
previewMobile: document.getElementById("sk-preview-mobile"),
|
||||
contactMobile: document.getElementById("sk-contact-mobile"),
|
||||
};
|
||||
|
||||
// Preview Manager initialisieren
|
||||
const api = window.SkriftBackendAPI;
|
||||
if (api) {
|
||||
window.envelopePreviewManager = new PreviewManager(api);
|
||||
window.contentPreviewManager = new PreviewManager(api);
|
||||
}
|
||||
|
||||
const dispatch = (action) => {
|
||||
// Scroll-Position VOR dem State-Update speichern
|
||||
const scrollY = window.scrollY;
|
||||
const scrollX = window.scrollX;
|
||||
|
||||
state = reducer(state, action);
|
||||
render({ state, dom, dispatch });
|
||||
|
||||
// Scroll-Position NACH dem Render wiederherstellen
|
||||
// Nur bei Actions die NICHT den Step wechseln (Navigation)
|
||||
const navigationActions = ['NAV_NEXT', 'NAV_PREV', 'SET_STEP'];
|
||||
if (!navigationActions.includes(action.type)) {
|
||||
// requestAnimationFrame stellt sicher, dass das DOM komplett gerendert ist
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Event-Handler mit Cleanup-Tracking
|
||||
const prevClickHandler = () => {
|
||||
flushAllTables(); // Tabellen-Daten speichern vor Navigation
|
||||
dispatch({ type: "NAV_PREV" });
|
||||
};
|
||||
|
||||
// Next-Handler mit Validierung bei Content-Step
|
||||
const nextClickHandler = async () => {
|
||||
flushAllTables(); // Tabellen-Daten speichern vor Navigation
|
||||
|
||||
// WICHTIG: Aktuellen State verwenden, nicht den gecachten aus dem Closure
|
||||
const currentState = window.currentGlobalState || state;
|
||||
|
||||
// Bei Content-Step: Textlänge validieren und alle Previews generieren
|
||||
if (currentState.step === STEPS.CONTENT) {
|
||||
const previewManager = window.contentPreviewManager;
|
||||
if (previewManager) {
|
||||
showValidationOverlay();
|
||||
try {
|
||||
const validation = await previewManager.validateTextLength(currentState);
|
||||
|
||||
if (!validation.valid) {
|
||||
hideValidationOverlay();
|
||||
// Bei Overflow: Warnung anzeigen
|
||||
if (validation.overflowFiles && validation.overflowFiles.length > 0) {
|
||||
showOverflowWarning(validation.overflowFiles, dom.form);
|
||||
} else if (validation.error) {
|
||||
// Bei Fehler (z.B. keine Anfragen mehr): Fehlermeldung anzeigen
|
||||
showValidationError(validation.error, dom.form);
|
||||
}
|
||||
return; // Nicht weiter navigieren
|
||||
}
|
||||
|
||||
// Nach erfolgreicher Validierung: Umschlag-Previews generieren (gleiche Session)
|
||||
// So sind alle Dokumente im Cache wenn die Bestellung finalisiert wird
|
||||
const envelopeManager = window.envelopePreviewManager;
|
||||
if (envelopeManager && currentState.answers?.envelope === true) {
|
||||
try {
|
||||
envelopeManager.previewCount = parseInt(currentState.answers?.quantity) || 1;
|
||||
await envelopeManager.loadAllPreviews(currentState, true, true);
|
||||
console.log('[App] Envelope previews generated for cache');
|
||||
} catch (envError) {
|
||||
console.error('[App] Envelope preview generation failed:', envError);
|
||||
// Nicht blockieren - Umschläge sind nicht kritisch für Navigation
|
||||
}
|
||||
}
|
||||
|
||||
hideValidationOverlay();
|
||||
} catch (error) {
|
||||
hideValidationOverlay();
|
||||
console.error('[App] Validation error:', error);
|
||||
showValidationError(error.message || 'Validierung fehlgeschlagen', dom.form);
|
||||
return; // Nicht weiter navigieren
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch({ type: "NAV_NEXT" });
|
||||
};
|
||||
|
||||
dom.prev.addEventListener("click", prevClickHandler);
|
||||
dom.next.addEventListener("click", nextClickHandler);
|
||||
cleanupHandlers.push(() => {
|
||||
dom.prev.removeEventListener("click", prevClickHandler);
|
||||
dom.next.removeEventListener("click", nextClickHandler);
|
||||
});
|
||||
|
||||
// Keyboard Navigation für Previews (nur innerhalb des aktuellen Batches)
|
||||
const keydownHandler = (e) => {
|
||||
if (window.envelopePreviewManager && window.envelopePreviewManager.currentBatchPreviews.length > 0) {
|
||||
window.envelopePreviewManager.handleKeyboardNavigation(e, state, dom.preview, true);
|
||||
}
|
||||
if (window.contentPreviewManager && window.contentPreviewManager.currentBatchPreviews.length > 0) {
|
||||
window.contentPreviewManager.handleKeyboardNavigation(e, state, dom.preview, false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keydownHandler);
|
||||
cleanupHandlers.push(() => document.removeEventListener('keydown', keydownHandler));
|
||||
|
||||
render({ state, dom, dispatch });
|
||||
|
||||
// Konfigurator sichtbar machen nachdem erster Render abgeschlossen ist
|
||||
requestAnimationFrame(() => {
|
||||
root.classList.add('sk-ready');
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* Backend Integration für Skrift Konfigurator
|
||||
* Erweitert handleOrderSubmit um Backend-API Calls
|
||||
*/
|
||||
|
||||
import SkriftBackendAPI from './configurator-api.js';
|
||||
import { preparePlaceholdersForIndex } from './configurator-utils.js';
|
||||
|
||||
/**
|
||||
* Bereitet Letter-Daten für Backend vor
|
||||
*/
|
||||
function prepareLettersForBackend(state) {
|
||||
const letters = [];
|
||||
const quantity = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
// Haupttext
|
||||
const mainText = state.answers?.letterText || state.answers?.text || state.answers?.briefText || '';
|
||||
const font = state.answers?.font || 'tilda';
|
||||
const format = state.answers?.format || 'A4';
|
||||
|
||||
// Für jede Kopie einen Letter-Eintrag erstellen mit individuellen Platzhaltern
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const placeholders = preparePlaceholdersForIndex(state, i);
|
||||
|
||||
letters.push({
|
||||
index: i,
|
||||
text: mainText,
|
||||
font: mapFontToBackend(font),
|
||||
format: mapFormatToBackend(format),
|
||||
placeholders: placeholders,
|
||||
type: 'letter',
|
||||
});
|
||||
}
|
||||
|
||||
return letters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Envelope-Daten für Backend vor
|
||||
*/
|
||||
function prepareEnvelopesForBackend(state) {
|
||||
const envelopes = [];
|
||||
// envelope ist ein boolean (true/false), nicht 'yes'/'no'
|
||||
const hasEnvelope = state.answers?.envelope === true;
|
||||
|
||||
console.log('[Backend Integration] prepareEnvelopesForBackend:', {
|
||||
envelope: state.answers?.envelope,
|
||||
hasEnvelope,
|
||||
envelopeMode: state.answers?.envelopeMode,
|
||||
});
|
||||
|
||||
if (!hasEnvelope) {
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
const quantity = parseInt(state.answers?.quantity) || 1;
|
||||
const envelopeMode = state.answers?.envelopeMode || 'recipientData';
|
||||
const format = state.answers?.format || 'A4';
|
||||
const font = state.answers?.envelopeFont || state.answers?.font || 'tilda';
|
||||
|
||||
// Envelope Format bestimmen
|
||||
const envelopeFormat = format === 'a4' ? 'DIN_LANG' : 'C6';
|
||||
|
||||
if (envelopeMode === 'recipientData') {
|
||||
// Empfängeradresse-Modus: Ein Envelope pro Brief mit individuellen Empfängerdaten
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const placeholders = preparePlaceholdersForIndex(state, i);
|
||||
const recipient = state.recipientRows?.[i] || {};
|
||||
|
||||
// Umschlagtext aus Empfängerdaten zusammenbauen
|
||||
const lines = [];
|
||||
const fullName = `${recipient.firstName || ''} ${recipient.lastName || ''}`.trim();
|
||||
if (fullName) lines.push(fullName);
|
||||
|
||||
const streetLine = `${recipient.street || ''} ${recipient.houseNumber || ''}`.trim();
|
||||
if (streetLine) lines.push(streetLine);
|
||||
|
||||
const location = `${recipient.zip || ''} ${recipient.city || ''}`.trim();
|
||||
if (location) lines.push(location);
|
||||
|
||||
if (recipient.country && recipient.country !== 'Deutschland') {
|
||||
lines.push(recipient.country);
|
||||
}
|
||||
|
||||
envelopes.push({
|
||||
index: i,
|
||||
text: lines.join('\n'),
|
||||
font: mapFontToBackend(font),
|
||||
format: envelopeFormat,
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: 'recipient',
|
||||
});
|
||||
}
|
||||
} else if (envelopeMode === 'customText') {
|
||||
// Custom Text Modus mit Platzhaltern
|
||||
const customText = state.answers?.envelopeCustomText || '';
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const placeholders = preparePlaceholdersForIndex(state, i);
|
||||
|
||||
envelopes.push({
|
||||
index: i,
|
||||
text: customText,
|
||||
font: mapFontToBackend(font),
|
||||
format: envelopeFormat,
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: 'custom',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
// Hinweis: preparePlaceholdersForIndex ist jetzt in configurator-utils.js
|
||||
|
||||
/**
|
||||
* Mapped Frontend-Font zu Backend-Font
|
||||
*/
|
||||
function mapFontToBackend(frontendFont) {
|
||||
const fontMap = {
|
||||
'tilda': 'tilda',
|
||||
'alva': 'alva',
|
||||
'ellie': 'ellie',
|
||||
// Füge weitere Mappings hinzu falls nötig
|
||||
};
|
||||
|
||||
return fontMap[frontendFont] || 'tilda';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped Frontend-Format zu Backend-Format
|
||||
*/
|
||||
function mapFormatToBackend(frontendFormat) {
|
||||
const formatMap = {
|
||||
'a4': 'A4',
|
||||
'a6p': 'A6_PORTRAIT',
|
||||
'a6l': 'A6_LANDSCAPE',
|
||||
'A4': 'A4',
|
||||
'A6_PORTRAIT': 'A6_PORTRAIT',
|
||||
'A6_LANDSCAPE': 'A6_LANDSCAPE',
|
||||
};
|
||||
|
||||
return formatMap[frontendFormat] || 'A4';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt das Umschlag-Format basierend auf Brief-Format
|
||||
*/
|
||||
function getEnvelopeFormat(letterFormat) {
|
||||
const format = String(letterFormat).toLowerCase();
|
||||
if (format === 'a4') return 'DIN_LANG';
|
||||
if (format === 'a6p' || format === 'a6l') return 'C6';
|
||||
return 'DIN_LANG';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Format für lesbare Ausgabe
|
||||
*/
|
||||
function formatFormatLabel(format) {
|
||||
const labels = {
|
||||
'a4': 'A4 Hochformat',
|
||||
'a6p': 'A6 Hochformat',
|
||||
'a6l': 'A6 Querformat',
|
||||
};
|
||||
return labels[format] || format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Font für lesbare Ausgabe
|
||||
*/
|
||||
function formatFontLabel(font) {
|
||||
const labels = {
|
||||
'tilda': 'Tilda',
|
||||
'alva': 'Alva',
|
||||
'ellie': 'Ellie',
|
||||
};
|
||||
return labels[font] || font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut das komplette Webhook-Datenobjekt zusammen
|
||||
* Enthält ALLE relevanten Felder für Bestellbestätigung und n8n Workflow
|
||||
*/
|
||||
function buildWebhookData(state, backendResult) {
|
||||
const answers = state.answers || {};
|
||||
const order = state.order || {};
|
||||
const quote = state.quote || {};
|
||||
const ctx = state.ctx || {};
|
||||
|
||||
// Gutschein-Informationen
|
||||
const voucherCode = order.voucherStatus?.valid ? order.voucherCode : null;
|
||||
const voucherDiscount = order.voucherStatus?.valid ? (order.voucherStatus.discount || 0) : 0;
|
||||
|
||||
// Umschlag-Format ermitteln
|
||||
const envelopeFormat = answers.envelope ? getEnvelopeFormat(answers.format) : null;
|
||||
|
||||
// Inland/Ausland zählen
|
||||
let domesticCount = 0;
|
||||
let internationalCount = 0;
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
const rows = addressMode === 'free' ? (state.freeAddressRows || []) : (state.recipientRows || []);
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row) continue;
|
||||
const country = addressMode === 'free' ? (row.line5 || '') : (row.country || '');
|
||||
const countryLower = country.toLowerCase().trim();
|
||||
const isDomestic = !countryLower ||
|
||||
countryLower === 'deutschland' ||
|
||||
countryLower === 'germany' ||
|
||||
countryLower === 'de';
|
||||
|
||||
if (isDomestic) {
|
||||
domesticCount++;
|
||||
} else {
|
||||
internationalCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// === BESTELLNUMMER & ZEITSTEMPEL ===
|
||||
orderNumber: backendResult?.orderNumber || null,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
// === KUNDE ===
|
||||
customerType: answers.customerType || 'private',
|
||||
customerTypeLabel: answers.customerType === 'business' ? 'Geschäftskunde' : 'Privatkunde',
|
||||
|
||||
// === PRODUKT ===
|
||||
product: ctx.product?.key || null,
|
||||
productLabel: ctx.product?.label || null,
|
||||
productCategory: ctx.product?.category || null,
|
||||
|
||||
// === MENGE ===
|
||||
quantity: parseInt(answers.quantity) || 0,
|
||||
domesticCount: domesticCount,
|
||||
internationalCount: internationalCount,
|
||||
|
||||
// === FORMAT & SCHRIFT ===
|
||||
format: answers.format || null,
|
||||
formatLabel: formatFormatLabel(answers.format),
|
||||
font: answers.font || 'tilda',
|
||||
fontLabel: formatFontLabel(answers.font || 'tilda'),
|
||||
|
||||
// === VERSAND ===
|
||||
shippingMode: answers.shippingMode || null,
|
||||
shippingModeLabel: answers.shippingMode === 'direct' ? 'Einzelversand durch Skrift' : 'Sammellieferung',
|
||||
|
||||
// === UMSCHLAG ===
|
||||
envelopeIncluded: answers.envelope === true,
|
||||
envelopeFormat: envelopeFormat,
|
||||
envelopeFormatLabel: envelopeFormat === 'DIN_LANG' ? 'DIN Lang' : (envelopeFormat === 'C6' ? 'C6' : null),
|
||||
envelopeMode: answers.envelopeMode || null,
|
||||
envelopeModeLabel: answers.envelopeMode === 'recipientData' ? 'Empfängeradresse' :
|
||||
(answers.envelopeMode === 'customText' ? 'Individueller Text' : null),
|
||||
envelopeFont: answers.envelopeFont || answers.font || 'tilda',
|
||||
envelopeFontLabel: formatFontLabel(answers.envelopeFont || answers.font || 'tilda'),
|
||||
envelopeCustomText: answers.envelopeCustomText || null,
|
||||
|
||||
// === INHALT ===
|
||||
contentCreateMode: answers.contentCreateMode || null,
|
||||
contentCreateModeLabel: answers.contentCreateMode === 'self' ? 'Selbst erstellt' :
|
||||
(answers.contentCreateMode === 'textservice' ? 'Textservice' : null),
|
||||
letterText: answers.letterText || null,
|
||||
|
||||
// === MOTIV ===
|
||||
motifNeeded: answers.motifNeed === true,
|
||||
motifSource: answers.motifSource || null,
|
||||
motifSourceLabel: answers.motifSource === 'upload' ? 'Eigenes Motiv hochgeladen' :
|
||||
(answers.motifSource === 'printed' ? 'Bedruckte Karten verwenden' :
|
||||
(answers.motifSource === 'design' ? 'Designservice' : null)),
|
||||
motifFileName: answers.motifFileName || null,
|
||||
motifFileMeta: answers.motifFileMeta || null,
|
||||
|
||||
// === SERVICES ===
|
||||
serviceText: answers.serviceText === true,
|
||||
serviceDesign: answers.serviceDesign === true,
|
||||
serviceApi: answers.serviceApi === true,
|
||||
|
||||
// === FOLLOW-UP DETAILS (nur bei Follow-ups) ===
|
||||
followupYearlyVolume: ctx.product?.isFollowUp ? (answers.followupYearlyVolume || null) : null,
|
||||
followupCreateMode: ctx.product?.isFollowUp ? (answers.followupCreateMode || null) : null,
|
||||
followupCreateModeLabel: ctx.product?.isFollowUp ? (
|
||||
answers.followupCreateMode === 'auto' ? 'Automatisch (API)' :
|
||||
(answers.followupCreateMode === 'manual' ? 'Manuell' : null)
|
||||
) : null,
|
||||
followupSourceSystem: ctx.product?.isFollowUp ? (answers.followupSourceSystem || null) : null,
|
||||
followupTriggerDescription: ctx.product?.isFollowUp ? (answers.followupTriggerDescription || null) : null,
|
||||
followupCheckCycle: ctx.product?.isFollowUp ? (answers.followupCheckCycle || null) : null,
|
||||
followupCheckCycleLabel: ctx.product?.isFollowUp ? (
|
||||
answers.followupCheckCycle === 'weekly' ? 'Wöchentlich' :
|
||||
(answers.followupCheckCycle === 'monthly' ? 'Monatlich' :
|
||||
(answers.followupCheckCycle === 'quarterly' ? 'Quartalsweise' : null))
|
||||
) : null,
|
||||
|
||||
// === GUTSCHEIN ===
|
||||
voucherCode: voucherCode,
|
||||
voucherDiscount: voucherDiscount,
|
||||
|
||||
// === PREISE ===
|
||||
currency: quote.currency || 'EUR',
|
||||
subtotalNet: quote.subtotalNet || 0,
|
||||
vatRate: quote.vatRate || 0.19,
|
||||
vatAmount: quote.vatAmount || 0,
|
||||
totalGross: quote.totalGross || 0,
|
||||
priceLines: quote.lines || [],
|
||||
|
||||
// === KUNDENDATEN (Rechnungsadresse) ===
|
||||
billingFirstName: order.billing?.firstName || '',
|
||||
billingLastName: order.billing?.lastName || '',
|
||||
billingCompany: order.billing?.company || '',
|
||||
billingEmail: order.billing?.email || '',
|
||||
billingPhone: order.billing?.phone || '',
|
||||
billingStreet: order.billing?.street || '',
|
||||
billingHouseNumber: order.billing?.houseNumber || '',
|
||||
billingZip: order.billing?.zip || '',
|
||||
billingCity: order.billing?.city || '',
|
||||
billingCountry: order.billing?.country || 'Deutschland',
|
||||
|
||||
// === LIEFERADRESSE (falls abweichend) ===
|
||||
shippingDifferent: order.shippingDifferent || false,
|
||||
shippingFirstName: order.shippingDifferent ? (order.shipping?.firstName || '') : null,
|
||||
shippingLastName: order.shippingDifferent ? (order.shipping?.lastName || '') : null,
|
||||
shippingCompany: order.shippingDifferent ? (order.shipping?.company || '') : null,
|
||||
shippingStreet: order.shippingDifferent ? (order.shipping?.street || '') : null,
|
||||
shippingHouseNumber: order.shippingDifferent ? (order.shipping?.houseNumber || '') : null,
|
||||
shippingZip: order.shippingDifferent ? (order.shipping?.zip || '') : null,
|
||||
shippingCity: order.shippingDifferent ? (order.shipping?.city || '') : null,
|
||||
shippingCountry: order.shippingDifferent ? (order.shipping?.country || 'Deutschland') : null,
|
||||
|
||||
// === EMPFÄNGERLISTE ===
|
||||
addressMode: addressMode,
|
||||
addressModeLabel: addressMode === 'free' ? 'Freie Adresszeilen' : 'Klassische Adresse',
|
||||
recipients: addressMode === 'classic' ? (state.recipientRows || []).map((r, i) => ({
|
||||
index: i,
|
||||
firstName: r?.firstName || '',
|
||||
lastName: r?.lastName || '',
|
||||
street: r?.street || '',
|
||||
houseNumber: r?.houseNumber || '',
|
||||
zip: r?.zip || '',
|
||||
city: r?.city || '',
|
||||
country: r?.country || 'Deutschland',
|
||||
})) : null,
|
||||
recipientsFree: addressMode === 'free' ? (state.freeAddressRows || []).map((r, i) => ({
|
||||
index: i,
|
||||
line1: r?.line1 || '',
|
||||
line2: r?.line2 || '',
|
||||
line3: r?.line3 || '',
|
||||
line4: r?.line4 || '',
|
||||
line5: r?.line5 || '',
|
||||
})) : null,
|
||||
|
||||
// === PLATZHALTER ===
|
||||
placeholdersEnvelope: state.placeholders?.envelope || [],
|
||||
placeholdersLetter: state.placeholders?.letter || [],
|
||||
placeholderValues: state.placeholderValues || {},
|
||||
|
||||
// === BACKEND RESULT (falls vorhanden) ===
|
||||
backendPath: backendResult?.path || null,
|
||||
backendFiles: backendResult?.files || [],
|
||||
backendSummary: backendResult?.summary || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Metadaten für Backend vor
|
||||
*/
|
||||
function prepareOrderMetadata(state) {
|
||||
return {
|
||||
customer: {
|
||||
type: state.answers?.customerType || 'private',
|
||||
firstName: state.order?.firstName || '',
|
||||
lastName: state.order?.lastName || '',
|
||||
company: state.order?.company || '',
|
||||
email: state.order?.email || '',
|
||||
phone: state.order?.phone || '',
|
||||
street: state.order?.street || '',
|
||||
zip: state.order?.zip || '',
|
||||
city: state.order?.city || '',
|
||||
},
|
||||
orderDate: new Date().toISOString(),
|
||||
product: state.ctx?.product?.key || '',
|
||||
quantity: state.answers?.quantity || 1,
|
||||
format: state.answers?.format || 'A4',
|
||||
shippingMode: state.answers?.shippingMode || 'direct',
|
||||
quote: state.quote || {},
|
||||
voucherCode: state.order?.voucherCode || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweiterte Order-Submit Funktion mit Backend-Integration
|
||||
*/
|
||||
export async function handleOrderSubmitWithBackend(state) {
|
||||
const isB2B = state.answers?.customerType === "business";
|
||||
const backend = window.SkriftConfigurator?.settings?.backend_connection || {};
|
||||
const webhookUrl = backend.order_webhook_url;
|
||||
const redirectUrlBusiness = backend.redirect_url_business;
|
||||
const redirectUrlPrivate = backend.redirect_url_private;
|
||||
const api = window.SkriftBackendAPI;
|
||||
|
||||
// Prüfe ob Backend konfiguriert ist
|
||||
if (!backend.api_url) {
|
||||
console.warn('[Backend Integration] Backend API URL nicht konfiguriert');
|
||||
// Fallback zur alten Logik
|
||||
return handleOrderSubmitLegacy(state);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Backend Health Check
|
||||
const isHealthy = await api.healthCheck();
|
||||
if (!isHealthy) {
|
||||
throw new Error('Backend ist nicht erreichbar');
|
||||
}
|
||||
|
||||
// 2. Bestellnummer generieren (fortlaufend vom WP-Backend)
|
||||
const orderNumber = await api.generateOrderNumber();
|
||||
|
||||
// 3. Daten vorbereiten
|
||||
const letters = prepareLettersForBackend(state);
|
||||
const envelopes = prepareEnvelopesForBackend(state);
|
||||
const metadata = prepareOrderMetadata(state);
|
||||
|
||||
console.log('[Backend Integration] Generating order:', {
|
||||
orderNumber,
|
||||
letters,
|
||||
envelopes,
|
||||
metadata,
|
||||
});
|
||||
|
||||
// 4. Order im Backend generieren
|
||||
const result = await api.generateOrder(
|
||||
orderNumber,
|
||||
letters,
|
||||
envelopes,
|
||||
metadata
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Order generation failed');
|
||||
}
|
||||
|
||||
console.log('[Backend Integration] Order generated successfully:', result);
|
||||
|
||||
// 5. Gutschein als verwendet markieren (falls vorhanden)
|
||||
const voucherCode = state.order?.voucherStatus?.valid
|
||||
? state.order.voucherCode
|
||||
: null;
|
||||
if (voucherCode) {
|
||||
try {
|
||||
const restUrl = window.SkriftConfigurator?.restUrl || "/wp-json/";
|
||||
await fetch(restUrl + "skrift/v1/voucher/use", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ code: voucherCode }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[Backend Integration] Fehler beim Markieren des Gutscheins:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Webhook aufrufen (wenn konfiguriert)
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
const webhookData = buildWebhookData(state, result);
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(webhookData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[Backend Integration] Webhook call failed:', response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Backend Integration] Webhook error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Weiterleitung
|
||||
if (isB2B) {
|
||||
if (redirectUrlBusiness) {
|
||||
// Bestellnummer als Query-Parameter anhängen
|
||||
const redirectUrl = new URL(redirectUrlBusiness);
|
||||
redirectUrl.searchParams.set('orderNumber', result.orderNumber);
|
||||
window.location.href = redirectUrl.toString();
|
||||
} else {
|
||||
alert(
|
||||
`Vielen Dank für Ihre Bestellung!\n\nBestellnummer: ${result.orderNumber}\n\nSie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Privatkunde: Zu PayPal weiterleiten
|
||||
if (redirectUrlPrivate) {
|
||||
const redirectUrl = new URL(redirectUrlPrivate);
|
||||
redirectUrl.searchParams.set('orderNumber', result.orderNumber);
|
||||
window.location.href = redirectUrl.toString();
|
||||
} else {
|
||||
alert(
|
||||
`Bestellung erfolgreich erstellt!\n\nBestellnummer: ${result.orderNumber}\n\nWeiterleitung zu PayPal folgt...`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Backend Integration] Order submission failed:', error);
|
||||
|
||||
alert(
|
||||
`Fehler bei der Bestellverarbeitung:\n\n${error.message}\n\nBitte versuchen Sie es erneut oder kontaktieren Sie uns.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy Order-Submit (Fallback ohne Backend)
|
||||
*/
|
||||
async function handleOrderSubmitLegacy(state) {
|
||||
const isB2B = state.answers?.customerType === "business";
|
||||
const backend = window.SkriftConfigurator?.settings?.backend_connection || {};
|
||||
const webhookUrl = backend.order_webhook_url;
|
||||
const redirectUrlBusiness = backend.redirect_url_business;
|
||||
const redirectUrlPrivate = backend.redirect_url_private;
|
||||
|
||||
// Gutschein als verwendet markieren
|
||||
const voucherCode = state.order?.voucherStatus?.valid
|
||||
? state.order.voucherCode
|
||||
: null;
|
||||
if (voucherCode) {
|
||||
try {
|
||||
const restUrl = window.SkriftConfigurator?.restUrl || "/wp-json/";
|
||||
await fetch(restUrl + "skrift/v1/voucher/use", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ code: voucherCode }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Fehler beim Markieren des Gutscheins:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook aufrufen
|
||||
if (isB2B && webhookUrl) {
|
||||
try {
|
||||
const webhookData = buildWebhookData(state, null);
|
||||
|
||||
await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(webhookData),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Webhook error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Weiterleitung
|
||||
if (isB2B && redirectUrlBusiness) {
|
||||
window.location.href = redirectUrlBusiness;
|
||||
} else if (!isB2B && redirectUrlPrivate) {
|
||||
window.location.href = redirectUrlPrivate;
|
||||
} else {
|
||||
alert("Vielen Dank für Ihre Bestellung!");
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
handleOrderSubmitWithBackend,
|
||||
prepareLettersForBackend,
|
||||
prepareEnvelopesForBackend,
|
||||
mapFontToBackend,
|
||||
mapFormatToBackend,
|
||||
buildWebhookData,
|
||||
};
|
||||
622
skrift-configurator/assets/js/configurator-preview-manager.js
Normal file
622
skrift-configurator/assets/js/configurator-preview-manager.js
Normal file
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* Preview Manager - Verwaltet Preview-Generation mit Batch-Loading, Navigation und Rate-Limiting
|
||||
*/
|
||||
|
||||
import { preparePlaceholdersForIndex } from './configurator-utils.js';
|
||||
|
||||
class PreviewManager {
|
||||
constructor(api) {
|
||||
this.api = api;
|
||||
this.currentBatchIndex = 0;
|
||||
this.currentDocIndex = 0;
|
||||
this.previewCount = 0;
|
||||
this.batchSize = 25;
|
||||
this.currentBatchPreviews = [];
|
||||
this.requestsRemaining = 10;
|
||||
this.maxRequests = 10;
|
||||
// Für Änderungserkennung und Validierungs-Caching
|
||||
this.lastValidatedTextHash = null;
|
||||
this.lastValidationResult = null;
|
||||
this.lastOverflowFiles = null; // null = keine Validierung durchgeführt
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt einen einfachen Hash für Text-Vergleich
|
||||
*/
|
||||
hashText(text, quantity, format, font) {
|
||||
const str = `${text || ''}|${quantity || 1}|${format || 'a4'}|${font || 'tilda'}`;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob sich der Text seit der letzten Validierung geändert hat
|
||||
*/
|
||||
hasTextChanged(state) {
|
||||
const currentHash = this.hashText(
|
||||
state.answers?.letterText,
|
||||
state.answers?.quantity,
|
||||
state.answers?.format,
|
||||
state.answers?.font
|
||||
);
|
||||
return currentHash !== this.lastValidatedTextHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert den aktuellen Text-Hash nach Validierung
|
||||
*/
|
||||
saveTextHash(state) {
|
||||
this.lastValidatedTextHash = this.hashText(
|
||||
state.answers?.letterText,
|
||||
state.answers?.quantity,
|
||||
state.answers?.format,
|
||||
state.answers?.font
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Platzhalter für ein bestimmtes Dokument vor
|
||||
* Verwendet jetzt die gemeinsame Utility-Funktion
|
||||
*/
|
||||
preparePlaceholders(state, index) {
|
||||
return preparePlaceholdersForIndex(state, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Letter-Objekt für ein bestimmtes Dokument
|
||||
*/
|
||||
prepareLetter(state, index, isEnvelope = false) {
|
||||
const placeholders = this.preparePlaceholders(state, index);
|
||||
|
||||
if (isEnvelope) {
|
||||
const isRecipientMode = state.answers?.envelopeMode === 'recipientData';
|
||||
const isCustomMode = state.answers?.envelopeMode === 'customText';
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
|
||||
let envelopeText = '';
|
||||
let envelopeType = 'recipient';
|
||||
|
||||
if (isRecipientMode) {
|
||||
if (addressMode === 'free' && Array.isArray(state.freeAddressRows) && state.freeAddressRows.length > index) {
|
||||
// Freie Adresse: Bis zu 5 Zeilen
|
||||
const freeAddr = state.freeAddressRows[index];
|
||||
const lines = [];
|
||||
if (freeAddr.line1) lines.push(freeAddr.line1);
|
||||
if (freeAddr.line2) lines.push(freeAddr.line2);
|
||||
if (freeAddr.line3) lines.push(freeAddr.line3);
|
||||
if (freeAddr.line4) lines.push(freeAddr.line4);
|
||||
if (freeAddr.line5) lines.push(freeAddr.line5);
|
||||
envelopeText = lines.join('\n');
|
||||
envelopeType = 'free';
|
||||
} else if (addressMode === 'classic' && Array.isArray(state.recipientRows) && state.recipientRows.length > index) {
|
||||
// Klassische Adresse
|
||||
const recipient = state.recipientRows[index];
|
||||
const lines = [];
|
||||
|
||||
const fullName = `${recipient.firstName || ''} ${recipient.lastName || ''}`.trim();
|
||||
if (fullName) lines.push(fullName);
|
||||
|
||||
const streetLine = `${recipient.street || ''} ${recipient.houseNumber || ''}`.trim();
|
||||
if (streetLine) lines.push(streetLine);
|
||||
|
||||
const location = `${recipient.zip || ''} ${recipient.city || ''}`.trim();
|
||||
if (location) lines.push(location);
|
||||
|
||||
if (recipient.country && recipient.country !== 'Deutschland') {
|
||||
lines.push(recipient.country);
|
||||
}
|
||||
|
||||
envelopeText = lines.join('\n');
|
||||
envelopeType = 'recipient';
|
||||
}
|
||||
} else if (isCustomMode) {
|
||||
envelopeText = state.answers?.envelopeCustomText || '';
|
||||
envelopeType = 'custom';
|
||||
}
|
||||
|
||||
return {
|
||||
index: index,
|
||||
text: envelopeText,
|
||||
font: state.answers?.envelopeFont || 'tilda',
|
||||
format: state.answers?.format === 'a4' ? 'DIN_LANG' : 'C6',
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: envelopeType,
|
||||
envelope: {
|
||||
type: envelopeType
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
index: index,
|
||||
text: state.answers?.letterText || state.answers?.text || state.answers?.briefText || '',
|
||||
font: state.answers?.font || 'tilda',
|
||||
format: state.answers?.format || 'a4',
|
||||
placeholders: placeholders,
|
||||
type: 'letter'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt ALLE Previews auf einmal
|
||||
* @param {boolean} skipLimitCheck - Wenn true, wird das Request-Limit ignoriert (für Validierung)
|
||||
*/
|
||||
async loadAllPreviews(state, isEnvelope = false, skipLimitCheck = false) {
|
||||
// Request-Limit nur prüfen wenn nicht übersprungen (normale Preview-Generierung)
|
||||
if (!skipLimitCheck) {
|
||||
if (this.requestsRemaining <= 0) {
|
||||
throw new Error(`Maximale Anzahl von ${this.maxRequests} Vorschau-Anfragen erreicht.`);
|
||||
}
|
||||
this.requestsRemaining--;
|
||||
}
|
||||
|
||||
const letters = [];
|
||||
for (let i = 0; i < this.previewCount; i++) {
|
||||
letters.push(this.prepareLetter(state, i, isEnvelope));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.api.generatePreviewBatch(letters);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Preview generation failed');
|
||||
}
|
||||
|
||||
this.currentBatchPreviews = result.previews;
|
||||
this.lastValidationResult = result; // Speichere für Overflow-Check
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Load error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Textlänge durch Preview-Generierung
|
||||
* Wenn Text nicht geändert wurde und Preview bereits existiert, wird gecachtes Ergebnis verwendet
|
||||
* @returns {Object} { valid: boolean, overflowFiles: Array, fromCache: boolean }
|
||||
*/
|
||||
async validateTextLength(state, forceRevalidate = false) {
|
||||
// Prüfe ob wir gecachtes Ergebnis verwenden können
|
||||
if (!forceRevalidate && !this.hasTextChanged(state) && this.lastOverflowFiles !== null) {
|
||||
console.log('[PreviewManager] Using cached validation result');
|
||||
return {
|
||||
valid: this.lastOverflowFiles.length === 0,
|
||||
overflowFiles: this.lastOverflowFiles,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
this.previewCount = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
try {
|
||||
// skipLimitCheck = true: Validierung soll immer möglich sein, auch ohne verbleibende Anfragen
|
||||
await this.loadAllPreviews(state, false, true);
|
||||
|
||||
const result = this.lastValidationResult;
|
||||
if (!result) {
|
||||
this.lastOverflowFiles = [];
|
||||
this.saveTextHash(state);
|
||||
return { valid: true, overflowFiles: [], fromCache: false };
|
||||
}
|
||||
|
||||
const hasOverflow = result.hasOverflow || false;
|
||||
const overflowFiles = (result.overflowFiles || []).map(f => ({
|
||||
index: f.index,
|
||||
lineCount: f.lineCount,
|
||||
lineLimit: f.lineLimit
|
||||
}));
|
||||
|
||||
// Cache das Ergebnis
|
||||
this.lastOverflowFiles = overflowFiles;
|
||||
this.saveTextHash(state);
|
||||
|
||||
return {
|
||||
valid: !hasOverflow,
|
||||
overflowFiles: overflowFiles,
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Validation error:', error);
|
||||
|
||||
// Bei Fehlern: Nicht durchlassen - Nutzer muss es erneut versuchen
|
||||
return {
|
||||
valid: false,
|
||||
overflowFiles: [],
|
||||
error: error.message,
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt gecachte Overflow-Infos zurück (ohne neue Anfrage)
|
||||
*/
|
||||
getCachedOverflowFiles() {
|
||||
return this.lastOverflowFiles || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Validierung bereits erfolgt ist (für UI-Anzeige)
|
||||
*/
|
||||
hasValidationResult() {
|
||||
return this.lastOverflowFiles !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Previews und zeigt Overflow-Warnung wenn nötig
|
||||
* @returns {Object} { success: boolean, hasOverflow: boolean, overflowFiles: Array }
|
||||
*/
|
||||
async generatePreviews(state, dom, isEnvelope = false) {
|
||||
const btn = dom.querySelector('.sk-preview-generate-btn');
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
const requestCounterEl = dom.querySelector('.sk-preview-request-counter');
|
||||
|
||||
if (!btn) return { success: false, hasOverflow: false, overflowFiles: [] };
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generiere Vorschau...';
|
||||
|
||||
this.previewCount = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Lade alle ${this.previewCount} Dokumente...`;
|
||||
}
|
||||
|
||||
await this.loadAllPreviews(state, isEnvelope);
|
||||
|
||||
// Text-Hash speichern für Änderungserkennung
|
||||
if (!isEnvelope) {
|
||||
this.saveTextHash(state);
|
||||
}
|
||||
|
||||
this.currentDocIndex = 0;
|
||||
this.showPreview(0, dom);
|
||||
this.showNavigationControls(dom, state, isEnvelope);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Dokument 1 von ${this.previewCount}`;
|
||||
}
|
||||
|
||||
if (requestCounterEl) {
|
||||
requestCounterEl.textContent = `Verbleibende Vorschau-Anfragen: ${this.requestsRemaining}/${this.maxRequests}`;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.textContent.includes('Umschlag') ? 'Umschlag Vorschau generieren' : 'Vorschau Schriftstück generieren';
|
||||
|
||||
// Overflow-Prüfung für Briefe (nicht Umschläge)
|
||||
if (!isEnvelope && this.lastValidationResult) {
|
||||
const hasOverflow = this.lastValidationResult.hasOverflow || false;
|
||||
const overflowFiles = (this.lastValidationResult.overflowFiles || []).map(f => ({
|
||||
index: f.index,
|
||||
lineCount: f.lineCount,
|
||||
lineLimit: f.lineLimit
|
||||
}));
|
||||
|
||||
// Cache speichern
|
||||
this.lastOverflowFiles = overflowFiles;
|
||||
|
||||
return { success: true, hasOverflow, overflowFiles };
|
||||
}
|
||||
|
||||
return { success: true, hasOverflow: false, overflowFiles: [] };
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Error:', error);
|
||||
btn.disabled = false;
|
||||
btn.textContent = isEnvelope ? 'Umschlag Vorschau generieren' : 'Vorschau generieren';
|
||||
|
||||
// Fehlermeldung im Preview-Bereich anzeigen
|
||||
const previewBox = dom.querySelector('.sk-preview-box');
|
||||
if (previewBox) {
|
||||
previewBox.innerHTML = '';
|
||||
const notice = document.createElement('div');
|
||||
notice.style.cssText = 'padding: 20px; text-align: center; color: #666;';
|
||||
notice.innerHTML = `
|
||||
<p style="margin-bottom: 15px;">Die Vorschau konnte nicht generiert werden.</p>
|
||||
<p style="color: #999; font-size: 13px;">Bitte versuchen Sie es erneut oder kontaktieren Sie uns bei anhaltenden Problemen.</p>
|
||||
`;
|
||||
previewBox.appendChild(notice);
|
||||
}
|
||||
|
||||
return { success: false, hasOverflow: false, overflowFiles: [], error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Preview an einem bestimmten Index
|
||||
*/
|
||||
showPreview(index, dom) {
|
||||
const preview = this.currentBatchPreviews[index];
|
||||
if (!preview) {
|
||||
console.warn('[PreviewManager] Preview not loaded:', index);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewBox = dom.querySelector('.sk-preview-box');
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
|
||||
if (!previewBox) {
|
||||
console.warn('[PreviewManager] Preview box not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.style.cssText = 'position: relative; overflow: hidden; margin-top: 15px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = `${this.api.baseURL}${preview.url}?t=${Date.now()}`;
|
||||
img.style.cssText = 'width: 100%; display: block;';
|
||||
|
||||
imgContainer.addEventListener('click', () => {
|
||||
this.showFullscreenPreview(img.src);
|
||||
});
|
||||
|
||||
imgContainer.addEventListener('mouseenter', () => {
|
||||
imgContainer.style.opacity = '0.9';
|
||||
});
|
||||
|
||||
imgContainer.addEventListener('mouseleave', () => {
|
||||
imgContainer.style.opacity = '1';
|
||||
});
|
||||
|
||||
imgContainer.appendChild(img);
|
||||
previewBox.innerHTML = '';
|
||||
previewBox.appendChild(imgContainer);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Dokument ${index + 1} von ${this.previewCount}`;
|
||||
}
|
||||
|
||||
this.currentDocIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigiert zur nächsten/vorherigen Preview
|
||||
*/
|
||||
navigateWithinBatch(direction, dom) {
|
||||
const newIndex = this.currentDocIndex + direction;
|
||||
|
||||
if (newIndex < 0 || newIndex >= this.currentBatchPreviews.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPreview(newIndex, dom);
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
// navigateToBatch wurde entfernt - Batch-Loading wird nicht mehr verwendet
|
||||
// Alle Previews werden jetzt auf einmal geladen (loadAllPreviews)
|
||||
|
||||
/**
|
||||
* Ermittelt den aktuellen lokalen Index
|
||||
*/
|
||||
getCurrentLocalIndex(dom) {
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
if (!statusEl) return 0;
|
||||
|
||||
const match = statusEl.textContent.match(/Dokument (\d+) von/);
|
||||
if (match) {
|
||||
return parseInt(match[1]) - 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Request Counter im DOM
|
||||
*/
|
||||
updateRequestCounter(dom) {
|
||||
const requestCounterEl = dom.querySelector('.sk-preview-request-counter');
|
||||
if (requestCounterEl) {
|
||||
requestCounterEl.textContent = `Verbleibende Vorschau-Anfragen: ${this.requestsRemaining}/${this.maxRequests}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert Button-States der Navigation
|
||||
*/
|
||||
updateNavigationButtons() {
|
||||
const navWrapper = document.querySelector('.sk-preview-navigation-container');
|
||||
if (!navWrapper) return;
|
||||
|
||||
const navContainer = navWrapper.querySelector('.sk-preview-navigation');
|
||||
if (!navContainer) return;
|
||||
|
||||
const prevDocBtn = navContainer.querySelector('.sk-preview-prev-doc');
|
||||
const nextDocBtn = navContainer.querySelector('.sk-preview-next-doc');
|
||||
|
||||
if (prevDocBtn) {
|
||||
const canPrev = this.currentDocIndex > 0;
|
||||
prevDocBtn.disabled = !canPrev;
|
||||
prevDocBtn.style.opacity = canPrev ? '1' : '0.5';
|
||||
prevDocBtn.style.cursor = canPrev ? 'pointer' : 'not-allowed';
|
||||
}
|
||||
|
||||
if (nextDocBtn) {
|
||||
const canNext = this.currentDocIndex < this.currentBatchPreviews.length - 1;
|
||||
nextDocBtn.disabled = !canNext;
|
||||
nextDocBtn.style.opacity = canNext ? '1' : '0.5';
|
||||
nextDocBtn.style.cursor = canNext ? 'pointer' : 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Navigation Controls
|
||||
*/
|
||||
showNavigationControls(dom, state, isEnvelope) {
|
||||
const navWrapper = dom.querySelector('.sk-preview-navigation-container');
|
||||
if (!navWrapper) return;
|
||||
|
||||
let navContainer = navWrapper.querySelector('.sk-preview-navigation');
|
||||
if (navContainer) {
|
||||
navContainer.remove();
|
||||
}
|
||||
|
||||
navContainer = document.createElement('div');
|
||||
navContainer.className = 'sk-preview-navigation';
|
||||
navContainer.style.display = 'flex';
|
||||
navContainer.style.gap = '10px';
|
||||
navContainer.style.alignItems = 'center';
|
||||
|
||||
const buttonStyle = {
|
||||
padding: '8px 12px',
|
||||
fontSize: '20px',
|
||||
lineHeight: '1',
|
||||
minWidth: '40px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
|
||||
const prevDocBtn = document.createElement('button');
|
||||
prevDocBtn.type = 'button';
|
||||
prevDocBtn.className = 'sk-btn sk-btn-secondary sk-preview-prev-doc';
|
||||
prevDocBtn.textContent = '‹';
|
||||
prevDocBtn.title = 'Vorheriges Dokument';
|
||||
Object.assign(prevDocBtn.style, buttonStyle);
|
||||
prevDocBtn.onclick = () => {
|
||||
this.navigateWithinBatch(-1, dom);
|
||||
};
|
||||
|
||||
const nextDocBtn = document.createElement('button');
|
||||
nextDocBtn.type = 'button';
|
||||
nextDocBtn.className = 'sk-btn sk-btn-secondary sk-preview-next-doc';
|
||||
nextDocBtn.textContent = '›';
|
||||
nextDocBtn.title = 'Nächstes Dokument';
|
||||
Object.assign(nextDocBtn.style, buttonStyle);
|
||||
nextDocBtn.onclick = () => {
|
||||
this.navigateWithinBatch(1, dom);
|
||||
};
|
||||
|
||||
navContainer.appendChild(prevDocBtn);
|
||||
navContainer.appendChild(nextDocBtn);
|
||||
|
||||
navWrapper.appendChild(navContainer);
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard Event Handler
|
||||
*/
|
||||
handleKeyboardNavigation(event, state, dom, isEnvelope = false) {
|
||||
if (this.currentBatchPreviews.length === 0) return;
|
||||
|
||||
const currentLocalIndex = this.getCurrentLocalIndex(dom);
|
||||
|
||||
if (event.key === 'ArrowLeft' && currentLocalIndex > 0) {
|
||||
event.preventDefault();
|
||||
this.navigateWithinBatch(-1, dom);
|
||||
} else if (event.key === 'ArrowRight' && currentLocalIndex < this.currentBatchPreviews.length - 1) {
|
||||
event.preventDefault();
|
||||
this.navigateWithinBatch(1, dom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.currentBatchPreviews = [];
|
||||
this.currentBatchIndex = 0;
|
||||
this.previewCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset für neue Session
|
||||
*/
|
||||
reset() {
|
||||
this.currentBatchPreviews = [];
|
||||
this.currentBatchIndex = 0;
|
||||
this.previewCount = 0;
|
||||
this.requestsRemaining = this.maxRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Fullscreen-Vorschau als Modal
|
||||
*/
|
||||
showFullscreenPreview(imgSrc) {
|
||||
const existingModal = document.getElementById('sk-preview-fullscreen-modal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'sk-preview-fullscreen-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const modalImg = document.createElement('img');
|
||||
modalImg.src = imgSrc;
|
||||
modalImg.style.cssText = `
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
font-size: 40px;
|
||||
color: white;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.textContent = 'Klicken zum Schließen';
|
||||
hint.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
modal.appendChild(modalImg);
|
||||
modal.appendChild(closeBtn);
|
||||
modal.appendChild(hint);
|
||||
|
||||
const closeModal = () => modal.remove();
|
||||
modal.addEventListener('click', closeModal);
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
}, { once: true });
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanzen
|
||||
window.envelopePreviewManager = null;
|
||||
window.contentPreviewManager = null;
|
||||
|
||||
export default PreviewManager;
|
||||
1035
skrift-configurator/assets/js/configurator-pricing.js
Normal file
1035
skrift-configurator/assets/js/configurator-pricing.js
Normal file
File diff suppressed because it is too large
Load Diff
1030
skrift-configurator/assets/js/configurator-state.js
Normal file
1030
skrift-configurator/assets/js/configurator-state.js
Normal file
File diff suppressed because it is too large
Load Diff
5446
skrift-configurator/assets/js/configurator-ui.js
Normal file
5446
skrift-configurator/assets/js/configurator-ui.js
Normal file
File diff suppressed because it is too large
Load Diff
96
skrift-configurator/assets/js/configurator-utils.js
Normal file
96
skrift-configurator/assets/js/configurator-utils.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Gemeinsame Utility-Funktionen für Skrift Konfigurator
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bereitet Platzhalter für einen bestimmten Index vor
|
||||
* Wird von PreviewManager und Backend-Integration verwendet
|
||||
*/
|
||||
export function preparePlaceholdersForIndex(state, index) {
|
||||
const placeholders = {};
|
||||
|
||||
// Prüfen ob strukturierte Empfängerdaten (klassische Adresse) im aktuellen Flow verwendet werden:
|
||||
// - Adressmodus muss 'classic' sein (bei 'free' gibt es keine Felder wie vorname, name etc.)
|
||||
// - UND: Direktversand ODER Kuvert mit Empfängeradresse
|
||||
const isClassicAddress = (state.addressMode || 'classic') === 'classic';
|
||||
const needsRecipientData = isClassicAddress && (
|
||||
state.answers?.shippingMode === 'direct' ||
|
||||
(state.answers?.envelope === true && state.answers?.envelopeMode === 'recipientData')
|
||||
);
|
||||
|
||||
// Empfänger-Feldnamen, die aus recipientRows kommen können
|
||||
const recipientFields = ['vorname', 'name', 'ort', 'strasse', 'hausnummer', 'plz', 'land'];
|
||||
|
||||
// Platzhalter aus placeholderValues extrahieren
|
||||
if (state.placeholderValues) {
|
||||
for (const [name, values] of Object.entries(state.placeholderValues)) {
|
||||
// Empfängerfelder nur überspringen wenn sie aus recipientRows kommen
|
||||
if (needsRecipientData && recipientFields.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(values) && values.length > index) {
|
||||
placeholders[name] = values[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empfängerdaten aus recipientRows hinzufügen (nur wenn benötigt)
|
||||
if (needsRecipientData && Array.isArray(state.recipientRows) && state.recipientRows.length > index) {
|
||||
const recipient = state.recipientRows[index];
|
||||
placeholders['vorname'] = recipient.firstName || '';
|
||||
placeholders['name'] = recipient.lastName || '';
|
||||
placeholders['ort'] = recipient.city || '';
|
||||
placeholders['strasse'] = recipient.street || '';
|
||||
placeholders['hausnummer'] = recipient.houseNumber || '';
|
||||
placeholders['plz'] = recipient.zip || '';
|
||||
placeholders['land'] = recipient.country || '';
|
||||
}
|
||||
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Empfängerzeilen (gemeinsame Logik für klassische und freie Adressen)
|
||||
*/
|
||||
export function validateRecipientRows(state, requiredCount) {
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
|
||||
if (addressMode === 'free') {
|
||||
// Freie Adresse: mindestens Zeile 1 muss ausgefüllt sein
|
||||
const rows = state.freeAddressRows || [];
|
||||
if (rows.length !== requiredCount) return false;
|
||||
|
||||
for (const r of rows) {
|
||||
if (!r) return false;
|
||||
if (!String(r.line1 || "").trim()) return false;
|
||||
}
|
||||
} else {
|
||||
// Klassische Adresse
|
||||
const rows = state.recipientRows || [];
|
||||
if (rows.length !== requiredCount) return false;
|
||||
|
||||
const required = [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"street",
|
||||
"houseNumber",
|
||||
"zip",
|
||||
"city",
|
||||
"country",
|
||||
];
|
||||
|
||||
for (const r of rows) {
|
||||
if (!r) return false;
|
||||
for (const k of required) {
|
||||
if (!String(r[k] || "").trim()) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
preparePlaceholdersForIndex,
|
||||
validateRecipientRows,
|
||||
};
|
||||
1145
skrift-configurator/assets/js/price-calculator.js
Normal file
1145
skrift-configurator/assets/js/price-calculator.js
Normal file
File diff suppressed because it is too large
Load Diff
110
skrift-configurator/check-db.php
Normal file
110
skrift-configurator/check-db.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* Schnelle Datenbank-Prüfung
|
||||
* Aufruf: /wp-content/plugins/skrift-konfigurator/check-db.php
|
||||
*/
|
||||
|
||||
// WordPress laden
|
||||
require_once('../../../wp-load.php');
|
||||
|
||||
// Sicherheit
|
||||
if (!current_user_can('manage_options')) {
|
||||
die('Keine Berechtigung');
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutschein DB Check</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; background: #f5f5f5; }
|
||||
.box { background: white; padding: 20px; margin: 10px 0; border: 2px solid #333; }
|
||||
.error { background: #ffebee; border-color: #c62828; }
|
||||
.success { background: #e8f5e9; border-color: #2e7d32; }
|
||||
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
|
||||
h2 { margin-top: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔍 Gutschein Datenbank Check</h1>
|
||||
|
||||
<?php
|
||||
// 1. Direkt aus DB
|
||||
$vouchers = get_option('skrift_konfigurator_vouchers', []);
|
||||
$count = count($vouchers);
|
||||
?>
|
||||
|
||||
<div class="box <?php echo $count > 0 ? 'success' : 'error'; ?>">
|
||||
<h2>1. Datenbank Status</h2>
|
||||
<p><strong>Anzahl Gutscheine:</strong> <?php echo $count; ?></p>
|
||||
<?php if ($count === 0): ?>
|
||||
<p style="color: #c62828;">⚠️ <strong>KEINE GUTSCHEINE IN DER DATENBANK!</strong></p>
|
||||
<?php else: ?>
|
||||
<p style="color: #2e7d32;">✅ Gutscheine gefunden!</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>2. Rohe Daten (PHP)</h2>
|
||||
<pre><?php var_dump($vouchers); ?></pre>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>3. JSON Encoding (wie im Frontend)</h2>
|
||||
<pre><?php echo wp_json_encode($vouchers, JSON_PRETTY_PRINT); ?></pre>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>4. JavaScript Test</h2>
|
||||
<script>
|
||||
const vouchers = <?php echo wp_json_encode($vouchers); ?>;
|
||||
console.log('Vouchers:', vouchers);
|
||||
console.log('Type:', Array.isArray(vouchers) ? 'ARRAY ❌' : 'OBJECT ✅');
|
||||
console.log('Keys:', Object.keys(vouchers));
|
||||
console.log('Count:', Object.keys(vouchers).length);
|
||||
</script>
|
||||
<p>Öffnen Sie die Browser Console (F12) für JavaScript-Output</p>
|
||||
</div>
|
||||
|
||||
<?php if ($count === 0): ?>
|
||||
<div class="box error">
|
||||
<h2>⚠️ Lösung: Gutscheine erstellen</h2>
|
||||
<p>Es sind keine Gutscheine in der Datenbank. Bitte:</p>
|
||||
<ol>
|
||||
<li>Gehen Sie zu: <a href="<?php echo admin_url('options-general.php?page=skrift-vouchers'); ?>">Gutschein-Verwaltung</a></li>
|
||||
<li>Erstellen Sie einen Test-Gutschein (z.B. Code: TEST10, Typ: Prozent, Wert: 10)</li>
|
||||
<li>Oder führen Sie <code>create-test-voucher.php</code> aus</li>
|
||||
</ol>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box">
|
||||
<h2>5. Schnell-Fix: Test-Gutschein erstellen</h2>
|
||||
<form method="post">
|
||||
<button type="submit" name="create_test" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
|
||||
🚀 Test-Gutschein "TEST10" jetzt erstellen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
if (isset($_POST['create_test'])) {
|
||||
$vouchers['TEST10'] = [
|
||||
'code' => 'TEST10',
|
||||
'type' => 'percent',
|
||||
'value' => 10,
|
||||
'expiry_date' => '',
|
||||
'usage_limit' => 0,
|
||||
'usage_count' => 0,
|
||||
];
|
||||
update_option('skrift_konfigurator_vouchers', $vouchers);
|
||||
echo '<p style="color: #2e7d32; font-weight: bold;">✅ Gutschein TEST10 wurde erstellt! Seite neu laden...</p>';
|
||||
echo '<script>setTimeout(() => location.reload(), 1500);</script>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
61
skrift-configurator/create-test-voucher.php
Normal file
61
skrift-configurator/create-test-voucher.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* EINMALIG AUSFÜHREN: Test-Gutschein erstellen
|
||||
*
|
||||
* Anleitung:
|
||||
* 1. Diese Datei in den Plugin-Ordner legen
|
||||
* 2. Im Browser aufrufen: https://ihre-domain.de/wp-content/plugins/skrift-konfigurator/create-test-voucher.php
|
||||
* 3. Nach erfolgreicher Ausführung diese Datei LÖSCHEN
|
||||
*/
|
||||
|
||||
// WordPress laden
|
||||
require_once('../../../wp-load.php');
|
||||
|
||||
// Sicherheit: Nur für Admins
|
||||
if (!current_user_can('manage_options')) {
|
||||
die('Keine Berechtigung');
|
||||
}
|
||||
|
||||
// Test-Gutscheine erstellen
|
||||
$vouchers = get_option('skrift_konfigurator_vouchers', []);
|
||||
|
||||
// Test-Gutschein 1: 10% Rabatt
|
||||
$vouchers['TEST10'] = [
|
||||
'code' => 'TEST10',
|
||||
'type' => 'percent',
|
||||
'value' => 10,
|
||||
'expiry_date' => '', // Unbegrenzt
|
||||
'usage_limit' => 0, // Unbegrenzt
|
||||
'usage_count' => 0,
|
||||
];
|
||||
|
||||
// Test-Gutschein 2: 5€ Rabatt
|
||||
$vouchers['SAVE5'] = [
|
||||
'code' => 'SAVE5',
|
||||
'type' => 'fixed',
|
||||
'value' => 5.00,
|
||||
'expiry_date' => '', // Unbegrenzt
|
||||
'usage_limit' => 0, // Unbegrenzt
|
||||
'usage_count' => 0,
|
||||
];
|
||||
|
||||
// Test-Gutschein 3: 20% Rabatt mit Limit
|
||||
$vouchers['WELCOME20'] = [
|
||||
'code' => 'WELCOME20',
|
||||
'type' => 'percent',
|
||||
'value' => 20,
|
||||
'expiry_date' => date('Y-m-d', strtotime('+30 days')), // 30 Tage gültig
|
||||
'usage_limit' => 10, // Max 10x einlösbar
|
||||
'usage_count' => 0,
|
||||
];
|
||||
|
||||
update_option('skrift_konfigurator_vouchers', $vouchers);
|
||||
|
||||
echo '<h1>✓ Test-Gutscheine erfolgreich erstellt!</h1>';
|
||||
echo '<ul>';
|
||||
echo '<li><strong>TEST10</strong> - 10% Rabatt (unbegrenzt)</li>';
|
||||
echo '<li><strong>SAVE5</strong> - 5,00€ Rabatt (unbegrenzt)</li>';
|
||||
echo '<li><strong>WELCOME20</strong> - 20% Rabatt (30 Tage gültig, max. 10x)</li>';
|
||||
echo '</ul>';
|
||||
echo '<p><a href="' . admin_url('options-general.php?page=skrift-vouchers') . '">→ Gutscheine im Backend anzeigen</a></p>';
|
||||
echo '<p style="color: red;"><strong>WICHTIG: Bitte löschen Sie diese Datei jetzt aus Sicherheitsgründen!</strong></p>';
|
||||
72
skrift-configurator/debug-vouchers.php
Normal file
72
skrift-configurator/debug-vouchers.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* DEBUG: Gutscheine prüfen
|
||||
* Aufruf: /wp-content/plugins/skrift-konfigurator/debug-vouchers.php
|
||||
*/
|
||||
|
||||
// WordPress laden
|
||||
require_once('../../../wp-load.php');
|
||||
|
||||
// Sicherheit: Nur für Admins
|
||||
if (!current_user_can('manage_options')) {
|
||||
die('Keine Berechtigung');
|
||||
}
|
||||
|
||||
echo '<h1>Gutschein Debug</h1>';
|
||||
echo '<style>pre { background: #f5f5f5; padding: 15px; border: 1px solid #ddd; }</style>';
|
||||
|
||||
// 1. Direkt aus Datenbank lesen
|
||||
$vouchers_db = get_option('skrift_konfigurator_vouchers', []);
|
||||
echo '<h2>1. Direkt aus Datenbank (get_option)</h2>';
|
||||
echo '<pre>';
|
||||
print_r($vouchers_db);
|
||||
echo '</pre>';
|
||||
|
||||
// 2. Über die Klasse
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/admin-vouchers.php';
|
||||
$vouchers_class = Skrift_Konfigurator_Vouchers::get_vouchers();
|
||||
echo '<h2>2. Über Klassen-Methode get_vouchers()</h2>';
|
||||
echo '<pre>';
|
||||
print_r($vouchers_class);
|
||||
echo '</pre>';
|
||||
|
||||
// 3. JSON-Encoding prüfen (wie wp_localize_script es macht)
|
||||
$vouchers_json = json_encode($vouchers_class);
|
||||
echo '<h2>3. JSON-Encoded (wie wp_localize_script)</h2>';
|
||||
echo '<pre>';
|
||||
echo htmlspecialchars($vouchers_json);
|
||||
echo '</pre>';
|
||||
|
||||
// 4. Zurück decodiert
|
||||
$vouchers_decoded = json_decode($vouchers_json, true);
|
||||
echo '<h2>4. JSON wieder decodiert</h2>';
|
||||
echo '<pre>';
|
||||
print_r($vouchers_decoded);
|
||||
echo '</pre>';
|
||||
|
||||
// 5. Test: Ist es ein assoziatives Array oder Objekt?
|
||||
echo '<h2>5. Datentyp-Analyse</h2>';
|
||||
echo '<pre>';
|
||||
echo 'is_array: ' . (is_array($vouchers_class) ? 'JA' : 'NEIN') . "\n";
|
||||
echo 'count: ' . count($vouchers_class) . "\n";
|
||||
echo 'empty: ' . (empty($vouchers_class) ? 'JA' : 'NEIN') . "\n";
|
||||
echo 'Keys: ' . print_r(array_keys($vouchers_class), true) . "\n";
|
||||
echo '</pre>';
|
||||
|
||||
// 6. Simuliere wp_localize_script
|
||||
echo '<h2>6. Simuliertes wp_localize_script Output</h2>';
|
||||
echo '<script>';
|
||||
echo "\n";
|
||||
echo 'var SkriftConfigurator = {';
|
||||
echo "\n";
|
||||
echo ' "version": "0.3.0",';
|
||||
echo "\n";
|
||||
echo ' "vouchers": ' . json_encode($vouchers_class);
|
||||
echo "\n";
|
||||
echo '};';
|
||||
echo "\n";
|
||||
echo 'console.log("Vouchers from simulated wp_localize_script:", SkriftConfigurator.vouchers);';
|
||||
echo "\n";
|
||||
echo '</script>';
|
||||
|
||||
echo '<p><strong>Öffnen Sie die Browser-Console, um das simulierte Output zu sehen!</strong></p>';
|
||||
186
skrift-configurator/includes/admin-orders.php
Normal file
186
skrift-configurator/includes/admin-orders.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Bestellnummer-Verwaltung für Skrift Konfigurator
|
||||
* Generiert fortlaufende Bestellnummern im Format: S-JAHR-MONAT-TAG-XXX
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
|
||||
final class Skrift_Konfigurator_Orders {
|
||||
|
||||
const COUNTER_OPTION_KEY = 'skrift_konfigurator_order_counter';
|
||||
const ORDERS_OPTION_KEY = 'skrift_konfigurator_orders';
|
||||
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', [$this, 'register_rest_routes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Routen registrieren
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
// Neue Bestellnummer generieren
|
||||
register_rest_route('skrift/v1', '/order/generate-number', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_generate_order_number'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
// Bestellung registrieren (nach erfolgreicher Zahlung)
|
||||
register_rest_route('skrift/v1', '/order/register', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_register_order'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die nächste fortlaufende Bestellnummer
|
||||
* Format: S-YYYY-MM-DD-XXX (z.B. S-2026-01-12-001)
|
||||
* Verwendet Transient-Lock um Race Conditions zu vermeiden
|
||||
*/
|
||||
public static function generate_order_number() {
|
||||
$lock_key = 'skrift_order_number_lock';
|
||||
$max_attempts = 10;
|
||||
$attempt = 0;
|
||||
|
||||
// Versuche Lock zu bekommen (einfaches Locking mit Transients)
|
||||
while ($attempt < $max_attempts) {
|
||||
// Prüfe ob Lock existiert
|
||||
if (get_transient($lock_key) === false) {
|
||||
// Setze Lock (5 Sekunden Timeout)
|
||||
set_transient($lock_key, time(), 5);
|
||||
|
||||
// Aktuellen Counter holen
|
||||
$counter_data = get_option(self::COUNTER_OPTION_KEY, [
|
||||
'date' => '',
|
||||
'counter' => 0
|
||||
]);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
|
||||
// Counter zurücksetzen wenn neuer Tag
|
||||
if ($counter_data['date'] !== $today) {
|
||||
$counter_data = [
|
||||
'date' => $today,
|
||||
'counter' => 0
|
||||
];
|
||||
}
|
||||
|
||||
// Counter erhöhen
|
||||
$counter_data['counter']++;
|
||||
|
||||
// Speichern
|
||||
update_option(self::COUNTER_OPTION_KEY, $counter_data);
|
||||
|
||||
// Lock freigeben
|
||||
delete_transient($lock_key);
|
||||
|
||||
// Format: S-YYYY-MM-DD-XXX
|
||||
$year = date('Y');
|
||||
$month = date('m');
|
||||
$day = date('d');
|
||||
$number = str_pad($counter_data['counter'], 3, '0', STR_PAD_LEFT);
|
||||
|
||||
return "S-{$year}-{$month}-{$day}-{$number}";
|
||||
}
|
||||
|
||||
// Warte kurz und versuche erneut
|
||||
usleep(100000); // 100ms
|
||||
$attempt++;
|
||||
}
|
||||
|
||||
// Fallback wenn Lock nicht bekommen werden konnte
|
||||
// Verwende Microtime für Eindeutigkeit
|
||||
$year = date('Y');
|
||||
$month = date('m');
|
||||
$day = date('d');
|
||||
$micro = substr(str_replace('.', '', microtime(true)), -6);
|
||||
|
||||
return "S-{$year}-{$month}-{$day}-{$micro}";
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Endpoint: Neue Bestellnummer generieren
|
||||
*/
|
||||
public function rest_generate_order_number($request) {
|
||||
$order_number = self::generate_order_number();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'orderNumber' => $order_number
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Endpoint: Bestellung registrieren
|
||||
*/
|
||||
public function rest_register_order($request) {
|
||||
$order_number = $request->get_param('orderNumber');
|
||||
$customer = $request->get_param('customer');
|
||||
$quote = $request->get_param('quote');
|
||||
$payment_method = $request->get_param('paymentMethod');
|
||||
$payment_id = $request->get_param('paymentId');
|
||||
|
||||
if (empty($order_number)) {
|
||||
return new WP_Error('missing_order_number', 'Bestellnummer fehlt', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Bestellung speichern
|
||||
$orders = get_option(self::ORDERS_OPTION_KEY, []);
|
||||
|
||||
$orders[$order_number] = [
|
||||
'orderNumber' => $order_number,
|
||||
'customer' => $customer,
|
||||
'quote' => $quote,
|
||||
'paymentMethod' => $payment_method,
|
||||
'paymentId' => $payment_id,
|
||||
'createdAt' => current_time('mysql'),
|
||||
'status' => 'pending'
|
||||
];
|
||||
|
||||
update_option(self::ORDERS_OPTION_KEY, $orders);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'orderNumber' => $order_number,
|
||||
'message' => 'Bestellung erfolgreich registriert'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestellung als bezahlt markieren
|
||||
*/
|
||||
public static function mark_order_paid($order_number, $payment_id = null) {
|
||||
$orders = get_option(self::ORDERS_OPTION_KEY, []);
|
||||
|
||||
if (isset($orders[$order_number])) {
|
||||
$orders[$order_number]['status'] = 'paid';
|
||||
$orders[$order_number]['paidAt'] = current_time('mysql');
|
||||
if ($payment_id) {
|
||||
$orders[$order_number]['paymentId'] = $payment_id;
|
||||
}
|
||||
update_option(self::ORDERS_OPTION_KEY, $orders);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Bestellungen abrufen
|
||||
*/
|
||||
public static function get_orders() {
|
||||
return get_option(self::ORDERS_OPTION_KEY, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Bestellung abrufen
|
||||
*/
|
||||
public static function get_order($order_number) {
|
||||
$orders = self::get_orders();
|
||||
return $orders[$order_number] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
new Skrift_Konfigurator_Orders();
|
||||
943
skrift-configurator/includes/admin-settings.php
Normal file
943
skrift-configurator/includes/admin-settings.php
Normal file
@@ -0,0 +1,943 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Settings für Skrift Konfigurator
|
||||
* Verwaltung von Produkten, Preisen und Beschreibungen
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
|
||||
class Skrift_Konfigurator_Admin_Settings {
|
||||
|
||||
const OPTION_KEY = 'skrift_konfigurator_settings';
|
||||
|
||||
public function __construct() {
|
||||
add_action('admin_menu', [$this, 'add_admin_menu']);
|
||||
add_action('admin_init', [$this, 'register_settings']);
|
||||
}
|
||||
|
||||
public function add_admin_menu(): void {
|
||||
add_options_page(
|
||||
'Skrift Konfigurator Einstellungen',
|
||||
'Skrift Konfigurator',
|
||||
'manage_options',
|
||||
'skrift-konfigurator',
|
||||
[$this, 'render_settings_page']
|
||||
);
|
||||
}
|
||||
|
||||
public function register_settings(): void {
|
||||
register_setting('skrift_konfigurator', self::OPTION_KEY, [
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => [$this, 'sanitize_settings'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function sanitize_settings($input) {
|
||||
$sanitized = [];
|
||||
|
||||
// Produkte sanitieren
|
||||
if (isset($input['products']) && is_array($input['products'])) {
|
||||
$sanitized['products'] = [];
|
||||
foreach ($input['products'] as $key => $product) {
|
||||
$sanitized['products'][sanitize_key($key)] = [
|
||||
'label' => sanitize_text_field($product['label'] ?? ''),
|
||||
'description' => sanitize_textarea_field($product['description'] ?? ''),
|
||||
'base_price' => floatval($product['base_price'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Preise sanitieren
|
||||
if (isset($input['prices']) && is_array($input['prices'])) {
|
||||
$sanitized['prices'] = [];
|
||||
foreach ($input['prices'] as $key => $value) {
|
||||
$sanitized['prices'][sanitize_key($key)] = floatval($value);
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamische Preisformeln sanitieren
|
||||
if (isset($input['dynamic_pricing'])) {
|
||||
$sanitized['dynamic_pricing'] = [
|
||||
'business_formula' => sanitize_textarea_field($input['dynamic_pricing']['business_formula'] ?? ''),
|
||||
'private_formula' => sanitize_textarea_field($input['dynamic_pricing']['private_formula'] ?? ''),
|
||||
'business_min_quantity' => intval($input['dynamic_pricing']['business_min_quantity'] ?? 0),
|
||||
'private_min_quantity' => intval($input['dynamic_pricing']['private_min_quantity'] ?? 0),
|
||||
'business_normal_quantity' => intval($input['dynamic_pricing']['business_normal_quantity'] ?? 0),
|
||||
'private_normal_quantity' => intval($input['dynamic_pricing']['private_normal_quantity'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
// Backend-Verbindung sanitieren
|
||||
if (isset($input['backend_connection'])) {
|
||||
$sanitized['backend_connection'] = [
|
||||
'api_url' => esc_url_raw($input['backend_connection']['api_url'] ?? ''),
|
||||
'api_token' => sanitize_text_field($input['backend_connection']['api_token'] ?? ''),
|
||||
'webhook_url_business' => esc_url_raw($input['backend_connection']['webhook_url_business'] ?? ''),
|
||||
'webhook_url_private' => esc_url_raw($input['backend_connection']['webhook_url_private'] ?? ''),
|
||||
'redirect_url_business' => esc_url_raw($input['backend_connection']['redirect_url_business'] ?? ''),
|
||||
'redirect_url_private' => esc_url_raw($input['backend_connection']['redirect_url_private'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
// REST API Key sanitieren
|
||||
if (isset($input['api_security'])) {
|
||||
$sanitized['api_security'] = [
|
||||
'api_key' => sanitize_text_field($input['api_security']['api_key'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
// PayPal-Verbindung sanitieren
|
||||
if (isset($input['paypal'])) {
|
||||
$sanitized['paypal'] = [
|
||||
'enabled' => !empty($input['paypal']['enabled']),
|
||||
'mode' => sanitize_text_field($input['paypal']['mode'] ?? 'sandbox'),
|
||||
'client_id_sandbox' => sanitize_text_field($input['paypal']['client_id_sandbox'] ?? ''),
|
||||
'client_secret_sandbox' => sanitize_text_field($input['paypal']['client_secret_sandbox'] ?? ''),
|
||||
'client_id_live' => sanitize_text_field($input['paypal']['client_id_live'] ?? ''),
|
||||
'client_secret_live' => sanitize_text_field($input['paypal']['client_secret_live'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
// Schriftmuster und Platzhalter-Hilfe sanitieren
|
||||
if (isset($input['font_sample'])) {
|
||||
$sanitized['font_sample'] = [
|
||||
'url' => esc_url_raw($input['font_sample']['url'] ?? ''),
|
||||
'placeholder_help_url' => esc_url_raw($input['font_sample']['placeholder_help_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
public function render_settings_page(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
?>
|
||||
<style>
|
||||
.sk-admin-wrap { max-width: 1200px; }
|
||||
.sk-admin-section { background: #fff; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4; box-shadow: 0 1px 1px rgba(0,0,0,.04); }
|
||||
.sk-admin-section h2 { margin-top: 0; padding-bottom: 10px; border-bottom: 1px solid #eee; }
|
||||
.sk-product-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-top: 20px; }
|
||||
.sk-product-card { background: #f9f9f9; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
.sk-product-card h3 { margin-top: 0; color: #0073aa; font-size: 16px; }
|
||||
.sk-field-row { margin-bottom: 15px; }
|
||||
.sk-field-row label { display: block; font-weight: 600; margin-bottom: 5px; }
|
||||
.sk-field-row input[type="text"], .sk-field-row textarea { width: 100%; }
|
||||
.sk-field-row input[type="number"] { width: 120px; }
|
||||
.sk-price-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; margin-top: 15px; }
|
||||
.sk-price-item { background: #f9f9f9; padding: 12px; border-left: 3px solid #0073aa; }
|
||||
.sk-price-item label { display: flex; justify-content: space-between; align-items: center; }
|
||||
.sk-price-item strong { color: #23282d; }
|
||||
.sk-multiplier-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
.sk-multiplier-table th, .sk-multiplier-table td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
.sk-multiplier-table th { background: #f0f0f1; font-weight: 600; }
|
||||
.sk-multiplier-table input[type="number"] { width: 100px; }
|
||||
</style>
|
||||
<div class="wrap sk-admin-wrap">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields('skrift_konfigurator');
|
||||
do_settings_sections('skrift_konfigurator');
|
||||
?>
|
||||
|
||||
<!-- Produkte -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>📦 Produkte</h2>
|
||||
<p>Verwalten Sie Namen, Beschreibungen und Startpreise für alle Produkte.</p>
|
||||
|
||||
<div class="sk-product-grid">
|
||||
<?php $this->render_product_card('businessbriefe', 'Businessbriefe', $settings); ?>
|
||||
<?php $this->render_product_card('business-postkarten', 'Business Postkarten', $settings); ?>
|
||||
<?php $this->render_product_card('follow-ups', 'Follow-ups', $settings); ?>
|
||||
<?php $this->render_product_card('einladungen', 'Einladungen', $settings); ?>
|
||||
<?php $this->render_product_card('private-briefe', 'Private Briefe', $settings); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Format Aufpreise -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>📐 Format Aufpreise</h2>
|
||||
<p>Aufpreise wenn bei bestimmten Produkten das Format gewechselt wird.</p>
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>A4 Aufpreis (Follow-ups/Einladungen)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][a4_upgrade_surcharge]"
|
||||
value="<?php echo esc_attr($settings['prices']['a4_upgrade_surcharge'] ?? '0.50'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Aufpreis pro Stück wenn bei Postkarten oder Einladungen auf A4 gewechselt wird</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Versand & Umschlag -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>🚚 Versand & Umschlag</h2>
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Porto Inland / Deutschland (pro Stück)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][shipping_domestic]"
|
||||
value="<?php echo esc_attr($settings['prices']['shipping_domestic'] ?? '0.95'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Portokosten für Versand innerhalb Deutschlands (0% MwSt.)</small>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Porto Ausland (pro Stück)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][shipping_international]"
|
||||
value="<?php echo esc_attr($settings['prices']['shipping_international'] ?? '1.25'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Portokosten für Auslandsversand (0% MwSt.)</small>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Serviceaufschlag Versand (pro Stück)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][shipping_service]"
|
||||
value="<?php echo esc_attr($settings['prices']['shipping_service'] ?? '0.95'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Service-Aufschlag für Direktversand (19% MwSt.)</small>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Bulkversand (einmalig)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][shipping_bulk]"
|
||||
value="<?php echo esc_attr($settings['prices']['shipping_bulk'] ?? '4.95'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Kuvert (Grundpreis pro Stück)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][envelope_base]"
|
||||
value="<?php echo esc_attr($settings['prices']['envelope_base'] ?? '0.50'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Grundpreis für Kuvert</small>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Aufschlag Beschriftung (pro Stück)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][envelope_labeling]"
|
||||
value="<?php echo esc_attr($settings['prices']['envelope_labeling'] ?? '0.50'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Aufschlag für Beschriftung des Umschlags (Empfängeradresse oder individueller Text)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zusatzleistungen -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>✨ Zusatzleistungen</h2>
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Motiv Upload <span style="color: #0073aa; font-size: 12px;">(einmalig)</span></strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][motif_upload]"
|
||||
value="<?php echo esc_attr($settings['prices']['motif_upload'] ?? '0.30'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Bedruckte Karten zusenden (pro Stück)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][motif_printed]"
|
||||
value="<?php echo esc_attr($settings['prices']['motif_printed'] ?? '0.00'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Designservice <span style="color: #0073aa; font-size: 12px;">(einmalig)</span></strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][motif_design]"
|
||||
value="<?php echo esc_attr($settings['prices']['motif_design'] ?? '0.00'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Textservice <span style="color: #0073aa; font-size: 12px;">(einmalig)</span></strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][textservice]"
|
||||
value="<?php echo esc_attr($settings['prices']['textservice'] ?? '0.00'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>API-Anbindung <span style="color: #0073aa; font-size: 12px;">(einmalig)</span></strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][api_connection]"
|
||||
value="<?php echo esc_attr($settings['prices']['api_connection'] ?? '250.00'); ?>"> €
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Einmalige Einrichtungsgebühr für die API-Anbindung</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Follow-ups Mengenstaffel -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>📊 Follow-ups Preis-Multiplikatoren</h2>
|
||||
<p>Die Gesamtkosten pro Schriftstück werden mit diesen Multiplikatoren je nach Menge multipliziert.</p>
|
||||
|
||||
<table class="sk-multiplier-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menge</th>
|
||||
<th>Multiplikator</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>5 - 49 Stück</strong></td>
|
||||
<td>
|
||||
<input type="number" step="0.1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][followup_mult_5_49]"
|
||||
value="<?php echo esc_attr($settings['prices']['followup_mult_5_49'] ?? '2.0'); ?>">
|
||||
<small style="margin-left: 10px; color: #666;">×</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>50 - 199 Stück</strong></td>
|
||||
<td>
|
||||
<input type="number" step="0.1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][followup_mult_50_199]"
|
||||
value="<?php echo esc_attr($settings['prices']['followup_mult_50_199'] ?? '1.7'); ?>">
|
||||
<small style="margin-left: 10px; color: #666;">×</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>200 - 499 Stück</strong></td>
|
||||
<td>
|
||||
<input type="number" step="0.1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][followup_mult_200_499]"
|
||||
value="<?php echo esc_attr($settings['prices']['followup_mult_200_499'] ?? '1.4'); ?>">
|
||||
<small style="margin-left: 10px; color: #666;">×</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>500 - 999 Stück</strong></td>
|
||||
<td>
|
||||
<input type="number" step="0.1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][followup_mult_500_999]"
|
||||
value="<?php echo esc_attr($settings['prices']['followup_mult_500_999'] ?? '1.2'); ?>">
|
||||
<small style="margin-left: 10px; color: #666;">×</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>1000+ Stück</strong></td>
|
||||
<td>
|
||||
<input type="number" step="0.1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][followup_mult_1000_plus]"
|
||||
value="<?php echo esc_attr($settings['prices']['followup_mult_1000_plus'] ?? '1.0'); ?>">
|
||||
<small style="margin-left: 10px; color: #666;">×</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Steuern -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>📋 Steuern</h2>
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Mehrwertsteuersatz (%)</strong>
|
||||
<span>
|
||||
<input type="number" step="0.01" min="0" max="100"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[prices][tax_rate]"
|
||||
value="<?php echo esc_attr($settings['prices']['tax_rate'] ?? '19'); ?>"> %
|
||||
</span>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Gilt für alle Positionen inkl. Versand</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamische Preisberechnung -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>🧮 Dynamische Preisberechnung</h2>
|
||||
<p>Konfigurieren Sie die dynamische Preisberechnung basierend auf Mengen. Die Formeln unterstützen Platzhalter wie <code>%qty%</code> (aktuelle Menge), <code>%norm_b%</code> (Normalpreis Menge Business), <code>%mind_b%</code> (Mind. Menge Business), <code>%norm_p%</code> (Normalpreis Menge Privat), <code>%mind_p%</code> (Mind. Menge Privat).</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 20px;">
|
||||
|
||||
<!-- Business -->
|
||||
<div style="background: #f9f9f9; padding: 20px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<h3 style="margin-top: 0; color: #0073aa;">Business</h3>
|
||||
|
||||
<div class="sk-field-row">
|
||||
<label><strong>Mindestmenge Business</strong></label>
|
||||
<input type="number" step="1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[dynamic_pricing][business_min_quantity]"
|
||||
value="<?php echo esc_attr($settings['dynamic_pricing']['business_min_quantity'] ?? '50'); ?>"
|
||||
style="width: 150px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Minimale Menge für Business-Bestellungen (außer Follow-ups)</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-field-row" style="margin-top: 15px;">
|
||||
<label><strong>Normalpreis Menge Business</strong></label>
|
||||
<input type="number" step="1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[dynamic_pricing][business_normal_quantity]"
|
||||
value="<?php echo esc_attr($settings['dynamic_pricing']['business_normal_quantity'] ?? '200'); ?>"
|
||||
style="width: 150px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Ab dieser Menge gilt der Normalpreis (Multiplikator = 1)</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-field-row" style="margin-top: 15px;">
|
||||
<label><strong>Dynamische Formel Business</strong></label>
|
||||
<textarea
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[dynamic_pricing][business_formula]"
|
||||
rows="8"
|
||||
style="width: 100%; font-family: monospace; font-size: 12px;"
|
||||
placeholder="(%qty% >= %norm_b%) ? 1 : (2 - Math.sqrt(%qty% / %norm_b%))"><?php echo esc_textarea($settings['dynamic_pricing']['business_formula'] ?? ''); ?></textarea>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">
|
||||
<strong>JavaScript-Formel zur Berechnung des Preis-Multiplikators.</strong><br>
|
||||
Platzhalter verwenden: <code>%qty%</code> für aktuelle Menge, <code>%norm_b%</code> für Normalpreis-Menge Business, <code>%mind_b%</code> für Mindestmenge Business.<br>
|
||||
Beispiel: <code>(%qty% >= %norm_b%) ? 1 : (2 - Math.sqrt(%qty% / %norm_b%))</code><br>
|
||||
Dies bedeutet: Wenn die Menge >= Normalpreis-Menge ist, dann Multiplikator = 1, sonst dynamische Berechnung.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privat -->
|
||||
<div style="background: #f9f9f9; padding: 20px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<h3 style="margin-top: 0; color: #0073aa;">Privat</h3>
|
||||
|
||||
<div class="sk-field-row">
|
||||
<label><strong>Mindestmenge Privat</strong></label>
|
||||
<input type="number" step="1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[dynamic_pricing][private_min_quantity]"
|
||||
value="<?php echo esc_attr($settings['dynamic_pricing']['private_min_quantity'] ?? '10'); ?>"
|
||||
style="width: 150px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Minimale Menge für Private Bestellungen</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-field-row" style="margin-top: 15px;">
|
||||
<label><strong>Normalpreis Menge Privat</strong></label>
|
||||
<input type="number" step="1" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[dynamic_pricing][private_normal_quantity]"
|
||||
value="<?php echo esc_attr($settings['dynamic_pricing']['private_normal_quantity'] ?? '50'); ?>"
|
||||
style="width: 150px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Ab dieser Menge gilt der Normalpreis (Multiplikator = 1)</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-field-row" style="margin-top: 15px;">
|
||||
<label><strong>Dynamische Formel Privat</strong></label>
|
||||
<textarea
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[dynamic_pricing][private_formula]"
|
||||
rows="8"
|
||||
style="width: 100%; font-family: monospace; font-size: 12px;"
|
||||
placeholder="(%qty% >= %norm_p%) ? 1 : (2 - Math.sqrt(%qty% / %norm_p%))"><?php echo esc_textarea($settings['dynamic_pricing']['private_formula'] ?? ''); ?></textarea>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">
|
||||
<strong>JavaScript-Formel zur Berechnung des Preis-Multiplikators.</strong><br>
|
||||
Platzhalter verwenden: <code>%qty%</code> für aktuelle Menge, <code>%norm_p%</code> für Normalpreis-Menge Privat, <code>%mind_p%</code> für Mindestmenge Privat.<br>
|
||||
Beispiel: <code>(%qty% >= %norm_p%) ? 1 : (2 - Math.sqrt(%qty% / %norm_p%))</code><br>
|
||||
Dies bedeutet: Wenn die Menge >= Normalpreis-Menge ist, dann Multiplikator = 1, sonst dynamische Berechnung.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107;">
|
||||
<strong>Verfügbare Platzhalter:</strong>
|
||||
<ul style="margin: 10px 0 0 20px;">
|
||||
<li><code>%qty%</code> - Aktuelle Menge (eingegebene Stückzahl)</li>
|
||||
<li><code>%norm_b%</code> - Normalpreis Menge Business</li>
|
||||
<li><code>%mind_b%</code> - Mindestmenge Business</li>
|
||||
<li><code>%norm_p%</code> - Normalpreis Menge Privat</li>
|
||||
<li><code>%mind_p%</code> - Mindestmenge Privat</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backend-Verbindung -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>🔌 Backend-Verbindung</h2>
|
||||
<p>Konfigurieren Sie die Verbindung zum Backend-System für erweiterte Funktionen.</p>
|
||||
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>API URL / Domain</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[backend_connection][api_url]"
|
||||
value="<?php echo esc_attr($settings['backend_connection']['api_url'] ?? ''); ?>"
|
||||
placeholder="https://api.example.com"
|
||||
style="width: 100%; margin-top: 8px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Basis-URL des Backend-Systems (z.B. https://api.example.com)</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>API Token / Authentifizierung</strong>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[backend_connection][api_token]"
|
||||
value="<?php echo esc_attr($settings['backend_connection']['api_token'] ?? ''); ?>"
|
||||
placeholder="sk_live_..."
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Authentifizierungs-Token für API-Zugriff</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Webhook URL Geschäftskunden (B2B)</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[backend_connection][webhook_url_business]"
|
||||
value="<?php echo esc_attr($settings['backend_connection']['webhook_url_business'] ?? ''); ?>"
|
||||
placeholder="https://api.example.com/webhooks/order-business"
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Webhook wird nach Klick auf "Jetzt kostenpflichtig bestellen" für Geschäftskunden aufgerufen</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Webhook URL Privatkunden (B2C)</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[backend_connection][webhook_url_private]"
|
||||
value="<?php echo esc_attr($settings['backend_connection']['webhook_url_private'] ?? ''); ?>"
|
||||
placeholder="https://api.example.com/webhooks/order-private"
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Webhook wird nach erfolgreicher PayPal-Zahlung für Privatkunden aufgerufen</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Redirect URL Geschäftskunden</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[backend_connection][redirect_url_business]"
|
||||
value="<?php echo esc_attr($settings['backend_connection']['redirect_url_business'] ?? ''); ?>"
|
||||
placeholder="https://example.com/danke-business"
|
||||
style="width: 100%; margin-top: 8px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Weiterleitung nach Bestellung für Geschäftskunden (nach Klick auf "Jetzt kostenpflichtig bestellen")</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Redirect URL Privatkunden</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[backend_connection][redirect_url_private]"
|
||||
value="<?php echo esc_attr($settings['backend_connection']['redirect_url_private'] ?? ''); ?>"
|
||||
placeholder="https://example.com/danke-privat"
|
||||
style="width: 100%; margin-top: 8px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Weiterleitung nach erfolgreicher PayPal-Zahlung für Privatkunden</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REST API Security -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>🔐 REST API Sicherheit</h2>
|
||||
<p>Konfigurieren Sie einen API-Key für die REST-API-Endpunkte. Dieser Key muss im Header <code>X-Skrift-API-Key</code> mitgesendet werden.</p>
|
||||
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item" style="grid-column: 1 / -1;">
|
||||
<label>
|
||||
<strong>API Key</strong>
|
||||
</label>
|
||||
<div style="display: flex; gap: 10px; margin-top: 8px;">
|
||||
<input type="text" id="sk-api-key-input"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[api_security][api_key]"
|
||||
value="<?php echo esc_attr($settings['api_security']['api_key'] ?? ''); ?>"
|
||||
placeholder="sk_api_..."
|
||||
style="flex: 1; font-family: monospace;">
|
||||
<button type="button" class="button" onclick="document.getElementById('sk-api-key-input').value = 'sk_api_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);">
|
||||
Generieren
|
||||
</button>
|
||||
</div>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Leer lassen um API-Key-Prüfung zu deaktivieren (nicht empfohlen für Produktion)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schriftmuster Fallback -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>✍️ Schriftmuster (Vorschau-Fallback)</h2>
|
||||
<p>Wenn die Vorschau-Generierung fehlschlägt oder nicht verfügbar ist, wird ein "Schriftmuster ansehen"-Link angezeigt. Der Link öffnet sich in einem neuen Tab.</p>
|
||||
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item" style="grid-column: 1 / -1;">
|
||||
<label>
|
||||
<strong>Schriftmuster-URL</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[font_sample][url]"
|
||||
value="<?php echo esc_attr($settings['font_sample']['url'] ?? ''); ?>"
|
||||
placeholder="https://example.com/schriftmuster"
|
||||
style="width: 100%; margin-top: 8px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">URL zur Schriftmuster-Seite (wird in neuem Tab geöffnet)</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item" style="grid-column: 1 / -1;">
|
||||
<label>
|
||||
<strong>Platzhalter-Hilfe URL</strong>
|
||||
</label>
|
||||
<input type="url"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[font_sample][placeholder_help_url]"
|
||||
value="<?php echo esc_attr($settings['font_sample']['placeholder_help_url'] ?? ''); ?>"
|
||||
placeholder="https://example.com/platzhalter-hilfe"
|
||||
style="width: 100%; margin-top: 8px;">
|
||||
<small style="display: block; margin-top: 5px; color: #666;">URL zur Platzhalter-Hilfeseite (wird bei Platzhalter-Infotexten als Link angezeigt)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PayPal-Verbindung -->
|
||||
<div class="sk-admin-section">
|
||||
<h2>💳 PayPal-Verbindung (nur Privatkunden)</h2>
|
||||
<p>Konfigurieren Sie die PayPal-Zahlungsintegration. PayPal ist nur für Privatkunden aktiviert.</p>
|
||||
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item" style="grid-column: 1 / -1;">
|
||||
<label style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="checkbox"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[paypal][enabled]"
|
||||
value="1"
|
||||
<?php checked($settings['paypal']['enabled'] ?? false); ?>>
|
||||
<strong>PayPal-Zahlung aktivieren</strong>
|
||||
</label>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Aktiviert PayPal als Zahlungsoption für Privatkunden</small>
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item" style="grid-column: 1 / -1;">
|
||||
<label>
|
||||
<strong>Modus</strong>
|
||||
</label>
|
||||
<select
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[paypal][mode]"
|
||||
style="width: 200px; margin-top: 8px;">
|
||||
<option value="sandbox" <?php selected(($settings['paypal']['mode'] ?? 'sandbox'), 'sandbox'); ?>>Sandbox (Test)</option>
|
||||
<option value="live" <?php selected(($settings['paypal']['mode'] ?? 'sandbox'), 'live'); ?>>Live (Produktion)</option>
|
||||
</select>
|
||||
<small style="display: block; margin-top: 5px; color: #666;">Sandbox für Tests, Live für echte Zahlungen</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd;">Sandbox-Zugangsdaten (Test)</h3>
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Client ID (Sandbox)</strong>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[paypal][client_id_sandbox]"
|
||||
value="<?php echo esc_attr($settings['paypal']['client_id_sandbox'] ?? ''); ?>"
|
||||
placeholder="AZn4..."
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Client Secret (Sandbox)</strong>
|
||||
</label>
|
||||
<input type="password"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[paypal][client_secret_sandbox]"
|
||||
value="<?php echo esc_attr($settings['paypal']['client_secret_sandbox'] ?? ''); ?>"
|
||||
placeholder="EL3..."
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd;">Live-Zugangsdaten (Produktion)</h3>
|
||||
<div class="sk-price-grid">
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Client ID (Live)</strong>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[paypal][client_id_live]"
|
||||
value="<?php echo esc_attr($settings['paypal']['client_id_live'] ?? ''); ?>"
|
||||
placeholder="AZn4..."
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
</div>
|
||||
|
||||
<div class="sk-price-item">
|
||||
<label>
|
||||
<strong>Client Secret (Live)</strong>
|
||||
</label>
|
||||
<input type="password"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[paypal][client_secret_live]"
|
||||
value="<?php echo esc_attr($settings['paypal']['client_secret_live'] ?? ''); ?>"
|
||||
placeholder="EL3..."
|
||||
style="width: 100%; margin-top: 8px; font-family: monospace;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding: 15px; background: #e7f3ff; border-left: 4px solid #2196f3;">
|
||||
<strong>Hinweis:</strong> Um PayPal zu aktivieren, benötigen Sie ein PayPal Business-Konto.
|
||||
Sie erhalten die API-Zugangsdaten im <a href="https://developer.paypal.com/dashboard/applications/" target="_blank">PayPal Developer Dashboard</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php submit_button('Einstellungen speichern'); ?>
|
||||
</form>
|
||||
|
||||
<!-- URL Parameter Dokumentation -->
|
||||
<div class="sk-section" style="margin-top: 40px; padding: 20px; background: #f9f9f9; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<h2 style="margin-top: 0;">URL-Parameter</h2>
|
||||
<p>Der Konfigurator unterstützt folgende URL-Parameter:</p>
|
||||
|
||||
<table class="widefat" style="margin-top: 15px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 200px;">Parameter</th>
|
||||
<th style="width: 200px;">Werte</th>
|
||||
<th>Beschreibung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>?businessbriefe</code><br>
|
||||
<code>?business-postkarten</code><br>
|
||||
<code>?follow-ups</code><br>
|
||||
<code>?einladungen</code><br>
|
||||
<code>?private-briefe</code></td>
|
||||
<td>–</td>
|
||||
<td>Produkt direkt vorauswählen. Der Produktauswahlschritt wird übersprungen.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>quantity</code></td>
|
||||
<td>Zahl (z.B. <code>100</code>)</td>
|
||||
<td>Menge vorausfüllen.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>format</code></td>
|
||||
<td><code>a4</code>, <code>a6h</code>, <code>a6q</code></td>
|
||||
<td>Format vorauswählen. <code>a6h</code> = A6 Hochformat, <code>a6q</code> = A6 Querformat.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>noPrice</code></td>
|
||||
<td>–</td>
|
||||
<td>Preise im Konfigurator ausblenden.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>noLimits</code></td>
|
||||
<td>–</td>
|
||||
<td>Keine Mindestmengen. Erlaubt Bestellungen ab 1 Stück.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 style="margin-top: 25px;">Beispiele</h3>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li><code>/konfigurator/?businessbriefe</code> – Direkt zu Business Briefe</li>
|
||||
<li><code>/konfigurator/?einladungen&quantity=25&format=a6h</code> – Einladungen mit 25 Stück im A6 Hochformat</li>
|
||||
<li><code>/konfigurator/?businessbriefe&noPrice</code> – Business Briefe ohne Preisanzeige</li>
|
||||
<li><code>/konfigurator/?private-briefe&noLimits</code> – Private Briefe ohne Mindestmenge</li>
|
||||
<li><code>/konfigurator/?business-postkarten&noLimits&noPrice</code> – Postkarten ohne Mindestmenge und ohne Preise</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_product_card(string $key, string $default_label, array $settings): void {
|
||||
$label = $settings['products'][$key]['label'] ?? $default_label;
|
||||
$description = $settings['products'][$key]['description'] ?? 'Professionelle handgeschriebene Korrespondenz';
|
||||
$base_price = $settings['products'][$key]['base_price'] ?? '2.50';
|
||||
?>
|
||||
<div class="sk-product-card">
|
||||
<h3><?php echo esc_html($default_label); ?></h3>
|
||||
|
||||
<div class="sk-field-row">
|
||||
<label>Produktname</label>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[products][<?php echo esc_attr($key); ?>][label]"
|
||||
value="<?php echo esc_attr($label); ?>"
|
||||
placeholder="<?php echo esc_attr($default_label); ?>">
|
||||
</div>
|
||||
|
||||
<div class="sk-field-row">
|
||||
<label>Beschreibung</label>
|
||||
<textarea
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[products][<?php echo esc_attr($key); ?>][description]"
|
||||
rows="3"
|
||||
placeholder="Professionelle handgeschriebene Korrespondenz"><?php echo esc_textarea($description); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="sk-field-row">
|
||||
<label>Startpreis (ab)</label>
|
||||
<div>
|
||||
<input type="number" step="0.01" min="0"
|
||||
name="<?php echo esc_attr(self::OPTION_KEY); ?>[products][<?php echo esc_attr($key); ?>][base_price]"
|
||||
value="<?php echo esc_attr($base_price); ?>"> €
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public static function get_settings(): array {
|
||||
$defaults = [
|
||||
'products' => [
|
||||
'businessbriefe' => [
|
||||
'label' => 'Businessbriefe',
|
||||
'description' => 'Professionelle handgeschriebene Korrespondenz',
|
||||
'base_price' => 2.50,
|
||||
],
|
||||
'business-postkarten' => [
|
||||
'label' => 'Business Postkarten',
|
||||
'description' => 'Professionelle handgeschriebene Korrespondenz',
|
||||
'base_price' => 1.80,
|
||||
],
|
||||
'follow-ups' => [
|
||||
'label' => 'Follow-ups',
|
||||
'description' => 'Professionelle handgeschriebene Korrespondenz',
|
||||
'base_price' => 2.50,
|
||||
],
|
||||
'einladungen' => [
|
||||
'label' => 'Einladungen',
|
||||
'description' => 'Professionelle handgeschriebene Korrespondenz',
|
||||
'base_price' => 1.80,
|
||||
],
|
||||
'private-briefe' => [
|
||||
'label' => 'Private Briefe',
|
||||
'description' => 'Professionelle handgeschriebene Korrespondenz',
|
||||
'base_price' => 2.50,
|
||||
],
|
||||
],
|
||||
'prices' => [
|
||||
// Versand & Umschlag
|
||||
'shipping_domestic' => 0.95, // Porto Inland
|
||||
'shipping_international' => 1.25, // Porto Ausland
|
||||
'shipping_service' => 0.95, // Serviceaufschlag Versand
|
||||
'shipping_bulk' => 4.95, // Bulkversand einmalig
|
||||
'envelope_base' => 0.50, // Kuvert Grundpreis
|
||||
'envelope_labeling' => 0.50, // Aufschlag Beschriftung
|
||||
// Legacy fields for backwards compatibility
|
||||
'shipping_direct' => 2.40,
|
||||
'envelope_recipient_address' => 0.50,
|
||||
'envelope_custom_text' => 0.30,
|
||||
|
||||
// Format Aufpreise
|
||||
'a4_upgrade_surcharge' => 0.50,
|
||||
|
||||
// Zusatzleistungen
|
||||
'motif_upload' => 0.30,
|
||||
'motif_printed' => 0.00,
|
||||
'motif_design' => 0.00,
|
||||
'textservice' => 0.00,
|
||||
'api_connection' => 250.00,
|
||||
|
||||
// Follow-ups Multiplikatoren
|
||||
'followup_mult_5_49' => 2.0,
|
||||
'followup_mult_50_199' => 1.7,
|
||||
'followup_mult_200_499' => 1.4,
|
||||
'followup_mult_500_999' => 1.2,
|
||||
'followup_mult_1000_plus' => 1.0,
|
||||
|
||||
// Steuern
|
||||
'tax_rate' => 19,
|
||||
'shipping_tax_rate' => 0,
|
||||
],
|
||||
'dynamic_pricing' => [
|
||||
'business_formula' => "(%qty% >= %norm_b%) ? 1 : (2 - Math.sqrt(%qty% / %norm_b%))",
|
||||
'private_formula' => "(%qty% >= %norm_p%) ? 1 : (2 - Math.sqrt(%qty% / %norm_p%))",
|
||||
'business_min_quantity' => 50,
|
||||
'private_min_quantity' => 10,
|
||||
'business_normal_quantity' => 200,
|
||||
'private_normal_quantity' => 50,
|
||||
],
|
||||
'backend_connection' => [
|
||||
'api_url' => '',
|
||||
'api_token' => '',
|
||||
'webhook_url_business' => '',
|
||||
'webhook_url_private' => '',
|
||||
'redirect_url_business' => '',
|
||||
'redirect_url_private' => '',
|
||||
],
|
||||
'paypal' => [
|
||||
'enabled' => false,
|
||||
'mode' => 'sandbox',
|
||||
'client_id_sandbox' => '',
|
||||
'client_secret_sandbox' => '',
|
||||
'client_id_live' => '',
|
||||
'client_secret_live' => '',
|
||||
],
|
||||
'api_security' => [
|
||||
'api_key' => '',
|
||||
],
|
||||
'font_sample' => [
|
||||
'url' => '',
|
||||
'placeholder_help_url' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$saved = get_option(self::OPTION_KEY, []);
|
||||
|
||||
// Merge nested arrays properly
|
||||
$merged = $defaults;
|
||||
foreach (['products', 'prices', 'dynamic_pricing', 'backend_connection', 'paypal', 'api_security', 'font_sample'] as $section) {
|
||||
if (isset($saved[$section]) && is_array($saved[$section])) {
|
||||
$merged[$section] = array_merge($defaults[$section], $saved[$section]);
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein API-Key gültig ist
|
||||
*/
|
||||
public static function validate_api_key($provided_key): bool {
|
||||
$settings = self::get_settings();
|
||||
$stored_key = $settings['api_security']['api_key'] ?? '';
|
||||
|
||||
// Wenn kein Key konfiguriert ist, ist alles erlaubt (für Entwicklung)
|
||||
if (empty($stored_key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Key vergleichen (timing-safe)
|
||||
return hash_equals($stored_key, $provided_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission Callback für REST API mit API-Key
|
||||
*/
|
||||
public static function rest_api_key_permission($request): bool {
|
||||
$api_key = $request->get_header('X-Skrift-API-Key');
|
||||
return self::validate_api_key($api_key ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
new Skrift_Konfigurator_Admin_Settings();
|
||||
403
skrift-configurator/includes/admin-vouchers.php
Normal file
403
skrift-configurator/includes/admin-vouchers.php
Normal file
@@ -0,0 +1,403 @@
|
||||
<?php
|
||||
/**
|
||||
* Gutschein-Verwaltung für Skrift Konfigurator
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
|
||||
final class Skrift_Konfigurator_Vouchers {
|
||||
|
||||
const OPTION_KEY = 'skrift_konfigurator_vouchers';
|
||||
|
||||
public function __construct() {
|
||||
add_action('admin_menu', [$this, 'add_menu_page']);
|
||||
add_action('admin_init', [$this, 'register_settings']);
|
||||
add_action('admin_post_sk_add_voucher', [$this, 'handle_add_voucher']);
|
||||
add_action('admin_post_sk_delete_voucher', [$this, 'handle_delete_voucher']);
|
||||
add_action('rest_api_init', [$this, 'register_rest_routes']);
|
||||
}
|
||||
|
||||
public function add_menu_page(): void {
|
||||
add_submenu_page(
|
||||
'options-general.php',
|
||||
'Skrift Gutscheine',
|
||||
'Skrift Gutscheine',
|
||||
'manage_options',
|
||||
'skrift-vouchers',
|
||||
[$this, 'render_vouchers_page']
|
||||
);
|
||||
}
|
||||
|
||||
public function register_settings(): void {
|
||||
register_setting('skrift_vouchers', self::OPTION_KEY, [
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => [$this, 'sanitize_vouchers'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function sanitize_vouchers($input) {
|
||||
if (!is_array($input)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sanitized = [];
|
||||
foreach ($input as $code => $voucher) {
|
||||
// Verwende den originalen Code (in Großbuchstaben) als Key
|
||||
$voucherCode = strtoupper(sanitize_text_field($voucher['code'] ?? ''));
|
||||
$sanitized[$voucherCode] = [
|
||||
'code' => $voucherCode,
|
||||
'type' => in_array($voucher['type'] ?? '', ['percent', 'fixed']) ? $voucher['type'] : 'percent',
|
||||
'value' => floatval($voucher['value'] ?? 0),
|
||||
'expiry_date' => sanitize_text_field($voucher['expiry_date'] ?? ''),
|
||||
'usage_limit' => intval($voucher['usage_limit'] ?? 0),
|
||||
'usage_count' => intval($voucher['usage_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
public function handle_add_voucher(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Keine Berechtigung');
|
||||
}
|
||||
|
||||
check_admin_referer('sk_add_voucher');
|
||||
|
||||
$vouchers = get_option(self::OPTION_KEY, []);
|
||||
$code = strtoupper(sanitize_text_field($_POST['voucher_code'] ?? ''));
|
||||
|
||||
if (empty($code)) {
|
||||
wp_redirect(add_query_arg(['page' => 'skrift-vouchers', 'error' => 'empty_code'], admin_url('options-general.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($vouchers[$code])) {
|
||||
wp_redirect(add_query_arg(['page' => 'skrift-vouchers', 'error' => 'duplicate'], admin_url('options-general.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
$vouchers[$code] = [
|
||||
'code' => $code,
|
||||
'type' => sanitize_text_field($_POST['voucher_type'] ?? 'percent'),
|
||||
'value' => floatval($_POST['voucher_value'] ?? 0),
|
||||
'expiry_date' => sanitize_text_field($_POST['voucher_expiry'] ?? ''),
|
||||
'usage_limit' => intval($_POST['voucher_limit'] ?? 0),
|
||||
'usage_count' => 0,
|
||||
];
|
||||
|
||||
update_option(self::OPTION_KEY, $vouchers);
|
||||
|
||||
wp_redirect(add_query_arg(['page' => 'skrift-vouchers', 'success' => 'added'], admin_url('options-general.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_delete_voucher(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Keine Berechtigung');
|
||||
}
|
||||
|
||||
check_admin_referer('sk_delete_voucher');
|
||||
|
||||
$code = sanitize_text_field($_GET['code'] ?? '');
|
||||
$vouchers = get_option(self::OPTION_KEY, []);
|
||||
|
||||
if (isset($vouchers[$code])) {
|
||||
unset($vouchers[$code]);
|
||||
update_option(self::OPTION_KEY, $vouchers);
|
||||
}
|
||||
|
||||
wp_redirect(add_query_arg(['page' => 'skrift-vouchers', 'success' => 'deleted'], admin_url('options-general.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function render_vouchers_page(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$vouchers = get_option(self::OPTION_KEY, []);
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Gutschein-Verwaltung</h1>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
<p>
|
||||
<?php
|
||||
if ($_GET['success'] === 'added') {
|
||||
echo 'Gutschein erfolgreich hinzugefügt!';
|
||||
} elseif ($_GET['success'] === 'deleted') {
|
||||
echo 'Gutschein erfolgreich gelöscht!';
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($_GET['error'])): ?>
|
||||
<div class="notice notice-error is-dismissible">
|
||||
<p>
|
||||
<?php
|
||||
if ($_GET['error'] === 'empty_code') {
|
||||
echo 'Gutscheincode darf nicht leer sein!';
|
||||
} elseif ($_GET['error'] === 'duplicate') {
|
||||
echo 'Dieser Gutscheincode existiert bereits!';
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="background: white; padding: 20px; margin-top: 20px; border: 1px solid #ccc;">
|
||||
<h2>Neuen Gutschein erstellen</h2>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="sk_add_voucher">
|
||||
<?php wp_nonce_field('sk_add_voucher'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><label for="voucher_code">Gutscheincode *</label></th>
|
||||
<td>
|
||||
<input type="text" id="voucher_code" name="voucher_code" class="regular-text" required
|
||||
style="text-transform: uppercase;" placeholder="z.B. SOMMER2025">
|
||||
<p class="description">Code wird automatisch in Großbuchstaben umgewandelt</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><label for="voucher_type">Rabatt-Art *</label></th>
|
||||
<td>
|
||||
<select id="voucher_type" name="voucher_type" required>
|
||||
<option value="percent">Prozent (%)</option>
|
||||
<option value="fixed">Festbetrag (€)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><label for="voucher_value">Rabatt-Wert *</label></th>
|
||||
<td>
|
||||
<input type="number" id="voucher_value" name="voucher_value" step="0.01" min="0" required
|
||||
class="small-text">
|
||||
<p class="description">Bei Prozent: z.B. 10 für 10% | Bei Festbetrag: z.B. 5.00 für 5€</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><label for="voucher_expiry">Ablaufdatum</label></th>
|
||||
<td>
|
||||
<input type="date" id="voucher_expiry" name="voucher_expiry" class="regular-text">
|
||||
<p class="description">Leer lassen für unbegrenzte Gültigkeit</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><label for="voucher_limit">Einlöse-Limit</label></th>
|
||||
<td>
|
||||
<input type="number" id="voucher_limit" name="voucher_limit" min="0" class="small-text" value="0">
|
||||
<p class="description">0 = Unbegrenzt oft einlösbar</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button('Gutschein erstellen'); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="background: white; padding: 20px; margin-top: 20px; border: 1px solid #ccc;">
|
||||
<h2>Vorhandene Gutscheine</h2>
|
||||
|
||||
<?php if (empty($vouchers)): ?>
|
||||
<p>Noch keine Gutscheine vorhanden.</p>
|
||||
<?php else: ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Typ</th>
|
||||
<th>Wert</th>
|
||||
<th>Ablaufdatum</th>
|
||||
<th>Limit</th>
|
||||
<th>Eingelöst</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($vouchers as $code => $voucher):
|
||||
$is_expired = !empty($voucher['expiry_date']) && strtotime($voucher['expiry_date']) < time();
|
||||
$is_used_up = $voucher['usage_limit'] > 0 && $voucher['usage_count'] >= $voucher['usage_limit'];
|
||||
$is_active = !$is_expired && !$is_used_up;
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html($voucher['code']); ?></strong></td>
|
||||
<td><?php echo $voucher['type'] === 'percent' ? 'Prozent' : 'Festbetrag'; ?></td>
|
||||
<td>
|
||||
<?php
|
||||
if ($voucher['type'] === 'percent') {
|
||||
echo number_format($voucher['value'], 1) . ' %';
|
||||
} else {
|
||||
echo number_format($voucher['value'], 2, ',', '.') . ' €';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
if (!empty($voucher['expiry_date'])) {
|
||||
echo date('d.m.Y', strtotime($voucher['expiry_date']));
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo $voucher['usage_limit'] > 0 ? $voucher['usage_limit'] : 'Unbegrenzt'; ?></td>
|
||||
<td><?php echo $voucher['usage_count']; ?></td>
|
||||
<td>
|
||||
<?php if ($is_active): ?>
|
||||
<span style="color: green; font-weight: bold;">✓ Aktiv</span>
|
||||
<?php elseif ($is_expired): ?>
|
||||
<span style="color: red;">✗ Abgelaufen</span>
|
||||
<?php elseif ($is_used_up): ?>
|
||||
<span style="color: orange;">✗ Limit erreicht</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?php echo wp_nonce_url(
|
||||
admin_url('admin-post.php?action=sk_delete_voucher&code=' . urlencode($code)),
|
||||
'sk_delete_voucher'
|
||||
); ?>"
|
||||
class="button button-small"
|
||||
onclick="return confirm('Gutschein <?php echo esc_js($code); ?> wirklich löschen?');">
|
||||
Löschen
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffentliche Funktion um Gutscheine abzurufen
|
||||
*/
|
||||
public static function get_vouchers() {
|
||||
return get_option(self::OPTION_KEY, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert einen Gutschein
|
||||
*/
|
||||
public static function validate_voucher($code) {
|
||||
$vouchers = self::get_vouchers();
|
||||
$code = strtoupper(trim($code));
|
||||
|
||||
if (!isset($vouchers[$code])) {
|
||||
return ['valid' => false, 'error' => 'Gutschein nicht gefunden'];
|
||||
}
|
||||
|
||||
$voucher = $vouchers[$code];
|
||||
|
||||
// Ablaufdatum prüfen
|
||||
if (!empty($voucher['expiry_date']) && strtotime($voucher['expiry_date']) < time()) {
|
||||
return ['valid' => false, 'error' => 'Gutschein ist abgelaufen'];
|
||||
}
|
||||
|
||||
// Nutzungslimit prüfen
|
||||
if ($voucher['usage_limit'] > 0 && $voucher['usage_count'] >= $voucher['usage_limit']) {
|
||||
return ['valid' => false, 'error' => 'Gutschein wurde bereits zu oft eingelöst'];
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => true,
|
||||
'voucher' => $voucher
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert einen Gutschein als verwendet
|
||||
*/
|
||||
public static function use_voucher($code) {
|
||||
$vouchers = self::get_vouchers();
|
||||
$code = strtoupper(trim($code));
|
||||
|
||||
if (isset($vouchers[$code])) {
|
||||
$vouchers[$code]['usage_count']++;
|
||||
update_option(self::OPTION_KEY, $vouchers);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Routen registrieren
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route('skrift/v1', '/voucher/use', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_use_voucher'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route('skrift/v1', '/voucher/validate', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_validate_voucher'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Endpoint: Gutschein validieren
|
||||
*/
|
||||
public function rest_validate_voucher($request) {
|
||||
$code = $request->get_param('code');
|
||||
|
||||
if (empty($code)) {
|
||||
return new WP_Error('missing_code', 'Gutschein-Code fehlt', ['status' => 400]);
|
||||
}
|
||||
|
||||
$result = self::validate_voucher($code);
|
||||
|
||||
if ($result['valid']) {
|
||||
return [
|
||||
'valid' => true,
|
||||
'voucher' => [
|
||||
'code' => $result['voucher']['code'],
|
||||
'type' => $result['voucher']['type'],
|
||||
'value' => $result['voucher']['value'],
|
||||
]
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => $result['error']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Endpoint: Gutschein als verwendet markieren
|
||||
*/
|
||||
public function rest_use_voucher($request) {
|
||||
$code = $request->get_param('code');
|
||||
|
||||
if (empty($code)) {
|
||||
return new WP_Error('missing_code', 'Gutschein-Code fehlt', ['status' => 400]);
|
||||
}
|
||||
|
||||
$result = self::use_voucher($code);
|
||||
|
||||
if ($result) {
|
||||
return ['success' => true, 'message' => 'Gutschein wurde als verwendet markiert'];
|
||||
} else {
|
||||
return new WP_Error('invalid_code', 'Ungültiger Gutschein-Code', ['status' => 404]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Skrift_Konfigurator_Vouchers();
|
||||
351
skrift-configurator/includes/api-proxy.php
Normal file
351
skrift-configurator/includes/api-proxy.php
Normal file
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
/**
|
||||
* API Proxy für Skrift Konfigurator
|
||||
* Leitet Anfragen an das Backend weiter, ohne den API-Token im Frontend zu exponieren
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
|
||||
final class Skrift_Konfigurator_API_Proxy {
|
||||
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', [$this, 'register_rest_routes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Routen registrieren
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
// Health Check
|
||||
register_rest_route('skrift/v1', '/proxy/health', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'proxy_health_check'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
// Preview Batch generieren
|
||||
register_rest_route('skrift/v1', '/proxy/preview/batch', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'proxy_preview_batch'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
// Einzelne Preview abrufen
|
||||
register_rest_route('skrift/v1', '/proxy/preview/(?P<sessionId>[a-zA-Z0-9_-]+)/(?P<index>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'proxy_preview_get'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
// Order generieren
|
||||
register_rest_route('skrift/v1', '/proxy/order/generate', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'proxy_order_generate'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
// Order finalisieren
|
||||
register_rest_route('skrift/v1', '/proxy/order/finalize', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'proxy_order_finalize'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
|
||||
// Motiv hochladen (ans Docker-Backend weiterleiten)
|
||||
register_rest_route('skrift/v1', '/proxy/motif/upload', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'proxy_motif_upload'],
|
||||
'permission_callback' => ['Skrift_Konfigurator_Admin_Settings', 'rest_api_key_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt Backend-Konfiguration
|
||||
*/
|
||||
private function get_backend_config(): array {
|
||||
$settings = Skrift_Konfigurator_Admin_Settings::get_settings();
|
||||
return [
|
||||
'api_url' => $settings['backend_connection']['api_url'] ?? '',
|
||||
'api_token' => $settings['backend_connection']['api_token'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen HTTP-Request ans Backend aus
|
||||
*/
|
||||
private function make_backend_request(string $method, string $endpoint, array $body = null): array {
|
||||
$config = $this->get_backend_config();
|
||||
|
||||
if (empty($config['api_url'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Backend API URL nicht konfiguriert',
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
|
||||
$url = rtrim($config['api_url'], '/') . $endpoint;
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'timeout' => 60,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
];
|
||||
|
||||
// API Token hinzufügen wenn vorhanden
|
||||
if (!empty($config['api_token'])) {
|
||||
$args['headers']['X-API-Token'] = $config['api_token'];
|
||||
}
|
||||
|
||||
// Body hinzufügen bei POST/PUT
|
||||
if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH'])) {
|
||||
$args['body'] = wp_json_encode($body);
|
||||
}
|
||||
|
||||
$response = wp_remote_request($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message(),
|
||||
'status' => 500,
|
||||
];
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$response_body = wp_remote_retrieve_body($response);
|
||||
|
||||
// Versuche JSON zu parsen
|
||||
$data = json_decode($response_body, true);
|
||||
|
||||
if ($status_code >= 200 && $status_code < 300) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $data ?? $response_body,
|
||||
'status' => $status_code,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $data['error'] ?? $data['message'] ?? 'Backend-Fehler',
|
||||
'status' => $status_code,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Health Check Proxy
|
||||
*/
|
||||
public function proxy_health_check($request) {
|
||||
$result = $this->make_backend_request('GET', '/health');
|
||||
|
||||
if (!$result['success']) {
|
||||
return new WP_Error('backend_error', $result['error'], ['status' => $result['status']]);
|
||||
}
|
||||
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Batch Proxy
|
||||
*/
|
||||
public function proxy_preview_batch($request) {
|
||||
$body = $request->get_json_params();
|
||||
|
||||
if (empty($body)) {
|
||||
return new WP_Error('invalid_request', 'Keine Daten empfangen', ['status' => 400]);
|
||||
}
|
||||
|
||||
$result = $this->make_backend_request('POST', '/api/preview/batch', $body);
|
||||
|
||||
if (!$result['success']) {
|
||||
// Rate Limiting Info weitergeben
|
||||
if ($result['status'] === 429 && isset($result['data']['retryAfter'])) {
|
||||
return new WP_REST_Response([
|
||||
'error' => $result['error'],
|
||||
'retryAfter' => $result['data']['retryAfter'],
|
||||
], 429);
|
||||
}
|
||||
return new WP_Error('backend_error', $result['error'], ['status' => $result['status']]);
|
||||
}
|
||||
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Preview abrufen Proxy
|
||||
*/
|
||||
public function proxy_preview_get($request) {
|
||||
$session_id = $request->get_param('sessionId');
|
||||
$index = $request->get_param('index');
|
||||
|
||||
// Sicherheits-Validierung: Session-ID darf nur alphanumerische Zeichen, Unterstriche und Bindestriche enthalten
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $session_id)) {
|
||||
return new WP_Error('invalid_session_id', 'Ungültige Session-ID', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Index muss eine positive Ganzzahl sein
|
||||
$index = absint($index);
|
||||
|
||||
$result = $this->make_backend_request('GET', "/api/preview/{$session_id}/{$index}");
|
||||
|
||||
if (!$result['success']) {
|
||||
return new WP_Error('backend_error', $result['error'], ['status' => $result['status']]);
|
||||
}
|
||||
|
||||
// Bei SVG-Daten: Content-Type setzen
|
||||
if (is_string($result['data']) && strpos($result['data'], '<svg') !== false) {
|
||||
$response = new WP_REST_Response($result['data']);
|
||||
$response->header('Content-Type', 'image/svg+xml');
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Order generieren Proxy
|
||||
*/
|
||||
public function proxy_order_generate($request) {
|
||||
$body = $request->get_json_params();
|
||||
|
||||
if (empty($body)) {
|
||||
return new WP_Error('invalid_request', 'Keine Daten empfangen', ['status' => 400]);
|
||||
}
|
||||
|
||||
$result = $this->make_backend_request('POST', '/api/order/generate', $body);
|
||||
|
||||
if (!$result['success']) {
|
||||
return new WP_Error('backend_error', $result['error'], ['status' => $result['status']]);
|
||||
}
|
||||
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Order finalisieren Proxy
|
||||
*/
|
||||
public function proxy_order_finalize($request) {
|
||||
$body = $request->get_json_params();
|
||||
|
||||
if (empty($body)) {
|
||||
return new WP_Error('invalid_request', 'Keine Daten empfangen', ['status' => 400]);
|
||||
}
|
||||
|
||||
$result = $this->make_backend_request('POST', '/api/order/finalize', $body);
|
||||
|
||||
if (!$result['success']) {
|
||||
return new WP_Error('backend_error', $result['error'], ['status' => $result['status']]);
|
||||
}
|
||||
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Motiv hochladen Proxy
|
||||
* Leitet Datei ans Docker-Backend weiter, speichert dort im Auftragsordner
|
||||
*/
|
||||
public function proxy_motif_upload($request) {
|
||||
// Datei aus Request holen
|
||||
$files = $request->get_file_params();
|
||||
|
||||
if (empty($files['motif'])) {
|
||||
return new WP_Error('no_file', 'Keine Datei empfangen', ['status' => 400]);
|
||||
}
|
||||
|
||||
$file = $files['motif'];
|
||||
$order_number = $request->get_param('orderNumber');
|
||||
|
||||
if (empty($order_number)) {
|
||||
return new WP_Error('no_order_number', 'Bestellnummer fehlt', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Überprüfe auf Upload-Fehler
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
return new WP_Error('upload_error', 'Upload-Fehler: ' . $file['error'], ['status' => 400]);
|
||||
}
|
||||
|
||||
// Erlaubte Dateitypen prüfen
|
||||
$allowed_types = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/svg+xml', 'application/pdf'];
|
||||
$file_type = wp_check_filetype($file['name']);
|
||||
|
||||
// MIME-Type Prüfung
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$real_type = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
// SVG wird oft als text/xml erkannt
|
||||
if ($real_type === 'text/xml' || $real_type === 'text/plain') {
|
||||
$content = file_get_contents($file['tmp_name']);
|
||||
if (strpos($content, '<svg') !== false) {
|
||||
$real_type = 'image/svg+xml';
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($real_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
|
||||
return new WP_Error('invalid_type', 'Ungültiger Dateityp: ' . $real_type, ['status' => 400]);
|
||||
}
|
||||
|
||||
// Datei ans Docker-Backend senden (multipart/form-data)
|
||||
$config = $this->get_backend_config();
|
||||
|
||||
if (empty($config['api_url'])) {
|
||||
return new WP_Error('no_backend', 'Backend API URL nicht konfiguriert', ['status' => 500]);
|
||||
}
|
||||
|
||||
$url = rtrim($config['api_url'], '/') . '/api/order/motif';
|
||||
|
||||
// Boundary für multipart
|
||||
$boundary = wp_generate_password(24, false);
|
||||
|
||||
// Multipart Body bauen
|
||||
$body = '';
|
||||
|
||||
// orderNumber Feld
|
||||
$body .= "--{$boundary}\r\n";
|
||||
$body .= "Content-Disposition: form-data; name=\"orderNumber\"\r\n\r\n";
|
||||
$body .= $order_number . "\r\n";
|
||||
|
||||
// Datei Feld
|
||||
$body .= "--{$boundary}\r\n";
|
||||
$body .= "Content-Disposition: form-data; name=\"motif\"; filename=\"" . basename($file['name']) . "\"\r\n";
|
||||
$body .= "Content-Type: " . ($real_type ?: 'application/octet-stream') . "\r\n\r\n";
|
||||
$body .= file_get_contents($file['tmp_name']) . "\r\n";
|
||||
$body .= "--{$boundary}--\r\n";
|
||||
|
||||
$args = [
|
||||
'method' => 'POST',
|
||||
'timeout' => 60,
|
||||
'headers' => [
|
||||
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
|
||||
],
|
||||
'body' => $body,
|
||||
];
|
||||
|
||||
// API Token hinzufügen wenn vorhanden
|
||||
if (!empty($config['api_token'])) {
|
||||
$args['headers']['X-API-Token'] = $config['api_token'];
|
||||
}
|
||||
|
||||
$response = wp_remote_post($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('backend_error', $response->get_error_message(), ['status' => 500]);
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$response_body = wp_remote_retrieve_body($response);
|
||||
$data = json_decode($response_body, true);
|
||||
|
||||
if ($status_code >= 200 && $status_code < 300) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return new WP_Error('backend_error', $data['error'] ?? $data['message'] ?? 'Upload fehlgeschlagen', ['status' => $status_code]);
|
||||
}
|
||||
}
|
||||
|
||||
new Skrift_Konfigurator_API_Proxy();
|
||||
0
skrift-configurator/readme.txt
Normal file
0
skrift-configurator/readme.txt
Normal file
200
skrift-configurator/skrift-konfigurator.php
Normal file
200
skrift-configurator/skrift-konfigurator.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Skrift Konfigurator
|
||||
* Description: Interaktiver Konfigurator für handgeschriebene Briefe
|
||||
* Version: 0.3.0
|
||||
* Author: Skrift
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
|
||||
// Admin Settings IMMER laden (für REST API Permission Callbacks)
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/admin-settings.php';
|
||||
|
||||
// Gutscheine IMMER laden (für REST API)
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/admin-vouchers.php';
|
||||
|
||||
// Bestellnummern-Verwaltung laden (für REST API)
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/admin-orders.php';
|
||||
|
||||
// Backend API Proxy laden (für REST API)
|
||||
require_once plugin_dir_path(__FILE__) . 'includes/api-proxy.php';
|
||||
|
||||
final class Skrift_Konfigurator_Plugin {
|
||||
|
||||
const VERSION = '0.3.0';
|
||||
const SLUG = 'skrift-konfigurator';
|
||||
|
||||
public function __construct() {
|
||||
add_action('wp_enqueue_scripts', [$this, 'register_assets']);
|
||||
add_filter('script_loader_tag', [$this, 'add_module_attribute'], 10, 3);
|
||||
add_shortcode('skrift_konfigurator', [$this, 'render_shortcode']);
|
||||
add_shortcode('skrift_preisrechner', [$this, 'render_preisrechner_shortcode']);
|
||||
}
|
||||
|
||||
public function register_assets(): void {
|
||||
$base = plugin_dir_url(__FILE__);
|
||||
|
||||
wp_register_style(
|
||||
self::SLUG,
|
||||
$base . 'assets/css/configurator.css',
|
||||
[],
|
||||
self::VERSION
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
self::SLUG,
|
||||
$base . 'assets/js/configurator-app.js',
|
||||
[],
|
||||
self::VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Preisrechner Script
|
||||
wp_register_script(
|
||||
'skrift-preisrechner',
|
||||
$base . 'assets/js/price-calculator.js',
|
||||
[],
|
||||
self::VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function add_module_attribute(string $tag, string $handle, string $src): string {
|
||||
// Beide Scripts als ES6-Module laden
|
||||
if ($handle !== self::SLUG && $handle !== 'skrift-preisrechner') return $tag;
|
||||
return '<script type="module" src="' . esc_url($src) . '"></script>';
|
||||
}
|
||||
|
||||
public function render_shortcode($atts = []): string {
|
||||
wp_enqueue_style(self::SLUG);
|
||||
wp_enqueue_script(self::SLUG);
|
||||
|
||||
// Einstellungen aus DB holen
|
||||
$settings = Skrift_Konfigurator_Admin_Settings::get_settings();
|
||||
$vouchers = Skrift_Konfigurator_Vouchers::get_vouchers();
|
||||
|
||||
// WICHTIG: Sicherstellen dass leere Arrays als Objekt {} encodiert werden, nicht als Array []
|
||||
if (empty($vouchers)) {
|
||||
$vouchers = new stdClass();
|
||||
}
|
||||
|
||||
// PayPal-Einstellungen für Frontend vorbereiten (ohne sensible Daten)
|
||||
$paypal_frontend = [];
|
||||
if (!empty($settings['paypal']['enabled'])) {
|
||||
$mode = $settings['paypal']['mode'] ?? 'sandbox';
|
||||
$client_id = ($mode === 'live')
|
||||
? ($settings['paypal']['client_id_live'] ?? '')
|
||||
: ($settings['paypal']['client_id_sandbox'] ?? '');
|
||||
|
||||
$paypal_frontend = [
|
||||
'enabled' => true,
|
||||
'mode' => $mode,
|
||||
'client_id' => $client_id,
|
||||
];
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<script>
|
||||
<?php
|
||||
// Settings kopieren aber sensible Daten entfernen
|
||||
$frontend_settings = $settings;
|
||||
// API Token NICHT ans Frontend senden - wird über WordPress Proxy gehandhabt
|
||||
unset($frontend_settings['backend_connection']['api_token']);
|
||||
// API Security Key NICHT ans Frontend senden
|
||||
unset($frontend_settings['api_security']);
|
||||
// PayPal Secrets NICHT ans Frontend senden
|
||||
unset($frontend_settings['paypal']['client_secret_sandbox']);
|
||||
unset($frontend_settings['paypal']['client_secret_live']);
|
||||
?>
|
||||
window.SkriftConfigurator = <?php echo wp_json_encode([
|
||||
'version' => self::VERSION,
|
||||
'restUrl' => esc_url_raw(rest_url()),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'apiKey' => $settings['api_security']['api_key'] ?? '', // API Key für REST-Aufrufe
|
||||
'settings' => $frontend_settings,
|
||||
'vouchers' => $vouchers,
|
||||
'paypal' => $paypal_frontend,
|
||||
]); ?>;
|
||||
</script>
|
||||
<style>.sk-configurator{opacity:0;transition:opacity .2s ease}.sk-configurator.sk-ready{opacity:1}</style>
|
||||
<div class="sk-configurator" data-skrift-konfigurator="1">
|
||||
<div class="sk-configurator__layout">
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="sk-main">
|
||||
|
||||
<!-- Top Bar mit Preis und Stepper -->
|
||||
<div id="sk-topbar" class="sk-topbar"></div>
|
||||
|
||||
<!-- Stepper -->
|
||||
<div id="sk-stepper"></div>
|
||||
|
||||
<!-- Form Content -->
|
||||
<div id="sk-form"></div>
|
||||
|
||||
<!-- Mobile Preview (nur auf mobilen Geräten sichtbar, vor dem Button) -->
|
||||
<div id="sk-preview-mobile" class="sk-preview-mobile"></div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div class="sk-nav">
|
||||
<button type="button" id="sk-prev" class="sk-btn sk-btn-secondary">
|
||||
← Zurück
|
||||
</button>
|
||||
<button type="button" id="sk-next" class="sk-btn sk-btn-primary">
|
||||
Weiter →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Contact Card (nach dem Button, nur mobile) -->
|
||||
<div id="sk-contact-mobile" class="sk-contact-card-mobile"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Preview Sidebar -->
|
||||
<aside class="sk-side">
|
||||
<div id="sk-preview"></div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert den Preisrechner Shortcode
|
||||
*/
|
||||
public function render_preisrechner_shortcode($atts = []): string {
|
||||
wp_enqueue_style(self::SLUG);
|
||||
wp_enqueue_script('skrift-preisrechner');
|
||||
|
||||
// Einstellungen aus DB holen (gleiche wie Konfigurator)
|
||||
$settings = Skrift_Konfigurator_Admin_Settings::get_settings();
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<script>
|
||||
<?php
|
||||
// Settings kopieren aber sensible Daten entfernen
|
||||
$frontend_settings = $settings;
|
||||
unset($frontend_settings['backend_connection']['api_token']);
|
||||
unset($frontend_settings['api_security']);
|
||||
unset($frontend_settings['paypal']);
|
||||
?>
|
||||
window.SkriftPreisrechner = <?php echo wp_json_encode([
|
||||
'version' => self::VERSION,
|
||||
'settings' => $frontend_settings,
|
||||
]); ?>;
|
||||
</script>
|
||||
<div class="sk-configurator" data-skrift-preisrechner="1">
|
||||
<!-- Preisrechner wird per JavaScript gerendert -->
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new Skrift_Konfigurator_Plugin();
|
||||
Reference in New Issue
Block a user