Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dbc6e7ea1 | ||
|
|
6d16a6b0f9 | ||
|
|
0a98740877 | ||
|
|
714807f648 | ||
|
|
b29fa5b2aa |
@@ -46,6 +46,7 @@ jobs:
|
||||
grep -q "^APP_KEY=$" "$DEPLOY_PATH/.env" && \
|
||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan key:generate --force || true
|
||||
|
||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan storage:link 2>/dev/null || true
|
||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan config:clear
|
||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:clear
|
||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:clear
|
||||
|
||||
@@ -44,8 +44,9 @@ class ProductController extends Controller
|
||||
'weight' => 'nullable|numeric|min:0',
|
||||
'active' => 'boolean',
|
||||
'featured' => 'boolean',
|
||||
'track_stock' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'track_stock' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'delivery_note' => 'nullable|string|max:255',
|
||||
]);
|
||||
$data['slug'] = Str::slug($data['name']);
|
||||
$data['active'] = $request->boolean('active');
|
||||
@@ -89,8 +90,9 @@ class ProductController extends Controller
|
||||
'weight' => 'nullable|numeric|min:0',
|
||||
'active' => 'boolean',
|
||||
'featured' => 'boolean',
|
||||
'track_stock' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'track_stock' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'delivery_note' => 'nullable|string|max:255',
|
||||
]);
|
||||
$data['slug'] = Str::slug($data['name']);
|
||||
$data['active'] = $request->boolean('active');
|
||||
|
||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
protected $fillable = ['category_id','name','slug','description','base_price','sale_price','tax_rate','weight','active','featured','track_stock','sort_order'];
|
||||
protected $fillable = ['category_id','name','slug','description','base_price','sale_price','tax_rate','weight','active','featured','track_stock','sort_order','delivery_note'];
|
||||
|
||||
protected $casts = ['active' => 'boolean', 'featured' => 'boolean', 'track_stock' => 'boolean', 'base_price' => 'decimal:2', 'sale_price' => 'decimal:2', 'tax_rate' => 'decimal:2'];
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void {
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('delivery_note')->nullable()->after('track_stock');
|
||||
});
|
||||
}
|
||||
public function down(): void {
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('delivery_note');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -165,6 +165,12 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Lieferhinweis <span class="text-gray-400 font-normal">(optional, z.B. "Geplante Lieferung: Mai 2026")</span></label>
|
||||
<input type="text" name="delivery_note" value="{{ old('delivery_note', $product->delivery_note ?? '') }}"
|
||||
class="w-full border rounded px-3 py-2 text-sm" placeholder="z.B. Geplante Lieferung: Mai 2026">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="flex-1 bg-blue-600 text-white py-2 rounded hover:bg-blue-700 text-sm">Speichern</button>
|
||||
<a href="{{ route('admin.products.index') }}" class="flex-1 text-center py-2 rounded border text-sm hover:bg-gray-50">Abbrechen</a>
|
||||
|
||||
@@ -24,7 +24,13 @@
|
||||
<span class="text-xs text-gray-400">inkl. {{ number_format($product->tax_rate, 0) }}% MwSt.</span>
|
||||
</div>
|
||||
@if($product->track_stock && $product->variants->sum('stock') <= 0)
|
||||
<p class="text-xs text-red-500 mt-1">Nicht vorrätig</p>
|
||||
@if($product->delivery_note)
|
||||
<p class="text-xs text-orange-500 mt-1">📦 {{ $product->delivery_note }}</p>
|
||||
@else
|
||||
<p class="text-xs text-red-500 mt-1">Nicht vorrätig</p>
|
||||
@endif
|
||||
@elseif($product->delivery_note && !($product->track_stock && $product->variants->sum('stock') > 0))
|
||||
<p class="text-xs text-orange-500 mt-1">📦 {{ $product->delivery_note }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@extends('layouts.shop')
|
||||
@section('title', $product->name)
|
||||
@section('content')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 mb-12" x-data="productDetail(@json($product->variants))">
|
||||
@php $firstImageUrl = $product->images->count() ? asset('storage/'.$product->images->first()->path) : ''; @endphp
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 mb-12" x-data="productDetail">
|
||||
|
||||
{{-- Bilder --}}
|
||||
<div>
|
||||
@if($product->images->count())
|
||||
<img :src="mainImage" x-init="mainImage = '{{ asset('storage/'.$product->images->first()->path) }}'" class="w-full rounded-lg mb-3 border">
|
||||
<img :src="mainImage" class="w-full h-96 object-contain rounded-lg mb-3 border bg-gray-50">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@foreach($product->images as $img)
|
||||
<img src="{{ asset('storage/'.$img->path) }}" @click="mainImage = '{{ asset('storage/'.$img->path) }}'"
|
||||
@@ -14,7 +15,7 @@
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="w-full h-72 bg-gray-100 rounded-lg flex items-center justify-center text-7xl">📦</div>
|
||||
<div class="w-full h-96 bg-gray-100 rounded-lg flex items-center justify-center text-7xl">📦</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -73,6 +74,10 @@
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if($product->delivery_note)
|
||||
<p class="text-sm text-orange-500 mb-3">📦 {{ $product->delivery_note }}</p>
|
||||
@endif
|
||||
|
||||
{{-- In den Warenkorb --}}
|
||||
<form method="POST" action="{{ route('cart.add') }}">
|
||||
@csrf
|
||||
@@ -109,13 +114,17 @@
|
||||
@endif
|
||||
|
||||
<script>
|
||||
function productDetail(variants) {
|
||||
return {
|
||||
document.addEventListener('alpine:init', () => {
|
||||
const variants = @json($product->variants->load('attributeValues'));
|
||||
const firstImage = @json($firstImageUrl);
|
||||
const basePrice = {{ $product->base_price }};
|
||||
|
||||
Alpine.data('productDetail', () => ({
|
||||
variants,
|
||||
selectedAttrs: {},
|
||||
selectedVariant: null,
|
||||
mainImage: '',
|
||||
currentPrice: {{ $product->base_price }},
|
||||
mainImage: firstImage,
|
||||
currentPrice: basePrice,
|
||||
selectAttr(name, id) {
|
||||
this.selectedAttrs[name] = id;
|
||||
this.findVariant();
|
||||
@@ -127,13 +136,13 @@ function productDetail(variants) {
|
||||
return selectedIds.every(id => ids.includes(id)) && ids.length === selectedIds.length;
|
||||
});
|
||||
if (this.selectedVariant) {
|
||||
this.currentPrice = this.selectedVariant.price_override ?? {{ $product->base_price }};
|
||||
this.currentPrice = this.selectedVariant.price_override ?? basePrice;
|
||||
}
|
||||
},
|
||||
formatPrice(p) {
|
||||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user