/// Signed-in account details, sync status, bank link, and sign-out. library; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import '../providers/bank_provider.dart'; import '../providers/sync_provider.dart'; import '../providers/tx_meta_provider.dart'; import '../utils/connect_bank.dart'; import '../utils/constants.dart'; import '../widgets/admin_create_transaction_dialog.dart'; import '../widgets/responsive_body.dart'; class AccountScreen extends StatefulWidget { const AccountScreen({super.key}); @override State createState() => _AccountScreenState(); } class _AccountScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; unawaited(context.read().refreshSession()); }); } @override Widget build(BuildContext context) { final auth = context.watch(); final meta = context.watch(); final bank = context.watch(); final theme = Theme.of(context); final colors = theme.colorScheme; final categorized = meta.labeledTransactionCount(bank.transactions); final perTxAnnotations = meta.syncableAnnotationCount; final viaMerchantOnly = meta.merchantOnlyLabeledCount(bank.transactions); return Scaffold( appBar: AppBar(title: const Text('Account')), body: ResponsiveBody( child: ListView( padding: const EdgeInsets.fromLTRB(16, 24, 16, 32), children: [ Center( child: CircleAvatar( radius: 36, backgroundColor: colors.primaryContainer, child: Text( auth.initials, style: theme.textTheme.headlineSmall?.copyWith( color: colors.onPrimaryContainer, ), ), ), ), const SizedBox(height: 16), Text( auth.displayName, textAlign: TextAlign.center, style: theme.textTheme.headlineSmall, ), if (auth.email case final email?) Padding( padding: const EdgeInsets.only(top: 4), child: Text( email, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: colors.onSurfaceVariant, ), ), ), if (auth.isAdmin) Padding( padding: const EdgeInsets.only(top: 8), child: Chip( avatar: Icon( Icons.shield_outlined, size: 16, color: colors.onSecondaryContainer, ), label: const Text('Admin'), visualDensity: VisualDensity.compact, backgroundColor: colors.secondaryContainer, labelStyle: theme.textTheme.labelMedium?.copyWith( color: colors.onSecondaryContainer, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 24), Card( child: Column( children: [ ListTile( leading: const Icon(Icons.cloud_outlined), title: const Text('PocketBase server'), subtitle: Text(kPocketBaseUrl), ), const Divider(height: 1), const _CloudSyncTile(), const Divider(height: 1), ListTile( leading: const Icon(Icons.phone_android_outlined), title: const Text('What syncs'), subtitle: Text( 'Categorized on device: $categorized\n' '· Per-tx annotations → PocketBase tx_meta: $perTxAnnotations\n' '· Via merchant rules only (not separate tx_meta rows): $viaMerchantOnly\n' 'Merchant rules synced: ${meta.merchantRuleCount}\n' 'Bank transactions → PocketBase transactions: ' '${bank.transactions.length}', ), isThreeLine: true, ), ], ), ), if (auth.isAdmin) ...[ const SizedBox(height: 16), Card( child: ListTile( leading: const Icon(Icons.add_card_outlined), title: const Text('Create test transaction'), subtitle: const Text( 'Admin only · local + syncs to PocketBase', ), trailing: const Icon(Icons.chevron_right), onTap: () async { final tx = await showAdminCreateTransactionDialog(context); if (tx == null || !context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Created “${tx.description}” · ' '${tx.amount.toStringAsFixed(2)} ${tx.currency}', ), ), ); // Push so other devices / PB see it soon. final sync = context.read(); if (sync.canSync && !sync.isSyncing) { unawaited(sync.syncNow()); } }, ), ), ], const SizedBox(height: 16), Card( child: ListTile( leading: Icon( bank.isLinked ? Icons.link : Icons.link_off_outlined, ), title: const Text('Bank link'), subtitle: Text( bank.isLinked ? 'Linked · ${bank.status.status}' : bank.hasData ? 'Not linked · ${bank.transactions.length} synced txs on device' : 'Not linked · connect to fetch live balance and feed', ), trailing: bank.isLinked ? null : TextButton( onPressed: bank.isConnecting ? null : () => runConnectBank(context, bank), child: Text( bank.isConnecting ? 'Opening…' : 'Connect', ), ), ), ), const SizedBox(height: 24), OutlinedButton.icon( onPressed: () async { await auth.logout(); if (!context.mounted) return; Navigator.of(context).popUntil((route) => route.isFirst); }, icon: const Icon(Icons.logout), label: const Text('Sign out'), ), ], ), ), ); } } /// Isolated so sync progress ticks do not rebuild account stats. class _CloudSyncTile extends StatelessWidget { const _CloudSyncTile(); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: context.read(), builder: (context, _) { final sync = context.read(); final lastSync = sync.lastSyncedAt; final syncSubtitle = sync.isSyncing ? (sync.statusDetail.isEmpty ? 'Syncing… ${(sync.syncProgress * 100).round()}%' : '${sync.statusDetail} · ${(sync.syncProgress * 100).round()}%') : sync.lastError != null ? 'Last error: ${sync.lastError}' : lastSync == null ? 'Not synced yet' : 'Last sync ${_formatTime(lastSync)}'; return Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: sync.isSyncing ? SizedBox( width: 24, height: 24, child: Padding( padding: const EdgeInsets.all(2), child: CircularProgressIndicator( strokeWidth: 2, value: sync.syncProgress <= 0 ? null : sync.syncProgress.clamp(0.02, 1.0), ), ), ) : Icon( sync.lastError == null ? Icons.sync : Icons.sync_problem_outlined, ), title: const Text('Cloud sync'), subtitle: Text(syncSubtitle), trailing: TextButton( onPressed: sync.isSyncing ? null : () => sync.syncNow(), child: const Text('Sync now'), ), ), if (sync.lastSyncSummary.isNotEmpty) ...[ const Divider(height: 1), ListTile( leading: const Icon(Icons.fact_check_outlined), title: const Text('Last sync detail'), subtitle: Text(sync.lastSyncSummary), ), ], ], ); }, ); } static String _formatTime(DateTime time) { final local = time.toLocal(); final h = local.hour.toString().padLeft(2, '0'); final m = local.minute.toString().padLeft(2, '0'); return '$h:$m'; } }