From 91f7858cfe82aea349b215e2109fe53f64ebb61e Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 18 Jun 2026 00:16:49 +0700 Subject: [PATCH] feat: add cash withdrawal functionality, including permission, controller method, and UI updates --- app/Enums/CashTransactionType.php | 21 ++++++++ app/Enums/Permission.php | 4 +- app/Enums/Role.php | 1 + .../Admin/Finance/CashController.php | 12 +++++ .../Admin/Finance/WithdrawCashRequest.php | 41 ++++++++++++++++ app/Models/CashTransaction.php | 8 +++- app/Services/Finance/CashService.php | 48 ++++++++++++++++++- ..._120002_create_cash_transactions_table.php | 2 + .../finance/cash/CashTransactionFormModal.vue | 33 +++++++++++-- .../js/pages/admin/finance/cash/Index.vue | 24 ++++++---- resources/js/types/cash.ts | 1 + routes/web.php | 4 ++ 12 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 app/Enums/CashTransactionType.php create mode 100644 app/Http/Requests/Admin/Finance/WithdrawCashRequest.php diff --git a/app/Enums/CashTransactionType.php b/app/Enums/CashTransactionType.php new file mode 100644 index 0000000..a4ed760 --- /dev/null +++ b/app/Enums/CashTransactionType.php @@ -0,0 +1,21 @@ + 'Setor Kas', + self::WITHDRAWAL => 'Tarik Kas', + }; + } +} diff --git a/app/Enums/Permission.php b/app/Enums/Permission.php index e838ee2..3638b6b 100644 --- a/app/Enums/Permission.php +++ b/app/Enums/Permission.php @@ -78,6 +78,7 @@ enum Permission: string case CASH_VIEW = 'cash.view'; case CASH_DEPOSIT = 'cash.deposit'; + case CASH_WITHDRAW = 'cash.withdraw'; case CASH_UPDATE = 'cash.update'; case CASH_DELETE = 'cash.delete'; @@ -179,6 +180,7 @@ public function label(): string self::CASH_VIEW => 'Lihat Kas', self::CASH_DEPOSIT => 'Setor Kas', + self::CASH_WITHDRAW => 'Tarik Kas', self::CASH_UPDATE => 'Ubah Setor Kas', self::CASH_DELETE => 'Hapus Setor Kas', @@ -237,7 +239,7 @@ public function group(): string self::CUTTINGS_VIEW, self::CUTTINGS_CREATE, self::CUTTINGS_UPDATE, self::CUTTINGS_DELETE, self::CUTTINGS_COMPLETE, self::CUTTINGS_VERIFY, self::CUTTINGS_REJECT => 'Cutting', - self::CASH_VIEW, self::CASH_DEPOSIT, self::CASH_UPDATE, self::CASH_DELETE => 'Kas', + self::CASH_VIEW, self::CASH_DEPOSIT, self::CASH_WITHDRAW, self::CASH_UPDATE, self::CASH_DELETE => 'Kas', self::EXPENSES_VIEW, self::EXPENSES_CREATE, self::EXPENSES_UPDATE, self::EXPENSES_DELETE => 'Pengeluaran', self::EMPLOYEE_ADVANCES_VIEW, self::EMPLOYEE_ADVANCES_CREATE, self::EMPLOYEE_ADVANCES_UPDATE, diff --git a/app/Enums/Role.php b/app/Enums/Role.php index 2ab6782..ac32555 100644 --- a/app/Enums/Role.php +++ b/app/Enums/Role.php @@ -133,6 +133,7 @@ public function permissions(): array Permission::CASH_VIEW, Permission::CASH_DEPOSIT, + Permission::CASH_WITHDRAW, Permission::CASH_UPDATE, Permission::CASH_DELETE, diff --git a/app/Http/Controllers/Admin/Finance/CashController.php b/app/Http/Controllers/Admin/Finance/CashController.php index b8b064b..4d436a1 100644 --- a/app/Http/Controllers/Admin/Finance/CashController.php +++ b/app/Http/Controllers/Admin/Finance/CashController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Finance\DepositCashRequest; use App\Http\Requests\Admin\Finance\UpdateCashTransactionRequest; +use App\Http\Requests\Admin\Finance\WithdrawCashRequest; use App\Models\CashTransaction; use App\Services\Finance\CashService; use Illuminate\Http\RedirectResponse; @@ -46,6 +47,17 @@ public function deposit(DepositCashRequest $request): RedirectResponse return redirect()->route('admin.finance.cash.index'); } + public function withdraw(WithdrawCashRequest $request): RedirectResponse + { + $cashAccount = $this->cashService->getDefaultAccount(); + + $this->cashService->withdraw($cashAccount, $request->validated(), $request->user()); + + $this->flashSuccess('Tarik kas berhasil dicatat.'); + + return redirect()->route('admin.finance.cash.index'); + } + public function update(UpdateCashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse { $this->cashService->updateDeposit($cashTransaction, $request->validated()); diff --git a/app/Http/Requests/Admin/Finance/WithdrawCashRequest.php b/app/Http/Requests/Admin/Finance/WithdrawCashRequest.php new file mode 100644 index 0000000..03eaba6 --- /dev/null +++ b/app/Http/Requests/Admin/Finance/WithdrawCashRequest.php @@ -0,0 +1,41 @@ +user()?->can(Permission::CASH_WITHDRAW->value) ?? false; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'amount' => ['required', 'integer', 'min:1'], + 'description' => ['required', 'string', 'max:100'], + ...$this->photoRules(), + ]; + } + + /** + * @return array + */ + public function attributes(): array + { + return [ + 'amount' => 'jumlah', + 'description' => 'keterangan', + ...$this->photoUploadAttributes('foto bukti'), + ]; + } +} diff --git a/app/Models/CashTransaction.php b/app/Models/CashTransaction.php index 023f3d4..b9487e6 100644 --- a/app/Models/CashTransaction.php +++ b/app/Models/CashTransaction.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\CashTransactionType; use App\Models\Concerns\HasModuleMedia; use App\Models\Concerns\InteractsWithActivityLog; use Illuminate\Database\Eloquent\Attributes\Appends; @@ -33,6 +34,7 @@ class CashTransaction extends Model implements HasMedia protected function casts(): array { return [ + 'type' => CashTransactionType::class, 'amount' => 'integer', 'balance_after' => 'integer', ]; @@ -89,7 +91,7 @@ public function isIncoming(): Attribute return $this->reference->repayment_cash_transaction_id === $this->id; } - return self::isIncomingTransaction($this); + return $this->type === CashTransactionType::DEPOSIT; }, ); } @@ -98,6 +100,10 @@ public function referenceLabel(): Attribute { return Attribute::make( get: function () { + if ($this->reference_type === null) { + return $this->type === CashTransactionType::WITHDRAWAL ? 'Tarik Kas' : 'Setor Kas'; + } + if ($this->reference_type === EmployeeAdvance::class && $this->reference instanceof EmployeeAdvance) { return $this->reference->repayment_cash_transaction_id === $this->id ? 'Pelunasan Kasbon' diff --git a/app/Services/Finance/CashService.php b/app/Services/Finance/CashService.php index 48fc810..b5a8afd 100644 --- a/app/Services/Finance/CashService.php +++ b/app/Services/Finance/CashService.php @@ -2,6 +2,7 @@ namespace App\Services\Finance; +use App\Enums\CashTransactionType; use App\Models\CashAccount; use App\Models\CashTransaction; use App\Models\User; @@ -73,6 +74,7 @@ public function deposit(CashAccount $cashAccount, array $validated, User $user): $transaction = CashTransaction::create([ 'cash_account_id' => $account->id, + 'type' => CashTransactionType::DEPOSIT, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $validated['description'], @@ -94,6 +96,50 @@ public function deposit(CashAccount $cashAccount, array $validated, User $user): return $transaction; } + /** + * @param array{amount: int, description: string} $validated + */ + public function withdraw(CashAccount $cashAccount, array $validated, User $user): CashTransaction + { + $transaction = DB::transaction(function () use ($cashAccount, $validated, $user): CashTransaction { + $account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id); + $amount = (int) $validated['amount']; + + if ($account->balance < $amount) { + throw ValidationException::withMessages([ + 'amount' => 'Saldo kas tidak mencukupi.', + ]); + } + + $newBalance = $account->balance - $amount; + + $account->balance = $newBalance; + $account->save(); + + $transaction = CashTransaction::create([ + 'cash_account_id' => $account->id, + 'type' => CashTransactionType::WITHDRAWAL, + 'amount' => $amount, + 'balance_after' => $newBalance, + 'description' => $validated['description'], + 'created_by_id' => $user->id, + ]); + + $this->syncPhotos($transaction, $validated); + + return $transaction; + }); + + $this->pushNotificationService->sendToRoles( + '🏦 Tarik Kas', + "Tarik kas {$transaction->amount_formatted} dengan keterangan: {$transaction->description} oleh {$user->profile?->full_name}.", + ['owner', 'developer', 'admin-toko'], + '/admin/finance/cash', + ); + + return $transaction; + } + public function recordOutgoing( Model $reference, int $amount, @@ -308,7 +354,7 @@ private function recalculateBalances(CashAccount $cashAccount): void ->get(); foreach ($transactions as $transaction) { - if (CashTransaction::isIncomingTransaction($transaction)) { + if ($transaction->type === CashTransactionType::DEPOSIT) { $runningBalance += $transaction->amount; } else { $runningBalance -= $transaction->amount; diff --git a/database/migrations/2026_06_10_120002_create_cash_transactions_table.php b/database/migrations/2026_06_10_120002_create_cash_transactions_table.php index 89d0207..e653b9f 100644 --- a/database/migrations/2026_06_10_120002_create_cash_transactions_table.php +++ b/database/migrations/2026_06_10_120002_create_cash_transactions_table.php @@ -1,5 +1,6 @@ nullableMorphs('reference'); $table->unsignedBigInteger('amount'); $table->unsignedBigInteger('balance_after'); + $table->enum('type', array_column(CashTransactionType::cases(), 'value'))->default(CashTransactionType::DEPOSIT->value); $table->string('description', 100); $table->timestamp('created_at')->useCurrent(); diff --git a/resources/js/components/admin/finance/cash/CashTransactionFormModal.vue b/resources/js/components/admin/finance/cash/CashTransactionFormModal.vue index 56e0f5d..d9d92d9 100644 --- a/resources/js/components/admin/finance/cash/CashTransactionFormModal.vue +++ b/resources/js/components/admin/finance/cash/CashTransactionFormModal.vue @@ -32,9 +32,18 @@ const open = defineModel('open', { default: false }); const props = defineProps<{ transaction?: CashTransactionListItem | null; + mode?: 'deposit' | 'withdrawal'; }>(); const isEditing = computed(() => props.transaction != null); +const currentMode = computed(() => { + if (isEditing.value && props.transaction) { + return props.transaction.type; + } + + return props.mode ?? 'deposit'; +}); +const isWithdrawal = computed(() => currentMode.value === 'withdrawal'); const existingPhotoId = ref(null); @@ -113,8 +122,8 @@ function submit() { }, onError: () => { toast.error(isEditing.value - ? 'Gagal memperbarui setor kas. Periksa kembali formulir.' - : 'Gagal mencatat setor kas. Periksa kembali formulir.'); + ? 'Gagal memperbarui transaksi kas. Periksa kembali formulir.' + : 'Gagal mencatat transaksi kas. Periksa kembali formulir.'); }, }; @@ -124,15 +133,29 @@ function submit() { return; } - form.transform(() => payload).post('/admin/finance/cash/deposit', options); + const endpoint = isWithdrawal.value ? '/admin/finance/cash/withdraw' : '/admin/finance/cash/deposit'; + + form.transform(() => payload).post(endpoint, options); } + +const dialogTitle = computed(() => { + if (isEditing.value) { + return isWithdrawal.value ? 'Ubah Tarik Kas' : 'Ubah Setor Kas'; + } + + return isWithdrawal.value ? 'Tarik Kas' : 'Setor Kas'; +}); + +const placeholder = computed(() => + isWithdrawal.value ? 'Contoh: Tarik kas ke owner' : 'Contoh: Setoran kas harian', +);