feat: implement transaction type filtering in CashAccount management
This commit is contained in:
parent
0d90c5913e
commit
3f12a65b05
@ -7,6 +7,7 @@
|
||||
use App\Models\CashTransaction;
|
||||
use App\Services\Admin\Finance\CashAccountService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@ -16,13 +17,16 @@ public function __construct(
|
||||
private CashAccountService $service
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$cashAccount = $this->service->get();
|
||||
|
||||
return Inertia::render('admin/finance/cash-account/index', [
|
||||
'cashAccount' => $cashAccount,
|
||||
'transactions' => $this->service->getAllTransactions(),
|
||||
'transactions' => $this->service->getAllTransactions(
|
||||
$request->only(['type'])
|
||||
),
|
||||
'filters' => $request->only(['type']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ public function get(): ?CashAccount
|
||||
return CashAccount::select('id', 'name', 'balance')->first();
|
||||
}
|
||||
|
||||
public function getAllTransactions(): Collection
|
||||
public function getAllTransactions(array $filters = []): Collection
|
||||
{
|
||||
$cashAccount = $this->get();
|
||||
|
||||
@ -34,6 +34,9 @@ public function getAllTransactions(): Collection
|
||||
return $cashAccount->cashTransactions()
|
||||
->select('id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at')
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->when($filters['type'] ?? null, function ($query, $type) {
|
||||
$query->where('type', $type);
|
||||
})
|
||||
->latest()
|
||||
->get()
|
||||
->map(fn (CashTransaction $transaction) => $this->formatTransaction($transaction));
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { Form, Head, router } from '@inertiajs/react';
|
||||
import { ArrowDownToLine, ArrowUpFromLine, Wallet } from 'lucide-react';
|
||||
import { ArrowDownToLine, ArrowUpFromLine, Filter, Wallet, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
import InputError from '@/components/input-error';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -14,14 +17,13 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
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';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
|
||||
type CashAccount = {
|
||||
id: number;
|
||||
@ -32,13 +34,17 @@ type CashAccount = {
|
||||
type Props = {
|
||||
cashAccount: CashAccount | null;
|
||||
transactions: CashTransaction[];
|
||||
filters: {
|
||||
type?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
||||
export default function CashAccountIndex({ cashAccount, transactions, filters }: 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 [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
const [depositReceiptKey, setDepositReceiptKey] = useState<string | null>(null);
|
||||
const [depositUploading, setDepositUploading] = useState(false);
|
||||
@ -52,6 +58,31 @@ export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
||||
const [editUploading, setEditUploading] = useState(false);
|
||||
const [editFileMeta, setEditFileMeta] = useState<{ size: number; type: string } | null>(null);
|
||||
|
||||
const hasActiveFilters = filters.type;
|
||||
|
||||
function applyFilter(key: string, value: string) {
|
||||
const newFilters = { ...filters };
|
||||
|
||||
if (value === '' || value === 'all') {
|
||||
delete newFilters[key as keyof typeof newFilters];
|
||||
} else {
|
||||
newFilters[key as keyof typeof newFilters] = value;
|
||||
}
|
||||
|
||||
router.get(cashAccountIndex(), newFilters, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
router.get(cashAccountIndex(), {}, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
setFilterOpen(false);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
@ -70,6 +101,61 @@ export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
||||
handleDeleteClick: (transaction) => setDeleting(transaction),
|
||||
});
|
||||
|
||||
const filterToolbar = (
|
||||
<Popover open={filterOpen} onOpenChange={setFilterOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="h-4 w-4" />
|
||||
Filter
|
||||
{hasActiveFilters && (
|
||||
<span className="ml-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
|
||||
{Object.values(filters).filter(Boolean).length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64" align="end">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Filter</span>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={clearFilters}
|
||||
>
|
||||
<X className="mr-1 h-3 w-3" />
|
||||
Hapus Semua
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Tipe Sumber
|
||||
</label>
|
||||
<Select
|
||||
value={filters.type ?? 'all'}
|
||||
onValueChange={(value) => applyFilter('type', value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Semua Tipe" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Tipe</SelectItem>
|
||||
<SelectItem value="deposit">Deposit</SelectItem>
|
||||
<SelectItem value="withdrawal">Withdrawal</SelectItem>
|
||||
<SelectItem value="expense">Pengeluaran</SelectItem>
|
||||
<SelectItem value="transfer">Transfer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Kas Toko" />
|
||||
@ -113,6 +199,7 @@ export default function CashAccountIndex({ cashAccount, transactions }: Props) {
|
||||
searchKey="description"
|
||||
searchPlaceholder="Cari transaksi..."
|
||||
emptyText="Belum ada riwayat transaksi."
|
||||
toolbar={filterToolbar}
|
||||
/>
|
||||
|
||||
<Dialog open={depositOpen} onOpenChange={(open) => {
|
||||
|
||||
@ -75,6 +75,244 @@
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FILTER - TRANSACTION TYPE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('index can filter by type deposit', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 200000]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Deposit pertama',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::WITHDRAWAL,
|
||||
'amount' => 50000,
|
||||
'balance_after' => 200000,
|
||||
'description' => 'Withdrawal pertama',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 200000,
|
||||
'description' => 'Deposit kedua',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'deposit']));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/finance/cash-account/index')
|
||||
->has('transactions', 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('index can filter by type withdrawal', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Deposit',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::WITHDRAWAL,
|
||||
'amount' => 30000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Withdrawal pertama',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::WITHDRAWAL,
|
||||
'amount' => 20000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Withdrawal kedua',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'withdrawal']));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/finance/cash-account/index')
|
||||
->has('transactions', 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('index can filter by type expense', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Deposit',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::EXPENSE,
|
||||
'amount' => 25000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Pengeluaran',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'expense']));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/finance/cash-account/index')
|
||||
->has('transactions', 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('index can filter by type transfer', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Deposit',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::TRANSFER,
|
||||
'amount' => 50000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Transfer',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'transfer']));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/finance/cash-account/index')
|
||||
->has('transactions', 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('index without type filter shows all transactions', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 200000]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Deposit',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::WITHDRAWAL,
|
||||
'amount' => 50000,
|
||||
'balance_after' => 200000,
|
||||
'description' => 'Withdrawal',
|
||||
]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::EXPENSE,
|
||||
'amount' => 30000,
|
||||
'balance_after' => 200000,
|
||||
'description' => 'Expense',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/finance/cash-account/index')
|
||||
->has('transactions', 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('index passes type filter to view', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'deposit']));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('filters.type', 'deposit')
|
||||
);
|
||||
});
|
||||
|
||||
test('index passes empty filters when no type specified', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('filters', [])
|
||||
);
|
||||
});
|
||||
|
||||
test('index with invalid type filter shows no transactions', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
|
||||
|
||||
CashTransaction::factory()->create([
|
||||
'cash_account_id' => $cashAccount->id,
|
||||
'created_by_id' => $user->id,
|
||||
'type' => CashTransactionType::DEPOSIT,
|
||||
'amount' => 100000,
|
||||
'balance_after' => 100000,
|
||||
'description' => 'Deposit',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'invalid_type']));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/finance/cash-account/index')
|
||||
->has('transactions', 0)
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEPOSIT
|
||||
|
||||
Loading…
Reference in New Issue
Block a user