feat(price request): membuat enum status

- membuat datatables
- membuat logika untuk handle approve dan reject
- casting dan scope model
- membuat badge di sidebar untuk counting data yang belum disetujui
- menyesuaikan logika untuk mengajukan harga beli dengan skema table yang baru
- menyesuaikan skema table yang baru
- menyesuaikan parameter yang dikriim untuk pengajuan
- menyesuaikan logika table
- membuat permission
This commit is contained in:
Yoga Pangestu 2025-11-21 14:58:49 +07:00
parent c984206d94
commit 396440eda7
16 changed files with 357 additions and 122 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace App\Enums;
use App\Traits\WithCommentEnum;
use App\Traits\WithValueEnum;
enum PriceRequestStatus: int
{
use WithCommentEnum, WithValueEnum;
case PENDING = 1;
case APPROVED = 2;
case REJECTED = 3;
public function label(): string
{
return match ($this) {
self::PENDING => 'Menunggu',
self::APPROVED => 'Disetujui',
self::REJECTED => 'Ditolak',
};
}
public function color()
{
return match ($this) {
self::PENDING => 'gray',
self::APPROVED => 'emerald',
self::REJECTED => 'red',
};
}
}

View File

@ -21,10 +21,16 @@ class BottlesTable extends DataTableComponent
protected $model = Bottle::class;
protected ?int $refreshTime = 3000;
protected int $refreshTime = 3000;
public function columns(): array
{
$canSeePrice = PriceRequest::where('user_id', auth()->id())
->where('module', 'botol')
->approved()
->where('finalized_at', '>', now()->subMinute(10))
->exists();
return [
Column::make('Nama')
->label(function ($row) {
@ -42,13 +48,7 @@ public function columns(): array
->html(),
Column::make('Harga Beli', 'cost_price')
->label(function ($row) {
$canSeePrice = PriceRequest::where('user_id', auth()->id())
->where('model', Bottle::class)
->whereNotNull('approved_at')
->where('expires_at', '>', now())
->exists();
->label(function ($row) use ($canSeePrice) {
if ($canSeePrice) {
return currency($row->cost_price, 'Rp');
}
@ -69,22 +69,22 @@ public function columns(): array
->hideIf(! auth()->user()->hasRole(['Developer', 'Owner'])),
Column::make('Harga Jual', 'sale_price')
->label(fn ($row, $column) => currency($row->sale_price, 'Rp'))
->label(fn($row, $column) => currency($row->sale_price, 'Rp'))
->searchable()
->sortable(),
ImageColumn::make('Gambar')
->location(fn ($row) => optional($row->getMedia('image')->first())->getUrl() ?? asset('assets/images/logo.png'))
->attributes(fn ($row) => [
->location(fn($row) => optional($row->getMedia('image')->first())->getUrl() ?? asset('assets/images/logo.png'))
->attributes(fn($row) => [
'style' => 'width: 50px; height: 50px;',
'alt' => $row->name,
]),
ArrayColumn::make('Outlet')
->data(
fn ($value, $row) => $row->outlets
->filter(fn ($outlet) => auth()->user()->outlets->pluck('id')->contains($outlet->id))
->map(fn ($outlet) => [
fn($value, $row) => $row->outlets
->filter(fn($outlet) => auth()->user()->outlets->pluck('id')->contains($outlet->id))
->map(fn($outlet) => [
'id' => $outlet->hash,
'bottle_id' => $row->hash,
'name' => $outlet->name,
@ -96,7 +96,7 @@ public function columns(): array
$canAudit = auth()->user()->can('audit bottle');
$tag = $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate';
$attrs = $canAudit ? '' : 'href="' . $route . '" wire:navigate';
return Blade::render("
<{$tag} {$attrs}
@ -142,7 +142,7 @@ public function builder(): Builder
->with('outlets')
->whereHas(
'outlets',
fn ($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id'))
fn($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id'))
);
}
}

View File

@ -20,10 +20,16 @@ class PerfumesTable extends DataTableComponent
protected $model = Perfume::class;
protected ?int $refreshTime = 3000;
protected $refreshTime = 3000;
public function columns(): array
{
$canSeePrice = PriceRequest::where('user_id', auth()->id())
->where('module', 'parfum')
->approved()
->where('finalized_at', '>', now()->subMinute(10))
->exists();
return [
Column::make('Nama')
->label(function ($row) {
@ -41,24 +47,18 @@ public function columns(): array
->html(),
ArrayColumn::make('Kategori')
->data(fn ($value, $row) => $row->categories->pluck('name')->toArray())
->outputFormat(fn ($index, $value) => Blade::render('<span class="text-xs text-gray-400 border border-gray-400 rounded px-1">'.$value.'</span>'))
->data(fn($value, $row) => $row->categories->pluck('name')->toArray())
->outputFormat(fn($index, $value) => Blade::render('<span class="text-xs text-gray-400 border border-gray-400 rounded px-1">' . $value . '</span>'))
->flexRow(['class' => 'gap-2 flex-wrap']),
Column::make('Merek', 'brand.name')->searchable()->sortable(),
Column::make('Concentration', 'concentration')
->label(fn ($row, $column) => Blade::render('<flux:badge color="'.$row->concentration->color().'">'.$row->concentration->label().'</flux:badge>'))
->label(fn($row, $column) => Blade::render('<flux:badge color="' . $row->concentration->color() . '">' . $row->concentration->label() . '</flux:badge>'))
->html(),
Column::make('Harga Beli', 'cost_price')
->label(function ($row) {
$canSeePrice = PriceRequest::where('user_id', auth()->id())
->where('model', Perfume::class)
->whereNotNull('approved_at')
->where('expires_at', '>', now())
->exists();
->label(function ($row) use ($canSeePrice) {
if ($canSeePrice) {
return currency($row->cost_price, 'Rp');
}
@ -79,15 +79,15 @@ public function columns(): array
->hideIf(! auth()->user()->hasRole(['Developer', 'Owner'])),
Column::make('Harga Jual', 'sale_price')
->label(fn ($row, $column) => currency($row->sale_price, 'Rp'))
->label(fn($row, $column) => currency($row->sale_price, 'Rp'))
->searchable()
->sortable(),
ArrayColumn::make('Outlet')
->data(
fn ($value, $row) => $row->outlets
->filter(fn ($outlet) => auth()->user()->outlets->pluck('id')->contains($outlet->id))
->map(fn ($outlet) => [
fn($value, $row) => $row->outlets
->filter(fn($outlet) => auth()->user()->outlets->pluck('id')->contains($outlet->id))
->map(fn($outlet) => [
'id' => $outlet->hash,
'perfume_id' => $row->hash,
'name' => $outlet->name,
@ -99,7 +99,7 @@ public function columns(): array
$canAudit = auth()->user()->can('audit perfume');
$tag = $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate';
$attrs = $canAudit ? '' : 'href="' . $route . '" wire:navigate';
return Blade::render("
<{$tag} {$attrs}
@ -159,7 +159,7 @@ public function builder(): Builder
])
->whereHas(
'outlets',
fn ($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id'))
fn($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id'))
);
}
}

View File

@ -20,10 +20,16 @@ class ProductsTable extends DataTableComponent
protected $model = Product::class;
protected ?int $refreshTime = 3000;
protected int $refreshTime = 3000;
public function columns(): array
{
$canSeePrice = PriceRequest::where('user_id', auth()->id())
->where('module', 'produk')
->approved()
->where('finalized_at', '>', now()->subMinute(10))
->exists();
return [
Column::make('Nama')
->label(function ($row) {
@ -41,13 +47,7 @@ public function columns(): array
->html(),
Column::make('Harga Beli', 'cost_price')
->label(function ($row) {
$canSeePrice = PriceRequest::where('user_id', auth()->id())
->where('model', Product::class)
->whereNotNull('approved_at')
->where('expires_at', '>', now())
->exists();
->label(function ($row) use ($canSeePrice) {
if ($canSeePrice) {
return currency($row->cost_price, 'Rp');
}
@ -68,15 +68,15 @@ public function columns(): array
->hideIf(! auth()->user()->hasRole(['Developer', 'Owner'])),
Column::make('Harga Jual', 'sale_price')
->label(fn ($row, $column) => currency($row->sale_price, 'Rp'))
->label(fn($row, $column) => currency($row->sale_price, 'Rp'))
->searchable()
->sortable(),
ArrayColumn::make('Outlet')
->data(
fn ($value, $row) => $row->outlets
->filter(fn ($outlet) => auth()->user()->outlets->pluck('id')->contains($outlet->id))
->map(fn ($outlet) => [
fn($value, $row) => $row->outlets
->filter(fn($outlet) => auth()->user()->outlets->pluck('id')->contains($outlet->id))
->map(fn($outlet) => [
'id' => $outlet->hash,
'product_id' => $row->hash,
'name' => $outlet->name,
@ -88,7 +88,7 @@ public function columns(): array
$canAudit = auth()->user()->can('audit product');
$tag = $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate';
$attrs = $canAudit ? '' : 'href="' . $route . '" wire:navigate';
return Blade::render("
<{$tag} {$attrs}
@ -134,7 +134,7 @@ public function builder(): Builder
->with('outlets')
->whereHas(
'outlets',
fn ($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id'))
fn($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id'))
);
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Livewire\Datatable\Studio\Manage;
use App\Enums\PriceRequestStatus;
use App\Models\PriceRequest;
use App\Traits\Datatable\WithAppendColumn;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
class PriceRequestsTable extends DataTableComponent
{
use WithConfiguration, WithPrependColumn, WithAppendColumn;
protected $model = PriceRequest::class;
public function columns(): array
{
return [
Column::make('Pemohon', 'user.employee.full_name')->searchable(),
Column::make('module', 'module')
->format(fn($row) => ucfirst($row))
->searchable(),
Column::make('Status', 'status')
->label(fn($row, $column) => Blade::render('<flux:badge color="' . $row->status->color() . '">' . $row->status->label() . '</flux:badge>'))
->html(),
Column::make('Tanggal')
->label(function ($row) {
$startDate = formatDate($row->created_at);
$startAgo = timeAgo($row->created_at);
$endDate = formatDate($row->finalized_at);
$endAgo = timeAgo($row->finalized_at);
return <<<HTML
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-300 gap-0.5">
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl Permohonan:</span>
<span>{$startDate}</span>
<span class="text-gray-500">({$startAgo})</span>
</div>
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl Ditanggapi:</span>
<span>{$endDate}</span>
<span class="text-gray-500">({$endAgo})</span>
</div>
</div>
HTML;
})
->html(),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
if (auth()->user()->can('approve price request') && $row->status === PriceRequestStatus::PENDING) {
$actions .= view('components.datatables.confirm', [
'id' => $row->hash,
'message' => 'Pemohon yang diterima bisa melihat data harga beli.',
'confirmButtonText' => 'Ya, Terima',
'target' => 'approve',
'title' => 'Terima',
'confirmButtonColor' => 'primary',
'icon' => 'check-circle',
])->render();
}
if (auth()->user()->can('reject price request') && $row->status === PriceRequestStatus::PENDING) {
$actions .= view('components.datatables.confirm', [
'id' => $row->hash,
'message' => 'Jika ditolak, pomohon tidak dapat melihat data harga beli.',
'confirmButtonText' => 'Ya, Tolak',
'target' => 'reject',
'title' => 'Tolak',
'confirmButtonColor' => 'red',
'icon' => 'x-circle',
])->render();
}
return $actions;
})
->html()
->hideIf(auth()->user()->cannot('approve price request') && auth()->user()->cannot('reject price request')),
];
}
public function builder(): Builder
{
return PriceRequest::select(
'price_requests.id',
'price_requests.user_id',
'module',
'price_requests.status',
'price_requests.created_at',
'finalized_at'
)
->with(['user', 'user.employee']);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Livewire\Studio\Manage;
use App\Enums\PriceRequestStatus;
use App\Jobs\StorePushNotification;
use App\Livewire\Datatable\Studio\Catalog\PerfumesTable;
use App\Models\PriceRequest as PriceRequestModel;
use App\Models\PushNotification;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\WithAuthorization;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Flux\Flux;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Permintaan Harga')]
class PriceRequest extends Component
{
use WithAuthorization, WithConfirmation, WithSubscribeNotification, WithToast;
public function approve(PriceRequestModel $priceRequest)
{
$this->canOrAbort('approve price request');
$priceRequest->update([
'status' => PriceRequestStatus::APPROVED->value,
'finalized_at' => now()
]);
StorePushNotification::dispatch(
PushNotification::where('user_id', $priceRequest->user_id)->get(),
[
'title' => 'Permintaan Harga Disetujui ✅',
'body' => "Selamat! Permintaan untuk melihat harga beli {$priceRequest->module} sudah disetujui 🎉",
],
);
$this->dispatch('refreshDatatable');
$this->toast('Permintaan harga berhasil disetujui.');
Flux::modals()->close();
}
public function reject(PriceRequestModel $priceRequest)
{
$this->canOrAbort('reject price request');
$priceRequest->update([
'status' => PriceRequestStatus::REJECTED->value,
'finalized_at' => now()
]);
StorePushNotification::dispatch(
PushNotification::where('user_id', $priceRequest->user_id)->get(),
[
'title' => 'Permintaan Harga Ditolak ❌',
'body' => "Maaf, permintaan untuk melihat harga beli {$priceRequest->module} tidak dapat dipenuhi 🙏",
],
);
$this->dispatch('refreshDatatable');
$this->toast('Permintaan harga berhasil ditolak.');
Flux::modals()->close();
}
public function render()
{
return view('livewire.studio.manage.price-requests', [
'pageTitle' => 'Permintaan Harga',
]);
}
}

View File

@ -2,6 +2,9 @@
namespace App\Models;
use App\Enums\PriceRequestStatus;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +16,32 @@ class PriceRequest extends Model
protected $guarded = ['id'];
protected function casts(): array
{
return [
'status' => PriceRequestStatus::class,
'finalized_at' => 'datetime',
];
}
#[Scope]
protected function pending(Builder $query): void
{
$query->where('status', PriceRequestStatus::PENDING->value);
}
#[Scope]
protected function approved(Builder $query): void
{
$query->where('status', PriceRequestStatus::APPROVED->value);
}
#[Scope]
protected function rejected(Builder $query): void
{
$query->where('status', PriceRequestStatus::REJECTED->value);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@ -2,6 +2,8 @@
namespace App\Providers;
use App\Enums\PriceRequestStatus;
use App\Models\PriceRequest;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@ -9,7 +11,9 @@ class ViewServiceProvider extends ServiceProvider
{
public function boot(): void
{
View::composer('*', function ($view) {
$priceRequestCount = PriceRequest::where('status', PriceRequestStatus::PENDING->value)->count();
View::composer('*', function ($view) use ($priceRequestCount) {
$sidebar = [
[
'heading' => 'Dasbor',
@ -165,6 +169,14 @@ public function boot(): void
'match' => 'studio.manage.article.*',
'can' => 'view article',
],
[
'label' => 'Permintaan Harga',
'icon' => 'exclamation-triangle',
'route' => 'studio.manage.price_request.index',
'match' => 'studio.manage.price_request.*',
'can' => 'view price request',
'badge' => $priceRequestCount,
],
[
'label' => 'FAQs',
'icon' => 'question-mark-circle',

View File

@ -2,102 +2,57 @@
namespace App\Traits;
use App\Models\Bottle;
use App\Models\Perfume;
use App\Jobs\StorePushNotification;
use App\Models\PriceRequest;
use App\Models\Product;
use App\Models\PushNotification;
use App\Models\User;
use App\Models\Whatsapp;
use Flux\Flux;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\DB;
trait WithShowPrice
{
public function requestPrice(string $module)
{
switch ($module) {
case 'perfume':
$label = 'parfum';
$model = Perfume::class;
break;
case 'product':
$label = 'produk';
$model = Product::class;
break;
default:
$label = 'botol';
$model = Bottle::class;
break;
}
$recentRequest = PriceRequest::where('user_id', auth()->id())
->where('model', $model)
->where('created_at', '>', now()->subMinutes(30))
->where('module', $module)
->pending()
->exists();
if ($recentRequest) {
$this->toast('Kamu baru saja mengajukan permintaan, tautan persetujuan masih berlaku selama 30 menit.', 'Peringatan', 'warning');
$this->toast('Permohonan kamu sedang di proses oleh owner, mohon tunggu dan kami akan memberi tahu Anda jika prosesnya sudah selesai.', 'Peringatan', 'warning');
Flux::modal('show-price')->close();
return;
}
$whatsapp = Whatsapp::first();
$owner = User::role('owner')->with('employee')->first();
$owner = User::role(['owner', 'Developer'])->with('employee')->first();
if (! $owner) {
$this->toast('Owner belum terdaftar, silakan hubungi Admin', 'Gagal', 'danger');
$this->toast('Owner belum terdaftar, silakan hubungi Admin.', 'Gagal', 'danger');
Flux::modal('show-price')->close();
return;
}
$phoneNumber = formatPhoneNumber($owner->employee?->phone_number, '62');
$causerName = auth()->user()->employee?->full_name;
try {
$priceRequest = PriceRequest::create([
DB::transaction(function () use ($module, $owner, $causerName) {
PriceRequest::create([
'user_id' => auth()->id(),
'model' => $model,
'expires_at' => now()->addMinutes(30),
'module' => $module,
]);
$signedUrl = URL::temporarySignedRoute(
'price-request.approve',
$priceRequest->expires_at,
['priceRequest' => $priceRequest->hash]
StorePushNotification::dispatch(
PushNotification::all(),
[
'title' => 'Permintaan Persetujuan Harga Beli',
'body' => "Haloo {$owner->employee?->full_name}, saya {$causerName}, saya ingin mengajukan permintaan persetujuan untuk mengakses data harga beli {$module}.",
'route' => route('studio.manage.price_request.index'),
],
);
});
$priceRequest->update([
'signature' => hash('sha256', $signedUrl),
]);
$causerName = auth()->user()->employee?->full_name;
$response = Http::withHeaders([
'id' => $whatsapp->zawa_id,
'session-id' => $whatsapp->session_id,
'Content-Type' => 'application/json',
])->post(config('services.zawa.url').'/message', [
'phone' => $phoneNumber,
'type' => 'text',
'text' => "Haloo {$owner->employee?->full_name}, saya {$causerName}, "
."saya ingin mengajukan permintaan persetujuan untuk mengakses data harga beli {$label}."
."\n\nSilakan klik tautan berikut untuk menyetujui:\n\n{$signedUrl}",
]);
if ($response->successful()) {
$this->toast('Permintaan persetujuan berhasil dikirim.');
} else {
$this->toast('Permintaan persetujuan gagal dikirim, silakan coba lagi.', 'Gagal', 'danger');
}
} catch (\Throwable $th) {
Log::error($th->getMessage());
$this->toast('Opps, terjadi kesalahan. Silakan coba lagi dan jika masih terjadi, hubungi Administrator.', 'Gagal', 'danger');
}
$this->toast('Permintaan persetujuan berhasil dikirim.');
Flux::modal('show-price')->close();
}

View File

@ -1,5 +1,6 @@
<?php
use App\Enums\PriceRequestStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -14,11 +15,9 @@ public function up(): void
Schema::create('price_requests', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('model', 20);
$table->string('signature')->unique()->nullable();
$table->timestamp('last_requested_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('approved_at')->nullable();
$table->string('module', 20);
$table->enum('status', [PriceRequestStatus::values()])->default(PriceRequestStatus::PENDING)->comment(PriceRequestStatus::comment());
$table->timestamp('finalized_at')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->softDeletes();

View File

@ -118,6 +118,10 @@ public function run(): void
'update article',
'delete article',
'view price request',
'approve price request',
'reject price request',
'view faq',
'create faq',
'update faq',

View File

@ -26,5 +26,5 @@ class="text-sm">
</div>
@include('components.confirmation.delete')
@include('components.confirmation.show-price', ['module' => 'bottle'])
@include('components.confirmation.show-price', ['module' => 'botol'])
</flux:main>

View File

@ -26,5 +26,5 @@ class="text-sm">
</div>
@include('components.confirmation.delete')
@include('components.confirmation.show-price', ['module' => 'perfume'])
@include('components.confirmation.show-price', ['module' => 'parfum'])
</flux:main>

View File

@ -26,5 +26,5 @@ class="text-sm">
</div>
@include('components.confirmation.delete')
@include('components.confirmation.show-price', ['module' => 'product'])
@include('components.confirmation.show-price', ['module' => 'produk'])
</flux:main>

View File

@ -0,0 +1,13 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
</div>
<div class="mt-6">
<livewire:datatable.studio.manage.price-requests-table />
</div>
@include('components.confirmation')
</flux:main>

View File

@ -33,6 +33,7 @@
use App\Livewire\Studio\Manage\Order\Create as OrderCreate;
use App\Livewire\Studio\Manage\Order\Index as OrderIndex;
use App\Livewire\Studio\Manage\Order\Show as OrderShow;
use App\Livewire\Studio\Manage\PriceRequest as PriceRequestComponent;
use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate;
use App\Livewire\Studio\Manage\Purchase\Index as PurchaseIndex;
use App\Livewire\Studio\Master\Formula as FormulaComponent;
@ -176,6 +177,12 @@
Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq');
Route::delete('/{faq}/delete', FaqComponent::class)->name('delete')->middleware('can:delete faq');
});
Route::prefix('price-requests')->name('price_request.')->group(function () {
Route::get('/', PriceRequestComponent::class)->name('index')->middleware('can:view price request');
Route::patch('/{priceRequest}/approve', PriceRequestComponent::class)->name('approve')->middleware('can:approve price request');
Route::patch('/{priceRequest}/reject', PriceRequestComponent::class)->name('reject')->middleware('can:reject price request');
});
});
Route::prefix('feature')->name('studio.feature.')->group(function () {