feat(warehouse): implementasi crud gudang

This commit is contained in:
Yoga Pangestu 2025-12-27 17:57:39 +07:00
parent 3c1b6fc915
commit d239d58029
12 changed files with 408 additions and 0 deletions

View File

@ -0,0 +1,53 @@
<?php
namespace App\Livewire\Datatable\Manage;
use App\Models\Warehouse;
use App\Traits\Datatable\WithAppendColumn;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
class WarehousesTable extends DataTableComponent
{
use WithAppendColumn, WithConfiguration, WithPrependColumn;
protected $model = Warehouse::class;
public function columns(): array
{
return [
Column::make('Nama', 'name')->searchable()->sortable(),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
if (auth()->user()->can('update warehouse')) {
$actions .= view('components.actions.table.edit-modal', [
'id' => $row->hash,
'method' => 'update',
'modalTitle' => 'Ubah Gudang',
])->render();
}
if (auth()->user()->can('delete warehouse')) {
$actions .= view('components.actions.table.delete', [
'id' => $row->hash,
])->render();
}
return $actions;
})
->html()
->hideIf(auth()->user()->cannot('update warehouse') && auth()->user()->cannot('delete warehouse')),
];
}
public function builder(): Builder
{
return Warehouse::select('id', 'name');
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Livewire\Forms\Studio\Master;
use App\Models\Warehouse;
use Livewire\Form;
class WarehouseForm extends Form
{
public ?Warehouse $warehouse;
public string $name = '';
public ?string $description = null;
public function setWarehouse(Warehouse $warehouse): void
{
$this->warehouse = $warehouse;
$this->name = $warehouse->name;
$this->description = $warehouse->description;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'description' => 'nullable',
];
}
public function validationAttributes(): array
{
return [
'name' => 'nama',
'description' => 'keterangan',
];
}
public function store(): void
{
$this->validate();
Warehouse::create($this->except('warehouse'));
$this->reset();
}
public function update(): void
{
$this->validate();
$this->warehouse->update($this->except('warehouse'));
}
public function delete(): void
{
$this->warehouse->delete();
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Livewire\Studio\Master;
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 Warehouse 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', [
'pageTitle' => 'Gudang',
]);
}
}

45
app/Models/Warehouse.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Dyrynda\Database\Support\CascadeSoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Veelasky\LaravelHashId\Eloquent\HashableId;
class Warehouse extends Model
{
use CascadeSoftDeletes, HasFactory, HashableId, SoftDeletes;
protected $guarded = ['id'];
protected $cascadeDeletes = ['perfumes', 'products', 'bottles', 'purchases', 'restocks'];
public function perfumes(): BelongsToMany
{
return $this->belongsToMany(Perfume::class)->withPivot('stock');
}
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class)->withPivot('stock');
}
public function bottles(): BelongsToMany
{
return $this->belongsToMany(Bottle::class)->withPivot('stock');
}
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class);
}
public function restocks(): HasMany
{
return $this->hasMany(Restock::class);
}
}

View File

@ -58,6 +58,13 @@ public function boot(): void
'match' => 'studio.master.outlet.*',
'can' => 'view outlet',
],
[
'label' => 'Gudang',
'icon' => 'building-office-2',
'route' => 'studio.master.warehouse.index',
'match' => 'studio.master.warehouse.*',
'can' => 'view warehouse',
],
[
'label' => 'Pegawai',
'icon' => 'users',
@ -190,6 +197,13 @@ public function boot(): void
'match' => 'studio.manage.purchase.*',
'can' => 'view purchase',
],
[
'label' => 'Restock Toko',
'icon' => 'arrows-right-left',
'route' => 'studio.manage.restock.index',
'match' => 'studio.manage.restock.*',
'can' => 'view restock',
],
[
'label' => 'Stock Opname',
'icon' => 'clipboard-document-check',

View File

@ -0,0 +1,27 @@
<?php
namespace Database\Factories;
use App\Models\Warehouse;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Warehouse>
*/
class WarehouseFactory extends Factory
{
protected $model = Warehouse::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->company().' Warehouse',
'description' => fake()->optional()->sentence(),
];
}
}

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('warehouses', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->text('description')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('warehouses');
}
};

View File

@ -11,6 +11,7 @@ public function run(): void
if (app()->environment(['staging', 'production'])) {
$this->call([
RolePermissionSeeder::class,
WarehouseSeeder::class,
OutletSeeder::class,
UserSeeder::class,
TierSeeder::class,
@ -19,6 +20,7 @@ public function run(): void
$this->call([
RolePermissionSeeder::class,
OutletSeeder::class,
WarehouseSeeder::class,
UserSeeder::class,
TierSeeder::class,
VoucherSeeder::class,

View File

@ -146,6 +146,16 @@ public function run(): void
'create broadcast',
'delete broadcast',
'view warehouse',
'create warehouse',
'update warehouse',
'delete warehouse',
'view restock',
'create restock',
'update restock',
'delete restock',
'view perfume feature',
'create perfume feature',
'update perfume feature',

View File

@ -0,0 +1,20 @@
<?php
namespace Database\Seeders;
use App\Models\Warehouse;
use Illuminate\Database\Seeder;
class WarehouseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Warehouse::create([
'name' => 'Gudang Pusat',
'description' => 'Gudang utama untuk penyimpanan semua produk.',
]);
}
}

View File

@ -0,0 +1,53 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@can('create warehouse')
<flux:modal.trigger name="form-modal">
<flux:button variant="primary" class="text-sm"
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Gudang'})">
Tambah
</flux:button>
</flux:modal.trigger>
@endcan
</div>
<div class="mt-6">
<livewire:datatable.manage.warehouses-table />
</div>
@include('components.modals.confirmation', [
'modalName' => 'delete-confirmation',
'modalTitle' => 'Apakah Anda yakin?',
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
'buttonVariant' => 'primary',
'buttonColor' => 'danger',
'buttonText' => 'Ya, Hapus',
])
<flux:modal name="form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
<div class="p-4 space-y-6">
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
<flux:field>
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Gudang Pusat" wire:model="form.name" autofocus autocomplete="off" />
<flux:error name="form.name" />
</flux:field>
<flux:field>
<flux:label>Deskripsi</flux:label>
<flux:textarea placeholder="..." wire:model="form.description" autocomplete="off" />
<flux:error name="form.description" />
</flux:field>
<div class="flex">
<flux:spacer />
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="{{ $method }}">
Simpan
</flux:button>
</div>
</div>
</flux:modal>
</flux:main>

View File

@ -50,6 +50,7 @@
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 as WarehouseComponent;
use App\Livewire\Studio\Setting\Account as AccountComponent;
use App\Livewire\Studio\Setting\Application as ApplicationComponent;
use Illuminate\Support\Facades\Route;
@ -72,6 +73,10 @@
Route::get('/{outlet}/edit', OutletEdit::class)->name('edit')->middleware('can:update outlet');
});
Route::prefix('warehouse')->name('warehouse.')->group(function () {
Route::get('/', WarehouseComponent::class)->name('index')->middleware('can:view warehouse');
});
Route::prefix('users')->name('user.')->group(function () {
Route::get('/', UserIndex::class)->name('index')->middleware('can:view user');
Route::get('/create', UserCreate::class)->name('create')->middleware('can:create user');