From c435d58f8911b737f44a9551d5fd07329d4300e7 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 1 Nov 2025 15:54:03 +0700 Subject: [PATCH] feat(audit): membuat enum untuk status - menjadikan data outlet bisa di klik dan di redirect ke halaman audit - catat log saat order create - menambahkan permission - menambahkan halaman baru - kasih card penjelasan cara audit --- app/Enums/AuditStockStatus.php | 19 ++++++ .../Datatable/Studio/Catalog/BottlesTable.php | 20 +++++-- .../Studio/Catalog/PerfumesTable.php | 20 +++++-- .../Studio/Catalog/ProductsTable.php | 20 +++++-- app/Livewire/Studio/Catalog/Bottle/Audit.php | 54 +++++++++++++++++ app/Livewire/Studio/Catalog/Perfume/Audit.php | 54 +++++++++++++++++ app/Livewire/Studio/Catalog/Product/Audit.php | 54 +++++++++++++++++ app/Livewire/Studio/Manage/Order/Create.php | 2 +- .../Studio/Manage/Purchase/Create.php | 9 ++- app/Traits/Purchase/WithUpdateStock.php | 39 ++++++++++-- database/seeders/RolePermissionSeeder.php | 9 +++ .../views/components/partials/audit.blade.php | 60 +++++++++++++++++++ .../studio/catalog/bottle/index.blade.php | 8 +++ .../studio/catalog/perfume/index.blade.php | 8 +++ .../studio/catalog/product/index.blade.php | 8 +++ .../studio/manage/purchase/form.blade.php | 7 +++ .../studio/manage/purchase/index.blade.php | 9 ++- routes/pages/studio.php | 6 ++ 18 files changed, 380 insertions(+), 26 deletions(-) create mode 100644 app/Enums/AuditStockStatus.php create mode 100644 app/Livewire/Studio/Catalog/Bottle/Audit.php create mode 100644 app/Livewire/Studio/Catalog/Perfume/Audit.php create mode 100644 app/Livewire/Studio/Catalog/Product/Audit.php create mode 100644 resources/views/components/partials/audit.blade.php diff --git a/app/Enums/AuditStockStatus.php b/app/Enums/AuditStockStatus.php new file mode 100644 index 0000000..5d065c2 --- /dev/null +++ b/app/Enums/AuditStockStatus.php @@ -0,0 +1,19 @@ + 'emerald', + self::DECREASED => 'red', + self::UPDATED => 'yellow', + }; + } +} diff --git a/app/Livewire/Datatable/Studio/Catalog/BottlesTable.php b/app/Livewire/Datatable/Studio/Catalog/BottlesTable.php index 2eef52c..2a17f19 100644 --- a/app/Livewire/Datatable/Studio/Catalog/BottlesTable.php +++ b/app/Livewire/Datatable/Studio/Catalog/BottlesTable.php @@ -82,15 +82,25 @@ public function columns(): array ArrayColumn::make('Outlet') ->data( fn ($value, $row) => $row->outlets->map(fn ($outlet) => [ + 'id' => $outlet->hash, + 'bottle_id' => $row->hash, 'name' => $outlet->name, 'stock' => currency($outlet->pivot->stock, ''), ])->toArray() ) - ->outputFormat(fn ($index, $value) => Blade::render(' -
- {{ $value["name"] }} - {{ $value["stock"] }} -
', ['value' => $value])) + ->outputFormat(function ($index, $value) { + $route = route('studio.catalog.bottle.audit', [$value['id'], $value['bottle_id']]); + + return Blade::render(' + + {{ $value["name"] }} + {{ $value["stock"] }} + ', [ + 'value' => $value, + 'route' => $route, + ]); + }) ->flexRow(['class' => 'gap-2 flex-wrap']), Column::make('Aksi') diff --git a/app/Livewire/Datatable/Studio/Catalog/PerfumesTable.php b/app/Livewire/Datatable/Studio/Catalog/PerfumesTable.php index e552f28..9c82e23 100644 --- a/app/Livewire/Datatable/Studio/Catalog/PerfumesTable.php +++ b/app/Livewire/Datatable/Studio/Catalog/PerfumesTable.php @@ -85,15 +85,25 @@ public function columns(): array ArrayColumn::make('Outlet') ->data( fn ($value, $row) => $row->outlets->map(fn ($outlet) => [ + 'id' => $outlet->hash, + 'perfume_id' => $row->hash, 'name' => $outlet->name, 'stock' => currency($outlet->pivot->stock, ''), ])->toArray() ) - ->outputFormat(fn ($index, $value) => Blade::render(' -
- {{ $value["name"] }} - {{ $value["stock"] }} -
', ['value' => $value])) + ->outputFormat(function ($index, $value) { + $route = route('studio.catalog.perfume.audit', [$value['id'], $value['perfume_id']]); + + return Blade::render(' + + {{ $value["name"] }} + {{ $value["stock"] }} + ', [ + 'value' => $value, + 'route' => $route, + ]); + }) ->flexRow(['class' => 'gap-2 flex-wrap']), Column::make('Aksi') diff --git a/app/Livewire/Datatable/Studio/Catalog/ProductsTable.php b/app/Livewire/Datatable/Studio/Catalog/ProductsTable.php index 93bcf32..22e7778 100644 --- a/app/Livewire/Datatable/Studio/Catalog/ProductsTable.php +++ b/app/Livewire/Datatable/Studio/Catalog/ProductsTable.php @@ -74,15 +74,25 @@ public function columns(): array ArrayColumn::make('Outlet') ->data( fn ($value, $row) => $row->outlets->map(fn ($outlet) => [ + 'id' => $outlet->hash, + 'product_id' => $row->hash, 'name' => $outlet->name, 'stock' => currency($outlet->pivot->stock, ''), ])->toArray() ) - ->outputFormat(fn ($index, $value) => Blade::render(' -
- {{ $value["name"] }} - {{ $value["stock"] }} -
', ['value' => $value])) + ->outputFormat(function ($index, $value) { + $route = route('studio.catalog.product.audit', [$value['id'], $value['product_id']]); + + return Blade::render(' + + {{ $value["name"] }} + {{ $value["stock"] }} + ', [ + 'value' => $value, + 'route' => $route, + ]); + }) ->flexRow(['class' => 'gap-2 flex-wrap']), Column::make('Aksi') diff --git a/app/Livewire/Studio/Catalog/Bottle/Audit.php b/app/Livewire/Studio/Catalog/Bottle/Audit.php new file mode 100644 index 0000000..f4e15a4 --- /dev/null +++ b/app/Livewire/Studio/Catalog/Bottle/Audit.php @@ -0,0 +1,54 @@ +label = $bottle->name; + + $this->activityLogs = Activity::where('subject_type', get_class($bottle)) + ->where('subject_id', $bottle->id) + ->where('properties->outlet_id', $outlet->id) + ->latest() + ->get() + ->map(function (Activity $activity) use ($outlet) { + + return [ + 'created_at' => $activity->created_at, + 'outlet' => $outlet->name, + 'event' => [ + 'label' => $activity->event, + 'color' => AuditStockStatus::from($activity->event)->color(), + ], + 'new_stock' => currency($activity->properties['new_stock']), + 'previous_stock' => currency($activity->properties['previous_stock']), + 'quantity_change' => currency($activity->properties['quantity_change']), + 'user' => $activity->causer?->profile?->full_name, + 'description' => $activity->description, + ]; + }) + ->toArray(); + } + + public function render() + { + return view('components.partials.audit', [ + 'pageTitle' => 'Audit '.$this->label, + 'module' => 'bottle', + ]); + } +} diff --git a/app/Livewire/Studio/Catalog/Perfume/Audit.php b/app/Livewire/Studio/Catalog/Perfume/Audit.php new file mode 100644 index 0000000..c2c1227 --- /dev/null +++ b/app/Livewire/Studio/Catalog/Perfume/Audit.php @@ -0,0 +1,54 @@ +label = $perfume->name; + + $this->activityLogs = Activity::where('subject_type', get_class($perfume)) + ->where('subject_id', $perfume->id) + ->where('properties->outlet_id', $outlet->id) + ->latest() + ->get() + ->map(function (Activity $activity) use ($outlet) { + + return [ + 'created_at' => $activity->created_at, + 'outlet' => $outlet->name, + 'event' => [ + 'label' => $activity->event, + 'color' => AuditStockStatus::from($activity->event)->color(), + ], + 'new_stock' => currency($activity->properties['new_stock']), + 'previous_stock' => currency($activity->properties['previous_stock']), + 'quantity_change' => currency($activity->properties['quantity_change']), + 'user' => $activity->causer?->profile?->full_name, + 'description' => $activity->description, + ]; + }) + ->toArray(); + } + + public function render() + { + return view('components.partials.audit', [ + 'pageTitle' => 'Audit '.$this->label, + 'module' => 'perfume', + ]); + } +} diff --git a/app/Livewire/Studio/Catalog/Product/Audit.php b/app/Livewire/Studio/Catalog/Product/Audit.php new file mode 100644 index 0000000..4685c20 --- /dev/null +++ b/app/Livewire/Studio/Catalog/Product/Audit.php @@ -0,0 +1,54 @@ +label = $product->name; + + $this->activityLogs = Activity::where('subject_type', get_class($product)) + ->where('subject_id', $product->id) + ->where('properties->outlet_id', $outlet->id) + ->latest() + ->get() + ->map(function (Activity $activity) use ($outlet) { + + return [ + 'created_at' => $activity->created_at, + 'outlet' => $outlet->name, + 'event' => [ + 'label' => $activity->event, + 'color' => AuditStockStatus::from($activity->event)->color(), + ], + 'new_stock' => currency($activity->properties['new_stock']), + 'previous_stock' => currency($activity->properties['previous_stock']), + 'quantity_change' => currency($activity->properties['quantity_change']), + 'user' => $activity->causer?->profile?->full_name, + 'description' => $activity->description, + ]; + }) + ->toArray(); + } + + public function render() + { + return view('components.partials.audit', [ + 'pageTitle' => 'Audit '.$this->label, + 'module' => 'product', + ]); + } +} diff --git a/app/Livewire/Studio/Manage/Order/Create.php b/app/Livewire/Studio/Manage/Order/Create.php index 93b4c2e..c986bf5 100644 --- a/app/Livewire/Studio/Manage/Order/Create.php +++ b/app/Livewire/Studio/Manage/Order/Create.php @@ -54,7 +54,7 @@ public function mount() $this->perfumes = Perfume::orderBy('name')->pluck('name', 'id')->toArray(); - $this->bottles = Bottle::orderBy('name')->pluck('name', 'id')->toArray(); + $this->bottles = Bottle::orderBy('size')->pluck('name', 'id')->toArray(); $this->products = Product::orderBy('name')->pluck('name', 'id')->toArray(); diff --git a/app/Livewire/Studio/Manage/Purchase/Create.php b/app/Livewire/Studio/Manage/Purchase/Create.php index 8ab5d39..8db3060 100644 --- a/app/Livewire/Studio/Manage/Purchase/Create.php +++ b/app/Livewire/Studio/Manage/Purchase/Create.php @@ -4,7 +4,6 @@ use App\Livewire\Forms\Studio\Manage\PurchaseForm; use App\Models\Bottle; -use App\Models\Outlet; use App\Models\Perfume; use App\Models\Product; use App\Traits\Purchase\WithAddItem; @@ -38,13 +37,13 @@ class Create extends Component public function mount() { - $this->outlets = Outlet::pluck('name', 'id')->toArray(); + $this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray(); - $this->perfumes = Perfume::pluck('name', 'id')->toArray(); + $this->perfumes = Perfume::orderBy('name')->pluck('name', 'id')->toArray(); - $this->products = Product::pluck('name', 'id')->toArray(); + $this->bottles = Bottle::orderBy('size')->pluck('name', 'id')->toArray(); - $this->bottles = Bottle::pluck('name', 'id')->toArray(); + $this->products = Product::orderBy('name')->pluck('name', 'id')->toArray(); $this->purchaseItems = $this->loadPurchaseItems(); diff --git a/app/Traits/Purchase/WithUpdateStock.php b/app/Traits/Purchase/WithUpdateStock.php index b71b644..7be4a43 100644 --- a/app/Traits/Purchase/WithUpdateStock.php +++ b/app/Traits/Purchase/WithUpdateStock.php @@ -26,18 +26,34 @@ public function increaseOutletStock($outlet, $item) return; } - $existing = $outlet->{$relation}()->where("{$relation}.id", $item->purchasable_id)->first(); + $existing = $outlet->{$relation}()->withPivot('stock')->where("{$relation}.id", $item->purchasable_id)->first(); if ($existing) { - $currentStock = $existing->pivot->stock ?? 0; + $previousStock = $existing->pivot->stock ?? 0; $outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [ - 'stock' => $currentStock + $quantity, + 'stock' => $previousStock + $quantity, ]); } else { + $previousStock = 0; $outlet->{$relation}()->attach($item->purchasable_id, [ 'stock' => $quantity, ]); } + + activity('Belanja') + ->performedOn($item->purchasable) + ->causedBy(auth()->user()) + ->withProperties([ + 'purchasable_id' => $item->purchasable_id, + 'purchasable_type' => $item->purchasable_type, + 'outlet_id' => $outlet->id, + 'quantity_change' => $quantity, + 'previous_stock' => $previousStock, + 'new_stock' => $previousStock + $quantity, + 'purchase_id' => $item->purchase_id ?? null, + ]) + ->event('Menambah') + ->log('Stok '.$item->purchasable->name.' di '.$outlet->name.' bertambah '.currency($quantity)); } public function decreaseOutletStock($outlet, $item) @@ -58,7 +74,7 @@ public function decreaseOutletStock($outlet, $item) return; } - $existing = $outlet->{$relation}()->where("{$relation}.id", $item->purchasable_id)->first(); + $existing = $outlet->{$relation}()->withPivot('stock')->where("{$relation}.id", $item->purchasable_id)->first(); if ($existing) { $currentStock = $existing->pivot->stock ?? 0; @@ -67,5 +83,20 @@ public function decreaseOutletStock($outlet, $item) 'stock' => $newStock, ]); } + + activity('Belanja') + ->performedOn($item->purchasable) + ->causedBy(auth()->user()) + ->withProperties([ + 'purchasable_id' => $item->purchasable_id, + 'purchasable_type' => $item->purchasable_type, + 'outlet_id' => $outlet->id, + 'quantity_change' => $quantity, + 'previous_stock' => $currentStock, + 'new_stock' => $newStock, + 'purchase_id' => $item->purchase_id, + ]) + ->event('Mengurangi') + ->log('Stok '.$item->purchasable->name.' di '.$outlet->name.' berkurang '.$quantity); } } diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index f8f99aa..fc4e1d8 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -73,16 +73,19 @@ public function run(): void 'create perfume', 'update perfume', 'delete perfume', + 'audit perfume', 'view product', 'create product', 'update product', 'delete product', + 'audit product', 'view bottle', 'create bottle', 'update bottle', 'delete bottle', + 'audit bottle', 'view expense', 'create expense', @@ -153,14 +156,17 @@ public function run(): void 'create perfume', 'update perfume', 'delete perfume', + 'audit perfume', 'create product', 'update product', 'delete product', + 'audit product', 'create bottle', 'update bottle', 'delete bottle', + 'audit bottle', ])); $admin->syncPermissions(array_diff($permissions, [ @@ -193,14 +199,17 @@ public function run(): void 'create perfume', 'update perfume', 'delete perfume', + 'audit perfume', 'create product', 'update product', 'delete product', + 'audit product', 'create bottle', 'update bottle', 'delete bottle', + 'audit bottle', 'view article', 'create article', diff --git a/resources/views/components/partials/audit.blade.php b/resources/views/components/partials/audit.blade.php new file mode 100644 index 0000000..e5acd7b --- /dev/null +++ b/resources/views/components/partials/audit.blade.php @@ -0,0 +1,60 @@ + +
+
+ {{ $pageTitle }} +
+
+ + Kembali + +
+
+ +
+ @forelse ($activityLogs as $log) + +
+
{{ $log['created_at'] }}
+
{{ $log['outlet'] }}
+
{{ $log['user'] }}
+ + {{ $log['event']['label'] }} + +
+ + + +
+
Stok Sebelumnya: {{ $log['previous_stock'] }}
+ +
Perubahan: {{ $log['quantity_change'] }}
+ +
Total Stok: {{ $log['new_stock'] }}
+
+ + + +

+ {{ $log['description'] }} +

+
+ @empty +
+ + + + Yahhh, sepertinya belum ada data yang tersedia. + +
+ @endforelse +
+ +
+@assets + +@endassets diff --git a/resources/views/livewire/studio/catalog/bottle/index.blade.php b/resources/views/livewire/studio/catalog/bottle/index.blade.php index 62831b4..3d8ce7d 100644 --- a/resources/views/livewire/studio/catalog/bottle/index.blade.php +++ b/resources/views/livewire/studio/catalog/bottle/index.blade.php @@ -14,6 +14,14 @@ class="text-sm">
+ + Informasi + + Untuk melihat harga pembelian, silakan klik pada bagian harga. Untuk melakukan audit stok, + silakan klik pada nama outlet. + + +
diff --git a/resources/views/livewire/studio/catalog/perfume/index.blade.php b/resources/views/livewire/studio/catalog/perfume/index.blade.php index 2d77fb7..3c18889 100644 --- a/resources/views/livewire/studio/catalog/perfume/index.blade.php +++ b/resources/views/livewire/studio/catalog/perfume/index.blade.php @@ -14,6 +14,14 @@ class="text-sm">
+ + Informasi + + Untuk melihat harga pembelian, silakan klik pada bagian harga. Untuk melakukan audit stok, + silakan klik pada nama outlet. + + +
diff --git a/resources/views/livewire/studio/catalog/product/index.blade.php b/resources/views/livewire/studio/catalog/product/index.blade.php index 90c816f..1043cbd 100644 --- a/resources/views/livewire/studio/catalog/product/index.blade.php +++ b/resources/views/livewire/studio/catalog/product/index.blade.php @@ -14,6 +14,14 @@ class="text-sm">
+ + Informasi + + Untuk melihat harga pembelian, silakan klik pada bagian harga. Untuk melakukan audit stok, + silakan klik pada nama outlet. + + +
diff --git a/resources/views/livewire/studio/manage/purchase/form.blade.php b/resources/views/livewire/studio/manage/purchase/form.blade.php index e1e433f..6ef1b7e 100644 --- a/resources/views/livewire/studio/manage/purchase/form.blade.php +++ b/resources/views/livewire/studio/manage/purchase/form.blade.php @@ -11,6 +11,13 @@
+ + Informasi + + Tolong perhatikan data yang Anda masukkan, karena tidak dapat diubah setelah disimpan. + + +
diff --git a/resources/views/livewire/studio/manage/purchase/index.blade.php b/resources/views/livewire/studio/manage/purchase/index.blade.php index 1c5039d..af15cb3 100644 --- a/resources/views/livewire/studio/manage/purchase/index.blade.php +++ b/resources/views/livewire/studio/manage/purchase/index.blade.php @@ -3,7 +3,7 @@
{{ $pageTitle }}
- @can ('create purchase') + @can('create purchase')
@@ -14,6 +14,13 @@ class="text-sm">
+ + Informasi + + Menghapus data belanja otomatis akan menambah stok. + + +
diff --git a/routes/pages/studio.php b/routes/pages/studio.php index 83f0dad..1991d27 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -1,14 +1,17 @@ name('create')->middleware('can:create perfume'); Route::get('/{perfume}/edit', PerfumeEdit::class)->name('edit')->middleware('can:update perfume'); Route::delete('/{perfume}/delete', PerfumeCreate::class)->name('delete')->middleware('can:delete perfume'); + Route::get('/{outlet}/{perfume}/audit', PerfumeAudit::class)->name('audit')->middleware('can:audit perfume'); }); Route::prefix('products')->name('product.')->group(function () { @@ -115,6 +119,7 @@ Route::get('/create', ProductCreate::class)->name('create')->middleware('can:create product'); Route::get('/{product}/edit', ProductEdit::class)->name('edit')->middleware('can:update product'); Route::delete('/{product}/delete', ProductCreate::class)->name('delete')->middleware('can:delete product'); + Route::get('/{outlet}/{product}/audit', ProductAudit::class)->name('audit')->middleware('can:audit product'); }); Route::prefix('bottles')->name('bottle.')->group(function () { @@ -122,6 +127,7 @@ Route::get('/create', BottleCreate::class)->name('create')->middleware('can:create bottle'); Route::get('/{bottle}/edit', BottleEdit::class)->name('edit')->middleware('can:update bottle'); Route::delete('/{bottle}/delete', BottleCreate::class)->name('delete')->middleware('can:delete bottle'); + Route::get('/{outlet}/{bottle}/audit', BottleAudit::class)->name('audit')->middleware('can:audit bottle'); }); });