import 'package:flutter/material.dart'; import '../../../core/utils/ru_date.dart'; import '../../../core/widgets/money_text.dart'; import '../../portfolio/labels.dart' show formatQty; import '../data/imports_api.dart'; /// Сверка: the report's own positions and cash balances against what the ledger derives. /// /// When everything matches this collapses to a single green line — a full table of zeroes /// is noise. A mismatch is the whole point of the screen, so it stays expanded and red. class ReconciliationCard extends StatelessWidget { const ReconciliationCard({required this.reconciliation, super.key}); final Reconciliation reconciliation; @override Widget build(BuildContext context) { final theme = Theme.of(context); final asOf = reconciliation.asOf; final title = asOf == null ? 'Сверка' : 'Сверка на ${ruDate(asOf)}'; if (reconciliation.isEmpty) { return Card( child: ListTile( leading: Icon(Icons.remove_circle_outline, color: theme.colorScheme.outline), title: Text(title), subtitle: const Text('В отчёте нет остатков для сверки'), ), ); } if (reconciliation.matches) { return Card( child: ListTile( leading: const Icon(Icons.check_circle_outline, color: Colors.green), title: Text(title), subtitle: Text( 'Всё сошлось: позиций ${reconciliation.positions.length}, ' 'остатков ${reconciliation.cash.length}', style: const TextStyle(color: Colors.green), ), ), ); } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.error_outline, color: theme.colorScheme.error), const SizedBox(width: 8), Text(title, style: theme.textTheme.titleMedium), ], ), const SizedBox(height: 4), Text( 'Отчёт и леджер разошлись. Импортировать можно, но расхождение стоит объяснить.', style: theme.textTheme.bodySmall, ), if (reconciliation.positions.isNotEmpty) ...[ const SizedBox(height: 12), Text('Позиции', style: theme.textTheme.titleSmall), const SizedBox(height: 4), _ScrollableTable( columns: const ['Позиция', 'Из отчёта', 'Из леджера', 'Расхождение'], rows: [ for (final p in reconciliation.positions) _Row( cells: [ p.title, _qty(p.qtyReport), _qty(p.qtyDerived), _qty(p.qtyDelta), ], highlight: !p.matches, ), ], ), ], if (reconciliation.cash.isNotEmpty) ...[ const SizedBox(height: 12), Text('Денежные остатки', style: theme.textTheme.titleSmall), const SizedBox(height: 4), _ScrollableTable( columns: const ['Валюта', 'Из отчёта', 'Из леджера', 'Расхождение'], rows: [ for (final c in reconciliation.cash) _Row( cells: [ c.currency, _money(c.balanceReport, c.currency), _money(c.balanceDerived, c.currency), _money(c.delta, c.currency), ], highlight: !c.matches, ), ], ), ], ], ), ), ); } /// A quantity the ledger could not produce is an em dash, not a zero: an unknown position /// and an empty one are different findings. static String _qty(String? value) => value == null || value.isEmpty ? '—' : formatQty(value); static String _money(String? value, String currency) => value == null || value.isEmpty ? '—' : MoneyText.format(value, currency); } class _Row { const _Row({required this.cells, required this.highlight}); final List cells; final bool highlight; } /// A narrow table that scrolls sideways rather than overflowing on a phone. class _ScrollableTable extends StatelessWidget { const _ScrollableTable({required this.columns, required this.rows}); final List columns; final List<_Row> rows; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( columnSpacing: 24, headingRowHeight: 36, dataRowMinHeight: 36, dataRowMaxHeight: 48, columns: [for (final c in columns) DataColumn(label: Text(c))], rows: [ for (final r in rows) DataRow( color: r.highlight ? WidgetStatePropertyAll(theme.colorScheme.errorContainer.withValues(alpha: 0.4)) : null, cells: [ for (final cell in r.cells) DataCell(Text( cell, style: r.highlight ? TextStyle(color: theme.colorScheme.error) : null, )), ], ), ], ), ); } }