cospend: filter recent activity by chart category selection
All checks were successful
CI / update (push) Successful in 1m27s
All checks were successful
CI / update (push) Successful in 1m27s
Clicking a category on the bar chart now filters the recent activity list to show only payments in that category. Includes a clear filter button and empty state message. Also increases recent splits from 10 to 30 for better coverage when filtering.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
let { data = { labels: [], datasets: [] }, title = '', height = '400px' } = $props<{ data?: any, title?: string, height?: string }>();
|
||||
let { data = { labels: [], datasets: [] }, title = '', height = '400px', onFilterChange = null } = $props<{ data?: any, title?: string, height?: string, onFilterChange?: ((categories: string[] | null) => void) | null }>();
|
||||
|
||||
let canvas = $state();
|
||||
let chart = $state();
|
||||
@@ -42,6 +42,19 @@
|
||||
return categoryColorMap[category] || nordColors[index % nordColors.length];
|
||||
}
|
||||
|
||||
function emitFilter() {
|
||||
if (!onFilterChange || !chart) return;
|
||||
const allVisible = chart.data.datasets.every((_, idx) => !chart.getDatasetMeta(idx).hidden);
|
||||
if (allVisible) {
|
||||
onFilterChange(null);
|
||||
} else {
|
||||
const visible = chart.data.datasets
|
||||
.filter((_, idx) => !chart.getDatasetMeta(idx).hidden)
|
||||
.map(ds => ds.label.toLowerCase());
|
||||
onFilterChange(visible);
|
||||
}
|
||||
}
|
||||
|
||||
function createChart() {
|
||||
if (!canvas || !data.datasets) return;
|
||||
|
||||
@@ -135,7 +148,6 @@
|
||||
},
|
||||
onClick: (event, legendItem, legend) => {
|
||||
const datasetIndex = legendItem.datasetIndex;
|
||||
const clickedMeta = chart.getDatasetMeta(datasetIndex);
|
||||
|
||||
// Check if only this dataset is currently visible
|
||||
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
|
||||
@@ -156,6 +168,7 @@
|
||||
}
|
||||
|
||||
chart.update();
|
||||
emitFilter();
|
||||
}
|
||||
},
|
||||
title: {
|
||||
@@ -229,6 +242,7 @@
|
||||
}
|
||||
|
||||
chart.update();
|
||||
emitFilter();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -89,7 +89,7 @@ export const GET: RequestHandler = async ({ locals, url }) => {
|
||||
},
|
||||
{ $unwind: '$paymentId' },
|
||||
{ $sort: { 'paymentId.date': -1, 'paymentId.createdAt': -1 } },
|
||||
{ $limit: 10 }
|
||||
{ $limit: 30 }
|
||||
]);
|
||||
|
||||
// For settlements, fetch the other user's split info
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
let error = $state(null);
|
||||
let monthlyExpensesData = $state(data.monthlyExpensesData || { labels: [], datasets: [] });
|
||||
let expensesLoading = $state(false);
|
||||
let categoryFilter = $state(null);
|
||||
|
||||
let filteredSplits = $derived(
|
||||
categoryFilter
|
||||
? (balance.recentSplits || []).filter(split => categoryFilter.includes(split.paymentId?.category))
|
||||
: balance.recentSplits || []
|
||||
);
|
||||
|
||||
// Component references for refreshing
|
||||
let enhancedBalanceComponent;
|
||||
@@ -177,6 +184,7 @@
|
||||
data={monthlyExpensesData}
|
||||
title="Monthly Expenses by Category"
|
||||
height="400px"
|
||||
onFilterChange={(categories) => categoryFilter = categories}
|
||||
/>
|
||||
{:else}
|
||||
<div class="loading">
|
||||
@@ -194,9 +202,17 @@
|
||||
<div class="error">Error: {error}</div>
|
||||
{:else if balance.recentSplits && balance.recentSplits.length > 0}
|
||||
<div class="recent-activity">
|
||||
<h2>Recent Activity</h2>
|
||||
<div class="recent-activity-header">
|
||||
<h2>Recent Activity{#if categoryFilter} <span class="filter-label">— {categoryFilter.map(c => getCategoryName(c)).join(', ')}</span>{/if}</h2>
|
||||
{#if categoryFilter}
|
||||
<button class="clear-filter" onclick={() => categoryFilter = null}>Clear filter</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if filteredSplits.length === 0}
|
||||
<p class="no-results">No recent activity in {categoryFilter.map(c => getCategoryName(c)).join(', ')}.</p>
|
||||
{/if}
|
||||
<div class="activity-dialog">
|
||||
{#each balance.recentSplits as split}
|
||||
{#each filteredSplits as split}
|
||||
{#if isSettlementPayment(split.paymentId)}
|
||||
<!-- Settlement Payment Display - User -> User Flow -->
|
||||
<a
|
||||
@@ -363,11 +379,49 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.recent-activity h2 {
|
||||
.recent-activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.recent-activity h2 {
|
||||
margin: 0;
|
||||
color: var(--nord0);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
color: var(--nord3);
|
||||
}
|
||||
|
||||
.clear-filter {
|
||||
background: none;
|
||||
border: 1px solid var(--nord4);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
color: var(--nord3);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.clear-filter:hover {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: var(--nord3);
|
||||
font-style: italic;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.recent-activity {
|
||||
background: var(--accent-dark);
|
||||
@@ -377,6 +431,24 @@
|
||||
.recent-activity h2 {
|
||||
color: var(--font-default-dark);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: var(--nord4);
|
||||
}
|
||||
|
||||
.clear-filter {
|
||||
border-color: var(--nord3);
|
||||
color: var(--nord4);
|
||||
}
|
||||
|
||||
.clear-filter:hover {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
color: var(--nord4);
|
||||
}
|
||||
}
|
||||
|
||||
.activity-dialog {
|
||||
|
||||
Reference in New Issue
Block a user