Skip to content

Commit 6da1bbe

Browse files
committed
feat: implement related products
1 parent 8aa687e commit 6da1bbe

15 files changed

Lines changed: 1415 additions & 0 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::create('pim_product_relations', function (Blueprint $table) {
12+
$table->id();
13+
$table->foreignId('parent_id')
14+
->constrained('catalogue_products', 'id')
15+
->cascadeOnUpdate()
16+
->cascadeOnDelete();
17+
$table->foreignId('child_id')
18+
->constrained('catalogue_products', 'id')
19+
->cascadeOnUpdate()
20+
->cascadeOnDelete();
21+
$table->string('type', 50);
22+
$table->tinyInteger('sort')->nullable();
23+
$table->timestamps();
24+
$table->unique(['parent_id', 'child_id', 'type']);
25+
$table->index(['parent_id', 'type']);
26+
});
27+
}
28+
29+
public function down(): void
30+
{
31+
Schema::dropIfExists('pim_product_relations');
32+
}
33+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="flex justify-center">
2+
<input
3+
type="checkbox"
4+
wire:click="toggleProductSelection({{ $getState()['id'] }})"
5+
@if($getState()['checked']) checked @endif
6+
class="fi-checkbox-input rounded border-none bg-white shadow-sm ring-1 ring-gray-950/10 checked:ring-0 focus:ring-2 focus:ring-primary-600 focus:checked:ring-primary-600 disabled:bg-gray-50 disabled:text-gray-50 disabled:checked:bg-current disabled:checked:text-gray-400 dark:bg-white/5 dark:ring-white/20 dark:checked:bg-primary-500 dark:focus:ring-primary-500 dark:disabled:bg-transparent dark:disabled:ring-white/10 dark:disabled:checked:bg-white/5 dark:disabled:checked:ring-white/10 text-primary-600 dark:text-primary-500"
7+
/>
8+
</div>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@livewire(\Eclipse\Catalogue\Livewire\ProductRelationsTable::class, [
2+
'productId' => $productId,
3+
'type' => $type
4+
])

src/CatalogueServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public function boot()
4747
// Register Livewire components
4848
if (class_exists(\Livewire\Livewire::class)) {
4949
\Livewire\Livewire::component('eclipse-catalogue::tenant-switcher', \Eclipse\Catalogue\Livewire\TenantSwitcher::class);
50+
\Livewire\Livewire::component('eclipse.catalogue.livewire.product-selector-table', \Eclipse\Catalogue\Livewire\ProductSelectorTable::class);
51+
\Livewire\Livewire::component('eclipse.catalogue.livewire.product-relations-table', \Eclipse\Catalogue\Livewire\ProductRelationsTable::class);
5052
}
5153

5254
FilamentAsset::register([

src/Enums/ProductRelationType.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Enums;
4+
5+
use Filament\Support\Contracts\HasLabel;
6+
7+
enum ProductRelationType: string implements HasLabel
8+
{
9+
case RELATED = 'related';
10+
case CROSS_SELL = 'cross_sell';
11+
case UPSELL = 'upsell';
12+
13+
/**
14+
* Get the label for the product relation type.
15+
*/
16+
public function getLabel(): ?string
17+
{
18+
return match ($this) {
19+
self::RELATED => 'Related',
20+
self::CROSS_SELL => 'Cross-sell',
21+
self::UPSELL => 'Upsell',
22+
};
23+
}
24+
25+
/**
26+
* Get the description for the product relation type.
27+
*/
28+
public function getDescription(): string
29+
{
30+
return match ($this) {
31+
self::RELATED => 'Similar products that customers might be interested in',
32+
self::CROSS_SELL => 'Complementary products or add-ons that enhance the main product',
33+
self::UPSELL => 'Higher-priced or premium versions with more features',
34+
};
35+
}
36+
}

src/Filament/Resources/ProductResource.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Eclipse\Catalogue\Filament\Resources;
44

55
use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions;
6+
use Eclipse\Catalogue\Enums\ProductRelationType;
67
use Eclipse\Catalogue\Enums\PropertyInputType;
78
use Eclipse\Catalogue\Filament\Filters\CustomPropertyConstraint;
89
use Eclipse\Catalogue\Filament\Forms\Components\ImageManager;
@@ -557,6 +558,47 @@ function ($query) {
557558
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp'])
558559
->columnSpanFull(),
559560
]),
561+
562+
Tabs\Tab::make('Related Products')
563+
->schema([
564+
Tabs::make('product_relations_tabs')
565+
->tabs([
566+
Tabs\Tab::make('related')
567+
->label('Related')
568+
->icon('heroicon-o-link')
569+
->schema([
570+
ViewComponent::make('eclipse-catalogue::livewire.product-relations-table')
571+
->viewData(fn (?Product $record) => [
572+
'productId' => $record?->id,
573+
'type' => ProductRelationType::RELATED->value,
574+
]),
575+
]),
576+
577+
Tabs\Tab::make('cross_sell')
578+
->label('Cross-sell')
579+
->icon('heroicon-o-plus-circle')
580+
->schema([
581+
ViewComponent::make('eclipse-catalogue::livewire.product-relations-table')
582+
->viewData(fn (?Product $record) => [
583+
'productId' => $record?->id,
584+
'type' => ProductRelationType::CROSS_SELL->value,
585+
]),
586+
]),
587+
588+
Tabs\Tab::make('upsell')
589+
->label('Upsell')
590+
->icon('heroicon-o-arrow-trending-up')
591+
->schema([
592+
ViewComponent::make('eclipse-catalogue::livewire.product-relations-table')
593+
->viewData(fn (?Product $record) => [
594+
'productId' => $record?->id,
595+
'type' => ProductRelationType::UPSELL->value,
596+
]),
597+
]),
598+
])
599+
->columnSpanFull(),
600+
])
601+
->visible(fn (?Product $record) => $record && $record->exists),
560602
])
561603
->columnSpanFull(),
562604
]);
@@ -1039,6 +1081,38 @@ protected static function getCustomPropertyColumns(): array
10391081
return $columns;
10401082
}
10411083

1084+
protected static function getRelationsColumns(): array
1085+
{
1086+
return [
1087+
TextColumn::make('related_count')
1088+
->label('Related')
1089+
->getStateUsing(function (Product $record) {
1090+
return $record->related()->count();
1091+
})
1092+
->badge()
1093+
->color('gray')
1094+
->toggleable(isToggledHiddenByDefault: true),
1095+
1096+
TextColumn::make('cross_sell_count')
1097+
->label('Cross-sell')
1098+
->getStateUsing(function (Product $record) {
1099+
return $record->crossSell()->count();
1100+
})
1101+
->badge()
1102+
->color('blue')
1103+
->toggleable(isToggledHiddenByDefault: true),
1104+
1105+
TextColumn::make('upsell_count')
1106+
->label('Upsell')
1107+
->getStateUsing(function (Product $record) {
1108+
return $record->upsell()->count();
1109+
})
1110+
->badge()
1111+
->color('green')
1112+
->toggleable(isToggledHiddenByDefault: true),
1113+
];
1114+
}
1115+
10421116
protected static function getCustomPropertyConstraints(): array
10431117
{
10441118
$constraints = [];

src/Filament/Resources/ProductResource/Pages/EditProduct.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
namespace Eclipse\Catalogue\Filament\Resources\ProductResource\Pages;
44

5+
use Eclipse\Catalogue\Enums\ProductRelationType;
56
use Eclipse\Catalogue\Filament\Resources\ProductResource;
7+
use Eclipse\Catalogue\Models\ProductRelation;
68
use Eclipse\Catalogue\Models\Property;
79
use Eclipse\Catalogue\Traits\HandlesTenantData;
810
use Eclipse\Catalogue\Traits\HasTenantFields;
911
use Filament\Actions;
1012
use Filament\Forms\Form;
1113
use Filament\Resources\Pages\EditRecord;
1214
use Illuminate\Database\Eloquent\Model;
15+
use Illuminate\Support\Facades\Log;
1316
use Nben\FilamentRecordNav\Actions\NextRecordAction;
1417
use Nben\FilamentRecordNav\Actions\PreviousRecordAction;
1518
use Nben\FilamentRecordNav\Concerns\WithRecordNavigation;
@@ -137,6 +140,13 @@ protected function afterSave(): void
137140
{
138141
if ($this->record) {
139142
$state = $this->form->getRawState();
143+
144+
$this->saveProductRelations([
145+
'related_products' => $this->data['related_products'] ?? [],
146+
'cross_sell_products' => $this->data['cross_sell_products'] ?? [],
147+
'upsell_products' => $this->data['upsell_products'] ?? [],
148+
]);
149+
140150
$propertyData = [];
141151
foreach ($state as $key => $value) {
142152
if (is_string($key) && str_starts_with($key, 'property_values_')) {
@@ -326,4 +336,59 @@ public function reorderImages(string $statePath, array $uuids): void
326336
->values()
327337
->toArray();
328338
}
339+
340+
protected function saveProductRelations(array $state): void
341+
{
342+
if (! $this->record) {
343+
return;
344+
}
345+
346+
$relationTypes = [
347+
'related_products' => ProductRelationType::RELATED,
348+
'cross_sell_products' => ProductRelationType::CROSS_SELL,
349+
'upsell_products' => ProductRelationType::UPSELL,
350+
];
351+
352+
foreach ($relationTypes as $fieldName => $relationType) {
353+
$relationItems = $state[$fieldName] ?? [];
354+
355+
if (empty($relationItems)) {
356+
continue;
357+
}
358+
359+
ProductRelation::where('parent_id', $this->record->id)
360+
->where('type', $relationType->value)
361+
->delete();
362+
363+
$position = 1;
364+
foreach ($relationItems as $item) {
365+
if (! is_array($item) || ! isset($item['product_id']) || empty($item['product_id'])) {
366+
continue;
367+
}
368+
369+
$childId = is_numeric($item['product_id']) ? (int) $item['product_id'] : null;
370+
371+
if ($childId && $childId !== $this->record->id) {
372+
try {
373+
ProductRelation::create([
374+
'parent_id' => $this->record->id,
375+
'child_id' => $childId,
376+
'type' => $relationType->value,
377+
'sort' => $position,
378+
]);
379+
$position++;
380+
} catch (\Exception $e) {
381+
Log::error('Failed to create product relation', [
382+
'parent_id' => $this->record->id,
383+
'child_id' => $childId,
384+
'type' => $relationType->value,
385+
'sort' => $position,
386+
'item' => $item,
387+
'error' => $e->getMessage(),
388+
]);
389+
}
390+
}
391+
}
392+
}
393+
}
329394
}

0 commit comments

Comments
 (0)