This commit is contained in:
2025-09-25 03:03:31 +05:00
commit ae480cf2f6
2768 changed files with 1485826 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Nova\Resources\Ecommerce\Channel;
use App\Models\Ecommerce\Channel\Channel as ChannelModel;
use App\Models\User;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Channel\Fields\ChannelFieldsForIndex;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use App\Nova\Resources\User\UserSearchResource;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\MorphMany;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\URL;
use Laravel\Nova\Http\Requests\NovaRequest;
use Outl1ne\NovaSortable\Traits\HasSortableRows;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Channel extends Resource
{
/**
* Sortable behavior
*/
use HasSortableRows;
/**
* The model the resource corresponds to.
*
* @var class-string<ChannelModel>
*/
public static $model = ChannelModel::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['media', 'user'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'name',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Channels');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Channel');
}
/**
* Fields for index
*/
public function fieldsForIndex(): array
{
return ChannelFieldsForIndex::make();
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200')
->rules('required')
->required(),
Text::make(__('Name'), 'name')
->rules(['required', 'string', 'max:255']),
Text::make(__('Description'), 'description')
->rules(['nullable', 'string', 'max:255']),
URL::make('URL'),
Hidden::make('is_default')->default(true),
Hidden::make('timezone')->default('Asia/Ashgabat'),
DateTime::make(__('Created at'), 'created_at')
->default(now())
->displayUsing(fn ($value) => $value->format('H:i, d.m.Y'))
->exceptOnForms()
->sortable(),
BelongsTo::make(__('User'), 'user', UserSearchResource::class)
->searchable()
->withSubtitles()
->readonly(fn () => $this->id === tmpostChannel()->id)
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->channelables_type = User::class;
$model->channelables_id = $request->input('user');
}),
NovaSwitcher::make(__('Active'), 'is_visible')
->default(true),
MorphMany::make(__('Products'), 'products', Product::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Nova\Resources\Ecommerce\Channel\Fields;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
class ChannelFieldsForIndex
{
/**
* Fields for index
*/
public static function make(): array
{
return [
ID::make()->hidden(),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200'),
Text::make(__('Name'), 'name')
->sortable(),
Text::make(__('User'), fn ($model) => $model->user?->fullname ?? '-'),
DateTime::make(__('Created at'), 'created_at')
->turkmenDate()
->sortable(),
];
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace App\Nova\Resources\Ecommerce\Payout;
use App\Models\Ecommerce\Payouts\Payout;
use App\Nova\Repos\Payouts\PayoutsRepo;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Order\Actions\ExportEvidenceForProducts;
use App\Repositories\CMS\Icon\IconRepository;
use App\Repositories\Ecommerce\Channel\ChannelRepository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Http\Requests\NovaRequest;
use Nurmuhammet\PayoutProducts\PayoutProducts;
class PayoutResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<\App\Models\Ecommerce\Payout>
*/
public static $model = Payout::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id',
];
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['channel'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Payout');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Payouts');
}
/**
* After resource has been created
*/
public static function afterCreate(NovaRequest $request, Model $model): void
{
PayoutsRepo::orderItemIdsFill($request, $model);
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Select::make(__('Channel'), 'channel_id')
->fullWidth()
->displayUsingLabels()
->searchable()
->options(ChannelRepository::values())
->rules('required')
->canSeeWhen('isAdmin', $this),
Date::make(__('Start date'), 'start_date')
->rules('required')
->fullWidth()
->displayUsing(fn ($value) => $value->format('d.m.Y'))
->readonly(fn ($request) => $request->isUpdateOrUpdateAttachedRequest())
->sortable(),
Date::make(__('End date'), 'end_date')
->rules('required')
->fullWidth()
->displayUsing(fn ($value) => $value->format('d.m.Y'))
->readonly(fn ($request) => $request->isUpdateOrUpdateAttachedRequest())
->sortable(),
PayoutProducts::make(__('Products'), 'order_items_ids')
->dependsOn(['channel_id', 'start_date', 'end_date'], PayoutsRepo::orderItemIdsDependsOn())
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->total_sum = $request->total_sum;
$model->entrepreneur_total = $request->entrepreneur_total;
$model->postshop_total = $request->postshop_total;
})
->products(PayoutsRepo::resolveOrderItems($request, $this))
->fullWidth()
->hideFromIndex(),
Text::make(__('Hasap'), 'total_sum')
->fullWidth()
->exceptOnForms()
->sortable(),
Text::make(__('Entrepreneur Total'), 'entrepreneur_total')
->fullWidth()
->exceptOnForms()
->sortable(),
Text::make(__('POSTSHOP Total'), 'postshop_total')
->fullWidth()
->exceptOnForms()
->sortable(),
Textarea::make(__('Notes'), 'notes')->fullWidth(),
];
}
/**
* Get the cards available for the request.
*/
public function cards(NovaRequest $request): array
{
return [];
}
/**
* Get the filters available for the resource.
*/
public function filters(NovaRequest $request): array
{
return [];
}
/**
* Get the lenses available for the resource.
*/
public function lenses(NovaRequest $request): array
{
return [];
}
/**
* Get the actions available for the resource.
*/
public function actions(NovaRequest $request): array
{
return [
ExportEvidenceForProducts::make()
->confirmText('Delilnama çykarmak isleýarmisiňiz?')
->confirmButtonText('Hawa')
->cancelButtonText('Ýok')
->icon(IconRepository::make()->documentDownload()),
];
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Attribute;
use App\Models\Ecommerce\Product\Property\Attribute as AttributeModel;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Category\Category;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Http\Requests\NovaRequest;
use NovaAttachMany\AttachMany;
use Outl1ne\MultiselectField\Multiselect;
use Outl1ne\NovaSortable\Traits\HasSortableRows;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Attribute extends Resource
{
/**
* Sortable behavior
*/
use HasSortableRows;
/**
* The model the resource corresponds to.
*
* @var class-string<AttributeModel>
*/
public static $model = AttributeModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'name',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Attributes');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Attribute');
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Text::make(__('Name'), 'name')
->rules('required', 'string')
->translatable(),
Select::make(__('Type'), 'type')
->displayUsingLabels()
->searchable()
->default(AttributeModel::typesFields()['text'])
->options(AttributeModel::typesFields())
->rules('required'),
Trix::make(__('Description'), 'description')
->translatable(),
NovaSwitcher::make(__('Is enabled'), 'is_visible')
->default(true)
->canSeeWhen('systemUser', $this),
NovaSwitcher::make(__('Is required'), 'is_required')
->default(true)
->canSeeWhen('systemUser', $this),
NovaSwitcher::make(__('Is searchable'), 'is_searchable')
->default(false)
->canSeeWhen('systemUser', $this),
NovaSwitcher::make(__('Is filterable'), 'is_filterable')
->default(false)
->canSeeWhen('systemUser', $this),
Boolean::make(__('Is enabled'), 'is_visible')
->canSeeWhen('vendor', $this),
Boolean::make(__('Is required'), 'is_required')
->canSeeWhen('vendor', $this),
Boolean::make(__('Is searchable'), 'is_searchable')
->canSeeWhen('vendor', $this),
Boolean::make(__('Is filterable'), 'is_filterable')
->canSeeWhen('vendor', $this),
AttachMany::make(__('Categories'), 'categories', Category::class),
Multiselect::make(__('Categories'), 'categories')->belongsToMany(Category::class)
->onlyOnDetail(),
HasMany::make(__('Attribute values'), 'values', AttributeValue::class)
->hideFromDetail(function (NovaRequest $request, $resource) {
return $this->type !== 'select';
}),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Attribute;
use App\Models\Ecommerce\Product\Property\AttributeValue as AttributeValueModel;
use App\Nova\Resource;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class AttributeValue extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<AttributeValueModel>
*/
public static $model = AttributeValueModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'value';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id', 'value',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Attribute Values');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Attribute Value');
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterCreate(NovaRequest $request, $resource): string
{
return sprintf('/resources/attributes/%s', $resource->attribute_id);
}
/**
* Return the location to redirect the user after update.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterUpdate(NovaRequest $request, $resource)
{
return sprintf('/resources/attributes/%s', $resource->attribute_id);
}
/**
* Get the fields displayed by the resource.
*
* @return array
*/
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Text::make(__('Value'), 'value')
->translatable()
->rules('required'),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Brand;
use App\Models\Ecommerce\Product\Brand\Brand as BrandModel;
use App\Nova\Filters\VisableFilter;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Http\Requests\NovaRequest;
use Outl1ne\NovaSortable\Traits\HasSortableRows;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Brand extends Resource
{
/**
* Sortable behavior
*/
use HasSortableRows;
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = BrandModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['media'];
/**
* Indicates if the resource should be globally searchable.
*
* @var bool
*/
public static $globallySearchable = true;
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'name', 'slug',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Brands');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Brand');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*/
public static function indexQuery(NovaRequest $request, $query)
{
return $query;
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200')
->rules('required')
->required(),
Text::make(__('Name'), 'name')
->rules(['required', 'max:150'])
->creationRules('unique:brands,name'),
Text::make(__('Web site'), 'website')
->hideFromIndex(),
Trix::make(__('description'), 'description')
->translatable(),
Boolean::make(__('Market related'), 'type')
->sortable()
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = $request->boolean('type') ? 'market' : '';
}),
Text::make(__('Seo title'), 'seo_title')
->hideFromIndex(),
Textarea::make(__('Seo description'), 'seo_description'),
NovaSwitcher::make(__('Is enabled'), 'is_visible')
->default(true)
->canSeeWhen('systemUser', $this)
->sortable(),
HasMany::make('Products', 'products', Product::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [
VisableFilter::make(),
];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Category;
use App\Models\Ecommerce\Product\Category\Category as CategoryModel;
use App\Nova\Filters\VisableFilter;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Category\Filters\Level;
use App\Nova\Resources\Ecommerce\Product\Category\Filters\RelatedToMarket;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use App\Repositories\Ecommerce\Product\Category\CategoryRepository;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Color;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\MorphMany;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Http\Requests\NovaRequest;
use Outl1ne\NovaSortable\Traits\HasSortableRows;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Category extends Resource
{
/**
* Sortable behavior
*/
use HasSortableRows;
/**
* The model the resource corresponds to.
*
* @var class-string<CategoryModel>
*/
public static $model = CategoryModel::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array<string>
*/
public static $with = ['parent', 'media'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'name', 'slug',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Category');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Categories');
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200')
->rules('required')
->required(),
Color::make(__('Color'), 'color')
->rules('required')
->hideFromIndex()
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->options->set([
'color' => $request->input($attribute),
]);
}),
Select::make(__('Parent Category'), 'parent_id')
->searchable()
->options(fn () => (new CategoryRepository(request()))->getNamesForNova())
->onlyOnForms(),
BelongsTo::make(__('Parent'), 'parent', self::class)
->exceptOnForms(),
Text::make(__('Name'), 'name')
->rules('required', 'min:2')
->translatable(),
Textarea::make(__('Description'), 'description'),
Text::make(__('Tax percentage'), 'tax_percentage')
->rules('required', 'numeric', 'max:100'),
NovaSwitcher::make(__('Is enabled'), 'is_visible')
->default(true)
->canSeeWhen('systemUser', $this)
->sortable(),
NovaSwitcher::make(__('Market related'), 'type')
->canSeeWhen('systemUser', $this)
->sortable()
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = $request->boolean('type') ? 'market' : '';
}),
Text::make(__('Seo title'), 'seo_title')
->hideFromIndex(),
Textarea::make(__('Seo description'), 'seo_description'),
HasMany::make(__('Children'), 'children', self::class),
MorphMany::make('Products', 'products', Product::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*/
public function filters(NovaRequest $request): array
{
return [
RelatedToMarket::make(),
Level::make(),
VisableFilter::make(),
];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Category\Filters;
use Laravel\Nova\Filters\Filter;
use Laravel\Nova\Http\Requests\NovaRequest;
class Level extends Filter
{
/**
* The filter's component.
*
* @var string
*/
public $component = 'select-filter';
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(NovaRequest $request, $query, $value)
{
if ($value == 1) {
return $query->where('parent_id', null);
}
if ($value == 2) {
$query->whereNotNull('parent_id');
}
// if ($value == 3) {
// $query->isLeaf();
// }
return $query;
}
/**
* Get the filter's available options.
*
* @return array
*/
public function options(NovaRequest $request)
{
return [
1 => 1,
2 => 2,
// 3 => 3
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Category\Filters;
use Laravel\Nova\Filters\BooleanFilter;
use Laravel\Nova\Http\Requests\NovaRequest;
class RelatedToMarket extends BooleanFilter
{
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(NovaRequest $request, $query, $value)
{
return $value['market']
? $query->where('type', 'market')
: $query;
}
/**
* Get the filter's available options.
*
* @return array
*/
public function options(NovaRequest $request)
{
return [
__('Market related') => 'market',
];
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Collection;
use App\Models\Ecommerce\Product\Collection\Collection as CollectionModel;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\MorphMany;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Http\Requests\NovaRequest;
use Outl1ne\NovaSortable\Traits\HasSortableRows;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Collection extends Resource
{
/**
* Sortable behavior
*/
use HasSortableRows;
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = CollectionModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['media'];
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'name',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Collections');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Collection');
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200')
->rules('required')
->required(),
Text::make(__('Name'), 'name')
->rules('required')
->translatable(),
Trix::make(__('description'), 'description')
->translatable(),
NovaSwitcher::make(__('Active'), 'is_visible')
->canSeeWhen('systemUser', $this)
->default(false),
Text::make(__('SEO title'), 'seo_title')->hideFromIndex(),
Text::make(__('SEO description'), 'seo_description')->hideFromIndex(),
MorphMany::make('Products', 'products', Product::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
public static function canSort(NovaRequest $request, $resource): bool
{
return true;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Coupon;
use App\Models\Ecommerce\Product\Coupon\Coupon as CouponModel;
use App\Nova\Resource;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Coupon extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<CouponModel>
*/
public static $model = CouponModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* Indicates if the resource should be globally searchable.
*
* @var bool
*/
public static $globallySearchable = true;
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'code',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Coupons');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Coupon');
}
/**
* Get the fields displayed by the resource.
*
* @return array
*/
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Text::make(__('Code'), 'code')
->rules('required', 'max:255')
->creationRules('unique:coupons,code'),
Select::make(__('Discount Type'), 'discount_type')
->displayUsingLabels()
->searchable()
->options(CouponModel::discountTypes())
->default(CouponModel::defaultDiscountType())
->rules('required'),
Text::make(__('Discount Value'), 'discount_value')
->dependsOn('discount_type', function ($field, $request, $formData) {
if ($formData->discount_type === 'percentage') {
$field->rules('required', 'numeric', 'min:1', 'max:100');
}
})
->rules('required', 'numeric'),
Text::make(__('Notes'), 'notes')
->rules('nullable', 'max:255')
->hideFromIndex(),
NovaSwitcher::make(__('Active'), 'is_active')
->default(true),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Inventory;
use App\Models\Ecommerce\Product\Inventory\Inventory as InventoryModel;
use App\Models\System\Settings\Location\Province;
use App\Models\System\Settings\Location\Region;
use App\Nova\Permissions\NovaPermission;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Channel\Channel;
use App\Nova\Resources\Ecommerce\Product\Inventory\Product\ProductResource;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Currency;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class Inventory extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<\App\Models\Shop\Inventory\Inventory>
*/
public static $model = InventoryModel::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['channel'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = ['name', 'code', 'email', 'phone_number'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Inventories');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Inventory');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$user = $request->user();
if ($user->hasRole('vendor')) {
$query->where('channel_id', $user->channel()->id);
}
return $query;
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
BelongsTo::make(__('Channel'), 'channel', Channel::class)
->rules('required')
->canSeeWhen('systemUser', $this)
->sortable(),
Hidden::make('channel_id')
->default(fn () => $request->isCreateOrAttachRequest() ? $request->user()->channel()?->id : null)
->canSee(NovaPermission::canSeeIfUserIsEntrepreneur()),
Text::make(__('Name'), 'name')
->rules('required', 'string', 'max:255')
->sortable(),
Text::make(__('Code'), 'code')->exceptOnForms()->sortable(),
Select::make(__('Region'), 'region')
->displayUsingLabels()
->searchable()
->options(Region::values())
->default(Region::AG)
->rules('required')
->sortable(),
Select::make(__('Province'), 'province_id')
->displayUsingLabels()
->searchable()
->dependsOn(['region'], function ($field, $request, $formData) {
if ($formData->region != 'ag') {
$field->rules('required');
}
$field->options(
Province::where('region', $formData->region)->pluck(
'name',
'id'
)
);
})
->hideFromIndex()
->sortable(),
Text::make(__('Address'), 'street_address')
->rules('nullable', 'string', 'max:255')
->hideFromIndex(),
Text::make(__('Description'), 'description')->hideFromIndex(),
Text::make(__('Email'), 'email')
->rules('nullable', 'string', 'max:255')
->hideFromIndex()
->sortable(),
Currency::make(__('Phone'), 'phone_number')
->symbol('+993')
->displayUsing(fn ($value) => $value ? '+(993)-'.$value : '—')
->textAlign('left')
->rules('nullable', 'string', 'max:255')
->sortable(),
Number::make(__('Zipcode'), 'zipcode')
->rules('nullable', 'integer')
->hideFromIndex()
->sortable(),
Text::make(__('Longitude'), 'longitude')->hideFromIndex(),
Text::make(__('Latitude'), 'latitude')->hideFromIndex(),
Boolean::make(__('Sharable'), 'shareable')
->canSeeWhen('isAdmin', $this)
->sortable(),
HasMany::make(__('Inventory entry'), 'history', InventoryHistoryResource::class),
HasMany::make(__('Inventory exit'), 'historyRemoved', InventoryHistoryRemovedResource::class),
BelongsToMany::make(__('Products'), 'products', ProductResource::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Inventory;
use App\Models\Ecommerce\Product\Inventory\InventoryHistoryItem;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class InventoryHistoryItemResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<\App\Models\Ecommerce\Product\Inventory\InventoryHistory>
*/
public static $model = InventoryHistoryItem::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['inventoryHistory', 'product'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Items');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Item');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$user = $request->user();
return $query;
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForUpdate(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Number::make(__('Quantity'), 'quantity')
->rules('required', 'min:1', 'numeric'),
Text::make(__('Cost amount'), 'cost_amount'),
Text::make(__('Total'), 'total'),
];
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
BelongsTo::make(__('Inventory history'), 'inventoryHistory', InventoryHistoryResource::class),
BelongsTo::make(__('Product'), 'product', Product::class),
Number::make(__('Quantity'), 'quantity')
->rules('required', 'min:1', 'numeric'),
Number::make(__('Cost amount'), 'cost_amount'),
Text::make(__('Total'), 'total'),
];
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterUpdate(NovaRequest $request, $resource): string
{
return sprintf('/resources/inventory-history-resources/%s', $resource->inventory_history_id);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Inventory;
use App\Models\Ecommerce\Product\Inventory\InventoryHistoryRemovedItem;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class InventoryHistoryRemovedItemResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<\App\Models\Ecommerce\Product\Inventory\InventoryHistory>
*/
public static $model = InventoryHistoryRemovedItem::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['inventoryHistoryRemoved', 'product'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Items');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Item');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$user = $request->user();
return $query;
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForUpdate(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Number::make(__('Quantity'), 'quantity')
->rules('required', 'min:1', 'numeric'),
Text::make(__('Cost amount'), 'cost_amount'),
Text::make(__('Total'), 'total'),
];
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
BelongsTo::make(__('Inventory history'), 'inventoryHistoryRemoved', InventoryHistoryRemovedResource::class),
BelongsTo::make(__('Product'), 'product', Product::class),
Number::make(__('Quantity'), 'quantity')
->rules('required', 'min:1', 'numeric'),
Number::make(__('Cost amount'), 'cost_amount'),
Text::make(__('Total'), 'total'),
];
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterUpdate(NovaRequest $request, $resource): string
{
return sprintf('/resources/inventory-history-resources/%s', $resource->inventory_history_id);
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Inventory;
use App\Models\Ecommerce\Product\Inventory\InventoryHistoryRemoved;
use App\Models\Ecommerce\Product\Product\Product;
use App\Nova\Repeater\InventoryHistoryItemRepeater;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Channel\Channel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Repeater;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class InventoryHistoryRemovedResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<\App\Models\Ecommerce\Product\Inventory\InventoryHistoryRemoved>
*/
public static $model = InventoryHistoryRemoved::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['channel', 'inventory'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'code';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Inventory exits');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Inventory exit');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$user = $request->user();
if ($user->hasRole('vendor')) {
$query->where('channel_id', $user->channel()->id);
}
return $query;
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterCreate(NovaRequest $request, $resource): string
{
return 'resources/inventories/'.$resource->inventory_id;
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Date::make(__('Date'), 'date')
->rules('required')
->displayUsing(fn ($value) => $value->format('d.m.Y'))
->readonly(fn ($request) => $request->isUpdateOrUpdateAttachedRequest())
->default(now()->toDate())
->sortable(),
Text::make(__('R. Number'), 'number')
->rules('required')
->readonly(fn ($request) => $request->isUpdateOrUpdateAttachedRequest())
->sortable(),
BelongsTo::make(__('Inventory'), 'inventory', Inventory::class),
BelongsTo::make(__('Channel'), 'channel', Channel::class),
Text::make(__('Notes'), 'notes')
->rules('nullable', 'string', 'max:255')
->hideFromIndex(),
Text::make(__('Total'), fn () => $this->total),
Text::make(__('Total'), 'readonly_total')
->onlyOnForms()
->readonly(),
Repeater::make('Options', 'options')
->repeatables([
InventoryHistoryItemRepeater::make(),
])
->asJson()
->fullWidth()
->hideWhenUpdating(),
HasMany::make(__('Items'), 'items', InventoryHistoryRemovedItemResource::class),
];
}
/**
* Register a callback to be called after the resource is created.
*/
public static function afterCreate(NovaRequest $request, Model $model): void
{
$total = 0;
$request->collect('options')->each(function ($item) use ($model, &$total) {
$total = floatval($total) + floatval($item['fields']['total']);
$model->items()->create([
'product_id' => $item['fields']['product_id'],
'quantity' => $item['fields']['quantity'],
'total' => floatval($item['fields']['total']),
'cost_amount' => $item['fields']['cost_amount'],
]);
$product = Product::find($item['fields']['product_id']);
$inventoryRecordQuery = DB::table('inventory_product')
->where('product_id', $product->id)
->where('inventory_id', $model->inventory_id);
$inventoryRecord = $inventoryRecordQuery->first();
$inventoryRecord
? $inventoryRecordQuery->update([
'stock' => intval($inventoryRecord->stock) - intval($item['fields']['quantity']),
]) : DB::table('inventory_product')->insert([
'product_id' => $product->id,
'inventory_id' => $model->inventory_id,
'stock' => -intval($item['fields']['quantity']),
]);
$product->updateQuietly([
'stock' => $product->inventories()->sum('inventory_product.stock'),
]);
});
$model->updateQuietly(['total' => $total]);
}
/**
* Determine if the current user can update the given resource.
*/
public function authorizedToUpdate(Request $request): bool
{
return false;
}
/**
* Determine if the current user can delete the given resource.
*/
public function authorizedToDelete(Request $request): bool
{
if (auth()->user()->isMe()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Inventory;
use App\Models\Ecommerce\Product\Inventory\InventoryHistory;
use App\Models\Ecommerce\Product\Product\Product;
use App\Nova\Repeater\InventoryHistoryItemRepeater;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Channel\Channel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Repeater;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class InventoryHistoryResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<\App\Models\Ecommerce\Product\Inventory\InventoryHistory>
*/
public static $model = InventoryHistory::class;
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['channel', 'inventory'];
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'code';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Inventory entries');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Inventory entry');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$user = $request->user();
if ($user->hasRole('vendor')) {
$query->where('channel_id', $user->channel()->id);
}
return $query;
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterCreate(NovaRequest $request, $resource): string
{
return 'resources/inventories/'.$resource->inventory_id;
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Date::make(__('Date'), 'date')
->rules('required')
->displayUsing(fn ($value) => $value->format('d.m.Y'))
->readonly(fn ($request) => $request->isUpdateOrUpdateAttachedRequest())
->default(now()->toDate())
->sortable(),
Text::make(__('R. Number'), 'number')
->rules('required')
->readonly(fn ($request) => $request->isUpdateOrUpdateAttachedRequest())
->sortable(),
BelongsTo::make(__('Inventory'), 'inventory', Inventory::class),
BelongsTo::make(__('Channel'), 'channel', Channel::class),
Text::make(__('Notes'), 'notes')
->rules('nullable', 'string', 'max:255')
->hideFromIndex(),
Text::make(__('Total'), fn () => $this->total),
Text::make(__('Total'), 'readonly_total')
->onlyOnForms()
->readonly(),
Repeater::make('Options', 'options')
->repeatables([
InventoryHistoryItemRepeater::make(),
])
->asJson()
->fullWidth()
->hideWhenUpdating(),
HasMany::make(__('Items'), 'items', InventoryHistoryItemResource::class),
];
}
/**
* Register a callback to be called after the resource is created.
*/
public static function afterCreate(NovaRequest $request, Model $model): void
{
$total = 0;
$request->collect('options')->each(function ($item) use ($model, &$total) {
$total = floatval($total) + floatval($item['fields']['total']);
$model->items()->create([
'product_id' => $item['fields']['product_id'],
'quantity' => $item['fields']['quantity'],
'total' => floatval($item['fields']['total']),
'cost_amount' => $item['fields']['cost_amount'],
]);
$product = Product::find($item['fields']['product_id']);
$inventoryRecordQuery = DB::table('inventory_product')
->where('product_id', $product->id)
->where('inventory_id', $model->inventory_id);
$inventoryRecord = $inventoryRecordQuery->first();
$inventoryRecord
? $inventoryRecordQuery->update([
'stock' => intval($inventoryRecord->stock) + intval($item['fields']['quantity']),
]) : DB::table('inventory_product')->insert([
'product_id' => $product->id,
'inventory_id' => $model->inventory_id,
'stock' => intval($item['fields']['quantity']),
]);
$product->updateQuietly([
'stock' => $product->inventories()->sum('inventory_product.stock'),
]);
});
$model->updateQuietly(['total' => $total]);
}
/**
* Determine if the current user can update the given resource.
*/
public function authorizedToUpdate(Request $request): bool
{
return false;
}
/**
* Determine if the current user can delete the given resource.
*/
public function authorizedToDelete(Request $request): bool
{
if (auth()->user()->isMe()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Inventory\Product;
use App\Models\Ecommerce\Product\Product\Product;
use App\Nova\Fields\FieldHelpers;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Brand\Brand;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class ProductResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<Product>
*/
public static $model = Product::class;
/**
* The number of resources to show per page via relationships.
*
* @var int
*/
public static $perPageViaRelationship = 20;
/**
* The pagination per-page options configured for this resource.
*/
public static function perPageOptions(): array
{
return [50, 100, 150];
}
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['brand', 'media'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Product');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Products');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*/
public static function indexQuery(NovaRequest $request, $query): Builder
{
return $query->where('parent_id', null);
}
/**
* Fields
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')->conversionOnIndexView('thumb200x200'),
Text::make(__('Name'), fn () => $this->novaDetailPage())
->displayUsing(FieldHelpers::asLink(
link: $this->novaDetailPage(),
title: $this->name
))
->asHtml(),
BelongsTo::make(__('Brand'), 'brand', Brand::class)->sortable(),
Number::make(__('Price'), 'cost_amount')->sortable(),
Text::make(__('SKU'), 'sku')->sortable(),
Text::make(__('Total count'), FieldHelpers::formatQuantity())->asHtml(),
Text::make(__('Giriş çykyş boýunça'), FieldHelpers::formatQuantity(
intval(DB::table('inventory_history_items')->where('product_id', $this->id)->sum('quantity'))
-
intval(DB::table('inventory_history_removed_items')->where('product_id', $this->id)->sum('quantity'))
))->asHtml(),
Boolean::make(__('Active'), 'is_visible')
->sortable()
->canSeeWhen('isVendor', $this),
NovaSwitcher::make(__('Is visible'), 'is_visible')
->sortable()
->canSeeWhen('isAdmin', $this),
];
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Actions;
use App\Models\Ecommerce\Payouts\Payout;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Nova\Actions\Action;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Http\Requests\NovaRequest;
use PhpOffice\PhpWord\SimpleType\TblWidth;
class ExportEvidenceForProducts extends Action
{
use InteractsWithQueue, Queueable;
/**
* The displayable name of the action.
*
* @var string
*/
public function name(): string
{
return __('Evidence for products');
}
/**
* Perform the action on the given models.
*
* @return mixed
*/
public function handle(ActionFields $fields, Collection $models)
{
$payout = $models->first();
$orderItems = $payout->orderItems;
$channel = $payout->channel;
$user = $channel->user;
exec('rm -rf app-docs/*');
$documentsPath = 'app-docs/'.Str::random();
exec("mkdir {$documentsPath}");
$this->generatePriceNegotiationDocument($user, $payout, $orderItems, $documentsPath);
$this->generateEvidenceForProductsDocument($user, $payout, $orderItems, $documentsPath);
return Action::openInNewTab(route('user.docs', ['folder' => $documentsPath]));
}
protected function generatePriceNegotiationDocument(User $user, Payout $payout, Collection $orderItems, string $folder): void
{
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor(resource_path('docs/order/evidence-price-negotiation.docx'));
$templateProcessor->setValues([
'year' => date('Y'),
'product_owner' => $user->companyName(),
]);
$table = new \PhpOffice\PhpWord\Element\Table([
'borderSize' => 2,
'borderColor' => 'black',
'width' => 5000,
'width' => 6000,
'unit' => TblWidth::TWIP,
]);
$table->addRow();
$table->addCell()->addText('');
$table->addCell()->addText('Harydyň ady');
$table->addCell()->addText('Komitentiň satyş bahasy, manat');
$table->addCell()->addText('Komissioneriň satyş bahasy, manat');
$i = 0;
$total_cost_amount = 0;
$total_unit_price_amount = 0;
foreach ($orderItems as $orderItem) {
$i++;
$table->addRow();
$table->addCell()->addText($i);
$table->addCell()->addText($orderItem->product_name);
$table->addCell()->addText($orderItem->unit_cost_amount);
$table->addCell()->addText($orderItem->unit_price_amount);
$total_cost_amount += $orderItem->unit_cost_amount;
$total_unit_price_amount += $orderItem->unit_price_amount;
}
$table->addRow();
$table->addCell()->addText();
$table->addCell()->addText('Jemi');
$table->addCell()->addText($total_cost_amount);
$table->addCell()->addText($total_unit_price_amount);
$templateProcessor->setComplexBlock('products_table', $table);
$documentPath = "{$folder}/hasaplashyk-{$payout->id}-{$user->companyName()}-baha-ylalashyk.docx";
$templateProcessor->saveAs(public_path($documentPath));
}
public function generateEvidenceForProductsDocument(User $user, Payout $payout, Collection $orderItems, string $folder): void
{
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor(resource_path('docs/order/evidence.docx'));
$templateProcessor->setValues([
'date' => date('Y'),
'o_date' => $payout->created_at->format('d.m.Y'),
'product_owner' => $user->companyName(),
]);
$table = new \PhpOffice\PhpWord\Element\Table([
'borderSize' => 2,
'borderColor' => 'black',
'width' => 5000,
'width' => 6000,
'unit' => TblWidth::TWIP,
]);
$table->addRow();
$table->addCell()->addText('No');
$table->addCell()->addText('Harydyň ady');
$table->addCell()->addText('Mukdary, san');
$table->addCell()->addText('Biriniň bahasy, manat');
$table->addCell()->addText('Jemi bahasy, manat');
$i = 0;
$total_price = 0;
foreach ($orderItems as $orderItem) {
$i++;
$table->addRow();
$table->addCell()->addText($i);
$table->addCell()->addText($orderItem->product_name);
$table->addCell()->addText($orderItem->quantity);
$table->addCell()->addText($orderItem->unit_cost_amount);
$table->addCell()->addText($orderItem->unit_cost_amount * $orderItem->quantity);
$total_price += $orderItem->unit_cost_amount * $orderItem->quantity;
}
$table->addRow();
$table->addCell()->addText('');
$table->addCell()->addText('Jemi');
$table->addCell()->addText($i);
$table->addCell()->addText();
$table->addCell()->addText($total_price);
$templateProcessor->setComplexBlock('products_table', $table);
$documentPath = "{$folder}/sargyt-{$payout->id}-{$user->companyName()}-delilnama.docx";
$templateProcessor->saveAs(public_path($documentPath));
}
/**
* Get the fields available on the action.
*/
public function fields(NovaRequest $request): array
{
return [];
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Actions;
use App\Models\Ecommerce\Product\Order\Order;
use App\Models\Ecommerce\Product\Order\Shipping\OrderShipping;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Collection;
use Laravel\Nova\Actions\Action;
use Laravel\Nova\Actions\ActionResponse;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Http\Requests\NovaRequest;
use PhpOffice\PhpWord\Element\Table;
use PhpOffice\PhpWord\SimpleType\TblWidth;
use PhpOffice\PhpWord\TemplateProcessor;
class ExportOrderInvoiceAction extends Action
{
use InteractsWithQueue, Queueable;
/**
* The displayable name of the action.
*
* @var string
*/
public function name(): string
{
return __('Invoice');
}
/**
* Perform the action on the given models.
*/
public function handle(ActionFields $fields, Collection $models): mixed
{
if ($models->count() !== 1) {
return Action::danger(
__('Please run this on only one user resource')
);
}
$order = $models->first();
$products = $order
->items()
->with(['channel', 'product'])
->get();
$documentPath = $this->generateDocument(
order: $order,
products: $products,
templatePath: resource_path('docs/order/invoice.docx'),
productsTotal: $products->sum('total')
);
return ActionResponse::download(
name: sprintf('sargyt-%s.docx', $order->id),
url: url($documentPath)
);
}
private function generateDocument(
Order $order,
Collection $products,
string $templatePath,
float $productsTotal
): string {
$templateProcessor = new TemplateProcessor($templatePath);
$templateProcessor->setValues([
'id' => $order->id,
'payment_type' => $order->formattedPaymentType(),
'created_at' => $order->created_at->format('H:i, d.m.Y'),
'order_shipping_method' => $order->formattedShippingMethod(),
'shipping_price' => $order->shipping_method === OrderShipping::SELF_PICKUP
? 'Özüm baryp aljak'
: (string) $order->shippingPrice().' TMT',
'order_name' => $order->customer_name,
'order_address' => $order->fullAddress(),
'order_phone' => $order->customer_phone,
'p_t' => $productsTotal,
't' => $productsTotal + $order->shippingPrice(),
'notes' => $order->notes,
'adt_tax' => $order->additional_tax ?: '0.0',
]);
$table = $this->generateTable($products);
$templateProcessor->setComplexBlock('products_table', $table);
$documentPath = public_path(
sprintf('app-docs/sargyt-%s.docx', $order->id)
);
$templateProcessor->saveAs($documentPath);
return sprintf('app-docs/sargyt-%s.docx', $order->id);
}
private function generateTable(Collection $products): Table
{
$table = new Table([
'borderSize' => 2,
'borderColor' => 'black',
'width' => 5000,
'unit' => TblWidth::PERCENT,
]);
$table->addRow();
$table->addCell()->addText('№');
$table->addCell()->addText('HARYDYŇ ADY');
$table->addCell()->addText('BAHASY');
$table->addCell()->addText('SATYJY');
$table->addCell()->addText('MUKDARY');
$table->addCell()->addText('ARZANLADYŞ');
$table->addCell()->addText('JEMI BAHASY');
$products->each(function ($product, $index) use ($table) {
$table->addRow();
$table->addCell()->addText($index + 1);
$table->addCell()->addText($product->product->name);
$table->addCell()->addText($product->unit_price_amount);
$table
->addCell()
->addText($product->channel?->name ?? 'Türkmenpoçta PAK');
$table->addCell()->addText($product->quantity);
$table
->addCell()
->addText(
$product->product->caluculateDiscountDifference(
$product->unit_price_amount
)
);
$table
->addCell()
->addText($product->quantity * $product->unit_price_amount);
});
return $table;
}
/**
* Get the fields available on the action.
*/
public function fields(NovaRequest $request): array
{
return [];
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Actions;
use App\Exports\Ecommerce\Product\Order\ExportOrderReport;
use App\Nova\Fields\FieldHelpers;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Laravel\Nova\Actions\Action;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Http\Requests\NovaRequest;
class ExportOrderReportAction extends Action
{
use InteractsWithQueue, Queueable;
/**
* Get the displayable name of the action.
*/
public function name(): string
{
return __('Export Order Report');
}
/**
* Perform the action on the given models.
*/
public function handle(ActionFields $fields, Collection $models): mixed
{
$start_date = Carbon::createFromFormat('Y-m-d', $fields->start_date)->format('d.m.Y');
$end_date = Carbon::createFromFormat('Y-m-d', $fields->end_date)->format('d.m.Y');
(new ExportOrderReport(
start_date: $fields->start_date,
end_date: $fields->end_date
))->store('order-reports.xlsx', 'order_reports');
return Action::download(
url: Storage::disk('order_reports')->url('order-reports.xlsx'),
name: sprintf('Hasabat %s - %s.xlsx', $start_date, $end_date)
);
}
/**
* Get the fields available on the action.
*
* @return array
*/
public function fields(NovaRequest $request)
{
return [
Date::make(__('Start date'), 'start_date')
->rules('required')
->displayUsing(FieldHelpers::tmDate()),
Date::make(__('End date'), 'end_date')
->rules('required')
->displayUsing(FieldHelpers::tmDate()),
];
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Concerns;
use App\Models\Ecommerce\Product\Order\Shipping\OrderShipping;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use App\Models\System\Settings\Location\Region;
use App\Nova\Fields\FieldHelpers;
use Laravel\Nova\Fields\Badge;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
trait HasFieldsForIndex
{
/**
* Get the fields displayed by the resource.
*/
public function fieldsForIndex(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Text::make(__('Customer'), 'customer_name')
->canSeeWhen('systemUser', $this)
->sortable(),
Text::make(__('Phone'), 'customer_phone')
->canSeeWhen('systemUser', $this)
->sortable(),
Select::make(__('Welaýat'), 'region')
->displayUsingLabels()
->options(Region::values())
->canSeeWhen('systemUser', $this)
->sortable(),
Select::make(__('Delivery time'), 'delivery_time')
->displayUsingLabels()
->searchable()
->options(OrderShipping::times())
->rules('required')
->sortable(),
Date::make(__('Delivery date'), 'delivery_at')
->displayUsing(FieldHelpers::tmDate())
->sortable(),
Text::make(__('Total'), fn () => $this->resource->fullPriceWithShipping().' TMT')
->canSeeWhen('systemUser', $this)
->sortable(),
Badge::make('Status')->map(OrderStatus::classes())->addTypes([
'primary' => 'dark:bg-gray-900 bg-gray-600 text-white',
])->labels(OrderStatus::values())->sortable(),
];
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Concerns;
use App\Models\Ecommerce\Product\Order\Payment\OrderPayment;
use App\Models\Ecommerce\Product\Order\Shipping\OrderShipping;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use App\Models\System\Settings\Location\Province;
use App\Models\System\Settings\Location\Region;
use App\Models\System\Settings\OS;
use App\Repositories\Ecommerce\Order\NovaOrderRepository;
use Illuminate\Support\Str;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Nurmuhammet\NovaInputmask\NovaInputmask;
class OrderFieldsForCreate
{
/**
* Order fields for create
*/
public static function make($resource, $request): array
{
return [
Hidden::make('number')->default(Str::random(30)),
Hidden::make('user_id')->default($request->user()->id),
Hidden::make('source_app')->default(OS::ADMIN),
ID::make(),
Text::make(__('Customer name'), 'customer_name')
->rules('nullable', 'string', 'max:255')
->canSeeWhen('systemUser', $resource)
->default('Walk in customer'),
NovaInputmask::make(__('Customer phone'), 'customer_phone')
->mask(phoneMaskFormat())
->storeRawValue()
->canSeeWhen('systemUser', $resource),
Select::make(__('Region'), 'region')
->displayUsingLabels()
->searchable()
->options(Region::values())
->default(Region::default())
->canSeeWhen('systemUser', $resource),
Select::make(__('Province'), 'province_id')
->displayUsingLabels()
->searchable()
->dependsOn(['region'], NovaOrderRepository::dependsOnWhere('region', Province::class)),
Text::make(__('Customer address'), 'customer_address')
->rules('nullable', 'string', 'max:255'),
Select::make(__('Payment type'), 'payment_type_id')
->displayUsingLabels()
->searchable()
->options(OrderPayment::values())
->default(OrderPayment::default()),
Select::make(__('Shipping method'), 'shipping_method')
->displayUsingLabels()
->searchable()
->options(OrderShipping::values())
->default(OrderShipping::default())
->sortable(),
Text::make(__('Shipping price'), 'shipping_price')
->rules('required', 'numeric')
->dependsOn('shipping_method', function ($field, $request, $formData) {
if ($formData->shipping_method) {
$field->setValue(OrderShipping::priceFor($formData->shipping_method));
}
}),
Date::make(__('Delivery at'), 'delivery_at')
->default(date('Y-m-d'))
->rules('required'),
Select::make(__('Delivery time'), 'delivery_time')
->displayUsingLabels()
->searchable()
->options(OrderShipping::times())
->default(OrderShipping::defaultTime()),
Text::make(__('Additional tax'), 'additional_tax')
->rules('nullable', 'numeric'),
Select::make(__('Status'), 'status')
->displayUsingLabels()
->searchable()
->options(OrderStatus::values())
->default(OrderStatus::default()),
Text::make(__('Notes'), 'notes')
->rules('nullable', 'string', 'max:255'),
];
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Concerns;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use App\Models\System\Settings\OS;
use App\Nova\Resources\Ecommerce\Product\Order\OrderItem;
use App\Nova\Resources\System\Payments\PaymentType;
use App\Nova\User;
use Laravel\Nova\Fields\Badge;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Nurmuhammet\NovaInputmask\NovaInputmask;
class OrderFieldsForDetail
{
/**
* Order fields for detail
*/
public static function make($resource, $request): array
{
// nova has a wierd bug, can load detail fields while on Index
if (! $request->isResourceDetailRequest()) {
if ($request->viaResource && $request->viaRelationship && $request->relationshipType) {
} else {
return [];
}
}
return [
ID::make()->sortable(),
Text::make(__('Customer name'), 'customer_name'),
NovaInputmask::make(__('Customer phone'), 'customer_phone')
->mask(phoneMaskFormat())
->storeRawValue(),
Text::make(
name: __('Address'),
attribute: fn ($model) => view('vendor.nova.resources.order.create-fields.address', [
'model' => $model,
])->render()
)
->asHtml(),
BelongsTo::make(__('Payment type'), 'paymentType', PaymentType::class),
Text::make(
name: __('Shipping'),
attribute: fn ($model) => view('vendor.nova.resources.order.create-fields.shipping', [
'model' => $model,
])->render()
)
->asHtml(),
Text::make(
name: __('Shipping time'),
attribute: fn ($model) => view('vendor.nova.resources.order.create-fields.shipping-time', [
'model' => $model,
])->render()
)
->asHtml(),
Select::make(__('App'), 'source_app')
->displayUsingLabels()
->options(OS::apps())
->sortable(),
Badge::make('Status')->map(OrderStatus::classes())->addTypes([
'primary' => 'dark:bg-gray-900 bg-gray-600 text-white',
])->labels(OrderStatus::values())->sortable(),
Text::make(__('Notes'), 'notes'),
Text::make(__('Additional tax'), fn () => $resource->additional_tax.' TMT')
->canSeeWhen('systemUser', $resource),
Text::make(__('Products total'), fn () => $resource->total().' TMT')
->canSeeWhen('systemUser', $resource),
Text::make(__('Total'), fn () => $resource->fullPriceWithShipping().' TMT')
->canSeeWhen('systemUser', $resource),
DateTime::make(__('Created at'), 'created_at')
->turkmenDate(),
BelongsTo::make(__('Filled by:'), 'user', User::class),
HasMany::make(__('Products'), 'items', OrderItem::class),
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Concerns;
use App\Models\Ecommerce\Product\Order\Shipping\OrderShipping;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use App\Models\System\Settings\Location\Region;
use App\Models\System\Settings\OS;
use App\Nova\Fields\FieldHelpers;
use Laravel\Nova\Fields\Badge;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Nurmuhammet\NovaInputmask\NovaInputmask;
class OrderFieldsForIndex
{
/**
* Fields for index
*/
public static function make($resource, $request): array
{
return [
ID::make()->sortable(),
Text::make(__('Customer'), 'customer_name')
->sortable(),
NovaInputmask::make(__('Customer phone'), 'customer_phone')
->mask(phoneMaskFormat())
->storeRawValue()
->sortable(),
Select::make(__('Welaýat'), 'region')
->displayUsingLabels()
->options(Region::values())
->sortable(),
Select::make(__('Delivery time'), 'delivery_time')
->displayUsingLabels()
->searchable()
->options(OrderShipping::times())
->rules('required')
->sortable(),
Date::make(__('Delivery date'), 'delivery_at')
->displayUsing(FieldHelpers::tmDate())
->sortable(),
Select::make(__('Shipping method'), 'shipping_method')
->displayUsingLabels()
->options(OrderShipping::values())
->default(OrderShipping::default())
->sortable(),
Select::make(__('Source'), 'source_app')
->displayUsingLabels()
->options(OS::apps())
->sortable(),
Text::make(__('Total'), fn () => $resource->fullPriceWithShipping().' TMT')
->canSeeWhen('systemUser', $resource)
->sortable(),
DateTime::make(__('Created at'), 'created_at')
->turkmenDate(),
Badge::make('Status')
->map(OrderStatus::classes())
->labels(OrderStatus::values())
->addTypes([
'primary' => 'dark:bg-gray-900 bg-gray-600 text-white',
])
->sortable(),
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Concerns;
use App\Models\Ecommerce\Product\Order\Payment\OrderPayment;
use App\Models\Ecommerce\Product\Order\Shipping\OrderShipping;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use App\Models\System\Settings\Location\Province;
use App\Models\System\Settings\Location\Region;
use App\Repositories\Ecommerce\Order\NovaOrderRepository;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Nurmuhammet\NovaInputmask\NovaInputmask;
class OrderFieldsForUpdate
{
/**
* Order
*/
public static function make($resource, $request): array
{
return [
ID::make()->sortable(),
Text::make(__('Customer name'), 'customer_name')
->rules('nullable', 'string', 'max:255')
->canSeeWhen('systemUser', $resource),
NovaInputmask::make(__('Customer phone'), 'customer_phone')
->mask(phoneMaskFormat())
->storeRawValue()
->canSeeWhen('systemUser', $resource),
Select::make(__('Region'), 'region')
->displayUsingLabels()
->searchable()
->options(Region::values())
->default(Region::default())
->canSeeWhen('systemUser', $resource),
Select::make(__('Province'), 'province_id')
->displayUsingLabels()
->searchable()
->dependsOn(['region'], NovaOrderRepository::dependsOnWhere('region', Province::class)),
Text::make(__('Customer address'), 'customer_address')
->rules('nullable', 'string', 'max:255'),
Select::make(__('Payment type'), 'payment_type_id')
->displayUsingLabels()
->searchable()
->options(OrderPayment::values())
->default(OrderPayment::default()),
Select::make(__('Shipping method'), 'shipping_method')
->displayUsingLabels()
->searchable()
->options(OrderShipping::values())
->default(OrderShipping::default())
->sortable(),
Text::make(__('Shipping price'), 'shipping_price')
->rules('required', 'numeric')
->dependsOn('shipping_method', function ($field, $request, $formData) use ($resource) {
if ($formData->shipping_price) {
return;
}
if ($formData->shipping_method) {
$field->setValue(OrderShipping::orderShippingPrice($resource));
}
}),
Text::make(__('Additional tax'), 'additional_tax')
->rules('nullable', 'numeric'),
Date::make(__('Delivery at'), 'delivery_at')
->default(date('Y-m-d'))
->rules('required'),
Select::make(__('Delivery time'), 'delivery_time')
->displayUsingLabels()
->searchable()
->options(OrderShipping::times())
->default(OrderShipping::defaultTime()),
Select::make(__('Status'), 'status')
->displayUsingLabels()
->searchable()
->options(OrderStatus::values())
->default(OrderStatus::default()),
Text::make(__('Notes'), 'notes')
->rules('nullable', 'string', 'max:255'),
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Metrics;
use App\Models\Ecommerce\Product\Order\Order;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Metrics\Value;
class NewOrders extends Value
{
/**
* Calculate the value of the metric.
*
* @return mixed
*/
public function calculate(NovaRequest $request)
{
if ($request->user()->hasRole('vendor')) {
$channel = $request->user()->channel();
$vendor_orders = DB::table('order_items')->where('channel_id', $channel->id)->pluck('order_id');
return $this->count($request, Order::whereIntegerInRaw('id', $vendor_orders));
}
return $this->count($request, Order::class);
}
/**
* Get the ranges available for the metric.
*/
public function ranges(): array
{
return [
'TODAY' => __('Today'),
30 => __('30 Days'),
60 => __('60 Days'),
365 => __('365 Days'),
'MTD' => __('Month To Date'),
'QTD' => __('Quarter To Date'),
'YTD' => __('Year To Date'),
];
}
/**
* Determine the amount of time the results of the metric should be cached.
*
* @return \DateTimeInterface|\DateInterval|float|int|null
*/
public function cacheFor()
{
return now()->addMinutes(5);
}
/**
* Get the URI key for the metric.
*/
public function uriKey(): string
{
return 'orders-new-orders';
}
/**
* Get the displayable name of the metric
*/
public function name(): string
{
return __('Orders');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Metrics;
use App\Models\Ecommerce\Product\Order\Order;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use DateTimeInterface;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Metrics\Partition;
class OrdersPerStatus extends Partition
{
/**
* Calculate the value of the metric.
*/
public function calculate(NovaRequest $request): mixed
{
$model = Order::class;
if ($request->user()->hasRole('vendor')) {
$channel = $request->user()->channel();
$vendor_orders = DB::table('order_items')->where('channel_id', $channel->id)->pluck('order_id');
$model = Order::whereIntegerInRaw('id', $vendor_orders);
}
return $this->count($request, $model, 'status')
->colors(OrderStatus::colors())
->label(fn ($value) => OrderStatus::formattedStatusFor($value));
}
/**
* Determine the amount of time the results of the metric should be cached.
*
* @return \DateTimeInterface|\DateInterval|float|int|null
*/
public function cacheFor(): DateTimeInterface|DateInterval|float|int|null
{
return now()->addMinutes(5);
}
/**
* Get the URI key for the metric.
*/
public function uriKey(): string
{
return 'orders-orders-per-status';
}
/**
* Get the displayable name of the metric
*/
public function name(): string
{
return __('Order Per Status');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\Metrics;
use App\Models\Ecommerce\Product\Order\OrderItem;
use DateTimeInterface;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Metrics\Trend;
class ProductSoldPerDay extends Trend
{
/**
* Calculate the value of the metric.
*/
public function calculate(NovaRequest $request): mixed
{
if ($request->user()->hasRole('vendor')) {
$channel = $request->user()->channel();
return $this->countByDays($request, OrderItem::where('channel_id', $channel->id))->showLatestValue();
}
return $this->countByDays($request, OrderItem::class)->showLatestValue();
}
/**
* Get the ranges available for the metric.
*/
public function ranges(): array
{
return [
30 => __('30 Days'),
60 => __('60 Days'),
90 => __('90 Days'),
];
}
/**
* Determine the amount of time the results of the metric should be cached.
*
* @return \DateTimeInterface|\DateInterval|float|int|null
*/
public function cacheFor(): DateTimeInterface|DateInterval|float|int|null
{
return now()->addMinutes(5);
}
/**
* Get the URI key for the metric.
*/
public function uriKey(): string
{
return 'orders-product-sold-per-day';
}
/**
* Get the displayable name of the metric
*/
public function name(): string
{
return __('Product Sold Per Day');
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order;
use App\Models\Ecommerce\Product\Order\Order as OrderModel;
use App\Models\Ecommerce\Product\Order\Status\OrderStatus;
use App\Nova\Filters\RegionFilter;
use App\Nova\Filters\StatusFilter;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Order\Actions\ExportOrderInvoiceAction;
use App\Nova\Resources\Ecommerce\Product\Order\Actions\ExportOrderReportAction;
use App\Nova\Resources\Ecommerce\Product\Order\Concerns\OrderFieldsForCreate;
use App\Nova\Resources\Ecommerce\Product\Order\Concerns\OrderFieldsForDetail;
use App\Nova\Resources\Ecommerce\Product\Order\Concerns\OrderFieldsForIndex;
use App\Nova\Resources\Ecommerce\Product\Order\Concerns\OrderFieldsForUpdate;
use App\Repositories\CMS\Icon\IconRepository;
use DigitalCreative\ColumnToggler\ColumnTogglerTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Http\Requests\NovaRequest;
class Order extends Resource
{
use ColumnTogglerTrait;
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = OrderModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id', 'customer_name', 'customer_phone',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Orders');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Order');
}
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['items'];
/**
* Indicates whether the resource should automatically poll for new resources.
*
* @var bool
*/
public static $polling = true;
/**
* The interval at which Nova should poll for new resources.
*
* @var int
*/
public static $pollingInterval = 120;
/**
* Indicates whether to show the polling toggle button inside Nova.
*
* @var bool
*/
public static $showPollingToggle = true;
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function indexQuery(NovaRequest $request, $query)
{
$user = $request->user();
if ($user->hasRole('vendor')) {
$order_ids = DB::table('order_items')->where('channel_id', $user->channel()->id)->distinct()->pluck('order_id');
$query->whereIntegerInRaw('id', $order_ids);
}
return $query;
}
/**
* After order was updated
*/
public static function afterUpdate(NovaRequest $request, Model $model): void
{
if ($model->status === OrderStatus::CANCELLED) {
$orderItems = $model->items()->with('product')->get(['id', 'product_id', 'order_id', 'quantity']);
$orderItems->each(function ($item) {
$item->product->update([
'stock' => intval($item->product->stock) + intval($item->quantity),
]);
});
}
}
/**
* Get the fields for index.
*/
public function fieldsForIndex(NovaRequest $request): array
{
return OrderFieldsForIndex::make($this, $request);
}
/**
* Get the fields for create.
*/
public function fieldsForCreate(NovaRequest $request): array
{
return OrderFieldsForCreate::make($this, $request);
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForDetail(NovaRequest $request): array
{
return OrderFieldsForDetail::make($this, $request);
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForUpdate(NovaRequest $request): array
{
return OrderFieldsForUpdate::make($this, $request);
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
HasMany::make(__('Products'), 'items', OrderItem::class),
];
}
/**
* Get the actions available for the resource.
*/
public function actions(NovaRequest $request): array
{
return [
ExportOrderReportAction::make()
->standalone()
->canSeeWhen('isAdmin', $this)
->onlyOnIndex()
->icon(IconRepository::make()->documentReport()),
ExportOrderInvoiceAction::make()
->confirmText(__('Are you sure you want to run this action?'))
->confirmButtonText(__('Yes'))
->cancelButtonText(__('No'))
->exceptOnIndex()
->icon(IconRepository::make()->documentDownload()),
];
}
/**
* Get the filters available for the resource.
*/
public function filters(NovaRequest $request): array
{
return [
RegionFilter::make(),
StatusFilter::make(),
];
}
/**
* Get the lenses available for the resource.
*/
public function lenses(NovaRequest $request): array
{
return [
];
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order;
use App\Models\Ecommerce\Product\Order\OrderItem as OrderItemModel;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Order\OrderItem\OrderItemFieldsForCreate;
use App\Nova\Resources\Ecommerce\Product\Order\OrderItem\OrderItemFieldsForIndex;
use App\Nova\Resources\Ecommerce\Product\Order\OrderItem\OrderItemFieldsForUpdate;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class OrderItem extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<OrderItemModel>
*/
public static $model = OrderItemModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id',
];
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
// public static $with = ['product', 'product.media'];
/**
* The number of resources to show per page via relationships.
*
* @var int
*/
public static $perPageViaRelationship = 20;
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*/
public static function indexQuery(NovaRequest $request, $query): Builder
{
$user = $request->user();
if ($user->hasRole('vendor')) {
$query->with(['product', 'product.media'])
->where('channel_id', $request->user()->channel()->id);
} else {
$query->with(['product', 'product.media', 'channel']);
}
return $query;
}
/**
* Get the fields for index.
*/
public function fieldsForIndex(NovaRequest $request): array
{
return OrderItemFieldsForIndex::make($this, $request);
}
/**
* Get the fields for create.
*/
public function fieldsForCreate(NovaRequest $request): array
{
return OrderItemFieldsForCreate::make($this, $request);
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForUpdate(NovaRequest $request): array
{
return OrderItemFieldsForUpdate::make($this, $request);
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterCreate(NovaRequest $request, $resource)
{
return sprintf('/resources/orders/%s', $resource->order_id);
}
/**
* Return the location to redirect the user after update.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterUpdate(NovaRequest $request, $resource)
{
return sprintf('/resources/orders/%s', $resource->order_id);
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Text::make(__('Quantity'), 'quantity'),
BelongsTo::make(__('Order'), 'order', Order::class),
];
}
/**
* Get the cards available for the request.
*
* @return array
*/
public function cards(NovaRequest $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*/
public function filters(NovaRequest $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @return array
*/
public function lenses(NovaRequest $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @return array
*/
public function actions(NovaRequest $request)
{
return [];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\OrderItem;
use App\Models\Ecommerce\Product\Product\Product as ProductModel;
use App\Nova\Resources\Ecommerce\Product\Order\Order;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Outl1ne\MultiselectField\Multiselect;
class OrderItemFieldsForCreate
{
/**
* Order item fields for create
*/
public static function make($resource, $request): array
{
return [
ID::make(),
Hidden::make('order_id')
->default($request->viaResource()),
Multiselect::make(__('Product'), 'product_id')
->asyncResource(Product::class)
->placeholder(__('Search by id, name, sku, barcode...'))
->singleSelect()
->rules('required'),
Number::make(__('Quantity'), 'quantity')
->rules('required', 'min:1')
->default(1),
Text::make(__('Price'), 'unit_price_amount')
->rules('required')
->dependsOn('product_id', function ($field, $request, $formData) {
if (! $formData->product_id || ! is_int($formData->product_id)) {
return;
}
$product = DB::table('products')->where('id', $formData->product_id)->first();
$field->setValue($product->price_amount);
})->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$product = ProductModel::find($request->product_id);
$model->unit_price_amount = $request->unit_price_amount;
$model->unit_cost_amount = $product->cost_amount;
$model->product_name = $product->name;
$model->channel_id = $product->channels()->first()?->id ?? tmpostChannel()->id;
}),
Number::make(__('Total'), 'total')
->dependsOn(['quantity', 'unit_price_amount'], function ($field, $request, $formData) {
if (! $formData->unit_price_amount || ! $formData->quantity) {
return;
}
$field->setValue($formData->unit_price_amount * $formData->quantity);
})->readonly(),
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\OrderItem;
use App\Repositories\Nova\NovaRepo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\URL;
class OrderItemFieldsForIndex
{
/**
* Order item's index fields
*/
public static function make($resource): array
{
return [
ID::make()->sortable()->hidden(),
Text::make(
name: __('Image'),
attribute: fn () => NovaRepo::rawImage(url: $resource->product?->thumbnail())
)->asHtml(),
Text::make(
name: __('Product name'),
attribute: fn () => $resource->product?->novaDetailPage(),
)->displayUsing(function () use ($resource) {
$product = $resource->product;
if (! $product) {
return;
}
return sprintf('
<a href="%s" target="_blank" class="link-default"> %s <span class="link-default">%s</span> <span class="link-default">%s</span></a>',
$product->novaDetailPage(),
$product->name,
$product->color,
$product->size,
);
})->asHtml(),
URL::make(
name: __('Channel'),
attribute: fn () => $resource->channel?->novaDetailPage(),
)
->displayUsing(fn () => $resource->channel?->name)
->canSeeWhen('isAdmin', $resource),
Text::make(__('Cost amount'), fn () => $resource->product?->cost_amount),
Text::make(__('Price'), 'unit_price_amount'),
Text::make(__('Quantity'), 'quantity'),
Text::make(__('Total'), fn () => intval($resource->quantity) * floatval($resource->unit_price_amount)),
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Order\OrderItem;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
class OrderItemFieldsForUpdate
{
/**
* Order item fields for update
*/
public static function make($resource, $request): array
{
return [
ID::make(),
Hidden::make('order_id'),
Text::make(__('Product name'), 'product_name')
->readonly(),
Number::make(__('Quantity'), 'quantity')
->rules('required', 'min:1'),
Text::make(__('Price'), 'unit_price_amount')
->rules('required'),
Number::make(__('Total'), 'total')
->dependsOn(['quantity', 'unit_price_amount'], function ($field, $request, $formData) {
if (! $formData->unit_price_amount || ! $formData->quantity) {
return;
}
$field->setValue($formData->unit_price_amount * $formData->quantity);
})->readonly(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns;
use Illuminate\Database\Eloquent\Model;
use Laravel\Nova\Http\Requests\NovaRequest;
trait HandlesProductResourceEvents
{
public static function afterCreate(NovaRequest $request, Model $model): void
{
ProductFieldsForCreate::afterCreate($request, $model);
}
public static function afterUpdate(NovaRequest $request, Model $model): void
{
ProductFieldsForUpdate::afterUpdate($request, $model);
}
}

View File

@@ -0,0 +1,220 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns;
use App\Models\Ecommerce\Product\Property\ProductAttributeValue;
use App\Models\Ecommerce\Product\Property\ProductProperty;
use App\Nova\Forms\NovaForm;
use App\Nova\Resources\Ecommerce\Product\Product\Product as ProductResource;
use App\Nova\Resources\Ecommerce\Product\Product\ProductVariant as ProductVariantResource;
use App\Repositories\Ecommerce\Channel\ChannelRepository;
use App\Repositories\Ecommerce\Collection\CollectionRepository;
use App\Repositories\Ecommerce\Product\Barcode\BarcodeRepository;
use App\Repositories\Ecommerce\Product\Brand\BrandRepository;
use App\Repositories\Ecommerce\Product\Category\CategoryRepository;
use App\Repositories\Ecommerce\Product\NovaProductRepository;
use App\Repositories\Ecommerce\Product\ProductRepository;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Panel;
use Nurmuhammet\DynamicFields\DynamicFields;
use Nurmuhammet\InlineRelationship\InlineRelationship;
use Nurmuhammet\ProductInventory\ProductInventory;
use Outl1ne\MultiselectField\Multiselect;
class ProductFieldsForCreate
{
/**
* Get the fields for create.
*/
public static function make(ProductResource $resource, NovaRequest $request): array
{
return [
ID::make()->sortable(),
new Panel(__('Product relations'), [
Hidden::make('is_visible')
->default(function () {
if (auth()->user()->isEntrepreneur()) {
return false;
}
return true;
}),
Select::make(__('Channel'), 'channel')
->fullWidth()
->displayUsingLabels()
->searchable()
->options(ChannelRepository::values())
->default(tmpostChannel()->id)
->rules('required')
->fillUsing(NovaForm::fillEmpty())
->canSeeWhen('isAdmin', $resource),
Select::make(__('Brand'), 'brand_id')
->fullWidth()
->displayUsingLabels()
->searchable()
->options(BrandRepository::values())
->rules('nullable'),
Multiselect::make(__('Category'), 'categories')
->fullWidth()
->options(CategoryRepository::namesWithTaxes())
->help(__('Biggest tax is chosen from categories'))
->rules('required')
->fillUsing(NovaForm::fillEmpty()),
Multiselect::make(__('Collection'), 'collections')
->fullWidth()
->options(CollectionRepository::values())
->rules('nullable')
->fillUsing(NovaForm::fillEmpty()),
]),
new Panel(__('General information'), [
Text::make(__('Ady'), 'name')
->fullWidth()
->rules('required', 'string', 'max:255'),
Trix::make(__('Description'), 'description')
->fullWidth()
->withFiles('public')
->rules('nullable', 'string'),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200')
->rules('required')
->setFileName(NovaForm::fillMediaFileName())
->required(),
]),
new Panel(__('Prices'), [
Text::make(__('Price'), 'cost_amount')
->fullWidth()
->rules('required', 'numeric'),
Text::make(__('Price without discount'), 'old_price_amount')
->fullWidth()
->rules('nullable', 'numeric', 'gt:readonly_price_amount'),
Text::make(__('Purchase price'), 'readonly_price_amount')
->fullWidth()
->readonly()
->dependsOn(['cost_amount', 'categories'], NovaProductRepository::priceAmountDependsOn()),
Hidden::make('price_amount')
->fillUsing(NovaForm::fillAttribute('price_amount', 'readonly_price_amount')),
]),
new Panel(__('Inventory'), [
Text::make('SKU', 'sku')
->fullWidth()
->rules('nullable', 'string', 'max:255'),
Boolean::make(__('Generate new barcode'), 'generate_new_barcode')
->onlyOnForms()
->fillUsing(NovaForm::fillEmpty()),
Text::make(__('Barcode'), 'barcode')
->fullWidth()
->rules('nullable', 'string', 'max:255', 'unique:products,barcode')
->dependsOn(
attributes: 'generate_new_barcode',
mixin: fn ($field, $request, $formData) => boolval($formData->generate_new_barcode)
? $field->setValue(BarcodeRepository::generateBarcode())
: null
),
ProductInventory::make(__('Inventory'), 'inventories')
->fullWidth()
->rules('required')
->dependsOn('channel', NovaProductRepository::inventoryDependsOn('channel'))
->fillUsing(NovaForm::fillEmpty()),
Hidden::make('stock')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = array_sum(array_values($request->input('inventories')));
}),
Hidden::make('options')->default(json_encode(ProductRepository::shippingAttributes())),
]),
new Panel(__('Attributes'), [
DynamicFields::make(__('Attributes'), 'properties')
->fullWidth()
->fillWithArrayName('properties')
->dependsOn(['categories'], NovaProductRepository::createRequestAttributesDependsOn($resource))
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$properties = $request->input('properties') ?? [];
foreach ($properties as $key => $value) {
try {
if (in_array($key, ['size', 'colour'])) {
$model->{$key} = $value;
}
} catch (Exception) {
}
}
}),
]),
new Panel(__('Variations'), [
// InlineRelationship::make(__('Variations'), 'variations', ProductVariantResource::class)
// ->hide()
// ->dependsOn(['categories'], NovaProductRepository::createRequestVariationsDependsOn()),
]),
];
}
/**
* Register a callback to be called after the resource is created.
*/
public static function afterCreate(NovaRequest $request, Model $model): void
{
$model::withoutEvents(function () use ($request, $model) {
$user = $request->user();
$model->categories()->attach($request->input('categories'));
$model->collections()->attach($request->input('collections'));
if ($user->isEntrepreneur()) {
$model->channels()->attach($user->channel()->id);
} else {
$model->channels()->attach($request->input('channel'));
}
$request->collect('inventories')->each(
fn ($stockValue, $inventoryID) => $model->inventories()->attach($inventoryID, ['stock' => $stockValue])
);
$properties = $request->input('properties');
$attribute_ids = $properties
? DB::table('attributes')->whereIn('slug', array_keys($properties))->get(['id', 'type', 'slug'])
: collect();
$attribute_ids->each(function ($attribute) use ($model, $properties) {
$productAttribute = ProductProperty::create([
'product_id' => $model->id,
'attribute_id' => $attribute->id,
]);
ProductAttributeValue::create([
'product_attribute_id' => $productAttribute->id,
'attribute_value_id' => in_array($attribute->type, ['text', 'number']) ? null : $properties[$attribute->slug],
'product_custom_value' => in_array($attribute->type, ['text', 'number']) ? $properties[$attribute->slug] : null,
]);
});
});
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns;
use App\Nova\Resources\Ecommerce\Channel\Channel;
use App\Nova\Resources\Ecommerce\Product\Brand\Brand;
use App\Nova\Resources\Ecommerce\Product\Category\Category;
use App\Nova\Resources\Ecommerce\Product\Collection\Collection;
use App\Nova\Resources\Ecommerce\Product\Product\Product as ProductResource;
use App\Nova\Resources\Ecommerce\Product\Product\ProductVariant;
use App\Nova\Resources\Ecommerce\Product\Review\Review;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Illuminate\Support\Str;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Http\Requests\NovaRequest;
use Milon\Barcode\Facades\DNS1DFacade;
use Nurmuhammet\DynamicFields\DynamicFields;
use Nurmuhammet\ProductInventory\ProductInventory;
use Outl1ne\MultiselectField\Multiselect;
use ShuvroRoy\NovaTabs\Tab;
use ShuvroRoy\NovaTabs\Tabs;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class ProductFieldsForDetail
{
/**
* Get the fields displayed by the resource on detail page.
*/
public static function make(ProductResource $resource, NovaRequest $request): array
{
// nova has a wierd bug, can load detail fields while on Index
if (! $request->isResourceDetailRequest()) {
if ($request->viaResource && $request->viaRelationship && $request->relationshipType) {
} else {
return [];
}
if ($request->viaResource === 'inventory-history-resources') {
return [];
}
}
return [
ID::make(),
Text::make(__('Name'), 'name'),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200'),
Trix::make(__('Description'), 'description')->withFiles('public')->alwaysShow(),
Text::make(__('Price'), 'cost_amount'),
Text::make(__('Price For Sale'), 'price_amount'),
Text::make(__('Price without discount'), 'old_price_amount'),
NovaSwitcher::make(__('Active'), 'is_visible')->canSeeWhen('isAdmin', $resource),
Tabs::make('Main', [
Tab::make(__('Product relations'), [
BelongsTo::make(__('Brand'), 'brand', Brand::class),
Multiselect::make(__('Channel'), 'channels')
->belongsToMany(Channel::class)
->canSeeWhen('isAdmin', $resource),
Multiselect::make(__('Category'), 'categories')->belongsToMany(Category::class),
Multiselect::make(__('Collection'), 'collections')->belongsToMany(Collection::class),
]),
Tab::make(__('Inventory'), [
ProductInventory::make(
name: __('Inventory'),
attribute: fn () => $resource->inventories()
->get(['inventories.name', 'inventories.id'])
->map(fn ($inventory) => [
'name' => Str::title($inventory->name),
'value' => $inventory->pivot?->stock,
])->toArray()
)->fullWidth(),
Text::make('SKU', 'sku'),
Text::make(__('Barcode'), function () use ($resource) {
if (! is_numeric($resource->barcode)) {
return;
}
return view('vendor.nova.products.barcode', [
'barcode' => $resource->barcode,
'image' => DNS1DFacade::getBarcodeHTML(
code: $resource->barcode,
type: config('barcode.default_type'),
),
])->render();
})->asHtml()->showWhen(boolval($resource->barcode)),
Text::make(__('Stock'), 'stock'),
Text::make(__('Security stock'), 'security_stock'),
]),
Tab::make(__('Shipping'), [
Boolean::make(__('Back order'), 'back_order'),
Number::make(__('Weight value'), 'weight_value'),
Select::make(__('Weight unit'), 'weight_unit'),
Number::make(__('Height value'), 'height_value'),
Select::make(__('Height unit'), 'height_unit'),
Number::make(__('Width value'), 'width_value'),
Select::make(__('Width unit'), 'width_unit'),
Number::make(__('Depth value'), 'depth_value'),
Select::make(__('Depth unit'), 'depth_unit'),
Number::make(__('Volume value'), 'volume_value'),
Select::make(__('Volume unit'), 'volume_unit'),
]),
Tab::make(__('SEO'), [
Text::make(__('Seo title'), 'seo_title'),
Text::make(__('Seo description'), 'seo_description'),
]),
]),
Tabs::make(__('Properties'), [
DynamicFields::make('Attributes', 'attributes')
->fields(function () use ($resource) {
$attributes = $resource->properties()->with(['attribute.values', 'values.value'])->get()->map(fn ($property) => [
'label' => $property->attribute->name,
'name' => $property->attribute->slug,
'type' => $property->attribute->type,
'default' => $property->attribute->type === 'select'
? $property->values->first()->value?->id
: $property->values->first()->product_custom_value,
'options' => $property->attribute->values->map(fn ($value) => [
'label' => $value->value,
'value' => $value->id,
])->toArray(),
]);
return $attributes->toArray();
}),
]),
HasMany::make(__('Variations'), 'variations', ProductVariant::class),
HasMany::make(__('Reviews'), 'reviews', Review::class),
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns;
use App\Nova\Fields\FieldHelpers;
use App\Nova\Resources\Ecommerce\Product\Brand\Brand;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class ProductFieldsForIndex
{
/**
* Get the fields for index.
*/
public static function make(NovaRequest $request, $resource): array
{
if ($request->filled('search') && is_null($request->filters)) {
return [];
}
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')->conversionOnIndexView('thumb200x200'),
Text::make(__('Name'), 'name')->sortable(),
BelongsTo::make(__('Brand'), 'brand', Brand::class)
->sortable()
->searchable()
->filterable(),
Number::make(__('Price'), 'cost_amount')
->sortable()
->filterable(),
Text::make(__('SKU'), 'sku')->sortable(),
Text::make(__('Count'), FieldHelpers::formatQuantity())->asHtml(),
// For filter
Number::make(__('Count'), 'stock')
->hideFromIndex()
->filterable(),
Boolean::make(__('Active'), 'is_visible')
->sortable()
->canSeeWhen('isVendor', $resource),
NovaSwitcher::make(__('Is visible'), 'is_visible')
->sortable()
->canSeeWhen('isAdmin', $resource),
];
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns;
use App\Jobs\Ecommerce\Product\UpdateProductRelations;
use App\Nova\Forms\NovaForm;
use App\Nova\Resources\Ecommerce\Product\Product\Product as ProductResource;
use App\Repositories\Ecommerce\Channel\ChannelRepository;
use App\Repositories\Ecommerce\Collection\CollectionRepository;
use App\Repositories\Ecommerce\Product\Barcode\BarcodeRepository;
use App\Repositories\Ecommerce\Product\Brand\BrandRepository;
use App\Repositories\Ecommerce\Product\Category\CategoryRepository;
use App\Repositories\Ecommerce\Product\NovaProductRepository;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Http\Requests\NovaRequest;
use Nurmuhammet\DynamicFields\DynamicFields;
use Nurmuhammet\ProductInventory\ProductInventory;
use Outl1ne\MultiselectField\Multiselect;
use ShuvroRoy\NovaTabs\Tab;
use ShuvroRoy\NovaTabs\Tabs;
class ProductFieldsForUpdate
{
/**
* Get the fields displayed by the resource on detail page.
*/
public static function make(ProductResource $resource, NovaRequest $request): array
{
return [
ID::make(),
Text::make(__('Name'), 'name')
->rules('required', 'string', 'max:255'),
Trix::make(__('Description'), 'description')
->withFiles('public')
->rules('nullable', 'string'),
Images::make(__('Image'), 'uploads')
->required()
->rules('required')
->showStatistics()
->setFileName(fn ($originalFilename, $extension, $model) => sprintf('%s.%s', Str::random(70), $extension)),
Text::make(__('Price'), 'cost_amount')
->rules('required', 'numeric'),
Text::make(__('Purchase price'), 'readonly_price_amount')
->readonly()
->dependsOn(['cost_amount', 'categories'], NovaProductRepository::priceAmountDependsOn()),
Hidden::make('price_amount')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = $request->input('readonly_price_amount');
}),
Text::make(__('Price without discount'), 'old_price_amount')
->rules('nullable', 'gt:readonly_price_amount'),
Boolean::make(__('Active'), 'is_visible')
->canSeeWhen('isAdmin', $resource),
Tabs::make('Main', [
Tab::make(__('Product relations'), [
Select::make(__('Brand'), 'brand_id')
->displayUsingLabels()
->searchable()
->options(BrandRepository::values())
->rules('nullable'),
Select::make(__('Channel'), 'channel')
->displayUsingLabels()
->searchable()
->options(ChannelRepository::values())
->rules('required')
->resolveUsing(fn ($value, $resource, $attribute) => $resource->owner()?->id)
->fillUsing(NovaForm::fillEmpty())
->canSeeWhen('isAdmin', $resource),
Multiselect::make(__('Category'), 'categories')
->options(CategoryRepository::namesWithTaxes())
->help(__('Biggest tax is chosen from categories'))
->rules('required')
->resolveUsing(fn ($value, $resource, $attribute) => $value->pluck('id'))
->fillUsing(NovaForm::fillEmpty()),
Multiselect::make(__('Collection'), 'collections')
->options(CollectionRepository::values())
->rules('nullable')
->resolveUsing(fn ($value, $resource, $attribute) => $value->pluck('id'))
->fillUsing(NovaForm::fillEmpty()),
]),
Tab::make(__('Inventory'), [
ProductInventory::make(__('Inventory'), 'inventories')
->rules('required')
->dependsOn('channel', NovaProductRepository::inventoryDependsOn('channel'))
->fillUsing(NovaForm::fillEmpty()),
Text::make('SKU', 'sku')
->rules('nullable', 'string', 'max:255'),
Boolean::make(__('Generate new barcode'), 'generate_new_barcode')
->onlyOnForms()
->fillUsing(NovaForm::fillEmpty()),
Text::make(__('Barcode'), 'barcode')
->rules('nullable', 'string', 'max:255', 'unique:products,barcode,{{resourceId}}')
->dependsOn(
attributes: 'generate_new_barcode',
mixin: function ($field, $request, $formData) use ($resource) {
if (boolval($formData->generate_new_barcode)) {
$field->setValue(BarcodeRepository::generateBarcode());
return;
}
$field->setValue($resource->barcode);
}
),
Number::make(__('Security stock'), 'security_stock')
->rules('nullable', 'integer'),
Hidden::make('stock')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = array_sum(array_values($request->input('inventories')));
}),
]),
Tab::make(__('Attributes'), [
DynamicFields::make('Attributes', 'workingpn')
->fillWithArrayName('properties')
->dependsOn(['categories'], NovaProductRepository::updateRequestAttributesDependsOn($resource))
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$properties = $request->input('properties');
if (! $properties) {
return;
}
foreach ($properties as $key => $value) {
try {
if (in_array($key, ['size', 'colour'])) {
$model->{$key} = $value;
}
} catch (Exception) {
}
}
}),
]),
Tab::make(__('Shipping'), [
Boolean::make(__('Back order'), 'back_order')
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Weight value'), 'weight_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Weight unit'), 'weight_unit')
->options(['kg' => 'kg'])
->default('kg')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Height value'), 'height_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Height unit'), 'height_unit')
->options(['cm' => 'cm'])
->default('cm')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Width value'), 'width_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Width unit'), 'width_unit')
->options(['cm' => 'cm'])
->default('cm')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Depth value'), 'depth_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Depth unit'), 'depth_unit')
->options(['cm' => 'cm'])
->default('cm')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Volume value'), 'volume_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Volume unit'), 'volume_unit')
->options(['kg' => 'kg'])
->default('kg')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
]),
Tab::make(__('SEO'), [
Text::make(__('Seo title'), 'seo_title'),
Text::make(__('Seo description'), 'seo_description'),
]),
]),
];
}
/**
* Register a callback to be called after the resource is updated.
*/
public static function afterUpdate(NovaRequest $request, Model $model): void
{
UpdateProductRelations::dispatch(
user: $request->user(),
model: $model,
categories: $request->input('categories'),
collections: $request->input('collections'),
channel: $request->input('channel'),
properties: $request->input('properties'),
inventories: $request->collect('inventories')
->mapWithKeys(fn ($stockValue, $inventoryID) => [$inventoryID => ['stock' => $stockValue]])
->toArray()
);
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant;
use App\Models\Ecommerce\Product\Inventory\Inventory;
use App\Models\Ecommerce\Product\Product\Product as ProductModel;
use App\Models\Ecommerce\Product\Property\ProductAttributeValue;
use App\Models\Ecommerce\Product\Property\ProductProperty;
use App\Nova\Forms\NovaForm;
use App\Repositories\Ecommerce\Product\NovaProductRepository;
use App\Repositories\Ecommerce\Product\ProductRepository;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\Heading;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Nurmuhammet\DynamicFields\DynamicFields;
use Nurmuhammet\ProductInventory\ProductInventory;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class VariantFieldsForCreate
{
/**
* Get the fields for create.
*/
public static function make(NovaRequest $request): array
{
if ($request->viaRelationship()) {
$parent = ProductModel::find($request->viaResourceId);
$categories = $parent->categories;
} else {
$parent = null;
$categories = session('product_categories') ?? [];
}
return [
ID::make()->sortable(),
ProductInventory::make(__('Inventory'), 'inventories')
->rules('required')
->options(Inventory::pluck('name', 'id'))
->fillUsing(NovaForm::fillEmpty()),
DynamicFields::make('Attributes', 'properties')
->fillWithArrayName('properties')
->fields(fn () => NovaProductRepository::attributeFields($categories))
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$properties = $request->input('properties');
foreach ($properties as $key => $value) {
try {
if (in_array($key, ['size', 'colour'])) {
$model->{$key} = $value;
}
} catch (Exception) {
}
}
}),
Heading::make(__('Leave empty to use original image')),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb150x150')
->showStatistics()
->help(__('Leave empty to use original image')),
Text::make(__('Price'), 'cost_amount')
->default(optional($parent)->cost_amount)
->rules('nullable', 'numeric'),
Text::make(__('Price without discount'), 'old_price_amount')
->default(optional($parent)->old_price_amount)
->rules('nullable', 'numeric'),
Text::make(__('Purchase price'), 'readonly_price_amount')
->readonly()
->dependsOn(['cost_amount'], NovaProductRepository::priceAmountDependsOn($categories)),
Hidden::make('price_amount')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = $request->input('readonly_price_amount');
}),
Text::make('SKU', 'sku')
->rules('nullable', 'string', 'max:255'),
Number::make(__('Security stock'), 'security_stock')
->rules('nullable', 'integer'),
Hidden::make('name')
->default($parent->name ?? 'inlineCreated'),
Hidden::make('options')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
if ($request->name === 'inlineCreated') {
$model->options->set('inline_created', true);
} else {
json_encode(ProductRepository::shippingAttributes());
}
}),
Hidden::make('stock')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = array_sum(array_values($request->input('inventories')));
}),
];
}
/**
* After resource has been created
*/
public static function afterCreate(NovaRequest $request, Model $model): void
{
$model::withoutEvents(function () use ($request, $model) {
if ($request->viaRelationship() && $request->missing('__media__')) {
$parent = ProductModel::find($request->viaResourceId);
$parent->media->each(function (Media $media) use ($model) {
$model->addMedia($media->getPath())
->preservingOriginal()
->toMediaCollection($media->collection_name);
});
}
$request->collect('inventories')->each(
fn ($stockValue, $inventoryID) => $model->inventories()->attach($inventoryID, ['stock' => $stockValue])
);
$properties = $request->input('properties');
$attribute_ids = $properties
? DB::table('attributes')->whereIn('slug', array_keys($properties))->get(['id', 'type', 'slug'])
: collect();
$attribute_ids->each(function ($attribute) use ($model, $properties) {
$productAttribute = ProductProperty::create([
'product_id' => $model->id,
'attribute_id' => $attribute->id,
]);
ProductAttributeValue::create([
'product_attribute_id' => $productAttribute->id,
'attribute_value_id' => in_array($attribute->type, ['text', 'number']) ? null : $properties[$attribute->slug],
'product_custom_value' => in_array($attribute->type, ['text', 'number']) ? $properties[$attribute->slug] : null,
]);
});
});
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Nurmuhammet\DynamicFields\DynamicFields;
class VariantFieldsForDetail
{
/**
* Get the fields for create.
*/
public static function make(NovaRequest $request, $resource): array
{
if (! $request->isResourceDetailRequest()) {
if ($request->viaResource && $request->viaRelationship && $request->relationshipType) {
} else {
return [];
}
}
return [
ID::make()->sortable(),
BelongsTo::make(__('Parent'), 'parent', Product::class),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200'),
Text::make(__('Price'), 'cost_amount')
->rules('required', 'numeric'),
Text::make('SKU', 'sku')
->rules('nullable', 'string', 'max:255'),
Number::make(__('Stock'), 'stock')
->rules('required', 'integer'),
DynamicFields::make('Attributes', 'attributes')
->fields(function () use ($resource) {
$attributes = $resource->properties()->with(['attribute.values', 'values.value'])->get()->map(fn ($property) => [
'label' => $property->attribute->name,
'name' => $property->attribute->slug,
'type' => $property->attribute->type,
'default' => $property->attribute->type === 'select'
? $property->values->first()->value?->id
: $property->values->first()->product_custom_value,
'options' => $property->attribute->values->map(fn ($value) => [
'label' => $value->value,
'value' => $value->id,
])->toArray(),
]);
return $attributes->toArray();
}),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant;
use App\Repositories\Ecommerce\Product\Property\PropertyRepository;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
class VariantFieldsForIndex
{
/**
* Get the fields for create.
*/
public static function make(NovaRequest $request, $resource): array
{
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')
->conversionOnIndexView('thumb200x200'),
Text::make(__('Price'), 'cost_amount')
->rules('required', 'numeric'),
Text::make(__('Color'), fn () => PropertyRepository::getRealValue('colour', $resource->colour)),
Text::make(__('Size'), fn () => PropertyRepository::getRealValue('size', $resource->size)),
Number::make(__('Stock'), 'stock')
->rules('required', 'integer'),
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant;
use Laravel\Nova\Http\Requests\NovaRequest;
use Nurmuhammet\DynamicFields\DynamicFields;
class VariantFieldsForPreview
{
/**
* Fields For Preview
*/
public static function make(NovaRequest $request, $resource): array
{
if (! $request->isResourcePreviewRequest()) {
return [
DynamicFields::make('Attributes', 'attributes')
->fields([]),
];
}
return [
DynamicFields::make('Attributes', 'attributes')
->fields(function () use ($resource) {
$attributes = $resource->properties()->with(['attribute.values', 'values.value'])->get()->map(fn ($property) => [
'label' => $property->attribute->name,
'name' => $property->attribute->slug,
'type' => $property->attribute->type,
'default' => $property->attribute->type === 'select'
? $property->values->first()->value?->id
: $property->values->first()->product_custom_value,
'options' => $property->attribute->values->map(fn ($value) => [
'label' => $value->value,
'value' => $value->id,
])->toArray(),
]);
return $attributes->toArray();
}),
];
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant;
use App\Jobs\Ecommerce\Product\UpdateProductRelations;
use App\Models\Ecommerce\Product\Inventory\Inventory;
use App\Nova\Forms\NovaForm;
use App\Repositories\Ecommerce\Product\NovaProductRepository;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Hidden;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Panel;
use Nurmuhammet\DynamicFields\DynamicFields;
use Nurmuhammet\ProductInventory\ProductInventory;
class VariantFieldsForUpdate
{
/**
* Get the fields for create.
*/
public static function make(NovaRequest $request, $resource): array
{
$categories = $resource->parent->categories;
return [
ID::make()->sortable(),
Images::make(__('Image'), 'uploads')
->showStatistics()
->setFileName(fn ($originalFilename, $extension, $model) => sprintf('%s.%s', md5($originalFilename), $extension)),
Text::make(__('Price'), 'cost_amount')
->rules('required', 'numeric'),
Text::make(__('Price without discount'), 'old_price_amount')
->rules('nullable', 'numeric'),
Text::make(__('Purchase price'), 'readonly_price_amount')
->readonly()
->dependsOn(['cost_amount'], NovaProductRepository::priceAmountDependsOn($categories)),
Hidden::make('price_amount')
->fillUsing(NovaForm::fillAttribute('price_amount', 'readonly_price_amount')),
new Panel(__('Inventory'), [
Text::make('SKU', 'sku')
->rules('nullable', 'string', 'max:255'),
Text::make(__('Barcode'), 'barcode')
->rules('nullable', 'string', 'max:255'),
ProductInventory::make(__('Inventory'), 'inventories')
->rules('required')
->options(Inventory::pluck('name', 'id'))
->fillUsing(NovaForm::fillEmpty()),
Hidden::make('stock')
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$model->{$attribute} = array_sum(array_values($request->input('inventories')));
}),
Number::make(__('Security stock'), 'security_stock')
->rules('nullable', 'integer'),
]),
new Panel(__('Attributes'), [
DynamicFields::make('Attributes', 'workingpn')
->fillWithArrayName('properties')
->fields(fn () => NovaProductRepository::attributeFieldsWithValues($categories, $resource))
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
$properties = $request->input('properties');
foreach ($properties as $key => $value) {
try {
if (in_array($key, ['size', 'colour'])) {
$model->{$key} = $value;
}
} catch (Exception) {
}
}
}),
]),
new Panel(__('Shipping'), [
Boolean::make(__('Back order'), 'back_order')
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Weight value'), 'weight_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Weight unit'), 'weight_unit')
->options(['kg' => 'kg'])
->default('kg')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Height value'), 'height_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Height unit'), 'height_unit')
->options(['cm' => 'cm'])
->default('cm')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Width value'), 'width_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Width unit'), 'width_unit')
->options(['cm' => 'cm'])
->default('cm')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Depth value'), 'depth_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Depth unit'), 'depth_unit')
->options(['cm' => 'cm'])
->default('cm')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
Number::make(__('Volume value'), 'volume_value')
->fillUsing(NovaForm::fillSchemalessField()),
Select::make(__('Volume unit'), 'volume_unit')
->options(['kg' => 'kg'])
->default('kg')
->searchable()
->fillUsing(NovaForm::fillSchemalessField()),
]),
];
}
/**
* Register a callback to be called after the resource is updated.
*/
public static function afterUpdate(NovaRequest $request, Model $model): void
{
UpdateProductRelations::dispatch(
model: $model,
properties: $request->input('properties'),
inventories: $request->collect('inventories')
->mapWithKeys(fn ($stockValue, $inventoryID) => [$inventoryID => ['stock' => $stockValue]])
->toArray()
);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product\Filters;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Filters\Filter;
use Laravel\Nova\Http\Requests\NovaRequest;
class ProductEntrepreneurFilter extends Filter
{
/**
* The filter's component.
*
* @var string
*/
public $component = 'select-filter';
/**
* Get the displayable name of the filter.
*/
public function name(): string
{
return __('Entrepreneur');
}
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(NovaRequest $request, $query, $value)
{
$vendorProducts = DB::table('product_has_relations')
->where('productable_type', 'channel')
->where('productable_id', $value)
->pluck('product_id');
$query->whereIntegerInRaw('id', $vendorProducts);
return $query;
}
/**
* Get the filter's available options.
*
* @return array
*/
public function options(NovaRequest $request)
{
if (! Cache::has('channels')) {
Cache::remember('channels', 60 * 5, fn () => DB::table('channels')->pluck('id', 'name'));
}
return Cache::get('channels');
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product;
use App\Models\Ecommerce\Product\Product\Product as ProductModel;
use App\Models\User;
use App\Nova\Actions\Ecommerce\Product\ProductImportAction;
use App\Nova\Filters\ResourceLimitFilter;
use App\Nova\Filters\VisableFilter;
use App\Nova\Lenses\MostSoldProducts;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\HandlesProductResourceEvents;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\ProductFieldsForCreate;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\ProductFieldsForDetail;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\ProductFieldsForIndex;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\ProductFieldsForUpdate;
use App\Nova\Resources\Ecommerce\Product\Product\Filters\ProductEntrepreneurFilter;
use App\Repositories\CMS\Icon\IconRepository;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use ShuvroRoy\NovaTabs\Traits\HasTabs;
class Product extends Resource
{
/**
* Handles product resource events
*/
use HandlesProductResourceEvents;
/**
* Supports tabs
*/
use HasTabs;
/**
* The model the resource corresponds to.
*
* @var class-string<ProductModel>
*/
public static $model = ProductModel::class;
/**
* The number of resources to show per page via relationships.
*
* @var int
*/
public static $perPageViaRelationship = 5;
/**
* The pagination per-page options configured for this resource.
*/
public static function perPageOptions(): array
{
return [50, 100, 150];
}
/**
* Get the value that should be displayed to represent the resource.
*/
public function title(): string
{
if (request()->filled('search') && is_null(request('filters'))) {
return sprintf('%s (%s)', $this->name, $this->barcode);
}
return $this->name;
}
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
// public static $with = ['brand', 'media'];
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id', 'name', 'sku', 'barcode',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Product');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Products');
}
/**
* Build an "index" query for the given resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*/
public static function indexQuery(NovaRequest $request, $query): Builder
{
if ($request->filled('search') && is_null($request->filters)) {
} else {
$query->with(['brand', 'media']);
}
if ($request->viaResource() != Product::class) {
$query->where('parent_id', null);
// ->whereNot('name', 'inlineCreated');
}
$user = $request->user();
if ($user->hasRole('vendor')) {
$vendorProducts = DB::table('product_has_relations')
->where('productable_type', 'channel')
->where('productable_id', $user->channel()->id)
->pluck('product_id');
$query->whereIntegerInRaw('id', $vendorProducts);
}
return $query;
}
/**
* Get the fields for index.
*/
public function fieldsForIndex(NovaRequest $request): array
{
return ProductFieldsForIndex::make($request, $this);
}
/**
* Get the fields for create.
*/
public function fieldsForCreate(NovaRequest $request): array
{
return ProductFieldsForCreate::make($this, $request);
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForDetail(NovaRequest $request): array
{
return ProductFieldsForDetail::make($this, $request);
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForUpdate(NovaRequest $request): array
{
return ProductFieldsForUpdate::make($this, $request);
}
/**
* Get the fields.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Text::make(__('Ady'), 'name'),
HasMany::make(__('Variations'), 'variations', ProductVariant::class),
];
}
/**
* Get the filters available for the resource.
*/
public function filters(NovaRequest $request): array
{
return [
ProductEntrepreneurFilter::make(),
VisableFilter::make(),
// ResourceLimitFilter::make($this),
];
}
/**
* Get the lenses available for the resource.
*/
public function lenses(NovaRequest $request): array
{
return [
MostSoldProducts::make()
->canSeeWhen('isAdmin', User::class),
];
}
/**
* Get the actions available for the resource.
*/
public function actions(NovaRequest $request): array
{
return [
ProductImportAction::make()
->standalone()
->onlyOnIndex()
->icon(IconRepository::make()->upload()),
];
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Product;
use App\Models\Ecommerce\Product\Product\Product as ProductModel;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant\VariantFieldsForCreate;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant\VariantFieldsForDetail;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant\VariantFieldsForIndex;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant\VariantFieldsForPreview;
use App\Nova\Resources\Ecommerce\Product\Product\Concerns\Variant\VariantFieldsForUpdate;
use App\Rules\CommaSeparatedIntegers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Validator;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Http\Requests\NovaRequest;
use ShuvroRoy\NovaTabs\Traits\HasTabs;
class ProductVariant extends Resource
{
/**
* Supports tabs
*/
use HasTabs;
/**
* The model the resource corresponds to.
*
* @var class-string<ProductModel>
*/
public static $model = ProductModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['media'];
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id', 'name', 'sku', 'barcode',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Product Variations');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Product Variation');
}
/**
* Build an "index" query for the given resource.
*/
public static function indexQuery(NovaRequest $request, $query)
{
if (Validator::make($request->all(), [
'ids' => ['required', 'string', new CommaSeparatedIntegers()],
])->passes()) {
$query->whereIntegerInRaw('id', explode(',', $request->ids));
return $query;
}
$query->whereNotNull('parent_id');
return $query;
}
/**
* Get the fields for index.
*/
public function fieldsForIndex(NovaRequest $request): array
{
return VariantFieldsForIndex::make($request, $this);
}
/**
* Get fields for preview
*/
public function fieldsForPreview(NovaRequest $request): array
{
return VariantFieldsForPreview::make($request, $this);
}
/**
* Get the fields for create.
*/
public function fieldsForDetail(NovaRequest $request): array
{
return VariantFieldsForDetail::make($request, $this);
}
/**
* Get the fields for create.
*/
public function fieldsForCreate(NovaRequest $request): array
{
return VariantFieldsForCreate::make($request);
}
/**
* Get the fields displayed by the resource on detail page.
*/
public function fieldsForUpdate(NovaRequest $request): array
{
return VariantFieldsForUpdate::make($request, $this);
}
/**
* Get the fields.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
];
}
/**
* After resource has been created
*/
public static function afterCreate(NovaRequest $request, Model $model): void
{
VariantFieldsForCreate::afterCreate($request, $model);
}
/**
* After resource has been updated
*/
public static function afterUpdate(NovaRequest $request, Model $model): void
{
VariantFieldsForUpdate::afterUpdate($request, $model);
}
/**
* Return the location to redirect the user after creation.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterCreate(NovaRequest $request, $resource)
{
return sprintf('/resources/products/%s', $resource->parent_id);
}
/**
* Return the location to redirect the user after update.
*
* @param \Laravel\Nova\Resource $resource
* @return \Laravel\Nova\URL|string
*/
public static function redirectAfterUpdate(NovaRequest $request, $resource)
{
return sprintf('/resources/products/%s', $resource->parent_id);
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace App\Nova\Resources\Ecommerce\Product\Review;
use App\Models\Ecommerce\Product\Review\Review as ReviewModel;
use App\Models\System\Settings\OS;
use App\Nova\Filters\VisableFilter;
use App\Nova\Resource;
use App\Nova\Resources\Ecommerce\Product\Product\Product;
use App\Nova\User;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Trin4ik\NovaSwitcher\NovaSwitcher;
class Review extends Resource
{
/**
* The model the resource corresponds to.
*
* @var class-string<ReviewModel>
*/
public static $model = ReviewModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'title';
/**
* The relationships that should be eager loaded on index queries.
*
* @var array
*/
public static $with = ['user', 'product'];
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'title',
];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return __('Reviews');
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return __('Review');
}
/**
* Get the fields displayed by the resource.
*/
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
BelongsTo::make(__('User'), 'user', User::class)
->searchable(),
BelongsTo::make(__('Product'), 'product', Product::class)
->searchable(),
Number::make(__('Rating'), 'rating')
->rules('required', 'integer', 'min:0', 'max:5'),
Text::make(__('Title'), 'title')
->defaultStringRules(),
Text::make(__('Content'), 'content')
->defaultStringRules()
->hideFromIndex(),
Select::make(__('Source'), 'source')
->displayUsingLabels()
->searchable()
->options(OS::apps())
->default(OS::default())
->rules('required'),
NovaSwitcher::make(__('Active'), 'is_visible')
->default(false),
NovaSwitcher::make(__('Is recommended'), 'is_recommended')
->default(false),
DateTime::make(__('Created at'), 'created_at')
->turkmenDate()
->exceptOnForms(),
DateTime::make(__('Updated at'), 'updated_at')
->turkmenDate()
->exceptOnForms(),
];
}
/**
* Get the cards available for the request.
*/
public function cards(NovaRequest $request): array
{
return [];
}
/**
* Get the filters available for the resource.
*/
public function filters(NovaRequest $request): array
{
return [
VisableFilter::make(),
];
}
/**
* Get the lenses available for the resource.
*/
public function lenses(NovaRequest $request): array
{
return [];
}
/**
* Get the actions available for the resource.
*/
public function actions(NovaRequest $request): array
{
return [];
}
}