import 'package:dio/dio.dart'; import 'package:fintracker_api/fintracker_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/api/api_client.dart'; import '../../core/auth/auth_controller.dart'; import '../../core/widgets/async_value_view.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/money_text.dart'; import 'providers.dart'; const _roleOrder = [AccountRole.liquid, AccountRole.savings, AccountRole.investment, AccountRole.debt]; String _roleLabel(AccountRole role) => switch (role) { AccountRole.liquid => 'Ликвидные', AccountRole.savings => 'Сбережения', AccountRole.investment => 'Инвестиции', AccountRole.debt => 'Долги', AccountRole.unknownDefaultOpenApi => 'Неизвестно', }; String _kindLabel(AccountKind kind) => switch (kind) { AccountKind.zmCash => 'Наличные', AccountKind.zmCard => 'Карта', AccountKind.zmChecking => 'Расчётный счёт', AccountKind.zmDeposit => 'Вклад', AccountKind.zmLoan => 'Кредит', AccountKind.zmEmoney => 'Электронные деньги', AccountKind.zmDebt => 'Долг', AccountKind.broker => 'Брокерский счёт', AccountKind.manualAsset => 'Актив вручную', AccountKind.unknownDefaultOpenApi => 'Неизвестно', }; /// Счета: every account grouped by role, with in-place role and /// include-in-net-worth edits (`PATCH /accounts/{id}`). Archived accounts /// collapse into their own section at the bottom regardless of role. class AccountsPage extends ConsumerWidget { const AccountsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final accounts = ref.watch(accountsProvider); return Scaffold( appBar: AppBar(title: const Text('Счета')), body: RefreshIndicator( onRefresh: () async => ref.invalidate(accountsProvider), child: AsyncValueView( value: accounts, onRetry: () => ref.invalidate(accountsProvider), data: (rows) { if (rows.isEmpty) { return ListView( children: const [ EmptyState( icon: Icons.account_balance_outlined, message: 'Счетов ещё нет — нужна синхронизация.', ), ], ); } final active = rows.where((a) => !a.archived).toList(); final archived = rows.where((a) => a.archived).toList(); return ListView( padding: const EdgeInsets.all(16), children: [ for (final role in _roleOrder) if (active.any((a) => a.role == role)) _RoleSection( title: _roleLabel(role), accounts: active.where((a) => a.role == role).toList(), ), if (archived.isNotEmpty) ExpansionTile( title: Text('Архивные (${archived.length})'), initiallyExpanded: false, children: [for (final a in archived) _AccountTile(account: a)], ), ], ); }, ), ), ); } } class _RoleSection extends StatelessWidget { const _RoleSection({required this.title, required this.accounts}); final String title; final List accounts; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(bottom: 4), child: Text(title, style: Theme.of(context).textTheme.titleMedium), ), for (final a in accounts) _AccountTile(account: a), ], ), ); } } class _AccountTile extends ConsumerStatefulWidget { const _AccountTile({required this.account}); final AccountOut account; @override ConsumerState<_AccountTile> createState() => _AccountTileState(); } class _AccountTileState extends ConsumerState<_AccountTile> { bool _saving = false; Future _patch(AccountPatch patch) async { setState(() => _saving = true); try { await ref.read(apiProvider).getAccountsApi().accountsPatch( accountId: widget.account.id, accountPatch: patch, ); ref.invalidate(accountsProvider); } on DioException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(problemMessage(e)))); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final a = widget.account; return Card( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(a.name, style: Theme.of(context).textTheme.titleSmall), Text( '${_kindLabel(a.kind)} · ${a.currency}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), MoneyText( a.balance ?? '0', currency: a.currency, style: Theme.of(context).textTheme.titleMedium, ), ], ), const SizedBox(height: 4), Row( children: [ Expanded( child: DropdownButtonFormField( initialValue: a.role, isDense: true, decoration: const InputDecoration(labelText: 'Роль', isDense: true), items: [ for (final r in _roleOrder) DropdownMenuItem(value: r, child: Text(_roleLabel(r))), ], onChanged: _saving ? null : (role) { if (role != null) _patch(AccountPatch(role: role)); }, ), ), const SizedBox(width: 12), Column( children: [ const Text('В капитал', style: TextStyle(fontSize: 11)), Switch( value: a.includeInNetWorth, onChanged: _saving ? null : (v) => _patch(AccountPatch(includeInNetWorth: v)), ), ], ), ], ), ], ), ), ); } }