diff --git a/app/Enums/ExpenseType.php b/app/Enums/ExpenseType.php new file mode 100644 index 0000000..f849a1c --- /dev/null +++ b/app/Enums/ExpenseType.php @@ -0,0 +1,33 @@ + 'Operasional', + self::PURCHASE => 'Belanja', + self::SALARY => 'Gaji', + }; + } + + public function color() + { + return match ($this) { + self::OPERATIONAL => 'emerald', + self::PURCHASE => 'rose', + self::SALARY => 'cyan', + }; + } +} diff --git a/app/Livewire/Datatable/ExpensesTable.php b/app/Livewire/Datatable/ExpensesTable.php new file mode 100644 index 0000000..c33afb7 --- /dev/null +++ b/app/Livewire/Datatable/ExpensesTable.php @@ -0,0 +1,53 @@ +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'); + } +} diff --git a/app/Livewire/Forms/ExpenseForm.php b/app/Livewire/Forms/ExpenseForm.php new file mode 100644 index 0000000..f2e854e --- /dev/null +++ b/app/Livewire/Forms/ExpenseForm.php @@ -0,0 +1,79 @@ + ['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'); + }); + } +} diff --git a/app/Livewire/Studio/Finance/Expense.php b/app/Livewire/Studio/Finance/Expense.php new file mode 100644 index 0000000..22b8fdb --- /dev/null +++ b/app/Livewire/Studio/Finance/Expense.php @@ -0,0 +1,93 @@ +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', + ]); + } +} diff --git a/app/Models/Expense.php b/app/Models/Expense.php new file mode 100644 index 0000000..ace189e --- /dev/null +++ b/app/Models/Expense.php @@ -0,0 +1,29 @@ + ExpenseType::class, + 'amount' => 'int', + ]; + } + + public function outlet(): BelongsTo + { + return $this->belongsTo(Outlet::class); + } +} diff --git a/app/Models/Outlet.php b/app/Models/Outlet.php index a044925..6e573f1 100644 --- a/app/Models/Outlet.php +++ b/app/Models/Outlet.php @@ -67,4 +67,9 @@ public function bottles(): BelongsToMany { return $this->belongsToMany(Bottle::class); } + + public function expenses(): HasMany + { + return $this->hasMany(Expense::class); + } } diff --git a/database/migrations/2025_10_02_114249_create_expenses_table.php b/database/migrations/2025_10_02_114249_create_expenses_table.php new file mode 100644 index 0000000..951e1a2 --- /dev/null +++ b/database/migrations/2025_10_02_114249_create_expenses_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/resources/views/components/partials/studio/_sidebar.blade.php b/resources/views/components/partials/studio/_sidebar.blade.php index c23af4d..e297054 100644 --- a/resources/views/components/partials/studio/_sidebar.blade.php +++ b/resources/views/components/partials/studio/_sidebar.blade.php @@ -62,5 +62,14 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z + +
+
Keuangan
+
+ Pengeluaran + +
+
diff --git a/resources/views/flux/icon/banknote-arrow-down.blade.php b/resources/views/flux/icon/banknote-arrow-down.blade.php new file mode 100644 index 0000000..82e67fa --- /dev/null +++ b/resources/views/flux/icon/banknote-arrow-down.blade.php @@ -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 + +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" +> + + + + + + + diff --git a/resources/views/livewire/studio/finance/expenses.blade.php b/resources/views/livewire/studio/finance/expenses.blade.php new file mode 100644 index 0000000..92d8648 --- /dev/null +++ b/resources/views/livewire/studio/finance/expenses.blade.php @@ -0,0 +1,71 @@ + +
+
+ {{ $pageTitle }} +
+
+ + Tambah + + +
+
+ +
+ +
+ + @include('components.confirmation.delete') + +
+ {{ $modalTitle }} + + + Keterangan * + + + + + + Jumlah * + + + Rp + + + + + +
+

Gambar

+
+ + @error('form.image') + + @enderror +
+
+ +
+ + + Simpan + +
+
+
+
diff --git a/routes/pages/studio.php b/routes/pages/studio.php index 2dacd2a..447c735 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -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'); + }); + }); });