diff --git a/app/Http/Controllers/Admin/Finance/CashAccountController.php b/app/Http/Controllers/Admin/Finance/CashAccountController.php index 3b80842..020cdcc 100644 --- a/app/Http/Controllers/Admin/Finance/CashAccountController.php +++ b/app/Http/Controllers/Admin/Finance/CashAccountController.php @@ -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']), ]); } diff --git a/app/Services/Admin/Finance/CashAccountService.php b/app/Services/Admin/Finance/CashAccountService.php index 30f6fad..c7c7107 100644 --- a/app/Services/Admin/Finance/CashAccountService.php +++ b/app/Services/Admin/Finance/CashAccountService.php @@ -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)); diff --git a/resources/js/pages/admin/finance/cash-account/index.tsx b/resources/js/pages/admin/finance/cash-account/index.tsx index 0bb4f68..53dd897 100644 --- a/resources/js/pages/admin/finance/cash-account/index.tsx +++ b/resources/js/pages/admin/finance/cash-account/index.tsx @@ -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(null); const [deleting, setDeleting] = useState(null); + const [filterOpen, setFilterOpen] = useState(false); const [depositReceiptKey, setDepositReceiptKey] = useState(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 = ( + + + + + +
+
+ Filter + {hasActiveFilters && ( + + )} +
+ +
+ + +
+
+
+
+ ); + return ( <> @@ -113,6 +199,7 @@ export default function CashAccountIndex({ cashAccount, transactions }: Props) { searchKey="description" searchPlaceholder="Cari transaksi..." emptyText="Belum ada riwayat transaksi." + toolbar={filterToolbar} /> { diff --git a/tests/Feature/Admin/Finance/CashAccountTest.php b/tests/Feature/Admin/Finance/CashAccountTest.php index 700952c..6ce1557 100644 --- a/tests/Feature/Admin/Finance/CashAccountTest.php +++ b/tests/Feature/Admin/Finance/CashAccountTest.php @@ -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