Initial commit: Django Shop ohne große Dateien
This commit is contained in:
commit
2d088f693d
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# Django static files
|
||||||
|
staticfiles/
|
||||||
|
|
||||||
|
# Media files (uploads)
|
||||||
|
media/
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Lade .env Datei
|
||||||
|
load_dotenv(BASE_DIR / '.env')
|
||||||
|
|
||||||
|
# E-Mail-Einstellungen (temporär Console-Backend)
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
DEFAULT_FROM_EMAIL = 'Fursuit Shop <noreply@fursuitshop.com>'
|
||||||
|
|
||||||
|
# Admin-E-Mail-Empfänger
|
||||||
|
ADMINS = [
|
||||||
|
('Shop Admin', 'admin@fursuitshop.com'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Lagerbestand-Einstellungen
|
||||||
|
LOW_STOCK_THRESHOLD = 5 # Schwellenwert für niedrigen Lagerbestand
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
# E-Mail-System Dokumentation
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Das E-Mail-System des Fursuit Shops versendet automatisch Benachrichtigungen an Kunden und Administratoren bei verschiedenen Shop-Ereignissen. Das System ist mehrsprachig (DE/EN) und verwendet responsive HTML-Templates mit Text-Alternativen.
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### E-Mail-Einstellungen
|
||||||
|
|
||||||
|
Die E-Mail-Konfiguration erfolgt über Umgebungsvariablen in der `.env`-Datei:
|
||||||
|
|
||||||
|
```env
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USE_TLS=True
|
||||||
|
EMAIL_HOST_USER=your-email@gmail.com
|
||||||
|
EMAIL_HOST_PASSWORD=your-app-specific-password
|
||||||
|
DEFAULT_FROM_EMAIL=Fursuit Shop <noreply@fursuitshop.com>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin-Benachrichtigungen
|
||||||
|
|
||||||
|
Administratoren werden in `settings.py` konfiguriert:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ADMINS = [
|
||||||
|
('Shop Admin', 'admin@fursuitshop.com'),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lagerbestand-Schwellenwert
|
||||||
|
|
||||||
|
```python
|
||||||
|
LOW_STOCK_THRESHOLD = 5 # Benachrichtigung bei ≤ 5 Artikeln
|
||||||
|
```
|
||||||
|
|
||||||
|
## E-Mail-Typen
|
||||||
|
|
||||||
|
### 1. Kundenbenachrichtigungen
|
||||||
|
|
||||||
|
#### Bestellbestätigung
|
||||||
|
- Gesendet nach erfolgreicher Zahlung
|
||||||
|
- Enthält Bestelldetails, Produkte und Preise
|
||||||
|
- Template: `order_confirmation.html/txt`
|
||||||
|
|
||||||
|
#### Status-Updates
|
||||||
|
- Gesendet bei Statusänderungen der Bestellung
|
||||||
|
- Kann Fortschrittsbilder und Beschreibungen enthalten
|
||||||
|
- Template: `order_status_update.html/txt`
|
||||||
|
|
||||||
|
#### Versandbestätigung
|
||||||
|
- Gesendet wenn Bestellung versendet wurde
|
||||||
|
- Enthält Tracking-Nummer und Versanddetails
|
||||||
|
- Template: `shipping_confirmation.html/txt`
|
||||||
|
|
||||||
|
### 2. Admin-Benachrichtigungen
|
||||||
|
|
||||||
|
#### Neue Bestellung
|
||||||
|
- Bei jeder neuen Bestellung
|
||||||
|
- Spezielle Markierung für Fursuit-Bestellungen
|
||||||
|
- Template: `admin_notification.html/txt`
|
||||||
|
|
||||||
|
#### Zahlungsfehler
|
||||||
|
- Bei fehlgeschlagenen Zahlungen
|
||||||
|
- Enthält detaillierte Fehlerinformationen
|
||||||
|
- Template: `admin_notification.html/txt`
|
||||||
|
|
||||||
|
#### Custom Design
|
||||||
|
- Bei Bestellungen mit Custom Designs
|
||||||
|
- Enthält Design-Dateien und Notizen
|
||||||
|
- Template: `admin_notification.html/txt`
|
||||||
|
|
||||||
|
#### Niedriger Lagerbestand
|
||||||
|
- Bei Unterschreitung des Schwellenwerts
|
||||||
|
- Enthält Produktdetails und aktuellen Bestand
|
||||||
|
- Template: `low_stock_notification.html/txt`
|
||||||
|
|
||||||
|
## Signal-Handler
|
||||||
|
|
||||||
|
Das System verwendet Django-Signals für automatische Benachrichtigungen:
|
||||||
|
|
||||||
|
### Order-Signals
|
||||||
|
```python
|
||||||
|
@receiver(post_save, sender=Order)
|
||||||
|
def handle_order_notifications(sender, instance, created, **kwargs):
|
||||||
|
# Sendet Benachrichtigungen bei neuen Bestellungen und Statusänderungen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Payment-Signals
|
||||||
|
```python
|
||||||
|
@receiver(post_save, sender=PaymentError)
|
||||||
|
def handle_payment_error(sender, instance, created, **kwargs):
|
||||||
|
# Benachrichtigt Admins über Zahlungsfehler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Product-Signals
|
||||||
|
```python
|
||||||
|
@receiver(pre_save, sender=Product)
|
||||||
|
def check_stock_level(sender, instance, **kwargs):
|
||||||
|
# Überprüft Lagerbestand und sendet Warnungen
|
||||||
|
```
|
||||||
|
|
||||||
|
## E-Mail-Templates
|
||||||
|
|
||||||
|
Alle E-Mail-Templates:
|
||||||
|
- Sind vollständig responsive
|
||||||
|
- Unterstützen HTML und Text-Alternativen
|
||||||
|
- Sind mehrsprachig (DE/EN)
|
||||||
|
- Verwenden einheitliches Branding
|
||||||
|
|
||||||
|
### Template-Struktur
|
||||||
|
```
|
||||||
|
shop/templates/shop/emails/
|
||||||
|
├── order_confirmation.html
|
||||||
|
├── order_confirmation.txt
|
||||||
|
├── order_status_update.html
|
||||||
|
├── order_status_update.txt
|
||||||
|
├── shipping_confirmation.html
|
||||||
|
├── shipping_confirmation.txt
|
||||||
|
├── admin_notification.html
|
||||||
|
├── admin_notification.txt
|
||||||
|
├── low_stock_notification.html
|
||||||
|
└── low_stock_notification.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
|
||||||
|
- TLS-Verschlüsselung für E-Mail-Versand
|
||||||
|
- Keine sensiblen Daten in E-Mails
|
||||||
|
- Sichere Links zu Admin-Bereich
|
||||||
|
- App-spezifische Passwörter für SMTP
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
- Robuste Exception-Handling
|
||||||
|
- Logging von Versandfehlern
|
||||||
|
- Vermeidung von Doppel-Benachrichtigungen
|
||||||
|
- Fallback auf Text-Version bei HTML-Problemen
|
||||||
|
|
||||||
|
## Wartung
|
||||||
|
|
||||||
|
### Neue E-Mail-Typen hinzufügen
|
||||||
|
|
||||||
|
1. Templates erstellen (HTML und Text)
|
||||||
|
2. E-Mail-Funktion in `emails.py` hinzufügen
|
||||||
|
3. Signal-Handler in `signals.py` registrieren
|
||||||
|
4. Übersetzungen in `.po`-Dateien hinzufügen
|
||||||
|
|
||||||
|
### Template-Anpassung
|
||||||
|
|
||||||
|
- CSS-Styles in Template-Header
|
||||||
|
- Einheitliche Farbcodes und Abstände
|
||||||
|
- Bootstrap-kompatible Klassen
|
||||||
|
- Responsive Breakpoints
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def add_test_data():
|
||||||
|
conn = sqlite3.connect('shop.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Lösche vorhandene Daten
|
||||||
|
cursor.execute("DELETE FROM products")
|
||||||
|
|
||||||
|
# Testprodukte
|
||||||
|
products = [
|
||||||
|
("Gaming Maus", "Hochwertige Gaming-Maus mit RGB-Beleuchtung", 49.99, 10),
|
||||||
|
("Mechanische Tastatur", "Mechanische Gaming-Tastatur mit blauen Switches", 89.99, 5),
|
||||||
|
("Gaming Headset", "7.1 Surround Sound Gaming Headset", 79.99, 8),
|
||||||
|
("Mousepad XL", "Extra großes Gaming-Mousepad", 19.99, 15),
|
||||||
|
("Webcam HD", "1080p Webcam für Streaming", 59.99, 3)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Füge Produkte ein
|
||||||
|
cursor.executemany(
|
||||||
|
"INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
|
||||||
|
products
|
||||||
|
)
|
||||||
|
|
||||||
|
# Speichere Änderungen
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Testdaten wurden erfolgreich hinzugefügt!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
add_test_data()
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_db():
|
||||||
|
conn = sqlite3.connect('shop.db')
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with get_db() as conn:
|
||||||
|
# Tabelle erstellen
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price REAL NOT NULL,
|
||||||
|
stock INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Beispieldaten einfügen
|
||||||
|
products = [
|
||||||
|
("Laptop", "Leistungsstarker Laptop für Arbeit und Gaming", 999.99, 5),
|
||||||
|
("Smartphone", "Neuestes Modell mit Top-Kamera", 699.99, 10),
|
||||||
|
("Kopfhörer", "Kabellose Kopfhörer mit Noise-Cancelling", 199.99, 15),
|
||||||
|
("Tablet", "Perfekt für Unterhaltung und Produktivität", 449.99, 8),
|
||||||
|
("Smartwatch", "Fitness-Tracking und Benachrichtigungen", 299.99, 12)
|
||||||
|
]
|
||||||
|
|
||||||
|
conn.execute("DELETE FROM products") # Alte Daten löschen
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
|
||||||
|
products
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_db()
|
||||||
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webshop.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import PayPalConfig
|
||||||
|
|
||||||
|
@admin.register(PayPalConfig)
|
||||||
|
class PayPalConfigAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('get_mode', 'client_id', 'is_sandbox', 'updated_at')
|
||||||
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
fieldsets = (
|
||||||
|
('API Konfiguration', {
|
||||||
|
'fields': ('client_id', 'client_secret', 'is_sandbox')
|
||||||
|
}),
|
||||||
|
('Zeitstempel', {
|
||||||
|
'fields': ('created_at', 'updated_at'),
|
||||||
|
'classes': ('collapse',)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
class PayPalConfig(models.Model):
|
||||||
|
client_id = models.CharField(max_length=255)
|
||||||
|
client_secret = models.CharField(max_length=255)
|
||||||
|
is_sandbox = models.BooleanField(default=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'PayPal Konfiguration'
|
||||||
|
verbose_name_plural = 'PayPal Konfigurationen'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"PayPal Config ({self.get_mode()})"
|
||||||
|
|
||||||
|
def get_mode(self):
|
||||||
|
return "Sandbox" if self.is_sandbox else "Live"
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'paypal'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('process/<int:order_id>/', views.process_payment, name='process_payment'),
|
||||||
|
path('execute/<int:order_id>/', views.execute_payment, name='execute_payment'),
|
||||||
|
path('cancel/<int:order_id>/', views.cancel_payment, name='cancel_payment'),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.conf import settings
|
||||||
|
import paypalrestsdk
|
||||||
|
from .models import PayPalConfig
|
||||||
|
from products.models import Order
|
||||||
|
|
||||||
|
def setup_paypal():
|
||||||
|
config = PayPalConfig.objects.first()
|
||||||
|
if config:
|
||||||
|
paypalrestsdk.configure({
|
||||||
|
"mode": "sandbox" if config.is_sandbox else "live",
|
||||||
|
"client_id": config.client_id,
|
||||||
|
"client_secret": config.client_secret
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def process_payment(request, order_id):
|
||||||
|
if not setup_paypal():
|
||||||
|
messages.error(request, 'PayPal ist nicht richtig konfiguriert. Bitte kontaktieren Sie den Administrator.')
|
||||||
|
return redirect('order_detail', order_id=order_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
order = Order.objects.get(id=order_id, user=request.user)
|
||||||
|
|
||||||
|
payment = paypalrestsdk.Payment({
|
||||||
|
"intent": "sale",
|
||||||
|
"payer": {
|
||||||
|
"payment_method": "paypal"
|
||||||
|
},
|
||||||
|
"redirect_urls": {
|
||||||
|
"return_url": request.build_absolute_uri(f'/paypal/execute/{order_id}/'),
|
||||||
|
"cancel_url": request.build_absolute_uri(f'/paypal/cancel/{order_id}/')
|
||||||
|
},
|
||||||
|
"transactions": [{
|
||||||
|
"amount": {
|
||||||
|
"total": str(order.total_amount),
|
||||||
|
"currency": "EUR"
|
||||||
|
},
|
||||||
|
"description": f"Bestellung #{order.id}"
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
if payment.create():
|
||||||
|
for link in payment.links:
|
||||||
|
if link.method == "REDIRECT":
|
||||||
|
return redirect(link.href)
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Fehler bei der Erstellung der PayPal-Zahlung.')
|
||||||
|
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
messages.error(request, 'Bestellung nicht gefunden.')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f'Ein Fehler ist aufgetreten: {str(e)}')
|
||||||
|
|
||||||
|
return redirect('order_detail', order_id=order_id)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def execute_payment(request, order_id):
|
||||||
|
payment_id = request.GET.get('paymentId')
|
||||||
|
payer_id = request.GET.get('PayerID')
|
||||||
|
|
||||||
|
try:
|
||||||
|
payment = paypalrestsdk.Payment.find(payment_id)
|
||||||
|
if payment.execute({"payer_id": payer_id}):
|
||||||
|
order = Order.objects.get(id=order_id, user=request.user)
|
||||||
|
order.status = 'paid'
|
||||||
|
order.payment_method = 'paypal'
|
||||||
|
order.payment_id = payment_id
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
messages.success(request, 'Zahlung erfolgreich durchgeführt!')
|
||||||
|
return redirect('payment_success', order_id=order_id)
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Fehler bei der Ausführung der Zahlung.')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f'Ein Fehler ist aufgetreten: {str(e)}')
|
||||||
|
|
||||||
|
return redirect('payment_failed', order_id=order_id)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def cancel_payment(request, order_id):
|
||||||
|
messages.info(request, 'Die PayPal-Zahlung wurde abgebrochen.')
|
||||||
|
return redirect('order_detail', order_id=order_id)
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import (
|
||||||
|
Product, Cart, CartItem, Order, OrderItem,
|
||||||
|
Review, UserProfile, FAQ, ContactMessage,
|
||||||
|
CustomOrder, OrderProgress, GalleryImage
|
||||||
|
)
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
@admin.register(Product)
|
||||||
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'price', 'stock', 'fursuit_type', 'style', 'is_featured', 'is_custom_order')
|
||||||
|
list_filter = ('fursuit_type', 'style', 'is_featured', 'is_custom_order')
|
||||||
|
search_fields = ('name', 'description')
|
||||||
|
ordering = ('-created',)
|
||||||
|
|
||||||
|
@admin.register(Order)
|
||||||
|
class OrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'full_name', 'total_amount', 'status', 'payment_status', 'created')
|
||||||
|
list_filter = ('status', 'payment_status', 'payment_method')
|
||||||
|
search_fields = ('full_name', 'email')
|
||||||
|
ordering = ('-created',)
|
||||||
|
|
||||||
|
@admin.register(OrderItem)
|
||||||
|
class OrderItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('order', 'product_name', 'quantity', 'price')
|
||||||
|
search_fields = ('product_name',)
|
||||||
|
|
||||||
|
@admin.register(Review)
|
||||||
|
class ReviewAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('product', 'user', 'rating', 'created')
|
||||||
|
list_filter = ('rating',)
|
||||||
|
search_fields = ('comment', 'user__username')
|
||||||
|
|
||||||
|
@admin.register(Cart)
|
||||||
|
class CartAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'user', 'created')
|
||||||
|
search_fields = ('user__username',)
|
||||||
|
|
||||||
|
@admin.register(CartItem)
|
||||||
|
class CartItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('cart', 'product', 'quantity')
|
||||||
|
|
||||||
|
@admin.register(UserProfile)
|
||||||
|
class UserProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'phone', 'newsletter')
|
||||||
|
list_filter = ('newsletter',)
|
||||||
|
search_fields = ('user__username', 'phone', 'address')
|
||||||
|
|
||||||
|
@admin.register(FAQ)
|
||||||
|
class FAQAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('question', 'category', 'order')
|
||||||
|
list_filter = ('category',)
|
||||||
|
search_fields = ('question', 'answer')
|
||||||
|
ordering = ('category', 'order')
|
||||||
|
|
||||||
|
@admin.register(ContactMessage)
|
||||||
|
class ContactMessageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('subject', 'name', 'email', 'category', 'status', 'created')
|
||||||
|
list_filter = ('category', 'status')
|
||||||
|
search_fields = ('name', 'email', 'subject', 'message')
|
||||||
|
ordering = ('-created',)
|
||||||
|
|
||||||
|
@admin.register(CustomOrder)
|
||||||
|
class CustomOrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('character_name', 'user', 'fursuit_type', 'status', 'created')
|
||||||
|
list_filter = ('fursuit_type', 'style', 'status')
|
||||||
|
search_fields = ('character_name', 'user__username', 'character_description')
|
||||||
|
ordering = ('-created',)
|
||||||
|
|
||||||
|
@admin.register(OrderProgress)
|
||||||
|
class OrderProgressAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('custom_order', 'stage', 'completed', 'created')
|
||||||
|
list_filter = ('stage', 'completed')
|
||||||
|
search_fields = ('custom_order__character_name', 'description')
|
||||||
|
ordering = ('custom_order', 'created')
|
||||||
|
|
||||||
|
@admin.register(GalleryImage)
|
||||||
|
class GalleryImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('title', 'fursuit_type', 'style', 'is_featured', 'order', 'created', 'admin_image')
|
||||||
|
list_filter = ('fursuit_type', 'style', 'is_featured')
|
||||||
|
search_fields = ('title', 'description')
|
||||||
|
ordering = ('order', '-created')
|
||||||
|
list_editable = ('order', 'is_featured')
|
||||||
|
readonly_fields = ('admin_image',)
|
||||||
|
|
||||||
|
def admin_image(self, obj):
|
||||||
|
if obj.image:
|
||||||
|
return mark_safe(f'<img src="{obj.image.url}" style="max-height: 50px;" />')
|
||||||
|
return "Kein Bild"
|
||||||
|
admin_image.short_description = 'Vorschau'
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProductsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'products'
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from .models import Order, Product, Review, UserProfile, ContactMessage, CustomOrder, OrderProgress
|
||||||
|
|
||||||
|
class OrderForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
fields = ['full_name', 'email', 'address', 'phone']
|
||||||
|
widgets = {
|
||||||
|
'full_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||||
|
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'full_name': 'Vollständiger Name',
|
||||||
|
'email': 'E-Mail-Adresse',
|
||||||
|
'address': 'Lieferadresse',
|
||||||
|
'phone': 'Telefonnummer',
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomUserCreationForm(UserCreationForm):
|
||||||
|
email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'}))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['username', 'email', 'password1', 'password2']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field in self.fields:
|
||||||
|
self.fields[field].widget.attrs['class'] = 'form-control'
|
||||||
|
self.fields['username'].label = 'Benutzername'
|
||||||
|
self.fields['password1'].label = 'Passwort'
|
||||||
|
self.fields['password2'].label = 'Passwort bestätigen'
|
||||||
|
|
||||||
|
class CustomAuthenticationForm(AuthenticationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field in self.fields:
|
||||||
|
self.fields[field].widget.attrs['class'] = 'form-control'
|
||||||
|
self.fields['username'].label = 'Benutzername'
|
||||||
|
self.fields['password'].label = 'Passwort'
|
||||||
|
|
||||||
|
class ReviewForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Review
|
||||||
|
fields = ['rating', 'comment']
|
||||||
|
widgets = {
|
||||||
|
'rating': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'comment': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'rating': 'Bewertung',
|
||||||
|
'comment': 'Kommentar',
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
first_name = forms.CharField(max_length=30, required=False, label='Vorname')
|
||||||
|
last_name = forms.CharField(max_length=30, required=False, label='Nachname')
|
||||||
|
email = forms.EmailField(required=True, label='E-Mail-Adresse')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserProfile
|
||||||
|
fields = ['phone', 'address', 'default_shipping_address', 'newsletter']
|
||||||
|
labels = {
|
||||||
|
'phone': 'Telefonnummer',
|
||||||
|
'address': 'Adresse',
|
||||||
|
'default_shipping_address': 'Standard-Lieferadresse',
|
||||||
|
'newsletter': 'Newsletter abonnieren',
|
||||||
|
}
|
||||||
|
widgets = {
|
||||||
|
'address': forms.Textarea(attrs={'rows': 3}),
|
||||||
|
'default_shipping_address': forms.Textarea(attrs={'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.instance.user:
|
||||||
|
self.fields['first_name'].initial = self.instance.user.first_name
|
||||||
|
self.fields['last_name'].initial = self.instance.user.last_name
|
||||||
|
self.fields['email'].initial = self.instance.user.email
|
||||||
|
for field in self.fields:
|
||||||
|
self.fields[field].widget.attrs['class'] = 'form-control'
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
profile = super().save(commit=False)
|
||||||
|
if commit:
|
||||||
|
user = profile.user
|
||||||
|
user.first_name = self.cleaned_data['first_name']
|
||||||
|
user.last_name = self.cleaned_data['last_name']
|
||||||
|
user.email = self.cleaned_data['email']
|
||||||
|
user.save()
|
||||||
|
profile.save()
|
||||||
|
return profile
|
||||||
|
|
||||||
|
class ContactForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ContactMessage
|
||||||
|
fields = ['name', 'email', 'category', 'order_number', 'subject', 'message']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||||
|
'category': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'order_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'subject': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'message': forms.Textarea(attrs={'class': 'form-control', 'rows': 5}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'name': 'Name',
|
||||||
|
'email': 'E-Mail-Adresse',
|
||||||
|
'category': 'Kategorie',
|
||||||
|
'order_number': 'Bestellnummer (optional)',
|
||||||
|
'subject': 'Betreff',
|
||||||
|
'message': 'Ihre Nachricht',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if user and user.is_authenticated:
|
||||||
|
self.fields['name'].initial = user.get_full_name() or user.username
|
||||||
|
self.fields['email'].initial = user.email
|
||||||
|
|
||||||
|
class CustomOrderForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CustomOrder
|
||||||
|
fields = [
|
||||||
|
'fursuit_type', 'style', 'character_name',
|
||||||
|
'character_description', 'reference_images',
|
||||||
|
'special_requests', 'measurements',
|
||||||
|
'color_preferences', 'budget_range',
|
||||||
|
'deadline_request'
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'fursuit_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'style': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'character_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'character_description': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 4,
|
||||||
|
'placeholder': 'Beschreiben Sie Ihren Character so detailliert wie möglich...'
|
||||||
|
}),
|
||||||
|
'special_requests': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 3,
|
||||||
|
'placeholder': 'Besondere Wünsche oder Anforderungen...'
|
||||||
|
}),
|
||||||
|
'measurements': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 4,
|
||||||
|
'placeholder': 'Bitte geben Sie alle relevanten Maße an (Kopfumfang, Körpergröße, etc.)'
|
||||||
|
}),
|
||||||
|
'color_preferences': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 3,
|
||||||
|
'placeholder': 'Beschreiben Sie die gewünschten Farben und Farbkombinationen...'
|
||||||
|
}),
|
||||||
|
'budget_range': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'z.B. 2000-3000€'
|
||||||
|
}),
|
||||||
|
'deadline_request': forms.DateInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'type': 'date'
|
||||||
|
}),
|
||||||
|
'reference_images': forms.FileInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': 'image/*'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'fursuit_type': 'Art des Fursuits',
|
||||||
|
'style': 'Gewünschter Stil',
|
||||||
|
'character_name': 'Name des Characters',
|
||||||
|
'character_description': 'Beschreibung des Characters',
|
||||||
|
'reference_images': 'Referenzbilder',
|
||||||
|
'special_requests': 'Besondere Wünsche',
|
||||||
|
'measurements': 'Maße',
|
||||||
|
'color_preferences': 'Farbwünsche',
|
||||||
|
'budget_range': 'Budget-Rahmen',
|
||||||
|
'deadline_request': 'Gewünschter Fertigstellungstermin'
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'character_description': 'Je detaillierter die Beschreibung, desto besser können wir Ihre Vision umsetzen.',
|
||||||
|
'reference_images': 'Laden Sie bis zu 5 Referenzbilder hoch (max. 5MB pro Bild)',
|
||||||
|
'measurements': 'Genaue Maße sind wichtig für eine perfekte Passform.',
|
||||||
|
'budget_range': 'Geben Sie einen Bereich an, in dem Sie sich preislich bewegen möchten.',
|
||||||
|
'deadline_request': 'Optional: Wenn Sie einen bestimmten Termin im Auge haben.'
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderProgressForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = OrderProgress
|
||||||
|
fields = ['stage', 'description', 'image', 'completed']
|
||||||
|
widgets = {
|
||||||
|
'stage': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'description': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 3,
|
||||||
|
'placeholder': 'Beschreiben Sie den aktuellen Fortschritt...'
|
||||||
|
}),
|
||||||
|
'image': forms.FileInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': 'image/*'
|
||||||
|
}),
|
||||||
|
'completed': forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'stage': 'Arbeitsschritt',
|
||||||
|
'description': 'Beschreibung des Fortschritts',
|
||||||
|
'image': 'Foto des Fortschritts',
|
||||||
|
'completed': 'Abgeschlossen'
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'description': 'Beschreiben Sie detailliert, was in diesem Schritt gemacht wurde.',
|
||||||
|
'image': 'Fügen Sie ein Foto hinzu, um den Fortschritt zu dokumentieren.',
|
||||||
|
'completed': 'Markieren Sie diesen Schritt als abgeschlossen, wenn er fertig ist.'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 14:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('stock', models.IntegerField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 14:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Cart',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('session_id', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CartItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.cart')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('cart', 'product')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 14:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0002_cart_cartitem'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='featured',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='products/'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('full_name', models.CharField(max_length=200)),
|
||||||
|
('email', models.EmailField(max_length=254)),
|
||||||
|
('address', models.TextField()),
|
||||||
|
('phone', models.CharField(max_length=20)),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'In Bearbeitung'), ('shipped', 'Versendet'), ('delivered', 'Geliefert'), ('cancelled', 'Storniert')], default='pending', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('product_name', models.CharField(max_length=200)),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('quantity', models.PositiveIntegerField()),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.order')),
|
||||||
|
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.product')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 14:25
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0003_product_category_product_featured_product_image_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Review',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('rating', models.IntegerField(choices=[(1, '1 - Sehr schlecht'), (2, '2 - Schlecht'), (3, '3 - Okay'), (4, '4 - Gut'), (5, '5 - Sehr gut')], validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])),
|
||||||
|
('comment', models.TextField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='products.product')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('product', 'user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 14:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0004_review'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=20)),
|
||||||
|
('address', models.TextField(blank=True)),
|
||||||
|
('default_shipping_address', models.TextField(blank=True)),
|
||||||
|
('newsletter', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Wishlist',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('products', models.ManyToManyField(to='products.product')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 14:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0005_userprofile_wishlist'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FAQ',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('question', models.CharField(max_length=255, verbose_name='Frage')),
|
||||||
|
('answer', models.TextField(verbose_name='Antwort')),
|
||||||
|
('category', models.CharField(max_length=100, verbose_name='Kategorie')),
|
||||||
|
('order', models.IntegerField(default=0, verbose_name='Reihenfolge')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'FAQ',
|
||||||
|
'verbose_name_plural': 'FAQs',
|
||||||
|
'ordering': ['category', 'order'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContactMessage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('email', models.EmailField(max_length=254)),
|
||||||
|
('order_number', models.CharField(blank=True, max_length=50, null=True)),
|
||||||
|
('category', models.CharField(choices=[('general', 'Allgemeine Anfrage'), ('order', 'Bestellung'), ('return', 'Rückgabe/Umtausch'), ('complaint', 'Beschwerde'), ('technical', 'Technische Frage')], max_length=20)),
|
||||||
|
('subject', models.CharField(max_length=200)),
|
||||||
|
('message', models.TextField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('status', models.CharField(choices=[('new', 'Neu'), ('in_progress', 'In Bearbeitung'), ('resolved', 'Erledigt'), ('closed', 'Geschlossen')], default='new', max_length=20)),
|
||||||
|
('staff_notes', models.TextField(blank=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Kontaktanfrage',
|
||||||
|
'verbose_name_plural': 'Kontaktanfragen',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 15:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0006_faq_contactmessage'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='extras_description',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='fursuit_type',
|
||||||
|
field=models.CharField(choices=[('fullsuit', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Kopf'), ('paws', 'Pfoten'), ('tail', 'Schwanz'), ('other', 'Sonstiges')], default='head', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='includes_extras',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='is_custom_order',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='production_time_weeks',
|
||||||
|
field=models.IntegerField(default=8),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='style',
|
||||||
|
field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistisch'), ('realistic', 'Realistisch')], default='toony', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock',
|
||||||
|
field=models.IntegerField(default=1),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomOrder',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('fursuit_type', models.CharField(choices=[('fullsuit', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Kopf'), ('paws', 'Pfoten'), ('tail', 'Schwanz'), ('other', 'Sonstiges')], max_length=20)),
|
||||||
|
('style', models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistisch'), ('realistic', 'Realistisch')], max_length=20)),
|
||||||
|
('character_name', models.CharField(max_length=100)),
|
||||||
|
('character_description', models.TextField()),
|
||||||
|
('reference_images', models.FileField(blank=True, null=True, upload_to='references/')),
|
||||||
|
('special_requests', models.TextField(blank=True)),
|
||||||
|
('measurements', models.TextField()),
|
||||||
|
('color_preferences', models.TextField()),
|
||||||
|
('budget_range', models.CharField(max_length=100)),
|
||||||
|
('deadline_request', models.DateField(blank=True, null=True)),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Anfrage eingegangen'), ('quoted', 'Angebot erstellt'), ('approved', 'Angebot akzeptiert'), ('in_progress', 'In Arbeit'), ('ready', 'Fertig zur Abholung'), ('shipped', 'Versendet'), ('completed', 'Abgeschlossen'), ('cancelled', 'Storniert')], default='pending', max_length=20)),
|
||||||
|
('quoted_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderProgress',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('stage', models.CharField(choices=[('design', 'Design & Planung'), ('base', 'Grundform'), ('fur', 'Fell'), ('details', 'Details'), ('electronics', 'Elektronik (optional)'), ('finishing', 'Finishing')], max_length=20)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='progress/')),
|
||||||
|
('completed', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('custom_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_updates', to='products.customorder')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 18:29
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0007_product_extras_description_product_fursuit_type_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='contactmessage',
|
||||||
|
options={'ordering': ['-created'], 'verbose_name': 'Kontaktanfrage', 'verbose_name_plural': 'Kontaktanfragen'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='customorder',
|
||||||
|
options={'ordering': ['-created']},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='order',
|
||||||
|
options={'ordering': ['-created']},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='orderprogress',
|
||||||
|
options={'ordering': ['created']},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='product',
|
||||||
|
options={'ordering': ['-created']},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='review',
|
||||||
|
options={'ordering': ['-created']},
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='cart',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='cart',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='cartitem',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='contactmessage',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='customorder',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='customorder',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='faq',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='faq',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='order',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='order',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='orderprogress',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='product',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='product',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='review',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='review',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='userprofile',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='userprofile',
|
||||||
|
old_name='updated_at',
|
||||||
|
new_name='updated',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='wishlist',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderprogress',
|
||||||
|
name='updated',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cart',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_carts', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='order',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_orders', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-30 07:31
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0008_alter_contactmessage_options_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='category',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='extras_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='featured',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='includes_extras',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='production_time_weeks',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='review',
|
||||||
|
name='updated',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='payment_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='payment_method',
|
||||||
|
field=models.CharField(blank=True, choices=[('card', 'Kreditkarte'), ('sepa', 'SEPA-Lastschrift'), ('giropay', 'Giropay'), ('sofort', 'Sofort'), ('bancontact', 'Bancontact')], max_length=20, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='payment_status',
|
||||||
|
field=models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'Wird bearbeitet'), ('paid', 'Bezahlt'), ('failed', 'Fehlgeschlagen'), ('refunded', 'Zurückerstattet')], default='pending', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='stripe_payment_intent_id',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='stripe_payment_method_id',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='is_featured',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='wishlist_users',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='wishlist_products', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customorder',
|
||||||
|
name='fursuit_type',
|
||||||
|
field=models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customorder',
|
||||||
|
name='style',
|
||||||
|
field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='fursuit_type',
|
||||||
|
field=models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], default='other', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='is_custom_order',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='price',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='style',
|
||||||
|
field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], default='toony', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='review',
|
||||||
|
name='rating',
|
||||||
|
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)]),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='order',
|
||||||
|
index=models.Index(fields=['status'], name='products_or_status_bd22a2_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='order',
|
||||||
|
index=models.Index(fields=['payment_status'], name='products_or_payment_0d94df_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='order',
|
||||||
|
index=models.Index(fields=['created'], name='products_or_created_a2e72d_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['name'], name='products_pr_name_9ff0a3_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['price'], name='products_pr_price_9b1a5f_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['created'], name='products_pr_created_9a1943_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['fursuit_type'], name='products_pr_fursuit_fde435_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['style'], name='products_pr_style_de3c68_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-30 07:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0009_remove_product_category_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GalleryImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=200, verbose_name='Titel')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
||||||
|
('image', models.ImageField(upload_to='gallery/', verbose_name='Bild')),
|
||||||
|
('fursuit_type', models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], max_length=20, verbose_name='Fursuit-Typ')),
|
||||||
|
('style', models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], max_length=20, verbose_name='Stil')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
||||||
|
('is_featured', models.BooleanField(default=False, verbose_name='Hervorgehoben')),
|
||||||
|
('order', models.IntegerField(default=0, verbose_name='Reihenfolge')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Galeriebild',
|
||||||
|
'verbose_name_plural': 'Galeriebilder',
|
||||||
|
'ordering': ['order', '-created'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-30 08:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0010_galleryimage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='galleryimage',
|
||||||
|
name='fursuit_type',
|
||||||
|
field=models.CharField(choices=[('full', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Head Only'), ('other', 'Other')], default='full', max_length=20, verbose_name='Fursuit-Typ'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='galleryimage',
|
||||||
|
name='style',
|
||||||
|
field=models.CharField(choices=[('toony', 'Toony'), ('semi', 'Semi-Realistic'), ('real', 'Realistic'), ('anime', 'Anime')], default='toony', max_length=20, verbose_name='Stil'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-30 11:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0011_alter_galleryimage_fursuit_type_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('slug', models.SlugField(unique=True, verbose_name='URL-Slug')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Kategorie',
|
||||||
|
'verbose_name_plural': 'Kategorien',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='products.category', verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-30 11:54
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0012_category_product_category'),
|
||||||
|
('shop', '0003_contactmessage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_items', to='shop.category', verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Category',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-06-02 06:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0013_alter_product_category_delete_category'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Payment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('variant', models.CharField(max_length=255)),
|
||||||
|
('status', models.CharField(choices=[('waiting', 'Waiting for confirmation'), ('preauth', 'Pre-authorized'), ('confirmed', 'Confirmed'), ('rejected', 'Rejected'), ('refunded', 'Refunded'), ('error', 'Error'), ('input', 'Input')], default='waiting', max_length=10)),
|
||||||
|
('fraud_status', models.CharField(choices=[('unknown', 'Unknown'), ('accept', 'Passed'), ('reject', 'Rejected'), ('review', 'Review')], default='unknown', max_length=10, verbose_name='fraud check')),
|
||||||
|
('fraud_message', models.TextField(blank=True, default='')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('modified', models.DateTimeField(auto_now=True)),
|
||||||
|
('transaction_id', models.CharField(blank=True, max_length=255)),
|
||||||
|
('currency', models.CharField(max_length=10)),
|
||||||
|
('total', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
|
||||||
|
('delivery', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
|
||||||
|
('tax', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('billing_first_name', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_last_name', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_address_1', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_address_2', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_city', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_postcode', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_country_code', models.CharField(blank=True, max_length=2)),
|
||||||
|
('billing_country_area', models.CharField(blank=True, max_length=256)),
|
||||||
|
('billing_email', models.EmailField(blank=True, max_length=254)),
|
||||||
|
('billing_phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None)),
|
||||||
|
('customer_ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||||
|
('extra_data', models.TextField(blank=True, default='')),
|
||||||
|
('message', models.TextField(blank=True, default='')),
|
||||||
|
('token', models.CharField(blank=True, default='', max_length=36)),
|
||||||
|
('captured_amount', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='products.order')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-06-02 10:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0014_payment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='payment',
|
||||||
|
options={'ordering': ['-created']},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_phone',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_address_1',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_address_2',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_city',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_country_area',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_country_code',
|
||||||
|
field=models.CharField(blank=True, max_length=2, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_email',
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_first_name',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_last_name',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='billing_postcode',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='extra_data',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='fraud_message',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='fraud_status',
|
||||||
|
field=models.CharField(blank=True, max_length=10, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='message',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='order',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.order'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(max_length=10),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='token',
|
||||||
|
field=models.CharField(blank=True, max_length=36, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='transaction_id',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,416 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from shop.models import Category # Import Category aus der shop App
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
phone = models.CharField(max_length=20, blank=True)
|
||||||
|
address = models.TextField(blank=True)
|
||||||
|
default_shipping_address = models.TextField(blank=True)
|
||||||
|
newsletter = models.BooleanField(default=False)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Profil von {self.user.username}"
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def create_user_profile(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
UserProfile.objects.create(user=instance)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def save_user_profile(sender, instance, **kwargs):
|
||||||
|
instance.userprofile.save()
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
FURSUIT_TYPE_CHOICES = [
|
||||||
|
('partial', 'Partial'),
|
||||||
|
('fullsuit', 'Fullsuit'),
|
||||||
|
('head', 'Head Only'),
|
||||||
|
('paws', 'Paws'),
|
||||||
|
('tail', 'Tail'),
|
||||||
|
('other', 'Other'),
|
||||||
|
]
|
||||||
|
|
||||||
|
STYLE_CHOICES = [
|
||||||
|
('toony', 'Toony'),
|
||||||
|
('semi_realistic', 'Semi-Realistic'),
|
||||||
|
('realistic', 'Realistic'),
|
||||||
|
('anime', 'Anime'),
|
||||||
|
('chibi', 'Chibi'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
description = models.TextField()
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
|
||||||
|
stock = models.IntegerField(default=0)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
image = models.ImageField(upload_to='products/', null=True, blank=True)
|
||||||
|
|
||||||
|
# Neue Felder
|
||||||
|
fursuit_type = models.CharField(max_length=20, choices=FURSUIT_TYPE_CHOICES, default='other')
|
||||||
|
style = models.CharField(max_length=20, choices=STYLE_CHOICES, default='toony')
|
||||||
|
is_featured = models.BooleanField(default=False)
|
||||||
|
is_custom_order = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
category = models.ForeignKey('shop.Category', on_delete=models.SET_NULL, null=True, blank=True, related_name='product_items', verbose_name='Kategorie')
|
||||||
|
wishlist_users = models.ManyToManyField(User, related_name='wishlist_products', blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def average_rating(self):
|
||||||
|
if self.reviews.exists():
|
||||||
|
return self.reviews.aggregate(models.Avg('rating'))['rating__avg']
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['name']),
|
||||||
|
models.Index(fields=['price']),
|
||||||
|
models.Index(fields=['created']),
|
||||||
|
models.Index(fields=['fursuit_type']),
|
||||||
|
models.Index(fields=['style']),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Review(models.Model):
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews')
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
rating = models.IntegerField(validators=[MinValueValidator(1)])
|
||||||
|
comment = models.TextField()
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('product', 'user')
|
||||||
|
ordering = ['-created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Review by {self.user.username} for {self.product.name}'
|
||||||
|
|
||||||
|
class Cart(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='product_carts')
|
||||||
|
session_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def get_total(self):
|
||||||
|
return sum(item.get_subtotal() for item in self.items.all())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Cart {self.id} - {'User: ' + self.user.username if self.user else 'Session: ' + self.session_id}"
|
||||||
|
|
||||||
|
class CartItem(models.Model):
|
||||||
|
cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE)
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
quantity = models.PositiveIntegerField(default=1)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def get_subtotal(self):
|
||||||
|
return self.product.price * self.quantity
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.quantity}x {self.product.name} in Cart {self.cart.id}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('cart', 'product')
|
||||||
|
|
||||||
|
class Order(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', 'Ausstehend'),
|
||||||
|
('processing', 'In Bearbeitung'),
|
||||||
|
('shipped', 'Versendet'),
|
||||||
|
('delivered', 'Geliefert'),
|
||||||
|
('cancelled', 'Storniert'),
|
||||||
|
]
|
||||||
|
|
||||||
|
PAYMENT_STATUS_CHOICES = [
|
||||||
|
('pending', 'Ausstehend'),
|
||||||
|
('processing', 'Wird bearbeitet'),
|
||||||
|
('paid', 'Bezahlt'),
|
||||||
|
('failed', 'Fehlgeschlagen'),
|
||||||
|
('refunded', 'Zurückerstattet'),
|
||||||
|
]
|
||||||
|
|
||||||
|
PAYMENT_METHOD_CHOICES = [
|
||||||
|
('card', 'Kreditkarte'),
|
||||||
|
('sepa', 'SEPA-Lastschrift'),
|
||||||
|
('giropay', 'Giropay'),
|
||||||
|
('sofort', 'Sofort'),
|
||||||
|
('bancontact', 'Bancontact'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='product_orders')
|
||||||
|
full_name = models.CharField(max_length=200)
|
||||||
|
email = models.EmailField()
|
||||||
|
address = models.TextField()
|
||||||
|
phone = models.CharField(max_length=20)
|
||||||
|
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
# Zahlungsinformationen
|
||||||
|
payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending')
|
||||||
|
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHOD_CHOICES, null=True, blank=True)
|
||||||
|
stripe_payment_intent_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
stripe_payment_method_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
payment_date = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Bestellung #{self.id} von {self.full_name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['status']),
|
||||||
|
models.Index(fields=['payment_status']),
|
||||||
|
models.Index(fields=['created']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_payment_status_display_class(self):
|
||||||
|
status_classes = {
|
||||||
|
'pending': 'warning',
|
||||||
|
'processing': 'info',
|
||||||
|
'paid': 'success',
|
||||||
|
'failed': 'danger',
|
||||||
|
'refunded': 'secondary',
|
||||||
|
}
|
||||||
|
return status_classes.get(self.payment_status, 'secondary')
|
||||||
|
|
||||||
|
def get_order_status_display_class(self):
|
||||||
|
status_classes = {
|
||||||
|
'pending': 'warning',
|
||||||
|
'processing': 'info',
|
||||||
|
'shipped': 'primary',
|
||||||
|
'delivered': 'success',
|
||||||
|
'cancelled': 'danger',
|
||||||
|
}
|
||||||
|
return status_classes.get(self.status, 'secondary')
|
||||||
|
|
||||||
|
class OrderItem(models.Model):
|
||||||
|
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
|
||||||
|
product_name = models.CharField(max_length=200)
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
def get_subtotal(self):
|
||||||
|
return self.price * self.quantity
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.quantity}x {self.product_name} in Bestellung #{self.order.id}"
|
||||||
|
|
||||||
|
class Wishlist(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
products = models.ManyToManyField(Product)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Wunschliste von {self.user.username}"
|
||||||
|
|
||||||
|
class FAQ(models.Model):
|
||||||
|
question = models.CharField(max_length=255, verbose_name='Frage')
|
||||||
|
answer = models.TextField(verbose_name='Antwort')
|
||||||
|
category = models.CharField(max_length=100, verbose_name='Kategorie')
|
||||||
|
order = models.IntegerField(default=0, verbose_name='Reihenfolge')
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['category', 'order']
|
||||||
|
verbose_name = 'FAQ'
|
||||||
|
verbose_name_plural = 'FAQs'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.question
|
||||||
|
|
||||||
|
class ContactMessage(models.Model):
|
||||||
|
CATEGORY_CHOICES = [
|
||||||
|
('general', 'Allgemeine Anfrage'),
|
||||||
|
('order', 'Bestellung'),
|
||||||
|
('return', 'Rückgabe/Umtausch'),
|
||||||
|
('complaint', 'Beschwerde'),
|
||||||
|
('technical', 'Technische Frage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
email = models.EmailField()
|
||||||
|
order_number = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
|
||||||
|
subject = models.CharField(max_length=200)
|
||||||
|
message = models.TextField()
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=[
|
||||||
|
('new', 'Neu'),
|
||||||
|
('in_progress', 'In Bearbeitung'),
|
||||||
|
('resolved', 'Erledigt'),
|
||||||
|
('closed', 'Geschlossen'),
|
||||||
|
],
|
||||||
|
default='new'
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
staff_notes = models.TextField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created']
|
||||||
|
verbose_name = 'Kontaktanfrage'
|
||||||
|
verbose_name_plural = 'Kontaktanfragen'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.category} - {self.subject} ({self.created.strftime('%d.%m.%Y')})"
|
||||||
|
|
||||||
|
class CustomOrder(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', 'Anfrage eingegangen'),
|
||||||
|
('quoted', 'Angebot erstellt'),
|
||||||
|
('approved', 'Angebot akzeptiert'),
|
||||||
|
('in_progress', 'In Arbeit'),
|
||||||
|
('ready', 'Fertig zur Abholung'),
|
||||||
|
('shipped', 'Versendet'),
|
||||||
|
('completed', 'Abgeschlossen'),
|
||||||
|
('cancelled', 'Storniert'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
fursuit_type = models.CharField(max_length=20, choices=Product.FURSUIT_TYPE_CHOICES)
|
||||||
|
style = models.CharField(max_length=20, choices=Product.STYLE_CHOICES)
|
||||||
|
character_name = models.CharField(max_length=100)
|
||||||
|
character_description = models.TextField()
|
||||||
|
reference_images = models.FileField(upload_to='references/', null=True, blank=True)
|
||||||
|
special_requests = models.TextField(blank=True)
|
||||||
|
measurements = models.TextField()
|
||||||
|
color_preferences = models.TextField()
|
||||||
|
budget_range = models.CharField(max_length=100)
|
||||||
|
deadline_request = models.DateField(null=True, blank=True)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
quoted_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Custom Order #{self.id} - {self.character_name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created']
|
||||||
|
|
||||||
|
class OrderProgress(models.Model):
|
||||||
|
PROGRESS_CHOICES = [
|
||||||
|
('design', 'Design & Planung'),
|
||||||
|
('base', 'Grundform'),
|
||||||
|
('fur', 'Fell'),
|
||||||
|
('details', 'Details'),
|
||||||
|
('electronics', 'Elektronik (optional)'),
|
||||||
|
('finishing', 'Finishing'),
|
||||||
|
]
|
||||||
|
|
||||||
|
custom_order = models.ForeignKey(CustomOrder, on_delete=models.CASCADE, related_name='progress_updates')
|
||||||
|
stage = models.CharField(max_length=20, choices=PROGRESS_CHOICES)
|
||||||
|
description = models.TextField()
|
||||||
|
image = models.ImageField(upload_to='progress/', null=True, blank=True)
|
||||||
|
completed = models.BooleanField(default=False)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.custom_order.character_name} - {self.get_stage_display()}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['created']
|
||||||
|
|
||||||
|
class GalleryImage(models.Model):
|
||||||
|
FURSUIT_TYPE_CHOICES = [
|
||||||
|
('full', 'Fullsuit'),
|
||||||
|
('partial', 'Partial'),
|
||||||
|
('head', 'Head Only'),
|
||||||
|
('other', 'Other'),
|
||||||
|
]
|
||||||
|
|
||||||
|
STYLE_CHOICES = [
|
||||||
|
('toony', 'Toony'),
|
||||||
|
('semi', 'Semi-Realistic'),
|
||||||
|
('real', 'Realistic'),
|
||||||
|
('anime', 'Anime'),
|
||||||
|
]
|
||||||
|
|
||||||
|
title = models.CharField(max_length=200, verbose_name='Titel')
|
||||||
|
description = models.TextField(blank=True, verbose_name='Beschreibung')
|
||||||
|
image = models.ImageField(upload_to='gallery/', verbose_name='Bild')
|
||||||
|
fursuit_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=FURSUIT_TYPE_CHOICES,
|
||||||
|
default='full',
|
||||||
|
verbose_name='Fursuit-Typ'
|
||||||
|
)
|
||||||
|
style = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=STYLE_CHOICES,
|
||||||
|
default='toony',
|
||||||
|
verbose_name='Stil'
|
||||||
|
)
|
||||||
|
is_featured = models.BooleanField(default=False, verbose_name='Hervorgehoben')
|
||||||
|
order = models.IntegerField(default=0, verbose_name='Reihenfolge')
|
||||||
|
created = models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Galeriebild'
|
||||||
|
verbose_name_plural = 'Galeriebilder'
|
||||||
|
ordering = ['order', '-created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Payment(models.Model):
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE)
|
||||||
|
variant = models.CharField(max_length=255)
|
||||||
|
status = models.CharField(max_length=10)
|
||||||
|
fraud_status = models.CharField(max_length=10, null=True, blank=True)
|
||||||
|
fraud_message = models.TextField(null=True, blank=True)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
transaction_id = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
currency = models.CharField(max_length=10)
|
||||||
|
total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
|
||||||
|
delivery = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
|
||||||
|
tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
billing_first_name = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_last_name = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_address_1 = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_address_2 = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_city = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_postcode = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_country_code = models.CharField(max_length=2, null=True, blank=True)
|
||||||
|
billing_country_area = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
billing_email = models.EmailField(null=True, blank=True)
|
||||||
|
customer_ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||||
|
extra_data = models.TextField(null=True, blank=True)
|
||||||
|
message = models.TextField(null=True, blank=True)
|
||||||
|
token = models.CharField(max_length=36, null=True, blank=True)
|
||||||
|
captured_amount = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Payment {self.id} for Order {self.order.id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attrs(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_failure_url(self):
|
||||||
|
return f'/payment/failed/{self.order.id}/'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return f'/payment/success/{self.order.id}/'
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import Product, Review
|
||||||
|
|
||||||
|
class ReviewSerializer(serializers.ModelSerializer):
|
||||||
|
user = serializers.ReadOnlyField(source='user.username')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Review
|
||||||
|
fields = ['id', 'user', 'rating', 'comment', 'created_at', 'product']
|
||||||
|
read_only_fields = ['created_at']
|
||||||
|
|
||||||
|
class ProductSerializer(serializers.ModelSerializer):
|
||||||
|
average_rating = serializers.FloatField(read_only=True)
|
||||||
|
reviews = ReviewSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = ['id', 'name', 'description', 'price', 'stock', 'category',
|
||||||
|
'featured', 'image', 'created_at', 'average_rating', 'reviews']
|
||||||
|
read_only_fields = ['created_at']
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Unser Shop{% endblock %}</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #40E0D0 0%, #9370DB 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: rgba(20, 147, 140, 0.95);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.navbar-turquoise {
|
||||||
|
background: rgba(147, 112, 219, 0.95) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.navbar-turquoise .navbar-brand,
|
||||||
|
.navbar-turquoise .nav-link {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.navbar-turquoise .nav-link:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
background: rgba(168, 140, 226, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.navbar-turquoise .navbar-toggler {
|
||||||
|
border-color: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
.navbar-turquoise .navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
background: rgba(147, 112, 219, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.dropdown-menu .dropdown-item {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.dropdown-menu .dropdown-item:hover {
|
||||||
|
background-color: rgba(168, 140, 226, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.dropdown-menu .dropdown-divider {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.cart-icon-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
}
|
||||||
|
.cart-icon-container .bi-handbag-fill {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.cart-icon-container .bi-paw-fill {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transform: translateY(0.1rem);
|
||||||
|
}
|
||||||
|
.navbar-turquoise .cart-icon-container .bi-handbag-fill,
|
||||||
|
.navbar-turquoise .cart-icon-container .bi-paw-fill {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.navbar-turquoise .cart-icon-container .bi-paw-fill {
|
||||||
|
filter: drop-shadow(0px 0px 1px rgba(0,0,0,0.2));
|
||||||
|
}
|
||||||
|
.navbar-turquoise .nav-link:hover .cart-icon-container .bi-handbag-fill,
|
||||||
|
.navbar-turquoise .nav-link:hover .cart-icon-container .bi-paw-fill {
|
||||||
|
color: #e0f7f5;
|
||||||
|
}
|
||||||
|
/* Anpassungen für den Footer */
|
||||||
|
footer {
|
||||||
|
background: rgba(147, 112, 219, 0.95) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
margin-top: 2rem !important;
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
footer.bg-dark {
|
||||||
|
background: rgba(102, 51, 153, 0.95) !important;
|
||||||
|
}
|
||||||
|
footer .text-light {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
footer a.text-light:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer .list-unstyled a {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
footer .list-unstyled a:hover {
|
||||||
|
background: rgba(168, 140, 226, 0.3);
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
background: rgba(20, 147, 140, 0.95);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
background: rgba(20, 147, 140, 0.95);
|
||||||
|
}
|
||||||
|
.alert a {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.alert a:hover {
|
||||||
|
color: #e0f7f5;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: rgba(20, 147, 140, 0.95);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.table thead th {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.table td, .table th {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.form-control {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.form-control:focus {
|
||||||
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.btn-outline-secondary {
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.btn-outline-secondary:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
/* Verbesserte vertikale Ausrichtung für den Navigationslink */
|
||||||
|
.nav-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
/* Icon-spezifische Styles */
|
||||||
|
.bi {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
/* Fallback für den Fall, dass das Icon nicht geladen wird */
|
||||||
|
.bi-paw-fill::before {
|
||||||
|
content: "🐾";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-turquoise mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'products:product_list' %}">Unser Shop</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'product_list' %}active{% endif %}"
|
||||||
|
href="{% url 'products:product_list' %}">Produkte</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'gallery' %}active{% endif %}"
|
||||||
|
href="{% url 'products:gallery' %}">Fursuit Galerie</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'custom_order' %}active{% endif %}"
|
||||||
|
href="{% url 'products:custom_order' %}">Custom Order</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'products:cart_detail' %}">
|
||||||
|
<span class="cart-icon-container">
|
||||||
|
<i class="bi bi-handbag-fill"></i>
|
||||||
|
<i class="bi bi-paw-fill"></i>
|
||||||
|
</span>
|
||||||
|
Warenkorb
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ user.username }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'products:profile' %}">Profil</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'products:order_history' %}">Bestellungen</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'products:wishlist' %}">Wunschliste</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'logout' %}">Abmelden</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'login' %}">Anmelden</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'products:register' %}">Registrieren</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-light py-4">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5>Über uns</h5>
|
||||||
|
<p>Ihr vertrauenswürdiger Online-Shop für hochwertige Produkte.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5>Kontakt</h5>
|
||||||
|
<p>E-Mail: info@shop.de<br>
|
||||||
|
Tel: +49 123 456789</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5>Links</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><a href="{% url 'products:faq' %}" class="text-light">FAQ</a></li>
|
||||||
|
<li><a href="{% url 'products:contact' %}" class="text-light">Kontakt</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col text-center">
|
||||||
|
<small>© {% now "Y" %} Unser Shop. Alle Rechte vorbehalten.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap Bundle with Popper -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Fortschritt hinzufügen - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
Fortschritts-Update für {{ order.character_name }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.stage.id_for_label }}" class="form-label">
|
||||||
|
{{ form.stage.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.stage }}
|
||||||
|
{% if form.stage.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.stage.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
|
{{ form.description.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.description }}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.description.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.image.id_for_label }}" class="form-label">
|
||||||
|
{{ form.image.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.image }}
|
||||||
|
{% if form.image.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.image.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.completed }}
|
||||||
|
<label class="form-check-label" for="{{ form.completed.id_for_label }}">
|
||||||
|
{{ form.completed.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% if form.completed.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.completed.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'custom_order_detail' order.id %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Zurück
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Update speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Warenkorb - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="mb-4">Ihr Warenkorb</h1>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if cart.items.all %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Produkt</th>
|
||||||
|
<th>Preis pro Stück</th>
|
||||||
|
<th>Menge</th>
|
||||||
|
<th>Summe</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in cart.items.all %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'products:product_detail' item.product.id %}">{{ item.product.name }}</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.product.price }} €</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{% url 'products:update_cart_item' item.id %}" class="d-flex align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" max="{{ item.product.stock }}"
|
||||||
|
class="form-control form-control-sm" style="width: 70px;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-secondary ms-2">Aktualisieren</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.get_subtotal }} €</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{% url 'products:remove_from_cart' item.id %}" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-end"><strong>Gesamtsumme:</strong></td>
|
||||||
|
<td><strong>{{ cart.get_total }} €</strong></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 d-flex justify-content-between">
|
||||||
|
<a href="{% url 'products:product_list' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Weiter einkaufen
|
||||||
|
</a>
|
||||||
|
<form method="post" action="{% url 'products:create_order' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
Zur Kasse <i class="bi bi-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Ihr Warenkorb ist leer. <a href="{% url 'products:product_list' %}">Jetzt einkaufen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
{% extends 'shop/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Kasse - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.checkout-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-options {
|
||||||
|
margin-top: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-info {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<h1 class="mb-4">Checkout</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Bestellübersicht</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Artikel</th>
|
||||||
|
<th>Menge</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in order.items.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.product_name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.price }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><strong>Gesamtsumme:</strong></td>
|
||||||
|
<td><strong>{{ order.total_amount }} €</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Zahlungsmethoden</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'products/payment_button.html' with form=form order=order %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Kontakt - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h1 class="h4 mb-0">Kontaktieren Sie uns</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Kontaktinformationen -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5><i class="bi bi-envelope"></i> E-Mail</h5>
|
||||||
|
<p>info@shop.de</p>
|
||||||
|
|
||||||
|
<h5><i class="bi bi-telephone"></i> Telefon</h5>
|
||||||
|
<p>+49 123 456789</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5><i class="bi bi-clock"></i> Geschäftszeiten</h5>
|
||||||
|
<p>Mo-Fr: 9:00 - 18:00 Uhr<br>
|
||||||
|
Sa: 10:00 - 14:00 Uhr</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kontaktformular -->
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="{{ form.name.id_for_label }}" class="form-label">{{ form.name.label }}</label>
|
||||||
|
{{ form.name }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.name.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="{{ form.email.id_for_label }}" class="form-label">{{ form.email.label }}</label>
|
||||||
|
{{ form.email }}
|
||||||
|
{% if form.email.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.email.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="{{ form.category.id_for_label }}" class="form-label">{{ form.category.label }}</label>
|
||||||
|
{{ form.category }}
|
||||||
|
{% if form.category.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.category.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="{{ form.order_number.id_for_label }}" class="form-label">{{ form.order_number.label }}</label>
|
||||||
|
{{ form.order_number }}
|
||||||
|
{% if form.order_number.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.order_number.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.subject.id_for_label }}" class="form-label">{{ form.subject.label }}</label>
|
||||||
|
{{ form.subject }}
|
||||||
|
{% if form.subject.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.subject.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.message.id_for_label }}" class="form-label">{{ form.message.label }}</label>
|
||||||
|
{{ form.message }}
|
||||||
|
{% if form.message.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.message.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-send"></i> Nachricht senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ-Link -->
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p>Haben Sie eine allgemeine Frage?</p>
|
||||||
|
<a href="{% url 'products:faq' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-question-circle"></i> FAQ ansehen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Nachricht gesendet - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 text-center">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<i class="bi bi-check-circle text-success" style="font-size: 4rem;"></i>
|
||||||
|
<h1 class="h3 mt-3">Vielen Dank für Ihre Nachricht!</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Wir haben Ihre Anfrage erhalten und werden uns schnellstmöglich bei Ihnen melden.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
Unsere durchschnittliche Antwortzeit beträgt 24-48 Stunden an Werktagen.
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'products:product_list' %}" class="btn btn-primary me-2">
|
||||||
|
<i class="bi bi-shop"></i> Zurück zum Shop
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:faq' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-question-circle"></i> FAQ ansehen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Custom Fursuit Bestellung - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 200px; height: auto;">
|
||||||
|
<h1 class="display-5 fw-bold mb-4">Custom Fursuit Anfrage</h1>
|
||||||
|
<p class="lead">Lassen Sie Ihren Traum-Fursuit Realität werden</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Informationen -->
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="furry-card h-100">
|
||||||
|
<div class="furry-icon mb-4">
|
||||||
|
<i class="fas fa-magic"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-4">Bestellprozess</h4>
|
||||||
|
<ol class="list-group list-group-numbered mb-4">
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-file-alt me-2 text-primary"></i> Formular ausfüllen
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-calculator me-2 text-primary"></i> Angebot erhalten
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-money-bill-wave me-2 text-primary"></i> Anzahlung leisten
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-pencil-alt me-2 text-primary"></i> Design-Abstimmung
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-tools me-2 text-primary"></i> Produktion & Updates
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-shipping-fast me-2 text-primary"></i> Fertigstellung & Versand
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mb-3">Wichtige Hinweise</h5>
|
||||||
|
<ul class="list-group mb-4">
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-clock me-2 text-primary"></i> Produktionszeit: 8-12 Wochen
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-percentage me-2 text-primary"></i> Anzahlung: 30%
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 bg-transparent">
|
||||||
|
<i class="fas fa-ruler me-2 text-primary"></i> Genaue Maße erforderlich
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{% url 'products:gallery' %}" class="btn btn-light">
|
||||||
|
<i class="fas fa-images me-2"></i> Beispielarbeiten ansehen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="furry-card">
|
||||||
|
<form method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Character Informationen -->
|
||||||
|
<h5 class="mb-4">Character Informationen</h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="character_name" class="form-label">Character Name</label>
|
||||||
|
<input type="text" class="form-control" id="character_name" name="character_name" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="fursuit_type" class="form-label">Fursuit Typ</label>
|
||||||
|
<select class="form-select" id="fursuit_type" name="fursuit_type" required>
|
||||||
|
<option value="">Bitte wählen...</option>
|
||||||
|
<option value="fullsuit">Fullsuit</option>
|
||||||
|
<option value="partial">Partial</option>
|
||||||
|
<option value="head">Nur Kopf</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Design Präferenzen -->
|
||||||
|
<h5 class="mb-4">Design Präferenzen</h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="character_description" class="form-label">Character Beschreibung</label>
|
||||||
|
<textarea class="form-control" id="character_description" name="character_description" rows="4" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="style" class="form-label">Gewünschter Stil</label>
|
||||||
|
<select class="form-select" id="style" name="style" required>
|
||||||
|
<option value="">Bitte wählen...</option>
|
||||||
|
<option value="toony">Toony</option>
|
||||||
|
<option value="semi_realistic">Semi-Realistic</option>
|
||||||
|
<option value="realistic">Realistic</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="budget_range" class="form-label">Budget</label>
|
||||||
|
<select class="form-select" id="budget_range" name="budget_range" required>
|
||||||
|
<option value="">Bitte wählen...</option>
|
||||||
|
<option value="2000-3000">2.000€ - 3.000€</option>
|
||||||
|
<option value="3000-4000">3.000€ - 4.000€</option>
|
||||||
|
<option value="4000+">4.000€+</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zusätzliche Details -->
|
||||||
|
<h5 class="mb-4">Zusätzliche Details</h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="color_preferences" class="form-label">Farbwünsche</label>
|
||||||
|
<textarea class="form-control" id="color_preferences" name="color_preferences" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="special_requests" class="form-label">Besondere Wünsche</label>
|
||||||
|
<textarea class="form-control" id="special_requests" name="special_requests" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="reference_images" class="form-label">Referenzbilder</label>
|
||||||
|
<input type="file" class="form-control" id="reference_images" name="reference_images" accept="image/*" multiple>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="deadline_request" class="form-label">Gewünschter Fertigstellungstermin</label>
|
||||||
|
<input type="date" class="form-control" id="deadline_request" name="deadline_request">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i> Anfrage absenden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6; /* Helles Lila */
|
||||||
|
--secondary-color: #EC4899; /* Pink */
|
||||||
|
--accent-color: #F59E0B; /* Orange */
|
||||||
|
--dark-color: #1F2937; /* Dunkelgrau */
|
||||||
|
--light-color: #F3E8FF; /* Helles Lila */
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--white-color);
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
padding: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-light {
|
||||||
|
background: var(--white-color);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-light:hover {
|
||||||
|
background: var(--light-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Form Validation
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
Array.prototype.slice.call(forms).forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Custom Order #{{ order.id }} - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Order Details -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">Anfrage-Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
<span class="badge bg-{{ order.status }}">
|
||||||
|
{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Character:</strong>
|
||||||
|
{{ order.character_name }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Typ:</strong>
|
||||||
|
{{ order.get_fursuit_type_display }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Stil:</strong>
|
||||||
|
{{ order.get_style_display }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Budget:</strong>
|
||||||
|
{{ order.budget_range }}
|
||||||
|
</li>
|
||||||
|
{% if order.deadline_request %}
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Gewünschter Termin:</strong>
|
||||||
|
{{ order.deadline_request }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong>Bestelldatum:</strong>
|
||||||
|
{{ order.created|date:"d.m.Y" }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if order.quoted_price %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">Angebot</h6>
|
||||||
|
<p class="mb-0">{{ order.quoted_price }} €</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Character Details -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Character-Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Beschreibung</h6>
|
||||||
|
<p>{{ order.character_description }}</p>
|
||||||
|
|
||||||
|
{% if order.reference_images %}
|
||||||
|
<h6>Referenzbilder</h6>
|
||||||
|
<img src="{{ order.reference_images.url }}" class="img-fluid rounded" alt="Referenzbild">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h6 class="mt-3">Farbwünsche</h6>
|
||||||
|
<p>{{ order.color_preferences }}</p>
|
||||||
|
|
||||||
|
{% if order.special_requests %}
|
||||||
|
<h6>Besondere Wünsche</h6>
|
||||||
|
<p>{{ order.special_requests }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Timeline -->
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">Fortschritt</h5>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<a href="{% url 'add_progress_update' order.id %}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-plus"></i> Update hinzufügen
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if progress_updates %}
|
||||||
|
<div class="timeline">
|
||||||
|
{% for update in progress_updates %}
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker {% if update.completed %}bg-success{% endif %}"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h6 class="mb-2">
|
||||||
|
{{ update.get_stage_display }}
|
||||||
|
{% if update.completed %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
{% endif %}
|
||||||
|
</h6>
|
||||||
|
<p>{{ update.description }}</p>
|
||||||
|
{% if update.image %}
|
||||||
|
<img src="{{ update.image.url }}" class="img-fluid rounded mb-2" alt="Fortschrittsbild">
|
||||||
|
{% endif %}
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ update.created|date:"d.m.Y H:i" }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-clock text-muted" style="font-size: 2rem;"></i>
|
||||||
|
<p class="mt-2">Noch keine Fortschritts-Updates vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 15px;
|
||||||
|
height: 100%;
|
||||||
|
width: 2px;
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-marker {
|
||||||
|
position: absolute;
|
||||||
|
left: 7px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #dee2e6;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 0 2px #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-pending { background-color: #ffc107; }
|
||||||
|
.badge.bg-quoted { background-color: #17a2b8; }
|
||||||
|
.badge.bg-approved { background-color: #28a745; }
|
||||||
|
.badge.bg-in_progress { background-color: #007bff; }
|
||||||
|
.badge.bg-ready { background-color: #20c997; }
|
||||||
|
.badge.bg-shipped { background-color: #6f42c1; }
|
||||||
|
.badge.bg-completed { background-color: #28a745; }
|
||||||
|
.badge.bg-cancelled { background-color: #dc3545; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Anfrage erfolgreich - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 text-center">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
|
||||||
|
<h1 class="h3 mt-3">Ihre Fursuit-Anfrage wurde erfolgreich übermittelt!</h1>
|
||||||
|
<p class="lead mb-4">
|
||||||
|
Vielen Dank für Ihr Interesse an einem Custom Fursuit.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Ihre Anfrage-Details</h5>
|
||||||
|
<ul class="list-unstyled text-start">
|
||||||
|
<li><strong>Anfrage-ID:</strong> #{{ order.id }}</li>
|
||||||
|
<li><strong>Character:</strong> {{ order.character_name }}</li>
|
||||||
|
<li><strong>Typ:</strong> {{ order.get_fursuit_type_display }}</li>
|
||||||
|
<li><strong>Stil:</strong> {{ order.get_style_display }}</li>
|
||||||
|
<li><strong>Status:</strong> {{ order.get_status_display }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Wir werden Ihre Anfrage prüfen und uns innerhalb von 2-3 Werktagen mit einem detaillierten Angebot bei Ihnen melden.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'custom_order_detail' order.id %}" class="btn btn-primary me-2">
|
||||||
|
<i class="bi bi-eye"></i> Anfrage ansehen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'gallery' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-images"></i> Galerie ansehen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Mein Dashboard - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<!-- Willkommensbereich -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card bg-primary text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="card-title mb-0">Willkommen, {{ user.get_full_name|default:user.username }}!</h2>
|
||||||
|
<p class="mb-0">Hier finden Sie alle wichtigen Informationen zu Ihrem Account.</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<p class="mb-0"><strong>Mitglied seit:</strong> {{ user.date_joined|date:"d.m.Y" }}</p>
|
||||||
|
<p class="mb-0"><strong>Letzte Anmeldung:</strong> {{ user.last_login|date:"d.m.Y H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistik-Karten -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-info text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Bestellungen</h6>
|
||||||
|
<h2 class="mb-0">{{ stats.total_orders }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-success text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Custom Orders</h6>
|
||||||
|
<h2 class="mb-0">{{ stats.total_custom_orders }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-warning text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Bewertungen</h6>
|
||||||
|
<h2 class="mb-0">{{ stats.total_reviews }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stats-card bg-danger text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Wunschliste</h6>
|
||||||
|
<h2 class="mb-0">{{ stats.wishlist_count }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Linke Spalte -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<!-- Profil-Übersicht -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">Profil</h5>
|
||||||
|
<a href="{% url 'products:profile' %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="avatar-placeholder bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 64px; height: 64px;">
|
||||||
|
<i class="bi bi-person" style="font-size: 2rem;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ms-3">
|
||||||
|
<h6 class="mb-0">{{ user.get_full_name }}</h6>
|
||||||
|
<p class="text-muted mb-0">{{ user.email }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-telephone"></i> {{ user_profile.phone|default:"Keine Telefonnummer" }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-geo-alt"></i> {{ user_profile.address|default:"Keine Adresse"|linebreaksbr }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-envelope"></i> Newsletter:
|
||||||
|
{% if user_profile.newsletter %}
|
||||||
|
<span class="badge bg-success">Aktiviert</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Deaktiviert</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Links -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Schnellzugriff</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a href="{% url 'products:custom_order' %}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-plus-circle"></i> Neue Custom Order
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:wishlist' %}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-heart"></i> Wunschliste
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ wishlist.products.count }}</span>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:cart_detail' %}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-cart"></i> Warenkorb
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ cart.items.count }}</span>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:contact' %}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-chat"></i> Support kontaktieren
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Bewertungen -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Meine letzten Bewertungen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if reviews %}
|
||||||
|
{% for review in reviews %}
|
||||||
|
<div class="mb-3 {% if not forloop.last %}border-bottom pb-3{% endif %}">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<h6 class="mb-1">{{ review.product.name }}</h6>
|
||||||
|
<div class="text-warning">
|
||||||
|
{% for i in "12345"|make_list %}
|
||||||
|
{% if forloop.counter <= review.rating %}
|
||||||
|
<i class="bi bi-star-fill"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-star"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-1">{{ review.created|date:"d.m.Y" }}</p>
|
||||||
|
<p class="mb-0">{{ review.comment|truncatechars:100 }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">Sie haben noch keine Bewertungen abgegeben.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechte Spalte -->
|
||||||
|
<div class="col-md-8">
|
||||||
|
<!-- Custom Orders -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">Meine Custom Orders</h5>
|
||||||
|
<a href="{% url 'products:custom_order' %}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-plus"></i> Neue Anfrage
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if custom_orders %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Character</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Fortschritt</th>
|
||||||
|
<th>Erstellt am</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in custom_orders %}
|
||||||
|
<tr>
|
||||||
|
<td>#{{ order.id }}</td>
|
||||||
|
<td>{{ order.character_name }}</td>
|
||||||
|
<td>{{ order.get_fursuit_type_display }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge status-badge bg-{{ order.status }}">
|
||||||
|
{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="width: 200px;">
|
||||||
|
<div class="progress progress-bar-custom">
|
||||||
|
<div class="progress-bar" role="progressbar"
|
||||||
|
style="width: {{ order.progress_percentage }}%;"
|
||||||
|
aria-valuenow="{{ order.progress_percentage }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
{{ order.progress_percentage }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ order.created|date:"d.m.Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'products:custom_order_detail' order.id %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-emoji-smile text-muted" style="font-size: 2rem;"></i>
|
||||||
|
<p class="mt-2">Sie haben noch keine Custom Orders erstellt.</p>
|
||||||
|
<a href="{% url 'products:custom_order' %}" class="btn btn-primary">
|
||||||
|
Erste Custom Order erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Bestellungen -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">Letzte Bestellungen</h5>
|
||||||
|
<a href="{% url 'products:order_history' %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
Alle anzeigen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if recent_orders %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bestellung</th>
|
||||||
|
<th>Artikel</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in recent_orders %}
|
||||||
|
<tr>
|
||||||
|
<td>#{{ order.id }}</td>
|
||||||
|
<td>{{ order.items_count }}</td>
|
||||||
|
<td>{{ order.created|date:"d.m.Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge status-badge bg-{{ order.status }}">
|
||||||
|
{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ order.total_amount }} €</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#orderModal{{ order.id }}">
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Modal für Bestelldetails -->
|
||||||
|
<div class="modal fade" id="orderModal{{ order.id }}" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Bestellung #{{ order.id }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Lieferadresse</h6>
|
||||||
|
<p class="mb-0">
|
||||||
|
{{ order.full_name }}<br>
|
||||||
|
{{ order.address|linebreaksbr }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Kontakt</h6>
|
||||||
|
<p class="mb-0">
|
||||||
|
Email: {{ order.email }}<br>
|
||||||
|
Telefon: {{ order.phone }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6>Bestellte Artikel</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Artikel</th>
|
||||||
|
<th>Anzahl</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
<th>Gesamt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in order.items.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.product_name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.price }} €</td>
|
||||||
|
<td>{{ item.get_subtotal }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="3" class="text-end">Gesamtbetrag:</th>
|
||||||
|
<th>{{ order.total_amount }} €</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-bag text-muted" style="font-size: 2rem;"></i>
|
||||||
|
<p class="mt-2">Sie haben noch keine Bestellungen aufgegeben.</p>
|
||||||
|
<a href="{% url 'products:product_list' %}" class="btn btn-primary">
|
||||||
|
Jetzt einkaufen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}FAQ - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<h1 class="mb-4">Häufig gestellte Fragen</h1>
|
||||||
|
|
||||||
|
<!-- FAQ-Kategorien -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-outline-primary active" data-category="all">Alle</button>
|
||||||
|
{% for category in categories %}
|
||||||
|
<button class="btn btn-outline-primary" data-category="{{ category }}">{{ category }}</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ-Akkordeon -->
|
||||||
|
<div class="accordion" id="faqAccordion">
|
||||||
|
{% for faq in faqs %}
|
||||||
|
<div class="accordion-item" data-category="{{ faq.category }}">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#faq{{ forloop.counter }}">
|
||||||
|
{{ faq.question }}
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="faq{{ forloop.counter }}" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
{{ faq.answer|linebreaks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Aktuell sind keine FAQ-Einträge verfügbar.
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kontakt-Button -->
|
||||||
|
<div class="text-center mt-5">
|
||||||
|
<p>Keine Antwort auf Ihre Frage gefunden?</p>
|
||||||
|
<a href="{% url 'products:contact' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-envelope"></i> Kontaktieren Sie uns
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const categoryButtons = document.querySelectorAll('[data-category]');
|
||||||
|
const faqItems = document.querySelectorAll('.accordion-item');
|
||||||
|
|
||||||
|
categoryButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
// Aktiven Button markieren
|
||||||
|
categoryButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
button.classList.add('active');
|
||||||
|
|
||||||
|
// FAQs filtern
|
||||||
|
const selectedCategory = button.dataset.category;
|
||||||
|
faqItems.forEach(item => {
|
||||||
|
if (selectedCategory === 'all' || item.dataset.category === selectedCategory) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Galerie - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="gallery-container">
|
||||||
|
<!-- Filter-Bereich -->
|
||||||
|
<div class="filter-section mb-4">
|
||||||
|
<div class="furry-card">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fursuit_type">{% trans "Fursuit-Typ" %}</label>
|
||||||
|
<select class="form-select" id="fursuit_type" name="fursuit_type">
|
||||||
|
<option value="">{% trans "Alle Typen" %}</option>
|
||||||
|
{% for type_code, type_name in fursuit_types %}
|
||||||
|
<option value="{{ type_code }}" {% if current_type == type_code %}selected{% endif %}>
|
||||||
|
{{ type_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="style">{% trans "Style" %}</label>
|
||||||
|
<select class="form-select" id="style" name="style">
|
||||||
|
<option value="">{% trans "Alle Styles" %}</option>
|
||||||
|
{% for style_code, style_name in fursuit_styles %}
|
||||||
|
<option value="{{ style_code }}" {% if current_style == style_code %}selected{% endif %}>
|
||||||
|
{{ style_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sort">{% trans "Sortierung" %}</label>
|
||||||
|
<select class="form-select" id="sort" name="sort">
|
||||||
|
<option value="newest" {% if current_sort == 'newest' %}selected{% endif %}>
|
||||||
|
{% trans "Neueste zuerst" %}
|
||||||
|
</option>
|
||||||
|
<option value="oldest" {% if current_sort == 'oldest' %}selected{% endif %}>
|
||||||
|
{% trans "Älteste zuerst" %}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Galerie-Grid -->
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for image in images %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="gallery-card furry-card h-100">
|
||||||
|
<div class="gallery-image">
|
||||||
|
<img src="{{ image.image.url }}" alt="{{ image.title }}" class="img-fluid">
|
||||||
|
{% if image.is_featured %}
|
||||||
|
<span class="badge bg-primary position-absolute top-0 start-0 m-2">
|
||||||
|
<i class="fas fa-star me-1"></i> Featured
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="gallery-info p-3">
|
||||||
|
<h5 class="gallery-title mb-2">{{ image.title }}</h5>
|
||||||
|
<p class="gallery-description mb-3">{{ image.description }}</p>
|
||||||
|
<div class="gallery-meta">
|
||||||
|
<span class="badge bg-light text-dark me-2">
|
||||||
|
<i class="fas fa-tag me-1"></i> {{ image.get_fursuit_type_display }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-light text-dark">
|
||||||
|
<i class="fas fa-palette me-1"></i> {{ image.get_style_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info text-center">
|
||||||
|
{% trans "Keine Bilder gefunden." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.gallery-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-image {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 75%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-image img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-card:hover .gallery-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-info {
|
||||||
|
background: var(--white-color);
|
||||||
|
border-radius: 0 0 15px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-title {
|
||||||
|
font-family: 'Fredoka', sans-serif;
|
||||||
|
color: var(--dark-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-meta {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section .form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.gallery-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Filter-Handling
|
||||||
|
const filterSelects = document.querySelectorAll('select');
|
||||||
|
|
||||||
|
filterSelects.forEach(select => {
|
||||||
|
select.addEventListener('change', function() {
|
||||||
|
const fursuit_type = document.getElementById('fursuit_type').value;
|
||||||
|
const style = document.getElementById('style').value;
|
||||||
|
const sort = document.getElementById('sort').value;
|
||||||
|
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('fursuit_type', fursuit_type);
|
||||||
|
url.searchParams.set('style', style);
|
||||||
|
url.searchParams.set('sort', sort);
|
||||||
|
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Bestellung bestätigt - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h1 class="card-title mb-4">Vielen Dank für Ihre Bestellung!</h1>
|
||||||
|
<p class="lead">Ihre Bestellnummer lautet: <strong>#{{ order.id }}</strong></p>
|
||||||
|
<p>Wir haben Ihnen eine Bestätigungs-E-Mail an {{ order.email }} gesendet.</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<h4 class="alert-heading">Lieferadresse:</h4>
|
||||||
|
<p class="mb-0">
|
||||||
|
{{ order.full_name }}<br>
|
||||||
|
{{ order.address|linebreaks }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title mb-0">Bestellte Artikel</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Artikel</th>
|
||||||
|
<th>Menge</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
<th>Summe</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in order.items.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.product_name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.price }} €</td>
|
||||||
|
<td>{{ item.get_subtotal }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-end"><strong>Gesamtsumme:</strong></td>
|
||||||
|
<td><strong>{{ order.total_amount }} €</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'product_list' %}" class="btn btn-primary">Zurück zum Shop</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Bestellhistorie - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<h1 class="mb-4">Meine Bestellungen</h1>
|
||||||
|
|
||||||
|
{% if orders %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bestellnummer</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Zahlungsstatus</th>
|
||||||
|
<th>Gesamtbetrag</th>
|
||||||
|
<th>Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders %}
|
||||||
|
<tr>
|
||||||
|
<td>#{{ order.id }}</td>
|
||||||
|
<td>{{ order.created|date:"d.m.Y H:i" }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ order.get_order_status_display_class }}">
|
||||||
|
{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ order.get_payment_status_display_class }}">
|
||||||
|
{{ order.get_payment_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ order.total_amount }} €</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#orderModal{{ order.id }}">
|
||||||
|
Details anzeigen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Modal für Bestelldetails -->
|
||||||
|
<div class="modal fade" id="orderModal{{ order.id }}" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Bestellung #{{ order.id }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Lieferadresse</h6>
|
||||||
|
<p>
|
||||||
|
{{ order.full_name }}<br>
|
||||||
|
{{ order.address|linebreaksbr }}<br>
|
||||||
|
Tel: {{ order.phone }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Bestellinformationen</h6>
|
||||||
|
<p>
|
||||||
|
Datum: {{ order.created|date:"d.m.Y H:i" }}<br>
|
||||||
|
Status: {{ order.get_status_display }}<br>
|
||||||
|
Zahlungsmethode: {{ order.get_payment_method_display }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>Bestellte Artikel</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Artikel</th>
|
||||||
|
<th>Anzahl</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
<th>Gesamt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in order.items.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.product_name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.price }} €</td>
|
||||||
|
<td>{{ item.get_subtotal }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-end"><strong>Gesamtbetrag:</strong></td>
|
||||||
|
<td><strong>{{ order.total_amount }} €</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Sie haben noch keine Bestellungen aufgegeben.
|
||||||
|
<a href="{% url 'products:shop' %}" class="alert-link">Jetzt einkaufen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<div class="payment-buttons">
|
||||||
|
<!-- PayPal Button -->
|
||||||
|
{{ form.render }}
|
||||||
|
|
||||||
|
<!-- Alternative Zahlungsmethoden -->
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{% url 'products:payment_process' order.id %}" class="btn btn-primary">
|
||||||
|
Mit Kreditkarte bezahlen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.payment-buttons {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Zahlung fehlgeschlagen - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h2 class="mb-0">Zahlung fehlgeschlagen</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<i class="fas fa-times-circle text-danger" style="font-size: 4rem;"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Bestellung #{{ order.id }}</h3>
|
||||||
|
<p>Leider konnte Ihre Zahlung nicht verarbeitet werden.</p>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h4>Mögliche Gründe:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Unzureichende Deckung auf Ihrem Konto</li>
|
||||||
|
<li>Falsche oder abgelaufene Kartendaten</li>
|
||||||
|
<li>Technische Probleme bei der Zahlungsabwicklung</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Sie können die Zahlung erneut versuchen oder eine andere Zahlungsmethode wählen.</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'products:payment_process' order.id %}" class="btn btn-primary">
|
||||||
|
Zahlung erneut versuchen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:cart_detail' %}" class="btn btn-outline-primary">
|
||||||
|
Zurück zum Warenkorb
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<p>Bei weiteren Fragen kontaktieren Sie bitte unseren Kundenservice:</p>
|
||||||
|
<a href="{% url 'products:contact' %}" class="btn btn-outline-secondary">
|
||||||
|
Kontakt aufnehmen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Zahlung - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h2 class="mb-0">Zahlungsprozess</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Bestellung #{{ order.id }}</h3>
|
||||||
|
<p class="card-text">Gesamtbetrag: {{ order.total_amount }} €</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h4>Zahlungsinformationen</h4>
|
||||||
|
<p>Sie werden zu PayPal weitergeleitet, um die Zahlung sicher abzuschließen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
{{ form.render }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Zahlung erfolgreich - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h2 class="mb-0">Zahlung erfolgreich!</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<i class="fas fa-check-circle text-success" style="font-size: 4rem;"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Bestellung #{{ order.id }}</h3>
|
||||||
|
<p>Vielen Dank für Ihre Bestellung! Ihre Zahlung wurde erfolgreich verarbeitet.</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h4>Bestelldetails:</h4>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>Bestellnummer: #{{ order.id }}</li>
|
||||||
|
<li>Gesamtbetrag: {{ order.total_amount }} €</li>
|
||||||
|
<li>Bestelldatum: {{ order.created|date:"d.m.Y H:i" }}</li>
|
||||||
|
<li>Status: {{ order.get_status_display }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Sie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details zu Ihrer Bestellung.</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'products:order_history' %}" class="btn btn-primary">
|
||||||
|
Zu meinen Bestellungen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:product_list' %}" class="btn btn-outline-primary">
|
||||||
|
Weiter einkaufen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ product.name }} - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.product-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
border-radius: 15px 15px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
padding: 8px;
|
||||||
|
background: linear-gradient(45deg, #8B5CF6, #EC4899);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
cursor: zoom-in;
|
||||||
|
border-radius: 15px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #666;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-zoom-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-content {
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
margin-top: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-zoom {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 35px;
|
||||||
|
color: #f1f1f1;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviews-section {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-card {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(45deg, #8B5CF6, #6366F1);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 6px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 8px rgba(139, 92, 246, 0.3);
|
||||||
|
background: linear-gradient(45deg, #7C3AED, #4F46E5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
padding: 8px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
||||||
|
border-color: #8B5CF6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="product-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="product-details">
|
||||||
|
<h1 class="product-title">{{ product.name }}</h1>
|
||||||
|
|
||||||
|
{% if product.image %}
|
||||||
|
<div class="product-image-container">
|
||||||
|
<img src="{{ product.image.url }}"
|
||||||
|
alt="{{ product.name }}"
|
||||||
|
class="product-image"
|
||||||
|
onclick="openZoom(this.src)">
|
||||||
|
</div>
|
||||||
|
<!-- Zoom Modal -->
|
||||||
|
<div id="imageZoomModal" class="image-zoom-modal">
|
||||||
|
<span class="close-zoom" onclick="closeZoom()">×</span>
|
||||||
|
<img class="zoom-content" id="zoomedImage">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="product-description">{{ product.description }}</p>
|
||||||
|
|
||||||
|
<div class="product-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<div class="meta-label">Preis</div>
|
||||||
|
<div class="meta-value">{{ product.price }} €</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<div class="meta-label">Verfügbarkeit</div>
|
||||||
|
<div class="meta-value">{{ product.stock }} Stück</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<div class="meta-label">Hinzugefügt am</div>
|
||||||
|
<div class="meta-value">{{ product.created|date:"d.m.Y" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<div class="meta-label">Bewertung</div>
|
||||||
|
<div class="meta-value">
|
||||||
|
{% with avg_rating=product.average_rating %}
|
||||||
|
{% if avg_rating > 0 %}
|
||||||
|
{{ avg_rating|floatformat:1 }} von 5 Sternen
|
||||||
|
{% else %}
|
||||||
|
Noch keine Bewertungen
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-container">
|
||||||
|
<form method="post" action="{% url 'products:add_to_cart' product.id %}" class="d-flex align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="number" name="quantity" value="1" min="1" max="{{ product.stock }}"
|
||||||
|
class="form-control me-2" style="width: 100px;">
|
||||||
|
<button type="submit" class="btn btn-primary">In den Warenkorb</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bewertungen -->
|
||||||
|
<div class="reviews-section mt-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="mb-0">Bewertungen</h3>
|
||||||
|
{% if user.is_authenticated and not user_has_reviewed %}
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#reviewModal">
|
||||||
|
Bewertung schreiben
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if product.reviews.all %}
|
||||||
|
{% for review in product.reviews.all %}
|
||||||
|
<div class="review-card">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<h5 class="mb-1">{{ review.user.username }}</h5>
|
||||||
|
<small class="text-muted">{{ review.created|date:"d.m.Y" }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
{% for i in "12345"|make_list %}
|
||||||
|
{% if forloop.counter <= review.rating %}
|
||||||
|
<i class="bi bi-star-fill text-warning"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-star text-warning"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p class="mb-0">{{ review.comment }}</p>
|
||||||
|
</div>
|
||||||
|
{% if not forloop.last %}<hr>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center mb-0">Noch keine Bewertungen vorhanden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bewertungs-Modal -->
|
||||||
|
{% if user.is_authenticated and not user_has_reviewed %}
|
||||||
|
<div class="modal fade" id="reviewModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Bewertung schreiben</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{% url 'products:add_review' product.id %}">
|
||||||
|
<div class="modal-body">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in review_form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Bewertung absenden</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function openZoom(imgSrc) {
|
||||||
|
const modal = document.getElementById('imageZoomModal');
|
||||||
|
const zoomedImg = document.getElementById('zoomedImage');
|
||||||
|
modal.style.display = "block";
|
||||||
|
zoomedImg.src = imgSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeZoom() {
|
||||||
|
document.getElementById('imageZoomModal').style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schließen beim Klick außerhalb des Bildes
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('imageZoomModal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schließen mit Escape-Taste
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
document.getElementById('imageZoomModal').style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Produkte - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/products.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<h1 class="mb-4">Unsere Produkte</h1>
|
||||||
|
|
||||||
|
<!-- Filter-Bereich -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<!-- Suchleiste -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="search" class="form-label">Produktsuche</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="search" name="search" class="form-control"
|
||||||
|
placeholder="Produkt suchen..." value="{{ request.GET.search }}">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-search"></i> Suchen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preisbereich -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Preisbereich</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">€</span>
|
||||||
|
<input type="number" name="min_price" class="form-control"
|
||||||
|
placeholder="Min" value="{{ request.GET.min_price }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">€</span>
|
||||||
|
<input type="number" name="max_price" class="form-control"
|
||||||
|
placeholder="Max" value="{{ request.GET.max_price }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fursuit-Typ -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Fursuit-Typ</label>
|
||||||
|
<select name="fursuit_type" class="form-select">
|
||||||
|
<option value="">Alle Typen</option>
|
||||||
|
{% for value, label in fursuit_types %}
|
||||||
|
<option value="{{ value }}" {% if request.GET.fursuit_type == value %}selected{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Style -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Style</label>
|
||||||
|
<select name="style" class="form-select">
|
||||||
|
<option value="">Alle Styles</option>
|
||||||
|
{% for value, label in fursuit_styles %}
|
||||||
|
<option value="{{ value }}" {% if request.GET.style == value %}selected{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sortierung -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Sortierung</label>
|
||||||
|
<select name="sort" class="form-select">
|
||||||
|
<option value="-created" {% if request.GET.sort == '-created' %}selected{% endif %}>Neueste zuerst</option>
|
||||||
|
<option value="price" {% if request.GET.sort == 'price' %}selected{% endif %}>Preis aufsteigend</option>
|
||||||
|
<option value="-price" {% if request.GET.sort == '-price' %}selected{% endif %}>Preis absteigend</option>
|
||||||
|
<option value="name" {% if request.GET.sort == 'name' %}selected{% endif %}>Name A-Z</option>
|
||||||
|
<option value="-rating" {% if request.GET.sort == '-rating' %}selected{% endif %}>Beste Bewertung</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter-Buttons -->
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-funnel"></i> Filter anwenden
|
||||||
|
</button>
|
||||||
|
<a href="?" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Filter zurücksetzen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Featured Products -->
|
||||||
|
{% if featured_products and not request.GET.search %}
|
||||||
|
<div class="featured-section">
|
||||||
|
<h2 class="mb-4">Empfohlene Produkte</h2>
|
||||||
|
<div class="row">
|
||||||
|
{% for product in featured_products %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card product-card h-100">
|
||||||
|
<span class="featured-badge product-badge">
|
||||||
|
<i class="bi bi-star-fill"></i> Featured
|
||||||
|
</span>
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ product.name }}</h5>
|
||||||
|
<p class="card-text">{{ product.description|truncatewords:20 }}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="price-tag">{{ product.price }} €</span>
|
||||||
|
<div class="rating-stars">
|
||||||
|
{% if product.average_rating %}
|
||||||
|
{% for i in "12345"|make_list %}
|
||||||
|
<i class="bi bi-star-fill {% if forloop.counter <= product.average_rating %}text-warning{% else %}text-muted{% endif %}"></i>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% for i in "12345"|make_list %}
|
||||||
|
<i class="bi bi-star-fill text-muted"></i>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-top-0">
|
||||||
|
<a href="{% url 'products:product_detail' product.id %}" class="btn btn-primary w-100">Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Produktliste -->
|
||||||
|
{% if products %}
|
||||||
|
<div class="row">
|
||||||
|
{% for product in products %}
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card product-card h-100">
|
||||||
|
<span class="product-type-badge badge bg-primary">
|
||||||
|
{{ product.get_fursuit_type_display }}
|
||||||
|
</span>
|
||||||
|
{% if product.is_custom_order %}
|
||||||
|
<span class="product-badge badge bg-info">
|
||||||
|
Custom Order
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ product.name }}</h5>
|
||||||
|
<p class="card-text">{{ product.description|truncatewords:20 }}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="price-tag">{{ product.price }} €</span>
|
||||||
|
<div class="rating-stars">
|
||||||
|
{% if product.average_rating %}
|
||||||
|
{% for i in "12345"|make_list %}
|
||||||
|
<i class="bi bi-star-fill {% if forloop.counter <= product.average_rating %}text-warning{% else %}text-muted{% endif %}"></i>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% for i in "12345"|make_list %}
|
||||||
|
<i class="bi bi-star-fill text-muted"></i>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="ms-2 text-muted">({{ product.reviews.count|default:"0" }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">
|
||||||
|
<span class="stock-indicator {% if product.stock > 5 %}stock-high{% elif product.stock > 2 %}stock-medium{% else %}stock-low{% endif %}"></span>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if product.stock > 5 %}
|
||||||
|
Auf Lager
|
||||||
|
{% elif product.stock > 0 %}
|
||||||
|
Nur noch {{ product.stock }} verfügbar
|
||||||
|
{% else %}
|
||||||
|
Ausverkauft
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-top-0">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'products:product_detail' product.id %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-eye"></i> Details
|
||||||
|
</a>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<form method="post" action="{% url 'products:add_to_wishlist' product.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-outline-danger w-100">
|
||||||
|
<i class="bi bi-heart"></i> Zur Wunschliste
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginierung -->
|
||||||
|
{% if is_paginated %}
|
||||||
|
<nav aria-label="Seitennavigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ num }}</span>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ num }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> Keine Produkte gefunden.
|
||||||
|
{% if request.GET %}
|
||||||
|
<a href="?" class="alert-link">Filter zurücksetzen</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}Mein Profil - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Profil-Navigation -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Mein Konto</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a href="{% url 'products:profile' %}" class="list-group-item list-group-item-action active">
|
||||||
|
<i class="bi bi-person"></i> Profil
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:order_history' %}" class="list-group-item list-group-item-action">
|
||||||
|
<i class="bi bi-clock-history"></i> Bestellungen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:wishlist' %}" class="list-group-item list-group-item-action">
|
||||||
|
<i class="bi bi-heart"></i> Wunschliste
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'password_change' %}" class="list-group-item list-group-item-action">
|
||||||
|
<i class="bi bi-key"></i> Passwort ändern
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profilformular -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Profil bearbeiten</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ field.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Meine Wunschliste - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Seitennavigation -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Mein Konto</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a href="{% url 'products:profile' %}" class="list-group-item list-group-item-action">
|
||||||
|
<i class="bi bi-person"></i> Profil
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:order_history' %}" class="list-group-item list-group-item-action">
|
||||||
|
<i class="bi bi-clock-history"></i> Bestellungen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:wishlist' %}" class="list-group-item list-group-item-action active">
|
||||||
|
<i class="bi bi-heart"></i> Wunschliste
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'password_change' %}" class="list-group-item list-group-item-action">
|
||||||
|
<i class="bi bi-key"></i> Passwort ändern
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wunschliste -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<h1 class="mb-4">Meine Wunschliste</h1>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if wishlist.products.all %}
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||||
|
{% for product in wishlist.products.all %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}">
|
||||||
|
{% else %}
|
||||||
|
<img src="{% static 'images/placeholder.png' %}" class="card-img-top" alt="Placeholder">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ product.name }}</h5>
|
||||||
|
<p class="card-text">{{ product.description|truncatewords:20 }}</p>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Preis:</strong> {{ product.price }} €<br>
|
||||||
|
<strong>Typ:</strong> {{ product.get_fursuit_type_display }}<br>
|
||||||
|
<strong>Style:</strong> {{ product.get_style_display }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-top-0">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'products:product_detail' product.id %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-eye"></i> Details
|
||||||
|
</a>
|
||||||
|
<form method="post" action="{% url 'products:remove_from_wishlist' product.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-outline-danger w-100">
|
||||||
|
<i class="bi bi-heart-fill"></i> Von Wunschliste entfernen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Ihre Wunschliste ist leer.
|
||||||
|
<a href="{% url 'products:shop' %}" class="alert-link">Entdecken Sie unsere Produkte</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Registrieren - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-center mb-4">Registrieren</h2>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Registrieren</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p>Bereits ein Konto? <a href="{% url 'login' %}">Jetzt anmelden</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
from django.urls import path
|
||||||
|
from .views import (
|
||||||
|
ProductListView, ProductDetailView,
|
||||||
|
add_to_cart, cart_detail, update_cart_item,
|
||||||
|
remove_from_cart, create_order, checkout,
|
||||||
|
add_review, profile_view, order_history,
|
||||||
|
wishlist_view, add_to_wishlist, remove_from_wishlist,
|
||||||
|
faq_list, contact, contact_success,
|
||||||
|
custom_order, custom_order_success,
|
||||||
|
custom_order_detail, gallery,
|
||||||
|
add_progress_update,
|
||||||
|
payment_process,
|
||||||
|
payment_success,
|
||||||
|
payment_failed,
|
||||||
|
register
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name = 'products'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', ProductListView.as_view(), name='product_list'),
|
||||||
|
path('product/<int:pk>/', ProductDetailView.as_view(), name='product_detail'),
|
||||||
|
path('cart/add/<int:product_id>/', add_to_cart, name='add_to_cart'),
|
||||||
|
path('cart/', cart_detail, name='cart_detail'),
|
||||||
|
path('cart/update/<int:item_id>/', update_cart_item, name='update_cart_item'),
|
||||||
|
path('cart/remove/<int:item_id>/', remove_from_cart, name='remove_from_cart'),
|
||||||
|
path('order/create/', create_order, name='create_order'),
|
||||||
|
path('checkout/<int:order_id>/', checkout, name='checkout'),
|
||||||
|
path('product/<int:product_id>/review/', add_review, name='add_review'),
|
||||||
|
path('profile/', profile_view, name='profile'),
|
||||||
|
path('orders/', order_history, name='order_history'),
|
||||||
|
path('wishlist/', wishlist_view, name='wishlist'),
|
||||||
|
path('wishlist/add/<int:product_id>/', add_to_wishlist, name='add_to_wishlist'),
|
||||||
|
path('wishlist/remove/<int:product_id>/', remove_from_wishlist, name='remove_from_wishlist'),
|
||||||
|
path('faq/', faq_list, name='faq'),
|
||||||
|
path('contact/', contact, name='contact'),
|
||||||
|
path('contact/success/', contact_success, name='contact_success'),
|
||||||
|
path('custom-order/', custom_order, name='custom_order'),
|
||||||
|
path('custom-order/success/<int:order_id>/', custom_order_success, name='custom_order_success'),
|
||||||
|
path('custom-orders/<int:order_id>/', custom_order_detail, name='custom_order_detail'),
|
||||||
|
path('custom-orders/<int:order_id>/update/', add_progress_update, name='add_progress_update'),
|
||||||
|
path('gallery/', gallery, name='gallery'),
|
||||||
|
path('payment/process/<int:order_id>/', payment_process, name='payment_process'),
|
||||||
|
path('payment/success/<int:order_id>/', payment_success, name='payment_success'),
|
||||||
|
path('payment/failed/<int:order_id>/', payment_failed, name='payment_failed'),
|
||||||
|
path('register/', register, name='register'),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,808 @@
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.views.generic import ListView, DetailView
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db.models import Q, Avg, Count
|
||||||
|
from django.db import transaction
|
||||||
|
# from rest_framework import viewsets # Temporär auskommentiert
|
||||||
|
# from rest_framework.permissions import IsAuthenticatedOrReadOnly # Temporär auskommentiert
|
||||||
|
from .models import Product, Cart, CartItem, Order, OrderItem, Review, UserProfile, Wishlist, FAQ, ContactMessage, CustomOrder, OrderProgress, GalleryImage, Category
|
||||||
|
from .forms import OrderForm, CustomUserCreationForm, CustomAuthenticationForm, ReviewForm, UserProfileForm, ContactForm, CustomOrderForm, OrderProgressForm
|
||||||
|
# from .serializers import ProductSerializer, ReviewSerializer # Temporär auskommentiert
|
||||||
|
import stripe
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||||
|
import json
|
||||||
|
import paypalrestsdk
|
||||||
|
from payments import get_payment_model, RedirectNeeded
|
||||||
|
from paypal.standard.ipn.models import PayPalIPN
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from paypal.standard.forms import PayPalPaymentsForm
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
class ProductListView(ListView):
|
||||||
|
model = Product
|
||||||
|
template_name = 'products/product_list.html'
|
||||||
|
context_object_name = 'products'
|
||||||
|
paginate_by = 12
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = Product.objects.all().annotate(
|
||||||
|
average_rating=Avg('reviews__rating'),
|
||||||
|
review_count=Count('reviews')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Suchfilter
|
||||||
|
search_query = self.request.GET.get('search')
|
||||||
|
if search_query:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(name__icontains=search_query) |
|
||||||
|
Q(description__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Preisbereich Filter
|
||||||
|
min_price = self.request.GET.get('min_price')
|
||||||
|
max_price = self.request.GET.get('max_price')
|
||||||
|
if min_price:
|
||||||
|
queryset = queryset.filter(price__gte=float(min_price))
|
||||||
|
if max_price:
|
||||||
|
queryset = queryset.filter(price__lte=float(max_price))
|
||||||
|
|
||||||
|
# Fursuit-Typ Filter
|
||||||
|
fursuit_type = self.request.GET.get('fursuit_type')
|
||||||
|
if fursuit_type:
|
||||||
|
queryset = queryset.filter(fursuit_type=fursuit_type)
|
||||||
|
|
||||||
|
# Style Filter
|
||||||
|
style = self.request.GET.get('style')
|
||||||
|
if style:
|
||||||
|
queryset = queryset.filter(style=style)
|
||||||
|
|
||||||
|
# Sortierung
|
||||||
|
sort = self.request.GET.get('sort', '-created')
|
||||||
|
if sort == 'price':
|
||||||
|
queryset = queryset.order_by('price')
|
||||||
|
elif sort == '-price':
|
||||||
|
queryset = queryset.order_by('-price')
|
||||||
|
elif sort == 'name':
|
||||||
|
queryset = queryset.order_by('name')
|
||||||
|
elif sort == '-rating':
|
||||||
|
queryset = queryset.order_by('-average_rating')
|
||||||
|
else: # Default: Neueste zuerst
|
||||||
|
queryset = queryset.order_by('-created')
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Kategorien für Filter
|
||||||
|
context['categories'] = Category.objects.all()
|
||||||
|
context['selected_category'] = self.request.GET.get('category')
|
||||||
|
|
||||||
|
# Fursuit-Typen und Styles für Filter
|
||||||
|
context['fursuit_types'] = Product.FURSUIT_TYPE_CHOICES
|
||||||
|
context['fursuit_styles'] = Product.STYLE_CHOICES
|
||||||
|
|
||||||
|
# Filter-Werte
|
||||||
|
context['min_price'] = self.request.GET.get('min_price')
|
||||||
|
context['max_price'] = self.request.GET.get('max_price')
|
||||||
|
context['selected_type'] = self.request.GET.get('fursuit_type')
|
||||||
|
context['selected_style'] = self.request.GET.get('style')
|
||||||
|
context['search_query'] = self.request.GET.get('search')
|
||||||
|
context['sort'] = self.request.GET.get('sort', '-created')
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
class ProductDetailView(DetailView):
|
||||||
|
model = Product
|
||||||
|
template_name = 'products/product_detail.html'
|
||||||
|
context_object_name = 'product'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
context['user_has_reviewed'] = Review.objects.filter(
|
||||||
|
product=self.object,
|
||||||
|
user=self.request.user
|
||||||
|
).exists()
|
||||||
|
context['review_form'] = ReviewForm()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_or_create_cart(request):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
cart, created = Cart.objects.get_or_create(user=request.user)
|
||||||
|
else:
|
||||||
|
session_key = request.session.session_key
|
||||||
|
if not session_key:
|
||||||
|
request.session.create()
|
||||||
|
session_key = request.session.session_key
|
||||||
|
cart, created = Cart.objects.get_or_create(session_id=session_key)
|
||||||
|
return cart
|
||||||
|
|
||||||
|
def add_to_cart(request, product_id):
|
||||||
|
if request.method == 'POST':
|
||||||
|
product = get_object_or_404(Product, id=product_id)
|
||||||
|
quantity = int(request.POST.get('quantity', 1))
|
||||||
|
|
||||||
|
if quantity > product.stock:
|
||||||
|
messages.error(request, 'Nicht genügend Artikel auf Lager!')
|
||||||
|
return redirect('products:product_detail', pk=product_id)
|
||||||
|
|
||||||
|
cart = get_or_create_cart(request)
|
||||||
|
cart_item, created = CartItem.objects.get_or_create(
|
||||||
|
cart=cart,
|
||||||
|
product=product,
|
||||||
|
defaults={'quantity': quantity}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
cart_item.quantity += quantity
|
||||||
|
cart_item.save()
|
||||||
|
|
||||||
|
messages.success(request, f'{quantity}x {product.name} wurde zum Warenkorb hinzugefügt.')
|
||||||
|
return redirect('products:cart_detail')
|
||||||
|
|
||||||
|
def cart_detail(request):
|
||||||
|
cart = get_or_create_cart(request)
|
||||||
|
return render(request, 'products/cart_detail.html', {'cart': cart})
|
||||||
|
|
||||||
|
def create_order(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
cart = get_or_create_cart(request)
|
||||||
|
if not cart.items.exists():
|
||||||
|
messages.error(request, 'Ihr Warenkorb ist leer.')
|
||||||
|
return redirect('products:cart_detail')
|
||||||
|
|
||||||
|
# Erstelle eine neue Bestellung
|
||||||
|
order = Order.objects.create(
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
full_name=f"{request.user.first_name} {request.user.last_name}" if request.user.is_authenticated else "",
|
||||||
|
email=request.user.email if request.user.is_authenticated else "",
|
||||||
|
total_amount=cart.get_total()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Füge die Artikel aus dem Warenkorb zur Bestellung hinzu
|
||||||
|
for cart_item in cart.items.all():
|
||||||
|
OrderItem.objects.create(
|
||||||
|
order=order,
|
||||||
|
product=cart_item.product,
|
||||||
|
product_name=cart_item.product.name,
|
||||||
|
quantity=cart_item.quantity,
|
||||||
|
price=cart_item.product.price
|
||||||
|
)
|
||||||
|
|
||||||
|
# Leere den Warenkorb
|
||||||
|
cart.items.all().delete()
|
||||||
|
|
||||||
|
messages.success(request, 'Ihre Bestellung wurde erfolgreich erstellt.')
|
||||||
|
return redirect('products:checkout', order_id=order.id)
|
||||||
|
|
||||||
|
return redirect('products:cart_detail')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def checkout(request, order_id):
|
||||||
|
order = get_object_or_404(Order, id=order_id)
|
||||||
|
if request.user.is_authenticated and order.user != request.user:
|
||||||
|
messages.error(request, 'Sie haben keine Berechtigung, diese Bestellung einzusehen.')
|
||||||
|
return redirect('products:product_list')
|
||||||
|
|
||||||
|
# PayPal Zahlungsformular erstellen
|
||||||
|
host = request.get_host()
|
||||||
|
|
||||||
|
paypal_dict = {
|
||||||
|
"business": settings.PAYPAL_RECEIVER_EMAIL,
|
||||||
|
"amount": str(order.total_amount),
|
||||||
|
"item_name": f"Bestellung #{order.id}",
|
||||||
|
"invoice": str(order.id),
|
||||||
|
"currency_code": "EUR",
|
||||||
|
"notify_url": request.build_absolute_uri('/paypal/'),
|
||||||
|
"return_url": request.build_absolute_uri(f'/products/payment/success/{order.id}/'),
|
||||||
|
"cancel_return": request.build_absolute_uri(f'/products/payment/failed/{order.id}/'),
|
||||||
|
}
|
||||||
|
|
||||||
|
form = PayPalPaymentsForm(initial=paypal_dict)
|
||||||
|
return render(request, 'products/checkout.html', {
|
||||||
|
'order': order,
|
||||||
|
'form': form
|
||||||
|
})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@login_required
|
||||||
|
def create_paypal_order(request):
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({'error': 'Nur POST-Anfragen erlaubt'}, status=405)
|
||||||
|
|
||||||
|
data = json.loads(request.body)
|
||||||
|
order = get_object_or_404(Order, id=data['order_id'], user=request.user)
|
||||||
|
|
||||||
|
payment = paypalrestsdk.Payment({
|
||||||
|
"intent": "sale",
|
||||||
|
"payer": {
|
||||||
|
"payment_method": "paypal"
|
||||||
|
},
|
||||||
|
"transactions": [{
|
||||||
|
"amount": {
|
||||||
|
"total": str(order.total_amount),
|
||||||
|
"currency": "EUR"
|
||||||
|
},
|
||||||
|
"description": f"Bestellung #{order.id}"
|
||||||
|
}],
|
||||||
|
"redirect_urls": {
|
||||||
|
"return_url": f"{settings.SITE_URL}/products/order/confirmation/{order.id}/",
|
||||||
|
"cancel_url": f"{settings.SITE_URL}/products/checkout/{order.id}/"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if payment.create():
|
||||||
|
return JsonResponse({'paypal_order_id': payment.id})
|
||||||
|
else:
|
||||||
|
return JsonResponse({'error': payment.error}, status=400)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@login_required
|
||||||
|
def capture_paypal_order(request):
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({'error': 'Nur POST-Anfragen erlaubt'}, status=405)
|
||||||
|
|
||||||
|
data = json.loads(request.body)
|
||||||
|
order = get_object_or_404(Order, id=data['order_id'], user=request.user)
|
||||||
|
payment_id = data['paypal_order_id']
|
||||||
|
|
||||||
|
payment = paypalrestsdk.Payment.find(payment_id)
|
||||||
|
|
||||||
|
if payment.execute({'payer_id': payment.payer.payer_info.payer_id}):
|
||||||
|
order.status = 'paid'
|
||||||
|
order.payment_id = payment_id
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# E-Mail-Bestätigung senden
|
||||||
|
send_payment_confirmation_email(order)
|
||||||
|
|
||||||
|
return JsonResponse({'success': True})
|
||||||
|
else:
|
||||||
|
return JsonResponse({'error': payment.error}, status=400)
|
||||||
|
|
||||||
|
def order_confirmation(request, order_id):
|
||||||
|
order = get_object_or_404(Order, id=order_id)
|
||||||
|
if request.user.is_authenticated and order.user != request.user:
|
||||||
|
messages.error(request, 'Sie haben keine Berechtigung, diese Bestellung einzusehen.')
|
||||||
|
return redirect('products:product_list')
|
||||||
|
|
||||||
|
return render(request, 'products/order_confirmation.html', {'order': order})
|
||||||
|
|
||||||
|
def update_cart_item(request, item_id):
|
||||||
|
if request.method == 'POST':
|
||||||
|
cart_item = get_object_or_404(CartItem, id=item_id)
|
||||||
|
quantity = int(request.POST.get('quantity', 1))
|
||||||
|
|
||||||
|
if quantity > cart_item.product.stock:
|
||||||
|
messages.error(request, 'Nicht genügend Artikel auf Lager!')
|
||||||
|
elif quantity > 0:
|
||||||
|
cart_item.quantity = quantity
|
||||||
|
cart_item.save()
|
||||||
|
messages.success(request, 'Warenkorb wurde aktualisiert.')
|
||||||
|
else:
|
||||||
|
cart_item.delete()
|
||||||
|
messages.success(request, 'Artikel wurde aus dem Warenkorb entfernt.')
|
||||||
|
|
||||||
|
return redirect('products:cart_detail')
|
||||||
|
|
||||||
|
def remove_from_cart(request, item_id):
|
||||||
|
if request.method == 'POST':
|
||||||
|
cart_item = get_object_or_404(CartItem, id=item_id)
|
||||||
|
cart_item.delete()
|
||||||
|
messages.success(request, 'Artikel wurde aus dem Warenkorb entfernt.')
|
||||||
|
return redirect('products:cart_detail')
|
||||||
|
|
||||||
|
def register(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = CustomUserCreationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
login(request, user)
|
||||||
|
messages.success(request, 'Registrierung erfolgreich!')
|
||||||
|
return redirect('products:product_list')
|
||||||
|
else:
|
||||||
|
form = CustomUserCreationForm()
|
||||||
|
return render(request, 'registration/register.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_review(request, product_id):
|
||||||
|
product = get_object_or_404(Product, id=product_id)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ReviewForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
review = form.save(commit=False)
|
||||||
|
review.product = product
|
||||||
|
review.user = request.user
|
||||||
|
review.save()
|
||||||
|
messages.success(request, 'Ihre Bewertung wurde erfolgreich hinzugefügt.')
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Es gab einen Fehler beim Hinzufügen Ihrer Bewertung.')
|
||||||
|
return redirect('product_detail', pk=product_id)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = UserProfileForm(request.POST, instance=request.user.userprofile)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, 'Profil wurde erfolgreich aktualisiert.')
|
||||||
|
return redirect('products:profile')
|
||||||
|
else:
|
||||||
|
form = UserProfileForm(instance=request.user.userprofile)
|
||||||
|
|
||||||
|
return render(request, 'products/profile.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def order_history(request):
|
||||||
|
orders = Order.objects.filter(user=request.user).order_by('-created')
|
||||||
|
return render(request, 'products/order_history.html', {'orders': orders})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def wishlist_view(request):
|
||||||
|
wishlist, created = Wishlist.objects.get_or_create(user=request.user)
|
||||||
|
return render(request, 'products/wishlist.html', {'wishlist': wishlist})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_to_wishlist(request, product_id):
|
||||||
|
product = get_object_or_404(Product, id=product_id)
|
||||||
|
wishlist, created = Wishlist.objects.get_or_create(user=request.user)
|
||||||
|
wishlist.products.add(product)
|
||||||
|
messages.success(request, f'{product.name} wurde zu Ihrer Wunschliste hinzugefügt.')
|
||||||
|
return redirect('product_detail', product_id=product_id)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def remove_from_wishlist(request, product_id):
|
||||||
|
product = get_object_or_404(Product, id=product_id)
|
||||||
|
wishlist = get_object_or_404(Wishlist, user=request.user)
|
||||||
|
wishlist.products.remove(product)
|
||||||
|
messages.success(request, f'{product.name} wurde von Ihrer Wunschliste entfernt.')
|
||||||
|
return redirect('wishlist')
|
||||||
|
|
||||||
|
def faq_list(request):
|
||||||
|
faqs = FAQ.objects.all()
|
||||||
|
categories = FAQ.objects.values_list('category', flat=True).distinct()
|
||||||
|
return render(request, 'products/faq.html', {
|
||||||
|
'faqs': faqs,
|
||||||
|
'categories': categories
|
||||||
|
})
|
||||||
|
|
||||||
|
def contact(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ContactForm(request.POST, user=request.user)
|
||||||
|
if form.is_valid():
|
||||||
|
contact_message = form.save(commit=False)
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
contact_message.user = request.user
|
||||||
|
contact_message.save()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
'Vielen Dank für Ihre Nachricht! Wir werden uns schnellstmöglich bei Ihnen melden.'
|
||||||
|
)
|
||||||
|
return redirect('contact_success')
|
||||||
|
else:
|
||||||
|
form = ContactForm(user=request.user)
|
||||||
|
|
||||||
|
return render(request, 'products/contact.html', {'form': form})
|
||||||
|
|
||||||
|
def contact_success(request):
|
||||||
|
return render(request, 'products/contact_success.html')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def custom_order(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = CustomOrderForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
custom_order = form.save(commit=False)
|
||||||
|
custom_order.user = request.user
|
||||||
|
custom_order.save()
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
'Ihre Anfrage wurde erfolgreich übermittelt. Wir werden uns in Kürze mit einem Angebot bei Ihnen melden.'
|
||||||
|
)
|
||||||
|
return redirect('custom_order_success', order_id=custom_order.id)
|
||||||
|
else:
|
||||||
|
form = CustomOrderForm()
|
||||||
|
|
||||||
|
return render(request, 'products/custom_order.html', {
|
||||||
|
'form': form
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def custom_order_success(request, order_id):
|
||||||
|
order = get_object_or_404(CustomOrder, id=order_id, user=request.user)
|
||||||
|
return render(request, 'products/custom_order_success.html', {
|
||||||
|
'order': order
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def custom_order_detail(request, order_id):
|
||||||
|
order = get_object_or_404(CustomOrder, id=order_id, user=request.user)
|
||||||
|
progress_updates = order.progress_updates.all()
|
||||||
|
return render(request, 'products/custom_order_detail.html', {
|
||||||
|
'order': order,
|
||||||
|
'progress_updates': progress_updates
|
||||||
|
})
|
||||||
|
|
||||||
|
def gallery(request):
|
||||||
|
# Hole alle Galeriebilder
|
||||||
|
images = GalleryImage.objects.all().order_by('order', '-created')
|
||||||
|
|
||||||
|
# Filter nach Typ
|
||||||
|
fursuit_type = request.GET.get('fursuit_type')
|
||||||
|
if fursuit_type:
|
||||||
|
images = images.filter(fursuit_type=fursuit_type)
|
||||||
|
|
||||||
|
# Filter nach Stil
|
||||||
|
style = request.GET.get('style')
|
||||||
|
if style:
|
||||||
|
images = images.filter(style=style)
|
||||||
|
|
||||||
|
# Sortierung
|
||||||
|
sort = request.GET.get('sort', 'order')
|
||||||
|
if sort == 'newest':
|
||||||
|
images = images.order_by('-created')
|
||||||
|
elif sort == 'oldest':
|
||||||
|
images = images.order_by('created')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'images': images,
|
||||||
|
'fursuit_types': GalleryImage.FURSUIT_TYPE_CHOICES,
|
||||||
|
'fursuit_styles': GalleryImage.STYLE_CHOICES,
|
||||||
|
'current_type': fursuit_type,
|
||||||
|
'current_style': style,
|
||||||
|
'current_sort': sort
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Anzahl der geladenen Bilder: {images.count()}") # Debug-Ausgabe
|
||||||
|
for img in images:
|
||||||
|
print(f"Bild: {img.title}, URL: {img.image.url}") # Debug-Ausgabe
|
||||||
|
|
||||||
|
return render(request, 'products/gallery.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_progress_update(request, order_id):
|
||||||
|
order = get_object_or_404(CustomOrder, id=order_id)
|
||||||
|
if not request.user.is_staff:
|
||||||
|
messages.error(request, 'Sie haben keine Berechtigung für diese Aktion.')
|
||||||
|
return redirect('custom_order_detail', order_id=order_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = OrderProgressForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
progress = form.save(commit=False)
|
||||||
|
progress.custom_order = order
|
||||||
|
progress.save()
|
||||||
|
|
||||||
|
messages.success(request, 'Fortschritts-Update wurde hinzugefügt.')
|
||||||
|
|
||||||
|
# Wenn alle Schritte abgeschlossen sind, setze den Status auf "ready"
|
||||||
|
if all(update.completed for update in order.progress_updates.all()):
|
||||||
|
order.status = 'ready'
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
return redirect('custom_order_detail', order_id=order_id)
|
||||||
|
else:
|
||||||
|
form = OrderProgressForm()
|
||||||
|
|
||||||
|
return render(request, 'products/add_progress.html', {
|
||||||
|
'form': form,
|
||||||
|
'order': order
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard(request):
|
||||||
|
# Basis-Informationen
|
||||||
|
user_profile = request.user.userprofile
|
||||||
|
wishlist = Wishlist.objects.get_or_create(user=request.user)[0]
|
||||||
|
cart = get_or_create_cart(request)
|
||||||
|
|
||||||
|
# Custom Orders mit Fortschritt
|
||||||
|
custom_orders = CustomOrder.objects.filter(user=request.user).order_by('-created')
|
||||||
|
for order in custom_orders:
|
||||||
|
order.progress_percentage = calculate_order_progress(order)
|
||||||
|
|
||||||
|
# Letzte Bestellungen mit Details
|
||||||
|
recent_orders = Order.objects.filter(user=request.user).order_by('-created')[:5]
|
||||||
|
for order in recent_orders:
|
||||||
|
order.items_count = order.items.count()
|
||||||
|
|
||||||
|
# Bewertungsübersicht
|
||||||
|
reviews = Review.objects.filter(user=request.user).select_related('product').order_by('-created')[:5]
|
||||||
|
|
||||||
|
# Statistiken
|
||||||
|
stats = {
|
||||||
|
'total_orders': Order.objects.filter(user=request.user).count(),
|
||||||
|
'total_custom_orders': CustomOrder.objects.filter(user=request.user).count(),
|
||||||
|
'total_reviews': Review.objects.filter(user=request.user).count(),
|
||||||
|
'wishlist_count': wishlist.products.count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'products/dashboard.html', {
|
||||||
|
'user_profile': user_profile,
|
||||||
|
'custom_orders': custom_orders,
|
||||||
|
'recent_orders': recent_orders,
|
||||||
|
'reviews': reviews,
|
||||||
|
'stats': stats,
|
||||||
|
'wishlist': wishlist,
|
||||||
|
'cart': cart,
|
||||||
|
})
|
||||||
|
|
||||||
|
def calculate_order_progress(order):
|
||||||
|
"""Berechne den Fortschritt einer Custom Order in Prozent."""
|
||||||
|
if order.status == 'cancelled':
|
||||||
|
return 0
|
||||||
|
elif order.status == 'completed':
|
||||||
|
return 100
|
||||||
|
|
||||||
|
# Status-Gewichtung
|
||||||
|
status_weights = {
|
||||||
|
'pending': 10,
|
||||||
|
'quoted': 20,
|
||||||
|
'approved': 30,
|
||||||
|
'in_progress': 50,
|
||||||
|
'ready': 90,
|
||||||
|
'shipped': 95,
|
||||||
|
}
|
||||||
|
|
||||||
|
base_progress = status_weights.get(order.status, 0)
|
||||||
|
|
||||||
|
# Zusätzlicher Fortschritt basierend auf OrderProgress
|
||||||
|
if order.status == 'in_progress':
|
||||||
|
completed_stages = order.progress_updates.filter(completed=True).count()
|
||||||
|
total_stages = len(OrderProgress.PROGRESS_CHOICES)
|
||||||
|
stage_progress = (completed_stages / total_stages) * 40 # 40% für Fortschrittsstufen
|
||||||
|
return base_progress + stage_progress
|
||||||
|
|
||||||
|
return base_progress
|
||||||
|
|
||||||
|
# API ViewSets - Temporär auskommentiert
|
||||||
|
# class ProductViewSet(viewsets.ModelViewSet):
|
||||||
|
# queryset = Product.objects.all()
|
||||||
|
# serializer_class = ProductSerializer
|
||||||
|
# permission_classes = [IsAuthenticatedOrReadOnly]
|
||||||
|
|
||||||
|
# class ReviewViewSet(viewsets.ModelViewSet):
|
||||||
|
# queryset = Review.objects.all()
|
||||||
|
# serializer_class = ReviewSerializer
|
||||||
|
# permission_classes = [IsAuthenticatedOrReadOnly]
|
||||||
|
|
||||||
|
# def perform_create(self, serializer):
|
||||||
|
# serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
# Stripe-Konfiguration
|
||||||
|
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||||
|
|
||||||
|
def create_payment_intent(request, order_id):
|
||||||
|
"""Erstellt einen Stripe Payment Intent für eine Bestellung."""
|
||||||
|
try:
|
||||||
|
order = get_object_or_404(Order, id=order_id)
|
||||||
|
|
||||||
|
# Stripe akzeptiert nur Cent-Beträge
|
||||||
|
amount = int(order.total_amount * 100)
|
||||||
|
|
||||||
|
# Payment Intent erstellen
|
||||||
|
intent = stripe.PaymentIntent.create(
|
||||||
|
amount=amount,
|
||||||
|
currency='eur',
|
||||||
|
metadata={
|
||||||
|
'order_id': order.id,
|
||||||
|
'user_id': request.user.id if request.user.is_authenticated else None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order aktualisieren
|
||||||
|
order.stripe_payment_intent_id = intent.id
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'clientSecret': intent.client_secret,
|
||||||
|
'amount': amount
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
def payment_view(request, order_id):
|
||||||
|
"""Zeigt die Zahlungsseite an."""
|
||||||
|
order = get_object_or_404(Order, id=order_id)
|
||||||
|
|
||||||
|
# Überprüfen, ob die Bestellung dem Benutzer gehört
|
||||||
|
if request.user.is_authenticated and order.user != request.user:
|
||||||
|
messages.error(request, 'Sie haben keine Berechtigung für diese Bestellung.')
|
||||||
|
return redirect('dashboard')
|
||||||
|
|
||||||
|
# Überprüfen, ob die Bestellung bereits bezahlt wurde
|
||||||
|
if order.payment_status == 'paid':
|
||||||
|
messages.info(request, 'Diese Bestellung wurde bereits bezahlt.')
|
||||||
|
return redirect('order_detail', order_id=order.id)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'order': order,
|
||||||
|
'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
|
||||||
|
}
|
||||||
|
return render(request, 'products/payment.html', context)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def stripe_webhook(request):
|
||||||
|
"""Webhook für Stripe-Events."""
|
||||||
|
payload = request.body
|
||||||
|
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = stripe.Webhook.construct_event(
|
||||||
|
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
return JsonResponse({'error': 'Invalid payload'}, status=400)
|
||||||
|
except stripe.error.SignatureVerificationError as e:
|
||||||
|
return JsonResponse({'error': 'Invalid signature'}, status=400)
|
||||||
|
|
||||||
|
if event.type == 'payment_intent.succeeded':
|
||||||
|
payment_intent = event.data.object
|
||||||
|
handle_successful_payment(payment_intent)
|
||||||
|
elif event.type == 'payment_intent.payment_failed':
|
||||||
|
payment_intent = event.data.object
|
||||||
|
handle_failed_payment(payment_intent)
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'success'})
|
||||||
|
|
||||||
|
def handle_successful_payment(payment_intent):
|
||||||
|
"""Verarbeitet erfolgreiche Zahlungen."""
|
||||||
|
order_id = payment_intent.metadata.get('order_id')
|
||||||
|
if order_id:
|
||||||
|
try:
|
||||||
|
order = Order.objects.get(id=order_id)
|
||||||
|
order.payment_status = 'paid'
|
||||||
|
order.payment_date = timezone.now()
|
||||||
|
order.status = 'processing'
|
||||||
|
order.stripe_payment_method_id = payment_intent.payment_method
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# E-Mail-Benachrichtigung senden
|
||||||
|
send_payment_confirmation_email(order)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
print(f"Bestellung {order_id} nicht gefunden")
|
||||||
|
|
||||||
|
def handle_failed_payment(payment_intent):
|
||||||
|
"""Verarbeitet fehlgeschlagene Zahlungen."""
|
||||||
|
order_id = payment_intent.metadata.get('order_id')
|
||||||
|
if order_id:
|
||||||
|
try:
|
||||||
|
order = Order.objects.get(id=order_id)
|
||||||
|
order.payment_status = 'failed'
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# E-Mail-Benachrichtigung senden
|
||||||
|
send_payment_failed_email(order)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
print(f"Bestellung {order_id} nicht gefunden")
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def payment_success(request, order_id):
|
||||||
|
"""Zeigt die Erfolgsseite nach erfolgreicher Zahlung an."""
|
||||||
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
||||||
|
messages.success(request, 'Ihre Zahlung wurde erfolgreich verarbeitet!')
|
||||||
|
return render(request, 'products/payment_success.html', {'order': order})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def payment_failed(request, order_id):
|
||||||
|
"""Zeigt die Fehlerseite nach fehlgeschlagener Zahlung an."""
|
||||||
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
||||||
|
messages.error(request, 'Die Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut.')
|
||||||
|
return render(request, 'products/payment_failed.html', {'order': order})
|
||||||
|
|
||||||
|
# Hilfsfunktionen für E-Mail-Benachrichtigungen
|
||||||
|
def send_payment_confirmation_email(order):
|
||||||
|
"""Sendet eine Bestätigungs-E-Mail nach erfolgreicher Zahlung."""
|
||||||
|
subject = f'Zahlungsbestätigung für Bestellung #{order.id}'
|
||||||
|
message = f"""
|
||||||
|
Sehr geehrte(r) {order.full_name},
|
||||||
|
|
||||||
|
vielen Dank für Ihre Zahlung. Ihre Bestellung #{order.id} wurde erfolgreich bezahlt.
|
||||||
|
|
||||||
|
Bestelldetails:
|
||||||
|
- Gesamtbetrag: {order.total_amount} €
|
||||||
|
- Zahlungsmethode: {order.get_payment_method_display()}
|
||||||
|
- Zahlungsdatum: {order.payment_date.strftime('%d.%m.%Y %H:%M')}
|
||||||
|
|
||||||
|
Wir werden Ihre Bestellung schnellstmöglich bearbeiten.
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen
|
||||||
|
Ihr Fursuit-Shop Team
|
||||||
|
"""
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[order.email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_payment_failed_email(order):
|
||||||
|
"""Sendet eine Benachrichtigung nach fehlgeschlagener Zahlung."""
|
||||||
|
subject = f'Zahlungsproblem bei Bestellung #{order.id}'
|
||||||
|
message = f"""
|
||||||
|
Sehr geehrte(r) {order.full_name},
|
||||||
|
|
||||||
|
leider ist die Zahlung für Ihre Bestellung #{order.id} fehlgeschlagen.
|
||||||
|
|
||||||
|
Sie können die Zahlung unter folgendem Link erneut versuchen:
|
||||||
|
{settings.SITE_URL}/payment/{order.id}/
|
||||||
|
|
||||||
|
Falls Sie Fragen haben, kontaktieren Sie uns bitte.
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen
|
||||||
|
Ihr Fursuit-Shop Team
|
||||||
|
"""
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[order.email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def payment_process(request, order_id):
|
||||||
|
order = get_object_or_404(Order, id=order_id, user=request.user)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Initiiere PayPal-Zahlung
|
||||||
|
paypal_dict = {
|
||||||
|
"business": settings.PAYPAL_RECEIVER_EMAIL,
|
||||||
|
"amount": str(order.total_amount),
|
||||||
|
"item_name": f"Bestellung #{order.id}",
|
||||||
|
"invoice": str(order.id),
|
||||||
|
"notify_url": request.build_absolute_uri(reverse('paypal-ipn')),
|
||||||
|
"return_url": request.build_absolute_uri(reverse('payment_success', args=[order.id])),
|
||||||
|
"cancel_return": request.build_absolute_uri(reverse('payment_failed', args=[order.id])),
|
||||||
|
"custom": json.dumps({
|
||||||
|
"order_id": order.id,
|
||||||
|
"user_id": request.user.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
form = PayPalPaymentsForm(initial=paypal_dict)
|
||||||
|
return render(request, 'products/payment_process.html', {'order': order, 'form': form})
|
||||||
|
|
||||||
|
return render(request, 'products/payment_process.html', {'order': order})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def paypal_ipn(request):
|
||||||
|
"""PayPal IPN (Instant Payment Notification) Handler"""
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
ipn_obj = PayPalIPN(request.POST)
|
||||||
|
ipn_obj.verify()
|
||||||
|
|
||||||
|
if ipn_obj.payment_status == "Completed":
|
||||||
|
# Zahlung war erfolgreich
|
||||||
|
try:
|
||||||
|
order = Order.objects.get(id=ipn_obj.invoice)
|
||||||
|
order.status = 'paid'
|
||||||
|
order.payment_method = 'paypal'
|
||||||
|
order.payment_id = ipn_obj.txn_id
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Sende Bestätigungs-E-Mail
|
||||||
|
send_payment_confirmation_email(order)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return HttpResponse("OK")
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponse(str(e))
|
||||||
|
|
||||||
|
return HttpResponse("OK")
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from .models import (
|
||||||
|
Product, Category, FursuitGallery, GalleryImage,
|
||||||
|
DesignTemplate, CustomerDesign, Order, OrderProgress,
|
||||||
|
Cart, CartItem, ShippingAddress, Checkout,
|
||||||
|
ProductType, ProductImage, ProductVariant, CustomDesign,
|
||||||
|
PayPalPayment, PaymentError
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.register(Category)
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'slug', 'parent']
|
||||||
|
list_filter = ['parent']
|
||||||
|
search_fields = ['name']
|
||||||
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
|
||||||
|
@admin.register(ProductType)
|
||||||
|
class ProductTypeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'has_sizes', 'has_colors', 'has_custom_design', 'requires_measurements']
|
||||||
|
list_filter = ['has_sizes', 'has_colors', 'has_custom_design', 'requires_measurements']
|
||||||
|
search_fields = ['name']
|
||||||
|
|
||||||
|
class ProductImageInline(admin.TabularInline):
|
||||||
|
model = ProductImage
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class ProductVariantInline(admin.TabularInline):
|
||||||
|
model = ProductVariant
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Product)
|
||||||
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'category', 'price', 'stock', 'available', 'created']
|
||||||
|
list_filter = ['available', 'category', 'product_type', 'created']
|
||||||
|
search_fields = ['name', 'description']
|
||||||
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
inlines = [ProductImageInline, ProductVariantInline]
|
||||||
|
date_hierarchy = 'created'
|
||||||
|
|
||||||
|
@admin.register(CustomDesign)
|
||||||
|
class CustomDesignAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['product', 'customer', 'status', 'created']
|
||||||
|
list_filter = ['status', 'created']
|
||||||
|
search_fields = ['product__name', 'customer__username']
|
||||||
|
date_hierarchy = 'created'
|
||||||
|
|
||||||
|
@admin.register(DesignTemplate)
|
||||||
|
class DesignTemplateAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'created_at']
|
||||||
|
search_fields = ['name', 'description']
|
||||||
|
|
||||||
|
@admin.register(CustomerDesign)
|
||||||
|
class CustomerDesignAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['user', 'name', 'created_at']
|
||||||
|
search_fields = ['user__username', 'name']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
class OrderProgressInline(admin.TabularInline):
|
||||||
|
model = OrderProgress
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Order)
|
||||||
|
class OrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'user', 'product', 'status', 'payment_method', 'total_price', 'created_at']
|
||||||
|
list_filter = ['status', 'payment_method', 'created_at']
|
||||||
|
search_fields = ['user__username', 'product__name']
|
||||||
|
inlines = [OrderProgressInline]
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
class CartItemInline(admin.TabularInline):
|
||||||
|
model = CartItem
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Cart)
|
||||||
|
class CartAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['user', 'created_at', 'updated_at']
|
||||||
|
search_fields = ['user__username']
|
||||||
|
inlines = [CartItemInline]
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
@admin.register(ShippingAddress)
|
||||||
|
class ShippingAddressAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['user', 'first_name', 'last_name', 'city', 'country']
|
||||||
|
list_filter = ['country']
|
||||||
|
search_fields = ['user__username', 'first_name', 'last_name', 'city']
|
||||||
|
|
||||||
|
@admin.register(Checkout)
|
||||||
|
class CheckoutAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['user', 'status', 'payment_method', 'created_at']
|
||||||
|
list_filter = ['status', 'payment_method']
|
||||||
|
search_fields = ['user__username']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
@admin.register(PayPalPayment)
|
||||||
|
class PayPalPaymentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['order', 'payment_id', 'status', 'amount', 'currency', 'created_at']
|
||||||
|
list_filter = ['status', 'currency']
|
||||||
|
search_fields = ['payment_id', 'order__id']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
@admin.register(PaymentError)
|
||||||
|
class PaymentErrorAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['order', 'error_code', 'created_at']
|
||||||
|
search_fields = ['order__id', 'error_code', 'error_message']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
@admin.register(FursuitGallery)
|
||||||
|
class FursuitGalleryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'created_at']
|
||||||
|
search_fields = ['name', 'description']
|
||||||
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
|
||||||
|
class GalleryImageInline(admin.TabularInline):
|
||||||
|
model = GalleryImage
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(GalleryImage)
|
||||||
|
class GalleryImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['gallery', 'title', 'order']
|
||||||
|
list_filter = ['gallery']
|
||||||
|
search_fields = ['gallery__name', 'title']
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ShopConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'shop'
|
||||||
|
verbose_name = _('Shop')
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
"""
|
||||||
|
Importiert und registriert die Signal-Handler
|
||||||
|
"""
|
||||||
|
import shop.signals
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for shop project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shop.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
|
||||||
|
def send_order_confirmation(request, order):
|
||||||
|
"""
|
||||||
|
Sendet eine Bestellbestätigungs-E-Mail an den Kunden
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
'order': order,
|
||||||
|
'logo_url': f"{settings.STATIC_URL}images/logo.png",
|
||||||
|
'order_url': request.build_absolute_uri(
|
||||||
|
reverse('shop:my_orders')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML-Version
|
||||||
|
html_content = render_to_string(
|
||||||
|
'shop/emails/order_confirmation.html',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text-Version
|
||||||
|
text_content = render_to_string(
|
||||||
|
'shop/emails/order_confirmation.txt',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = _('Order Confirmation - Order #{}').format(order.id)
|
||||||
|
from_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
to_email = order.shipping_address.email
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject,
|
||||||
|
text_content,
|
||||||
|
from_email,
|
||||||
|
[to_email]
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
def send_order_status_update(request, order, update=None):
|
||||||
|
"""
|
||||||
|
Sendet eine E-Mail über Statusänderungen der Bestellung
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
'order': order,
|
||||||
|
'update': update,
|
||||||
|
'logo_url': f"{settings.STATIC_URL}images/logo.png",
|
||||||
|
'order_url': request.build_absolute_uri(
|
||||||
|
reverse('shop:my_orders')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML-Version
|
||||||
|
html_content = render_to_string(
|
||||||
|
'shop/emails/order_status_update.html',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text-Version
|
||||||
|
text_content = render_to_string(
|
||||||
|
'shop/emails/order_status_update.txt',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = _('Order Status Update - Order #{}').format(order.id)
|
||||||
|
from_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
to_email = order.shipping_address.email
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject,
|
||||||
|
text_content,
|
||||||
|
from_email,
|
||||||
|
[to_email]
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
def send_shipping_confirmation(request, order):
|
||||||
|
"""
|
||||||
|
Sendet eine Versandbestätigungs-E-Mail mit Tracking-Nummer
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
'order': order,
|
||||||
|
'logo_url': f"{settings.STATIC_URL}images/logo.png",
|
||||||
|
'order_url': request.build_absolute_uri(
|
||||||
|
reverse('shop:my_orders')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML-Version
|
||||||
|
html_content = render_to_string(
|
||||||
|
'shop/emails/shipping_confirmation.html',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text-Version
|
||||||
|
text_content = render_to_string(
|
||||||
|
'shop/emails/shipping_confirmation.txt',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = _('Your Order Has Been Shipped - Order #{}').format(order.id)
|
||||||
|
from_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
to_email = order.shipping_address.email
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject,
|
||||||
|
text_content,
|
||||||
|
from_email,
|
||||||
|
[to_email]
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
def send_admin_notification(request, order, notification_type, extra_context=None):
|
||||||
|
"""
|
||||||
|
Sendet eine Benachrichtigung an den Shop-Administrator
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
'order': order,
|
||||||
|
'notification_type': notification_type,
|
||||||
|
'logo_url': f"{settings.STATIC_URL}images/logo.png",
|
||||||
|
'order_url': request.build_absolute_uri(
|
||||||
|
reverse('admin:shop_order_change', args=[order.id])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if extra_context:
|
||||||
|
context.update(extra_context)
|
||||||
|
|
||||||
|
# HTML-Version
|
||||||
|
html_content = render_to_string(
|
||||||
|
'shop/emails/admin_notification.html',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text-Version
|
||||||
|
text_content = render_to_string(
|
||||||
|
'shop/emails/admin_notification.txt',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Betreff basierend auf Benachrichtigungstyp
|
||||||
|
subjects = {
|
||||||
|
'new_order': _('New Order Received - Order #{}'),
|
||||||
|
'payment_failed': _('Payment Failed - Order #{}'),
|
||||||
|
'custom_design': _('New Custom Design Order #{}'),
|
||||||
|
'fursuit_order': _('New Fursuit Order #{}'),
|
||||||
|
}
|
||||||
|
|
||||||
|
subject = subjects.get(notification_type, _('Order Notification - Order #{}')).format(order.id)
|
||||||
|
|
||||||
|
# E-Mail an alle konfigurierten Admin-E-Mail-Adressen senden
|
||||||
|
admin_emails = [email for name, email in settings.ADMINS]
|
||||||
|
|
||||||
|
if admin_emails:
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject,
|
||||||
|
text_content,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
admin_emails
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
def send_low_stock_notification(request, product):
|
||||||
|
"""
|
||||||
|
Benachrichtigt den Admin über niedrigen Lagerbestand
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
'product': product,
|
||||||
|
'logo_url': f"{settings.STATIC_URL}images/logo.png",
|
||||||
|
'product_url': request.build_absolute_uri(
|
||||||
|
reverse('admin:shop_product_change', args=[product.id])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML-Version
|
||||||
|
html_content = render_to_string(
|
||||||
|
'shop/emails/low_stock_notification.html',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text-Version
|
||||||
|
text_content = render_to_string(
|
||||||
|
'shop/emails/low_stock_notification.txt',
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = _('Low Stock Alert - {}').format(product.name)
|
||||||
|
admin_emails = [email for name, email in settings.ADMINS]
|
||||||
|
|
||||||
|
if admin_emails:
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject,
|
||||||
|
text_content,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
admin_emails
|
||||||
|
)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send()
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from .models import ShippingAddress, Order
|
||||||
|
|
||||||
|
class ShippingAddressForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ShippingAddress
|
||||||
|
fields = ['first_name', 'last_name', 'email', 'address', 'city', 'zip', 'country']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field in self.fields.values():
|
||||||
|
field.widget.attrs['class'] = 'form-control'
|
||||||
|
|
||||||
|
class PaymentMethodForm(forms.Form):
|
||||||
|
payment_method = forms.ChoiceField(
|
||||||
|
choices=Order.PAYMENT_METHODS,
|
||||||
|
widget=forms.HiddenInput(),
|
||||||
|
required=True,
|
||||||
|
error_messages={
|
||||||
|
'required': _('Please select a payment method.')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.core.mail import send_mail, get_connection
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from shop.models import Order, Product
|
||||||
|
from shop.emails import (
|
||||||
|
send_order_confirmation,
|
||||||
|
send_order_status_update,
|
||||||
|
send_shipping_confirmation,
|
||||||
|
send_admin_notification,
|
||||||
|
send_low_stock_notification
|
||||||
|
)
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Testet das E-Mail-System mit verschiedenen E-Mail-Typen'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--email',
|
||||||
|
type=str,
|
||||||
|
help='Test-E-Mail-Adresse',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--type',
|
||||||
|
type=str,
|
||||||
|
choices=['all', 'order', 'status', 'shipping', 'admin', 'stock'],
|
||||||
|
default='all',
|
||||||
|
help='Art der Test-E-Mail',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
test_email = options['email']
|
||||||
|
if not test_email:
|
||||||
|
self.stdout.write(self.style.ERROR('Bitte geben Sie eine Test-E-Mail-Adresse an mit --email=ihre@email.de'))
|
||||||
|
return
|
||||||
|
|
||||||
|
email_type = options['type']
|
||||||
|
|
||||||
|
self.stdout.write('Starte E-Mail-Test...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Debug-Informationen ausgeben
|
||||||
|
self.stdout.write(f'Backend: {settings.EMAIL_BACKEND}')
|
||||||
|
self.stdout.write(f'Host: {settings.EMAIL_HOST}')
|
||||||
|
self.stdout.write(f'Port: {settings.EMAIL_PORT}')
|
||||||
|
self.stdout.write(f'TLS: {settings.EMAIL_USE_TLS}')
|
||||||
|
self.stdout.write(f'Benutzer: {settings.EMAIL_HOST_USER}')
|
||||||
|
self.stdout.write('Passwort: ***versteckt***')
|
||||||
|
|
||||||
|
# Basis-Test
|
||||||
|
send_mail(
|
||||||
|
'Test E-Mail',
|
||||||
|
'Dies ist eine Test-E-Mail vom Fursuit Shop.',
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[test_email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS('✓ Basis-E-Mail erfolgreich gesendet'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'✗ Basis-E-Mail fehlgeschlagen: {str(e)}'))
|
||||||
|
return
|
||||||
|
|
||||||
|
if email_type in ['all', 'order']:
|
||||||
|
try:
|
||||||
|
# Test-Bestellung erstellen
|
||||||
|
order = Order.objects.first()
|
||||||
|
if order:
|
||||||
|
send_order_confirmation(None, order)
|
||||||
|
self.stdout.write(self.style.SUCCESS('✓ Bestellbestätigung gesendet'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'✗ Bestellbestätigung fehlgeschlagen: {str(e)}'))
|
||||||
|
|
||||||
|
if email_type in ['all', 'status']:
|
||||||
|
try:
|
||||||
|
order = Order.objects.first()
|
||||||
|
if order:
|
||||||
|
send_order_status_update(None, order)
|
||||||
|
self.stdout.write(self.style.SUCCESS('✓ Status-Update gesendet'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'✗ Status-Update fehlgeschlagen: {str(e)}'))
|
||||||
|
|
||||||
|
if email_type in ['all', 'shipping']:
|
||||||
|
try:
|
||||||
|
order = Order.objects.filter(tracking_number__isnull=False).first()
|
||||||
|
if order:
|
||||||
|
send_shipping_confirmation(None, order)
|
||||||
|
self.stdout.write(self.style.SUCCESS('✓ Versandbestätigung gesendet'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'✗ Versandbestätigung fehlgeschlagen: {str(e)}'))
|
||||||
|
|
||||||
|
if email_type in ['all', 'admin']:
|
||||||
|
try:
|
||||||
|
order = Order.objects.first()
|
||||||
|
if order:
|
||||||
|
send_admin_notification(None, order, 'new_order')
|
||||||
|
self.stdout.write(self.style.SUCCESS('✓ Admin-Benachrichtigung gesendet'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'✗ Admin-Benachrichtigung fehlgeschlagen: {str(e)}'))
|
||||||
|
|
||||||
|
if email_type in ['all', 'stock']:
|
||||||
|
try:
|
||||||
|
product = Product.objects.first()
|
||||||
|
if product:
|
||||||
|
send_low_stock_notification(None, product)
|
||||||
|
self.stdout.write(self.style.SUCCESS('✓ Lagerbestand-Warnung gesendet'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'✗ Lagerbestand-Warnung fehlgeschlagen: {str(e)}'))
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('\nE-Mail-Test abgeschlossen!'))
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 10:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Category',
|
||||||
|
'verbose_name_plural': 'Categories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DesignTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
||||||
|
('description', models.TextField(verbose_name='Description')),
|
||||||
|
('description_en', models.TextField(verbose_name='Description (English)')),
|
||||||
|
('image', models.ImageField(upload_to='designs/')),
|
||||||
|
('model_3d', models.FileField(blank=True, null=True, upload_to='3d_models/', verbose_name='3D Model')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FursuitGallery',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
('description', models.TextField(verbose_name='Description')),
|
||||||
|
('description_en', models.TextField(verbose_name='Description (English)')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Fursuit Gallery',
|
||||||
|
'verbose_name_plural': 'Fursuit Galleries',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomerDesign',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Design Name')),
|
||||||
|
('design_file', models.FileField(upload_to='customer_designs/')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GalleryImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(upload_to='gallery/')),
|
||||||
|
('title', models.CharField(blank=True, max_length=200, verbose_name='Title')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||||
|
('order', models.PositiveIntegerField(default=0)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('gallery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='shop.fursuitgallery')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['order', 'created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
|
||||||
|
('payment_method', models.CharField(choices=[('paypal', 'PayPal'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer')], max_length=20)),
|
||||||
|
('special_instructions', models.TextField(blank=True)),
|
||||||
|
('measurements', models.JSONField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('total_price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||||
|
('customer_design', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.customerdesign')),
|
||||||
|
('design_template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.designtemplate')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderProgress',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=200, verbose_name='Title')),
|
||||||
|
('description', models.TextField(verbose_name='Description')),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='progress/')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_updates', to='shop.order')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('product_type', models.CharField(choices=[('fursuit', 'Fursuit'), ('printed', 'Printed Item')], default='printed', max_length=10, verbose_name='Product Type')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||||
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
||||||
|
('description', models.TextField(verbose_name='Description')),
|
||||||
|
('description_en', models.TextField(verbose_name='Description (English)')),
|
||||||
|
('base_price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='Base Price')),
|
||||||
|
('image', models.ImageField(upload_to='products/')),
|
||||||
|
('model_3d', models.FileField(blank=True, null=True, upload_to='3d_models/', verbose_name='3D Model')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.category')),
|
||||||
|
('gallery', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.fursuitgallery')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='product',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-29 12:47
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shop', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProductType',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('has_sizes', models.BooleanField(default=False, verbose_name='Hat Größen')),
|
||||||
|
('has_colors', models.BooleanField(default=False, verbose_name='Hat Farben')),
|
||||||
|
('has_custom_design', models.BooleanField(default=False, verbose_name='Erlaubt Custom Design')),
|
||||||
|
('requires_measurements', models.BooleanField(default=False, verbose_name='Benötigt Maße')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Produkttyp',
|
||||||
|
'verbose_name_plural': 'Produkttypen',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='category',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Kategorie', 'verbose_name_plural': 'Kategorien'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='product',
|
||||||
|
options={'ordering': ['-created'], 'verbose_name': 'Produkt', 'verbose_name_plural': 'Produkte'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='category',
|
||||||
|
name='name_en',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='base_price',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='created_at',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='description_en',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='gallery',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='model_3d',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='name_en',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='updated_at',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='category',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Beschreibung'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='category',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(blank=True, upload_to='categories/', verbose_name='Bild'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='category',
|
||||||
|
name='parent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='shop.category', verbose_name='Übergeordnete Kategorie'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='available',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Verfügbar'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Erstellt'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='price',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='Preis'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(default='', max_length=200, unique=True, verbose_name='Slug'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='Lagerbestand'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, verbose_name='Aktualisiert'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='category',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=100, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='category',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(max_length=100, unique=True, verbose_name='Slug'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='shop.category', verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(verbose_name='Beschreibung'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(upload_to='products/', verbose_name='Hauptbild'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Cart',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CartItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1)),
|
||||||
|
('size', models.CharField(blank=True, max_length=20, null=True)),
|
||||||
|
('notes', models.TextField(blank=True, null=True)),
|
||||||
|
('custom_design', models.ImageField(blank=True, null=True, upload_to='custom_designs/')),
|
||||||
|
('added_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shop.cart')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-added_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomDesign',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('design_file', models.FileField(upload_to='designs/', verbose_name='Design-Datei')),
|
||||||
|
('notes', models.TextField(verbose_name='Anmerkungen')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Erstellt')),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Ausstehend'), ('approved', 'Genehmigt'), ('rejected', 'Abgelehnt'), ('in_progress', 'In Bearbeitung'), ('completed', 'Abgeschlossen')], default='pending', max_length=20, verbose_name='Status')),
|
||||||
|
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Kunde')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_designs', to='shop.product', verbose_name='Produkt')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Custom Design',
|
||||||
|
'verbose_name_plural': 'Custom Designs',
|
||||||
|
'ordering': ['-created'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PaymentError',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('error_code', models.CharField(max_length=100, verbose_name='Error Code')),
|
||||||
|
('error_message', models.TextField(verbose_name='Error Message')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_errors', to='shop.order')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PayPalPayment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('payment_id', models.CharField(max_length=100, verbose_name='PayPal Payment ID')),
|
||||||
|
('payer_id', models.CharField(max_length=100, verbose_name='PayPal Payer ID')),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ('refunded', 'Refunded')], max_length=20, verbose_name='Payment Status')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
|
||||||
|
('currency', models.CharField(default='EUR', max_length=3, verbose_name='Currency')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='paypal_payment', to='shop.order')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProductImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(upload_to='products/', verbose_name='Bild')),
|
||||||
|
('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alternativer Text')),
|
||||||
|
('is_feature', models.BooleanField(default=False, verbose_name='Ist Hauptbild')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Erstellt')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='shop.product', verbose_name='Produkt')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Produktbild',
|
||||||
|
'verbose_name_plural': 'Produktbilder',
|
||||||
|
'ordering': ['-is_feature', '-created'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='product_type',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.producttype', verbose_name='Produkttyp'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShippingAddress',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('first_name', models.CharField(max_length=100, verbose_name='First Name')),
|
||||||
|
('last_name', models.CharField(max_length=100, verbose_name='Last Name')),
|
||||||
|
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||||
|
('address', models.CharField(max_length=200, verbose_name='Address')),
|
||||||
|
('city', models.CharField(max_length=100, verbose_name='City')),
|
||||||
|
('zip', models.CharField(max_length=10, verbose_name='ZIP Code')),
|
||||||
|
('country', models.CharField(choices=[('DE', 'Deutschland'), ('AT', 'Österreich'), ('CH', 'Schweiz')], max_length=2, verbose_name='Country')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Checkout',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('payment_method', models.CharField(choices=[('paypal', 'PayPal'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer')], max_length=20, null=True, verbose_name='Payment Method')),
|
||||||
|
('status', models.CharField(choices=[('address', 'Shipping Address'), ('payment', 'Payment Method'), ('confirm', 'Confirmation'), ('completed', 'Completed')], default='address', max_length=20, verbose_name='Status')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('cart', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='shop.cart')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('shipping_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.shippingaddress')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProductVariant',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('size', models.CharField(blank=True, choices=[('XS', 'Extra Small'), ('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'Extra Large'), ('XXL', '2X Large'), ('XXXL', '3X Large')], max_length=10, verbose_name='Größe')),
|
||||||
|
('color', models.CharField(blank=True, max_length=50, verbose_name='Farbe')),
|
||||||
|
('sku', models.CharField(max_length=50, unique=True, verbose_name='Artikelnummer')),
|
||||||
|
('price_adjustment', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Preisanpassung')),
|
||||||
|
('stock', models.PositiveIntegerField(default=0, verbose_name='Lagerbestand')),
|
||||||
|
('image', models.ImageField(blank=True, upload_to='variants/', verbose_name='Variantenbild')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='shop.product', verbose_name='Produkt')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Produktvariante',
|
||||||
|
'verbose_name_plural': 'Produktvarianten',
|
||||||
|
'unique_together': {('product', 'size', 'color')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 5.2.1 on 2025-05-30 10:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shop', '0002_producttype_alter_category_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContactMessage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('email', models.EmailField(max_length=254, verbose_name='E-Mail')),
|
||||||
|
('subject', models.CharField(max_length=200, verbose_name='Betreff')),
|
||||||
|
('message', models.TextField(verbose_name='Nachricht')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Kontaktnachricht',
|
||||||
|
'verbose_name_plural': 'Kontaktnachrichten',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class FursuitGallery(models.Model):
|
||||||
|
name = models.CharField(_('Name'), max_length=200)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
description = models.TextField(_('Description'))
|
||||||
|
description_en = models.TextField(_('Description (English)'))
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.name)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Fursuit Gallery')
|
||||||
|
verbose_name_plural = _('Fursuit Galleries')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class GalleryImage(models.Model):
|
||||||
|
gallery = models.ForeignKey(FursuitGallery, on_delete=models.CASCADE, related_name='images')
|
||||||
|
image = models.ImageField(upload_to='gallery/')
|
||||||
|
title = models.CharField(_('Title'), max_length=200, blank=True)
|
||||||
|
description = models.TextField(_('Description'), blank=True)
|
||||||
|
order = models.PositiveIntegerField(default=0)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order', 'created_at']
|
||||||
|
|
||||||
|
class Category(models.Model):
|
||||||
|
name = models.CharField(_('Name'), max_length=100)
|
||||||
|
slug = models.SlugField(_('Slug'), max_length=100, unique=True)
|
||||||
|
description = models.TextField(_('Beschreibung'), blank=True)
|
||||||
|
image = models.ImageField(_('Bild'), upload_to='categories/', blank=True)
|
||||||
|
parent = models.ForeignKey('self', verbose_name=_('Übergeordnete Kategorie'),
|
||||||
|
on_delete=models.CASCADE, null=True, blank=True,
|
||||||
|
related_name='children')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Kategorie')
|
||||||
|
verbose_name_plural = _('Kategorien')
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.name)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class ProductType(models.Model):
|
||||||
|
name = models.CharField(_('Name'), max_length=100)
|
||||||
|
has_sizes = models.BooleanField(_('Hat Größen'), default=False)
|
||||||
|
has_colors = models.BooleanField(_('Hat Farben'), default=False)
|
||||||
|
has_custom_design = models.BooleanField(_('Erlaubt Custom Design'), default=False)
|
||||||
|
requires_measurements = models.BooleanField(_('Benötigt Maße'), default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Produkttyp')
|
||||||
|
verbose_name_plural = _('Produkttypen')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
category = models.ForeignKey(Category, verbose_name=_('Kategorie'),
|
||||||
|
on_delete=models.CASCADE, related_name='products')
|
||||||
|
product_type = models.ForeignKey(ProductType, verbose_name=_('Produkttyp'),
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(_('Name'), max_length=200)
|
||||||
|
slug = models.SlugField(_('Slug'), max_length=200, unique=True, default='')
|
||||||
|
description = models.TextField(_('Beschreibung'))
|
||||||
|
price = models.DecimalField(_('Preis'), max_digits=10, decimal_places=2,
|
||||||
|
validators=[MinValueValidator(Decimal('0.01'))],
|
||||||
|
default=Decimal('0.00'))
|
||||||
|
stock = models.PositiveIntegerField(_('Lagerbestand'), default=0)
|
||||||
|
available = models.BooleanField(_('Verfügbar'), default=True)
|
||||||
|
created = models.DateTimeField(_('Erstellt'), default=timezone.now)
|
||||||
|
updated = models.DateTimeField(_('Aktualisiert'), auto_now=True)
|
||||||
|
image = models.ImageField(_('Hauptbild'), upload_to='products/')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Produkt')
|
||||||
|
verbose_name_plural = _('Produkte')
|
||||||
|
ordering = ['-created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.name)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class ProductImage(models.Model):
|
||||||
|
product = models.ForeignKey(Product, verbose_name=_('Produkt'),
|
||||||
|
on_delete=models.CASCADE, related_name='images')
|
||||||
|
image = models.ImageField(_('Bild'), upload_to='products/')
|
||||||
|
alt_text = models.CharField(_('Alternativer Text'), max_length=200, blank=True)
|
||||||
|
is_feature = models.BooleanField(_('Ist Hauptbild'), default=False)
|
||||||
|
created = models.DateTimeField(_('Erstellt'), default=timezone.now)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Produktbild')
|
||||||
|
verbose_name_plural = _('Produktbilder')
|
||||||
|
ordering = ['-is_feature', '-created']
|
||||||
|
|
||||||
|
class ProductVariant(models.Model):
|
||||||
|
SIZE_CHOICES = [
|
||||||
|
('XS', 'Extra Small'),
|
||||||
|
('S', 'Small'),
|
||||||
|
('M', 'Medium'),
|
||||||
|
('L', 'Large'),
|
||||||
|
('XL', 'Extra Large'),
|
||||||
|
('XXL', '2X Large'),
|
||||||
|
('XXXL', '3X Large'),
|
||||||
|
]
|
||||||
|
|
||||||
|
product = models.ForeignKey(Product, verbose_name=_('Produkt'),
|
||||||
|
on_delete=models.CASCADE, related_name='variants')
|
||||||
|
size = models.CharField(_('Größe'), max_length=10, choices=SIZE_CHOICES, blank=True)
|
||||||
|
color = models.CharField(_('Farbe'), max_length=50, blank=True)
|
||||||
|
sku = models.CharField(_('Artikelnummer'), max_length=50, unique=True)
|
||||||
|
price_adjustment = models.DecimalField(_('Preisanpassung'),
|
||||||
|
max_digits=10, decimal_places=2, default=0)
|
||||||
|
stock = models.PositiveIntegerField(_('Lagerbestand'), default=0)
|
||||||
|
image = models.ImageField(_('Variantenbild'), upload_to='variants/', blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Produktvariante')
|
||||||
|
verbose_name_plural = _('Produktvarianten')
|
||||||
|
unique_together = ['product', 'size', 'color']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
variant_parts = []
|
||||||
|
if self.size:
|
||||||
|
variant_parts.append(f"Größe: {self.size}")
|
||||||
|
if self.color:
|
||||||
|
variant_parts.append(f"Farbe: {self.color}")
|
||||||
|
return f"{self.product.name} ({', '.join(variant_parts)})"
|
||||||
|
|
||||||
|
class CustomDesign(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', _('Ausstehend')),
|
||||||
|
('approved', _('Genehmigt')),
|
||||||
|
('rejected', _('Abgelehnt')),
|
||||||
|
('in_progress', _('In Bearbeitung')),
|
||||||
|
('completed', _('Abgeschlossen')),
|
||||||
|
]
|
||||||
|
|
||||||
|
product = models.ForeignKey(Product, verbose_name=_('Produkt'),
|
||||||
|
on_delete=models.CASCADE, related_name='custom_designs')
|
||||||
|
customer = models.ForeignKey('auth.User', verbose_name=_('Kunde'),
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
design_file = models.FileField(_('Design-Datei'), upload_to='designs/')
|
||||||
|
notes = models.TextField(_('Anmerkungen'))
|
||||||
|
created = models.DateTimeField(_('Erstellt'), default=timezone.now)
|
||||||
|
status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Custom Design')
|
||||||
|
verbose_name_plural = _('Custom Designs')
|
||||||
|
ordering = ['-created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Custom Design für {self.product.name} von {self.customer.username}"
|
||||||
|
|
||||||
|
class DesignTemplate(models.Model):
|
||||||
|
name = models.CharField(_('Name'), max_length=200)
|
||||||
|
name_en = models.CharField(_('Name (English)'), max_length=200)
|
||||||
|
description = models.TextField(_('Description'))
|
||||||
|
description_en = models.TextField(_('Description (English)'))
|
||||||
|
image = models.ImageField(upload_to='designs/')
|
||||||
|
model_3d = models.FileField(_('3D Model'), upload_to='3d_models/', null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class CustomerDesign(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(_('Design Name'), max_length=200)
|
||||||
|
design_file = models.FileField(upload_to='customer_designs/')
|
||||||
|
notes = models.TextField(_('Notes'), blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} - {self.name}"
|
||||||
|
|
||||||
|
class Order(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', _('Pending')),
|
||||||
|
('confirmed', _('Confirmed')),
|
||||||
|
('in_progress', _('In Progress')),
|
||||||
|
('completed', _('Completed')),
|
||||||
|
('cancelled', _('Cancelled')),
|
||||||
|
]
|
||||||
|
|
||||||
|
PAYMENT_METHODS = [
|
||||||
|
('paypal', 'PayPal'),
|
||||||
|
('credit_card', _('Credit Card')),
|
||||||
|
('bank_transfer', _('Bank Transfer')),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHODS)
|
||||||
|
customer_design = models.ForeignKey(CustomerDesign, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
design_template = models.ForeignKey(DesignTemplate, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
special_instructions = models.TextField(blank=True)
|
||||||
|
|
||||||
|
# Nur für Fursuits
|
||||||
|
measurements = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Order {self.id} - {self.product.name}"
|
||||||
|
|
||||||
|
class OrderProgress(models.Model):
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='progress_updates')
|
||||||
|
title = models.CharField(_('Title'), max_length=200)
|
||||||
|
description = models.TextField(_('Description'))
|
||||||
|
image = models.ImageField(upload_to='progress/', null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Progress update for Order {self.order.id}"
|
||||||
|
|
||||||
|
class Cart(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def get_total(self):
|
||||||
|
return sum(item.get_subtotal() for item in self.items.all())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Cart #{self.id} - {self.user.username}"
|
||||||
|
|
||||||
|
class CartItem(models.Model):
|
||||||
|
cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE)
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
quantity = models.PositiveIntegerField(default=1)
|
||||||
|
size = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
notes = models.TextField(blank=True, null=True)
|
||||||
|
custom_design = models.ImageField(upload_to='custom_designs/', blank=True, null=True)
|
||||||
|
added_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def get_subtotal(self):
|
||||||
|
return self.product.base_price * self.quantity
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-added_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.quantity}x {self.product.name} in Cart #{self.cart.id}"
|
||||||
|
|
||||||
|
class ShippingAddress(models.Model):
|
||||||
|
COUNTRY_CHOICES = [
|
||||||
|
('DE', 'Deutschland'),
|
||||||
|
('AT', 'Österreich'),
|
||||||
|
('CH', 'Schweiz'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
first_name = models.CharField(_('First Name'), max_length=100)
|
||||||
|
last_name = models.CharField(_('Last Name'), max_length=100)
|
||||||
|
email = models.EmailField(_('Email'))
|
||||||
|
address = models.CharField(_('Address'), max_length=200)
|
||||||
|
city = models.CharField(_('City'), max_length=100)
|
||||||
|
zip = models.CharField(_('ZIP Code'), max_length=10)
|
||||||
|
country = models.CharField(_('Country'), max_length=2, choices=COUNTRY_CHOICES)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.first_name} {self.last_name}, {self.city}"
|
||||||
|
|
||||||
|
def get_full_address(self):
|
||||||
|
return f"{self.address}, {self.zip} {self.city}, {self.get_country_display()}"
|
||||||
|
|
||||||
|
class Checkout(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('address', _('Shipping Address')),
|
||||||
|
('payment', _('Payment Method')),
|
||||||
|
('confirm', _('Confirmation')),
|
||||||
|
('completed', _('Completed')),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
cart = models.OneToOneField(Cart, on_delete=models.CASCADE)
|
||||||
|
shipping_address = models.ForeignKey(ShippingAddress, on_delete=models.SET_NULL, null=True)
|
||||||
|
payment_method = models.CharField(_('Payment Method'), max_length=20, choices=Order.PAYMENT_METHODS, null=True)
|
||||||
|
status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='address')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Checkout #{self.id} - {self.user.username}"
|
||||||
|
|
||||||
|
def get_total(self):
|
||||||
|
cart_total = self.cart.get_total()
|
||||||
|
if cart_total < 200:
|
||||||
|
return cart_total + Decimal('5.99')
|
||||||
|
return cart_total
|
||||||
|
|
||||||
|
class PayPalPayment(models.Model):
|
||||||
|
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='paypal_payment')
|
||||||
|
payment_id = models.CharField(_('PayPal Payment ID'), max_length=100)
|
||||||
|
payer_id = models.CharField(_('PayPal Payer ID'), max_length=100)
|
||||||
|
status = models.CharField(_('Payment Status'), max_length=20, choices=[
|
||||||
|
('pending', _('Pending')),
|
||||||
|
('completed', _('Completed')),
|
||||||
|
('failed', _('Failed')),
|
||||||
|
('refunded', _('Refunded')),
|
||||||
|
])
|
||||||
|
amount = models.DecimalField(_('Amount'), max_digits=10, decimal_places=2)
|
||||||
|
currency = models.CharField(_('Currency'), max_length=3, default='EUR')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"PayPal Payment {self.payment_id} for Order #{self.order.id}"
|
||||||
|
|
||||||
|
class PaymentError(models.Model):
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='payment_errors')
|
||||||
|
error_code = models.CharField(_('Error Code'), max_length=100)
|
||||||
|
error_message = models.TextField(_('Error Message'))
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Payment Error for Order #{self.order.id}: {self.error_code}"
|
||||||
|
|
||||||
|
class ContactMessage(models.Model):
|
||||||
|
name = models.CharField(_('Name'), max_length=100)
|
||||||
|
email = models.EmailField(_('E-Mail'))
|
||||||
|
subject = models.CharField(_('Betreff'), max_length=200)
|
||||||
|
message = models.TextField(_('Nachricht'))
|
||||||
|
created_at = models.DateTimeField(_('Erstellt am'), auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Kontaktnachricht')
|
||||||
|
verbose_name_plural = _('Kontaktnachrichten')
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.subject} - {self.name}"
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""
|
||||||
|
Django settings for shop project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.2.1.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-8m^(a*6xx46y=v*z8j*s*f=hup!+cyzghx8e9f^eugbg1)o!1s'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
# 'rest_framework', # Temporär auskommentiert
|
||||||
|
'products',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'shop.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'shop.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'de'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Berlin'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
LOGIN_REDIRECT_URL = 'product_list'
|
||||||
|
LOGOUT_REDIRECT_URL = 'product_list'
|
||||||
|
|
||||||
|
# REST Framework - Temporär auskommentiert
|
||||||
|
# REST_FRAMEWORK = {
|
||||||
|
# 'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
# 'rest_framework.permissions.IsAuthenticatedOrReadOnly',
|
||||||
|
# ],
|
||||||
|
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||||
|
# 'PAGE_SIZE': 10
|
||||||
|
# }
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
from django.db.models.signals import post_save, pre_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from .models import Order, Product, PaymentError
|
||||||
|
from .emails import (
|
||||||
|
send_order_confirmation,
|
||||||
|
send_order_status_update,
|
||||||
|
send_shipping_confirmation,
|
||||||
|
send_admin_notification,
|
||||||
|
send_low_stock_notification
|
||||||
|
)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Order)
|
||||||
|
def handle_order_notifications(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Sendet E-Mail-Benachrichtigungen basierend auf Bestellstatus
|
||||||
|
"""
|
||||||
|
if created:
|
||||||
|
# Neue Bestellung - Admin benachrichtigen
|
||||||
|
notification_type = 'fursuit_order' if any(p.product_type == 'fursuit' for p in instance.products.all()) else 'new_order'
|
||||||
|
if hasattr(instance, 'customer_design') and instance.customer_design:
|
||||||
|
notification_type = 'custom_design'
|
||||||
|
|
||||||
|
send_admin_notification(None, instance, notification_type)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Status-Änderung
|
||||||
|
try:
|
||||||
|
old_instance = Order.objects.get(id=instance.id)
|
||||||
|
if old_instance.status != instance.status:
|
||||||
|
# Status hat sich geändert - Kunde benachrichtigen
|
||||||
|
send_order_status_update(None, instance)
|
||||||
|
|
||||||
|
# Bei Versand zusätzlich Versandbestätigung senden
|
||||||
|
if instance.status == 'completed' and instance.tracking_number:
|
||||||
|
send_shipping_confirmation(None, instance)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@receiver(post_save, sender=PaymentError)
|
||||||
|
def handle_payment_error(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Benachrichtigt den Admin über Zahlungsfehler
|
||||||
|
"""
|
||||||
|
if created:
|
||||||
|
send_admin_notification(None, instance.order, 'payment_failed', {
|
||||||
|
'payment_error': instance
|
||||||
|
})
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=Product)
|
||||||
|
def check_stock_level(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Überprüft den Lagerbestand und sendet Benachrichtigungen bei niedrigem Stand
|
||||||
|
"""
|
||||||
|
if not instance.pk: # Neues Produkt
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_instance = Product.objects.get(pk=instance.pk)
|
||||||
|
|
||||||
|
# Wenn der Lagerbestand unter den Schwellenwert fällt
|
||||||
|
if (old_instance.stock > settings.LOW_STOCK_THRESHOLD and
|
||||||
|
instance.stock <= settings.LOW_STOCK_THRESHOLD):
|
||||||
|
send_low_stock_notification(None, instance)
|
||||||
|
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,497 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Kasico Art & Design - Fursuit Shop{% endblock %}</title>
|
||||||
|
<!-- Bootstrap 5 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Font Awesome für Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Quicksand:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Ko-fi Widget -->
|
||||||
|
<script type='text/javascript' src='https://storage.ko-fi.com/cdn/widget/Widget_2.js'></script>
|
||||||
|
<script type='text/javascript'>
|
||||||
|
kofiwidget2.init('Unterstütze mich auf Ko-fi', '#c800ff', 'C0C41FM0ZT');
|
||||||
|
</script>
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="{% static 'css/kofi-button.css' %}">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6; /* Helles Lila */
|
||||||
|
--secondary-color: #EC4899; /* Pink */
|
||||||
|
--accent-color: #F59E0B; /* Orange */
|
||||||
|
--dark-color: #1F2937; /* Dunkelgrau */
|
||||||
|
--light-color: #F3E8FF; /* Helles Lila */
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
--gradient-end: #F59E0B;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Quicksand', sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--light-color), #FDF2F8);
|
||||||
|
color: var(--dark-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Fredoka', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Styling */
|
||||||
|
.navbar {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
padding: 1rem 0;
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: rgba(255,255,255,0.9) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: white !important;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
border-color: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.9%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.navbar-collapse {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Container */
|
||||||
|
.content-container {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
padding: 3rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 95%;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container::before {
|
||||||
|
content: '🐾';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container::after {
|
||||||
|
content: '🐾';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
right: -10px;
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 160px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
border: none;
|
||||||
|
color: var(--white-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-light {
|
||||||
|
background: var(--white-color);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-light:hover {
|
||||||
|
background: var(--light-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.furry-card {
|
||||||
|
background: var(--white-color);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
max-width: 300px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons */
|
||||||
|
.furry-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
color: var(--white-color);
|
||||||
|
font-size: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-middle), var(--gradient-start));
|
||||||
|
color: var(--white-color);
|
||||||
|
padding: 4rem 0 2rem;
|
||||||
|
margin-top: 4rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, var(--gradient-middle), var(--gradient-start));
|
||||||
|
transform: skewY(-1deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: var(--white-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pawWiggle {
|
||||||
|
0%, 100% { transform: translateY(-50%) rotate(-15deg); }
|
||||||
|
50% { transform: translateY(-50%) rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ko-Fi Button Styling */
|
||||||
|
.kofi-button {
|
||||||
|
background: #FF5F5F;
|
||||||
|
color: white !important;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
margin-right: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 95, 95, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kofi-button:hover {
|
||||||
|
background: #FF7070;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 95, 95, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kofi-icon-container {
|
||||||
|
position: relative;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kofi-icon-container img {
|
||||||
|
height: 20px;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- CSRF Token für AJAX Requests -->
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="navbar navbar-expand-lg sticky-top">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'shop:home' %}">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}" alt="Kasico Art & Design Logo">
|
||||||
|
<span class="d-none d-sm-inline">Kasico</span>
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}"
|
||||||
|
href="{% url 'shop:home' %}">
|
||||||
|
<i class="fas fa-home me-1"></i> Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'shop' %}active{% endif %}"
|
||||||
|
href="{% url 'products:product_list' %}">
|
||||||
|
<i class="fas fa-store me-1"></i> Shop
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'gallery' %}active{% endif %}"
|
||||||
|
href="{% url 'products:gallery' %}">
|
||||||
|
<i class="fas fa-images me-1"></i> Galerie
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'custom_order' %}active{% endif %}"
|
||||||
|
href="{% url 'products:custom_order' %}">
|
||||||
|
<i class="fas fa-magic me-1"></i> Custom Orders
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<!-- Ko-Fi Button -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<script type='text/javascript'>kofiwidget2.draw();</script>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'products:cart_detail' %}">
|
||||||
|
<i class="fas fa-shopping-cart me-1"></i> Warenkorb
|
||||||
|
{% with total_items=cart.get_total_items %}
|
||||||
|
{% if total_items > 0 %}
|
||||||
|
<span class="badge bg-danger">{{ total_items }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'products:profile' %}">
|
||||||
|
<i class="fas fa-user me-1"></i> Profil
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'logout' %}">
|
||||||
|
<i class="fas fa-sign-out-alt me-1"></i> Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'login' %}">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'products:register' %}">
|
||||||
|
<i class="fas fa-user-plus me-1"></i> Registrieren
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container py-5">
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mt-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5><i class="fas fa-paw me-2"></i> Über uns</h5>
|
||||||
|
<p>Kasico Art & Design - Ihre erste Wahl für hochwertige, handgefertigte Fursuits und Custom Designs.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5><i class="fas fa-envelope me-2"></i> Kontakt</h5>
|
||||||
|
<p>Email: info@kasico-art.com</p>
|
||||||
|
<p>Discord: Kasico#1234</p>
|
||||||
|
<p>Telegram: @KasicoArt</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5><i class="fas fa-link me-2"></i> Quick Links</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><a href="#faq"><i class="fas fa-question-circle me-2"></i> FAQ</a></li>
|
||||||
|
<li><a href="#terms"><i class="fas fa-file-contract me-2"></i> AGB</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col text-center">
|
||||||
|
<p class="mb-0">© 2025 Kasico Art & Design. Alle Rechte vorbehalten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- CSRF Token Update Script -->
|
||||||
|
<script>
|
||||||
|
// CSRF Token aus dem Cookie holen
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF Token für AJAX Requests setzen
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
|
||||||
|
// Token bei jedem AJAX Request mitschicken
|
||||||
|
function csrfSafeMethod(method) {
|
||||||
|
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJAX Setup für CSRF
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Token bei Seitenlade aktualisieren
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const tokenElement = document.querySelector('input[name="csrfmiddlewaretoken"]');
|
||||||
|
if (tokenElement) {
|
||||||
|
tokenElement.value = csrftoken;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Shopping Cart" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.quantity-input {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-item {
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-item {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-item:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Cart Header -->
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="display-5 fw-bold mb-4">Ihr Warenkorb</h1>
|
||||||
|
{% if cart_items %}
|
||||||
|
<p class="lead">{{ cart_items|length }} Artikel in Ihrem Warenkorb</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="lead">Ihr Warenkorb ist leer</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if cart_items %}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Cart Items -->
|
||||||
|
<div class="col-lg-8 mb-4">
|
||||||
|
<div class="furry-card">
|
||||||
|
{% for item in cart_items %}
|
||||||
|
<div class="cart-item {% if not forloop.last %}border-bottom{% endif %} py-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<!-- Product Image -->
|
||||||
|
<div class="col-md-3 mb-3 mb-md-0">
|
||||||
|
<img src="{{ item.product.image.url }}"
|
||||||
|
alt="{{ item.product.name }}"
|
||||||
|
class="img-fluid rounded-3"
|
||||||
|
style="width: 100%; height: 150px; object-fit: cover;"
|
||||||
|
onerror="this.src='{% static 'images/placeholder.jpg' %}'">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Details -->
|
||||||
|
<div class="col-md-5 mb-3 mb-md-0">
|
||||||
|
<h3 class="h5 mb-2">{{ item.product.name }}</h3>
|
||||||
|
<p class="text-muted mb-2">{{ item.product.short_description|truncatewords:10 }}</p>
|
||||||
|
{% if item.size %}
|
||||||
|
<p class="mb-2"><small>Größe: {{ item.size }}</small></p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="me-3">
|
||||||
|
{% if item.product.on_sale %}
|
||||||
|
<span class="text-decoration-line-through text-muted me-2">{{ item.product.regular_price }} €</span>
|
||||||
|
<span class="text-danger">{{ item.product.sale_price }} €</span>
|
||||||
|
{% else %}
|
||||||
|
<span>{{ item.product.price }} €</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted">x {{ item.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quantity Controls -->
|
||||||
|
<div class="col-md-2 mb-3 mb-md-0">
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
onclick="updateQuantity('{{ item.id }}', -1)">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
<input type="number" class="form-control text-center"
|
||||||
|
value="{{ item.quantity }}" min="1" max="{{ item.product.stock }}"
|
||||||
|
onchange="updateQuantity('{{ item.id }}', 0, this.value)">
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
onclick="updateQuantity('{{ item.id }}', 1)">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item Total & Remove -->
|
||||||
|
<div class="col-md-2 text-end">
|
||||||
|
<p class="h5 mb-2">{{ item.total_price }} €</p>
|
||||||
|
<button class="btn btn-link text-danger p-0"
|
||||||
|
onclick="removeItem('{{ item.id }}')">
|
||||||
|
<i class="fas fa-trash me-1"></i> Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Summary -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="furry-card">
|
||||||
|
<h3 class="h4 mb-4">Bestellübersicht</h3>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Zwischensumme</span>
|
||||||
|
<span>{{ subtotal }} €</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if shipping_cost %}
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Versandkosten</span>
|
||||||
|
<span>{{ shipping_cost }} €</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if discount %}
|
||||||
|
<div class="d-flex justify-content-between mb-2 text-success">
|
||||||
|
<span>Rabatt</span>
|
||||||
|
<span>-{{ discount }} €</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-4">
|
||||||
|
<span class="h5">Gesamtsumme</span>
|
||||||
|
<span class="h5">{{ total }} €</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Coupon Code -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<form method="post" action="{% url 'cart:apply_coupon' %}" class="input-group">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" class="form-control" name="code"
|
||||||
|
placeholder="Gutscheincode eingeben">
|
||||||
|
<button class="btn btn-outline-primary" type="submit">
|
||||||
|
Einlösen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkout Button -->
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{% url 'checkout' %}" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-lock me-2"></i> Zur Kasse
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shipping Info -->
|
||||||
|
<div class="furry-card mt-4">
|
||||||
|
<h4 class="h5 mb-3">Versandinformationen</h4>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-truck text-primary me-2"></i>
|
||||||
|
Kostenloser Versand ab 200 €
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-box text-primary me-2"></i>
|
||||||
|
Sichere Verpackung
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fas fa-undo text-primary me-2"></i>
|
||||||
|
14 Tage Rückgaberecht
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<!-- Empty Cart -->
|
||||||
|
<div class="furry-card text-center py-5">
|
||||||
|
<i class="fas fa-shopping-cart fa-3x mb-3 text-muted"></i>
|
||||||
|
<h2 class="h4 mb-4">Ihr Warenkorb ist noch leer</h2>
|
||||||
|
<p class="text-muted mb-4">Entdecken Sie unsere einzigartigen Fursuits und Accessoires</p>
|
||||||
|
<a href="{% url 'products:product_list' %}" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-store me-2"></i> Zum Shop
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Toast Notifications -->
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
|
||||||
|
<div id="cartToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-header">
|
||||||
|
<i class="fas fa-shopping-cart me-2"></i>
|
||||||
|
<strong class="me-auto">Warenkorb</strong>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body" id="cartToastMessage">
|
||||||
|
Warenkorb wurde aktualisiert!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script>
|
||||||
|
function showToast(message) {
|
||||||
|
const toast = new bootstrap.Toast(document.getElementById('cartToast'));
|
||||||
|
document.getElementById('cartToastMessage').textContent = message;
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuantity(itemId, change, newValue = null) {
|
||||||
|
const quantity = newValue !== null ? newValue :
|
||||||
|
parseInt(document.querySelector(`input[data-item-id="${itemId}"]`).value) + change;
|
||||||
|
|
||||||
|
fetch('/cart/update/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
item_id: itemId,
|
||||||
|
quantity: quantity
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Ein Fehler ist aufgetreten');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('Ein Fehler ist aufgetreten');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(itemId) {
|
||||||
|
if (confirm('Möchten Sie diesen Artikel wirklich aus dem Warenkorb entfernen?')) {
|
||||||
|
fetch('/cart/remove/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
item_id: itemId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Ein Fehler ist aufgetreten');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('Ein Fehler ist aufgetreten');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,468 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Checkout" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.checkout-steps {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: -1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2px;
|
||||||
|
background: #dee2e6;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:last-child::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.completed {
|
||||||
|
color: var(--bs-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.completed .step-number {
|
||||||
|
background: var(--bs-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background: var(--bs-gray);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active .step-number {
|
||||||
|
background: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method.selected {
|
||||||
|
border-color: var(--bs-primary);
|
||||||
|
box-shadow: 0 0 0 1px var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method.selected .payment-check {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<!-- Checkout-Schritte -->
|
||||||
|
<div class="checkout-steps">
|
||||||
|
<div class="step {% if step == 'address' %}active{% elif step == 'payment' or step == 'confirm' %}completed{% endif %}">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<div class="step-title">{% trans "Shipping" %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="step {% if step == 'payment' %}active{% elif step == 'confirm' %}completed{% endif %}">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-title">{% trans "Payment" %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="step {% if step == 'confirm' %}active{% endif %}">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-title">{% trans "Confirm" %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Lieferadresse -->
|
||||||
|
{% if step == 'address' %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title h4 mb-4">{% trans "Shipping Address" %}</h3>
|
||||||
|
<form method="post" action="{% url 'shop:checkout' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="step" value="address">
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="firstName" class="form-label">{% trans "First Name" %}</label>
|
||||||
|
<input type="text" class="form-control" id="firstName" name="first_name"
|
||||||
|
value="{{ form.first_name.value|default:'' }}" required>
|
||||||
|
{% if form.first_name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.first_name.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="lastName" class="form-label">{% trans "Last Name" %}</label>
|
||||||
|
<input type="text" class="form-control" id="lastName" name="last_name"
|
||||||
|
value="{{ form.last_name.value|default:'' }}" required>
|
||||||
|
{% if form.last_name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.last_name.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="email" class="form-label">{% trans "Email" %}</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email"
|
||||||
|
value="{{ form.email.value|default:'' }}" required>
|
||||||
|
{% if form.email.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.email.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="address" class="form-label">{% trans "Street Address" %}</label>
|
||||||
|
<input type="text" class="form-control" id="address" name="address"
|
||||||
|
value="{{ form.address.value|default:'' }}" required>
|
||||||
|
{% if form.address.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.address.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="city" class="form-label">{% trans "City" %}</label>
|
||||||
|
<input type="text" class="form-control" id="city" name="city"
|
||||||
|
value="{{ form.city.value|default:'' }}" required>
|
||||||
|
{% if form.city.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.city.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="zip" class="form-label">{% trans "ZIP Code" %}</label>
|
||||||
|
<input type="text" class="form-control" id="zip" name="zip"
|
||||||
|
value="{{ form.zip.value|default:'' }}" required>
|
||||||
|
{% if form.zip.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.zip.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="country" class="form-label">{% trans "Country" %}</label>
|
||||||
|
<select class="form-select" id="country" name="country" required>
|
||||||
|
<option value="">{% trans "Choose..." %}</option>
|
||||||
|
<option value="DE" {% if form.country.value == 'DE' %}selected{% endif %}>Deutschland</option>
|
||||||
|
<option value="AT" {% if form.country.value == 'AT' %}selected{% endif %}>Österreich</option>
|
||||||
|
<option value="CH" {% if form.country.value == 'CH' %}selected{% endif %}>Schweiz</option>
|
||||||
|
</select>
|
||||||
|
{% if form.country.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.country.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
{% trans "Continue to Payment" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zahlungsmethode -->
|
||||||
|
{% elif step == 'payment' %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title h4 mb-4">{% trans "Payment Method" %}</h3>
|
||||||
|
<form method="post" action="{% url 'shop:checkout' %}" id="paymentForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="step" value="payment">
|
||||||
|
<input type="hidden" name="payment_method" id="selectedPayment" value="{{ form.payment_method.value|default:'' }}">
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- PayPal -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 payment-method {% if form.payment_method.value == 'paypal' %}selected{% endif %}"
|
||||||
|
onclick="selectPayment('paypal')">
|
||||||
|
<div class="payment-check text-primary">
|
||||||
|
<i class="bi bi-check-circle-fill"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-paypal display-4"></i>
|
||||||
|
<h5 class="mt-3">PayPal</h5>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
{% trans "Fast and secure payment with PayPal" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kreditkarte -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 payment-method {% if form.payment_method.value == 'credit_card' %}selected{% endif %}"
|
||||||
|
onclick="selectPayment('credit_card')">
|
||||||
|
<div class="payment-check text-primary">
|
||||||
|
<i class="bi bi-check-circle-fill"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-credit-card display-4"></i>
|
||||||
|
<h5 class="mt-3">{% trans "Credit Card" %}</h5>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
{% trans "Pay with Visa, Mastercard, or American Express" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Überweisung -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 payment-method {% if form.payment_method.value == 'bank_transfer' %}selected{% endif %}"
|
||||||
|
onclick="selectPayment('bank_transfer')">
|
||||||
|
<div class="payment-check text-primary">
|
||||||
|
<i class="bi bi-check-circle-fill"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-bank display-4"></i>
|
||||||
|
<h5 class="mt-3">{% trans "Bank Transfer" %}</h5>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
{% trans "Pay via bank transfer" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if form.payment_method.errors %}
|
||||||
|
<div class="alert alert-danger mt-3">
|
||||||
|
{{ form.payment_method.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'shop:checkout' %}?step=address" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i>
|
||||||
|
{% trans "Back to Shipping" %}
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
{% trans "Continue to Confirmation" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bestellbestätigung -->
|
||||||
|
{% elif step == 'confirm' %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title h4 mb-4">{% trans "Order Confirmation" %}</h3>
|
||||||
|
|
||||||
|
<!-- Lieferadresse -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>{% trans "Shipping Address" %}</h5>
|
||||||
|
<p class="mb-0">{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}</p>
|
||||||
|
<p class="mb-0">{{ order.shipping_address.address }}</p>
|
||||||
|
<p class="mb-0">{{ order.shipping_address.zip }} {{ order.shipping_address.city }}</p>
|
||||||
|
<p>{{ order.shipping_address.get_country_display }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zahlungsmethode -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>{% trans "Payment Method" %}</h5>
|
||||||
|
<p>
|
||||||
|
{% if order.payment_method == 'paypal' %}
|
||||||
|
<i class="bi bi-paypal"></i> PayPal
|
||||||
|
{% elif order.payment_method == 'credit_card' %}
|
||||||
|
<i class="bi bi-credit-card"></i> {% trans "Credit Card" %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-bank"></i> {% trans "Bank Transfer" %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bestellte Artikel -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>{% trans "Order Items" %}</h5>
|
||||||
|
{% for item in cart.items.all %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="fw-bold">{{ item.quantity }}x</span>
|
||||||
|
{{ item.product.name }}
|
||||||
|
{% if item.size %}
|
||||||
|
<small class="text-muted">({{ item.size }})</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span>{{ item.get_subtotal }} €</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Gesamtbetrag -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>{% trans "Subtotal" %}</span>
|
||||||
|
<span>{{ cart.get_total }} €</span>
|
||||||
|
</div>
|
||||||
|
{% if cart.get_total < 200 %}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>{% trans "Shipping" %}</span>
|
||||||
|
<span>5.99 €</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="d-flex justify-content-between text-success">
|
||||||
|
<span>{% trans "Shipping" %}</span>
|
||||||
|
<span>{% trans "FREE" %}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex justify-content-between fw-bold">
|
||||||
|
<span>{% trans "Total" %}</span>
|
||||||
|
<span>
|
||||||
|
{% if cart.get_total < 200 %}
|
||||||
|
{{ cart.get_total|add:"5.99" }} €
|
||||||
|
{% else %}
|
||||||
|
{{ cart.get_total }} €
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'shop:checkout' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="step" value="confirm">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'shop:checkout' %}?step=payment" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i>
|
||||||
|
{% trans "Back to Payment" %}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-check-circle"></i>
|
||||||
|
{% trans "Place Order" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bestellübersicht -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans "Order Summary" %}</h5>
|
||||||
|
|
||||||
|
{% for item in cart.items.all %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="fw-bold">{{ item.quantity }}x</span>
|
||||||
|
{{ item.product.name }}
|
||||||
|
</div>
|
||||||
|
<span>{{ item.get_subtotal }} €</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>{% trans "Subtotal" %}</span>
|
||||||
|
<span>{{ cart.get_total }} €</span>
|
||||||
|
</div>
|
||||||
|
{% if cart.get_total < 200 %}
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>{% trans "Shipping" %}</span>
|
||||||
|
<span>5.99 €</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="d-flex justify-content-between mb-2 text-success">
|
||||||
|
<span>{% trans "Shipping" %}</span>
|
||||||
|
<span>{% trans "FREE" %}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex justify-content-between fw-bold">
|
||||||
|
<span>{% trans "Total" %}</span>
|
||||||
|
<span>
|
||||||
|
{% if cart.get_total < 200 %}
|
||||||
|
{{ cart.get_total|add:"5.99" }} €
|
||||||
|
{% else %}
|
||||||
|
{{ cart.get_total }} €
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{% if step == 'payment' %}
|
||||||
|
<script>
|
||||||
|
function selectPayment(method) {
|
||||||
|
// Alle payment-method Cards zurücksetzen
|
||||||
|
document.querySelectorAll('.payment-method').forEach(card => {
|
||||||
|
card.classList.remove('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ausgewählte Card markieren
|
||||||
|
const selectedCard = document.querySelector(`.payment-method[onclick="selectPayment('${method}')"]`);
|
||||||
|
if (selectedCard) {
|
||||||
|
selectedCard.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden Input aktualisieren
|
||||||
|
document.getElementById('selectedPayment').value = method;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Kontakt - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white py-3">
|
||||||
|
<h1 class="h3 mb-0 text-center">Kontaktieren Sie uns</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Kontaktinformationen -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<i class="fas fa-envelope fa-2x text-primary me-3"></i>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">E-Mail</h5>
|
||||||
|
<p class="mb-0">info@kasico-art.de</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-phone fa-2x text-primary me-3"></i>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Telefon</h5>
|
||||||
|
<p class="mb-0">+49 123 456789</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<i class="fas fa-clock fa-2x text-primary me-3"></i>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Geschäftszeiten</h5>
|
||||||
|
<p class="mb-0">Mo-Fr: 9:00 - 18:00 Uhr<br>Sa: 10:00 - 14:00 Uhr</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<!-- Kontaktformular -->
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-success mb-4">
|
||||||
|
{% for message in messages %}
|
||||||
|
{{ message }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="name" class="form-label">Name *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Bitte geben Sie Ihren Namen ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="email" class="form-label">E-Mail *</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Bitte geben Sie eine gültige E-Mail-Adresse ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="subject" class="form-label">Betreff *</label>
|
||||||
|
<input type="text" class="form-control" id="subject" name="subject" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Bitte geben Sie einen Betreff ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="message" class="form-label">Ihre Nachricht *</label>
|
||||||
|
<textarea class="form-control" id="message" name="message" rows="5" required></textarea>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Bitte geben Sie Ihre Nachricht ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i>Nachricht senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ Link -->
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="text-muted">Haben Sie eine allgemeine Frage?</p>
|
||||||
|
<a href="#" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-question-circle me-2"></i>FAQ ansehen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Formularvalidierung
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
Array.prototype.slice.call(forms).forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.notification {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.notification-type {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.type-new_order {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.type-payment_failed {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
.type-custom_design {
|
||||||
|
background-color: #6f42c1;
|
||||||
|
}
|
||||||
|
.type-fursuit_order {
|
||||||
|
background-color: #fd7e14;
|
||||||
|
}
|
||||||
|
.customer-info {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.product {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.product:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.product-image {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-fursuit {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.badge-printed {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 2px solid #dee2e6;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{ logo_url }}" alt="Fursuit Shop" class="logo">
|
||||||
|
<h1>
|
||||||
|
{% if notification_type == 'new_order' %}
|
||||||
|
{% trans "New Order Received" %}
|
||||||
|
{% elif notification_type == 'payment_failed' %}
|
||||||
|
{% trans "Payment Failed" %}
|
||||||
|
{% elif notification_type == 'custom_design' %}
|
||||||
|
{% trans "New Custom Design Order" %}
|
||||||
|
{% elif notification_type == 'fursuit_order' %}
|
||||||
|
{% trans "New Fursuit Order" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Order Notification" %}
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notification">
|
||||||
|
<span class="notification-type type-{{ notification_type }}">
|
||||||
|
{% if notification_type == 'new_order' %}
|
||||||
|
{% trans "New Order" %}
|
||||||
|
{% elif notification_type == 'payment_failed' %}
|
||||||
|
{% trans "Payment Failed" %}
|
||||||
|
{% elif notification_type == 'custom_design' %}
|
||||||
|
{% trans "Custom Design" %}
|
||||||
|
{% elif notification_type == 'fursuit_order' %}
|
||||||
|
{% trans "Fursuit Order" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h2>{% trans "Order" %} #{{ order.id }}</h2>
|
||||||
|
|
||||||
|
<div class="customer-info">
|
||||||
|
<h3>{% trans "Customer Information" %}</h3>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Name" %}:</strong>
|
||||||
|
{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Email" %}:</strong>
|
||||||
|
{{ order.shipping_address.email }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Address" %}:</strong><br>
|
||||||
|
{{ order.shipping_address.address }}<br>
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}<br>
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>{% trans "Ordered Items" %}</h3>
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
<div class="product">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="product-image">
|
||||||
|
{% endif %}
|
||||||
|
<div style="flex-grow: 1;">
|
||||||
|
<h4 style="margin: 0;">{{ product.name }}</h4>
|
||||||
|
{% if product.product_type == 'fursuit' %}
|
||||||
|
<span class="badge badge-fursuit">{% trans "Fursuit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-printed">{% trans "Printed Item" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<strong>{{ product.base_price }} €</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="total">
|
||||||
|
{% trans "Total" %}: {{ order.total_price }} €
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notification_type == 'payment_failed' and payment_error %}
|
||||||
|
<div style="margin-top: 20px; padding: 15px; background-color: #f8d7da; border-radius: 4px; color: #721c24;">
|
||||||
|
<h3 style="margin-top: 0;">{% trans "Payment Error Details" %}</h3>
|
||||||
|
<p><strong>{% trans "Error Code" %}:</strong> {{ payment_error.error_code }}</p>
|
||||||
|
<p><strong>{% trans "Error Message" %}:</strong> {{ payment_error.error_message }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if notification_type == 'custom_design' and order.customer_design %}
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h3>{% trans "Custom Design Details" %}</h3>
|
||||||
|
<p><strong>{% trans "Design Name" %}:</strong> {{ order.customer_design.name }}</p>
|
||||||
|
{% if order.customer_design.notes %}
|
||||||
|
<p><strong>{% trans "Notes" %}:</strong> {{ order.customer_design.notes }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if order.customer_design.design_file %}
|
||||||
|
<p><strong>{% trans "Design File" %}:</strong>
|
||||||
|
<a href="{{ order.customer_design.design_file.url }}">
|
||||||
|
{% trans "Download Design File" %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ order_url }}" class="button">
|
||||||
|
{% trans "View Order Details" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if notification_type == 'new_order' %}
|
||||||
|
{% trans "New Order Received" %}
|
||||||
|
{% elif notification_type == 'payment_failed' %}
|
||||||
|
{% trans "Payment Failed" %}
|
||||||
|
{% elif notification_type == 'custom_design' %}
|
||||||
|
{% trans "New Custom Design Order" %}
|
||||||
|
{% elif notification_type == 'fursuit_order' %}
|
||||||
|
{% trans "New Fursuit Order" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Order Notification" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% trans "Order" %} #{{ order.id }}
|
||||||
|
|
||||||
|
{% trans "Customer Information" %}:
|
||||||
|
{% trans "Name" %}: {{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
|
||||||
|
{% trans "Email" %}: {{ order.shipping_address.email }}
|
||||||
|
|
||||||
|
{% trans "Address" %}:
|
||||||
|
{{ order.shipping_address.address }}
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
|
||||||
|
{% trans "Ordered Items" %}:
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
- {{ product.name }} ({% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %})
|
||||||
|
{{ product.base_price }} €
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% trans "Total" %}: {{ order.total_price }} €
|
||||||
|
|
||||||
|
{% if notification_type == 'payment_failed' and payment_error %}
|
||||||
|
{% trans "Payment Error Details" %}:
|
||||||
|
{% trans "Error Code" %}: {{ payment_error.error_code }}
|
||||||
|
{% trans "Error Message" %}: {{ payment_error.error_message }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if notification_type == 'custom_design' and order.customer_design %}
|
||||||
|
{% trans "Custom Design Details" %}:
|
||||||
|
{% trans "Design Name" %}: {{ order.customer_design.name }}
|
||||||
|
{% if order.customer_design.notes %}
|
||||||
|
{% trans "Notes" %}: {{ order.customer_design.notes }}
|
||||||
|
{% endif %}
|
||||||
|
{% if order.customer_design.design_file %}
|
||||||
|
{% trans "Design File" %}: {{ order.customer_design.design_file.url }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% trans "View Order Details" %}: {{ order_url }}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.product {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.product-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.product-details {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-fursuit {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.badge-printed {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.stock-level {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #dc3545;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{ logo_url }}" alt="Fursuit Shop" class="logo">
|
||||||
|
<h1>{% trans "Low Stock Alert" %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert">
|
||||||
|
<strong>{% trans "Warning" %}:</strong>
|
||||||
|
{% trans "The following product is running low on stock and needs attention." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="product-image">
|
||||||
|
{% endif %}
|
||||||
|
<div class="product-details">
|
||||||
|
<h2 style="margin-top: 0;">{{ product.name }}</h2>
|
||||||
|
{% if product.product_type == 'fursuit' %}
|
||||||
|
<span class="badge badge-fursuit">{% trans "Fursuit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-printed">{% trans "Printed Item" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="stock-level">
|
||||||
|
{% trans "Current Stock" %}: {{ product.stock }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "SKU" %}:</strong> {{ product.sku }}<br>
|
||||||
|
<strong>{% trans "Base Price" %}:</strong> {{ product.base_price }} €
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ product_url }}" class="button">
|
||||||
|
{% trans "Manage Product" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% trans "Low Stock Alert" %}
|
||||||
|
|
||||||
|
{% trans "Warning" %}: {% trans "The following product is running low on stock and needs attention." %}
|
||||||
|
|
||||||
|
{% trans "Product Details" %}:
|
||||||
|
{% trans "Name" %}: {{ product.name }}
|
||||||
|
{% trans "Type" %}: {% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %}
|
||||||
|
{% trans "SKU" %}: {{ product.sku }}
|
||||||
|
{% trans "Base Price" %}: {{ product.base_price }} €
|
||||||
|
|
||||||
|
{% trans "Current Stock" %}: {{ product.stock }}
|
||||||
|
|
||||||
|
{% trans "Manage this product at" %}: {{ product_url }}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.order-details {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.product {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.product:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.product-image {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.product-details {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.product-price {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-fursuit {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.badge-printed {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{ logo_url }}" alt="Fursuit Shop" class="logo">
|
||||||
|
<h1>{% trans "Thank You for Your Order!" %}</h1>
|
||||||
|
<p>{% trans "Your order has been confirmed and is being processed." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-details">
|
||||||
|
<h2>{% trans "Order Details" %}</h2>
|
||||||
|
<p><strong>{% trans "Order Number" %}:</strong> #{{ order.id }}</p>
|
||||||
|
<p><strong>{% trans "Order Date" %}:</strong> {{ order.created_at|date:"d.m.Y" }}</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Payment Method" %}:</strong>
|
||||||
|
{% if order.payment_method == 'paypal' %}
|
||||||
|
PayPal
|
||||||
|
{% elif order.payment_method == 'credit_card' %}
|
||||||
|
{% trans "Credit Card" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Bank Transfer" %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>{% trans "Shipping Address" %}</h3>
|
||||||
|
<p>
|
||||||
|
{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}<br>
|
||||||
|
{{ order.shipping_address.address }}<br>
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}<br>
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>{% trans "Ordered Items" %}</h3>
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
<div class="product">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="product-image">
|
||||||
|
{% endif %}
|
||||||
|
<div class="product-details">
|
||||||
|
<h4 style="margin: 0;">{{ product.name }}</h4>
|
||||||
|
{% if product.product_type == 'fursuit' %}
|
||||||
|
<span class="badge badge-fursuit">{% trans "Fursuit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-printed">{% trans "Printed Item" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="product-price">
|
||||||
|
{{ product.base_price }} €
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="total">
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Subtotal" %}:</strong>
|
||||||
|
{{ order.total_price }} €
|
||||||
|
</p>
|
||||||
|
{% if order.total_price < 200 %}
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Shipping" %}:</strong>
|
||||||
|
5.99 €
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 1.2em; font-weight: bold;">
|
||||||
|
<strong>{% trans "Total" %}:</strong>
|
||||||
|
{{ order.total_price|add:"5.99" }} €
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Shipping" %}:</strong>
|
||||||
|
<span style="color: #198754;">{% trans "FREE" %}</span>
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 1.2em; font-weight: bold;">
|
||||||
|
<strong>{% trans "Total" %}:</strong>
|
||||||
|
{{ order.total_price }} €
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ order_url }}" class="button">
|
||||||
|
{% trans "View Order Details" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>
|
||||||
|
{% trans "If you have any questions about your order, please contact our support team." %}<br>
|
||||||
|
<a href="mailto:support@fursuitshop.com">support@fursuitshop.com</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% trans "Thank You for Your Order!" %}
|
||||||
|
|
||||||
|
{% trans "Your order has been confirmed and is being processed." %}
|
||||||
|
|
||||||
|
{% trans "Order Details" %}:
|
||||||
|
{% trans "Order Number" %}: #{{ order.id }}
|
||||||
|
{% trans "Order Date" %}: {{ order.created_at|date:"d.m.Y" }}
|
||||||
|
{% trans "Payment Method" %}: {% if order.payment_method == 'paypal' %}PayPal{% elif order.payment_method == 'credit_card' %}{% trans "Credit Card" %}{% else %}{% trans "Bank Transfer" %}{% endif %}
|
||||||
|
|
||||||
|
{% trans "Shipping Address" %}:
|
||||||
|
{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
|
||||||
|
{{ order.shipping_address.address }}
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
|
||||||
|
{% trans "Ordered Items" %}:
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
- {{ product.name }} ({% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %})
|
||||||
|
{{ product.base_price }} €
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% trans "Subtotal" %}: {{ order.total_price }} €
|
||||||
|
{% if order.total_price < 200 %}
|
||||||
|
{% trans "Shipping" %}: 5.99 €
|
||||||
|
{% trans "Total" %}: {{ order.total_price|add:"5.99" }} €
|
||||||
|
{% else %}
|
||||||
|
{% trans "Shipping" %}: {% trans "FREE" %}
|
||||||
|
{% trans "Total" %}: {{ order.total_price }} €
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% trans "You can view your order details at" %}: {{ order_url }}
|
||||||
|
|
||||||
|
{% trans "If you have any questions about your order, please contact our support team." %}
|
||||||
|
support@fursuitshop.com
|
||||||
|
|
||||||
|
© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.status-update {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-pending {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.status-confirmed {
|
||||||
|
background-color: #0dcaf0;
|
||||||
|
}
|
||||||
|
.status-in_progress {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.status-completed {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
.progress-image {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{ logo_url }}" alt="Fursuit Shop" class="logo">
|
||||||
|
<h1>{% trans "Order Status Update" %}</h1>
|
||||||
|
<p>{% trans "Your order has been updated." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-update">
|
||||||
|
<h2>{% trans "Order" %} #{{ order.id }}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "New Status" %}:</strong><br>
|
||||||
|
<span class="status-badge status-{{ order.status }}">
|
||||||
|
{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if update %}
|
||||||
|
<h3>{{ update.title }}</h3>
|
||||||
|
<p>{{ update.description }}</p>
|
||||||
|
{% if update.image %}
|
||||||
|
<img src="{{ update.image.url }}" alt="{% trans 'Progress Image' %}" class="progress-image">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if order.status == 'confirmed' %}
|
||||||
|
<p>{% trans "Your order has been confirmed and will be processed soon." %}</p>
|
||||||
|
{% elif order.status == 'in_progress' %}
|
||||||
|
<p>{% trans "We are currently working on your order." %}</p>
|
||||||
|
{% elif order.status == 'completed' %}
|
||||||
|
<p>{% trans "Your order has been completed and will be shipped soon." %}</p>
|
||||||
|
{% if order.tracking_number %}
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Tracking Number" %}:</strong>
|
||||||
|
{{ order.tracking_number }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ order_url }}" class="button">
|
||||||
|
{% trans "View Order Details" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>
|
||||||
|
{% trans "If you have any questions about your order, please contact our support team." %}<br>
|
||||||
|
<a href="mailto:support@fursuitshop.com">support@fursuitshop.com</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% trans "Order Status Update" %}
|
||||||
|
|
||||||
|
{% trans "Your order has been updated." %}
|
||||||
|
|
||||||
|
{% trans "Order" %} #{{ order.id }}
|
||||||
|
|
||||||
|
{% trans "New Status" %}: {{ order.get_status_display }}
|
||||||
|
|
||||||
|
{% if update %}
|
||||||
|
{{ update.title }}
|
||||||
|
{{ update.description }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if order.status == 'confirmed' %}
|
||||||
|
{% trans "Your order has been confirmed and will be processed soon." %}
|
||||||
|
{% elif order.status == 'in_progress' %}
|
||||||
|
{% trans "We are currently working on your order." %}
|
||||||
|
{% elif order.status == 'completed' %}
|
||||||
|
{% trans "Your order has been completed and will be shipped soon." %}
|
||||||
|
{% if order.tracking_number %}
|
||||||
|
{% trans "Tracking Number" %}: {{ order.tracking_number }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% trans "You can view your order details at" %}: {{ order_url }}
|
||||||
|
|
||||||
|
{% trans "If you have any questions about your order, please contact our support team." %}
|
||||||
|
support@fursuitshop.com
|
||||||
|
|
||||||
|
© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.shipping-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.tracking-box {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.tracking-number {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0d6efd;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.product-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.product {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.product:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.product-image {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-fursuit {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.badge-printed {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.shipping-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #198754;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{ logo_url }}" alt="Fursuit Shop" class="logo">
|
||||||
|
<div class="shipping-icon">📦</div>
|
||||||
|
<h1>{% trans "Your Order Has Been Shipped!" %}</h1>
|
||||||
|
<p>{% trans "Great news! Your order has been shipped and is on its way to you." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shipping-info">
|
||||||
|
<h2>{% trans "Order" %} #{{ order.id }}</h2>
|
||||||
|
|
||||||
|
<div class="tracking-box">
|
||||||
|
<h3>{% trans "Tracking Information" %}</h3>
|
||||||
|
<div class="tracking-number">
|
||||||
|
{{ order.tracking_number }}
|
||||||
|
</div>
|
||||||
|
<p class="text-muted">
|
||||||
|
{% trans "Use this number to track your shipment" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>{% trans "Shipping Address" %}</h3>
|
||||||
|
<p>
|
||||||
|
{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}<br>
|
||||||
|
{{ order.shipping_address.address }}<br>
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}<br>
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="product-list">
|
||||||
|
<h3>{% trans "Shipped Items" %}</h3>
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
<div class="product">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="product-image">
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<h4 style="margin: 0;">{{ product.name }}</h4>
|
||||||
|
{% if product.product_type == 'fursuit' %}
|
||||||
|
<span class="badge badge-fursuit">{% trans "Fursuit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-printed">{% trans "Printed Item" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ order_url }}" class="button">
|
||||||
|
{% trans "Track Your Shipment" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>
|
||||||
|
{% trans "If you have any questions about your shipment, please contact our support team." %}<br>
|
||||||
|
<a href="mailto:support@fursuitshop.com">support@fursuitshop.com</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% trans "Your Order Has Been Shipped!" %}
|
||||||
|
|
||||||
|
{% trans "Great news! Your order has been shipped and is on its way to you." %}
|
||||||
|
|
||||||
|
{% trans "Order" %} #{{ order.id }}
|
||||||
|
|
||||||
|
{% trans "Tracking Information" %}:
|
||||||
|
{% trans "Tracking Number" %}: {{ order.tracking_number }}
|
||||||
|
|
||||||
|
{% trans "Shipping Address" %}:
|
||||||
|
{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
|
||||||
|
{{ order.shipping_address.address }}
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
|
||||||
|
{% trans "Ordered Items" %}:
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
- {{ product.name }} ({% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %})
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% trans "You can track your shipment and view your order details at" %}: {{ order_url }}
|
||||||
|
|
||||||
|
{% trans "If you have any questions about your shipment, please contact our support team." %}
|
||||||
|
support@fursuitshop.com
|
||||||
|
|
||||||
|
© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %}
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
{% extends 'shop/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Gallery Header -->
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 200px; height: auto;">
|
||||||
|
<h1 class="display-5 fw-bold mb-4">Unsere Fursuit Galerie</h1>
|
||||||
|
<p class="lead">Entdecken Sie unsere handgefertigten Kreationen und lassen Sie sich inspirieren</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Section -->
|
||||||
|
<div class="furry-card mb-4" style="background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));">
|
||||||
|
<form method="get" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="category" class="form-label text-white">Kategorie</label>
|
||||||
|
<select name="category" id="category" class="form-select border-0">
|
||||||
|
<option value="">Alle Kategorien</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category.slug }}" {% if selected_category == category.slug %}selected{% endif %}>
|
||||||
|
{{ category.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="style" class="form-label text-white">Stil</label>
|
||||||
|
<select name="style" id="style" class="form-select border-0">
|
||||||
|
<option value="">Alle Stile</option>
|
||||||
|
{% for style in styles %}
|
||||||
|
<option value="{{ style.slug }}" {% if selected_style == style.slug %}selected{% endif %}>
|
||||||
|
{{ style.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="submit" class="btn btn-light w-100">
|
||||||
|
<i class="fas fa-filter me-2"></i> Filter anwenden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gallery Grid -->
|
||||||
|
<div class="row g-4" id="gallery">
|
||||||
|
{% for item in gallery_items %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="furry-card gallery-item h-100" data-bs-toggle="modal" data-bs-target="#galleryModal{{ item.id }}">
|
||||||
|
<div class="position-relative">
|
||||||
|
<img src="{{ item.image.url }}"
|
||||||
|
alt="{{ item.title }}"
|
||||||
|
class="img-fluid rounded-3"
|
||||||
|
style="width: 100%; height: 300px; object-fit: cover;">
|
||||||
|
<div class="gallery-overlay">
|
||||||
|
<i class="fas fa-search-plus fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3">
|
||||||
|
<h3 class="h5 mb-2">{{ item.title }}</h3>
|
||||||
|
<p class="text-muted mb-2">{{ item.description|truncatewords:20 }}</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for tag in item.tags.all %}
|
||||||
|
<span class="badge bg-primary">{{ tag.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal für jedes Galerie-Item -->
|
||||||
|
<div class="modal fade" id="galleryModal{{ item.id }}" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
|
<div class="modal-content border-0 rounded-4 overflow-hidden">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="modal-title">{{ item.title }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<img src="{{ item.image.url }}"
|
||||||
|
alt="{{ item.title }}"
|
||||||
|
class="img-fluid"
|
||||||
|
style="width: 100%; max-height: 80vh; object-fit: contain;">
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 p-4">
|
||||||
|
<h4 class="mb-3">Details</h4>
|
||||||
|
<p>{{ item.description }}</p>
|
||||||
|
|
||||||
|
{% if item.specifications %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="mb-3">Spezifikationen</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
{% for spec in item.specifications %}
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-check text-primary me-2"></i>
|
||||||
|
<strong>{{ spec.name }}:</strong> {{ spec.value }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if item.product %}
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'products:product_detail' item.product.slug %}" class="btn btn-primary w-100">
|
||||||
|
<i class="fas fa-shopping-cart me-2"></i> Zum Shop
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="furry-card text-center py-5">
|
||||||
|
<i class="fas fa-images fa-3x mb-3 text-muted"></i>
|
||||||
|
<h3>Keine Galerie-Einträge gefunden</h3>
|
||||||
|
<p class="text-muted">Versuchen Sie es mit anderen Filtereinstellungen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if is_paginated %}
|
||||||
|
<nav aria-label="Galerie Navigation" class="mt-5">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}{% if request.GET.style %}&style={{ request.GET.style }}{% endif %}">
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ num }}</span>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ num }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}{% if request.GET.style %}&style={{ request.GET.style }}{% endif %}">
|
||||||
|
{{ num }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}{% if request.GET.style %}&style={{ request.GET.style }}{% endif %}">
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6; /* Helles Lila */
|
||||||
|
--secondary-color: #EC4899; /* Pink */
|
||||||
|
--accent-color: #F59E0B; /* Orange */
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover .gallery-overlay {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-overlay i {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: linear-gradient(135deg, var(--light-color), #FDF2F8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{{ gallery.name }} - {% trans "Gallery" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox@3.2.0/dist/css/glightbox.min.css">
|
||||||
|
<style>
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-button {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-button:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-button.liked {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specs-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specs-list .card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'shop:home' %}">{% trans "Home" %}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'shop:fursuit_gallery' %}">{% trans "Gallery" %}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ gallery.name }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Hauptbild -->
|
||||||
|
{% if gallery.images.first %}
|
||||||
|
<div class="position-relative mb-4">
|
||||||
|
<a href="{{ gallery.images.first.image.url }}" class="glightbox"
|
||||||
|
data-gallery="gallery-{{ gallery.id }}">
|
||||||
|
<img src="{{ gallery.images.first.image.url }}"
|
||||||
|
alt="{{ gallery.name }}"
|
||||||
|
class="img-fluid rounded shadow-sm"
|
||||||
|
style="width: 100%; max-height: 600px; object-fit: contain;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Galerie-Grid -->
|
||||||
|
{% if gallery.images.all|length > 1 %}
|
||||||
|
<div class="gallery-grid">
|
||||||
|
{% for image in gallery.images.all %}
|
||||||
|
{% if not forloop.first %}
|
||||||
|
<a href="{{ image.image.url }}"
|
||||||
|
class="gallery-item glightbox"
|
||||||
|
data-gallery="gallery-{{ gallery.id }}">
|
||||||
|
<img src="{{ image.image.url }}"
|
||||||
|
alt="{{ gallery.name }} - {% trans 'Image' %} {{ forloop.counter }}"
|
||||||
|
loading="lazy">
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<h1 class="card-title h2">{{ gallery.name }}</h1>
|
||||||
|
<button class="btn btn-outline-danger like-button {% if user_has_liked %}liked{% endif %}"
|
||||||
|
data-gallery-id="{{ gallery.id }}"
|
||||||
|
onclick="toggleLike(this)">
|
||||||
|
<i class="bi bi-heart-fill"></i>
|
||||||
|
<span class="likes-count">{{ gallery.likes }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{% if gallery.fursuit_type == 'fullsuit' %}
|
||||||
|
<span class="badge bg-primary">{% trans "Fullsuit" %}</span>
|
||||||
|
{% elif gallery.fursuit_type == 'partial' %}
|
||||||
|
<span class="badge bg-info">{% trans "Partial Suit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{% trans "Head Only" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if gallery.style == 'toony' %}
|
||||||
|
<span class="badge bg-success">{% trans "Toony" %}</span>
|
||||||
|
{% elif gallery.style == 'realistic' %}
|
||||||
|
<span class="badge bg-warning">{% trans "Realistic" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info">{% trans "Semi-Realistic" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="card-text">{{ gallery.description }}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="specs-list">
|
||||||
|
{% if gallery.features %}
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-2">{% trans "Features" %}</h6>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
{% for feature in gallery.features %}
|
||||||
|
<li><i class="bi bi-check-circle text-success"></i> {{ feature }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if gallery.materials %}
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-subtitle mb-2">{% trans "Materials" %}</h6>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
{% for material in gallery.materials %}
|
||||||
|
<li><i class="bi bi-circle-fill text-primary"></i> {{ material }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'shop:fursuit_order' %}?inspiration={{ gallery.id }}"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="bi bi-cart"></i>
|
||||||
|
{% trans "Order Similar Fursuit" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/glightbox@3.2.0/dist/js/glightbox.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Lightbox initialisieren
|
||||||
|
const lightbox = GLightbox({
|
||||||
|
touchNavigation: true,
|
||||||
|
loop: true,
|
||||||
|
autoplayVideo: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Like-Funktionalität
|
||||||
|
function toggleLike(button) {
|
||||||
|
const galleryId = button.dataset.galleryId;
|
||||||
|
const likesCount = button.querySelector('.likes-count');
|
||||||
|
const currentLikes = parseInt(likesCount.textContent);
|
||||||
|
|
||||||
|
fetch(`/api/gallery/${galleryId}/like/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
button.classList.toggle('liked');
|
||||||
|
likesCount.textContent = data.likes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Fursuit Gallery" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.gallery-item {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover img {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.masonry-grid {
|
||||||
|
columns: 1;
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.masonry-grid {
|
||||||
|
columns: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.masonry-grid {
|
||||||
|
columns: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-filters {
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Filter-Sidebar -->
|
||||||
|
<div class="col-lg-3 mb-4">
|
||||||
|
<div class="card gallery-filters">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans "Filter Gallery" %}</h5>
|
||||||
|
<form method="get">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{% trans "Fursuit Type" %}</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="type" value="fullsuit"
|
||||||
|
id="typeFullsuit" {% if 'fullsuit' in selected_types %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="typeFullsuit">
|
||||||
|
{% trans "Fullsuit" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="type" value="partial"
|
||||||
|
id="typePartial" {% if 'partial' in selected_types %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="typePartial">
|
||||||
|
{% trans "Partial Suit" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="type" value="head"
|
||||||
|
id="typeHead" {% if 'head' in selected_types %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="typeHead">
|
||||||
|
{% trans "Head Only" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{% trans "Style" %}</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="style" value="toony"
|
||||||
|
id="styleToony" {% if 'toony' in selected_styles %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="styleToony">
|
||||||
|
{% trans "Toony" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="style" value="realistic"
|
||||||
|
id="styleRealistic" {% if 'realistic' in selected_styles %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="styleRealistic">
|
||||||
|
{% trans "Realistic" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="style" value="semi_realistic"
|
||||||
|
id="styleSemiRealistic" {% if 'semi_realistic' in selected_styles %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="styleSemiRealistic">
|
||||||
|
{% trans "Semi-Realistic" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sort" class="form-label">{% trans "Sort by" %}</label>
|
||||||
|
<select class="form-select" name="sort" id="sort">
|
||||||
|
<option value="-created_at" {% if sort == '-created_at' %}selected{% endif %}>
|
||||||
|
{% trans "Newest First" %}
|
||||||
|
</option>
|
||||||
|
<option value="created_at" {% if sort == 'created_at' %}selected{% endif %}>
|
||||||
|
{% trans "Oldest First" %}
|
||||||
|
</option>
|
||||||
|
<option value="-likes" {% if sort == '-likes' %}selected{% endif %}>
|
||||||
|
{% trans "Most Liked" %}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans "Apply Filters" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Galerie -->
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>{% trans "Fursuit Gallery" %}</h2>
|
||||||
|
<span>{{ galleries|length }} {% trans "entries" %}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if galleries %}
|
||||||
|
<div class="masonry-grid">
|
||||||
|
{% for gallery in galleries %}
|
||||||
|
<div class="gallery-item">
|
||||||
|
<a href="{% url 'shop:gallery_detail' gallery.slug %}" class="text-decoration-none">
|
||||||
|
<div class="card">
|
||||||
|
{% if gallery.images.first %}
|
||||||
|
<img src="{{ gallery.images.first.image.url }}"
|
||||||
|
class="card-img-top" alt="{{ gallery.name }}"
|
||||||
|
loading="lazy">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ gallery.name }}</h5>
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
{% if gallery.fursuit_type == 'fullsuit' %}
|
||||||
|
<span class="badge bg-primary">{% trans "Fullsuit" %}</span>
|
||||||
|
{% elif gallery.fursuit_type == 'partial' %}
|
||||||
|
<span class="badge bg-info">{% trans "Partial Suit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{% trans "Head Only" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if gallery.style == 'toony' %}
|
||||||
|
<span class="badge bg-success">{% trans "Toony" %}</span>
|
||||||
|
{% elif gallery.style == 'realistic' %}
|
||||||
|
<span class="badge bg-warning">{% trans "Realistic" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info">{% trans "Semi-Realistic" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p class="card-text">{{ gallery.description|truncatewords:30 }}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-calendar"></i>
|
||||||
|
{{ gallery.created_at|date:"SHORT_DATE_FORMAT" }}
|
||||||
|
</small>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-heart"></i>
|
||||||
|
{{ gallery.likes }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% trans "No gallery entries found matching your criteria." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Willkommen bei Kasico Art & Design - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero-section mb-5">
|
||||||
|
<div class="position-relative overflow-hidden rounded-4 mb-4" style="height: 500px;">
|
||||||
|
<div class="w-100 h-100 d-flex align-items-center justify-content-center" style="background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid"
|
||||||
|
style="max-width: 400px; height: auto;">
|
||||||
|
</div>
|
||||||
|
<div class="position-absolute bottom-0 start-0 w-100 p-4 text-white" style="background: linear-gradient(transparent, rgba(0,0,0,0.8));">
|
||||||
|
<h1 class="display-4 fw-bold mb-3">Willkommen bei Kasico Art & Design</h1>
|
||||||
|
<p class="lead mb-4">Wo Ihre Fursuit-Träume Realität werden</p>
|
||||||
|
<a href="{% url 'products:custom_order' %}" class="btn btn-primary btn-lg me-3">
|
||||||
|
<i class="fas fa-magic me-2"></i> Custom Order starten
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:gallery' %}" class="btn btn-light btn-lg me-3">
|
||||||
|
<i class="fas fa-images me-2"></i> Galerie ansehen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'shop:contact' %}" class="btn btn-outline-light btn-lg">
|
||||||
|
<i class="fas fa-envelope me-2"></i> Kontakt
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Services Section -->
|
||||||
|
<section class="services-section mb-5">
|
||||||
|
<h2 class="text-center mb-4">Unsere Dienstleistungen</h2>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="furry-card h-100">
|
||||||
|
<div class="furry-icon mb-3">
|
||||||
|
<i class="fas fa-scissors"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Custom Design</h3>
|
||||||
|
<p>Ihr einzigartiger Charakter, zum Leben erweckt mit höchster Handwerkskunst und Liebe zum Detail.</p>
|
||||||
|
<ul class="list-unstyled mt-3">
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Individuelle Konzeption</li>
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>3D-Modellierung</li>
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Maßanfertigung</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="furry-card h-100">
|
||||||
|
<div class="furry-icon mb-3">
|
||||||
|
<i class="fas fa-tools"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Reparaturen</h3>
|
||||||
|
<p>Professionelle Pflege und Reparatur für Ihren bestehenden Fursuit.</p>
|
||||||
|
<ul class="list-unstyled mt-3">
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Fell-Erneuerung</li>
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Strukturreparaturen</li>
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Reinigung</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="furry-card h-100">
|
||||||
|
<div class="furry-icon mb-3">
|
||||||
|
<i class="fas fa-camera"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Fotoshootings</h3>
|
||||||
|
<p>Professionelle Fotografie-Sessions für Ihren Fursuit.</p>
|
||||||
|
<ul class="list-unstyled mt-3">
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Studio-Aufnahmen</li>
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Outdoor-Shootings</li>
|
||||||
|
<li class="mb-2"><i class="fas fa-paw me-2 text-primary"></i>Event-Dokumentation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Custom Orders Section -->
|
||||||
|
<section id="custom-orders" class="custom-orders-section mb-5 py-5" style="background: linear-gradient(135deg, var(--light-color), #FDF2F8);">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="text-center mb-4">Custom Orders</h2>
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||||
|
<div class="rounded-4 shadow d-flex align-items-center justify-content-center p-4" style="background: white; min-height: 300px;">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design"
|
||||||
|
class="img-fluid"
|
||||||
|
style="max-width: 300px; height: auto;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="furry-card">
|
||||||
|
<h3 class="mb-4">Ihr Traum-Fursuit wartet auf Sie</h3>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="mb-3">
|
||||||
|
<i class="fas fa-check-circle text-primary me-2"></i>
|
||||||
|
Kostenlose Designberatung
|
||||||
|
</li>
|
||||||
|
<li class="mb-3">
|
||||||
|
<i class="fas fa-check-circle text-primary me-2"></i>
|
||||||
|
Detaillierte 3D-Visualisierung
|
||||||
|
</li>
|
||||||
|
<li class="mb-3">
|
||||||
|
<i class="fas fa-check-circle text-primary me-2"></i>
|
||||||
|
Regelmäßige Fortschrittsberichte
|
||||||
|
</li>
|
||||||
|
<li class="mb-3">
|
||||||
|
<i class="fas fa-check-circle text-primary me-2"></i>
|
||||||
|
Höchste Materialqualität
|
||||||
|
</li>
|
||||||
|
<li class="mb-3">
|
||||||
|
<i class="fas fa-check-circle text-primary me-2"></i>
|
||||||
|
Maßgeschneiderte Passform
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a href="{% url 'products:custom_order' %}" class="btn btn-primary mt-3">
|
||||||
|
<i class="fas fa-envelope me-2"></i> Anfrage senden
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FAQ Section -->
|
||||||
|
<section class="faq-section mb-5">
|
||||||
|
<h2 class="text-center mb-4">Häufig gestellte Fragen</h2>
|
||||||
|
<div class="accordion" id="faqAccordion">
|
||||||
|
{% for faq in faqs %}
|
||||||
|
<div class="accordion-item border-0 mb-3 rounded-3 shadow-sm">
|
||||||
|
<h3 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed rounded-3" type="button" data-bs-toggle="collapse" data-bs-target="#faq{{ forloop.counter }}">
|
||||||
|
{{ faq.question }}
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
|
<div id="faq{{ forloop.counter }}" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
{{ faq.answer }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Contact Section -->
|
||||||
|
<section id="contact" class="contact-section mb-5">
|
||||||
|
<div class="furry-card text-center">
|
||||||
|
<h2 class="mb-4">Haben Sie Fragen?</h2>
|
||||||
|
<p class="lead mb-4">Wir sind für Sie da! Kontaktieren Sie uns für individuelle Beratung und Support.</p>
|
||||||
|
<a href="{% url 'shop:contact' %}" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-envelope me-2"></i> Kontaktieren Sie uns
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Login" %} - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="furry-card">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 150px; height: auto;">
|
||||||
|
<h1 class="h3 mb-3">{% trans "Login" %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.username.id_for_label }}" class="form-label">
|
||||||
|
{{ form.username.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.username }}
|
||||||
|
{% if form.username.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.username.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="{{ form.password.id_for_label }}" class="form-label">
|
||||||
|
{{ form.password.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.password }}
|
||||||
|
{% if form.password.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.password.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans "Login" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p>{% trans "Don't have an account?" %}
|
||||||
|
<a href="{% url 'register' %}">{% trans "Register here" %}</a>
|
||||||
|
</p>
|
||||||
|
<p><a href="{% url 'password_reset' %}">{% trans "Forgot your password?" %}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6; /* Helles Lila */
|
||||||
|
--secondary-color: #EC4899; /* Pink */
|
||||||
|
--accent-color: #F59E0B; /* Orange */
|
||||||
|
--dark-color: #1F2937; /* Dunkelgrau */
|
||||||
|
--light-color: #F3E8FF; /* Helles Lila */
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_username, #id_password {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_username:focus, #id_password:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--dark-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Formular neu laden, wenn der Benutzer zurück navigiert
|
||||||
|
window.addEventListener('pageshow', function(event) {
|
||||||
|
if (event.persisted) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "My Orders" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.order-card {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: var(--bs-warning);
|
||||||
|
color: var(--bs-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-confirmed {
|
||||||
|
background-color: var(--bs-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-in_progress {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background-color: var(--bs-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-timeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--bs-gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: -45px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bs-primary);
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 0 0 2px var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--bs-gray-600);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h2 mb-0">{% trans "My Orders" %}</h1>
|
||||||
|
<a href="{% url 'shop:product_list' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-cart-plus"></i>
|
||||||
|
{% trans "Continue Shopping" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if orders %}
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for order in orders %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card order-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<!-- Bestellnummer und Status -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h5 class="card-title mb-1">
|
||||||
|
{% trans "Order" %} #{{ order.id }}
|
||||||
|
</h5>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="badge status-{{ order.status }} status-badge">
|
||||||
|
{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
{{ order.created_at|date:"d.m.Y" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bestellte Artikel -->
|
||||||
|
<div class="col-md-5">
|
||||||
|
<h6 class="mb-2">{% trans "Order Items" %}</h6>
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
<div class="d-flex align-items-center mb-1">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}"
|
||||||
|
alt="{{ product.name }}"
|
||||||
|
class="rounded"
|
||||||
|
style="width: 40px; height: 40px; object-fit: cover;">
|
||||||
|
{% endif %}
|
||||||
|
<div class="ms-2">
|
||||||
|
{{ product.name }}
|
||||||
|
{% if product.product_type == 'fursuit' %}
|
||||||
|
<span class="badge bg-primary">{% trans "Fursuit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">{% trans "Printed Item" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zahlungsmethode und Gesamtbetrag -->
|
||||||
|
<div class="col-md-2">
|
||||||
|
<h6 class="mb-2">{% trans "Payment" %}</h6>
|
||||||
|
<p class="mb-1">
|
||||||
|
{% if order.payment_method == 'paypal' %}
|
||||||
|
<i class="bi bi-paypal"></i> PayPal
|
||||||
|
{% elif order.payment_method == 'credit_card' %}
|
||||||
|
<i class="bi bi-credit-card"></i> {% trans "Credit Card" %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-bank"></i> {% trans "Bank Transfer" %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p class="fw-bold mb-0">
|
||||||
|
{{ order.total_price }} €
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fortschritt -->
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button class="btn btn-outline-primary w-100"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#progress-{{ order.id }}"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-clock-history"></i>
|
||||||
|
{% trans "Show Progress" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fortschritts-Timeline -->
|
||||||
|
<div class="collapse mt-3" id="progress-{{ order.id }}">
|
||||||
|
<div class="progress-timeline">
|
||||||
|
{% for update in order.progress_updates.all %}
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-dot"></div>
|
||||||
|
<h6 class="mb-1">{{ update.title }}</h6>
|
||||||
|
<div class="timeline-date mb-2">
|
||||||
|
{{ update.created_at|date:"d.m.Y H:i" }}
|
||||||
|
</div>
|
||||||
|
<p class="mb-0">{{ update.description }}</p>
|
||||||
|
{% if update.image %}
|
||||||
|
<img src="{{ update.image.url }}"
|
||||||
|
alt="{% trans 'Progress Image' %}"
|
||||||
|
class="img-fluid rounded mt-2"
|
||||||
|
style="max-height: 200px;">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox display-1 text-muted mb-4"></i>
|
||||||
|
<h2>{% trans "No orders yet" %}</h2>
|
||||||
|
<p class="text-muted">
|
||||||
|
{% trans "You haven't placed any orders yet." %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'shop:product_list' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-shop"></i>
|
||||||
|
{% trans "Start Shopping" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Order Successful" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<i class="bi bi-check-circle text-success display-1 mb-4"></i>
|
||||||
|
|
||||||
|
<h1 class="h2 mb-4">{% trans "Thank You for Your Order!" %}</h1>
|
||||||
|
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
{% trans "Your payment was successful and your order has been confirmed." %}
|
||||||
|
<br>
|
||||||
|
{% trans "We'll send you an email with your order details shortly." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-4">{% trans "Order Details" %}</h5>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<p class="mb-1">
|
||||||
|
<strong>{% trans "Order Number" %}:</strong>
|
||||||
|
#{{ order.id }}
|
||||||
|
</p>
|
||||||
|
<p class="mb-1">
|
||||||
|
<strong>{% trans "Order Date" %}:</strong>
|
||||||
|
{{ order.created_at|date:"d.m.Y" }}
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
<strong>{% trans "Payment Method" %}:</strong>
|
||||||
|
{% if order.payment_method == 'paypal' %}
|
||||||
|
<i class="bi bi-paypal"></i> PayPal
|
||||||
|
{% elif order.payment_method == 'credit_card' %}
|
||||||
|
<i class="bi bi-credit-card"></i> {% trans "Credit Card" %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-bank"></i> {% trans "Bank Transfer" %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<p class="mb-1">
|
||||||
|
<strong>{% trans "Shipping To" %}:</strong>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}<br>
|
||||||
|
{{ order.shipping_address.address }}<br>
|
||||||
|
{{ order.shipping_address.zip }} {{ order.shipping_address.city }}<br>
|
||||||
|
{{ order.shipping_address.get_country_display }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="mb-3">{% trans "Ordered Items" %}</h6>
|
||||||
|
{% for product in order.products.all %}
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}"
|
||||||
|
alt="{{ product.name }}"
|
||||||
|
class="rounded"
|
||||||
|
style="width: 50px; height: 50px; object-fit: cover;">
|
||||||
|
{% endif %}
|
||||||
|
<div class="ms-3 flex-grow-1">
|
||||||
|
<h6 class="mb-0">{{ product.name }}</h6>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if product.product_type == 'fursuit' %}
|
||||||
|
<span class="badge bg-primary">{% trans "Fursuit" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">{% trans "Printed Item" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="fw-bold">{{ product.base_price }} €</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="text-end">
|
||||||
|
<p class="mb-1">
|
||||||
|
<span class="text-muted">{% trans "Subtotal" %}:</span>
|
||||||
|
<span class="ms-2">{{ order.total_price }} €</span>
|
||||||
|
</p>
|
||||||
|
{% if order.total_price < 200 %}
|
||||||
|
<p class="mb-1">
|
||||||
|
<span class="text-muted">{% trans "Shipping" %}:</span>
|
||||||
|
<span class="ms-2">5.99 €</span>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 fw-bold">
|
||||||
|
<span>{% trans "Total" %}:</span>
|
||||||
|
<span class="ms-2">{{ order.total_price|add:"5.99" }} €</span>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="mb-1">
|
||||||
|
<span class="text-muted">{% trans "Shipping" %}:</span>
|
||||||
|
<span class="ms-2 text-success">{% trans "FREE" %}</span>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 fw-bold">
|
||||||
|
<span>{% trans "Total" %}:</span>
|
||||||
|
<span class="ms-2">{{ order.total_price }} €</span>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<a href="{% url 'shop:my_orders' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-box"></i>
|
||||||
|
{% trans "View My Orders" %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'shop:product_list' %}" class="btn btn-outline-primary ms-3">
|
||||||
|
<i class="bi bi-shop"></i>
|
||||||
|
{% trans "Continue Shopping" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
{% extends 'shop/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Reset Password" %} - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="furry-card">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 150px; height: auto;">
|
||||||
|
<h1 class="h3 mb-3">{% trans "Reset Password" %}</h1>
|
||||||
|
<p class="text-muted">{% trans "Enter your email address below, and we'll send you instructions for setting a new password." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for field in form %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<p class="mb-0">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="{{ form.email.id_for_label }}" class="form-label">
|
||||||
|
{% trans "Email Address" %}
|
||||||
|
</label>
|
||||||
|
{{ form.email }}
|
||||||
|
{% if form.email.help_text %}
|
||||||
|
<div class="form-text">{{ form.email.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans "Send Reset Link" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p>{% trans "Remember your password?" %}
|
||||||
|
<a href="{% url 'login' %}">{% trans "Login here" %}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6;
|
||||||
|
--secondary-color: #EC4899;
|
||||||
|
--accent-color: #F59E0B;
|
||||||
|
--dark-color: #1F2937;
|
||||||
|
--light-color: #F3E8FF;
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_email {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_email:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
color: var(--dark-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
{% extends 'shop/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Password Reset Complete" %} - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="furry-card text-center">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 150px; height: auto;">
|
||||||
|
<h1 class="h3 mb-3">{% trans "Password Reset Complete" %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="success-icon mb-4">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-4">{% trans "Your password has been set. You may go ahead and log in now." %}</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-primary">
|
||||||
|
{% trans "Login" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6;
|
||||||
|
--secondary-color: #EC4899;
|
||||||
|
--accent-color: #F59E0B;
|
||||||
|
--dark-color: #1F2937;
|
||||||
|
--light-color: #F3E8FF;
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: linear-gradient(135deg, #10B981, #059669);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
{% extends 'shop/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Set New Password" %} - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="furry-card">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 150px; height: auto;">
|
||||||
|
<h1 class="h3 mb-3">{% trans "Set New Password" %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if validlink %}
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for field in form %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<p class="mb-0">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.new_password1.id_for_label }}" class="form-label">
|
||||||
|
{% trans "New Password" %}
|
||||||
|
</label>
|
||||||
|
{{ form.new_password1 }}
|
||||||
|
{% if form.new_password1.help_text %}
|
||||||
|
<div class="form-text">{{ form.new_password1.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="{{ form.new_password2.id_for_label }}" class="form-label">
|
||||||
|
{% trans "Confirm New Password" %}
|
||||||
|
</label>
|
||||||
|
{{ form.new_password2 }}
|
||||||
|
{% if form.new_password2.help_text %}
|
||||||
|
<div class="form-text">{{ form.new_password2.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans "Change Password" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="error-icon mb-4">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4">{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
|
||||||
|
<a href="{% url 'password_reset' %}" class="btn btn-primary">
|
||||||
|
{% trans "Request New Reset Link" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6;
|
||||||
|
--secondary-color: #EC4899;
|
||||||
|
--accent-color: #F59E0B;
|
||||||
|
--dark-color: #1F2937;
|
||||||
|
--light-color: #F3E8FF;
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: linear-gradient(135deg, #EF4444, #DC2626);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_new_password1, #id_new_password2 {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--light-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_new_password1:focus, #id_new_password2:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
color: var(--dark-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
{% extends 'shop/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Password Reset Email Sent" %} - Kasico Art & Design{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="furry-card text-center">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<img src="{% static 'images/kasicoLogo.png' %}"
|
||||||
|
alt="Kasico Art & Design Logo"
|
||||||
|
class="img-fluid mb-4"
|
||||||
|
style="max-width: 150px; height: auto;">
|
||||||
|
<h1 class="h3 mb-3">{% trans "Check Your Email" %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="success-icon mb-4">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-4">{% trans "We've emailed you instructions for setting your password. You should receive them shortly." %}</p>
|
||||||
|
|
||||||
|
<p class="mb-4">{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-primary">
|
||||||
|
{% trans "Return to Login" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #8B5CF6;
|
||||||
|
--secondary-color: #EC4899;
|
||||||
|
--accent-color: #F59E0B;
|
||||||
|
--dark-color: #1F2937;
|
||||||
|
--light-color: #F3E8FF;
|
||||||
|
--white-color: #FFFFFF;
|
||||||
|
--gradient-start: #8B5CF6;
|
||||||
|
--gradient-middle: #EC4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.furry-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at Kasico Art & Design.{% endblocktrans %}
|
||||||
|
|
||||||
|
{% trans "Please go to the following page and choose a new password:" %}
|
||||||
|
{% block reset_link %}
|
||||||
|
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
||||||
|
|
||||||
|
{% trans "Thanks for using our site!" %}
|
||||||
|
|
||||||
|
{% blocktrans %}The Kasico Art & Design Team{% endblocktrans %}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
{% blocktrans %}Password reset on Kasico Art & Design{% endblocktrans %}
|
||||||
|
{% endautoescape %}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends "shop/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Payment Failed" %} - Fursuit Shop{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="bi bi-exclamation-circle text-danger display-1 mb-4"></i>
|
||||||
|
|
||||||
|
<h1 class="h2 mb-4">{% trans "Payment Failed" %}</h1>
|
||||||
|
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
{% trans "We're sorry, but there was a problem processing your payment." %}
|
||||||
|
<br>
|
||||||
|
{% trans "Please try again or choose a different payment method." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if order.payment_errors.exists %}
|
||||||
|
<div class="alert alert-danger mx-auto" style="max-width: 500px;">
|
||||||
|
<h6 class="alert-heading">{% trans "Error Details" %}:</h6>
|
||||||
|
{% for error in order.payment_errors.all|slice:"-1:" %}
|
||||||
|
<p class="mb-0">{{ error.error_message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center gap-3">
|
||||||
|
<a href="{% url 'shop:checkout' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-arrow-left"></i>
|
||||||
|
{% trans "Return to Checkout" %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'shop:contact' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-envelope"></i>
|
||||||
|
{% trans "Contact Support" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue