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);
|
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>
|
</flux:navlist.item>
|
||||||
</div>
|
</div>
|
||||||
</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:navlist>
|
||||||
</flux:sidebar>
|
</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\Edit as ProductEdit;
|
||||||
use App\Livewire\Studio\Catalog\Product\Index as ProductIndex;
|
use App\Livewire\Studio\Catalog\Product\Index as ProductIndex;
|
||||||
use App\Livewire\Studio\Dashboard\Overview as OverviewComponent;
|
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\Customer as CustomerComponent;
|
||||||
use App\Livewire\Studio\Loyalty\Tier as TierComponent;
|
use App\Livewire\Studio\Loyalty\Tier as TierComponent;
|
||||||
use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate;
|
use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate;
|
||||||
@ -119,4 +120,13 @@
|
|||||||
Route::get('/{bottle}/delete', BottleCreate::class)->name('delete');
|
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