feat(purchase): implementasi fitur belanja/restock

-membuat relasi polimorfirk
-membuat beberapa trait untuk kebutuhan crud
-dan lainnya tentang implementasi fitur ini
This commit is contained in:
Yoga Pangestu 2025-10-04 21:09:20 +07:00
parent b62b86a6ae
commit 33e138be42
21 changed files with 935 additions and 0 deletions

View File

@ -0,0 +1,77 @@
<?php
namespace App\Livewire\Datatable\Studio\Manage;
use App\Models\Purchase;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use App\Traits\WithMediaHandler;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
use Rappasoft\LaravelLivewireTables\Views\Columns\ArrayColumn;
class PurchasesTable extends DataTableComponent
{
use WithConfiguration, WithMediaHandler, WithPrependColumn;
protected $model = Purchase::class;
public function columns(): array
{
return [
Column::make('Outlet', 'outlet.name')->searchable(),
Column::make('Nomor Faktur', 'invoice_number')->searchable(),
Column::make('Tanggal Belanja', 'purchase_date')
->format(fn ($value) => formatDate($value))
->searchable()
->sortable(),
Column::make('Total', 'total')
->format(fn ($value) => currency($value, 'Rp'))
->searchable()
->sortable(),
ArrayColumn::make('Item')
->data(
fn ($value, $row) => $row->items->map(fn ($item) => [
'name' => $item->purchasable?->name,
'quantity' => currency($item->quantity, ''),
'unit_price' => currency($item->unit_price, 'Rp'),
'total_price' => currency($item->total_price, 'Rp'),
])->toArray()
)
->outputFormat(
fn ($index, $value) => "
<div class='text-[13px] leading-tight mb-1'>
<div class='font-bold text-gray-800 dark:text-white'>{$value['name']}</div>
<div class='text-gray-400 text-[12px]'>{$value['quantity']} x {$value['unit_price']}</div>
<div class='text-gray-400 text-[12px]'>{$value['total_price']}</div>
</div>"
)
->flexCol(['class' => 'flex-col gap-3']),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
if (auth()->user()->can('delete purchase')) {
$actions .= view('components.datatables.delete', [
'id' => $row->hash,
'deleteRoute' => route('studio.manage.purchase.delete', $row->hash),
])->render();
}
return $actions;
})
->html(),
];
}
public function builder(): Builder
{
return Purchase::select('purchases.id', 'invoice_number', 'purchase_date', 'total')->with('outlet');
}
}

View File

@ -0,0 +1,126 @@
<?php
namespace App\Livewire\Forms\Studio\Manage;
use App\Models\Purchase;
use App\Rules\UnsignedInteger;
use App\Traits\Purchase\WithUpdateStock;
use App\Traits\WithMediaHandler;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Form;
class PurchaseForm extends Form
{
use WithMediaHandler, WithUpdateStock;
public ?Purchase $purchase = null;
public ?string $invoice_number = null;
public ?string $note = null;
public string $purchase_date = '';
public string $total = '';
public string $outlet_id = '';
public array $image = [];
public string $perfume_id = '';
public string $quantity_perfume = '';
public string $product_id = '';
public string $quantity_product = '';
public string $bottle_id = '';
public string $quantity_bottle = '';
public string $item_id = '';
public string $quantity_edit = '';
public $cartItems;
public function rules(): array
{
return [
'invoice_number' => ['nullable', 'string', 'max:50'],
'note' => ['nullable', 'string', 'max:100'],
'purchase_date' => ['required', 'date'],
'outlet_id' => ['required', Rule::exists('outlets', 'id')],
'total' => ['required', new UnsignedInteger],
'image' => ['nullable', 'array', 'max:1'],
];
}
public function validationAttributes(): array
{
return [
'invoice_number' => 'nomor faktur',
'note' => 'catatan',
'purchase_date' => 'tanggal belanja',
'outlet_id' => 'outlet',
'image' => 'gambar',
];
}
public function setPurchase(Purchase $purchase)
{
$this->purchase = $purchase;
$this->invoice_number = $purchase->invoice_number;
$this->note = $purchase->note;
$this->purchase_date = $purchase->purchase_date;
$this->outlet_id = $purchase->outlet_id;
$this->total = $purchase->total;
$this->image = $this->mapMediaCollection($purchase->getMedia('image'));
}
public function store()
{
$this->validate();
DB::transaction(function () {
$purchase = Purchase::create($this->prepareSavedData());
$purchase->load('outlet');
$this->uploadMedia($this->image, $purchase, 'image');
foreach ($this->cartItems as $item) {
$item->update(['purchase_id' => $purchase->id]);
$this->increaseOutletStock($purchase->outlet, $item);
}
});
}
public function update()
{
$this->validate();
DB::transaction(function () {
$this->purchase->update($this->prepareSavedData());
$this->syncMedia($this->image, $this->purchase, 'image');
$this->uploadMedia($this->image, $this->purchase, 'image');
});
}
private function prepareSavedData(): array
{
return [
'invoice_number' => $this->invoice_number,
'note' => $this->note,
'outlet_id' => $this->outlet_id,
'purchase_date' => $this->purchase_date,
'total' => replaceCurrency($this->total),
];
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Livewire\Studio\Manage\Purchase;
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\Models\PurchaseItem;
use App\Traits\Purchase\WithAddItem;
use App\Traits\Purchase\WithDeleteItem;
use App\Traits\Purchase\WithUpdateItem;
use App\Traits\WithAuthorization;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Tambah Belanja')]
class Create extends Component
{
use WithAddItem, WithAuthorization, WithConfirmation, WithDeleteItem, WithToast, WithUpdatedData, WithUpdateItem;
public PurchaseForm $form;
public array $outlets = [];
public array $perfumes = [];
public array $products = [];
public array $bottles = [];
public function mount()
{
$this->outlets = Outlet::pluck('name', 'id')->toArray();
$this->perfumes = Perfume::pluck('name', 'id')->toArray();
$this->products = Product::pluck('name', 'id')->toArray();
$this->bottles = Bottle::pluck('name', 'id')->toArray();
$this->form->cartItems = PurchaseItem::whereNull('purchase_id')->get();
$this->form->total = currency($this->form->cartItems->sum('total_price'), '');
}
public function save()
{
if ($this->form->cartItems->isEmpty()) {
$this->toast('Keranjang tidak boleh kosong.', 'Gagal', 'danger');
return;
}
$this->canOrAbort('create purchase');
$this->form->store();
$this->dispatch('refreshDatatable');
$this->toast('Belanja berhasil ditambahkan.');
$this->redirectRoute('studio.manage.purchase.index', navigate: true);
}
public function render()
{
return view('livewire.studio.manage.purchase.form', [
'pageTitle' => 'Tambah Belanja',
]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Livewire\Studio\Manage\Purchase;
use App\Models\Purchase;
use App\Traits\WithCloseModal;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Flux\Flux;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Belanja')]
class Index extends Component
{
use WithCloseModal, WithConfirmation, WithToast;
public function delete(Purchase $purchase)
{
$purchase->delete();
$this->dispatch('refreshDatatable');
$this->toast('Belanja berhasil dihapus.');
Flux::modals()->close();
}
public function render()
{
return view('livewire.studio.manage.purchase.index', [
'pageTitle' => 'Belanja',
]);
}
}

View File

@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -40,4 +41,9 @@ public function outlets(): BelongsToMany
{
return $this->belongsToMany(Outlet::class)->withPivot('stock');
}
public function purchaseItems(): MorphMany
{
return $this->morphMany(PurchaseItem::class, 'purchasable');
}
}

View File

@ -76,4 +76,9 @@ public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class);
}
}

View File

@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -50,4 +51,9 @@ public function outlets(): BelongsToMany
{
return $this->belongsToMany(Outlet::class)->withPivot('stock');
}
public function purchaseItems(): MorphMany
{
return $this->morphMany(PurchaseItem::class, 'purchasable');
}
}

View File

@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -37,4 +38,9 @@ public function outlets(): BelongsToMany
{
return $this->belongsToMany(Outlet::class)->withPivot('stock');
}
public function purchaseItems(): MorphMany
{
return $this->morphMany(PurchaseItem::class, 'purchasable');
}
}

36
app/Models/Purchase.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Veelasky\LaravelHashId\Eloquent\HashableId;
class Purchase extends Model implements HasMedia
{
use HasFactory, HashableId, InteractsWithMedia, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'total' => 'int',
];
}
public function outlet(): BelongsTo
{
return $this->belongsTo(Outlet::class);
}
public function items(): HasMany
{
return $this->hasMany(PurchaseItem::class);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class PurchaseItem extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'quantity' => 'int',
'unit_price' => 'int',
'total_price' => 'int',
];
}
public function purchase(): BelongsTo
{
return $this->belongsTo(Purchase::class);
}
public function purchasable(): MorphTo
{
return $this->morphTo();
}
}

View File

@ -130,6 +130,13 @@ public function boot(): void
'match' => 'studio.manage.article.*',
'can' => 'view article',
],
[
'label' => 'Belanja',
'icon' => 'shopping-bag',
'route' => 'studio.manage.purchase.index',
'match' => 'studio.manage.purchase.*',
'can' => 'view purchase',
],
],
],
];

View File

@ -0,0 +1,100 @@
<?php
namespace App\Traits\Purchase;
use App\Models\Bottle;
use App\Models\Perfume;
use App\Models\Product;
use App\Models\PurchaseItem;
use Illuminate\Support\Str;
trait WithAddItem
{
public function addItem(string $type)
{
switch ($type) {
case 'parfum':
$model = Perfume::class;
$id = $this->form->perfume_id;
$quantity = replaceCurrency($this->form->quantity_perfume);
break;
case 'produk':
$model = Product::class;
$id = $this->form->product_id;
$quantity = replaceCurrency($this->form->quantity_product);
break;
case 'botol':
$model = Bottle::class;
$id = $this->form->bottle_id;
$quantity = replaceCurrency($this->form->quantity_bottle);
break;
default:
$this->toast('Tipe item tidak dikenal.', 'Gagal', 'danger');
return;
}
$itemModel = $model::find($id);
if (! $itemModel) {
$this->toast(Str::ucfirst($type).' tidak ditemukan, silakan periksa kembali.', 'Gagal', 'danger');
return;
}
$costPrice = (int) $itemModel->cost_price;
if ($quantity <= 0) {
$this->toast('Jumlah '.$type.' tidak valid, silakan periksa kembali.', 'Gagal', 'danger');
return;
}
if ($costPrice <= 0) {
$this->toast('Harga '.$type.' tidak valid, silakan periksa kembali.', 'Gagal', 'danger');
return;
}
$existingItem = $this->form->cartItems
->where('purchasable_type', $model)
->where('purchasable_id', $itemModel->id)
->first();
if ($existingItem) {
$existingItem->quantity += $quantity;
$existingItem->total_price = $existingItem->unit_price * $existingItem->quantity;
$existingItem->save();
$existingItem->refresh();
$this->form->cartItems = $this->form->cartItems->map(fn ($i) => $i->id === $existingItem->id ? $existingItem : $i);
$this->toast('Jumlah '.$type.' diperbarui di keranjang.', 'Berhasil');
} else {
$item = PurchaseItem::create([
'purchasable_id' => $itemModel->id,
'purchasable_type' => $model,
'quantity' => $quantity,
'unit_price' => $costPrice,
'total_price' => $costPrice * $quantity,
]);
$this->form->cartItems->push($item);
$this->toast(Str::ucfirst($type).' ditambahkan ke keranjang.', 'Berhasil');
}
$this->form->total = currency($this->form->cartItems->sum('total_price'), '');
$this->reset('form.perfume_id');
$this->reset('form.product_id');
$this->reset('form.bottle_id');
$this->reset('form.quantity_perfume');
$this->reset('form.quantity_product');
$this->reset('form.quantity_bottle');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Traits\Purchase;
use App\Models\PurchaseItem;
trait WithDeleteItem
{
public function deleteItem(PurchaseItem $item)
{
$this->form->cartItems = $this->form->cartItems->reject(fn ($i) => $i->id === $item->id);
$item->delete();
$this->form->total = currency($this->form->cartItems->sum('total_price'), '');
$this->toast('Item berhasil dihapus.');
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Traits\Purchase;
use App\Models\PurchaseItem;
use Flux\Flux;
use Livewire\Attributes\On;
trait WithUpdateItem
{
public string $modalTitle = 'Ubah Item';
#[On('modal:open')]
public function openModal(PurchaseItem $item)
{
$this->modalTitle = $item->purchasable->name;
$this->form->item_id = $item->id;
$this->form->quantity_edit = currency($item->quantity, '');
}
public function closeModal()
{
$this->reset('form.item_id');
}
public function updateItem()
{
$item = PurchaseItem::find($this->form->item_id);
$item->update([
'quantity' => replaceCurrency($this->form->quantity_edit),
'total_price' => (int) $item->purchasable->cost_price * (int) replaceCurrency($this->form->quantity_edit),
]);
$item->refresh();
$this->form->cartItems = $this->form->cartItems->map(fn ($i) => $i->id === $item->id ? $item : $i);
$this->form->total = currency($this->form->cartItems->sum('total_price'), '');
$this->toast('Item berhasil diperbarui.');
Flux::modals('form-modal')->close();
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Traits\Purchase;
use App\Models\Bottle;
use App\Models\Perfume;
use App\Models\Product;
trait WithUpdateStock
{
public function increaseOutletStock($outlet, $item)
{
$quantity = $item->quantity;
switch ($item->purchasable_type) {
case Perfume::class:
$relation = 'perfumes';
break;
case Product::class:
$relation = 'products';
break;
case Bottle::class:
$relation = 'bottles';
break;
default:
return;
}
$existing = $outlet->{$relation}()
->where("{$relation}.id", $item->purchasable_id)
->first();
if ($existing) {
$currentStock = $existing->pivot->stock ?? 0;
$outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [
'stock' => $currentStock + $quantity,
]);
} else {
$outlet->{$relation}()->attach($item->purchasable_id, [
'stock' => $quantity,
]);
}
}
}

View File

@ -0,0 +1,34 @@
<?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('purchases', function (Blueprint $table) {
$table->id();
$table->foreignId('outlet_id')->constrained()->cascadeOnDelete();
$table->string('invoice_number', 50)->nullable();
$table->date('purchase_date');
$table->unsignedInteger('total');
$table->string('note', 100)->nullable();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('purchases');
}
};

View File

@ -0,0 +1,34 @@
<?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('purchase_items', function (Blueprint $table) {
$table->id();
$table->foreignId('purchase_id')->nullable()->constrained()->cascadeOnDelete();
$table->morphs('purchasable');
$table->unsignedInteger('quantity');
$table->unsignedInteger('unit_price');
$table->unsignedInteger('total_price');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('purchase_items');
}
};

View File

@ -88,6 +88,10 @@ public function run(): void
'create article',
'update article',
'delete article',
'view purchase',
'create purchase',
'delete purchase',
];
foreach ($permissions as $permission) {

View File

@ -0,0 +1,209 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
<div>
<flux:button href="{{ route('studio.manage.purchase.index') }}" wire:navigate.hover class="text-sm">
Kembali
</flux:button>
</div>
</div>
<div class="mt-6">
<div class="flex flex-col lg:flex-row gap-4">
<div class="w-full lg:w-3/4 space-y-4">
<flux:card class="space-y-6 p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:input label="Nomor Invoice" placeholder="Masukkan nomor invoice"
wire:model.live.debounce.500ms="form.invoice_number" autofocus autocomplete="off" />
<flux:input label="Catatan" placeholder="Masukkan catatan"
wire:model.live.debounce.500ms="form.note" autocomplete="off" />
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<flux:field>
<flux:label>Tanggal Belanja <span class="text-red-500 ms-1">*</span></flux:label>
<flux:date-picker with-today wire:model.live.debounce.500ms="form.purchase_date"
autocomplete="off" locale="id-ID" selectable-header />
<flux:error name="form.purchase_date" />
</flux:field>
<flux:field>
<flux:label>Total <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input.group>
<flux:input.group.prefix>Rp</flux:input.group.prefix>
<flux:input placeholder="Masukkan total" x-mask:dynamic="$money($input, ',')"
wire:model.live.debounce.500ms="form.total" autocomplete="off" />
</flux:input.group>
<flux:error name="form.total" />
</flux:field>
<flux:field>
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
<flux:select variant="listbox" searchable placeholder="Pilih Outlet"
wire:model.live.debounce.500ms="form.outlet_id">
@foreach ($outlets as $key => $value)
<flux:select.option value="{{ $key }}">{{ $value }}
</flux:select.option>
@endforeach
</flux:select>
<flux:error name="form.outlet_id" />
</flux:field>
</div>
</flux:card>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<flux:card class="space-y-6 p-6">
<flux:select label="Parfum" variant="listbox" searchable placeholder="Cari parfum..."
wire:model.live="form.perfume_id">
@foreach ($perfumes as $key => $perfume)
<flux:select.option value="{{ $key }}">{{ $perfume }}</flux:select.option>
@endforeach
</flux:select>
<flux:input label="Kuantitas" placeholder="Masukkan kuantitas parfum"
wire:model.live="form.quantity_perfume" autocomplete="off"
x-mask:dynamic="$money($input, ',')" />
<div class="flex justify-end">
<flux:button variant="primary" color="zinc" wire:click="addItem('parfum')">
Tambah
</flux:button>
</div>
</flux:card>
<flux:card class="space-y-6 p-6">
<flux:select label="Produk" variant="listbox" searchable placeholder="Cari produk..."
wire:model.live="form.product_id">
@foreach ($products as $key => $product)
<flux:select.option value="{{ $key }}">{{ $product }}</flux:select.option>
@endforeach
</flux:select>
<flux:input label="Kuantitas" placeholder="Masukkan kuantitas produk"
wire:model.live="form.quantity_product" autocomplete="off"
x-mask:dynamic="$money($input, ',')" />
<div class="flex justify-end">
<flux:button variant="primary" color="zinc" wire:click="addItem('produk')">
Tambah
</flux:button>
</div>
</flux:card>
<flux:card class="space-y-6 p-6">
<flux:select label="Botol" variant="listbox" searchable placeholder="Cari botol..."
wire:model.live="form.bottle_id">
@foreach ($bottles as $key => $bottle)
<flux:select.option value="{{ $key }}">{{ $bottle }}</flux:select.option>
@endforeach
</flux:select>
<flux:input label="Kuantitas" placeholder="Masukkan kuantitas botol"
wire:model.live="form.quantity_bottle" autocomplete="off"
x-mask:dynamic="$money($input, ',')" />
<div class="flex justify-end">
<flux:button variant="primary" color="zinc" wire:click="addItem('botol')">
Tambah
</flux:button>
</div>
</flux:card>
</div>
</div>
<div class="w-full lg:w-1/3 space-y-4">
<flux:card class="space-y-6 p-6">
<div class="space-y-3">
<h3 class="text-sm font-medium">Gambar</h3>
<div class="dropzone-wrapper">
<livewire:dropzone wire:model="form.image" :rules="['image', 'mimes:png,jpeg', 'max:10420']" :max-files="1"
:key="'image'" :files="$form->image" />
@error('form.image')
<div class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
{{ $message }}
</div>
@enderror
</div>
</div>
</flux:card>
<flux:card class="space-y-6 p-6">
<h3 class="text-sm font-medium">Keranjang</h3>
<div class="divide-y">
<flux:table>
<flux:table.columns>
<flux:table.column>Item</flux:table.column>
<flux:table.column>Harga</flux:table.column>
<flux:table.column>Aksi</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($form->cartItems as $item)
<flux:table.row>
<flux:table.cell>
<flux:heading>
<div>
<div>{{ $item->purchasable->name }}</div>
<span
class="text-xs text-gray-400">{{ currency($item->quantity, '') }}
x
{{ currency($item->purchasable->cost_price, 'Rp') }}</span>
</div>
</flux:heading>
</flux:table.cell>
<flux:table.cell>
{{ currency($item->total_price, 'Rp') }}
</flux:table.cell>
<flux:table.cell class="space-x-2">
<flux:modal.trigger name="form-modal">
<flux:tooltip content="Ubah">
<flux:button variant="primary" color="yellow" icon="pencil-square"
size="sm"
wire:click="$dispatch('modal:open', {'item': '{{ $item->id }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
<flux:button variant="danger" icon="trash" size="sm"
wire:click="deleteItem('{{ $item->id }}')">
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4" class="text-center">
<span class="text-sm text-zinc-500 italic">Keranjang Kosong...</span>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</div>
</flux:card>
</div>
</div>
<div class="flex justify-start mt-4">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="save">
Simpan
</flux:button>
</div>
</div>
<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">Ubah {{ $modalTitle }}</flux:heading>
<flux:input label="Kuantitas" placeholder="Masukkan kuantitas" wire:model.live="form.quantity_edit"
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
<div class="flex">
<flux:spacer />
<flux:button variant="primary" color="zinc" class="sm:w-auto cursor-pointer"
wire:click="updateItem">
Perbarui
</flux:button>
</div>
</div>
</flux:modal>
</flux:main>

View File

@ -0,0 +1,21 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@if (auth()->user()->can('create purchase'))
<div>
<flux:button href="{{ route('studio.manage.purchase.create') }}" variant="primary" wire:navigate.hover
class="text-sm">
Tambah
</flux:button>
</div>
@endif
</div>
<div class="mt-6">
<livewire:datatable.studio.manage.purchases-table />
</div>
@include('components.confirmation.delete')
</flux:main>

View File

@ -21,6 +21,8 @@
use App\Livewire\Studio\Manage\Article\Create as ArticleCreate;
use App\Livewire\Studio\Manage\Article\Edit as ArticleEdit;
use App\Livewire\Studio\Manage\Article\Index as ArticleIndex;
use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate;
use App\Livewire\Studio\Manage\Purchase\Index as PurchaseIndex;
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit;
use App\Livewire\Studio\Master\Outlet\Index as OutletIndex;
@ -125,5 +127,11 @@
Route::get('/{article}/edit', ArticleEdit::class)->name('edit')->middleware('can:update article');
Route::delete('/{article}/delete', ArticleCreate::class)->name('delete')->middleware('can:delete article');
});
Route::prefix('purchases')->name('purchase.')->group(function () {
Route::get('/', PurchaseIndex::class)->name('index')->middleware('can:view purchase');
Route::get('/create', PurchaseCreate::class)->name('create')->middleware('can:create purchase');
Route::delete('/{purchase}/delete', PurchaseCreate::class)->name('delete')->middleware('can:delete purchase');
});
});
});