diff --git a/packages/admin/resources/lang/en/discount.php b/packages/admin/resources/lang/en/discount.php index 57ad128850..089b30b67c 100644 --- a/packages/admin/resources/lang/en/discount.php +++ b/packages/admin/resources/lang/en/discount.php @@ -4,13 +4,317 @@ 'plural_label' => 'Discounts', 'label' => 'Discount', 'form' => [ + 'conditions' => [ + 'heading' => 'Conditions', + ], + 'buy_x_get_y' => [ + 'heading' => 'Buy X Get Y', + ], + 'amount_off' => [ + 'heading' => 'Amount Off', + ], 'name' => [ 'label' => 'Name', ], + 'handle' => [ + 'label' => 'Handle', + ], + 'starts_at' => [ + 'label' => 'Start Date', + ], + 'ends_at' => [ + 'label' => 'End Date', + ], + 'priority' => [ + 'label' => 'Priority', + 'helper_text' => 'Discounts with higher priority will be applied first.', + 'options' => [ + 'low' => [ + 'label' => 'Low', + ], + 'medium' => [ + 'label' => 'Low', + ], + 'high' => [ + 'label' => 'High', + ], + ], + ], + 'stop' => [ + 'label' => 'Stop other discounts applying after this one', + ], + 'coupon' => [ + 'label' => 'Coupon', + 'helper_text' => 'Enter the coupon required for the discount to apply, if left blank it will apply automatically.', + ], + 'max_uses' => [ + 'label' => 'Max uses', + 'helper_text' => 'Leave blank for unlimited uses.', + ], + 'max_uses_per_user' => [ + 'label' => 'Max uses per user', + 'helper_text' => 'Leave blank for unlimited uses.', + ], + 'minimum_cart_amount' => [ + 'label' => 'Minimum Cart Amount', + ], + 'min_qty' => [ + 'label' => 'Product Quantity', + 'helper_text' => 'Set how many qualifying products are required for the discount to apply.', + ], + 'reward_qty' => [ + 'label' => 'No. of free items', + 'helper_text' => 'How many of each item are discounted.', + ], + 'max_reward_qty' => [ + 'label' => 'Maximum reward quantity', + 'helper_text' => 'The maximum amount of products which can be discounted, regardless of criteria.', + ], ], 'table' => [ 'name' => [ 'label' => 'Name', ], + 'status' => [ + 'label' => 'Status', + \Lunar\Models\Discount::ACTIVE => [ + 'label' => 'Active', + ], + \Lunar\Models\Discount::PENDING => [ + 'label' => 'Pending', + ], + \Lunar\Models\Discount::EXPIRED => [ + 'label' => 'Expired', + ], + \Lunar\Models\Discount::SCHEDULED => [ + 'label' => 'Scheduled', + ], + ], + 'type' => [ + 'label' => 'Type', + ], + 'starts_at' => [ + 'label' => 'Start Date', + ], + 'ends_at' => [ + 'label' => 'End Date', + ], + ], + 'pages' => [ + 'availability' => [ + 'label' => 'Availability', + ], + 'limitations' => [ + 'label' => 'Limitations', + ], + ], + 'relationmanagers' => [ + 'collections' => [ + 'title' => 'Collections', + 'description' => 'Select which collections this discount should be limited to.', + 'actions' => [ + 'attach' => [ + 'label' => 'Attach Collection', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + 'form' => [ + 'type' => [ + 'options' => [ + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + ], + ], + 'brands' => [ + 'title' => 'Brands', + 'description' => 'Select which brands this discount should be limited to.', + 'actions' => [ + 'attach' => [ + 'label' => 'Attach Brand', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + 'form' => [ + 'type' => [ + 'options' => [ + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + ], + ], + 'products' => [ + 'title' => 'Products', + 'description' => 'Select which products this discount should be limited to.', + 'actions' => [ + 'attach' => [ + 'label' => 'Add Product', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + 'form' => [ + 'type' => [ + 'options' => [ + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + ], + ], + 'rewards' => [ + 'title' => 'Product Rewards', + 'description' => 'Select which products will be discounted if they exist in the cart and the above conditions are met.', + 'actions' => [ + 'attach' => [ + 'label' => 'Add Product', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + 'form' => [ + 'type' => [ + 'options' => [ + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + ], + ], + 'conditions' => [ + 'title' => 'Product Conditions', + 'description' => 'Select the products required for the discount to apply.', + 'actions' => [ + 'attach' => [ + 'label' => 'Add Product', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'type' => [ + 'label' => 'Type', + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + 'form' => [ + 'type' => [ + 'options' => [ + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + ], + ], + 'productvariants' => [ + 'title' => 'Product Variants', + 'description' => 'Select which product variants this discount should be limited to.', + 'actions' => [ + 'attach' => [ + 'label' => 'Add Product Variant', + ], + ], + 'table' => [ + 'name' => [ + 'label' => 'Name', + ], + 'sku' => [ + 'label' => 'SKU', + ], + 'values' => [ + 'label' => 'Option(s)', + ], + ], + 'form' => [ + 'type' => [ + 'options' => [ + 'limitation' => [ + 'label' => 'Limitation', + ], + 'exclusion' => [ + 'label' => 'Exclusion', + ], + ], + ], + ], + ], ], ]; diff --git a/packages/admin/src/Filament/Resources/DiscountResource.php b/packages/admin/src/Filament/Resources/DiscountResource.php index efadd2e4f4..e537486909 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource.php +++ b/packages/admin/src/Filament/Resources/DiscountResource.php @@ -4,12 +4,25 @@ use Filament\Forms; use Filament\Forms\Components\Component; +use Filament\Forms\Form; +use Filament\Pages\Page; use Filament\Pages\SubNavigationPosition; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Support\Str; use Lunar\Admin\Filament\Resources\DiscountResource\Pages; +use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\BrandLimitationRelationManager; +use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\CollectionLimitationRelationManager; +use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\ProductConditionRelationManager; +use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\ProductLimitationRelationManager; +use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\ProductRewardRelationManager; +use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\ProductVariantLimitationRelationManager; use Lunar\Admin\Support\Resources\BaseResource; +use Lunar\DiscountTypes\AmountOff; +use Lunar\DiscountTypes\BuyXGetY; +use Lunar\Facades\Discounts; +use Lunar\Models\Currency; use Lunar\Models\Discount; class DiscountResource extends BaseResource @@ -42,22 +55,259 @@ public static function getNavigationGroup(): ?string return 'Sales'; } + public static function getDefaultForm(Form $form): Form + { + return $form->schema([ + Forms\Components\Section::make('')->schema( + static::getMainFormComponents() + ), + Forms\Components\Section::make('conditions')->schema( + static::getConditionsFormComponents() + )->heading( + __('lunarpanel::discount.form.conditions.heading') + ), + Forms\Components\Section::make('buy_x_get_y') + ->heading( + __('lunarpanel::discount.form.buy_x_get_y.heading') + ) + ->visible( + fn (Forms\Get $get) => $get('type') == BuyXGetY::class + )->schema( + static::getBuyXGetYFormComponents() + ), + Forms\Components\Section::make('amount_off') + ->heading( + __('lunarpanel::discount.form.amount_off.heading') + ) + ->visible( + fn (Forms\Get $get) => $get('type') == AmountOff::class + )->schema( + static::getAmountOffFormComponents() + ), + ]); + } + protected static function getMainFormComponents(): array { return [ - static::getNameFormComponent(), + Forms\Components\Group::make([ + static::getNameFormComponent(), + static::getHandleFormComponent(), + ])->columns(2), + Forms\Components\Group::make([ + static::getStartsAtFormComponent(), + static::getEndsAtFormComponent(), + ])->columns(2), + Forms\Components\Group::make([ + static::getPriorityFormComponent(), + static::getDiscountTypeFormComponent(), + ])->columns(2), + static::getStopFormComponent(), + ]; + } + + protected static function getConditionsFormComponents(): array + { + return [ + Forms\Components\Group::make([ + static::getCouponFormComponent(), + static::getMaxUsesFormComponent(), + static::getMaxUsesPerUserFormComponent(), + ])->columns(3), + Forms\Components\Fieldset::make()->schema( + static::getMinimumCartAmountsFormComponents() + )->label( + __('lunarpanel::discount.form.minimum_cart_amount.label') + ), ]; } - protected static function getNameFormComponent(): Component + public static function getNameFormComponent(): Component { return Forms\Components\TextInput::make('name') ->label(__('lunarpanel::discount.form.name.label')) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, $state, Forms\Set $set) { + if ($operation !== 'create') { + return; + } + $set('handle', Str::slug($state)); + }) ->required() ->maxLength(255) ->autofocus(); } + public static function getHandleFormComponent(): Component + { + return Forms\Components\TextInput::make('handle') + ->label(__('lunarpanel::discount.form.handle.label')) + ->required() + ->unique(ignoreRecord: true) + ->maxLength(255) + ->autofocus(); + } + + public static function getStartsAtFormComponent(): Component + { + return Forms\Components\DateTimePicker::make('starts_at') + ->label(__('lunarpanel::discount.form.starts_at.label')) + ->required() + ->before(function (Forms\Get $get) { + return $get('ends_at'); + }); + } + + public static function getEndsAtFormComponent(): Component + { + return Forms\Components\DateTimePicker::make('ends_at') + ->label(__('lunarpanel::discount.form.ends_at.label')); + } + + protected static function getPriorityFormComponent(): Component + { + return Forms\Components\Select::make('priority') + ->label(__('lunarpanel::discount.form.priority.label')) + ->helperText( + __('lunarpanel::discount.form.priority.helper_text') + ) + ->options(function () { + return [ + 1 => __('lunarpanel::discount.form.priority.options.low.label'), + 5 => __('lunarpanel::discount.form.priority.options.medium.label'), + 10 => __('lunarpanel::discount.form.priority.options.high.label'), + ]; + }); + } + + protected static function getStopFormComponent(): Component + { + return Forms\Components\Toggle::make('stop') + ->label( + __('lunarpanel::discount.form.stop.label') + ); + } + + protected static function getCouponFormComponent(): Component + { + return Forms\Components\TextInput::make('coupon') + ->label( + __('lunarpanel::discount.form.coupon.label') + )->helperText( + __('lunarpanel::discount.form.coupon.helper_text') + ); + } + + protected static function getMaxUsesFormComponent(): Component + { + return Forms\Components\TextInput::make('max_uses') + ->label( + __('lunarpanel::discount.form.max_uses.label') + )->helperText( + __('lunarpanel::discount.form.max_uses.helper_text') + ); + } + + protected static function getMaxUsesPerUserFormComponent(): Component + { + return Forms\Components\TextInput::make('max_uses_per_user') + ->label( + __('lunarpanel::discount.form.max_uses_per_user.label') + )->helperText( + __('lunarpanel::discount.form.max_uses_per_user.helper_text') + ); + } + + protected static function getMinimumCartAmountsFormComponents(): array + { + $currencies = Currency::enabled()->get(); + $inputs = []; + + foreach ($currencies as $currency) { + $inputs[] = Forms\Components\TextInput::make('data.min_prices.'.$currency->code)->label( + $currency->code + )->afterStateHydrated(function (Forms\Components\TextInput $component, $state) { + $currencyCode = last(explode('.', $component->getStatePath())); + $currency = Currency::whereCode($currencyCode)->first(); + + if ($currency) { + $component->state($state / $currency->factor); + } + }); + } + + return $inputs; + } + + public static function getDiscountTypeFormComponent(): Component + { + return Forms\Components\Select::make('type')->options( + Discounts::getTypes()->mapWithKeys( + fn ($type) => [get_class($type) => $type->getName()] + ) + )->required()->live(); + } + + protected static function getAmountOffFormComponents(): array + { + $currencies = Currency::get(); + + $currencyInputs = []; + + foreach ($currencies as $currency) { + $currencyInputs[] = Forms\Components\TextInput::make( + 'data.fixed_values.'.$currency->code + )->label($currency->name)->afterStateHydrated(function (Forms\Components\TextInput $component, $state) use ($currencies) { + $currencyCode = last(explode('.', $component->getStatePath())); + $currency = $currencies->first( + fn ($currency) => $currency->code == $currencyCode + ); + + if ($currency) { + $component->state($state / $currency->factor); + } + }); + } + + return [ + Forms\Components\Toggle::make('data.fixed_value')->live(), + Forms\Components\TextInput::make('data.percentage')->visible( + fn (Forms\Get $get) => ! $get('data.fixed_value') + )->numeric(), + Forms\Components\Group::make( + $currencyInputs + )->visible( + fn (Forms\Get $get) => (bool) $get('data.fixed_value') + )->columns(3), + ]; + } + + public static function getBuyXGetYFormComponents(): array + { + return [ + Forms\Components\TextInput::make('data.min_qty') + ->label( + __('lunarpanel::discount.form.min_qty.label') + )->helperText( + __('lunarpanel::discount.form.min_qty.helper_text') + )->numeric(), + Forms\Components\Group::make([ + Forms\Components\TextInput::make('data.reward_qty') + ->label( + __('lunarpanel::discount.form.reward_qty.label') + )->helperText( + __('lunarpanel::discount.form.reward_qty.helper_text') + )->numeric(), + Forms\Components\TextInput::make('data.max_reward_qty') + ->label( + __('lunarpanel::discount.form.max_reward_qty.label') + )->helperText( + __('lunarpanel::discount.form.max_reward_qty.helper_text') + )->numeric(), + ])->columns(2), + ]; + } + public static function getDefaultTable(Table $table): Table { return $table @@ -78,15 +328,54 @@ public static function getDefaultTable(Table $table): Table protected static function getTableColumns(): array { return [ + Tables\Columns\TextColumn::make('status') + ->formatStateUsing(function ($state) { + return __("lunarpanel::discount.table.status.{$state}.label"); + }) + ->label(__('lunarpanel::discount.table.status.label')) + ->badge() + ->color(fn (string $state): string => match ($state) { + Discount::ACTIVE => 'success', + Discount::EXPIRED => 'danger', + Discount::PENDING => 'gray', + Discount::SCHEDULED => 'info', + }), Tables\Columns\TextColumn::make('name') ->label(__('lunarpanel::discount.table.name.label')), + Tables\Columns\TextColumn::make('type') + ->formatStateUsing(function ($state) { + return (new $state)->getName(); + }) + ->label(__('lunarpanel::discount.table.type.label')), + Tables\Columns\TextColumn::make('starts_at') + ->label(__('lunarpanel::discount.table.starts_at.label')) + ->date(), + Tables\Columns\TextColumn::make('ends_at') + ->label(__('lunarpanel::discount.table.ends_at.label')) + ->date(), ]; } - public static function getRelations(): array + public static function getRecordSubNavigation(Page $page): array + { + return $page->generateNavigationItems([ + Pages\EditDiscount::class, + Pages\ManageDiscountAvailability::class, + Pages\ManageDiscountLimitations::class, + ]); + } + + protected static function getDefaultRelations(): array { return [ - // + CollectionLimitationRelationManager::class, + BrandLimitationRelationManager::class, + ProductLimitationRelationManager::class, + ProductVariantLimitationRelationManager::class, + ProductRewardRelationManager::class, + ProductConditionRelationManager::class, + ProductRewardRelationManager::class, + ProductConditionRelationManager::class, ]; } @@ -94,6 +383,9 @@ public static function getPages(): array { return [ 'index' => Pages\ListDiscounts::route('/'), + 'edit' => Pages\EditDiscount::route('/{record}'), + 'limitations' => Pages\ManageDiscountLimitations::route('/{record}/limitations'), + 'availability' => Pages\ManageDiscountAvailability::route('/{record}/availability'), ]; } } diff --git a/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php b/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php new file mode 100644 index 0000000000..754c8fe12b --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php @@ -0,0 +1,64 @@ +get(); + + foreach ($minPrices as $currencyCode => $value) { + $currency = $currencies->first( + fn ($currency) => $currency->code == $currencyCode + ); + + if (! $currency) { + continue; + } + $data['data']['min_prices'][$currencyCode] = (int) round($value * $currency->factor); + } + + foreach ($fixedPrices as $currencyCode => $fixedPrice) { + $currency = $currencies->first( + fn ($currency) => $currency->code == $currencyCode + ); + + if (! $currency) { + continue; + } + $data['data']['fixed_values'][$currencyCode] = (int) round($fixedPrice * $currency->factor); + } + + return $data; + } + + public function getRelationManagers(): array + { + $managers = []; + + if ($this->record->type == BuyXGetY::class) { + $managers[] = DiscountResource\RelationManagers\ProductConditionRelationManager::class; + $managers[] = DiscountResource\RelationManagers\ProductRewardRelationManager::class; + } + + return $managers; + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/Pages/ListDiscounts.php b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ListDiscounts.php index e6ca818968..a19d12b06e 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource/Pages/ListDiscounts.php +++ b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ListDiscounts.php @@ -3,6 +3,7 @@ namespace Lunar\Admin\Filament\Resources\DiscountResource\Pages; use Filament\Actions; +use Filament\Forms; use Lunar\Admin\Filament\Resources\DiscountResource; use Lunar\Admin\Support\Pages\BaseListRecords; @@ -13,7 +14,17 @@ class ListDiscounts extends BaseListRecords protected function getDefaultHeaderActions(): array { return [ - Actions\CreateAction::make(), + Actions\CreateAction::make()->form([ + Forms\Components\Group::make([ + DiscountResource::getNameFormComponent(), + DiscountResource::getHandleFormComponent(), + ])->columns(2), + Forms\Components\Group::make([ + DiscountResource::getStartsAtFormComponent(), + DiscountResource::getEndsAtFormComponent(), + ])->columns(2), + DiscountResource::getDiscountTypeFormComponent(), + ]), ]; } } diff --git a/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageBuyXGetYDiscount.php b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageBuyXGetYDiscount.php new file mode 100644 index 0000000000..78c097e387 --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageBuyXGetYDiscount.php @@ -0,0 +1,53 @@ +schema([]); + } + + protected function getFormActions(): array + { + return []; + } + + public function getRelationManagers(): array + { + return [ + RelationGroup::make('Limitations', [ + DiscountResource\RelationManagers\CollectionLimitationRelationManager::class, + DiscountResource\RelationManagers\BrandLimitationRelationManager::class, + DiscountResource\RelationManagers\ProductLimitationRelationManager::class, + DiscountResource\RelationManagers\ProductVariantLimitationRelationManager::class, + ]), + + ]; + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageDiscountAvailability.php b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageDiscountAvailability.php new file mode 100644 index 0000000000..476d107b4f --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageDiscountAvailability.php @@ -0,0 +1,47 @@ + [ + 'enabled', + 'visible', + ], + ]), + ]), + ]; + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageDiscountLimitations.php b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageDiscountLimitations.php new file mode 100644 index 0000000000..8b6f4919dd --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/Pages/ManageDiscountLimitations.php @@ -0,0 +1,53 @@ +schema([]); + } + + protected function getFormActions(): array + { + return []; + } + + public function getRelationManagers(): array + { + return [ + RelationGroup::make('Limitations', [ + DiscountResource\RelationManagers\CollectionLimitationRelationManager::class, + DiscountResource\RelationManagers\BrandLimitationRelationManager::class, + DiscountResource\RelationManagers\ProductLimitationRelationManager::class, + DiscountResource\RelationManagers\ProductVariantLimitationRelationManager::class, + ]), + + ]; + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/BrandLimitationRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/BrandLimitationRelationManager.php new file mode 100644 index 0000000000..187bf395d1 --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/BrandLimitationRelationManager.php @@ -0,0 +1,60 @@ +description( + __('lunarpanel::discount.relationmanagers.brands.description') + ) + ->paginated(false) + ->headerActions([ + Tables\Actions\AttachAction::make()->form(fn (Tables\Actions\AttachAction $action): array => [ + $action->getRecordSelect(), + Select::make('type') + ->options( + fn () => [ + 'limitation' => __('lunarpanel::discount.relationmanagers.brands.form.type.options.limitation.label'), + 'exclusion' => __('lunarpanel::discount.relationmanagers.brands.form.type.options.exclusion.label'), + ] + )->default('limitation'), + ])->recordTitle(function ($record) { + return $record->name; + })->preloadRecordSelect() + ->label( + __('lunarpanel::discount.relationmanagers.brands.actions.attach.label') + ), + ])->columns([ + Tables\Columns\TextColumn::make('name') + ->label( + __('lunarpanel::discount.relationmanagers.brands.table.name.label') + ), + Tables\Columns\TextColumn::make('pivot.type') + ->label( + __('lunarpanel::discount.relationmanagers.brands.table.type.label') + )->formatStateUsing( + fn (string $state) => __("lunarpanel::discount.relationmanagers.brands.table.type.{$state}.label") + ), + ])->actions([ + Tables\Actions\DetachAction::make(), + ]); + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/CollectionLimitationRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/CollectionLimitationRelationManager.php new file mode 100644 index 0000000000..9e0c5d54ed --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/CollectionLimitationRelationManager.php @@ -0,0 +1,64 @@ +description( + __('lunarpanel::discount.relationmanagers.collections.description') + ) + ->paginated(false) + ->headerActions([ + Tables\Actions\AttachAction::make()->form(fn (Tables\Actions\AttachAction $action): array => [ + $action->getRecordSelect(), + Select::make('type') + ->options( + fn () => [ + 'limitation' => __('lunarpanel::discount.relationmanagers.collections.form.type.options.limitation.label'), + 'exclusion' => __('lunarpanel::discount.relationmanagers.collections.form.type.options.exclusion.label'), + ] + )->default('limitation'), + ])->recordTitle(function ($record) { + return $record->attr('name'); + })->preloadRecordSelect() + ->label( + __('lunarpanel::discount.relationmanagers.collections.actions.attach.label') + ), + ])->columns([ + Tables\Columns\TextColumn::make('attribute_data.name') + ->label( + __('lunarpanel::discount.relationmanagers.collections.table.name.label') + ) + ->formatStateUsing( + fn (Model $record) => $record->attr('name') + ), + Tables\Columns\TextColumn::make('pivot.type') + ->label( + __('lunarpanel::discount.relationmanagers.collections.table.type.label') + )->formatStateUsing( + fn (string $state) => __("lunarpanel::discount.relationmanagers.collections.table.type.{$state}.label") + ), + ])->actions([ + Tables\Actions\DetachAction::make(), + ]); + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php new file mode 100644 index 0000000000..d642946cc4 --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php @@ -0,0 +1,83 @@ +heading( + __('lunarpanel::discount.relationmanagers.conditions.title') + ) + ->description( + __('lunarpanel::discount.relationmanagers.conditions.description') + ) + ->paginated(false) + ->modifyQueryUsing( + fn ($query) => $query->whereIn('type', ['condition']) + ->wherePurchasableType(Product::class) + ->whereHas('purchasable') + ) + ->headerActions([ + Tables\Actions\CreateAction::make()->form([ + Forms\Components\MorphToSelect::make('purchasable') + ->searchable(true) + ->types([ + Forms\Components\MorphToSelect\Type::make(Product::class) + ->titleAttribute('name.en') + ->getSearchResultsUsing(static function (Forms\Components\Select $component, string $search): array { + return Product::search($search) + ->get() + ->mapWithKeys(fn (Product $record): array => [$record->getKey() => $record->attr('name')]) + ->all(); + }), + ]), + ])->label( + __('lunarpanel::discount.relationmanagers.conditions.actions.attach.label') + )->mutateFormDataUsing(function (array $data) { + $data['type'] = 'condition'; + + return $data; + }), + ])->columns([ + Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') + ->collection('images') + ->conversion('small') + ->limit(1) + ->square() + ->label(''), + Tables\Columns\TextColumn::make('purchasable.attribute_data.name') + ->label( + __('lunarpanel::discount.relationmanagers.conditions.table.name.label') + ) + ->formatStateUsing( + fn (Model $record) => $record->purchasable->attr('name') + ), + ])->actions([ + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php new file mode 100644 index 0000000000..f7430351b9 --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php @@ -0,0 +1,78 @@ +heading( + __('lunarpanel::discount.relationmanagers.products.title') + ) + ->description( + __('lunarpanel::discount.relationmanagers.products.description') + ) + ->paginated(false) + ->modifyQueryUsing( + fn ($query) => $query->whereIn('type', ['limitation', 'exclusion']) + ->wherePurchasableType(Product::class) + ->whereHas('purchasable') + ) + ->headerActions([ + Tables\Actions\CreateAction::make()->form([ + Forms\Components\MorphToSelect::make('purchasable') + ->searchable(true) + ->types([ + Forms\Components\MorphToSelect\Type::make(Product::class) + ->titleAttribute('name.en') + ->getSearchResultsUsing(static function (Forms\Components\Select $component, string $search): array { + return Product::search($search) + ->get() + ->mapWithKeys(fn (Product $record): array => [$record->getKey() => $record->attr('name')]) + ->all(); + }), + ]), + ])->label( + __('lunarpanel::discount.relationmanagers.products.actions.attach.label') + )->mutateFormDataUsing(function (array $data) { + $data['type'] = 'limitation'; + + return $data; + }), + ])->columns([ + Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') + ->collection('images') + ->conversion('small') + ->limit(1) + ->square() + ->label(''), + Tables\Columns\TextColumn::make('purchasable.attribute_data.name') + ->label( + __('lunarpanel::discount.relationmanagers.products.table.name.label') + ) + ->formatStateUsing( + fn (Model $record) => $record->purchasable->attr('name') + ), + ])->actions([ + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php new file mode 100644 index 0000000000..e9d0de67aa --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php @@ -0,0 +1,83 @@ +heading( + __('lunarpanel::discount.relationmanagers.rewards.title') + ) + ->description( + __('lunarpanel::discount.relationmanagers.rewards.description') + ) + ->paginated(false) + ->modifyQueryUsing( + fn ($query) => $query->whereIn('type', ['reward']) + ->wherePurchasableType(Product::class) + ->whereHas('purchasable') + ) + ->headerActions([ + Tables\Actions\CreateAction::make()->form([ + Forms\Components\MorphToSelect::make('purchasable') + ->searchable(true) + ->types([ + Forms\Components\MorphToSelect\Type::make(Product::class) + ->titleAttribute('name.en') + ->getSearchResultsUsing(static function (Forms\Components\Select $component, string $search): array { + return Product::search($search) + ->get() + ->mapWithKeys(fn (Product $record): array => [$record->getKey() => $record->attr('name')]) + ->all(); + }), + ]), + ])->label( + __('lunarpanel::discount.relationmanagers.rewards.actions.attach.label') + )->mutateFormDataUsing(function (array $data) { + $data['type'] = 'reward'; + + return $data; + }), + ])->columns([ + Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') + ->collection('images') + ->conversion('small') + ->limit(1) + ->square() + ->label(''), + Tables\Columns\TextColumn::make('purchasable.attribute_data.name') + ->label( + __('lunarpanel::discount.relationmanagers.rewards.table.name.label') + ) + ->formatStateUsing( + fn (Model $record) => $record->purchasable->attr('name') + ), + ])->actions([ + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductVariantLimitationRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductVariantLimitationRelationManager.php new file mode 100644 index 0000000000..8c3802067a --- /dev/null +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductVariantLimitationRelationManager.php @@ -0,0 +1,88 @@ +heading( + __('lunarpanel::discount.relationmanagers.productvariants.title') + ) + ->description( + __('lunarpanel::discount.relationmanagers.productvariants.description') + ) + ->paginated(false) + ->modifyQueryUsing( + fn ($query) => $query->whereIn('type', ['limitation', 'exclusion']) + ->wherePurchasableType(ProductVariant::class) + ->whereHas('purchasable') + ) + ->headerActions([ + Tables\Actions\CreateAction::make()->form([ + Forms\Components\MorphToSelect::make('purchasable') + ->searchable(true) + ->types([ + Forms\Components\MorphToSelect\Type::make(ProductVariant::class) + ->titleAttribute('sku') + ->getSearchResultsUsing(static function (Forms\Components\Select $component, string $search): array { + $products = Product::search($search) + ->get(); + + return ProductVariant::whereIn('product_id', $products->pluck('id')) + ->get() + ->mapWithKeys(fn (ProductVariant $record): array => [$record->getKey() => $record->product->attr('name').' - '.$record->sku]) + ->all(); + }), + ]), + ])->label( + __('lunarpanel::discount.relationmanagers.productvariants.actions.attach.label') + )->mutateFormDataUsing(function (array $data) { + $data['type'] = 'limitation'; + + return $data; + }), + ])->columns([ + Tables\Columns\TextColumn::make('purchasable') + ->formatStateUsing( + fn (Model $model) => $model->purchasable->getDescription() + ) + ->label( + __('lunarpanel::discount.relationmanagers.productvariants.table.name.label') + ), + Tables\Columns\TextColumn::make('purchasable.sku') + ->label( + __('lunarpanel::discount.relationmanagers.productvariants.table.sku.label') + ), + Tables\Columns\TextColumn::make('purchasable.values') + ->formatStateUsing(function (Model $record) { + return $record->purchasable->values->map( + fn ($value) => $value->translate('name') + )->join(', '); + })->label( + __('lunarpanel::discount.relationmanagers.productvariants.table.values.label') + ), + ])->actions([ + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/packages/admin/src/LunarPanelManager.php b/packages/admin/src/LunarPanelManager.php index 4972c5e0e1..4a700e6015 100644 --- a/packages/admin/src/LunarPanelManager.php +++ b/packages/admin/src/LunarPanelManager.php @@ -111,6 +111,7 @@ public function register(): self 'lunar::customer-groups' => 'lucide-users', 'lunar::dashboard' => 'lucide-bar-chart-big', 'lunar::discounts' => 'lucide-percent-circle', + 'lunar::discount-limitations' => 'lucide-list-x', 'lunar::info' => 'lucide-info', 'lunar::languages' => 'lucide-languages', 'lunar::media' => 'lucide-image', diff --git a/packages/core/src/Models/Brand.php b/packages/core/src/Models/Brand.php index 3c74434e60..416ac5626b 100644 --- a/packages/core/src/Models/Brand.php +++ b/packages/core/src/Models/Brand.php @@ -76,4 +76,11 @@ public function products(): HasMany { return $this->hasMany(Product::class); } + + public function discounts() + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany(Discount::class, "{$prefix}brand_discount"); + } } diff --git a/packages/core/src/Models/Currency.php b/packages/core/src/Models/Currency.php index 416bd511ba..51bb4702be 100644 --- a/packages/core/src/Models/Currency.php +++ b/packages/core/src/Models/Currency.php @@ -44,6 +44,11 @@ protected static function newFactory(): CurrencyFactory return CurrencyFactory::new(); } + public function scopeEnabled($query, $enabled = true) + { + return $query->whereEnabled($enabled); + } + /** * Return the prices relationship */ diff --git a/packages/core/src/Models/Discount.php b/packages/core/src/Models/Discount.php index 88471e58e7..2592fdc3a0 100644 --- a/packages/core/src/Models/Discount.php +++ b/packages/core/src/Models/Discount.php @@ -36,6 +36,14 @@ class Discount extends BaseModel protected $guarded = []; + const ACTIVE = 'active'; + + const PENDING = 'pending'; + + const EXPIRED = 'expired'; + + const SCHEDULED = 'scheduled'; + /** * Define which attributes should be cast. * @@ -55,6 +63,23 @@ protected static function newFactory(): DiscountFactory return DiscountFactory::new(); } + public function getStatusAttribute() + { + $active = $this->starts_at?->isPast() && ! $this->ends_at?->isPast(); + $expired = $this->ends_at?->isPast(); + $future = $this->starts_at?->isFuture(); + + if ($expired) { + return static::EXPIRED; + } + + if ($future) { + return static::SCHEDULED; + } + + return $active ? static::ACTIVE : static::PENDING; + } + public function users(): BelongsToMany { $prefix = config('lunar.database.table_prefix'); diff --git a/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/EditDiscountTest.php b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/EditDiscountTest.php new file mode 100644 index 0000000000..b185c850bb --- /dev/null +++ b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/EditDiscountTest.php @@ -0,0 +1,47 @@ +group('resource.discount'); + +beforeEach(function () { + $this->asStaff(); +}); + +it('can render discount edit page', function () { + get( + \Lunar\Admin\Filament\Resources\DiscountResource::getUrl( + 'edit', + ['record' => \Lunar\Models\Discount::factory()->create()] + ) + )->assertSuccessful(); +}); + +it('can edit discount', function () { + $discount = \Lunar\Models\Discount::factory()->create(); + \Livewire\Livewire::test(\Lunar\Admin\Filament\Resources\DiscountResource\Pages\EditDiscount::class, + ['record' => $discount->getKey()] + )->fillForm([ + 'name' => 'Updated Name', + 'handle' => 'updated_name', + ])->call('save')->assertHasNoErrors(); + + assertDatabaseHas(\Lunar\Models\Discount::class, [ + 'name' => 'Updated Name', + 'handle' => 'updated_name', + ]); +}); + +it('can validate start and end date', function () { + $discount = \Lunar\Models\Discount::factory()->create(); + \Livewire\Livewire::test(\Lunar\Admin\Filament\Resources\DiscountResource\Pages\EditDiscount::class, + ['record' => $discount->getKey()] + )->fillForm([ + 'starts_at' => now(), + 'ends_at' => now()->subWeek(), + ])->call('save')->assertHasFormErrors([ + 'starts_at' => 'before', + ]); +}); diff --git a/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ListDiscountsTest.php b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ListDiscountsTest.php new file mode 100644 index 0000000000..275a04344a --- /dev/null +++ b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ListDiscountsTest.php @@ -0,0 +1,28 @@ +group('resource.discount'); + +beforeEach(function () { + $this->asStaff(); +}); + +it('can list discounts', function () { + get( + \Lunar\Admin\Filament\Resources\DiscountResource::getUrl('index') + )->assertSuccessful(); +}); + +it('can create a discount', function () { + $discount = \Lunar\Models\Discount::factory()->create(); + \Livewire\Livewire::test( + \Lunar\Admin\Filament\Resources\DiscountResource\Pages\ListDiscounts::class + )->callAction('create', [ + 'name' => 'Discount A', + 'handle' => 'discount_a', + 'starts_at' => now(), + 'type' => \Lunar\DiscountTypes\BuyXGetY::class, + ])->assertHasNoErrors(); +}); diff --git a/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ManageDiscountAvailabilityTest.php b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ManageDiscountAvailabilityTest.php new file mode 100644 index 0000000000..b41af193f7 --- /dev/null +++ b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ManageDiscountAvailabilityTest.php @@ -0,0 +1,20 @@ +group('resource.discount'); + +beforeEach(function () { + $this->asStaff(); +}); + +it('can render discount availability page', function () { + $record = \Lunar\Models\Discount::factory()->create(); + + \Lunar\Models\Channel::factory()->create(['default' => true]); + + get(\Lunar\Admin\Filament\Resources\DiscountResource::getUrl('availability', [ + 'record' => $record, + ]))->assertSuccessful(); +}); diff --git a/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ManageDiscountLimitationsTest.php b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ManageDiscountLimitationsTest.php new file mode 100644 index 0000000000..229ba2c13d --- /dev/null +++ b/tests/admin/Feature/Filament/Resources/DiscountResource/Pages/ManageDiscountLimitationsTest.php @@ -0,0 +1,20 @@ +group('resource.discount'); + +beforeEach(function () { + $this->asStaff(); +}); + +it('can render discount limitations page', function () { + $record = \Lunar\Models\Discount::factory()->create(); + + \Lunar\Models\Channel::factory()->create(['default' => true]); + + get(\Lunar\Admin\Filament\Resources\DiscountResource::getUrl('limitations', [ + 'record' => $record, + ]))->assertSuccessful(); +});