feat: Refactor FAQ, add category typing with enum, introduce article categories, and update various Livewire components and views.

This commit is contained in:
Yoga Pangestu 2025-12-21 14:10:33 +07:00
parent 37387fe124
commit b469fbff53
24 changed files with 227 additions and 193 deletions

View File

@ -0,0 +1,30 @@
<?php
namespace App\Enums;
use App\Traits\Enums\WithCommentEnum;
use App\Traits\Enums\WithValueEnum;
enum CategoryType: int
{
use WithCommentEnum, WithValueEnum;
case PERFUME = 1;
case ARTICLE = 2;
public function label(): string
{
return match ($this) {
self::PERFUME => 'Parfum',
self::ARTICLE => 'Artikel',
};
}
public function color(): string
{
return match ($this) {
self::PERFUME => 'emerald',
self::ARTICLE => 'blue',
};
}
}

View File

@ -1,135 +0,0 @@
<?php
namespace App\Exports;
use App\Models\Category;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class CategoryExport implements FromCollection, ShouldAutoSize, WithColumnWidths, WithEvents, WithHeadings, WithMapping, WithStyles, WithTitle
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return Category::all();
}
public function title(): string
{
return 'Data Kategori';
}
public function headings(): array
{
return [
'Nama',
'Deskripsi',
'Tanggal Dibuat',
'Tanggal Diperbarui',
];
}
/**
* @param Category $category
*/
public function map($category): array
{
return [
$category->name,
strip_tags($category->description) ?: '-',
formatDateLocalized($category->created_at, 'd/m/Y H:i'),
formatDateLocalized($category->updated_at, 'd/m/Y H:i'),
];
}
public function columnWidths(): array
{
return [
'A' => 20, // Category Name
'B' => 30, // Description
'C' => 18, // Created Date
'D' => 18, // Updated Date
];
}
/**
* @return array
*/
public function styles(Worksheet $sheet)
{
$lastRow = $sheet->getHighestRow();
$lastColumn = $sheet->getHighestColumn();
// Header style
$headerStyle = [
'font' => [
'bold' => true,
'size' => 12,
'color' => ['rgb' => 'FFFFFF'],
],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['rgb' => '78716C'], // Stone - elegant
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER,
],
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['rgb' => '000000'],
],
],
];
// Data rows style
$dataStyle = [
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['rgb' => '000000'],
],
],
'alignment' => [
'vertical' => Alignment::VERTICAL_TOP,
],
];
return [
// Header styling
1 => $headerStyle,
// Data rows styling
'A2:'.$lastColumn.$lastRow => $dataStyle,
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
// Auto-size columns with max width
foreach (range('A', 'D') as $column) {
$event->sheet->getColumnDimension($column)->setAutoSize(true);
}
// Set row height for header
$event->sheet->getRowDimension(1)->setRowHeight(25);
},
];
}
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exports\BrandExport; use App\Exports\BrandExport;
use App\Exports\CategoryExport;
use App\Exports\FormulaExport; use App\Exports\FormulaExport;
use App\Exports\PerfumeExport; use App\Exports\PerfumeExport;
use App\Exports\ProductExport; use App\Exports\ProductExport;
@ -45,17 +44,6 @@ public function formulas(): BinaryFileResponse
); );
} }
/**
* Export categories to Excel
*/
public function categories(): BinaryFileResponse
{
return Excel::download(
new CategoryExport,
'data_kategori_'.now()->format('Y-m-d_H-i-s').'.xlsx'
);
}
/** /**
* Export brands to Excel * Export brands to Excel
*/ */

View File

@ -11,6 +11,7 @@
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column; use Rappasoft\LaravelLivewireTables\Views\Column;
use Rappasoft\LaravelLivewireTables\Views\Columns\ArrayColumn;
class ArticlesTable extends DataTableComponent class ArticlesTable extends DataTableComponent
{ {
@ -25,6 +26,11 @@ public function columns(): array
Column::make('Judul', 'title')->searchable(), Column::make('Judul', 'title')->searchable(),
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>'))
->flexRow(['class' => 'gap-2 flex-wrap']),
Column::make('Status', 'status') Column::make('Status', 'status')
->format( ->format(
fn ($value) => Blade::render('<flux:badge color="'.$value->color().'">'.$value->label().'</flux:badge>') fn ($value) => Blade::render('<flux:badge color="'.$value->color().'">'.$value->label().'</flux:badge>')
@ -66,6 +72,6 @@ public function columns(): array
public function builder(): Builder public function builder(): Builder
{ {
return Article::select('articles.id', 'author_id', 'title', 'articles.status', 'published_at', 'views')->with(['author', 'author.employee']); return Article::select('articles.id', 'author_id', 'title', 'articles.status', 'published_at', 'views')->with(['author', 'author.employee', 'categories']);
} }
} }

View File

@ -1,12 +1,13 @@
<?php <?php
namespace App\Livewire\Datatable\Catalog; namespace App\Livewire\Datatable\Master;
use App\Models\Category; use App\Models\Category;
use App\Traits\Datatable\WithConfiguration; use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn; use App\Traits\Datatable\WithPrependColumn;
use App\Traits\Media\WithMediaHandler; use App\Traits\Media\WithMediaHandler;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade;
use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column; use Rappasoft\LaravelLivewireTables\Views\Column;
use Rappasoft\LaravelLivewireTables\Views\Columns\ImageColumn; use Rappasoft\LaravelLivewireTables\Views\Columns\ImageColumn;
@ -22,6 +23,10 @@ public function columns(): array
return [ return [
Column::make('Nama', 'name')->searchable(), Column::make('Nama', 'name')->searchable(),
Column::make('Jenis', 'type')
->label(fn ($row, $column) => Blade::render('<flux:badge color="'.$row->type->color().'">'.$row->type->label().'</flux:badge>'))
->html(),
ImageColumn::make('Gambar') ImageColumn::make('Gambar')
->location(fn ($row) => optional($row->getMedia('image')->first())->getUrl() ?? asset('assets/images/logo.png')) ->location(fn ($row) => optional($row->getMedia('image')->first())->getUrl() ?? asset('assets/images/logo.png'))
->attributes(fn ($row) => [ ->attributes(fn ($row) => [
@ -99,6 +104,6 @@ public function columns(): array
public function builder(): Builder public function builder(): Builder
{ {
return Category::select('id', 'name', 'sort_order')->orderBy('sort_order'); return Category::select('id', 'name', 'type', 'sort_order')->orderBy('sort_order');
} }
} }

View File

@ -76,7 +76,10 @@ public function rules(): array
'quota' => ['nullable', new UnsignedInteger], 'quota' => ['nullable', new UnsignedInteger],
'limit_per_user' => ['nullable', new UnsignedInteger], 'limit_per_user' => ['nullable', new UnsignedInteger],
'summary' => ['required', 'string', 'max:200'], 'summary' => ['required', 'string', 'max:200'],
'start_date' => ['required', 'date', 'after:yesterday'], 'start_date' => ['required', 'date', Rule::when(
$this->start_date !== $this->voucher->start_date,
'after_or_equal:today'
)],
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
'tier_ids' => ['required', 'array', 'min:1'], 'tier_ids' => ['required', 'array', 'min:1'],
'tier_ids.*' => Rule::exists('tiers', 'id'), 'tier_ids.*' => Rule::exists('tiers', 'id'),

View File

@ -23,6 +23,8 @@ class ArticleForm extends Form
public string $status = '1'; public string $status = '1';
public array $category_ids = [];
public array $thumbnail = []; public array $thumbnail = [];
public function rules(): array public function rules(): array
@ -32,6 +34,8 @@ public function rules(): array
'excerpt' => ['required', 'string'], 'excerpt' => ['required', 'string'],
'content' => ['required', 'string'], 'content' => ['required', 'string'],
'status' => ['required', Rule::in(ArticleStatus::cases())], 'status' => ['required', Rule::in(ArticleStatus::cases())],
'category_ids' => ['required', 'array', 'min:1'],
'category_ids.*' => Rule::exists('categories', 'id'),
'thumbnail' => ['required', 'array'], 'thumbnail' => ['required', 'array'],
]; ];
} }
@ -42,6 +46,8 @@ public function validationAttributes(): array
'title' => 'judul', 'title' => 'judul',
'excerpt' => 'cuplikan', 'excerpt' => 'cuplikan',
'content' => 'konten', 'content' => 'konten',
'category_ids' => 'kategori',
'category_ids.*' => 'kategori',
]; ];
} }
@ -53,6 +59,7 @@ public function setArticle(Article $article): void
$this->excerpt = $article->excerpt; $this->excerpt = $article->excerpt;
$this->content = $article->content; $this->content = $article->content;
$this->status = $article->status->value; $this->status = $article->status->value;
$this->category_ids = $article->categories->pluck('id')->toArray();
$this->thumbnail = $this->mapMediaCollection($article->getMedia('thumbnail')); $this->thumbnail = $this->mapMediaCollection($article->getMedia('thumbnail'));
} }
@ -70,6 +77,8 @@ public function store(): string
'published_at' => $this->status == ArticleStatus::PUBLISHED->value ? now() : null, 'published_at' => $this->status == ArticleStatus::PUBLISHED->value ? now() : null,
]); ]);
$article->categories()->sync($this->category_ids);
$this->uploadMedia($this->thumbnail, $article, 'thumbnail'); $this->uploadMedia($this->thumbnail, $article, 'thumbnail');
}); });
@ -93,6 +102,8 @@ public function update(): void
: [] : []
)); ));
$this->article->categories()->sync($this->category_ids);
$this->syncMedia($this->thumbnail, $this->article, 'thumbnail'); $this->syncMedia($this->thumbnail, $this->article, 'thumbnail');
$this->uploadMedia($this->thumbnail, $this->article, 'thumbnail'); $this->uploadMedia($this->thumbnail, $this->article, 'thumbnail');
}); });

View File

@ -1,7 +1,8 @@
<?php <?php
namespace App\Livewire\Forms\Studio\Catalog; namespace App\Livewire\Forms\Studio\Master;
use App\Enums\CategoryType;
use App\Models\Category; use App\Models\Category;
use App\Traits\Media\WithMediaHandler; use App\Traits\Media\WithMediaHandler;
use App\Traits\Utilities\WithSortOrder; use App\Traits\Utilities\WithSortOrder;
@ -19,14 +20,17 @@ class CategoryForm extends Form
public ?string $description = null; public ?string $description = null;
public int $type = CategoryType::PERFUME->value;
public array $image = []; public array $image = [];
public function rules(): array public function rules(): array
{ {
return [ return [
'name' => ['required', 'string', 'max:20', Rule::unique('categories', 'name')->whereNull('deleted_at')->ignore($this->category)], 'name' => ['required', 'string', 'max:20', Rule::unique('categories', 'name')->whereNull('deleted_at')->ignore($this->category)],
'description' => 'nullable', 'description' => $this->type === CategoryType::PERFUME->value ? 'nullable' : 'prohibited',
'image' => 'array', 'type' => ['required', Rule::in(CategoryType::values())],
'image' => $this->type === CategoryType::PERFUME->value ? 'array' : 'prohibited',
]; ];
} }
@ -35,6 +39,7 @@ public function validationAttributes(): array
return [ return [
'name' => 'nama', 'name' => 'nama',
'description' => 'deskripsi', 'description' => 'deskripsi',
'type' => 'tipe',
'image' => 'gambar', 'image' => 'gambar',
]; ];
} }
@ -45,6 +50,7 @@ public function setCategory(Category $category): void
$this->name = $category->name; $this->name = $category->name;
$this->description = $category->description; $this->description = $category->description;
$this->type = $category->type->value;
$this->image = $this->mapMediaCollection($category->getMedia('image')); $this->image = $this->mapMediaCollection($category->getMedia('image'));
} }
@ -59,11 +65,14 @@ public function store(): void
$category = Category::create([ $category = Category::create([
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->type === CategoryType::PERFUME->value ? $this->description : null,
'type' => $this->type,
'sort_order' => 1, 'sort_order' => 1,
]); ]);
if ($this->type === CategoryType::PERFUME->value) {
$this->uploadMedia($this->image, $category, 'image'); $this->uploadMedia($this->image, $category, 'image');
}
}); });
} }
@ -74,11 +83,16 @@ public function update(): void
DB::transaction(function () { DB::transaction(function () {
$this->category->update([ $this->category->update([
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->type === CategoryType::PERFUME->value ? $this->description : null,
'type' => $this->type,
]); ]);
if ($this->type === CategoryType::PERFUME->value) {
$this->syncMedia($this->image, $this->category, 'image'); $this->syncMedia($this->image, $this->category, 'image');
$this->uploadMedia($this->image, $this->category, 'image'); $this->uploadMedia($this->image, $this->category, 'image');
} else {
$this->category->clearMediaCollection('image');
}
}); });
} }

View File

@ -36,7 +36,7 @@ public function mount(): void
$this->brands = Brand::pluck('name', 'id')->toArray(); $this->brands = Brand::pluck('name', 'id')->toArray();
$this->categories = Category::pluck('name', 'id')->toArray(); $this->categories = Category::perfume()->pluck('name', 'id')->toArray();
} }
public function save(): void public function save(): void

View File

@ -2,10 +2,13 @@
namespace App\Livewire\Studio\Manage\Article; namespace App\Livewire\Studio\Manage\Article;
use App\Enums\CategoryType;
use App\Jobs\StorePushNotification; use App\Jobs\StorePushNotification;
use App\Livewire\Forms\Studio\Manage\ArticleForm; use App\Livewire\Forms\Studio\Manage\ArticleForm;
use App\Models\Category;
use App\Models\PushNotification; use App\Models\PushNotification;
use App\Traits\Authorization\WithAuthorization; use App\Traits\Authorization\WithAuthorization;
use App\Traits\Components\WithCategorySelector;
use App\Traits\Components\WithToast; use App\Traits\Components\WithToast;
use App\Traits\Utilities\WithUpdatedData; use App\Traits\Utilities\WithUpdatedData;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@ -16,10 +19,17 @@
#[Title('Tambah Artikel')] #[Title('Tambah Artikel')]
class Create extends Component class Create extends Component
{ {
use WithAuthorization, WithToast, WithUpdatedData; use WithAuthorization, WithCategorySelector, WithToast, WithUpdatedData;
public ArticleForm $form; public ArticleForm $form;
public array $categories = [];
public function mount(): void
{
$this->categories = Category::article()->pluck('name', 'id')->toArray();
}
public function save(): void public function save(): void
{ {
$this->canOrAbort('create article'); $this->canOrAbort('create article');
@ -43,6 +53,7 @@ public function render(): View
{ {
return view('livewire.studio.manage.article.form', [ return view('livewire.studio.manage.article.form', [
'pageTitle' => 'Tambah Artikel', 'pageTitle' => 'Tambah Artikel',
'categories' => Category::query()->where('type', CategoryType::ARTICLE)->orderBy('sort_order')->get(),
]); ]);
} }
} }

View File

@ -2,9 +2,12 @@
namespace App\Livewire\Studio\Manage\Article; namespace App\Livewire\Studio\Manage\Article;
use App\Enums\CategoryType;
use App\Livewire\Forms\Studio\Manage\ArticleForm; use App\Livewire\Forms\Studio\Manage\ArticleForm;
use App\Models\Article; use App\Models\Article;
use App\Models\Category;
use App\Traits\Authorization\WithAuthorization; use App\Traits\Authorization\WithAuthorization;
use App\Traits\Components\WithCategorySelector;
use App\Traits\Components\WithToast; use App\Traits\Components\WithToast;
use App\Traits\Utilities\WithUpdatedData; use App\Traits\Utilities\WithUpdatedData;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@ -14,13 +17,17 @@
#[Title('Ubah Artikel')] #[Title('Ubah Artikel')]
class Edit extends Component class Edit extends Component
{ {
use WithAuthorization, WithToast, WithUpdatedData; use WithAuthorization, WithCategorySelector, WithToast, WithUpdatedData;
public ArticleForm $form; public ArticleForm $form;
public array $categories = [];
public function mount(Article $article): void public function mount(Article $article): void
{ {
$this->form->setArticle($article); $this->form->setArticle($article);
$this->categories = Category::article()->pluck('name', 'id')->toArray();
} }
public function save(): void public function save(): void
@ -38,6 +45,7 @@ public function render(): View
{ {
return view('livewire.studio.manage.article.form', [ return view('livewire.studio.manage.article.form', [
'pageTitle' => 'Ubah Artikel', 'pageTitle' => 'Ubah Artikel',
'categories' => Category::query()->where('type', CategoryType::ARTICLE)->orderBy('sort_order')->get(),
]); ]);
} }
} }

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Livewire\Studio\Catalog; namespace App\Livewire\Studio\Master;
use App\Livewire\Forms\Studio\Catalog\CategoryForm; use App\Livewire\Forms\Studio\Master\CategoryForm;
use App\Models\Category as CategoryModel; use App\Models\Category as CategoryModel;
use App\Traits\Authorization\WithAuthorization; use App\Traits\Authorization\WithAuthorization;
use App\Traits\Components\WithCloseModal; use App\Traits\Components\WithCloseModal;
@ -94,7 +94,7 @@ public function openModal(string $method, string $modalTitle, ?string $id = null
public function render(): View public function render(): View
{ {
return view('livewire.studio.catalog.categories', [ return view('livewire.studio.master.categories', [
'pageTitle' => 'Kategori', 'pageTitle' => 'Kategori',
]); ]);
} }

View File

@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
@ -39,4 +40,9 @@ public function author(): BelongsTo
{ {
return $this->belongsTo(User::class, 'author_id'); return $this->belongsTo(User::class, 'author_id');
} }
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
} }

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ArticleCategory extends Model
{
protected $table = 'article_category';
protected $guarded = ['id'];
}

View File

@ -2,6 +2,9 @@
namespace App\Models; namespace App\Models;
use App\Enums\CategoryType;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -22,9 +25,22 @@ protected function casts(): array
{ {
return [ return [
'sort_order' => 'int', 'sort_order' => 'int',
'type' => CategoryType::class,
]; ];
} }
#[Scope]
protected function perfume(Builder $query): Builder
{
return $query->where('type', CategoryType::PERFUME);
}
#[Scope]
protected function article(Builder $query): Builder
{
return $query->where('type', CategoryType::ARTICLE);
}
public function getSlugOptions(): SlugOptions public function getSlugOptions(): SlugOptions
{ {
return SlugOptions::create() return SlugOptions::create()
@ -36,4 +52,9 @@ public function perfumes(): BelongsToMany
{ {
return $this->belongsToMany(Perfume::class); return $this->belongsToMany(Perfume::class);
} }
public function articles(): BelongsToMany
{
return $this->belongsToMany(Article::class);
}
} }

View File

@ -71,6 +71,13 @@ public function boot(): void
'match' => 'studio.master.formula.*', 'match' => 'studio.master.formula.*',
'can' => 'view formula', 'can' => 'view formula',
], ],
[
'label' => 'Kategori',
'icon' => 'list-bullet',
'route' => 'studio.master.category.index',
'match' => 'studio.master.category.*',
'can' => 'view category',
],
], ],
], ],
[ [
@ -116,13 +123,6 @@ public function boot(): void
[ [
'heading' => 'Katalog', 'heading' => 'Katalog',
'items' => [ 'items' => [
[
'label' => 'Kategori',
'icon' => 'list-bullet',
'route' => 'studio.catalog.category.index',
'match' => 'studio.catalog.category.*',
'can' => 'view category',
],
[ [
'label' => 'Merek', 'label' => 'Merek',
'icon' => 'tag', 'icon' => 'tag',

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Enums\CategoryType;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
@ -16,6 +17,7 @@ public function up(): void
$table->string('name', 20); $table->string('name', 20);
$table->string('slug', 30); $table->string('slug', 30);
$table->text('description')->nullable(); $table->text('description')->nullable();
$table->integer('type')->default(CategoryType::PERFUME->value)->comment(CategoryType::comment());
$table->unsignedInteger('sort_order'); $table->unsignedInteger('sort_order');
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();

View File

@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('article_category', function (Blueprint $table) {
$table->id();
$table->foreignId('article_id')->constrained()->cascadeOnDelete();
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('article_category');
}
};

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@can('view perfume') @can('view perfume')
<a href="{{ route('studio.export.perfumes') }}" class="inline-flex"> <a href="{{ route('studio.export.perfume') }}" class="inline-flex">
<flux:button variant="primary" color="zinc" class="text-sm"> <flux:button variant="primary" color="zinc" class="text-sm">
Ekspor Ecxel Ekspor Ecxel
</flux:button> </flux:button>

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@can('view product') @can('view product')
<a href="{{ route('studio.export.products') }}" class="inline-flex"> <a href="{{ route('studio.export.product') }}" class="inline-flex">
<flux:button variant="primary" color="zinc" class="text-sm"> <flux:button variant="primary" color="zinc" class="text-sm">
Ekspor Ecxel Ekspor Ecxel
</flux:button> </flux:button>

View File

@ -53,6 +53,24 @@
</flux:field> </flux:field>
</flux:card> </flux:card>
<flux:card class="space-y-6 p-6">
<flux:field>
<flux:label>Kategori <span class="text-red-500 ms-1">*</span></flux:label>
<flux:select variant="listbox" multiple searchable placeholder="Pilih kategori"
wire:model="form.category_ids">
<flux:select.option wire:click="selectAllCategories" wire:ignore>Pilih Semua
</flux:select.option>
<flux:select.option wire:click="deselectAllCategories" wire:ignore>Hapus Semua
</flux:select.option>
@foreach ($categories as $key => $value)
<flux:select.option value="{{ $key }}">{{ $value }}
</flux:select.option>
@endforeach
</flux:select>
<flux:error name="form.category_ids" />
</flux:field>
</flux:card>
<flux:card class="space-y-6 p-6"> <flux:card class="space-y-6 p-6">
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-sm font-medium">Thumbnail <span class="text-red-500 ms-1">*</span></h3> <h3 class="text-sm font-medium">Thumbnail <span class="text-red-500 ms-1">*</span></h3>

View File

@ -4,13 +4,6 @@
<flux:heading size="xl">{{ $pageTitle }}</flux:heading> <flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@can('view category')
<a href="{{ route('studio.export.categories') }}" class="inline-flex">
<flux:button variant="primary" color="zinc" class="text-sm">
Ekspor Ecxel
</flux:button>
</a>
@endcan
@can('create category') @can('create category')
<flux:modal.trigger name="form-modal"> <flux:modal.trigger name="form-modal">
<flux:button variant="primary" class="text-sm" <flux:button variant="primary" class="text-sm"
@ -22,7 +15,7 @@
</div> </div>
<div class="mt-6"> <div class="mt-6">
<livewire:datatable.catalog.categories-table /> <livewire:datatable.master.categories-table />
</div> </div>
@include('components.modals.confirmation', [ @include('components.modals.confirmation', [
@ -47,6 +40,18 @@
placeholder="Aromaterapi merupakan wewangian yang dapat membantu meredakan stress dan meredakan perasaan sakit kepala." placeholder="Aromaterapi merupakan wewangian yang dapat membantu meredakan stress dan meredakan perasaan sakit kepala."
auotocomplete="off" class="**:data-[slot=content]:min-h-[100px]!" /> auotocomplete="off" class="**:data-[slot=content]:min-h-[100px]!" />
<flux:field>
<flux:label>Jenis <span class="text-red-500 ms-1">*</span></flux:label>
<flux:radio.group wire:model="form.type" variant="buttons" class="w-full *:flex-1">
@foreach (\App\Enums\CategoryType::cases() as $item)
<flux:radio value="{{ $item->value }}">
{{ $item->label() }}
</flux:radio>
@endforeach
</flux:radio.group>
<flux:error name="form.type" />
</flux:field>
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-sm font-medium">Gambar</h3> <h3 class="text-sm font-medium">Gambar</h3>
<div class="dropzone-wrapper"> <div class="dropzone-wrapper">

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@can('view formula') @can('view formula')
<a href="{{ route('studio.export.formulas') }}" class="inline-flex"> <a href="{{ route('studio.export.formula') }}" class="inline-flex">
<flux:button variant="primary" color="zinc" class="text-sm"> <flux:button variant="primary" color="zinc" class="text-sm">
Ekspor Ecxel Ekspor Ecxel
</flux:button> </flux:button>

View File

@ -6,7 +6,6 @@
use App\Livewire\Studio\Catalog\Bottle\Edit as BottleEdit; use App\Livewire\Studio\Catalog\Bottle\Edit as BottleEdit;
use App\Livewire\Studio\Catalog\Bottle\Index as BottleIndex; use App\Livewire\Studio\Catalog\Bottle\Index as BottleIndex;
use App\Livewire\Studio\Catalog\Brand as BrandComponent; use App\Livewire\Studio\Catalog\Brand as BrandComponent;
use App\Livewire\Studio\Catalog\Category as CategoryComponent;
use App\Livewire\Studio\Catalog\Perfume\Audit as PerfumeAudit; use App\Livewire\Studio\Catalog\Perfume\Audit as PerfumeAudit;
use App\Livewire\Studio\Catalog\Perfume\Create as PerfumeCreate; use App\Livewire\Studio\Catalog\Perfume\Create as PerfumeCreate;
use App\Livewire\Studio\Catalog\Perfume\Edit as PerfumeEdit; use App\Livewire\Studio\Catalog\Perfume\Edit as PerfumeEdit;
@ -43,6 +42,7 @@
use App\Livewire\Studio\Manage\StockOpname\Index as StockOpnameIndex; use App\Livewire\Studio\Manage\StockOpname\Index as StockOpnameIndex;
use App\Livewire\Studio\Manage\StockOpname\Manage as StockOpnameManage; use App\Livewire\Studio\Manage\StockOpname\Manage as StockOpnameManage;
use App\Livewire\Studio\Manage\StockOpname\Show as StockOpnameShow; use App\Livewire\Studio\Manage\StockOpname\Show as StockOpnameShow;
use App\Livewire\Studio\Master\Category as CategoryComponent;
use App\Livewire\Studio\Master\Formula as FormulaComponent; use App\Livewire\Studio\Master\Formula as FormulaComponent;
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate; use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit; use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit;
@ -81,6 +81,10 @@
Route::prefix('formulas')->name('formula.')->group(function () { Route::prefix('formulas')->name('formula.')->group(function () {
Route::get('/', FormulaComponent::class)->name('index')->middleware('can:view formula'); Route::get('/', FormulaComponent::class)->name('index')->middleware('can:view formula');
}); });
Route::prefix('categories')->name('category.')->group(function () {
Route::get('/', CategoryComponent::class)->name('index')->middleware('can:view category');
});
}); });
Route::prefix('loyalty')->name('studio.loyalty.')->group(function () { Route::prefix('loyalty')->name('studio.loyalty.')->group(function () {
@ -106,10 +110,6 @@
}); });
Route::prefix('catalog')->name('studio.catalog.')->group(function () { Route::prefix('catalog')->name('studio.catalog.')->group(function () {
Route::prefix('categories')->name('category.')->group(function () {
Route::get('/', CategoryComponent::class)->name('index')->middleware('can:view category');
});
Route::prefix('brands')->name('brand.')->group(function () { Route::prefix('brands')->name('brand.')->group(function () {
Route::get('/', BrandComponent::class)->name('index')->middleware('can:view brand'); Route::get('/', BrandComponent::class)->name('index')->middleware('can:view brand');
}); });
@ -203,10 +203,9 @@
// Export Routes // Export Routes
Route::prefix('export')->name('studio.export.')->group(function () { Route::prefix('export')->name('studio.export.')->group(function () {
Route::get('/perfumes', [ExportController::class, 'perfumes'])->name('perfumes')->middleware('can:view perfume'); Route::get('/perfumes', [ExportController::class, 'perfumes'])->name('perfume')->middleware('can:view perfume');
Route::get('/products', [ExportController::class, 'products'])->name('products')->middleware('can:view product'); Route::get('/products', [ExportController::class, 'products'])->name('product')->middleware('can:view product');
Route::get('/formulas', [ExportController::class, 'formulas'])->name('formulas')->middleware('can:view formula'); Route::get('/formulas', [ExportController::class, 'formulas'])->name('formula')->middleware('can:view formula');
Route::get('/categories', [ExportController::class, 'categories'])->name('categories')->middleware('can:view category'); Route::get('/brands', [ExportController::class, 'brands'])->name('brand')->middleware('can:view brand');
Route::get('/brands', [ExportController::class, 'brands'])->name('brands')->middleware('can:view brand');
}); });
}); });