Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9d1eeae
flutter: Share→SharePlus migration step 1 — switch to SharePlus.insta…
zensgit Sep 27, 2025
6301183
flutter: clean QR widget placeholder + remove local Share stub; use s…
zensgit Sep 27, 2025
c1a00d2
flutter: fix use_build_context_synchronously in AcceptInvitationDialo…
zensgit Sep 27, 2025
139d958
flutter: context cleanup batch 1 (right_click_copy, custom_theme_edit…
zensgit Sep 28, 2025
cb8f290
flutter: context safety fixes in ThemeShareDialog and DeleteFamilyDialog
zensgit Sep 28, 2025
f187d07
flutter: context cleanup batch 2 (admin template page + batch op bar)
zensgit Sep 28, 2025
741b89f
flutter: fix AuditLogsScreen statistics type; continue context-safety…
zensgit Sep 28, 2025
51c6be6
flutter: context cleanup batch 3 prep — refine messenger/navigator ca…
zensgit Sep 28, 2025
39c2f5f
flutter: context cleanup batch 3 — move captures post-await + scoped …
zensgit Sep 28, 2025
a1e7950
flutter: context cleanup batch 3 — ThemeManagementScreen messenger/na…
zensgit Sep 28, 2025
d4124c3
flutter: add User Assets overview screen + route; fix analyzer blocke…
zensgit Sep 28, 2025
3973474
flutter: transactions Phase A scaffold — add search/filter bar and gr…
zensgit Sep 28, 2025
bff7320
flutter: wire Dashboard RecentTransactions filter button to open Tran…
zensgit Sep 28, 2025
50736fa
docs: Transactions Filters & Grouping Phase B design (draft)
zensgit Sep 28, 2025
58110a6
flutter: stabilize transactions UI scaffold; fix tests (search bar + …
zensgit Sep 28, 2025
3437c06
flutter: TransactionsScreen uses shared TransactionList with search/g…
zensgit Sep 28, 2025
c03b2fe
flutter: transactions Phase B1 — persist grouping and collapsed group…
zensgit Sep 28, 2025
b54a119
fix: remove stray closing brace in transaction_provider
zensgit Sep 28, 2025
d9fd115
flutter: transactions — add grouping menu wired to controller; keep n…
zensgit Sep 29, 2025
b675dde
Merge main into feature/transactions-phase-b1
zensgit Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/FEATURE_TX_FILTERS_GROUPING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Transactions Filters & Grouping — Phase B Design (草案)

Purpose
- Deliver practical, performant filtering + grouping for transactions.
- Keep UI declarative, leverage existing providers; avoid duplicating domain rules.

Scope
- Filters: text (描述/备注/收款方), 日期范围, 类型(支出/收入/转账), 账户, 分类, 标签, 金额区间。
- Grouping: 按 日期 / 分类 / 账户;每组显示小计,总计在页眉/页脚。
- Persist: 轻量本地记忆最近一次筛选与分组(per-ledger)。
- Non-goals: 复杂多级排序、跨家庭聚合、导出(独立PR)。

UX Outline
- TransactionsScreen 顶部显示 FilterBar(收起/展开)。
- FilterBar:
- 搜索框(回车触发)、“筛选”按钮打开面板(日期/类型/账户/分类/标签/金额)、“重置”按钮。
- 右侧“分组切换”:日期/分类/账户;在移动端为 Segmented 形式。
- 列表:
- 分组头包含:组名 + 小计金额(正/负色),可折叠。
- 空状态支持“清空筛选”。

State & Providers
- transactionControllerProvider(现有):
- 扩展:`applyFilter(TransactionQuery q)`、`clearFilter()`、`setGrouping(Grouping g)`、`toggleGroupCollapse(key)`。
- 状态新增:`currentQuery`、`grouping`、`groupCollapse: Set<String>`。
- Query 模型(新):
```dart
class TransactionQuery {
final String? text;
final DateTimeRange? dateRange;
final Set<TransactionType>? types;
final Set<String>? accountIds;
final Set<String>? categoryIds;
final Set<String>? tagIds;
final double? amountMin;
final double? amountMax;
const TransactionQuery({ ... });
TransactionQuery copyWith(...);
bool get isEmpty;
}
```
- Grouping(新枚举):`date | category | account`。
- 组合选择器:账户/分类/标签用现有 providers 源数据,支持多选(Chip/BottomSheet)。

Data Flow
- UI → controller.applyFilter(query) → 计算/筛选 in-memory(Phase B)
- 未来 Phase C:若列表很大,落地到服务端 query(分页 + 去抖)。

API/Backend Impact
- Phase B:无服务端改动。
- Phase C(另案):增量新增 `/transactions/search` 支持字段与分页;Rust 侧生成 SQLx 查询 + .sqlx 更新。

Persistence
- SharedPreferences key: `tx_ui_<ledgerId>_{query,grouping}`;
- 在 initState 读取并应用。

Accessibility & i18n
- 分组头可聚焦;筛选面板控件提供语义标签;所有文案纳入 i18n 字典(后续批量替换)。

Performance
- 过滤与分组在内存进行:
- 先按日期预分桶(Map<Date, List>)复用;
- 计算小计 O(n);
- 大列表按需构建(ListView.builder)。

Acceptance Criteria
- 可对任意组合条件过滤,切换分组视图,显示正确小计与总计。
- 刷新后保留上次筛选/分组。
- analyzer 0 hard errors;tests 通过。

Phasing
- B1:完成 Query 模型 + 控制器 + UI 面板(不含服务端)。
- B2:分组小计 + 折叠 + 持久化。
- B3:微交互与过渡动画;边界测试。

Risks / Out of Scope
- 非线性金额转换/多货币换算(另案)。
- 高维度组合过滤在低端机的性能(必要时分页)。
68 changes: 68 additions & 0 deletions jive-flutter/lib/providers/transaction_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import 'package:jive_money/services/api/transaction_service.dart';
import 'package:jive_money/models/transaction.dart';
import 'package:jive_money/models/transaction_filter.dart';
import 'package:shared_preferences/shared_preferences.dart';
<<<<<<< HEAD
=======
import 'package:jive_money/providers/ledger_provider.dart';

enum TransactionGrouping { date, category, account }
>>>>>>> origin/main

/// 交易状态
enum TransactionGrouping { date, category, account }

class TransactionState {
final List<Transaction> transactions;
final List<Transaction> filteredTransactions;
Expand All @@ -18,6 +23,10 @@ class TransactionState {
final int totalCount;
final double totalIncome;
final double totalExpense;
<<<<<<< HEAD
// Phase B scaffolding: grouping + collapsed groups
=======
>>>>>>> origin/main
final TransactionGrouping grouping;
final Set<String> groupCollapse;

Expand Down Expand Up @@ -302,6 +311,65 @@ class TransactionController extends StateNotifier<TransactionState> {
state = state.copyWith(error: null);
}

/// 设置分组方式(Phase B)
void setGrouping(TransactionGrouping grouping) {
if (state.grouping == grouping) return;
state = state.copyWith(grouping: grouping);
_persistGrouping();
}

/// 切换分组折叠状态(Phase B)
void toggleGroupCollapse(String key) {
final collapsed = Set<String>.from(state.groupCollapse);
if (collapsed.contains(key)) {
collapsed.remove(key);
} else {
collapsed.add(key);
}
state = state.copyWith(groupCollapse: collapsed);
_persistGroupCollapse(collapsed);
}

// ---- View preference persistence (Phase B1) ----
Future<void> _loadViewPrefs() async {
try {
final prefs = await SharedPreferences.getInstance();
final groupingStr = prefs.getString("tx_grouping");
Comment on lines +336 to +337
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The persistence keys 'tx_grouping' and 'tx_group_collapse' are not namespaced per ledger as mentioned in the PR description. Consider adding ledger context to these keys to support multiple ledgers properly.

Copilot uses AI. Check for mistakes.
TransactionGrouping grouping = state.grouping;
if (groupingStr != null) {
grouping = TransactionGrouping.values.firstWhere(
(g) => g.name == groupingStr,
orElse: () => TransactionGrouping.date,
);
}
final collapsedList =
prefs.getStringList("tx_group_collapse") ?? const <String>[];
if (grouping != state.grouping ||
collapsedList.length != state.groupCollapse.length) {
state = state.copyWith(
grouping: grouping,
groupCollapse: collapsedList.toSet(),
);
}
} catch (_) {
// Ignore persistence errors
}
}

Future<void> _persistGrouping() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString("tx_grouping", state.grouping.name);
} catch (_) {}
}

Future<void> _persistGroupCollapse(Set<String> collapsed) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList("tx_group_collapse", collapsed.toList());
} catch (_) {}
}

/// 更新状态并计算统计数据
void _updateState(List<Transaction> transactions) {
final filteredTransactions = state.filter != null
Expand Down
5 changes: 4 additions & 1 deletion jive-flutter/lib/screens/dashboard/dashboard_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ class DashboardScreen extends ConsumerWidget {
RecentTransactions(
transactions: recentTransactions,
onViewAll: () {
// 导航到交易页面
context.go(AppRoutes.transactions);
},
onFilter: () {
context.go(AppRoutes.transactions);
},
),
const SizedBox(height: 24),
Expand Down
53 changes: 38 additions & 15 deletions jive-flutter/lib/screens/transactions/transactions_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:jive_money/core/router/app_router.dart';
import 'package:jive_money/providers/transaction_provider.dart';
import 'package:jive_money/ui/components/transactions/transaction_list_item.dart';
import 'package:jive_money/ui/components/transactions/transaction_list.dart';
import 'package:jive_money/models/transaction.dart';

class TransactionsScreen extends ConsumerStatefulWidget {
Expand Down Expand Up @@ -35,6 +35,7 @@ class _TransactionsScreenState extends ConsumerState<TransactionsScreen>
@override
Widget build(BuildContext context) {
final transactionState = ref.watch(transactionControllerProvider);
final groupByDate = transactionState.grouping == TransactionGrouping.date;

return Scaffold(
appBar: AppBar(
Expand All @@ -49,6 +50,23 @@ class _TransactionsScreenState extends ConsumerState<TransactionsScreen>
],
),
actions: [
PopupMenuButton<TransactionGrouping>(
tooltip: "分组方式",
onSelected: (g) {
ref.read(transactionControllerProvider.notifier).setGrouping(g);
if (g != TransactionGrouping.date) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("分类/账户分组预览中,暂以平铺显示")),
);
}
},
itemBuilder: (context) => const [
PopupMenuItem(value: TransactionGrouping.date, child: Text("按日期分组")),
PopupMenuItem(value: TransactionGrouping.category, child: Text("按分类分组(预览)")),
PopupMenuItem(value: TransactionGrouping.account, child: Text("按账户分组(预览)")),
],
icon: const Icon(Icons.view_list),
),
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: _showFilterDialog,
Expand Down Expand Up @@ -131,22 +149,27 @@ class _TransactionsScreenState extends ConsumerState<TransactionsScreen>
return _buildEmptyState(type);
}

return RefreshIndicator(
return TransactionList(
transactions: filtered,
groupByDate: groupByDate,
showSearchBar: true,
onSearch: (q) =>
ref.read(transactionControllerProvider.notifier).search(q),
onClearSearch: () =>
ref.read(transactionControllerProvider.notifier).search(''),
onToggleGroup: () {
final next = groupByDate ? TransactionGrouping.account : TransactionGrouping.date;
ref.read(transactionControllerProvider.notifier).setGrouping(next);
if (next != TransactionGrouping.date) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("非日期分组预览中,暂以平铺显示")),
);
}
},
onRefresh: () =>
ref.read(transactionControllerProvider.notifier).refresh(),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 80),
itemCount: filtered.length,
itemBuilder: (context, index) {
final transaction = filtered[index];
return TransactionListItem(
transaction: transaction,
onTap: () {
context.go('${AppRoutes.transactions}/${transaction.id}');
},
);
},
),
onTransactionTap: (t) =>
context.go('${AppRoutes.transactions}/${t.id}'),
);
}

Expand Down
16 changes: 16 additions & 0 deletions jive-flutter/lib/ui/components/dashboard/recent_transactions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:jive_money/models/transaction.dart';
import 'package:jive_money/ui/components/cards/transaction_card.dart';

class RecentTransactions extends StatelessWidget {
final VoidCallback? onFilter;
final List<Transaction> transactions;
final String title;
final VoidCallback? onViewAll;
Expand All @@ -16,6 +17,7 @@ class RecentTransactions extends StatelessWidget {
this.title = '最近交易',
this.onViewAll,
this.maxItems = 5,
this.onFilter,
});

@override
Expand Down Expand Up @@ -43,6 +45,12 @@ class RecentTransactions extends StatelessWidget {
),
),
const Spacer(),
if (onFilter != null)
IconButton(
tooltip: "筛选",
icon: const Icon(Icons.filter_list),
onPressed: onFilter,
),
if (onViewAll != null)
TextButton(
onPressed: onViewAll,
Expand Down Expand Up @@ -130,13 +138,15 @@ class GroupedRecentTransactions extends StatelessWidget {
final List<Transaction> transactions;
final String title;
final VoidCallback? onViewAll;
final VoidCallback? onFilter;
final int maxDays;

const GroupedRecentTransactions({
super.key,
required this.transactions,
this.title = '最近交易',
this.onViewAll,
this.onFilter,
this.maxDays = 3,
});

Expand Down Expand Up @@ -166,6 +176,12 @@ class GroupedRecentTransactions extends StatelessWidget {
),
),
const Spacer(),
if (onFilter != null)
IconButton(
tooltip: "筛选",
icon: const Icon(Icons.filter_list),
onPressed: onFilter,
),
if (onViewAll != null)
TextButton(
onPressed: onViewAll,
Expand Down
Loading
Loading