feat: add update and delete functionality for cash transactions in CashAccountController and CashAccountService
This commit is contained in:
parent
c6c765b906
commit
db58fbe049
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,8 +94,6 @@ public function delete(Expense $expense): bool
|
||||
$newBalance = $cashAccount->balance + $expense->amount;
|
||||
$cashAccount->update(['balance' => $newBalance]);
|
||||
|
||||
$cashTransaction->delete();
|
||||
|
||||
return $expense->delete();
|
||||
});
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -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']);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user