{data.data.map((item, index) => {
const Icon = (Icons[
item.category.icon as keyof typeof Icons
@@ -103,8 +108,21 @@ export function BreakdownCard({
index,
);
- return (
-
+ const categoryUrl = period
+ ? transactionsIndex({
+ query: {
+ category_ids: item.category_id,
+ date_from: format(
+ period.from,
+ 'yyyy-MM-dd',
+ ),
+ date_to: format(period.to, 'yyyy-MM-dd'),
+ },
+ }).url
+ : null;
+
+ const rowContent = (
+ <>
+ >
+ );
+
+ return categoryUrl ? (
+
+ {rowContent}
+
+ ) : (
+
+ {rowContent}
);
})}
diff --git a/resources/js/components/charts/sankey-chart.tsx b/resources/js/components/charts/sankey-chart.tsx
index a9edc67b..25ba2180 100644
--- a/resources/js/components/charts/sankey-chart.tsx
+++ b/resources/js/components/charts/sankey-chart.tsx
@@ -1,3 +1,4 @@
+import { index as transactionsIndex } from '@/actions/App/Http/Controllers/TransactionController';
import {
Popover,
PopoverContent,
@@ -16,6 +17,8 @@ import { cn } from '@/lib/utils';
import { Category } from '@/types/category';
import { formatCurrency } from '@/utils/currency';
import { __ } from '@/utils/i18n';
+import { router } from '@inertiajs/react';
+import { format } from 'date-fns';
import { useMemo, useState } from 'react';
interface SankeyChartProps {
@@ -24,6 +27,7 @@ interface SankeyChartProps {
className?: string;
currency?: string;
groupingThreshold?: number;
+ period?: { from: Date; to: Date };
}
interface NodeData {
@@ -142,6 +146,7 @@ export function SankeyChart({
className,
currency = 'USD',
groupingThreshold = 0.03,
+ period,
}: SankeyChartProps) {
const [hoveredNode, setHoveredNode] = useState
(null);
const [hoveredLink, setHoveredLink] = useState(null);
@@ -428,16 +433,62 @@ export function SankeyChart({
const otherGroup = isOtherNode
? otherGroups[node.id]
: null;
+ const categoryUrl =
+ node.category && period
+ ? transactionsIndex({
+ query: {
+ category_ids: node.category.id,
+ date_from: format(
+ period.from,
+ 'yyyy-MM-dd',
+ ),
+ date_to: format(
+ period.to,
+ 'yyyy-MM-dd',
+ ),
+ },
+ }).url
+ : null;
+ const isNavigable =
+ !isOtherNode && categoryUrl !== null;
const nodeContent = (
setHoveredNode(node.id)}
onMouseLeave={() => setHoveredNode(null)}
+ onClick={() => {
+ if (categoryUrl) {
+ router.visit(categoryUrl);
+ }
+ }}
+ onKeyDown={(event) => {
+ if (!categoryUrl) {
+ return;
+ }
+
+ if (
+ event.key === 'Enter' ||
+ event.key === ' '
+ ) {
+ event.preventDefault();
+ router.visit(categoryUrl);
+ }
+ }}
+ role={isNavigable ? 'link' : undefined}
+ tabIndex={isNavigable ? 0 : undefined}
+ aria-label={
+ isNavigable
+ ? `View ${node.label} transactions`
+ : undefined
+ }
className={cn(
'transition-all duration-200',
isOtherNode && 'cursor-pointer',
- !isOtherNode && 'cursor-default',
+ isNavigable && 'cursor-pointer',
+ !isOtherNode &&
+ !isNavigable &&
+ 'cursor-default',
)}
>
)}
@@ -146,6 +147,7 @@ export default function CashflowPage() {
data={incomeBreakdown}
loading={isLoading}
currency={auth.user.currency_code}
+ period={period}
/>
diff --git a/tests/Browser/CashflowCategoryNavigationTest.php b/tests/Browser/CashflowCategoryNavigationTest.php
new file mode 100644
index 00000000..507cac42
--- /dev/null
+++ b/tests/Browser/CashflowCategoryNavigationTest.php
@@ -0,0 +1,65 @@
+onboarded()->create();
+ $account = Account::factory()->create(['user_id' => $user->id]);
+ $category = Category::factory()->create([
+ 'user_id' => $user->id,
+ 'type' => CategoryType::Expense,
+ 'name' => 'Groceries',
+ ]);
+
+ Transaction::factory()->create([
+ 'user_id' => $user->id,
+ 'account_id' => $account->id,
+ 'category_id' => $category->id,
+ 'amount' => -5000,
+ 'transaction_date' => now()->startOfMonth(),
+ ]);
+
+ $period = now()->format('Y-m');
+
+ $page = $this->actingAs($user)->visit("/cashflow?period={$period}");
+
+ $page->waitForText('Groceries', 10)
+ ->click('Groceries')
+ ->wait(2)
+ ->assertPathIs('/transactions')
+ ->assertQueryStringHas('category_ids', $category->id)
+ ->assertNoJavascriptErrors();
+});
+
+it('clicking an income category on cashflow page navigates to transactions with filters', function () {
+ $user = User::factory()->onboarded()->create();
+ $account = Account::factory()->create(['user_id' => $user->id]);
+ $category = Category::factory()->create([
+ 'user_id' => $user->id,
+ 'type' => CategoryType::Income,
+ 'name' => 'Salary',
+ ]);
+
+ Transaction::factory()->create([
+ 'user_id' => $user->id,
+ 'account_id' => $account->id,
+ 'category_id' => $category->id,
+ 'amount' => 200000,
+ 'transaction_date' => now()->startOfMonth(),
+ ]);
+
+ $period = now()->format('Y-m');
+
+ $page = $this->actingAs($user)->visit("/cashflow?period={$period}");
+
+ $page->waitForText('Salary', 10)
+ ->click('Salary')
+ ->wait(2)
+ ->assertPathIs('/transactions')
+ ->assertQueryStringHas('category_ids', $category->id)
+ ->assertNoJavascriptErrors();
+});