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\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
||||||
|
use App\Models\CashTransaction;
|
||||||
use App\Services\Admin\Finance\CashAccountService;
|
use App\Services\Admin\Finance\CashAccountService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@ -42,4 +43,22 @@ public function withdrawal(CashTransactionRequest $request): RedirectResponse
|
|||||||
|
|
||||||
return to_route('admin.finance.cash-accounts.index');
|
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'],
|
'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;
|
$newBalance = $cashAccount->balance + $expense->amount;
|
||||||
$cashAccount->update(['balance' => $newBalance]);
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
|
|
||||||
$cashTransaction->delete();
|
|
||||||
|
|
||||||
return $expense->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 { ArrowDownToLine, ArrowUpFromLine, Wallet } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
@ -16,9 +16,11 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { index as cashAccountIndex, deposit, withdrawal } from '@/routes/admin/finance/cash-accounts';
|
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 { createTransactionColumns } from './transaction-columns';
|
||||||
import type { CashTransaction } from './transaction-columns';
|
import type { CashTransaction } from './transaction-columns';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
|
|
||||||
type CashAccount = {
|
type CashAccount = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -34,8 +36,23 @@ type Props = {
|
|||||||
export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
||||||
const [depositOpen, setDepositOpen] = useState(false);
|
const [depositOpen, setDepositOpen] = useState(false);
|
||||||
const [withdrawalOpen, setWithdrawalOpen] = 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -163,6 +180,70 @@ export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
|||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { ArrowUpDown } from 'lucide-react';
|
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
export type CashTransaction = {
|
export type CashTransaction = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -55,7 +61,16 @@ function getReferenceLabel(type: string): string {
|
|||||||
return labels[type] ?? '-';
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
id: 'no',
|
id: 'no',
|
||||||
@ -167,5 +182,57 @@ export function createTransactionColumns(): ColumnDef<CashTransaction>[] {
|
|||||||
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
|
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::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/deposit', [CashAccountController::class, 'deposit'])->name('cash-accounts.deposit');
|
||||||
Route::post('cash-accounts/withdrawal', [CashAccountController::class, 'withdrawal'])->name('cash-accounts.withdrawal');
|
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']);
|
Route::resource('expenses', ExpenseController::class)->except(['show', 'create', 'edit']);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user