feat: add update and delete functionality for cash transactions in CashAccountController and CashAccountService

This commit is contained in:
Yoga Pangestu 2026-07-29 20:25:43 +07:00
parent c6c765b906
commit db58fbe049
6 changed files with 232 additions and 6 deletions

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
use App\Models\CashTransaction;
use App\Services\Admin\Finance\CashAccountService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
@ -42,4 +43,22 @@ public function withdrawal(CashTransactionRequest $request): RedirectResponse
return to_route('admin.finance.cash-accounts.index');
}
public function update(CashTransactionRequest $request, CashTransaction $transaction): RedirectResponse
{
$this->service->updateTransaction($transaction, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Transaksi berhasil diperbarui.']);
return to_route('admin.finance.cash-accounts.index');
}
public function destroy(CashTransaction $transaction): RedirectResponse
{
$this->service->deleteTransaction($transaction);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Transaksi berhasil dihapus.']);
return to_route('admin.finance.cash-accounts.index');
}
}

View File

@ -68,4 +68,63 @@ public function withdrawal(array $data): CashTransaction
'description' => $data['description'],
]);
}
public function updateTransaction(CashTransaction $transaction, array $data): CashTransaction
{
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
throw ValidationException::withMessages([
'amount' => 'Transaksi ini tidak dapat diedit.',
]);
}
$cashAccount = CashAccount::firstOrFail();
$oldAmount = $transaction->amount;
$newAmount = $data['amount'];
$isDeposit = $transaction->type === CashTransactionType::DEPOSIT;
$difference = $isDeposit ? $newAmount - $oldAmount : $oldAmount - $newAmount;
if ($difference < 0 && $cashAccount->balance < abs($difference)) {
throw ValidationException::withMessages([
'amount' => 'Saldo tidak mencukupi.',
]);
}
$newBalance = $cashAccount->balance + $difference;
$cashAccount->update(['balance' => $newBalance]);
$transaction->update([
'amount' => $newAmount,
'balance_after' => $newBalance,
'description' => $data['description'],
]);
return $transaction;
}
public function deleteTransaction(CashTransaction $transaction): bool
{
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
throw ValidationException::withMessages([
'amount' => 'Transaksi ini tidak dapat dihapus.',
]);
}
$cashAccount = CashAccount::firstOrFail();
$isDeposit = $transaction->type === CashTransactionType::DEPOSIT;
$newBalance = $isDeposit
? $cashAccount->balance - $transaction->amount
: $cashAccount->balance + $transaction->amount;
if ($newBalance < 0) {
throw ValidationException::withMessages([
'amount' => 'Saldo tidak mencukupi untuk menghapus transaksi ini.',
]);
}
$cashAccount->update(['balance' => $newBalance]);
return $transaction->delete();
}
}

View File

@ -94,8 +94,6 @@ public function delete(Expense $expense): bool
$newBalance = $cashAccount->balance + $expense->amount;
$cashAccount->update(['balance' => $newBalance]);
$cashTransaction->delete();
return $expense->delete();
});
}

View File

@ -1,4 +1,4 @@
import { Form, Head } from '@inertiajs/react';
import { Form, Head, router } from '@inertiajs/react';
import { ArrowDownToLine, ArrowUpFromLine, Wallet } from 'lucide-react';
import { useState } from 'react';
import InputError from '@/components/input-error';
@ -16,9 +16,11 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { formatCurrency } from '@/lib/utils';
import { index as cashAccountIndex, deposit, withdrawal } from '@/routes/admin/finance/cash-accounts';
import { update as updateTransaction, destroy as destroyTransaction } from '@/routes/admin/finance/cash-accounts/transactions';
import { createTransactionColumns } from './transaction-columns';
import type { CashTransaction } from './transaction-columns';
import { DataTable } from '@/components/data-table';
import { ConfirmDialog } from '@/components/confirm-dialog';
type CashAccount = {
id: number;
@ -34,8 +36,23 @@ type Props = {
export default function CashAccountIndex({ cashAccount, transactions }: Props) {
const [depositOpen, setDepositOpen] = useState(false);
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
const [editing, setEditing] = useState<CashTransaction | null>(null);
const [deleting, setDeleting] = useState<CashTransaction | null>(null);
const columns = createTransactionColumns();
function handleDelete() {
if (!deleting) {
return;
}
router.delete(destroyTransaction(deleting.id), {
onSuccess: () => setDeleting(null),
});
}
const columns = createTransactionColumns({
handleEdit: (transaction) => setEditing(transaction),
handleDeleteClick: (transaction) => setDeleting(transaction),
});
return (
<>
@ -163,6 +180,70 @@ export default function CashAccountIndex({ cashAccount, transactions }: Props) {
</Form>
</DialogContent>
</Dialog>
<Dialog
open={editing !== null}
onOpenChange={(open) => {
if (!open) {
setEditing(null);
}
}}
>
<DialogContent>
{editing && (
<Form action={updateTransaction(editing.id)} resetOnSuccess onSuccess={() => setEditing(null)}>
{({ errors, processing }) => (
<>
<DialogHeader>
<DialogTitle>Edit Transaksi</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label>
Jumlah <span className="text-destructive">*</span>
</Label>
<RupiahInput name="amount" defaultValue={editing.amount} min={1} />
<InputError message={errors.amount} />
</div>
<div className="grid gap-2">
<Label>
Keterangan <span className="text-destructive">*</span>
</Label>
<Input
name="description"
placeholder="Masukkan keterangan"
defaultValue={editing.description}
/>
<InputError message={errors.description} />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setEditing(null)}>
Batal
</Button>
<Button type="submit" disabled={processing}>
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</>
)}
</Form>
)}
</DialogContent>
</Dialog>
<ConfirmDialog
open={deleting !== null}
onOpenChange={(open) => {
if (!open) {
setDeleting(null);
}
}}
title="Hapus Transaksi"
description={`Apakah Anda yakin ingin menghapus transaksi "${deleting?.description}"? Tindakan ini tidak dapat dibatalkan.`}
confirmLabel="Hapus"
onConfirm={handleDelete}
/>
</div>
</>
);

View File

@ -1,7 +1,13 @@
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { formatCurrency } from '@/lib/utils';
import type { ColumnDef } from '@tanstack/react-table';
import { ArrowUpDown } from 'lucide-react';
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
export type CashTransaction = {
id: number;
@ -55,7 +61,16 @@ function getReferenceLabel(type: string): string {
return labels[type] ?? '-';
}
export function createTransactionColumns(): ColumnDef<CashTransaction>[] {
type CreateColumnsParams = {
handleEdit: (transaction: CashTransaction) => void;
handleDeleteClick: (transaction: CashTransaction) => void;
};
export function createTransactionColumns(
params: CreateColumnsParams,
): ColumnDef<CashTransaction>[] {
const { handleEdit, handleDeleteClick } = params;
return [
{
id: 'no',
@ -167,5 +182,57 @@ export function createTransactionColumns(): ColumnDef<CashTransaction>[] {
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
},
},
{
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
className: 'w-[100px] text-center',
headerClassName: 'w-[100px] text-center',
},
cell: ({ row }) => {
const transaction = row.original;
const canEdit = transaction.type === 'deposit' || transaction.type === 'withdrawal';
if (!canEdit) {
return <span className="block text-center">-</span>;
}
return (
<TooltipProvider>
<div className="flex items-center justify-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(transaction)}
>
<Pencil className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
Edit
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(transaction)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
Hapus
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
);
},
},
];
}

View File

@ -22,6 +22,8 @@
Route::get('cash-accounts', [CashAccountController::class, 'index'])->name('cash-accounts.index');
Route::post('cash-accounts/deposit', [CashAccountController::class, 'deposit'])->name('cash-accounts.deposit');
Route::post('cash-accounts/withdrawal', [CashAccountController::class, 'withdrawal'])->name('cash-accounts.withdrawal');
Route::put('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'update'])->name('cash-accounts.transactions.update');
Route::delete('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'destroy'])->name('cash-accounts.transactions.destroy');
Route::resource('expenses', ExpenseController::class)->except(['show', 'create', 'edit']);
});