feat(expense): membuat crud menggunakan modal, belum jalan karena outletnya belum diisi
This commit is contained in:
parent
739f7e761e
commit
e1b96b2e2f
33
app/Enums/ExpenseType.php
Normal file
33
app/Enums/ExpenseType.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\WithCommentEnum;
|
||||
use App\Traits\WithValueEnum;
|
||||
|
||||
enum ExpenseType: int
|
||||
{
|
||||
use WithCommentEnum, WithValueEnum;
|
||||
|
||||
case OPERATIONAL = 1;
|
||||
case PURCHASE = 2;
|
||||
case SALARY = 3;
|
||||
|
||||
public function label()
|
||||
{
|
||||
return match ($this) {
|
||||
self::OPERATIONAL => 'Operasional',
|
||||
self::PURCHASE => 'Belanja',
|
||||
self::SALARY => 'Gaji',
|
||||
};
|
||||
}
|
||||
|
||||
public function color()
|
||||
{
|
||||
return match ($this) {
|
||||
self::OPERATIONAL => 'emerald',
|
||||
self::PURCHASE => 'rose',
|
||||
self::SALARY => 'cyan',
|
||||
};
|
||||
}
|
||||
}
|
||||
53
app/Livewire/Datatable/ExpensesTable.php
Normal file
53
app/Livewire/Datatable/ExpensesTable.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable;
|
||||
|
||||
use App\Models\Expense;
|
||||
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;
|
||||
|
||||
class ExpensesTable extends DataTableComponent
|
||||
{
|
||||
use WithConfiguration, WithMediaHandler, WithPrependColumn;
|
||||
|
||||
protected $model = Expense::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Keterangan', 'description')->searchable(),
|
||||
|
||||
Column::make('Jumlah', 'amount')
|
||||
->format(fn ($value) => currency($value, 'Rp'))
|
||||
->searchable(),
|
||||
|
||||
Column::make('Aksi')
|
||||
->label(function ($row) {
|
||||
$actions = '';
|
||||
|
||||
$actions .= view('components.datatables.edit-modal', [
|
||||
'id' => $row->id,
|
||||
'method' => 'update',
|
||||
'modalTitle' => 'Ubah Kategori',
|
||||
])->render();
|
||||
|
||||
$actions .= view('components.datatables.delete', [
|
||||
'id' => $row->id,
|
||||
'deleteRoute' => route('studio.finance.expense.delete', $row->id),
|
||||
])->render();
|
||||
|
||||
return $actions;
|
||||
})
|
||||
->html(),
|
||||
];
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Expense::select('id', 'description', 'amount');
|
||||
}
|
||||
}
|
||||
79
app/Livewire/Forms/ExpenseForm.php
Normal file
79
app/Livewire/Forms/ExpenseForm.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Models\Expense;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Form;
|
||||
|
||||
class ExpenseForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
|
||||
public ?Expense $expense = null;
|
||||
|
||||
public string $description = '';
|
||||
|
||||
public string $amount = '';
|
||||
|
||||
public array $image = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'description' => ['required', 'string', 'max:100'],
|
||||
'amount' => ['required', 'numeric', new UnsignedInteger],
|
||||
'image' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'description' => 'keterangan',
|
||||
'amount' => 'jumlah',
|
||||
'image' => 'gambar',
|
||||
];
|
||||
}
|
||||
|
||||
public function setExpense(Expense $expense)
|
||||
{
|
||||
$this->expense = $expense;
|
||||
|
||||
$this->description = $expense->description;
|
||||
$this->amount = $expense->amount;
|
||||
|
||||
$this->image = $this->mapMediaCollection($expense->getMedia('image'));
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$expense = Expense::create([
|
||||
'description' => $this->description,
|
||||
'amount' => $this->amount,
|
||||
]);
|
||||
|
||||
$this->uploadMedia($this->image, $expense, 'image');
|
||||
});
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$this->expense->update([
|
||||
'description' => $this->description,
|
||||
'amount' => $this->amount,
|
||||
]);
|
||||
|
||||
$this->syncMedia($this->image, $this->expense, 'image');
|
||||
$this->uploadMedia($this->image, $this->expense, 'image');
|
||||
});
|
||||
}
|
||||
}
|
||||
93
app/Livewire/Studio/Finance/Expense.php
Normal file
93
app/Livewire/Studio/Finance/Expense.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Finance;
|
||||
|
||||
use App\Livewire\Forms\ExpenseForm;
|
||||
use App\Models\Expense as ExpenseModel;
|
||||
use App\Traits\WithCloseModal;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Pengeluaran')]
|
||||
class Expense extends Component
|
||||
{
|
||||
use WithCloseModal, WithConfirmation, WithUpdatedData;
|
||||
|
||||
public ExpenseForm $form;
|
||||
|
||||
public string $method = 'create';
|
||||
|
||||
public string $modalTitle = '';
|
||||
|
||||
#[On('modal:open')]
|
||||
public function openModal(string $method, string $modalTitle, ?string $id = null)
|
||||
{
|
||||
$this->resetValidation();
|
||||
$this->resetErrorBag();
|
||||
|
||||
$this->method = $method;
|
||||
$this->modalTitle = $modalTitle;
|
||||
|
||||
if ($id) {
|
||||
$this->form->setExpense(ExpenseModel::findOrFail($id));
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$this->form->store();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
Flux::toast(
|
||||
heading: 'Berhasil',
|
||||
text: 'Pengeluaran berhasil ditambahkan.',
|
||||
variant: 'success',
|
||||
duration: 3000
|
||||
);
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->form->update();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
Flux::toast(
|
||||
heading: 'Berhasil',
|
||||
text: 'Pengeluaran berhasil diperbarui.',
|
||||
variant: 'success',
|
||||
duration: 3000
|
||||
);
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function delete(ExpenseModel $expense)
|
||||
{
|
||||
$expense->delete();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
Flux::toast(
|
||||
heading: 'Berhasil',
|
||||
text: 'Pengeluaran berhasil dihapus.',
|
||||
variant: 'success',
|
||||
);
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.studio.finance.expenses', [
|
||||
'pageTitle' => 'Pengeluaran',
|
||||
]);
|
||||
}
|
||||
}
|
||||
29
app/Models/Expense.php
Normal file
29
app/Models/Expense.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ExpenseType;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Expense extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => ExpenseType::class,
|
||||
'amount' => 'int',
|
||||
];
|
||||
}
|
||||
|
||||
public function outlet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Outlet::class);
|
||||
}
|
||||
}
|
||||
@ -67,4 +67,9 @@ public function bottles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Bottle::class);
|
||||
}
|
||||
|
||||
public function expenses(): HasMany
|
||||
{
|
||||
return $this->hasMany(Expense::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ExpenseType;
|
||||
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('expenses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('outlet_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('amount');
|
||||
$table->string('description', 100);
|
||||
$table->enum('type', ExpenseType::values())->comment(ExpenseType::comment());
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('expenses');
|
||||
}
|
||||
};
|
||||
@ -62,5 +62,14 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
|
||||
</flux:navlist.item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mb-1">
|
||||
<div class="text-zinc-500 dark:text-gray-300 text-sm/6">Keuangan</div>
|
||||
<div class="grid gap-2">
|
||||
<flux:navlist.item icon="banknote-arrow-down" href="{{ route('studio.finance.expense.index') }}"
|
||||
:current="request()->routeIs('studio.finance.expense.*')" wire:navigate.hover>Pengeluaran
|
||||
</flux:navlist.item>
|
||||
</div>
|
||||
</div>
|
||||
</flux:navlist>
|
||||
</flux:sidebar>
|
||||
|
||||
46
resources/views/flux/icon/banknote-arrow-down.blade.php
Normal file
46
resources/views/flux/icon/banknote-arrow-down.blade.php
Normal file
@ -0,0 +1,46 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')
|
||||
->add(match($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
});
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg
|
||||
{{ $attributes->class($classes) }}
|
||||
data-flux-icon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="{{ $strokeWidth }}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<path d="M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5" />
|
||||
<path d="m16 19 3 3 3-3" />
|
||||
<path d="M18 12h.01" />
|
||||
<path d="M19 16v6" />
|
||||
<path d="M6 12h.01" />
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
</svg>
|
||||
71
resources/views/livewire/studio/finance/expenses.blade.php
Normal file
71
resources/views/livewire/studio/finance/expenses.blade.php
Normal file
@ -0,0 +1,71 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:modal.trigger name="form-modal">
|
||||
<flux:button variant="primary" class="text-sm"
|
||||
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Pengeluaran'})">Tambah
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.expenses-table />
|
||||
</div>
|
||||
|
||||
@include('components.confirmation.delete')
|
||||
<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>Keterangan <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="Masukkan keterangan" wire:model.live.debounce.500ms="form.description"
|
||||
autofocus autocomplete="off" />
|
||||
<flux:error name="form.description" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Jumlah <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 jumlah" x-mask:dynamic="$money($input, ',')"
|
||||
wire:model.live.debounce.500ms="form.amount" autocomplete="off" />
|
||||
</flux:input.group>
|
||||
<flux:error name="form.amount" />
|
||||
</flux:field>
|
||||
|
||||
<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 role="alert" aria-live="polite" aria-atomic="true"
|
||||
class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||
<svg class="shrink-0 [:where(&)]:size-5 inline" data-flux-icon=""
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@ -12,6 +12,7 @@
|
||||
use App\Livewire\Studio\Catalog\Product\Edit as ProductEdit;
|
||||
use App\Livewire\Studio\Catalog\Product\Index as ProductIndex;
|
||||
use App\Livewire\Studio\Dashboard\Overview as OverviewComponent;
|
||||
use App\Livewire\Studio\Finance\Expense as ExpenseComponent;
|
||||
use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
|
||||
use App\Livewire\Studio\Loyalty\Tier as TierComponent;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate;
|
||||
@ -119,4 +120,13 @@
|
||||
Route::get('/{bottle}/delete', BottleCreate::class)->name('delete');
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('finance')
|
||||
->name('studio.finance.')
|
||||
->group(function () {
|
||||
Route::prefix('expenses')->name('expense.')->group(function () {
|
||||
Route::get('/', ExpenseComponent::class)->name('index');
|
||||
Route::delete('/{expense}/delete', ExpenseComponent::class)->name('delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user