From c2bcb3d47b99984f27c580480c3aa6df17c3e248 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 10 Feb 2026 11:45:48 +0700 Subject: [PATCH] feat: Implement inventory history feature including dedicated pages, routes, navigation, and permissions. --- .../Studio/Stock/InventoryHistory/Index.php | 69 +++++++++++++++++ .../Studio/Stock/InventoryHistory/Show.php | 76 +++++++++++++++++++ app/Providers/ViewServiceProvider.php | 9 ++- database/seeders/RolePermissionSeeder.php | 8 ++ .../stock/inventory-history/index.blade.php | 65 ++++++++++++++++ .../stock/inventory-history/show.blade.php | 74 ++++++++++++++++++ routes/pages/studio.php | 7 ++ 7 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 app/Livewire/Studio/Stock/InventoryHistory/Index.php create mode 100644 app/Livewire/Studio/Stock/InventoryHistory/Show.php create mode 100644 resources/views/livewire/studio/stock/inventory-history/index.blade.php create mode 100644 resources/views/livewire/studio/stock/inventory-history/show.blade.php diff --git a/app/Livewire/Studio/Stock/InventoryHistory/Index.php b/app/Livewire/Studio/Stock/InventoryHistory/Index.php new file mode 100644 index 0000000..75276db --- /dev/null +++ b/app/Livewire/Studio/Stock/InventoryHistory/Index.php @@ -0,0 +1,69 @@ +when($this->search, fn ($q) => $q->where('name', 'like', '%'.$this->search.'%')) + ->when($this->typeFilter !== 'all' && $this->typeFilter !== 'perfume', fn ($q) => $q->whereNull('id')) + ->get() + ->map(function ($item) { + $item->type_label = 'Parfum'; + $item->type = 'perfume'; + $item->total_stock = $item->outlets->sum('pivot.stock') + $item->warehouses->sum('pivot.stock'); + + return $item; + }); + + $products = Product::with(['outlets', 'warehouses']) + ->when($this->search, fn ($q) => $q->where('name', 'like', '%'.$this->search.'%')) + ->when($this->typeFilter !== 'all' && $this->typeFilter !== 'product', fn ($q) => $q->whereNull('id')) + ->get() + ->map(function ($item) { + $item->type_label = 'Produk'; + $item->type = 'product'; + $item->total_stock = $item->outlets->sum('pivot.stock') + $item->warehouses->sum('pivot.stock'); + + return $item; + }); + + $bottles = Bottle::with(['outlets', 'warehouses']) + ->when($this->search, fn ($q) => $q->where('name', 'like', '%'.$this->search.'%')) + ->when($this->typeFilter !== 'all' && $this->typeFilter !== 'bottle', fn ($q) => $q->whereNull('id')) + ->get() + ->map(function ($item) { + $item->type_label = 'Botol'; + $item->type = 'bottle'; + $item->total_stock = $item->outlets->sum('pivot.stock') + $item->warehouses->sum('pivot.stock'); + + return $item; + }); + + $items = $perfumes->concat($products)->concat($bottles)->sortBy('name'); + + return view('livewire.studio.stock.inventory-history.index', [ + 'items' => $items, + ]); + } +} diff --git a/app/Livewire/Studio/Stock/InventoryHistory/Show.php b/app/Livewire/Studio/Stock/InventoryHistory/Show.php new file mode 100644 index 0000000..ace73c3 --- /dev/null +++ b/app/Livewire/Studio/Stock/InventoryHistory/Show.php @@ -0,0 +1,76 @@ +type = $type; + $this->item = match ($type) { + 'perfume' => Perfume::byHash($hash), + 'product' => Product::byHash($hash), + 'bottle' => Bottle::byHash($hash), + default => null, + }; + + if (! $this->item) { + abort(404); + } + + $this->activityLogs = Activity::where('subject_type', get_class($this->item)) + ->where('subject_id', $this->item->id) + ->latest() + ->get() + ->map(function (Activity $activity) { + $status = match ($activity->event) { + 'increase' => AuditStockStatus::INCREASED, + 'decrease' => AuditStockStatus::DECREASED, + 'created' => AuditStockStatus::INCREASED, + 'updated' => AuditStockStatus::UPDATED, + 'deleted' => AuditStockStatus::DELETED, + default => AuditStockStatus::tryFrom($activity->event), + }; + + $color = $status?->color() ?? 'gray'; + $label = $status?->label() ?? ucfirst($activity->event); + + $classes = match ($color) { + 'emerald' => 'bg-emerald-200 text-emerald-800 dark:bg-emerald-800 dark:text-emerald-200', + 'orange' => 'bg-orange-200 text-orange-800 dark:bg-orange-800 dark:text-orange-200', + 'yellow' => 'bg-yellow-200 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-200', + 'red' => 'bg-red-200 text-red-800 dark:bg-red-800 dark:text-red-200', + default => 'bg-gray-200 text-gray-800 dark:bg-gray-800 dark:text-gray-200', + }; + + $activity->event = [ + 'label' => $label, + 'color' => $color, + 'classes' => $classes, + ]; + + return $activity; + }); + } + + public function render(): View + { + return view('livewire.studio.stock.inventory-history.show'); + } +} diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 8636b59..20b903c 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -184,7 +184,7 @@ public function boot(): void ], ], [ - 'heading' => 'Kelola', + 'heading' => 'Stok', 'items' => [ [ 'label' => 'Belanja', @@ -214,6 +214,13 @@ public function boot(): void 'match' => 'studio.stock.stock_opname.*', 'can' => 'view stock opname', ], + [ + 'label' => 'Riwayat Stok', + 'icon' => 'clock', + 'route' => 'studio.stock.inventory_history.index', + 'match' => 'studio.stock.inventory_history.*', + 'can' => 'view stock history', + ], ], ], [ diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 1b902c7..ec4d645 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -160,6 +160,9 @@ public function run(): void 'approve stock opname', 'manage stock opname', + 'view stock history', + 'show stock history', + 'view studio overview', 'view testimonial', @@ -312,6 +315,9 @@ public function run(): void 'update warehouse', 'delete warehouse', + 'view stock history', + 'show stock history', + ])); $admin->syncPermissions(array_diff($permissions, [ @@ -454,6 +460,8 @@ public function run(): void 'update warehouse', 'delete warehouse', + 'view stock history', + 'show stock history', ])); $partner->syncPermissions([ diff --git a/resources/views/livewire/studio/stock/inventory-history/index.blade.php b/resources/views/livewire/studio/stock/inventory-history/index.blade.php new file mode 100644 index 0000000..0a98378 --- /dev/null +++ b/resources/views/livewire/studio/stock/inventory-history/index.blade.php @@ -0,0 +1,65 @@ + +
+
+ Riwayat Stok + Lihat histori perubahan stok untuk semua barang. +
+
+ +
+ + +
+ + Semua Tipe + Parfum + Produk + Botol + +
+
+ +
+ + + + Nama Barang + Tipe + Stok Saat Ini + Aksi + + + + @forelse ($items as $item) + + {{ $item->name }} + + {{ $item->type_label }} + + + {{ formatCurrencyNumber($item->total_stock) }} + + + + Riwayat + + + + @empty + + +
+ + Tidak ada barang ditemukan. +
+
+
+ @endforelse +
+
+
+
+
diff --git a/resources/views/livewire/studio/stock/inventory-history/show.blade.php b/resources/views/livewire/studio/stock/inventory-history/show.blade.php new file mode 100644 index 0000000..bf5c72c --- /dev/null +++ b/resources/views/livewire/studio/stock/inventory-history/show.blade.php @@ -0,0 +1,74 @@ + +
+
+ Riwayat Stok: {{ $item->name }} + Riwayat perubahan stok di berbagai lokasi. +
+
+ + Kembali + +
+
+ +
+ @forelse ($activityLogs as $log) + +
+
{{ $log->log_name }}
+
{{ formatDateLocalized($log->created_at) }}
+
+ +
+
+ {{ $log->properties['location_name'] ?? 'Lokasi Tidak Diketahui' }} + ({{ ucfirst($log->properties['location_type'] ?? '') }}) +
+ + {{ $log->event['label'] }} + +
+ + + +
+
Awal: {{ formatCurrencyNumber($log->properties['previous_stock'] ?? 0) }} +
+
+ + + +
+ +

+ {{ $log->causer?->employee?->full_name ?? ($log->causer?->name ?? 'System') }} +

+
+ +

+ "{{ $log->description }}" +

+
+ @empty + @include('components.animations.lottie.boxing-cat') +
+ Belum ada riwayat tercatat untuk barang ini. +
+ @endforelse +
+
+ +@assets + +@endassets diff --git a/routes/pages/studio.php b/routes/pages/studio.php index 22b0dbe..fe7420b 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -47,6 +47,8 @@ use App\Livewire\Studio\Master\Warehouse\Show as WarehouseShow; use App\Livewire\Studio\Setting\Account as AccountComponent; use App\Livewire\Studio\Setting\Application as ApplicationComponent; +use App\Livewire\Studio\Stock\InventoryHistory\Index as InventoryHistoryIndex; +use App\Livewire\Studio\Stock\InventoryHistory\Show as InventoryHistoryShow; use App\Livewire\Studio\Stock\Purchase\Create as PurchaseCreate; use App\Livewire\Studio\Stock\Purchase\Index as PurchaseIndex; use App\Livewire\Studio\Stock\Restock\Create as RestockCreate; @@ -192,6 +194,11 @@ Route::get('/{stockOpname}/show', StockOpnameShow::class)->name('show')->middleware('can:show stock opname'); Route::get('/{stockOpname}/manage', StockOpnameManage::class)->name('manage')->middleware('can:manage stock opname'); }); + + Route::prefix('inventory-history')->name('inventory_history.')->group(function () { + Route::get('/', InventoryHistoryIndex::class)->name('index')->middleware('can:view stock history'); + Route::get('/{type}/{hash}/show', InventoryHistoryShow::class)->name('show')->middleware('can:show stock history'); + }); }); Route::prefix('information')->name('studio.information.')->group(function () {