feat(warehouse): membuat fitur gudang

This commit is contained in:
Yoga Pangestu 2026-01-01 15:27:16 +07:00
parent a2efc0f349
commit 77babcf44c
8 changed files with 345 additions and 4 deletions

View File

@ -1,6 +1,6 @@
<?php
namespace App\Livewire\Datatable\Manage;
namespace App\Livewire\Datatable\Master;
use App\Models\Warehouse;
use App\Traits\Datatable\WithAppendColumn;
@ -25,6 +25,12 @@ public function columns(): array
->label(function ($row) {
$actions = '';
if (auth()->user()->can('show warehouse')) {
$actions .= view('components.actions.table.show', [
'showRoute' => route('studio.master.warehouse.show', $row->hash),
])->render();
}
if (auth()->user()->can('update warehouse')) {
$actions .= view('components.actions.table.edit-modal', [
'id' => $row->hash,
@ -42,7 +48,7 @@ public function columns(): array
return $actions;
})
->html()
->hideIf(auth()->user()->cannot('update warehouse') && auth()->user()->cannot('delete warehouse')),
->hideIf(auth()->user()->cannot('view warehouse') && auth()->user()->cannot('update warehouse') && auth()->user()->cannot('delete warehouse')),
];
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Livewire\Studio\Master\Warehouse;
use App\Livewire\Forms\Studio\Master\WarehouseForm;
use App\Models\Warehouse as WarehouseModel;
use App\Traits\Authorization\WithAuthorization;
use App\Traits\Components\WithCloseModal;
use App\Traits\Components\WithConfirmation;
use App\Traits\Components\WithToast;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\Utilities\WithUpdatedData;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Gudang')]
class Index extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
public WarehouseForm $form;
public string $method = 'create';
public string $modalTitle = '';
public function create(): void
{
$this->canOrAbort('create warehouse');
$this->form->store();
$this->dispatch('refreshDatatable');
$this->toast('Gudang berhasil ditambahkan.');
Flux::modals()->close();
}
public function update(): void
{
$this->canOrAbort('update warehouse');
$this->form->update();
$this->dispatch('refreshDatatable');
$this->toast('Gudang berhasil diperbarui.');
Flux::modals()->close();
}
public function delete(WarehouseModel $warehouse): void
{
$this->canOrAbort('delete warehouse');
$this->form->warehouse = $warehouse;
$this->form->delete();
$this->dispatch('refreshDatatable');
$this->toast('Gudang berhasil dihapus.');
Flux::modals()->close();
}
#[On('modal:open')]
public function openModal(string $method, string $modalTitle, ?string $id = null): void
{
$this->resetValidation();
$this->resetErrorBag();
$this->method = $method;
$this->modalTitle = $modalTitle;
$id && $this->form->setWarehouse(WarehouseModel::byHashOrFail($id));
}
public function render(): View
{
return view('livewire.studio.master.warehouse.index', [
'pageTitle' => 'Gudang',
]);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Livewire\Studio\Master\Warehouse;
use App\Models\Warehouse;
use App\Traits\Authorization\WithAuthorization;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Detail Gudang')]
class Show extends Component
{
use WithAuthorization;
public Warehouse $warehouse;
public function mount(Warehouse $warehouse): void
{
$this->warehouse = $warehouse;
}
public function getStatsProperty()
{
$perfumesCount = $this->warehouse->perfumes()->count();
$productsCount = $this->warehouse->products()->count();
$bottlesCount = $this->warehouse->bottles()->count();
$totalItems = $perfumesCount + $productsCount + $bottlesCount;
return [
[
'title' => 'Total Item',
'value' => number_format($totalItems, 0, ',', '.'),
],
[
'title' => 'Parfum',
'value' => number_format($perfumesCount, 0, ',', '.'),
],
[
'title' => 'Produk',
'value' => number_format($productsCount, 0, ',', '.'),
],
[
'title' => 'Botol',
'value' => number_format($bottlesCount, 0, ',', '.'),
],
];
}
public function getItemsProperty()
{
$warehouse = $this->warehouse;
$perfumes = $warehouse->perfumes()->get()->map(function ($item) {
return [
'id' => 'perfume_'.$item->id,
'type' => 'Parfum',
'name' => $item->name,
'stock' => $item->pivot->stock ?? 0,
];
});
$products = $warehouse->products()->get()->map(function ($item) {
return [
'id' => 'product_'.$item->id,
'type' => 'Produk',
'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,
];
});
return $perfumes->concat($products)->concat($bottles);
}
public function render(): View
{
return view('livewire.studio.master.warehouse.show', [
'pageTitle' => 'Detail Gudang',
'stats' => $this->stats,
]);
}
}

View File

@ -0,0 +1,31 @@
<?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('perfume_warehouse', function (Blueprint $table) {
$table->id();
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete();
$table->foreignId('perfume_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('stock')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('perfume_warehouse');
}
};

View File

@ -0,0 +1,31 @@
<?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('product_warehouse', function (Blueprint $table) {
$table->id();
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete();
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('stock')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_warehouse');
}
};

View File

@ -0,0 +1,31 @@
<?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('bottle_warehouse', function (Blueprint $table) {
$table->id();
$table->foreignId('bottle_id')->constrained()->cascadeOnDelete();
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('stock')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bottle_warehouse');
}
};

View File

@ -14,7 +14,7 @@
</div>
<div class="mt-6">
<livewire:datatable.manage.warehouses-table />
<livewire:datatable.master.warehouses-table />
</div>
@include('components.modals.confirmation', [
@ -38,7 +38,8 @@
<flux:field>
<flux:label>Deskripsi</flux:label>
<flux:textarea placeholder="..." wire:model="form.description" autocomplete="off" />
<flux:textarea placeholder="Gudang utama untuk penyimpanan semua produk." wire:model="form.description"
autocomplete="off" />
<flux:error name="form.description" />
</flux:field>

View File

@ -0,0 +1,61 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
<div>
<flux:button href="{{ route('studio.master.warehouse.index') }}" wire:navigate class="text-sm">
Kembali
</flux:button>
</div>
</div>
<div class="mt-6">
@if (!empty($stats))
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
@foreach ($stats as $stat)
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700">
<flux:subheading>{{ $stat['title'] }}</flux:subheading>
<flux:heading size="xl" class="mb-2">{{ $stat['value'] }}</flux:heading>
</div>
@endforeach
</div>
@endif
<flux:card class="mt-8">
<div class="mb-4">
<flux:heading size="lg">Daftar Item</flux:heading>
<flux:subheading>Daftar semua item yang tersedia di gudang ini.</flux:subheading>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>Tipe</flux:table.column>
<flux:table.column>Item</flux:table.column>
<flux:table.column>Stok</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($this->items as $item)
<flux:table.row :key="$item['id']">
<flux:table.cell>
<flux:badge size="sm"
:color="$item['type'] === 'Parfum' ? 'fuchsia' : ($item['type'] === 'Produk' ? 'cyan' :
'zinc')">
{{ $item['type'] }}</flux:badge>
</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.row>
@empty
<flux:table.row>
<flux:table.cell colspan="3" class="text-center text-zinc-500">Tidak ada item di gudang
ini.
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
</div>
</flux:main>