Add all files
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/default/*
|
||||
/public/lucentui/*
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
themes/*
|
||||
!themes/default
|
||||
!themes/lucentui
|
||||
*/.DS_Store
|
||||
.DS_Store
|
||||
|
||||
**/caddy
|
||||
frankenphp
|
||||
frankenphp-worker.php
|
||||
26
Captcha.php
Normal file
26
Captcha.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Captcha extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.captcha');
|
||||
}
|
||||
}
|
||||
113
Disabled/Affiliates/Admin/Resources/AffiliateResource.php
Normal file
113
Disabled/Affiliates/Admin/Resources/AffiliateResource.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources;
|
||||
|
||||
use App\Admin\Components\UserComponent;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
|
||||
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\RelationManagers;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
|
||||
class AffiliateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Affiliate::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-hand-coin-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-hand-coin-fill';
|
||||
|
||||
protected static ?string $navigationGroup = 'Administration';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Toggle::make('enabled')->default(true)->columnSpanFull(),
|
||||
UserComponent::make('user_id'),
|
||||
TextInput::make('code')
|
||||
->label('Referral Code')
|
||||
->required()
|
||||
->minLength(5)
|
||||
->maxLength(20)
|
||||
->unique(ignoreRecord: true),
|
||||
TextInput::make('reward')
|
||||
->label('Affiliate Reward')
|
||||
->helperText('Percentage of the purchase amount the affiliated user would receive as a reward.')
|
||||
->placeholder($extension->config('default_reward'))
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.name')->description(function (Affiliate $affiliate) {
|
||||
if (count($affiliate->earnings) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'Earnings - ' . implode(', ', array_map(function ($key, $value) {
|
||||
return "$key: $value";
|
||||
}, array_keys($affiliate->earnings), $affiliate->earnings));
|
||||
}),
|
||||
TextColumn::make('code')
|
||||
->badge(),
|
||||
TextColumn::make('visitors')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('signups')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Signed Up')
|
||||
->since()
|
||||
->sortable()
|
||||
->dateTimeTooltip(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\AffiliatesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListAffiliates::route('/'),
|
||||
'edit' => Pages\EditAffiliate::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getCreateFormAction()
|
||||
->formId('form'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource;
|
||||
|
||||
class CreateAffiliate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AffiliateResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource;
|
||||
|
||||
class EditAffiliate extends EditRecord
|
||||
{
|
||||
protected static string $resource = AffiliateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource;
|
||||
|
||||
class ListAffiliates extends ListRecords
|
||||
{
|
||||
protected static string $resource = AffiliateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\RelationManagers;
|
||||
|
||||
use App\Admin\Resources\OrderResource;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\AffiliateOrder;
|
||||
|
||||
class AffiliatesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'orders';
|
||||
|
||||
protected static ?string $model = AffiliateOrder::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('affiliate_id')
|
||||
->columns([
|
||||
TextColumn::make('order.id'),
|
||||
TextColumn::make('earnings')->formatStateUsing(function (AffiliateOrder $affiliateOrder) {
|
||||
if (count($affiliateOrder->affiliate->earnings) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return implode(', ', array_map(function ($key, $value) {
|
||||
return "$key: $value";
|
||||
}, array_keys($affiliateOrder->affiliate->earnings), $affiliateOrder->affiliate->earnings));
|
||||
})->label('Earning'),
|
||||
TextColumn::make('order.created_at')
|
||||
->label('Created At')
|
||||
->since()
|
||||
->sortable()
|
||||
->dateTimeTooltip(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make()->url(fn ($record) => OrderResource::getUrl('edit', ['record' => $record->order])),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
132
Disabled/Affiliates/Affiliates.php
Normal file
132
Disabled/Affiliates/Affiliates.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use App\Events\Invoice\Paid as InvoicePaid;
|
||||
use App\Events\Order\Created as OrderCreated;
|
||||
use App\Events\User\Created as UserCreated;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Livewire\Livewire;
|
||||
use Paymenter\Extensions\Others\Affiliates\Listeners\AssociateOrderWithAffiliate;
|
||||
use Paymenter\Extensions\Others\Affiliates\Listeners\IncreamentAffiliateSignups;
|
||||
use Paymenter\Extensions\Others\Affiliates\Listeners\RewardAffiliate;
|
||||
use Paymenter\Extensions\Others\Affiliates\Livewire\Affiliates\Affiliate as AffiliateComponent;
|
||||
use Paymenter\Extensions\Others\Affiliates\Middleware\AffiliatesMiddleware;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
|
||||
class Affiliates extends Extension
|
||||
{
|
||||
public function __construct(public $config = []) {}
|
||||
|
||||
/**
|
||||
* Get all the configuration for the extension
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'default_reward',
|
||||
'label' => 'Default Affiliate Reward',
|
||||
'type' => 'number',
|
||||
'description' => 'Percentage of the purchase amount the affiliated user would receive as a reward.',
|
||||
'required' => true,
|
||||
'suffix' => '%',
|
||||
'validation' => 'integer|min:0|max:100',
|
||||
],
|
||||
[
|
||||
'name' => 'cookie_max_age',
|
||||
'label' => 'Referral Cookie Max-Age',
|
||||
'type' => 'number',
|
||||
'description' => 'Amount of days for which the referral cookie be valid. (Set 0 for infinite)',
|
||||
'required' => true,
|
||||
'validation' => 'integer|min:0',
|
||||
],
|
||||
[
|
||||
'name' => 'type',
|
||||
'label' => 'Affiliate Code Type',
|
||||
'type' => 'select',
|
||||
'default' => 'random',
|
||||
'description' => 'How the affiliate would be assigned.',
|
||||
'required' => true,
|
||||
'options' => [
|
||||
'random' => 'Random',
|
||||
'custom' => 'Custom',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function enabled()
|
||||
{
|
||||
// Run migrations
|
||||
Artisan::call('migrate', ['--path' => 'extensions/Others/Affiliates/database/migrations/2024_12_25_075634_create_ext_affiliates_table.php', '--force' => true]);
|
||||
Artisan::call('migrate', ['--path' => 'extensions/Others/Affiliates/database/migrations/2025_01_31_155928_create_ext_affiliate_orders_table.php', '--force' => true]);
|
||||
}
|
||||
|
||||
public function disabled() {}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
require __DIR__ . '/routes/web.php';
|
||||
View::addNamespace('affiliates', __DIR__ . '/resources/views');
|
||||
Lang::addNamespace('affiliates', __DIR__ . '/resources/lang');
|
||||
|
||||
Livewire::component('affiliate', AffiliateComponent::class);
|
||||
|
||||
User::resolveRelationUsing('affiliate', function (User $userModel) {
|
||||
return $userModel->hasOne(Affiliate::class, 'user_id');
|
||||
});
|
||||
|
||||
ExtensionHelper::registerMiddleware(AffiliatesMiddleware::class);
|
||||
|
||||
// Listen for UserCreated and InvoicePaid events
|
||||
Event::listen(
|
||||
UserCreated::class,
|
||||
IncreamentAffiliateSignups::class,
|
||||
);
|
||||
Event::listen(
|
||||
InvoicePaid::class,
|
||||
RewardAffiliate::class,
|
||||
);
|
||||
Event::listen(
|
||||
OrderCreated::class,
|
||||
AssociateOrderWithAffiliate::class,
|
||||
);
|
||||
|
||||
Event::listen('api.permissions', function () {
|
||||
return [
|
||||
'admin.affiliates.view' => 'View Affiliates',
|
||||
'admin.affiliates.create' => 'Create Affiliates',
|
||||
'admin.affiliates.update' => 'Update Affiliates',
|
||||
'admin.affiliates.delete' => 'Delete Affiliates',
|
||||
];
|
||||
});
|
||||
|
||||
// Event::listen('navigation.dashboard', function ($routes) {
|
||||
// dd($routes);
|
||||
// return [
|
||||
// 'name' => __('affiliates::affiliate.affiliate'),
|
||||
// 'route' => 'affiliate.index',
|
||||
// 'icon' => 'heroicon-o-banknotes',
|
||||
// 'group' => 'Administration',
|
||||
// ];
|
||||
// });
|
||||
|
||||
// Hook onto account navigation
|
||||
Event::listen('navigation.account', function () {
|
||||
return [
|
||||
'name' => __('affiliates::affiliate.affiliate'),
|
||||
'route' => 'affiliate.index',
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
90
Disabled/Affiliates/Http/Controllers/AffiliateController.php
Normal file
90
Disabled/Affiliates/Http/Controllers/AffiliateController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Api\ApiController;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Dedoc\Scramble\Attributes\QueryParameter;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Requests\CreateAffiliateRequest;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Requests\DeleteAffiliateRequest;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Requests\GetAffiliateRequest;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Requests\GetAffiliatesRequest;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Requests\UpdateAffiliateRequest;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Resources\AffiliateResource;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
#[Group(name: 'Affiliates', weight: 1)]
|
||||
class AffiliateController extends ApiController
|
||||
{
|
||||
protected const INCLUDES = [
|
||||
'user',
|
||||
'orders',
|
||||
];
|
||||
|
||||
/**
|
||||
* List Affiliates
|
||||
*/
|
||||
#[QueryParameter('per_page', 'How many items to show per page.', type: 'int', default: 15, example: 20)]
|
||||
#[QueryParameter('page', 'Which page to show.', type: 'int', example: 2)]
|
||||
public function index(GetAffiliatesRequest $request)
|
||||
{
|
||||
// Fetch affiliates with pagination
|
||||
$affiliates = QueryBuilder::for(Affiliate::class)
|
||||
->allowedFilters(['affiliate_id', 'code', 'visitors', 'reward', 'discount'])
|
||||
->allowedIncludes($this->allowedIncludes(self::INCLUDES))
|
||||
->allowedSorts(['id', 'code', 'visitors', 'reward', 'discount', 'created_at'])
|
||||
->simplePaginate(request('per_page', 15));
|
||||
|
||||
// Return the affiliates as a JSON response
|
||||
return AffiliateResource::collection($affiliates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new affiliate
|
||||
*/
|
||||
public function store(CreateAffiliateRequest $request)
|
||||
{
|
||||
// Validate and create the affiliate
|
||||
$affiliate = Affiliate::create($request->validated());
|
||||
|
||||
// Return the created affiliate as a JSON response
|
||||
return new AffiliateResource($affiliate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a specific affiliate
|
||||
*/
|
||||
public function show(GetAffiliateRequest $request, Affiliate $affiliate)
|
||||
{
|
||||
$affiliate = QueryBuilder::for(Affiliate::class)
|
||||
->allowedIncludes($this->allowedIncludes(self::INCLUDES))
|
||||
->findOrFail($affiliate->id);
|
||||
|
||||
// Return the affiliate as a JSON response
|
||||
return new AffiliateResource($affiliate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific affiliate
|
||||
*/
|
||||
public function update(UpdateAffiliateRequest $request, Affiliate $affiliate)
|
||||
{
|
||||
// Validate and update the affiliate
|
||||
$affiliate->update($request->validated());
|
||||
|
||||
// Return the updated affiliate as a JSON response
|
||||
return new AffiliateResource($affiliate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific affiliate
|
||||
*/
|
||||
public function destroy(DeleteAffiliateRequest $request, Affiliate $affiliate)
|
||||
{
|
||||
// Delete the affiliate
|
||||
$affiliate->delete();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
20
Disabled/Affiliates/Http/Requests/CreateAffiliateRequest.php
Normal file
20
Disabled/Affiliates/Http/Requests/CreateAffiliateRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
|
||||
|
||||
use App\Http\Requests\Api\Admin\AdminApiRequest;
|
||||
|
||||
class CreateAffiliateRequest extends AdminApiRequest
|
||||
{
|
||||
protected $permission = 'affiliates.create';
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'code' => 'required|string|max:255|unique:affiliates,code',
|
||||
'enabled' => 'nullable|boolean',
|
||||
'reward' => 'nullable|numeric|min:0|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
10
Disabled/Affiliates/Http/Requests/DeleteAffiliateRequest.php
Normal file
10
Disabled/Affiliates/Http/Requests/DeleteAffiliateRequest.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
|
||||
|
||||
use App\Http\Requests\Api\Admin\AdminApiRequest;
|
||||
|
||||
class DeleteAffiliateRequest extends AdminApiRequest
|
||||
{
|
||||
protected $permission = 'affiliates.delete';
|
||||
}
|
||||
10
Disabled/Affiliates/Http/Requests/GetAffiliateRequest.php
Normal file
10
Disabled/Affiliates/Http/Requests/GetAffiliateRequest.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
|
||||
|
||||
use App\Http\Requests\Api\Admin\AdminApiRequest;
|
||||
|
||||
class GetAffiliateRequest extends AdminApiRequest
|
||||
{
|
||||
protected $permission = 'affiliates.view';
|
||||
}
|
||||
10
Disabled/Affiliates/Http/Requests/GetAffiliatesRequest.php
Normal file
10
Disabled/Affiliates/Http/Requests/GetAffiliatesRequest.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
|
||||
|
||||
use App\Http\Requests\Api\Admin\AdminApiRequest;
|
||||
|
||||
class GetAffiliatesRequest extends AdminApiRequest
|
||||
{
|
||||
protected $permission = 'affiliates.view';
|
||||
}
|
||||
20
Disabled/Affiliates/Http/Requests/UpdateAffiliateRequest.php
Normal file
20
Disabled/Affiliates/Http/Requests/UpdateAffiliateRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
|
||||
|
||||
use App\Http\Requests\Api\Admin\AdminApiRequest;
|
||||
|
||||
class UpdateAffiliateRequest extends AdminApiRequest
|
||||
{
|
||||
protected $permission = 'affiliates.update';
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'nullable|exists:users,id',
|
||||
'code' => 'nullable|string|max:255|unique:affiliates,code,' . $this->route()->parameter('affiliate')->id,
|
||||
'enabled' => 'nullable|boolean',
|
||||
'reward' => 'nullable|numeric|min:0|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Resources;
|
||||
|
||||
use TiMacDonald\JsonApi\JsonApiResource;
|
||||
|
||||
/** @mixin \App\Models\AffiliateOrder */
|
||||
class AffiliateOrderResource extends JsonApiResource
|
||||
{
|
||||
public $attributes = [
|
||||
'id',
|
||||
'order_id',
|
||||
];
|
||||
}
|
||||
27
Disabled/Affiliates/Http/Resources/AffiliateResource.php
Normal file
27
Disabled/Affiliates/Http/Resources/AffiliateResource.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Http\Resources;
|
||||
|
||||
use App\Http\Resources\UserResource;
|
||||
use TiMacDonald\JsonApi\JsonApiResource;
|
||||
|
||||
/** @mixin \App\Models\Affiliate */
|
||||
class AffiliateResource extends JsonApiResource
|
||||
{
|
||||
public $attributes = [
|
||||
'id',
|
||||
'code',
|
||||
'enabled',
|
||||
'visitors',
|
||||
'reward',
|
||||
'discount',
|
||||
'earnings',
|
||||
'updated_at',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public $relationships = [
|
||||
'user' => UserResource::class,
|
||||
'orders' => AffiliateOrderResource::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Listeners;
|
||||
|
||||
use App\Events\Order\Created;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\AffiliateOrder;
|
||||
|
||||
class AssociateOrderWithAffiliate
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(Created $event): void
|
||||
{
|
||||
$referral_code = Cookie::get('referred_by');
|
||||
|
||||
/** @var Affiliate */
|
||||
$affiliate = Affiliate::where('code', $referral_code)->first();
|
||||
if (!$affiliate || $affiliate->user->id === $event->order->user_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
AffiliateOrder::create([
|
||||
'order_id' => $event->order->id,
|
||||
'affiliate_id' => $affiliate->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
34
Disabled/Affiliates/Listeners/IncreamentAffiliateSignups.php
Normal file
34
Disabled/Affiliates/Listeners/IncreamentAffiliateSignups.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Listeners;
|
||||
|
||||
use App\Events\User\Created;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
|
||||
class IncreamentAffiliateSignups
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(Created $event): void
|
||||
{
|
||||
$referral_code = Cookie::get('referred_by');
|
||||
|
||||
/** @var Affiliate */
|
||||
$affiliate = Affiliate::where('code', $referral_code)->first();
|
||||
if (!$affiliate) {
|
||||
return;
|
||||
}
|
||||
|
||||
$affiliate->increment('signups');
|
||||
}
|
||||
}
|
||||
71
Disabled/Affiliates/Listeners/RewardAffiliate.php
Normal file
71
Disabled/Affiliates/Listeners/RewardAffiliate.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Listeners;
|
||||
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\AffiliateOrder;
|
||||
|
||||
class RewardAffiliate
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(object $event): void
|
||||
{
|
||||
/**
|
||||
* @var Invoice $invoice
|
||||
*/
|
||||
$invoice = $event->invoice;
|
||||
|
||||
if ($invoice->items()->first()->reference_type !== Service::class) {
|
||||
return;
|
||||
}
|
||||
$order = $invoice->items()->first()->reference->order;
|
||||
$referral = AffiliateOrder::where('order_id', $order->id)->first();
|
||||
|
||||
if (!$referral) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Affiliate $affiliate
|
||||
*/
|
||||
$affiliate = $referral->affiliate;
|
||||
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
|
||||
$reward_percentage = $affiliate->reward ?: $extension->config('default_reward');
|
||||
$reward_amount = $invoice->total * $reward_percentage / 100;
|
||||
|
||||
/**
|
||||
* @var Collection
|
||||
*/
|
||||
$user_credits = $affiliate->user->credits;
|
||||
$affiliate_credits = $user_credits->filter(function ($credit) use ($invoice) {
|
||||
return $credit->currency_code === $invoice->currency_code;
|
||||
})->first();
|
||||
|
||||
if ($affiliate_credits) {
|
||||
// Add reward to credits
|
||||
$affiliate->user->credits()->where('currency_code', $invoice->currency_code)->update([
|
||||
'amount' => $affiliate_credits->amount + $reward_amount,
|
||||
]);
|
||||
} else {
|
||||
// Create new credits with the invoice's currency code
|
||||
$affiliate->user->credits()->create([
|
||||
'amount' => $reward_amount,
|
||||
'currency_code' => $invoice->currency_code,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Disabled/Affiliates/Livewire/Affiliates/Affiliate.php
Normal file
63
Disabled/Affiliates/Livewire/Affiliates/Affiliate.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Livewire\Affiliates;
|
||||
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate as AffiliateModel;
|
||||
|
||||
class Affiliate extends Component
|
||||
{
|
||||
public string $signup_type = 'custom';
|
||||
|
||||
public ?AffiliateModel $affiliate;
|
||||
|
||||
public string $referral_code = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->affiliate = Auth::user()->affiliate;
|
||||
$this->signup_type = ExtensionHelper::getExtension('other', 'Affiliates')->config('type');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('affiliates::index')->layoutData([
|
||||
'sidebar' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function signup()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
/**
|
||||
* @var App\Models\User
|
||||
*/
|
||||
$user = Auth::user();
|
||||
if ($user->affiliate) {
|
||||
$this->notify(__('affiliates::affiliate.you-are-already-affiliated'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->affiliate = $user->affiliate()->create([
|
||||
'code' => $this->signup_type === 'custom' ? $this->referral_code : Str::random(10),
|
||||
'visitors' => 0,
|
||||
'reward' => null,
|
||||
'discount' => null,
|
||||
])->refresh();
|
||||
|
||||
$this->notify(__('affiliates::affiliate.signup-success'));
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'referral_code' => [Rule::requiredIf($this->signup_type === 'custom'), 'alpha_num:ascii', 'unique:ext_affiliates,code', 'min:5', 'max:25'],
|
||||
];
|
||||
}
|
||||
}
|
||||
50
Disabled/Affiliates/Middleware/AffiliatesMiddleware.php
Normal file
50
Disabled/Affiliates/Middleware/AffiliatesMiddleware.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Middleware;
|
||||
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AffiliatesMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Continue if request doesn't contain `ref` query parameter
|
||||
if (!request()->has('ref')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$affiliate = Affiliate::where('code', request('ref'))->first();
|
||||
|
||||
if (!$affiliate || $affiliate->user->id === auth()->id()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Increase the visitor count if a cookie is not present already, or is not the same one
|
||||
if (!Cookie::has('referred_by') || Cookie::get('referred_by') !== $affiliate->code) {
|
||||
$affiliate->increment('visitors');
|
||||
}
|
||||
|
||||
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
|
||||
$cookie_max_age = (int) $extension->config('cookie_max_age');
|
||||
|
||||
if ($cookie_max_age > 0) {
|
||||
// Set affiliate cookie (and set it to expire after `$cookie_max_age` days)
|
||||
Cookie::queue('referred_by', $affiliate->code, minutes: 60 * 24 * $cookie_max_age);
|
||||
} else {
|
||||
// Never expire
|
||||
Cookie::queue(Cookie::forever('referred_by', $affiliate->code));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
56
Disabled/Affiliates/Models/Affiliate.php
Normal file
56
Disabled/Affiliates/Models/Affiliate.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Affiliate extends Model
|
||||
{
|
||||
protected $table = 'ext_affiliates';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'code',
|
||||
'visitors',
|
||||
'reward',
|
||||
'discount',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(AffiliateOrder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the earnings made by this affiliate
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function earnings(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function (): array {
|
||||
$earnings = [];
|
||||
$this->orders->each(function ($order) use (&$earnings) {
|
||||
foreach ($order->earnings as $currency => $total) {
|
||||
if (!isset($earnings[$currency])) {
|
||||
$earnings[$currency] = 0;
|
||||
}
|
||||
$earnings[$currency] += $total;
|
||||
}
|
||||
});
|
||||
|
||||
return $earnings;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
67
Disabled/Affiliates/Models/AffiliateOrder.php
Normal file
67
Disabled/Affiliates/Models/AffiliateOrder.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Affiliates\Models;
|
||||
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AffiliateOrder extends Model
|
||||
{
|
||||
protected $table = 'ext_affiliate_orders';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'affiliate_id',
|
||||
];
|
||||
|
||||
public function affiliate(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Affiliate::class);
|
||||
}
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the earnings made from this order
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function earnings(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function (): array {
|
||||
$earnings = [];
|
||||
|
||||
/** @var Collection */
|
||||
$invoices = $this->order->invoices;
|
||||
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
|
||||
$reward_percentage = $this->affiliate->reward ?: $extension->config('default_reward');
|
||||
|
||||
$invoices->each(function ($invoice) use (&$earnings, $reward_percentage) {
|
||||
if ($invoice->status !== 'paid') {
|
||||
return;
|
||||
}
|
||||
if (!isset($earnings[$invoice->currency_code])) {
|
||||
$earnings[$invoice->currency_code] = 0;
|
||||
}
|
||||
$earnings[$invoice->currency_code] += $invoice->total * $reward_percentage / 100;
|
||||
});
|
||||
|
||||
foreach ($earnings as $currency => $total) {
|
||||
// Round to 2 decimal places
|
||||
$earnings[$currency] = round($total, 2);
|
||||
}
|
||||
|
||||
return $earnings;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_affiliates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
|
||||
$table->string('code')->unique();
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->integer('reward')->unsigned()->nullable();
|
||||
$table->integer('discount')->unsigned()->nullable();
|
||||
$table->integer('visitors')->unsigned()->default(0);
|
||||
$table->integer('signups')->unsigned()->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('affiliates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Order;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_affiliate_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(Affiliate::class)->constrained()->cascadeOnDelete();
|
||||
$table->foreignIdFor(Order::class)->unique()->constrained()->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('affiliate_orders');
|
||||
}
|
||||
};
|
||||
19
Disabled/Affiliates/resources/lang/en/affiliate.php
Normal file
19
Disabled/Affiliates/resources/lang/en/affiliate.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'affiliate' => 'Affiliate',
|
||||
'signup-for-affiliate' => 'Signup for Affiliate',
|
||||
'code' => 'Code',
|
||||
'visitors' => 'Visitors',
|
||||
'total-visitors' => 'Total Visitors',
|
||||
'signups' => 'Signups',
|
||||
'total-signups' => 'Total Signups',
|
||||
'earnings' => 'Earnings',
|
||||
'total-earnings' => 'Total Earnings',
|
||||
'your-affiliate-link' => 'Your affiliate link',
|
||||
'copy' => 'Copy',
|
||||
|
||||
'you-havent-signed-up-yet' => 'You have not signed up for affiliates program yet!',
|
||||
'you-are-already-affiliated' => 'You have already signed up for affiliate program.',
|
||||
'signup-success' => 'Successfully signed up for affiliate program!',
|
||||
];
|
||||
73
Disabled/Affiliates/resources/views/index.blade.php
Normal file
73
Disabled/Affiliates/resources/views/index.blade.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<div>
|
||||
<x-navigation.breadcrumb />
|
||||
<div class="px-2">
|
||||
|
||||
@isset($affiliate)
|
||||
<div class="mt-4 md:grid-cols-3 grid">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.visitors') }}</span>
|
||||
<span class="text-gray-500">{{ __('affiliates::affiliate.total-visitors') }}</span>
|
||||
<span class="text-2xl font-semibold">{{ Number::format($affiliate->visitors) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.signups') }}</span>
|
||||
<span class="text-gray-500">{{ __('affiliates::affiliate.total-signups') }}</span>
|
||||
<span class="text-2xl font-semibold">
|
||||
{{ Number::format($affiliate->signups) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.earnings') }}</span>
|
||||
<span class="text-gray-500">{{ __('affiliates::affiliate.total-earnings') }}</span>
|
||||
<span class="text-2xl font-semibold">
|
||||
<ul>
|
||||
@foreach ($affiliate->earnings as $currency => $amount)
|
||||
<li>
|
||||
{{ $currency }}: {{ $amount }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-3 flex flex-col mt-4">
|
||||
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.affiliate') }}</span>
|
||||
<span class="text-gray-500">{{ __('affiliates::affiliate.your-affiliate-link') }}</span>
|
||||
<div class="flex flex-row gap-2 mt-2">
|
||||
<x-form.input value="{{ url('/') }}?ref={{ $affiliate->code }}" name="ref"
|
||||
divClass="!mt-0" type="text" readonly />
|
||||
|
||||
<x-button.primary class="!w-fit" type="button"
|
||||
onclick="copyToClipboard('{{ url('/?ref=' . $affiliate->code) }}')">{{ __('affiliates::affiliate.copy') }}</x-button.primary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function copyToClipboard(textToCopy) {
|
||||
const temp = document.createElement("input")
|
||||
temp.type = "text"
|
||||
temp.value = textToCopy
|
||||
|
||||
document.body.appendChild(temp)
|
||||
temp.select()
|
||||
document.execCommand("Copy")
|
||||
document.body.removeChild(temp)
|
||||
}
|
||||
</script>
|
||||
@else
|
||||
<p class="mb-4">{{ __('affiliates::affiliate.you-havent-signed-up-yet') }}</p>
|
||||
<h3 class="text-lg font-bold mb-4">{{ __('affiliates::affiliate.signup-for-affiliate') }}</h3>
|
||||
<form wire:submit.prevent="signup" method="POST">
|
||||
|
||||
@if ($signup_type === 'custom')
|
||||
<x-form.input name="referral_code" type="text" :label="__('affiliates::affiliate.code')" wire:model="referral_code"
|
||||
required />
|
||||
@endif
|
||||
|
||||
<x-button.primary type="submit" class="text-sm !w-fit mt-4">
|
||||
{{ __('auth.sign_up') }}
|
||||
</x-button.primary>
|
||||
</form>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
</div>
|
||||
15
Disabled/Affiliates/routes/web.php
Normal file
15
Disabled/Affiliates/routes/web.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Paymenter\Extensions\Others\Affiliates\Http\Controllers\AffiliateController;
|
||||
use Paymenter\Extensions\Others\Affiliates\Livewire\Affiliates\Affiliate;
|
||||
|
||||
Route::group(['middleware' => ['web', 'auth']], function () {
|
||||
Route::get('/account/affiliate', Affiliate::class)->name('affiliate.index');
|
||||
});
|
||||
|
||||
Route::group(['middleware' => ['api', 'api.admin'], 'prefix' => '/api/v1/admin'], function () {
|
||||
Route::apiResources([
|
||||
'affiliates' => AffiliateController::class,
|
||||
]);
|
||||
});
|
||||
81
Disabled/Credit/Credit.php
Normal file
81
Disabled/Credit/Credit.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// /var/www/paymenter/extensions/Others/Credit/Credit.php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Credit;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use App\Events\User\Created as UserCreated;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Credit extends Extension
|
||||
{
|
||||
/**
|
||||
* Get all the configuration for the extension
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'start_credit',
|
||||
'type' => 'number',
|
||||
'label' => 'Initial Credits',
|
||||
'required' => true,
|
||||
'validation' => 'min:0',
|
||||
'description' => 'Amount of credit given to new users'
|
||||
],
|
||||
[
|
||||
'name' => 'currency_code',
|
||||
'type' => 'text',
|
||||
'label' => 'Currency Code',
|
||||
'required' => true,
|
||||
'default' => 'USD',
|
||||
'description' => '3-letter currency code for credits'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
Log::info('Credit extension initialized');
|
||||
|
||||
Event::listen(UserCreated::class, function ($event) {
|
||||
try {
|
||||
$credits = (float) $this->config('start_credit');
|
||||
$currency = $this->config('currency_code', 'USD');
|
||||
$userId = $event->user->id;
|
||||
|
||||
Log::info('Processing new user credits', [
|
||||
'user_id' => $userId,
|
||||
'amount' => $credits,
|
||||
'currency' => $currency
|
||||
]);
|
||||
|
||||
if ($credits > 0) {
|
||||
DB::table('credits')->insert([
|
||||
'user_id' => $userId,
|
||||
'currency_code' => $currency,
|
||||
'amount' => $credits,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
Log::info('Credits added successfully', [
|
||||
'user_id' => $userId,
|
||||
'amount' => $credits,
|
||||
'currency' => $currency
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Credit extension error', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
165
Disabled/DiscordPlus/DiscordPlus.php
Normal file
165
Disabled/DiscordPlus/DiscordPlus.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DiscordPlus Extension for Paymenter
|
||||
*
|
||||
* Created by: Exp (Discord: exp_yt)
|
||||
* Company: ExpHost - https://www.exphost.net/
|
||||
*
|
||||
* This code is the intellectual property of Exp and ExpHost.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*/
|
||||
|
||||
namespace Paymenter\Extensions\Others\DiscordPlus;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use App\Events\Invoice\Finalized as InvoiceCreated;
|
||||
use App\Events\Invoice\Paid as InvoicePaid;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class DiscordPlus extends Extension
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'DiscordPlus';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Sends Discord DMs to users when invoices are created or paid.';
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getAuthor(): string
|
||||
{
|
||||
return 'Exp_YT';
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$botToken = $this->config('discord_bot_token');
|
||||
if (!$botToken) return;
|
||||
|
||||
app()->bind(\App\Http\Controllers\SocialLoginController::class, function () {
|
||||
return new \Paymenter\Extensions\Others\DiscordPlus\SocialLoginController();
|
||||
});
|
||||
|
||||
Event::listen(InvoiceCreated::class, function ($event) use ($botToken) {
|
||||
$this->sendDm($event->invoice, 'invoice_created', $botToken);
|
||||
});
|
||||
|
||||
Event::listen(InvoicePaid::class, function ($event) use ($botToken) {
|
||||
$this->sendDm($event->invoice, 'invoice_paid', $botToken);
|
||||
});
|
||||
}
|
||||
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'discord_bot_token',
|
||||
'type' => 'text',
|
||||
'label' => 'Discord Bot Token',
|
||||
'required' => true,
|
||||
'encrypted' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function sendDm($invoice, $type, $botToken)
|
||||
{
|
||||
if (!$invoice || !$invoice->user) {
|
||||
return;
|
||||
}
|
||||
$user = $invoice->user;
|
||||
$discordId = $user->discord_id;
|
||||
$botToken = trim($botToken);
|
||||
|
||||
if (empty($discordId) || !is_string($discordId) || !ctype_digit((string)$discordId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$company = \Config::get('app.name', 'Paymenter');
|
||||
$invoiceUrl = \URL::to("/invoices/{$invoice->id}");
|
||||
$amount = (string)$invoice->formattedTotal;
|
||||
$dueDate = $invoice->due_at ? $invoice->due_at->format('d M Y') : 'N/A';
|
||||
|
||||
if ($type === 'invoice_created') {
|
||||
$embed = [
|
||||
'embeds' => [[
|
||||
'title' => '🧾 New Invoice Created',
|
||||
'color' => 0x7289DA,
|
||||
'description' => "A new invoice has been generated for your account.\n[View & Pay Invoice]({$invoiceUrl})",
|
||||
'fields' => [
|
||||
[ 'name' => 'Invoice #', 'value' => "#{$invoice->number}", 'inline' => true ],
|
||||
[ 'name' => 'Amount Due', 'value' => $amount, 'inline' => true ],
|
||||
[ 'name' => 'Due Date', 'value' => ($dueDate !== 'N/A' ? "`{$dueDate}`" : '*N/A*'), 'inline' => false ],
|
||||
],
|
||||
'footer' => [ 'text' => $company ],
|
||||
]]
|
||||
];
|
||||
} else {
|
||||
$embed = [
|
||||
'embeds' => [[
|
||||
'title' => '✅ Invoice Paid',
|
||||
'color' => 0x43B581,
|
||||
'description' => "Thank you for your payment!\n[View Invoice]({$invoiceUrl})",
|
||||
'fields' => [
|
||||
[ 'name' => 'Invoice #', 'value' => "#{$invoice->number}", 'inline' => true ],
|
||||
[ 'name' => 'Amount', 'value' => $amount, 'inline' => true ],
|
||||
],
|
||||
'footer' => [ 'text' => $company ],
|
||||
]]
|
||||
];
|
||||
}
|
||||
|
||||
$dmResponse = \Http::withHeaders([
|
||||
'Authorization' => 'Bot ' . $botToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post('https://discord.com/api/v10/users/@me/channels', [
|
||||
'recipient_id' => $discordId,
|
||||
]);
|
||||
|
||||
if ($dmResponse->failed()) {
|
||||
\Log::error('[DiscordPlus] Failed to create DM channel for discord_id: ' . $discordId . '. Response: ' . $dmResponse->body());
|
||||
return;
|
||||
}
|
||||
|
||||
$channelId = $dmResponse->json('id');
|
||||
if (!$channelId) {
|
||||
\Log::error('[DiscordPlus] No channel ID returned for discord_id: ' . $discordId . '. Response: ' . $dmResponse->body());
|
||||
return;
|
||||
}
|
||||
|
||||
$msgResponse = \Http::withHeaders([
|
||||
'Authorization' => 'Bot ' . $botToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post("https://discord.com/api/v10/channels/{$channelId}/messages", $embed);
|
||||
|
||||
if ($msgResponse->failed()) {
|
||||
\Log::error('[DiscordPlus] Failed to send DM to channel: ' . $channelId . '. Response: ' . $msgResponse->body());
|
||||
}
|
||||
}
|
||||
|
||||
public function enabled(): void
|
||||
{
|
||||
if (!\Schema::hasColumn('users', 'discord_id')) {
|
||||
\Artisan::call('migrate', [
|
||||
'--path' => 'extensions/Others/DiscordPlus/migrations/2025_08_27_000000_add_discord_id_to_users_table.php',
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
public function disabled(): void {}
|
||||
public function updated(): void {}
|
||||
public function install(): void {}
|
||||
}
|
||||
111
Disabled/DiscordPlus/SocialLoginController.php
Normal file
111
Disabled/DiscordPlus/SocialLoginController.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DiscordPlus Extension for Paymenter
|
||||
*
|
||||
* Created by: Exp (Discord: exp_yt)
|
||||
* Company: ExpHost - https://www.exphost.net/
|
||||
*
|
||||
* This code is the intellectual property of Exp and ExpHost.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*/
|
||||
|
||||
namespace Paymenter\Extensions\Others\DiscordPlus;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Laravel\Socialite\Two\GithubProvider;
|
||||
use Laravel\Socialite\Two\GoogleProvider;
|
||||
use SocialiteProviders\Discord\Provider as DiscordProvider;
|
||||
|
||||
class SocialLoginController extends \App\Http\Controllers\Controller
|
||||
{
|
||||
protected $discord_driver;
|
||||
protected $github_driver;
|
||||
protected $google_driver;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->discord_driver = Socialite::buildProvider(DiscordProvider::class, [
|
||||
'client_id' => config('settings.oauth_discord_client_id'),
|
||||
'client_secret' => config('settings.oauth_discord_client_secret'),
|
||||
'redirect' => '/oauth/discord/callback',
|
||||
]);
|
||||
$this->github_driver = Socialite::buildProvider(GithubProvider::class, [
|
||||
'client_id' => config('settings.oauth_github_client_id'),
|
||||
'client_secret' => config('settings.oauth_github_client_secret'),
|
||||
'redirect' => '/oauth/github/callback',
|
||||
]);
|
||||
$this->google_driver = Socialite::buildProvider(GoogleProvider::class, [
|
||||
'client_id' => config('settings.oauth_google_client_id'),
|
||||
'client_secret' => config('settings.oauth_google_client_secret'),
|
||||
'redirect' => '/oauth/google/callback',
|
||||
]);
|
||||
}
|
||||
|
||||
public function redirect($provider)
|
||||
{
|
||||
if (!config("settings.oauth_$provider")) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return match ($provider) {
|
||||
'discord' => $this->discord_driver->scopes(['email'])->redirect(),
|
||||
'github' => $this->github_driver->scopes(['user:email'])->redirect(),
|
||||
'google' => $this->google_driver->scopes(['email'])->redirect(),
|
||||
default => abort(404)
|
||||
};
|
||||
}
|
||||
|
||||
public function handle($provider)
|
||||
{
|
||||
if ($provider == 'discord') {
|
||||
$oauth_user = $this->discord_driver->user();
|
||||
|
||||
if (!($oauth_user->user['verified'] ?? true)) {
|
||||
return redirect()->route('login')->with('error', __('auth.oauth.unverified_discord_account'));
|
||||
}
|
||||
|
||||
$user = User::where('email', $oauth_user->email)->first();
|
||||
if (!$user) {
|
||||
return redirect()->route('register')->with('error', __('auth.oauth.account_not_registered'));
|
||||
}
|
||||
|
||||
// Only set discord_id if it exists and is not already set
|
||||
$discordId = $oauth_user->getId() ?? ($oauth_user->id ?? $oauth_user->user['id'] ?? null);
|
||||
if ($discordId && (!$user->discord_id || $user->discord_id != $discordId)) {
|
||||
$user->discord_id = $discordId;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
Auth::login($user, true);
|
||||
|
||||
return redirect()->route('home');
|
||||
} elseif ($provider == 'google') {
|
||||
$oauth_user = $this->google_driver->user();
|
||||
|
||||
$user = User::where('email', $oauth_user->email)->first();
|
||||
if (!$user) {
|
||||
return redirect()->route('register')->with('error', __('auth.oauth.account_not_registered'));
|
||||
}
|
||||
|
||||
Auth::login($user, true);
|
||||
|
||||
return redirect()->route('home');
|
||||
} elseif ($provider == 'github') {
|
||||
$oauth_user = $this->github_driver->user();
|
||||
|
||||
$user = User::where('email', $oauth_user->email)->first();
|
||||
if (!$user) {
|
||||
return redirect()->route('register')->with('error', __('auth.oauth.account_not_registered'));
|
||||
}
|
||||
|
||||
Auth::login($user, true);
|
||||
|
||||
return redirect()->route('home');
|
||||
} else {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('users', 'discord_id')) {
|
||||
$table->string('discord_id')->nullable()->unique()->after('email');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'discord_id')) {
|
||||
$table->dropColumn('discord_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
61
Disabled/FreeTrial/FreeTrial.php
Normal file
61
Disabled/FreeTrial/FreeTrial.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Gateways\FreeTrial;
|
||||
|
||||
use App\Classes\Extension\Gateway;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\Invoice;
|
||||
|
||||
class FreeTrial extends Gateway
|
||||
{
|
||||
|
||||
/**
|
||||
* Get all the configuration for the extension
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'max_order_value',
|
||||
'label' => 'Max Order Value',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a view or a url to redirect to
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @param float $total
|
||||
* @return string
|
||||
*/
|
||||
public function pay(Invoice $invoice, $total)
|
||||
{
|
||||
$items = $invoice->items;
|
||||
$maxOrderValue = $this->config('max_order_value');
|
||||
|
||||
// Make sure that the invoice is not for adding credits
|
||||
foreach ($items as $item) {
|
||||
if ($item->reference_type == 'App\\Models\\Credit') {
|
||||
return redirect()->route('invoices.show', ['invoice' => $invoice->id])
|
||||
->with('notification', ['message' => 'Cannot be used on Credits', 'type' => 'error']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($total > $maxOrderValue) {
|
||||
return redirect()->route('invoices.show', ['invoice' => $invoice->id])
|
||||
->with('notification', ['message' => 'Item not eligible', 'type' => 'error']);
|
||||
}
|
||||
|
||||
ExtensionHelper::addPayment($invoice->id, 'FreeTrial', $total);
|
||||
return redirect()->route('invoices.show', ['invoice' => $invoice->id])->with('notification', [
|
||||
'message' => 'Trial Activated',
|
||||
'type' => 'success'
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
Disabled/FreeTrial/LICENSE
Normal file
21
Disabled/FreeTrial/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Vaibhav Dhiman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
60
Disabled/FreeTrial/README.md
Normal file
60
Disabled/FreeTrial/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Free Trial Payment Gateway for Paymenter
|
||||
|
||||
A simple extension for [Paymenter](https://paymenter.org) that allows automatic invoice approval for orders under a configurable maximum value. Ideal for offering **free trials** or promotional services with zero friction.
|
||||
|
||||
## Supported Versions
|
||||
> The last version which supported Paymenter v0.9.5 is [v1.0.0](https://github.com/VaibhavSys/FreeTrial-Paymenter-Extension/releases/tag/v1.0.0)
|
||||
- **Supported Version:** Paymenter v1.0.4
|
||||
- **Extension Version:** v2.0.0
|
||||
|
||||
## Features
|
||||
- Automatically approves invoices if the total is under a configured maximum value.
|
||||
- Seamless free trial support — no real payment is processed.
|
||||
- Lightweight and easy to configure.
|
||||
|
||||
> ⚠️ **Note:** This gateway cannot be used to credit user accounts. It only applies to invoices which do not add credits to a user account.
|
||||
|
||||
## How It Works
|
||||
|
||||
Once enabled, if a user attempts to pay an invoice and the total amount is **less than or equal to** the configured **Max Order Value**, the payment is **automatically marked as paid**. If the amount exceeds the limit, the payment is **rejected**.
|
||||
|
||||
This gives you full flexibility to:
|
||||
- Create promotional plans priced under the trial limit.
|
||||
- Use one-time coupons and products to offer free trials.
|
||||
- Test services internally without actual transactions.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Install the extension**
|
||||
- Replace `/var/www/paymenter` with your actual Paymenter installation path.
|
||||
- **One-liner Install:**
|
||||
```sh
|
||||
git clone https://github.com/VaibhavSys/FreeTrial-Paymenter-Extension.git /var/www/paymenter/extensions/Gateways/FreeTrial
|
||||
```
|
||||
- **Manual Install:**
|
||||
[Download](https://github.com/VaibhavSys/FreeTrial-Paymenter-Extension/releases/latest/download/FreeTrial.zip) the extension and extract it to:
|
||||
```
|
||||
/var/www/paymenter/extensions/Gateways
|
||||
```
|
||||
1. **Enable the extension** from the Paymenter admin panel.
|
||||
1. **Configure the Max Order Value** in the admin panel (e.g., `0.5` to allow automatic approval for invoices ≤ $0.5).
|
||||
|
||||
That’s it — you’re ready to offer free trials!
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Max Order Value (number):** The maximum invoice amount (in your invoice currency) that can be auto-approved. Any invoice above this value will be rejected.
|
||||
|
||||

|
||||
|
||||
> 💡 If you want to use this with a one-time promotional coupon/product, ensure the final payable invoice amount (after discount) is within the Max Order Value.
|
||||
|
||||
## Support
|
||||
|
||||
For questions, suggestions, or issues:
|
||||
- **Discord:** [@vaibhavd](https://discord.com/users/914452175839723550)
|
||||
- **GitHub Issues:** [FreeTrial-Paymenter Issues](https://github.com/VaibhavSys/FreeTrial-Paymenter-Extension/issues)
|
||||
|
||||
## Additional Information
|
||||
|
||||
- **License:** MIT — see [LICENSE](LICENSE) for details.
|
||||
BIN
Disabled/FreeTrial/Settings.png
Normal file
BIN
Disabled/FreeTrial/Settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
Disabled/FreeTrial/cover.png
Normal file
BIN
Disabled/FreeTrial/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
Disabled/FreeTrial/logo.png
Normal file
BIN
Disabled/FreeTrial/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
83
Disabled/Mollie/Mollie.php
Normal file
83
Disabled/Mollie/Mollie.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Gateways\Mollie;
|
||||
|
||||
use App\Classes\Extension\Gateway;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Mollie extends Gateway
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
require __DIR__ . '/routes.php';
|
||||
// Register webhook route
|
||||
}
|
||||
|
||||
private function request($url, $method = 'get', $data = [])
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->config('api_key'),
|
||||
'Content-Type' => 'application/json',
|
||||
])->$method('https://api.mollie.com' . $url, $data);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Mollie API error: ' . $response->json()['detail']);
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the configuration for the extension
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'api_key',
|
||||
'label' => 'API Key',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a view or a url to redirect to
|
||||
*
|
||||
* @param float $total
|
||||
* @return string
|
||||
*/
|
||||
public function pay(Invoice $invoice, $total)
|
||||
{
|
||||
$response = $this->request('/v2/payments', 'post', [
|
||||
'amount' => [
|
||||
'currency' => $invoice->currency_code,
|
||||
'value' => number_format($total, 2, '.', ''),
|
||||
],
|
||||
'description' => 'Invoice #' . $invoice->id,
|
||||
'redirectUrl' => route('invoices.show', $invoice) . '?checkPayment=true',
|
||||
'webhookUrl' => route('extensions.gateways.mollie.webhook', $invoice),
|
||||
'metadata' => [
|
||||
'invoice_id' => $invoice->id,
|
||||
],
|
||||
]);
|
||||
|
||||
return $response['_links']['checkout']['href'];
|
||||
}
|
||||
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
$payment = $this->request('/v2/payments/' . $request->input('id'));
|
||||
|
||||
if ($payment['status'] == 'paid') {
|
||||
ExtensionHelper::addPayment($payment['metadata']['invoice_id'], 'Mollie', $payment['amount']['value'], transactionId: $payment['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Disabled/Mollie/routes.php
Normal file
7
Disabled/Mollie/routes.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Paymenter\Extensions\Gateways\Mollie\Mollie;
|
||||
|
||||
Route::post('/extensions/gateways/mollie/webhook', [Mollie::class, 'webhook'])->withoutMiddleware([VerifyCsrfToken::class])->name('extensions.gateways.mollie.webhook');
|
||||
115
Disabled/Pages/Admin/Resources/PageResource.php
Normal file
115
Disabled/Pages/Admin/Resources/PageResource.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages\Admin\Resources;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Str;
|
||||
use Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource\Pages;
|
||||
use Paymenter\Extensions\Others\Pages\Models\Page;
|
||||
|
||||
class PageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Page::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-file-text-line';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuration';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('title')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
if (($get('slug') ?? '') !== Str::slug($old)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
Forms\Components\TextInput::make('slug')->rule('regex:/^[a-z0-9-]+$/')
|
||||
->required()->unique(ignoreRecord: true),
|
||||
Forms\Components\Textarea::make('description'),
|
||||
Forms\Components\RichEditor::make('content')->enableToolbarButtons()->hidden(fn (Get $get) => $get('as_html')),
|
||||
Forms\Components\MarkdownEditor::make('content')->disableAllToolbarButtons()->hidden(fn (Get $get) => !$get('as_html')),
|
||||
Forms\Components\Toggle::make('visible')
|
||||
->label('Visible'),
|
||||
Forms\Components\Toggle::make('as_html')
|
||||
->live()
|
||||
->label('Show content as raw HTML (also removes title from the page)'),
|
||||
Forms\Components\Select::make('visibility')
|
||||
->options([
|
||||
'public' => __('Public'),
|
||||
'client' => __('Customers Only'),
|
||||
'admin' => __('Admin Only'),
|
||||
])
|
||||
->default('public'),
|
||||
Forms\Components\Select::make('navigation')
|
||||
->options([
|
||||
'none' => __('None (hidden)'),
|
||||
'top' => __('Top Navigation'),
|
||||
'account_dropdown' => __('Account Dropdown (requires login)'),
|
||||
'dashboard' => __('Client area/Dashboard (requires login)'),
|
||||
])
|
||||
->default('none')
|
||||
->label('Show in navigation'),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('title')
|
||||
->label('Title')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('slug')
|
||||
->label('Slug')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('visible')
|
||||
->label('Visible')
|
||||
->boolean(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->reorderable('sort')
|
||||
->defaultSort('sort', 'asc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListPages::route('/'),
|
||||
'create' => Pages\CreatePage::route('/create'),
|
||||
'edit' => Pages\EditPage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource;
|
||||
|
||||
class CreatePage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PageResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource;
|
||||
|
||||
class EditPage extends EditRecord
|
||||
{
|
||||
protected static string $resource = PageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource;
|
||||
|
||||
class ListPages extends ListRecords
|
||||
{
|
||||
protected static string $resource = PageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
38
Disabled/Pages/Livewire/Page.php
Normal file
38
Disabled/Pages/Livewire/Page.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages\Livewire;
|
||||
|
||||
use App\Livewire\Component;
|
||||
use Paymenter\Extensions\Others\Pages\Models\Page as ModelsPage;
|
||||
|
||||
class Page extends Component
|
||||
{
|
||||
public ModelsPage $page;
|
||||
|
||||
public function mount($fallbackPlaceholder)
|
||||
{
|
||||
// Validate if page exists
|
||||
$this->page = ModelsPage::where('slug', $fallbackPlaceholder)->firstOrFail();
|
||||
if (!$this->page->visible) {
|
||||
abort(404);
|
||||
}
|
||||
// Validate if user is logged in
|
||||
if ($this->page->visibility == 'client' && !auth()->check()) {
|
||||
abort(404);
|
||||
}
|
||||
if ($this->page->visibility == 'admin' && (!auth()->check() || is_null(auth()->user()->role))) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('others.pages::page', [
|
||||
'page' => $this->page,
|
||||
])->layout('layouts.app', [
|
||||
'title' => $this->page->title,
|
||||
'description' => $this->page->description,
|
||||
'sidebar' => $this->page->navigation == 'dashboard' ? true : false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
Disabled/Pages/Models/Page.php
Normal file
33
Disabled/Pages/Models/Page.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Page extends Model
|
||||
{
|
||||
protected $table = 'ext_pages';
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'title',
|
||||
'description',
|
||||
'content',
|
||||
'visible',
|
||||
'as_html',
|
||||
'visibility',
|
||||
'navigation',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'visible' => 'boolean',
|
||||
'html' => 'boolean',
|
||||
'visibility' => 'string',
|
||||
];
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
}
|
||||
108
Disabled/Pages/Pages.php
Normal file
108
Disabled/Pages/Pages.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Pages;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Paymenter\Extensions\Others\Pages\Admin\Resources\PageResource;
|
||||
use Paymenter\Extensions\Others\Pages\Livewire\Page;
|
||||
use Paymenter\Extensions\Others\Pages\Models\Page as ModelsPage;
|
||||
|
||||
class Pages extends Extension
|
||||
{
|
||||
/**
|
||||
* Get all the configuration for the extension
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
try {
|
||||
return [
|
||||
[
|
||||
'name' => 'Notice',
|
||||
'type' => 'placeholder',
|
||||
'label' => new HtmlString('You can use this extension to create custom frontend pages, go to <a class="text-primary-600" href="' . PageResource::getUrl() . '">Pages</a> to get started.'),
|
||||
],
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
[
|
||||
'name' => 'Notice',
|
||||
'type' => 'placeholder',
|
||||
'label' => new HtmlString('You can use this extension to create custom frontend pages, you\'ll need to enable this extension above to get started.'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function enabled()
|
||||
{
|
||||
// Run migrations
|
||||
Artisan::call('migrate', ['--path' => 'extensions/Others/Pages/database/migrations', '--force' => true]);
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
// Register routes
|
||||
require __DIR__ . '/routes.php';
|
||||
View::addNamespace('others.pages', __DIR__ . '/resources/views');
|
||||
|
||||
// Register livewire
|
||||
\Livewire\Livewire::component('extensions.others.pages.show', Page::class);
|
||||
|
||||
Event::listen('navigation', function () {
|
||||
if (ModelsPage::where('visible', true)->where('navigation', 'top')->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pages = ModelsPage::where('visible', true)->where('navigation', 'top')->orderBy('sort')->get();
|
||||
|
||||
return $this->toNavigation($pages);
|
||||
});
|
||||
|
||||
Event::listen('navigation.account-dropdown', function () {
|
||||
if (ModelsPage::where('visible', true)->where('navigation', 'account_dropdown')->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pages = ModelsPage::where('visible', true)->where('navigation', 'account_dropdown')->orderBy('sort')->get();
|
||||
|
||||
return $this->toNavigation($pages);
|
||||
});
|
||||
|
||||
Event::listen('navigation.dashboard', function () {
|
||||
if (ModelsPage::where('visible', true)->where('navigation', 'dashboard')->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pages = ModelsPage::where('visible', true)->where('navigation', 'dashboard')->orderBy('sort')->get();
|
||||
|
||||
return $this->toNavigation($pages);
|
||||
});
|
||||
}
|
||||
|
||||
private function toNavigation($pages)
|
||||
{
|
||||
$navigation = [];
|
||||
foreach ($pages as $page) {
|
||||
if ($page->visibility == 'client' && !auth()->check()) {
|
||||
continue;
|
||||
}
|
||||
if ($page->visibility == 'admin' && (!auth()->check() || is_null(auth()->user()->role))) {
|
||||
continue;
|
||||
}
|
||||
$navigation[] = [
|
||||
'name' => $page->title,
|
||||
'route' => 'extensions.others.pages',
|
||||
'params' => ['fallbackPlaceholder' => $page->slug],
|
||||
];
|
||||
}
|
||||
|
||||
return $navigation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_pages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug')->unique();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->longText('content')->nullable();
|
||||
$table->boolean('visible')->default(true);
|
||||
$table->boolean('as_html')->default(false);
|
||||
$table->enum('visibility', ['public', 'client', 'admin'])->default('public');
|
||||
$table->enum('navigation', ['none', 'top', 'account_dropdown', 'dashboard'])->default('none');
|
||||
$table->unsignedTinyInteger('sort')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ext_pages');
|
||||
}
|
||||
};
|
||||
8
Disabled/Pages/readme.md
Normal file
8
Disabled/Pages/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Thanks for buying this extension!
|
||||
|
||||
Buying this extension means you are supporting the development of Paymenter and helping us to keep it free for everyone.
|
||||
|
||||
To install this extension just drop it in the `extensions/Others` folder of your Paymenter installation. You can find the `extensions/Others` folder in the root directory of your Paymenter installation.
|
||||
|
||||
|
||||
If you require assistance with this extension, please go to the [Paymenter Discord](https://discord.gg/kJWDZ86NUZ) and ask for help in the 3rd party support channel.
|
||||
8
Disabled/Pages/resources/views/page.blade.php
Normal file
8
Disabled/Pages/resources/views/page.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="prose dark:prose-invert max-w-full bg-background-secondary border border-neutral p-6 rounded-lg">
|
||||
@if($page->as_html)
|
||||
{!! $page->content !!}
|
||||
@else
|
||||
<h1 class="mb-2">{{ $page->title }}</h1>
|
||||
<div>{!! $page->content !!}</div>
|
||||
@endif
|
||||
</div>
|
||||
6
Disabled/Pages/routes.php
Normal file
6
Disabled/Pages/routes.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Paymenter\Extensions\Others\Pages\Livewire\Page;
|
||||
|
||||
Route::fallback(Page::class)->middleware('web')->name('extensions.others.pages');
|
||||
87
Disabled/PayPal_IPN/PayPal_IPN.php
Normal file
87
Disabled/PayPal_IPN/PayPal_IPN.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Gateways\PayPal_IPN;
|
||||
|
||||
use App\Classes\Extension\Gateway;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PayPal_IPN extends Gateway
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
require __DIR__ . '/routes.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the configuration for the extension
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'email',
|
||||
'label' => 'PayPal Email',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'test_mode',
|
||||
'label' => 'Test Mode',
|
||||
'type' => 'checkbox',
|
||||
'required' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a view or a url to redirect to
|
||||
*
|
||||
* @param float $total
|
||||
* @return string
|
||||
*/
|
||||
public function pay(Invoice $invoice, $total)
|
||||
{
|
||||
$paypal_url = $this->config('test_mode') ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr';
|
||||
$paypal_email = $this->config('email');
|
||||
$return_url = route('invoices.show', $invoice);
|
||||
$cancel_url = route('invoices.show', $invoice);
|
||||
|
||||
$notify_url = route('extensions.gateways.paypal_ipn.notify');
|
||||
|
||||
$query = '?cmd=_xclick';
|
||||
$query .= '&item_name=' . urlencode('Invoice #' . $invoice->id);
|
||||
$query .= '&item_number=' . $invoice->id;
|
||||
$query .= '&amount=' . number_format($total, 2, '.', '');
|
||||
$query .= '¤cy_code=' . $invoice->currency_code;
|
||||
$query .= '&business=' . $paypal_email;
|
||||
$query .= '¬ify_url=' . $notify_url;
|
||||
$query .= '&return=' . $return_url;
|
||||
$query .= '&cancel_return=' . $cancel_url;
|
||||
|
||||
return $paypal_url . $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the IPN request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notify(Request $request)
|
||||
{
|
||||
// Send the request to PayPal
|
||||
$response = Http::asForm()->post($this->config('test_mode') ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' : 'https://ipnpb.paypal.com/cgi-bin/webscr', [
|
||||
'cmd' => '_notify-validate',
|
||||
] + $request->all());
|
||||
|
||||
// Check if the response is verified
|
||||
if ($response->body() == 'VERIFIED') {
|
||||
ExtensionHelper::addPayment($request->item_number, 'PayPal', $request->mc_gross, $request->mc_fee, transactionId: $request->txn_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Disabled/PayPal_IPN/routes.php
Normal file
6
Disabled/PayPal_IPN/routes.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Paymenter\Extensions\Gateways\PayPal_IPN\PayPal_IPN;
|
||||
|
||||
Route::post('/extensions/paypal_ipn/notify', [PayPal_IPN::class, 'notify'])->name('extensions.gateways.paypal_ipn.notify');
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\ProductPageResource;
|
||||
|
||||
class CreateProductPage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductPageResource::class;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\Pages;
|
||||
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\ProductPageResource;
|
||||
|
||||
class EditProductPage extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProductPageResource::class;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\ProductPageResource;
|
||||
|
||||
class ListProductPages extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProductPageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources;
|
||||
|
||||
use App\Models\Category;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Models\ProductPageSettings;
|
||||
|
||||
class ProductPageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ProductPageSettings::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-pages-line';
|
||||
|
||||
protected static ?string $navigationGroup = 'Content';
|
||||
|
||||
protected static ?string $navigationLabel = 'Product Pages';
|
||||
|
||||
protected static ?int $navigationSort = 16;
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Product Page';
|
||||
|
||||
protected static ?string $modelLabel = 'Product Page';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Basic Information')
|
||||
->schema([
|
||||
FileUpload::make('hero_section.background')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('product-pages/hero')
|
||||
->required()
|
||||
->label('Background Image'),
|
||||
TextInput::make('hero_section.title')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->label('Title')
|
||||
->afterStateUpdated(function ($state, callable $set) {
|
||||
$set('hero_section.slug', \Str::slug($state));
|
||||
$set('seo.title', $state);
|
||||
}),
|
||||
TextInput::make('hero_section.slug')
|
||||
->required()
|
||||
->unique(
|
||||
table: 'ext_product_page_builder',
|
||||
column: 'hero_section->slug',
|
||||
ignorable: fn ($record) => $record
|
||||
)
|
||||
->label('URL Slug'),
|
||||
RichEditor::make('hero_section.description')
|
||||
->required()
|
||||
->label('Description')
|
||||
->afterStateUpdated(function ($state, callable $set) {
|
||||
$set('seo.description', strip_tags($state));
|
||||
}),
|
||||
Select::make('category_id')
|
||||
->label('Category')
|
||||
->options(Category::all()->pluck('name', 'id'))
|
||||
->required(),
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('SEO Settings')
|
||||
->schema([
|
||||
TextInput::make('seo.title')
|
||||
->label('SEO Title')
|
||||
->placeholder('Will use Basic Information title if left empty'),
|
||||
TextInput::make('seo.url')
|
||||
->label('Canonical URL')
|
||||
->placeholder('Auto-generated from slug')
|
||||
->dehydrated(false)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record ? url('/product/' . ($record->hero_section['slug'] ?? '')) : ''
|
||||
),
|
||||
FileUpload::make('seo.banner')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('product-pages/seo')
|
||||
->label('Social Media Banner'),
|
||||
Textarea::make('seo.description')
|
||||
->label('SEO Description')
|
||||
->placeholder('Will use Basic Information description if left empty')
|
||||
->rows(3),
|
||||
/*
|
||||
==================================================================================
|
||||
This only work if you inside themes/default/views/layouts/app.blade.php
|
||||
|
||||
{{--
|
||||
@isset($keywords)
|
||||
<meta name="keywords" content="{{ $keywords }}">
|
||||
@endisset
|
||||
--}}
|
||||
|
||||
delete {{-- and --}}
|
||||
|
||||
and also in extension/Others/ProductPageBuilder/resources/views/show.blade.php
|
||||
|
||||
{{--
|
||||
<x-slot name="keywords">{{ is_array($page->seo['keywords'] ?? null) ? implode(', ', $page->seo['keywords']) : ($page->seo['keywords'] ?? '') }}</x-slot>
|
||||
--}}
|
||||
|
||||
delete {{-- and --}}
|
||||
|
||||
TagsInput::make('seo.keywords')
|
||||
->label('SEO Keywords')
|
||||
->placeholder('Add keywords and press enter')
|
||||
->separator(','),
|
||||
|
||||
==================================================================================
|
||||
*/
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Features')
|
||||
->schema([
|
||||
Repeater::make('features')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Select::make('icon_type')
|
||||
->label('Icon Type')
|
||||
->options([
|
||||
'image' => 'Image',
|
||||
'svg' => 'SVG',
|
||||
])
|
||||
->default('image')
|
||||
->required()
|
||||
->reactive()
|
||||
->formatStateUsing(function ($state, $record) {
|
||||
if (!empty($state)) {
|
||||
return $state;
|
||||
}
|
||||
if (!empty($record['icon_svg'])) {
|
||||
return 'svg';
|
||||
}
|
||||
if (!empty($record['icon'])) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
return 'image';
|
||||
}),
|
||||
Textarea::make('icon_svg')
|
||||
->label('SVG Code')
|
||||
->rows(5)
|
||||
->placeholder('<svg ...>...</svg>')
|
||||
->visible(fn ($get) => $get('icon_type') === 'svg'),
|
||||
FileUpload::make('icon')
|
||||
->directory('product-pages/features')
|
||||
->visible(fn ($get) => $get('icon_type') === 'image'),
|
||||
TextInput::make('title'),
|
||||
// ->required(),
|
||||
RichEditor::make('description'),
|
||||
// ->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Game Preview')
|
||||
->schema([
|
||||
Repeater::make('game_preview')
|
||||
->maxItems(1)
|
||||
->schema([
|
||||
TextInput::make('title'),
|
||||
// ->required(),
|
||||
RichEditor::make('description'),
|
||||
// ->required(),
|
||||
TextInput::make('youtube_embed')
|
||||
// ->required()
|
||||
->placeholder('e.g. https://www.youtube.com/embed/xxxxx'),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Game Panel')
|
||||
->schema([
|
||||
Repeater::make('game_panel')
|
||||
->maxItems(1)
|
||||
->schema([
|
||||
TextInput::make('title'),
|
||||
// ->required(),
|
||||
RichEditor::make('description'),
|
||||
// ->required(),
|
||||
FileUpload::make('gallery')
|
||||
// ->required()
|
||||
->multiple()
|
||||
->directory('product-pages/gallery'),
|
||||
TagsInput::make('features')
|
||||
->label('Panel Features')
|
||||
->placeholder('Add feature and press enter')
|
||||
->separator(','),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('FAQs')
|
||||
->schema([
|
||||
Repeater::make('faqs')
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('question'),
|
||||
// ->required(),
|
||||
RichEditor::make('answer'),
|
||||
// ->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('Last Updated')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('hero_section.title')
|
||||
->label('Title'),
|
||||
Tables\Columns\TextColumn::make('hero_section.slug')
|
||||
->label('Slug'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\ViewAction::make()
|
||||
->url(fn ($record) => "/games/{$record->hero_section['slug']}")
|
||||
->openUrlInNewTab(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
/*
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()
|
||||
->label('Create Product Page')
|
||||
])*/
|
||||
->defaultSort('updated_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListProductPages::route('/'),
|
||||
'create' => Pages\CreateProductPage::route('/create'),
|
||||
'edit' => Pages\EditProductPage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Models\ProductPageSettings;
|
||||
|
||||
class ProductPageController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$pages = ProductPageSettings::all();
|
||||
|
||||
return view('product_page_builder::index', compact('pages'));
|
||||
}
|
||||
|
||||
public function show($slug)
|
||||
{
|
||||
$page = ProductPageSettings::whereJsonContains('hero_section->slug', $slug)->firstOrFail();
|
||||
|
||||
return view('product_page_builder::show', compact('page'));
|
||||
}
|
||||
}
|
||||
70
Disabled/ProductPageBuilder/Models/ProductPageSettings.php
Normal file
70
Disabled/ProductPageBuilder/Models/ProductPageSettings.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Models;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProductPageSettings extends Model
|
||||
{
|
||||
protected $table = 'ext_product_page_builder';
|
||||
|
||||
protected $fillable = [
|
||||
'hero_section',
|
||||
'features',
|
||||
'pricing_tables',
|
||||
'seo',
|
||||
'category_id',
|
||||
'game_preview',
|
||||
'game_panel',
|
||||
'faqs',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'hero_section' => 'array',
|
||||
'features' => 'array',
|
||||
'pricing_tables' => 'array',
|
||||
'seo' => 'array',
|
||||
'game_preview' => 'array',
|
||||
'game_panel' => 'array',
|
||||
'faqs' => 'array',
|
||||
];
|
||||
|
||||
protected function heroSection(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn ($value) => json_decode($value, true) ?? [],
|
||||
set: fn ($value) => json_encode($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function getMinPriceDetailsAttribute()
|
||||
{
|
||||
if (!$this->category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$minPrice = $this->category->products()
|
||||
->join('plans', function ($join) {
|
||||
$join->on('plans.priceable_id', '=', 'products.id')
|
||||
->where('plans.priceable_type', '=', 'App\\Models\\Product');
|
||||
})
|
||||
->join('prices', function ($join) {
|
||||
$join->on('prices.plan_id', '=', 'plans.id');
|
||||
})
|
||||
->select('prices.price', 'prices.currency_code')
|
||||
->orderBy('prices.price', 'asc')
|
||||
->first();
|
||||
|
||||
return $minPrice ? [
|
||||
'price' => $minPrice->price,
|
||||
'currency_code' => $minPrice->currency_code,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
89
Disabled/ProductPageBuilder/ProductPageBuilder.php
Normal file
89
Disabled/ProductPageBuilder/ProductPageBuilder.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Models\ProductPageSettings;
|
||||
|
||||
class ProductPageBuilder extends Extension
|
||||
{
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'notice',
|
||||
'type' => 'placeholder',
|
||||
'label' => 'Use this extension to customize your product pages with custom sections.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function enabled()
|
||||
{
|
||||
Artisan::call('migrate', [
|
||||
'--path' => 'extensions/Others/ProductPageBuilder/database/migrations',
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
// Add category_id column if it doesn't exist
|
||||
if (!$this->columnExists('ext_product_page_builder', 'category_id')) {
|
||||
DB::statement('ALTER TABLE ext_product_page_builder ADD COLUMN category_id bigint unsigned NULL');
|
||||
DB::statement('ALTER TABLE ext_product_page_builder ADD CONSTRAINT product_page_builder_category_id_foreign FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL');
|
||||
}
|
||||
|
||||
// Register views
|
||||
View::addNamespace('product_page_builder', __DIR__ . '/resources/views');
|
||||
|
||||
// Register routes
|
||||
Route::middleware('web')->group(__DIR__ . '/routes/web.php');
|
||||
|
||||
// Register Filament resources
|
||||
if (class_exists('\Filament\Facades\Filament')) {
|
||||
Filament::registerResources([
|
||||
Admin\Resources\ProductPageResource::class,
|
||||
]);
|
||||
}
|
||||
|
||||
Event::listen('product.page', function ($product) {
|
||||
$settings = ProductPageSettings::first();
|
||||
if (!$settings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'view' => view('product_page_builder::product', [
|
||||
'settings' => $settings,
|
||||
'product' => $product,
|
||||
]),
|
||||
];
|
||||
});
|
||||
|
||||
Event::listen('navigation', function () {
|
||||
if (ProductPageSettings::count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => 'Games',
|
||||
'route' => 'product-page.index',
|
||||
// 'icon' => '',
|
||||
'separator' => true,
|
||||
'children' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function columnExists($table, $column)
|
||||
{
|
||||
return DB::getSchemaBuilder()->hasColumn($table, $column);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ProductPageBuilderServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
$this->loadRoutesFrom(__DIR__ . '/routes/web.php');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
|
||||
$this->loadViewsFrom(__DIR__ . '/resources/views', 'product-page-builder');
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
10
Disabled/ProductPageBuilder/composer.json
Normal file
10
Disabled/ProductPageBuilder/composer.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "paymenter/product-page-builder",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Paymenter\\Extensions\\Others\\ProductPageBuilder\\ProductPageBuilderServiceProvider"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('ext_product_page_builder');
|
||||
|
||||
Schema::create('ext_product_page_builder', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('hero_section')->nullable();
|
||||
$table->json('features')->nullable();
|
||||
$table->json('seo')->nullable();
|
||||
$table->json('game_preview')->nullable();
|
||||
$table->json('game_panel')->nullable();
|
||||
$table->json('faqs')->nullable();
|
||||
$table->foreignId('category_id')->nullable()->constrained('categories')->onDelete('set null');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('ext_product_page_builder');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
@props([
|
||||
'options' => [],
|
||||
'placeholder' => 'Select option'
|
||||
])
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
selectOpen: false,
|
||||
selectedValue: @entangle($attributes->wire('model')),
|
||||
selectableItems: {{ json_encode($options) }},
|
||||
selectableItemActive: null,
|
||||
selectId: $id('select'),
|
||||
selectDropdownPosition: 'bottom',
|
||||
|
||||
init() {
|
||||
this.$watch('selectOpen', () => {
|
||||
if (this.selectOpen) {
|
||||
this.selectableItemActive = this.selectableItems.find(item => item.value === this.selectedValue)
|
||||
|| this.selectableItems[0];
|
||||
this.$nextTick(() => this.selectScrollToActiveItem());
|
||||
}
|
||||
this.$nextTick(() => this.selectPositionUpdate());
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => this.selectPositionUpdate());
|
||||
},
|
||||
|
||||
selectableItemIsActive(item) {
|
||||
return this.selectableItemActive && this.selectableItemActive.value === item.value;
|
||||
},
|
||||
|
||||
selectableItemActiveNext() {
|
||||
let index = this.selectableItems.indexOf(this.selectableItemActive);
|
||||
if (index < this.selectableItems.length - 1) {
|
||||
this.selectableItemActive = this.selectableItems[index + 1];
|
||||
this.selectScrollToActiveItem();
|
||||
}
|
||||
},
|
||||
|
||||
selectableItemActivePrevious() {
|
||||
let index = this.selectableItems.indexOf(this.selectableItemActive);
|
||||
if (index > 0) {
|
||||
this.selectableItemActive = this.selectableItems[index - 1];
|
||||
this.selectScrollToActiveItem();
|
||||
}
|
||||
},
|
||||
|
||||
selectScrollToActiveItem() {
|
||||
if (this.selectableItemActive) {
|
||||
const activeElement = document.getElementById(this.selectableItemActive.value + '-' + this.selectId);
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectPositionUpdate() {
|
||||
if (!this.$refs.selectButton || !this.$refs.selectableItemsList) return;
|
||||
|
||||
const selectDropdownBottomPos = this.$refs.selectButton.getBoundingClientRect().top +
|
||||
this.$refs.selectButton.offsetHeight +
|
||||
this.$refs.selectableItemsList.offsetHeight;
|
||||
|
||||
this.selectDropdownPosition = window.innerHeight < selectDropdownBottomPos ? 'top' : 'bottom';
|
||||
}
|
||||
}"
|
||||
@keydown.escape="selectOpen = false"
|
||||
@keydown.down.prevent="if(selectOpen) { selectableItemActiveNext() } else { selectOpen = true }"
|
||||
@keydown.up.prevent="if(selectOpen) { selectableItemActivePrevious() } else { selectOpen = true }"
|
||||
@keydown.enter.prevent="selectedValue = selectableItemActive.value; selectOpen = false"
|
||||
class="relative w-full"
|
||||
>
|
||||
<button
|
||||
x-ref="selectButton"
|
||||
@click="selectOpen = !selectOpen"
|
||||
:class="{ 'ring-2 ring-offset-2 ring-neutral-800': selectOpen }"
|
||||
class="relative w-full min-h-[38px] py-2 pl-3 pr-10 text-left bg-background-secondary border border-neutral rounded-md shadow-sm cursor-pointer text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-800"
|
||||
type="button"
|
||||
>
|
||||
<span x-text="selectableItems.find(item => item.value === selectedValue)?.label ?? '{{ $placeholder }}'" class="block truncate"></span>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<x-ri-expand-up-down-line class="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
x-show="selectOpen"
|
||||
x-ref="selectableItemsList"
|
||||
@click.away="selectOpen = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
:class="{ 'bottom-full mb-1': selectDropdownPosition === 'top', 'top-full mt-1': selectDropdownPosition === 'bottom' }"
|
||||
class="absolute z-50 w-full py-1 overflow-auto bg-background-secondary border border-neutral rounded-lg shadow-lg max-h-60 focus:outline-none text-sm"
|
||||
x-cloak
|
||||
>
|
||||
<template x-for="item in selectableItems" :key="item.value">
|
||||
<li
|
||||
:id="item.value + '-' + selectId"
|
||||
@click="selectedValue = item.value; selectOpen = false"
|
||||
@mousemove="selectableItemActive = item"
|
||||
:class="{ 'bg-neutral-700': selectableItemIsActive(item) }"
|
||||
class="relative py-2 pl-8 pr-4 cursor-pointer select-none hover:bg-neutral-700"
|
||||
>
|
||||
<span
|
||||
class="block truncate"
|
||||
:class="{ 'font-semibold': item.value === selectedValue }"
|
||||
x-text="item.label"
|
||||
></span>
|
||||
<span
|
||||
x-show="item.value === selectedValue"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-2 text-neutral-400"
|
||||
>
|
||||
<x-ri-check-fill class="size-4" />
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
47
Disabled/ProductPageBuilder/resources/views/index.blade.php
Normal file
47
Disabled/ProductPageBuilder/resources/views/index.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="title">Games</x-slot>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
@foreach($pages as $page)
|
||||
<div class="group relative bg-background-secondary hover:bg-background-secondary/80 border border-neutral rounded-lg overflow-hidden transition-all duration-300 hover:shadow-xl p-4">
|
||||
@if(isset($page->hero_section['background']))
|
||||
<div class="w-full h-36 overflow-hidden rounded-lg mb-4">
|
||||
<img src="{{ Storage::url($page->hero_section['background']) }}"
|
||||
alt="{{ $page->hero_section['title'] }}"
|
||||
class="w-full h-full object-cover">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative">
|
||||
<h2 class="text-2xl font-bold text-base mb-4">{{ $page->hero_section['title'] }}</h2>
|
||||
@if(isset($page->hero_section['description']))
|
||||
<div class="prose dark:prose-invert line-clamp-2 mb-4">
|
||||
{!! strip_tags($page->hero_section['description']) !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
@if($page->min_price_details)
|
||||
<div class="text-base text-white font-semibold">
|
||||
@php
|
||||
$currency = \App\Models\Currency::firstWhere('code', $page->min_price_details['currency_code']);
|
||||
$formattedPrice = $currency ?
|
||||
($currency->prefix ?? '') . number_format($page->min_price_details['price'], 2) . ($currency->suffix ?? '') :
|
||||
number_format($page->min_price_details['price'], 2);
|
||||
@endphp
|
||||
Starting from {{ $formattedPrice }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<a href="{{ route('product-page.show', $page->hero_section['slug']) }}" wire:navigate class="flex items-center gap-2 justify-center bg-primary text-white text-sm font-semibold hover:bg-primary/80 py-2.5 lg:py-2 px-4.5 rounded-md w-full duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50">
|
||||
View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-app-layout>
|
||||
273
Disabled/ProductPageBuilder/resources/views/show.blade.php
Normal file
273
Disabled/ProductPageBuilder/resources/views/show.blade.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="title">{{ $page->seo['title'] ?? $page->hero_section['title'] }}</x-slot>
|
||||
<x-slot name="description">{!! $page->seo['description'] ?? strip_tags($page->hero_section['description']) !!}</x-slot>
|
||||
{{--
|
||||
<x-slot name="keywords">{{ is_array($page->seo['keywords'] ?? null) ? implode(', ', $page->seo['keywords']) : ($page->seo['keywords'] ?? '') }}</x-slot>
|
||||
--}}
|
||||
@if(isset($page->seo['banner']))
|
||||
<x-slot name="image">{{ Storage::url($page->seo['banner']) }}</x-slot>
|
||||
@endif
|
||||
|
||||
<!-- Main content -->
|
||||
@if(isset($page->hero_section))
|
||||
<div class="relative bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 rounded-lg flex items-center overflow-hidden">
|
||||
@if(isset($page->hero_section['background']))
|
||||
<div class="absolute inset-0 transform">
|
||||
<img src="{{ Storage::url($page->hero_section['background']) }}"
|
||||
alt="{{ $page->hero_section['title'] }}"
|
||||
class="h-full w-full object-cover">
|
||||
<div class="absolute inset-0 bg-background-secondary/50 dark:bg-background-secondary/60"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-28">
|
||||
<h1 class="text-4xl lg:text-5xl font-extrabold text-base mb-4 leading-tight">
|
||||
{{ $page->hero_section['title'] }}
|
||||
</h1>
|
||||
<div class="text-xl prose dark:prose-invert max-w-5xl">
|
||||
{!! $page->hero_section['description'] !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($page->category)
|
||||
<div class="py-24">
|
||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach($page->category->products()->with(['plans.prices'])->get() as $product)
|
||||
@php
|
||||
$lowestPlan = $product->plans
|
||||
->flatMap(function($plan) {
|
||||
return $plan->prices->map(function($price) use ($plan) {
|
||||
return ['plan' => $plan, 'price' => $price];
|
||||
});
|
||||
})
|
||||
->sortBy('price.price')
|
||||
->first();
|
||||
@endphp
|
||||
<div class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 lg:p-6 rounded-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
|
||||
{{-- Prdocut Image Banner
|
||||
@if($product->image)
|
||||
<div class="mb-4 rounded-lg overflow-hidden">
|
||||
<img src="{{ Storage::url($product->image) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-48 object-cover">
|
||||
</div>
|
||||
@endif
|
||||
--}}
|
||||
{{-- Product Image Icon --}}
|
||||
@if($product->image)
|
||||
<div class="mb-4 rounded-lg overflow-hidden">
|
||||
<img src="{{ Storage::url($product->image) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-24 object-contain">
|
||||
</div>
|
||||
@endif
|
||||
<h3 class="text-3xl font-bold text-base my-4 text-center">
|
||||
{{ $product->name }}
|
||||
</h3>
|
||||
<div class="prose dark:prose-invert text-base mb-4">{!! $product->description !!}</div>
|
||||
@if($lowestPlan)
|
||||
@php
|
||||
$currency = \App\Models\Currency::firstWhere('code', $lowestPlan['price']->currency_code);
|
||||
$formattedPrice = $currency ?
|
||||
($currency->prefix ?? '') . number_format($lowestPlan['price']->price, 2) . ($currency->suffix ?? '') :
|
||||
number_format($lowestPlan['price']->price, 2);
|
||||
@endphp
|
||||
<div class="prose dark:prose-invert mb-4 text-center flex items-center gap-2 justify-center">
|
||||
<span class="font-medium text-lg">Starting from</span>
|
||||
<span class="text-base text-2xl font-bold">{{ $formattedPrice }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<a href="{{ route('products.show', ['category' => $page->category->slug, 'product' => $product->slug]) }}"
|
||||
wire:navigate
|
||||
class="flex items-center gap-2 justify-center bg-primary text-base text-sm font-semibold hover:bg-primary/80 py-2.5 lg:py-2 px-4.5 rounded-md w-full duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50">
|
||||
{{ __('general.view') }}
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$activeFeatures = collect($page->features ?? [])->filter(fn($feature) => $feature['is_active'] ?? true);
|
||||
@endphp
|
||||
|
||||
@if($activeFeatures->isNotEmpty())
|
||||
<div class="py-12 lg:py-24">
|
||||
<h2 class="text-3xl font-bold text-center text-base mb-12">Features</h2>
|
||||
<div class="grid grid-cols-1 gap-12 lg:grid-cols-3">
|
||||
@foreach($activeFeatures as $feature)
|
||||
<div class="relative group">
|
||||
<div class="relative bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 lg:p-6 rounded-lg shadow-xl">
|
||||
@if(!empty($feature['icon_svg']))
|
||||
<div class="h-12 w-12 mb-4 flex items-center justify-center">
|
||||
{!! $feature['icon_svg'] !!}
|
||||
</div>
|
||||
@elseif(isset($feature['icon']))
|
||||
<div class="h-12 w-12 mb-4">
|
||||
<img src="{{ Storage::url($feature['icon']) }}"
|
||||
alt="{{ $feature['title'] }}"
|
||||
class="h-full w-full object-contain">
|
||||
</div>
|
||||
@endif
|
||||
<h3 class="text-xl font-semibold text-base mb-3">{{ $feature['title'] }}</h3>
|
||||
<div class="prose prose dark:prose-invert">{!! $feature['description'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if(!empty($page->game_preview))
|
||||
@foreach($page->game_preview as $preview)
|
||||
@if($preview['is_active'] ?? true)
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center py-12 lg:py-24">
|
||||
<div class="max-w-xl me-auto lg:text-left text-center">
|
||||
<h3 class="text-3xl font-bold text-base mb-6">{{ $preview['title'] }}</h3>
|
||||
<div class="prose dark:prose-invert text-base">{!! $preview['description'] !!}</div>
|
||||
</div>
|
||||
@if(isset($preview['youtube_embed']))
|
||||
<div class="h-48 lg:h-96 rounded-lg overflow-hidden">
|
||||
<iframe src="{{ $preview['youtube_embed'] }}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="w-full h-full"></iframe>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@php
|
||||
$activeGamePanels = collect($page->game_panel ?? [])->filter(fn($panel) => $panel['is_active'] ?? true);
|
||||
$activeGamePreviews = collect($page->game_preview ?? [])->filter(fn($preview) => $preview['is_active'] ?? true);
|
||||
$swapOrder = $activeGamePanels->isNotEmpty() && $activeGamePreviews->isNotEmpty();
|
||||
@endphp
|
||||
|
||||
@if(!empty($page->game_panel))
|
||||
@foreach($page->game_panel as $panel)
|
||||
@if($panel['is_active'] ?? true)
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center py-12 lg:py-24">
|
||||
<div class="max-w-xl ms-auto lg:text-left text-center {{ $swapOrder ? 'order-2' : 'order-1' }}">
|
||||
<h3 class="text-3xl font-bold text-base mb-6">{{ $panel['title'] }}</h3>
|
||||
<div class="prose dark:prose-invert text-base">{!! $panel['description'] !!}</div>
|
||||
@php
|
||||
$features = is_array($panel['features'] ?? null)
|
||||
? $panel['features']
|
||||
: (empty($panel['features']) ? [] : explode(',', $panel['features']));
|
||||
@endphp
|
||||
@if(!empty($features))
|
||||
<div class="flex flex-wrap justify-center lg:justify-start gap-2 my-4">
|
||||
@foreach($features as $feature)
|
||||
<span class="flex items-center gap-1 bg-background-secondary border border-neutral text-base px-3 py-1 rounded-lg text-sm font-semibold">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-primary">
|
||||
<path d="M9.9997 15.1709L19.1921 5.97852L20.6063 7.39273L9.9997 17.9993L3.63574 11.6354L5.04996 10.2212L9.9997 15.1709Z"></path>
|
||||
</svg>
|
||||
{{ trim($feature) }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if(!empty($panel['gallery']))
|
||||
<div
|
||||
x-data="{
|
||||
slides: {{ json_encode($panel['gallery']) }},
|
||||
current: 0,
|
||||
goTo(idx) { this.current = idx },
|
||||
next() { this.current = (this.current + 1) % this.slides.length },
|
||||
prev() { this.current = (this.current - 1 + this.slides.length) % this.slides.length }
|
||||
}"
|
||||
class="relative rounded-lg overflow-hidden shadow-lg"
|
||||
>
|
||||
<div class="relative h-48 md:h-96">
|
||||
<template x-for="(image, idx) in slides" :key="idx">
|
||||
<div
|
||||
x-show="current === idx"
|
||||
x-transition:enter="transition-opacity duration-700"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity duration-700"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<img :src="'{{ Storage::url('') }}' + image" class="object-cover w-full h-full" alt="Slide" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Slider indicators -->
|
||||
<div class="flex absolute bottom-5 left-1/2 z-30 -translate-x-1/2 space-x-2">
|
||||
<template x-for="(image, idx) in slides" :key="idx">
|
||||
<button
|
||||
type="button"
|
||||
:class="{'bg-primary': current === idx, 'bg-gray-300': current !== idx}"
|
||||
class="w-4 h-1 rounded-lg hover:bg-gray-400 focus:outline-none transition"
|
||||
@click="goTo(idx)"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Slider controls -->
|
||||
<button type="button"
|
||||
class="flex absolute top-1/2 -translate-y-1/2 left-3 z-40 items-center justify-center w-8 h-8 bg-gray-200/50 rounded-full hover:bg-gray-300 focus:outline-none transition cursor-pointer"
|
||||
@click="prev"
|
||||
>
|
||||
<svg class="w-4 h-4 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex absolute top-1/2 -translate-y-1/2 right-3 z-40 items-center justify-center w-8 h-8 bg-gray-200/50 rounded-full hover:bg-gray-300 focus:outline-none transition cursor-pointer"
|
||||
@click="next"
|
||||
>
|
||||
<svg class="w-4 h-4 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@php
|
||||
$activeFaqs = collect($page->faqs ?? [])->filter(fn($faq) => $faq['is_active'] ?? true);
|
||||
@endphp
|
||||
|
||||
@if($activeFaqs->isNotEmpty())
|
||||
<div class="max-w-7xl mx-auto py-12 lg:py-24">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-3xl font-bold text-center text-base mb-12">Frequently Asked Questions</h2>
|
||||
<div class="space-y-8">
|
||||
@foreach($activeFaqs as $faq)
|
||||
<div class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 rounded-lg" x-data="{ open: false }">
|
||||
<dt>
|
||||
<button @click="open = !open" class="w-full flex justify-between items-center text-left cursor-pointer">
|
||||
<span class="text-base lg:text-lg font-semibold text-base">{{ $faq['question'] }}</span>
|
||||
<span class="ml-6 flex-shrink-0">
|
||||
<svg class="h-4 w-4 transform transition-transform duration-200" :class="{'rotate-180': open}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</dt>
|
||||
<dd class="mt-4" x-show="open" x-transition>
|
||||
<div class="prose prose-sm max-w-none prose dark:prose-invert">
|
||||
{!! $faq['answer'] !!}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</x-app-layout>
|
||||
7
Disabled/ProductPageBuilder/routes/web.php
Normal file
7
Disabled/ProductPageBuilder/routes/web.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Http\Controllers\ProductPageController;
|
||||
|
||||
Route::get('/games', [ProductPageController::class, 'index'])->name('product-page.index');
|
||||
Route::get('/games/{slug}', [ProductPageController::class, 'show'])->name('product-page.show');
|
||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# Stage 0:
|
||||
# Build the assets that are needed for the frontend. This build stage is then discarded
|
||||
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
|
||||
# level distribution of Paymenter.
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
RUN npm install \
|
||||
&& npm run build
|
||||
|
||||
# Stage 1:
|
||||
# Build the actual container with all of the needed PHP dependencies that will run the application.
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
COPY --from=0 /app/public/default /app/public/default
|
||||
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev autoconf make g++ gcc libc-dev linux-headers gmp-dev \
|
||||
&& docker-php-ext-configure zip \
|
||||
&& docker-php-ext-install bcmath gd pdo_mysql zip intl sockets gmp \
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
&& apk del autoconf make g++ gcc libc-dev \
|
||||
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||
&& cp .env.example .env \
|
||||
&& chmod 777 -R bootstrap storage/* \
|
||||
&& composer install --no-dev --optimize-autoloader \
|
||||
&& rm -rf .env bootstrap/cache/*.php \
|
||||
&& chown -R nginx:nginx .
|
||||
|
||||
RUN rm /usr/local/etc/php-fpm.conf \
|
||||
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
|
||||
&& mkdir -p /var/run/php /var/run/nginx
|
||||
|
||||
COPY .github/docker/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
|
||||
COPY .github/docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
EXPOSE 80
|
||||
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
36
Dockerfile.dev
Normal file
36
Dockerfile.dev
Normal file
@@ -0,0 +1,36 @@
|
||||
# Use PHP 8.2 FPM Debian as base image
|
||||
FROM serversideup/php:8.3-fpm-nginx AS base
|
||||
|
||||
# Switch to root so we can do root things
|
||||
USER root
|
||||
|
||||
# Install the intl extension with root permissions
|
||||
RUN install-php-extensions pdo_mysql mbstring exif pcntl bcmath gd intl zip xml curl redis
|
||||
|
||||
# Install JavaScript dependencies
|
||||
ARG NODE_VERSION=20.18.0
|
||||
ENV PATH=/usr/local/node/bin:$PATH
|
||||
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
|
||||
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
|
||||
corepack enable && \
|
||||
rm -rf /tmp/node-build-master
|
||||
|
||||
# Drop back to our unprivileged user
|
||||
USER www-data
|
||||
FROM base
|
||||
|
||||
# # Get latest Composer
|
||||
# COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# # Set working directory
|
||||
# WORKDIR /var/www/html
|
||||
|
||||
COPY --chown=www-data:www-data ./ /var/www/html
|
||||
|
||||
ENV PHP_OPCACHE_ENABLE=1
|
||||
|
||||
RUN /usr/bin/composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
RUN npm install --immutable && \
|
||||
npm run build && \
|
||||
rm -rf node_modules
|
||||
44
app/Admin/Actions/AuditAction.php
Normal file
44
app/Admin/Actions/AuditAction.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Actions;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AuditAction extends Action
|
||||
{
|
||||
public $auditChildren = [];
|
||||
|
||||
public function auditChildren($children): self
|
||||
{
|
||||
$this->auditChildren = $children;
|
||||
|
||||
// Array of children of the model to also show audits for
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'audits';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this
|
||||
->label('Audits')
|
||||
->color('gray')
|
||||
->slideOver()
|
||||
->modalContent(fn (Model $record): View => view(
|
||||
'admin.actions.audits',
|
||||
['record' => $record, 'children' => $this->auditChildren]
|
||||
))
|
||||
->modalHeading('test')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelAction(false);
|
||||
}
|
||||
}
|
||||
18
app/Admin/Clusters/Services.php
Normal file
18
app/Admin/Clusters/Services.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Clusters;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
|
||||
class Services extends Cluster
|
||||
{
|
||||
protected static ?string $navigationIcon = 'ri-archive-stack-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-archive-stack-fill';
|
||||
|
||||
public static ?string $navigationGroup = 'Administration';
|
||||
|
||||
protected static ?int $navigationSort = 0;
|
||||
|
||||
protected static ?string $slug = 'services';
|
||||
}
|
||||
29
app/Admin/Components/UserComponent.php
Normal file
29
app/Admin/Components/UserComponent.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Components;
|
||||
|
||||
use App\Admin\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\Select;
|
||||
|
||||
class UserComponent extends Select
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return parent::make($name)
|
||||
->label('User')
|
||||
->relationship('user', 'id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->getOptionLabelFromRecordUsing(fn ($record) => $record->name . ' (' . $record->email . ')')
|
||||
->getSearchResultsUsing(fn (string $search): array => User::where('first_name', 'like', "%$search%")
|
||||
->orWhere('last_name', 'like', "%$search%")
|
||||
->limit(50)
|
||||
->get()
|
||||
->mapWithKeys(fn ($user) => [$user->id => $user->name . ' (' . $user->email . ')'])
|
||||
->toArray())
|
||||
->hint(fn ($get) => $get('user_id') ? new \Illuminate\Support\HtmlString('<a href="' . UserResource::getUrl('edit', ['record' => $get('user_id')]) . '" target="_blank">Go to User</a>') : null)
|
||||
->live()
|
||||
->required();
|
||||
}
|
||||
}
|
||||
12
app/Admin/Pages/Dashboard.php
Normal file
12
app/Admin/Pages/Dashboard.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Pages;
|
||||
|
||||
use Filament\Pages\Dashboard as BaseDashboard;
|
||||
|
||||
class Dashboard extends BaseDashboard
|
||||
{
|
||||
protected static ?string $navigationIcon = 'ri-function-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-function-fill';
|
||||
}
|
||||
173
app/Admin/Pages/Settings.php
Normal file
173
app/Admin/Pages/Settings.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Pages;
|
||||
|
||||
use App\Classes\FilamentInput;
|
||||
use App\Classes\Settings as ClassesSettings;
|
||||
use App\Models\Setting;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class Settings extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static ?string $navigationGroup = 'System';
|
||||
|
||||
protected static ?string $title = 'Settings';
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-settings-3-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-settings-3-fill';
|
||||
|
||||
protected static string $view = 'admin.pages.settings';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$setting_values = [];
|
||||
foreach (\App\Classes\Settings::settings() as $group => $settings) {
|
||||
foreach ($settings as $setting) {
|
||||
$setting_values[$setting['name']] = config("settings.{$setting['name']}", $setting['default'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
$this->form->fill($setting_values);
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
$tabs = [];
|
||||
|
||||
foreach (ClassesSettings::settings() as $key => $categories) {
|
||||
$tab = Tabs\Tab::make($key)
|
||||
->label(ucwords(str_replace('-', ' ', $key)))
|
||||
->schema(function () use ($categories, $key) {
|
||||
$inputs = [];
|
||||
foreach ($categories as $setting) {
|
||||
$inputs[] = FilamentInput::convert($setting);
|
||||
}
|
||||
if ($key === 'theme') {
|
||||
// Add a reset colors button if there are color settings
|
||||
array_unshift($inputs, Actions::make([
|
||||
Actions\Action::make('resetColors')
|
||||
->label('Reset Colors')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(fn () => $this->resetColors()),
|
||||
]));
|
||||
// Wrap the first two inputs in a group if there are more than one
|
||||
if (count($inputs) > 1) {
|
||||
$inputs[0] = Group::make([
|
||||
$inputs[1]->columnSpan(3),
|
||||
$inputs[0],
|
||||
])->columns(4)->columnSpanFull();
|
||||
unset($inputs[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return $inputs;
|
||||
});
|
||||
|
||||
$tabs[] = $tab;
|
||||
}
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs($tabs)
|
||||
->persistTabInQueryString(),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
Gate::authorize('has-permission', 'admin.settings.update');
|
||||
|
||||
$data = $this->form->getState();
|
||||
|
||||
$settings = Setting::where('settingable_type', null)
|
||||
->whereIn('key', array_keys($data))
|
||||
->get()
|
||||
->keyBy('key');
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
// Get only the settings that have changed
|
||||
$avSetting = (object) collect(\App\Classes\Settings::settings())->flatten(1)->firstWhere('name', $key);
|
||||
$avSetting->value = $settings[$key]->value ?? $avSetting->default ?? null;
|
||||
|
||||
if ($value !== $avSetting->value || (($avSetting->database_type ?? 'string') === 'boolean' && (bool) $value !== (bool) $avSetting->value)) {
|
||||
if ($setting = $settings[$key] ?? null) {
|
||||
$setting->update([
|
||||
'value' => $value,
|
||||
'type' => $avSetting->database_type ?? 'string',
|
||||
]);
|
||||
} else {
|
||||
Setting::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'settingable_type' => null,
|
||||
'type' => $avSetting->database_type ?? 'string',
|
||||
'encrypted' => $avSetting->encrypted ?? false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Saved successfully!')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function resetColors(): void
|
||||
{
|
||||
Gate::authorize('has-permission', 'admin.settings.update');
|
||||
|
||||
$colorSettings = [];
|
||||
foreach (\App\Classes\Settings::settings() as $group => $settings) {
|
||||
foreach ($settings as $setting) {
|
||||
if (($setting['type'] ?? '') === 'color') {
|
||||
$colorSettings[$setting['name']] = $setting['default'] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$currentData = $this->form->getState();
|
||||
foreach ($colorSettings as $key => $defaultValue) {
|
||||
$currentData[$key] = $defaultValue;
|
||||
}
|
||||
$this->form->fill($currentData);
|
||||
|
||||
Notification::make()
|
||||
->title('Colors has been reset!')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public static function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('save')
|
||||
->submit('save')
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user && $user->hasPermission('admin.settings.view');
|
||||
}
|
||||
}
|
||||
65
app/Admin/Pages/Updates.php
Normal file
65
app/Admin/Pages/Updates.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Pages;
|
||||
|
||||
use App\Console\Commands\CheckForUpdates;
|
||||
use App\Console\Commands\Upgrade;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class Updates extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-loop-left-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-loop-left-fill';
|
||||
|
||||
protected static string $view = 'admin.pages.updates';
|
||||
|
||||
protected static ?string $navigationGroup = 'System';
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('checkUpdates')
|
||||
->action(function () {
|
||||
Artisan::call(CheckForUpdates::class);
|
||||
})
|
||||
->label('Check for updates'),
|
||||
];
|
||||
}
|
||||
|
||||
public function update(): Action
|
||||
{
|
||||
return Action::make('update')
|
||||
->requiresConfirmation()
|
||||
->action(function () {
|
||||
$output = new BufferedOutput;
|
||||
|
||||
// Check if current chdir is the root of the project
|
||||
if (getcwd() !== base_path()) {
|
||||
chdir(base_path());
|
||||
}
|
||||
|
||||
if (config('app.version') == 'beta') {
|
||||
Artisan::call(Upgrade::class, ['--no-interaction' => true, '--url' => 'https://api.paymenter.org/beta'], $output);
|
||||
} else {
|
||||
Artisan::call(Upgrade::class, ['--no-interaction' => true], $output);
|
||||
}
|
||||
$this->dispatch('update-completed', [
|
||||
'output' => $output->fetch(),
|
||||
]);
|
||||
})
|
||||
->label('Update');
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->check() && auth()->user()->hasPermission('admin.updates.update');
|
||||
}
|
||||
}
|
||||
91
app/Admin/Resources/ApiResource.php
Normal file
91
app/Admin/Resources/ApiResource.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\ApiResource\Pages;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class ApiResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-key-2-line';
|
||||
|
||||
protected static ?string $navigationGroup = 'Other';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
|
||||
$extensionApiPermissions = once(fn () => Arr::dot(
|
||||
array_merge_recursive(...Event::dispatch('api.permissions', []))
|
||||
));
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->label('API Name'),
|
||||
Forms\Components\TagsInput::make('ip_addresses')
|
||||
->label('Allowed IP Addresses')
|
||||
->placeholder('Enter IP addresses'),
|
||||
Forms\Components\Toggle::make('enabled')
|
||||
->default(true)
|
||||
->visibleOn('edit')
|
||||
->label('Active Status'),
|
||||
|
||||
Forms\Components\CheckboxList::make('permissions')
|
||||
->options(array_merge(Arr::dot(config('permissions.api')), $extensionApiPermissions))
|
||||
->columns(4)
|
||||
->bulkToggleable()
|
||||
->searchable()
|
||||
->noSearchResultsMessage('Permission could not be found'),
|
||||
|
||||
])
|
||||
->columns(1);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name'),
|
||||
Tables\Columns\TextColumn::make('ip_addresses')
|
||||
->formatStateUsing(fn ($state) => is_array($state) ? implode(', ', $state) : $state)
|
||||
->label('Allowed IP Addresses'),
|
||||
Tables\Columns\IconColumn::make('enabled')
|
||||
->boolean(),
|
||||
Tables\Columns\TextColumn::make('last_used_at')
|
||||
->dateTime()
|
||||
->label('Last Used At')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageApis::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Admin/Resources/ApiResource/Pages/ManageApis.php
Normal file
35
app/Admin/Resources/ApiResource/Pages/ManageApis.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\ApiResource\Pages;
|
||||
|
||||
use App\Admin\Resources\ApiResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ManageApis extends ManageRecords
|
||||
{
|
||||
protected static string $resource = ApiResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()->mutateFormDataUsing(function (array $data): array {
|
||||
$token = 'PAYM' . bin2hex(random_bytes(32));
|
||||
$data['token'] = hash('sha256', $token);
|
||||
$data['type'] = 'admin';
|
||||
|
||||
Notification::make()
|
||||
->title('API Token Created')
|
||||
->body(Str::markdown("Here is your new API token. Please copy it now, as it will not be shown again.\nToken: ```" . $token . '```'))
|
||||
->icon('heroicon-o-key')
|
||||
->success()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
return $data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
118
app/Admin/Resources/CategoryResource.php
Normal file
118
app/Admin/Resources/CategoryResource.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\CategoryResource\Pages;
|
||||
use App\Models\Category;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CategoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Category::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Administration';
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-folder-6-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-folder-6-fill';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
if (($get('slug') ?? '') !== Str::slug($old)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
Forms\Components\TextInput::make('slug')
|
||||
->required(),
|
||||
Forms\Components\RichEditor::make('description')
|
||||
->required(),
|
||||
Forms\Components\Select::make('parent_id')
|
||||
->relationship('parent', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->label('Parent Category')
|
||||
// Disallow having same category as it's own parent
|
||||
->disableOptionWhen(fn (string $value, ?Category $record): bool => $record && (int) $value === $record->id),
|
||||
Forms\Components\FileUpload::make('image')->label('Image')->nullable()->acceptedFileTypes(['image/*'])->columnSpanFull(),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('slug')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make()->before(function (DeleteBulkAction $action) {
|
||||
foreach ($action->getRecords() as $record) {
|
||||
if ($record->products()->exists() || $record->children()->exists()) {
|
||||
Notification::make()
|
||||
->title('Cannot delete category')
|
||||
->body('The category has products or children categories.')
|
||||
->duration(5000)
|
||||
->icon('ri-error-warning-line')
|
||||
->danger()
|
||||
->send();
|
||||
$action->cancel();
|
||||
}
|
||||
}
|
||||
}),
|
||||
]),
|
||||
])
|
||||
->defaultSort(function (Builder $query): Builder {
|
||||
return $query
|
||||
->orderBy('sort', 'asc');
|
||||
})
|
||||
->reorderable('sort');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListCategories::route('/'),
|
||||
'create' => Pages\CreateCategory::route('/create'),
|
||||
'edit' => Pages\EditCategory::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CategoryResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CategoryResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCategory extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CategoryResource::class;
|
||||
}
|
||||
31
app/Admin/Resources/CategoryResource/Pages/EditCategory.php
Normal file
31
app/Admin/Resources/CategoryResource/Pages/EditCategory.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CategoryResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CategoryResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCategory extends EditRecord
|
||||
{
|
||||
protected static string $resource = CategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()->before(function (Actions\DeleteAction $action) {
|
||||
if ($this->record->products()->exists() || $this->record->children()->exists()) {
|
||||
Notification::make()
|
||||
->title('Cannot delete category')
|
||||
->body('The category has products or children categories.')
|
||||
->duration(5000)
|
||||
->icon('ri-error-warning-line')
|
||||
->danger()
|
||||
->send();
|
||||
$action->cancel();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CategoryResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CategoryResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCategories extends ListRecords
|
||||
{
|
||||
protected static string $resource = CategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\Common\RelationManagers;
|
||||
|
||||
use App\Models\CustomProperty;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PropertiesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'properties';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Select::make('custom_property_id')->label('Custom Property')
|
||||
->required()
|
||||
->options(function ($livewire): array {
|
||||
return CustomProperty::where('model', get_class($livewire->ownerRecord))->pluck('name', 'id')->toArray();
|
||||
})->nullable(),
|
||||
Forms\Components\TextInput::make('name')->translateLabel()->nullable(),
|
||||
Forms\Components\TextInput::make('key')->translateLabel()->required(),
|
||||
Forms\Components\TextInput::make('value')->translateLabel()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name'),
|
||||
Tables\Columns\TextColumn::make('parent_property.name'),
|
||||
Tables\Columns\TextColumn::make('key'),
|
||||
Tables\Columns\TextInputColumn::make('value'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
162
app/Admin/Resources/ConfigOptionResource.php
Normal file
162
app/Admin/Resources/ConfigOptionResource.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\ConfigOptionResource\Pages;
|
||||
use App\Models\ConfigOption;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ConfigOptionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ConfigOption::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuration';
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-equalizer-2-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-equalizer-2-fill';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Tabs::make()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\Tabs\Tab::make('General')->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Enter the name of the configuration option'),
|
||||
Forms\Components\TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->maxLength(255)
|
||||
->placeholder('Enter the environment variable name'),
|
||||
Forms\Components\Select::make('type')
|
||||
->label('Type')
|
||||
->native(false)
|
||||
->required()
|
||||
->reactive()
|
||||
->options([
|
||||
'text' => 'Text',
|
||||
'number' => 'Number',
|
||||
'select' => 'Select',
|
||||
'radio' => 'Radio',
|
||||
'checkbox' => 'Checkbox',
|
||||
'slider' => 'Slider',
|
||||
]),
|
||||
Forms\Components\Checkbox::make('hidden')
|
||||
->label('Hidden'),
|
||||
Forms\Components\Checkbox::make('upgradable')
|
||||
->visible(fn (Get $get): bool => in_array($get('type'), ['select', 'radio', 'slider']))
|
||||
->label('Upgradable')
|
||||
->helperText('If enabled, this configuration option can be upgraded in the future.'),
|
||||
Forms\Components\Select::make('products')
|
||||
->label('Products')
|
||||
->relationship('products', 'name')
|
||||
->multiple()
|
||||
->preload()
|
||||
->placeholder('Select the products that this configuration option belongs to'),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('Options')
|
||||
->visible(fn (Get $get): bool => in_array($get('type'), ['select', 'radio', 'slider', 'checkbox']))
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('Options')
|
||||
->relationship('children')
|
||||
->label('Options')
|
||||
->addActionLabel('Add Option')
|
||||
->columnSpanFull()
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->cloneable()
|
||||
->reorderable()
|
||||
->orderColumn('sort')
|
||||
->columns(2)
|
||||
// When the type is checkbox only allow 1 child
|
||||
->maxItems(function (Get $get): ?int {
|
||||
if (in_array($get('type'), ['select', 'radio', 'slider'])) {
|
||||
return null; // unlimited children
|
||||
}
|
||||
|
||||
return 1; // checkbox
|
||||
})
|
||||
->minItems(1)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->live()
|
||||
->maxLength(255)
|
||||
->placeholder('Enter the name of the configuration option'),
|
||||
Forms\Components\TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Enter the environment variable name'),
|
||||
// if the type is select, radio or checkbox then allow unlimited children (otherwise only allow 1)
|
||||
ProductResource::plan()->columnSpanFull()->label('Pricing')->reorderable(false)->deleteAction(null),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('Name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('type')
|
||||
->label('Type')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('hidden')
|
||||
->badge()
|
||||
->label('Hidden')
|
||||
->sortable(),
|
||||
])
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('parent_id', null))
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->defaultSort(function (Builder $query): Builder {
|
||||
return $query
|
||||
->orderBy('sort', 'asc');
|
||||
})
|
||||
->reorderable('sort');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListConfigOptions::route('/'),
|
||||
'create' => Pages\CreateConfigOption::route('/create'),
|
||||
'edit' => Pages\EditConfigOption::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\ConfigOptionResource\Pages;
|
||||
|
||||
use App\Admin\Resources\ConfigOptionResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateConfigOption extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ConfigOptionResource::class;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\ConfigOptionResource\Pages;
|
||||
|
||||
use App\Admin\Resources\ConfigOptionResource;
|
||||
use App\Models\ConfigOption;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditConfigOption extends EditRecord
|
||||
{
|
||||
protected static string $resource = ConfigOptionResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
// Show warning when there are child config options
|
||||
Actions\DeleteAction::make('Delete')
|
||||
->requiresConfirmation()
|
||||
->modalDescription(
|
||||
fn (ConfigOption $record) => $record->children()->exists()
|
||||
? 'This config option has services connected to it. Deleting it will also delete it from the services it is associated with. Are you sure you want to delete this config option?'
|
||||
: 'Are you sure you want to delete this config option?',
|
||||
)
|
||||
->action(function () {
|
||||
$this->record->serviceConfigs()->delete();
|
||||
$this->record->delete();
|
||||
|
||||
return redirect()->to(ConfigOptionResource::getUrl());
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\ConfigOptionResource\Pages;
|
||||
|
||||
use App\Admin\Resources\ConfigOptionResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListConfigOptions extends ListRecords
|
||||
{
|
||||
protected static string $resource = ConfigOptionResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
136
app/Admin/Resources/CouponResource.php
Normal file
136
app/Admin/Resources/CouponResource.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\CouponResource\Pages;
|
||||
use App\Admin\Resources\CouponResource\RelationManagers;
|
||||
use App\Models\Coupon;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CouponResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Coupon::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-coupon-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-coupon-fill';
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuration';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('code')
|
||||
->label('Code')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(static::getModel(), 'code', ignoreRecord: true)
|
||||
->placeholder('Enter the code of the coupon'),
|
||||
|
||||
Forms\Components\TextInput::make('value')
|
||||
->label('Value')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(fn (Get $get) => $get('type') === 'percentage' ? 100 : null)
|
||||
->mask(RawJs::make(
|
||||
<<<'JS'
|
||||
$money($input, '.', '', 2)
|
||||
JS
|
||||
))
|
||||
->hidden(fn (Get $get) => $get('type') === 'free_setup')
|
||||
->suffix(fn (Get $get) => $get('type') === 'percentage' ? '%' : config('settings.default_currency'))
|
||||
->placeholder('Enter the value of the coupon'),
|
||||
|
||||
Forms\Components\Select::make('type')
|
||||
->label('Type')
|
||||
->required()
|
||||
->default('percentage')
|
||||
->live()
|
||||
->options([
|
||||
'percentage' => 'Percentage',
|
||||
'fixed' => 'Fixed amount',
|
||||
'free_setup' => 'Free setup',
|
||||
])
|
||||
->placeholder('Select the type of the coupon'),
|
||||
|
||||
Forms\Components\TextInput::make('recurring')
|
||||
->label('Recurring')
|
||||
->numeric()
|
||||
->nullable()
|
||||
->minValue(0)
|
||||
->hidden(fn (Get $get) => $get('type') === 'free_setup')
|
||||
->placeholder('How many billing cycles the discount will be applied')
|
||||
->helperText('Enter 0 to apply it to all billing cycles, 1 to apply it only to the first billing cycle, etc.'),
|
||||
|
||||
Forms\Components\TextInput::make('max_uses')
|
||||
->label('Max Uses')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->placeholder('Enter the maximum number of total uses of the coupon'),
|
||||
|
||||
Forms\Components\TextInput::make('max_uses_per_user')
|
||||
->label('Max Uses Per User')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->placeholder('Enter the maximum number of uses per user'),
|
||||
|
||||
Forms\Components\DatePicker::make('starts_at')
|
||||
->label('Starts At'),
|
||||
|
||||
Forms\Components\DatePicker::make('expires_at')
|
||||
->label('Expires At'),
|
||||
|
||||
Forms\Components\Select::make('products')
|
||||
->label('Products')
|
||||
->relationship('products', 'name')
|
||||
->multiple()
|
||||
->preload()
|
||||
->placeholder('Select the products that this coupon applies to')
|
||||
->hint('Leave empty to apply the coupon to all products'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('code')->searchable(),
|
||||
Tables\Columns\TextColumn::make('value')->searchable()->formatStateUsing(fn ($record) => $record->value . ($record->type === 'percentage' ? '%' : config('settings.default_currency'))),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\ServicesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListCoupons::route('/'),
|
||||
'create' => Pages\CreateCoupon::route('/create'),
|
||||
'edit' => Pages\EditCoupon::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Admin/Resources/CouponResource/Pages/CreateCoupon.php
Normal file
11
app/Admin/Resources/CouponResource/Pages/CreateCoupon.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CouponResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CouponResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCoupon extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CouponResource::class;
|
||||
}
|
||||
19
app/Admin/Resources/CouponResource/Pages/EditCoupon.php
Normal file
19
app/Admin/Resources/CouponResource/Pages/EditCoupon.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CouponResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CouponResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCoupon extends EditRecord
|
||||
{
|
||||
protected static string $resource = CouponResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Admin/Resources/CouponResource/Pages/ListCoupons.php
Normal file
19
app/Admin/Resources/CouponResource/Pages/ListCoupons.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CouponResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CouponResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCoupons extends ListRecords
|
||||
{
|
||||
protected static string $resource = CouponResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CouponResource\RelationManagers;
|
||||
|
||||
use App\Admin\Resources\ServiceResource;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ServicesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'services';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('id')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id'),
|
||||
Tables\Columns\TextColumn::make('order.user.name')->label('User'),
|
||||
Tables\Columns\TextColumn::make('product.name')->label('Product'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('view')
|
||||
->label('View')
|
||||
->url(fn ($record) => ServiceResource::getUrl('edit', ['record' => $record])),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
99
app/Admin/Resources/CurrencyResource.php
Normal file
99
app/Admin/Resources/CurrencyResource.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\CurrencyResource\Pages;
|
||||
use App\Models\Currency;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CurrencyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Currency::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuration';
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-money-dollar-circle-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-money-dollar-circle-fill';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('code')
|
||||
->label('Code')
|
||||
->required()
|
||||
->maxLength(3)
|
||||
->disabledOn('edit')
|
||||
->unique(static::getModel(), 'code', ignoreRecord: true)
|
||||
->placeholder('Enter the currency code'),
|
||||
Forms\Components\TextInput::make('prefix')
|
||||
->label('Prefix')
|
||||
->maxLength(10)
|
||||
->placeholder('Enter the currency prefix'),
|
||||
Forms\Components\TextInput::make('suffix')
|
||||
->label('Suffix')
|
||||
->maxLength(10)
|
||||
->placeholder('Enter the currency suffix'),
|
||||
Forms\Components\Select::make('format')
|
||||
->label('Format')
|
||||
->options([
|
||||
'1.000,00' => '1.000,00',
|
||||
'1,000.00' => '1,000.00',
|
||||
'1 000,00' => '1 000,00',
|
||||
'1 000.00' => '1 000.00',
|
||||
])
|
||||
->default('1.000,00'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('code')
|
||||
->label('Code')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('prefix')
|
||||
->label('Prefix')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('suffix')
|
||||
->label('Suffix')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListCurrencies::route('/'),
|
||||
'create' => Pages\CreateCurrency::route('/create'),
|
||||
'edit' => Pages\EditCurrency::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CurrencyResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CurrencyResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCurrency extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CurrencyResource::class;
|
||||
}
|
||||
23
app/Admin/Resources/CurrencyResource/Pages/EditCurrency.php
Normal file
23
app/Admin/Resources/CurrencyResource/Pages/EditCurrency.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CurrencyResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CurrencyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCurrency extends EditRecord
|
||||
{
|
||||
protected static string $resource = CurrencyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
if (config('settings.default_currency') == $this->record->code) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CurrencyResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CurrencyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCurrencies extends ListRecords
|
||||
{
|
||||
protected static string $resource = CurrencyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
104
app/Admin/Resources/CustomPropertyResource.php
Normal file
104
app/Admin/Resources/CustomPropertyResource.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\CustomPropertyResource\Pages;
|
||||
use App\Models\CustomProperty;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CustomPropertyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CustomProperty::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Configuration';
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-list-settings-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-list-settings-fill';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('key')->required(),
|
||||
Forms\Components\Select::make('model')->options([
|
||||
'App\Models\User' => 'User',
|
||||
])->required(),
|
||||
Forms\Components\Select::make('type')->options([
|
||||
'string' => 'Short Text',
|
||||
'text' => 'Long Text',
|
||||
'number' => 'Number',
|
||||
'select' => 'Select',
|
||||
'checkbox' => 'Checkbox',
|
||||
'radio' => 'Radio',
|
||||
'date' => 'Date',
|
||||
])->required(),
|
||||
Forms\Components\Textarea::make('description')->nullable()->columnSpanFull()->rows(2),
|
||||
Forms\Components\TagsInput::make('allowed_values')->nullable(),
|
||||
Forms\Components\TextInput::make('validation')->nullable(),
|
||||
|
||||
Forms\Components\Section::make()
|
||||
->columns([
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Toggle::make('non_editable')->default(false),
|
||||
Forms\Components\Toggle::make('required')->default(false),
|
||||
Forms\Components\Toggle::make('show_on_invoice')->default(false),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')->label('Property')
|
||||
->description(fn (CustomProperty $record): string => mb_strimwidth(($record->description ?? ''),
|
||||
0,
|
||||
75,
|
||||
'...'
|
||||
)),
|
||||
Tables\Columns\TextColumn::make('key'),
|
||||
Tables\Columns\TextColumn::make('type')->formatStateUsing(fn ($state) => str($state)->title()),
|
||||
Tables\Columns\ToggleColumn::make('non_editable'),
|
||||
Tables\Columns\ToggleColumn::make('required'),
|
||||
Tables\Columns\ToggleColumn::make('show_on_invoice'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
])->defaultGroup('model');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListCustomProperty::route('/'),
|
||||
'create' => Pages\CreateCustomProperty::route('/create'),
|
||||
'edit' => Pages\EditCustomProperty::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CustomPropertyResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CustomPropertyResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCustomProperty extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CustomPropertyResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CustomPropertyResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CustomPropertyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCustomProperty extends EditRecord
|
||||
{
|
||||
protected static string $resource = CustomPropertyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources\CustomPropertyResource\Pages;
|
||||
|
||||
use App\Admin\Resources\CustomPropertyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCustomProperty extends ListRecords
|
||||
{
|
||||
protected static string $resource = CustomPropertyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
107
app/Admin/Resources/EmailLogResource.php
Normal file
107
app/Admin/Resources/EmailLogResource.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Resources;
|
||||
|
||||
use App\Admin\Resources\EmailLogResource\Pages;
|
||||
use App\Models\EmailLog;
|
||||
use Filament\Infolists;
|
||||
use Filament\Infolists\Components;
|
||||
use Filament\Infolists\Components\View;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class EmailLogResource extends Resource
|
||||
{
|
||||
protected static ?string $model = EmailLog::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-mail-send-line';
|
||||
|
||||
protected static ?string $activeNavigationIcon = 'ri-mail-send-fill';
|
||||
|
||||
protected static ?string $navigationGroup = 'Other';
|
||||
|
||||
public static function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('subject')
|
||||
->size(Components\TextEntry\TextEntrySize::Medium)
|
||||
->label('Subject'),
|
||||
Infolists\Components\TextEntry::make('to')
|
||||
->size(Components\TextEntry\TextEntrySize::Medium)
|
||||
->label('To'),
|
||||
Infolists\Components\TextEntry::make('sent_at')
|
||||
->size(Components\TextEntry\TextEntrySize::Medium)
|
||||
->date()
|
||||
->hidden(fn ($state) => $state === null)
|
||||
->label('Sent At'),
|
||||
Infolists\Components\TextEntry::make('status')
|
||||
->badge()
|
||||
->color(fn (EmailLog $record) => match ($record->status) {
|
||||
'pending' => 'gray',
|
||||
'sent' => 'success',
|
||||
'failed' => 'danger',
|
||||
})
|
||||
->formatStateUsing(fn (string $state) => ucfirst($state))
|
||||
->label('Status'),
|
||||
|
||||
Infolists\Components\TextEntry::make('error')
|
||||
->columnSpanFull()
|
||||
->size(Components\TextEntry\TextEntrySize::Medium)
|
||||
->hidden(fn ($state) => $state === null)
|
||||
->label('Error'),
|
||||
View::make('admin.infolists.components.iframe')->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('subject')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('to')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('sent_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn (EmailLog $record) => match ($record->status) {
|
||||
'pending' => 'gray',
|
||||
'sent' => 'success',
|
||||
'failed' => 'danger',
|
||||
})
|
||||
->formatStateUsing(fn (string $state) => ucfirst($state))
|
||||
->searchable(),
|
||||
])
|
||||
->defaultSort(function (Builder $query): Builder {
|
||||
return $query
|
||||
->orderBy('id', 'desc');
|
||||
})
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListEmailLogs::route('/'),
|
||||
'view' => Pages\ViewEmailLog::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user