feat: Implement warehouse item stock audit functionality with search, type filtering, and dedicated audit pages.
This commit is contained in:
parent
1ebb50139a
commit
60fe24b7bf
@ -62,7 +62,7 @@ public function render(): View
|
||||
{
|
||||
return view('components.sections.ui.audit', [
|
||||
'pageTitle' => 'Audit '.$this->label,
|
||||
'module' => 'bottle',
|
||||
'backRoute' => route('studio.catalog.bottle.index'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ public function render(): View
|
||||
{
|
||||
return view('components.sections.ui.audit', [
|
||||
'pageTitle' => 'Audit '.$this->label,
|
||||
'module' => 'perfume',
|
||||
'backRoute' => route('studio.catalog.perfume.index'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ public function render(): View
|
||||
{
|
||||
return view('components.sections.ui.audit', [
|
||||
'pageTitle' => 'Audit '.$this->label,
|
||||
'module' => 'product',
|
||||
'backRoute' => route('studio.catalog.product.index'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
68
app/Livewire/Studio/Master/Warehouse/Audit/Bottle.php
Normal file
68
app/Livewire/Studio/Master/Warehouse/Audit/Bottle.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Master\Warehouse\Audit;
|
||||
|
||||
use App\Enums\AuditStockStatus;
|
||||
use App\Models\Bottle as BottleModel;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
#[Title('Audit')]
|
||||
class Bottle extends Component
|
||||
{
|
||||
public string $label;
|
||||
|
||||
public Collection $activityLogs;
|
||||
|
||||
public function mount(Warehouse $warehouse, BottleModel $bottle): void
|
||||
{
|
||||
$this->label = $bottle->name.' di '.$warehouse->name;
|
||||
|
||||
$this->activityLogs = Activity::where('subject_type', get_class($bottle))
|
||||
->where('subject_id', $bottle->id)
|
||||
->where('properties->warehouse_id', $warehouse->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('components.sections.ui.audit', [
|
||||
'pageTitle' => 'Audit '.$this->label,
|
||||
'backRoute' => route('studio.master.warehouse.show', ['warehouse' => request()->route('warehouse')]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
68
app/Livewire/Studio/Master/Warehouse/Audit/Perfume.php
Normal file
68
app/Livewire/Studio/Master/Warehouse/Audit/Perfume.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Master\Warehouse\Audit;
|
||||
|
||||
use App\Enums\AuditStockStatus;
|
||||
use App\Models\Perfume as PerfumeModel;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
#[Title('Audit')]
|
||||
class Perfume extends Component
|
||||
{
|
||||
public string $label;
|
||||
|
||||
public Collection $activityLogs;
|
||||
|
||||
public function mount(Warehouse $warehouse, PerfumeModel $perfume): void
|
||||
{
|
||||
$this->label = $perfume->name.' di '.$warehouse->name;
|
||||
|
||||
$this->activityLogs = Activity::where('subject_type', get_class($perfume))
|
||||
->where('subject_id', $perfume->id)
|
||||
->where('properties->warehouse_id', $warehouse->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('components.sections.ui.audit', [
|
||||
'pageTitle' => 'Audit '.$this->label,
|
||||
'backRoute' => route('studio.master.warehouse.show', ['warehouse' => request()->route('warehouse')]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
68
app/Livewire/Studio/Master/Warehouse/Audit/Product.php
Normal file
68
app/Livewire/Studio/Master/Warehouse/Audit/Product.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Master\Warehouse\Audit;
|
||||
|
||||
use App\Enums\AuditStockStatus;
|
||||
use App\Models\Product as ProductModel;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
#[Title('Audit')]
|
||||
class Product extends Component
|
||||
{
|
||||
public string $label;
|
||||
|
||||
public Collection $activityLogs;
|
||||
|
||||
public function mount(Warehouse $warehouse, ProductModel $product): void
|
||||
{
|
||||
$this->label = $product->name.' di '.$warehouse->name;
|
||||
|
||||
$this->activityLogs = Activity::where('subject_type', get_class($product))
|
||||
->where('subject_id', $product->id)
|
||||
->where('properties->warehouse_id', $warehouse->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('components.sections.ui.audit', [
|
||||
'pageTitle' => 'Audit '.$this->label,
|
||||
'backRoute' => route('studio.master.warehouse.show', ['warehouse' => request()->route('warehouse')]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -15,12 +15,16 @@ class Show extends Component
|
||||
|
||||
public Warehouse $warehouse;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public array $type = [];
|
||||
|
||||
public function mount(Warehouse $warehouse): void
|
||||
{
|
||||
$this->warehouse = $warehouse;
|
||||
}
|
||||
|
||||
public function getStatsProperty()
|
||||
public function getStatsProperty(): array
|
||||
{
|
||||
$perfumesCount = $this->warehouse->perfumes()->count();
|
||||
$productsCount = $this->warehouse->products()->count();
|
||||
@ -50,33 +54,69 @@ public function getStatsProperty()
|
||||
public function getItemsProperty()
|
||||
{
|
||||
$warehouse = $this->warehouse;
|
||||
$search = $this->search;
|
||||
$typeFilter = $this->type;
|
||||
|
||||
$perfumes = $warehouse->perfumes()->get()->map(function ($item) {
|
||||
return [
|
||||
'id' => 'perfume_'.$item->id,
|
||||
'type' => 'Parfum',
|
||||
'name' => $item->name,
|
||||
'stock' => $item->pivot->stock ?? 0,
|
||||
];
|
||||
});
|
||||
$perfumes = collect();
|
||||
$products = collect();
|
||||
$bottles = collect();
|
||||
|
||||
$products = $warehouse->products()->get()->map(function ($item) {
|
||||
return [
|
||||
'id' => 'product_'.$item->id,
|
||||
'type' => 'Produk',
|
||||
'name' => $item->name,
|
||||
'stock' => $item->pivot->stock ?? 0,
|
||||
];
|
||||
});
|
||||
// Load perfumes if no type filter or 'perfume' is selected
|
||||
if (empty($typeFilter) || in_array('perfume', $typeFilter)) {
|
||||
$perfumes = $warehouse->perfumes()
|
||||
->when(! empty($search), function ($query) use ($search) {
|
||||
return $query->where('name', 'like', '%'.$search.'%');
|
||||
})
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => 'perfume_'.$item->id,
|
||||
'hash_id' => $item->hash,
|
||||
'type' => 'Parfum',
|
||||
'type_key' => 'perfume',
|
||||
'name' => $item->name,
|
||||
'stock' => $item->pivot->stock ?? 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$bottles = $warehouse->bottles()->get()->map(function ($item) {
|
||||
return [
|
||||
'id' => 'bottle_'.$item->id,
|
||||
'type' => 'Botol',
|
||||
'name' => $item->name,
|
||||
'stock' => $item->pivot->stock ?? 0,
|
||||
];
|
||||
});
|
||||
// Load products if no type filter or 'product' is selected
|
||||
if (empty($typeFilter) || in_array('product', $typeFilter)) {
|
||||
$products = $warehouse->products()
|
||||
->when(! empty($search), function ($query) use ($search) {
|
||||
return $query->where('name', 'like', '%'.$search.'%');
|
||||
})
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => 'product_'.$item->id,
|
||||
'hash_id' => $item->hash,
|
||||
'type' => 'Produk',
|
||||
'type_key' => 'product',
|
||||
'name' => $item->name,
|
||||
'stock' => $item->pivot->stock ?? 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
// Load bottles if no type filter or 'bottle' is selected
|
||||
if (empty($typeFilter) || in_array('bottle', $typeFilter)) {
|
||||
$bottles = $warehouse->bottles()
|
||||
->when(! empty($search), function ($query) use ($search) {
|
||||
return $query->where('name', 'like', '%'.$search.'%');
|
||||
})
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => 'bottle_'.$item->id,
|
||||
'hash_id' => $item->hash,
|
||||
'type' => 'Botol',
|
||||
'type_key' => 'bottle',
|
||||
'name' => $item->name,
|
||||
'stock' => $item->pivot->stock ?? 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return $perfumes->concat($products)->concat($bottles);
|
||||
}
|
||||
|
||||
@ -194,6 +194,7 @@ public function run(): void
|
||||
'show warehouse',
|
||||
'update warehouse',
|
||||
'delete warehouse',
|
||||
'audit stock warehouse',
|
||||
|
||||
];
|
||||
|
||||
@ -314,6 +315,7 @@ public function run(): void
|
||||
'show warehouse',
|
||||
'update warehouse',
|
||||
'delete warehouse',
|
||||
'audit stock warehouse',
|
||||
|
||||
'view stock history',
|
||||
'show stock history',
|
||||
@ -462,6 +464,7 @@ public function run(): void
|
||||
'show warehouse',
|
||||
'update warehouse',
|
||||
'delete warehouse',
|
||||
'audit stock warehouse',
|
||||
|
||||
'view stock history',
|
||||
'show stock history',
|
||||
|
||||
57
docs/WAREHOUSE_AUDIT_FEATURE.md
Normal file
57
docs/WAREHOUSE_AUDIT_FEATURE.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Fitur Audit Gudang
|
||||
|
||||
## Deskripsi
|
||||
Fitur audit gudang memungkinkan pengguna untuk melihat riwayat perubahan stok untuk setiap item (parfum, produk, botol) di gudang tertentu. Fitur ini mirip dengan fitur audit stock di outlet, tetapi disesuaikan untuk konteks gudang.
|
||||
|
||||
## File yang Dibuat
|
||||
|
||||
### 1. Livewire Components
|
||||
- `app/Livewire/Studio/Master/Warehouse/Audit/Perfume.php` - Component untuk audit parfum di gudang
|
||||
- `app/Livewire/Studio/Master/Warehouse/Audit/Product.php` - Component untuk audit produk di gudang
|
||||
- `app/Livewire/Studio/Master/Warehouse/Audit/Bottle.php` - Component untuk audit botol di gudang
|
||||
|
||||
### 2. View Template
|
||||
- `resources/views/components/sections/ui/warehouse-audit.blade.php` - Template view untuk menampilkan audit log
|
||||
|
||||
## File yang Dimodifikasi
|
||||
|
||||
### 1. Routes
|
||||
- `routes/pages/studio.php`
|
||||
- Menambahkan import untuk 3 audit components
|
||||
- Menambahkan 3 route baru untuk audit:
|
||||
- `studio.master.warehouse.perfume.audit`
|
||||
- `studio.master.warehouse.product.audit`
|
||||
- `studio.master.warehouse.bottle.audit`
|
||||
|
||||
### 2. Warehouse Show Component
|
||||
- `app/Livewire/Studio/Master/Warehouse/Show.php`
|
||||
- Mengupdate method `getItemsProperty()` untuk menambahkan `hash_id` dan `type_key` pada setiap item
|
||||
|
||||
### 3. Warehouse Show View
|
||||
- `resources/views/livewire/studio/master/warehouse/show.blade.php`
|
||||
- Menambahkan kolom "Aksi" pada tabel
|
||||
- Menambahkan tombol "Audit" untuk setiap item yang mengarah ke halaman audit
|
||||
|
||||
## Cara Penggunaan
|
||||
|
||||
1. Buka halaman detail gudang (`/studio/master/warehouses/{warehouse}/show`)
|
||||
2. Pada tabel daftar item, klik tombol "Audit" pada item yang ingin dilihat riwayatnya
|
||||
3. Halaman audit akan menampilkan:
|
||||
- Log aktivitas stok (increase, decrease, updated, deleted)
|
||||
- Informasi lokasi dan tipe lokasi
|
||||
- Stok awal, perubahan, dan stok akhir
|
||||
- User yang melakukan perubahan
|
||||
- Deskripsi aktivitas
|
||||
- Timestamp
|
||||
|
||||
## Permissions
|
||||
Fitur ini menggunakan permission yang sama dengan audit outlet:
|
||||
- `can:audit perfume`
|
||||
- `can:audit product`
|
||||
- `can:audit bottle`
|
||||
|
||||
## Catatan Teknis
|
||||
- Activity log menggunakan Spatie Activity Log package
|
||||
- Filter berdasarkan `properties->warehouse_id` untuk memastikan hanya menampilkan log yang relevan dengan gudang tertentu
|
||||
- Menggunakan `AuditStockStatus` enum untuk status perubahan stok
|
||||
- View menggunakan Flux UI components
|
||||
@ -4,7 +4,7 @@
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:button href="{{ route('studio.catalog.' . $module . '.index') }}" wire:navigate class="text-sm">
|
||||
<flux:button href="{{ $backRoute }}" wire:navigate class="text-sm">
|
||||
Kembali
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@ -22,6 +22,19 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<flux:input icon="magnifying-glass" placeholder="Cari item..." wire:model.live.debounce.500ms="search"
|
||||
autocomplete="off" />
|
||||
|
||||
<flux:checkbox.group wire:model.live="type" variant="buttons">
|
||||
<flux:checkbox value="perfume" label="Parfum" />
|
||||
<flux:checkbox value="product" label="Produk" />
|
||||
<flux:checkbox value="bottle" label="Botol" />
|
||||
</flux:checkbox.group>
|
||||
</div>
|
||||
|
||||
<flux:separator class="mb-6" />
|
||||
|
||||
<flux:card class="mt-8">
|
||||
<div class="mb-4">
|
||||
<flux:heading size="lg">Daftar Item</flux:heading>
|
||||
@ -33,8 +46,12 @@
|
||||
<flux:table.column>Tipe</flux:table.column>
|
||||
<flux:table.column>Item</flux:table.column>
|
||||
<flux:table.column>Stok</flux:table.column>
|
||||
@can('audit stock warehouse')
|
||||
<flux:table.column>Aksi</flux:table.column>
|
||||
@endcan
|
||||
</flux:table.columns>
|
||||
|
||||
|
||||
<flux:table.rows>
|
||||
@forelse ($this->items as $item)
|
||||
<flux:table.row :key="$item['id']">
|
||||
@ -46,10 +63,20 @@
|
||||
</flux:table.cell>
|
||||
<flux:table.cell class="font-medium">{{ $item['name'] }}</flux:table.cell>
|
||||
<flux:table.cell>{{ number_format($item['stock'], 0, ',', '.') }}</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
@can('audit stock warehouse')
|
||||
<flux:tooltip content="Audit">
|
||||
<flux:button size="sm" variant="primary" icon="clipboard-document-list"
|
||||
href="{{ route('studio.master.warehouse.' . $item['type_key'] . '.audit', ['warehouse' => $warehouse->hash, $item['type_key'] => $item['hash_id']]) }}"
|
||||
wire:navigate>
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
@endcan
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="3" class="text-center text-zinc-500">Tidak ada item di gudang
|
||||
<flux:table.cell colspan="4" class="text-center text-zinc-500">Tidak ada item di gudang
|
||||
ini.
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
@ -43,6 +43,9 @@
|
||||
use App\Livewire\Studio\Master\User\Create as UserCreate;
|
||||
use App\Livewire\Studio\Master\User\Edit as UserEdit;
|
||||
use App\Livewire\Studio\Master\User\Index as UserIndex;
|
||||
use App\Livewire\Studio\Master\Warehouse\Audit\Bottle as WarehouseBottleAudit;
|
||||
use App\Livewire\Studio\Master\Warehouse\Audit\Perfume as WarehousePerfumeAudit;
|
||||
use App\Livewire\Studio\Master\Warehouse\Audit\Product as WarehouseProductAudit;
|
||||
use App\Livewire\Studio\Master\Warehouse\Index as WarehouseIndex;
|
||||
use App\Livewire\Studio\Master\Warehouse\Show as WarehouseShow;
|
||||
use App\Livewire\Studio\Setting\Account as AccountComponent;
|
||||
@ -81,6 +84,9 @@
|
||||
Route::prefix('warehouses')->name('warehouse.')->group(function () {
|
||||
Route::get('/', WarehouseIndex::class)->name('index')->middleware('can:view warehouse');
|
||||
Route::get('/{warehouse}/show', WarehouseShow::class)->name('show')->middleware('can:view warehouse');
|
||||
Route::get('/{warehouse}/perfume/{perfume}/audit', WarehousePerfumeAudit::class)->name('perfume.audit')->middleware('can:audit perfume');
|
||||
Route::get('/{warehouse}/product/{product}/audit', WarehouseProductAudit::class)->name('product.audit')->middleware('can:audit product');
|
||||
Route::get('/{warehouse}/bottle/{bottle}/audit', WarehouseBottleAudit::class)->name('bottle.audit')->middleware('can:audit bottle');
|
||||
});
|
||||
|
||||
Route::prefix('users')->name('user.')->group(function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user