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 + +