/// Signed-in account details, sync status, and sign-out. library; 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/constants.dart'; import '../widgets/responsive_body.dart'; class AccountScreen extends StatelessWidget { const AccountScreen({super.key}); @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, ), ), ), 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, ), ], ), ), const SizedBox(height: 24), OutlinedButton.icon( onPressed: () async { await auth.logout(); if (!context.mounted) return; Navigator.of(context).pop(); }, 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'; } }