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:
parent
b62b86a6ae
commit
33e138be42
77
app/Livewire/Datatable/Studio/Manage/PurchasesTable.php
Normal file
77
app/Livewire/Datatable/Studio/Manage/PurchasesTable.php
Normal 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');
|
||||
}
|
||||
}
|
||||
126
app/Livewire/Forms/Studio/Manage/PurchaseForm.php
Normal file
126
app/Livewire/Forms/Studio/Manage/PurchaseForm.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
76
app/Livewire/Studio/Manage/Purchase/Create.php
Normal file
76
app/Livewire/Studio/Manage/Purchase/Create.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
35
app/Livewire/Studio/Manage/Purchase/Index.php
Normal file
35
app/Livewire/Studio/Manage/Purchase/Index.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,4 +76,9 @@ public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
public function purchases(): HasMany
|
||||
{
|
||||
return $this->hasMany(Purchase::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
36
app/Models/Purchase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
35
app/Models/PurchaseItem.php
Normal file
35
app/Models/PurchaseItem.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
100
app/Traits/Purchase/WithAddItem.php
Normal file
100
app/Traits/Purchase/WithAddItem.php
Normal 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');
|
||||
}
|
||||
}
|
||||
19
app/Traits/Purchase/WithDeleteItem.php
Normal file
19
app/Traits/Purchase/WithDeleteItem.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
47
app/Traits/Purchase/WithUpdateItem.php
Normal file
47
app/Traits/Purchase/WithUpdateItem.php
Normal 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();
|
||||
}
|
||||
}
|
||||
44
app/Traits/Purchase/WithUpdateStock.php
Normal file
44
app/Traits/Purchase/WithUpdateStock.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -88,6 +88,10 @@ public function run(): void
|
||||
'create article',
|
||||
'update article',
|
||||
'delete article',
|
||||
|
||||
'view purchase',
|
||||
'create purchase',
|
||||
'delete purchase',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
|
||||
209
resources/views/livewire/studio/manage/purchase/form.blade.php
Normal file
209
resources/views/livewire/studio/manage/purchase/form.blade.php
Normal 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>
|
||||
@ -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>
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user