import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../core/services/api_service.dart'; /// Transaction Sync Dialog /// Shows PhonePe transactions and suggests payment matches class TransactionSyncDialog extends StatefulWidget { const TransactionSyncDialog({super.key}); @override State createState() => _TransactionSyncDialogState(); } class _TransactionSyncDialogState extends State { final _apiService = ApiService(); bool _isSyncing = false; bool _isLoadingReview = false; Map? _syncResults; List _reviewQueue = []; int _daysBack = 7; @override void initState() { super.initState(); _loadReviewQueue(); } Future _loadReviewQueue() async { setState(() { _isLoadingReview = true; }); try { final response = await _apiService.get('/transaction-sync/review-queue'); if (response['success'] == true) { setState(() { _reviewQueue = response['data']['review_queue'] ?? []; _isLoadingReview = false; }); } else { setState(() { _isLoadingReview = false; }); } } catch (e) { setState(() { _isLoadingReview = false; }); Get.snackbar( 'Error', 'Failed to load review queue: $e', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, ); } } Future _syncTransactions() async { setState(() { _isSyncing = true; _syncResults = null; }); try { final response = await _apiService.post( '/transaction-sync/sync', {'days_back': _daysBack}, ); if (response['success'] == true) { setState(() { _syncResults = response['data']; _isSyncing = false; }); Get.snackbar( 'Sync Complete!', '${response['data']['autoRecorded']} payments auto-recorded, ' '${response['data']['needsReview']} need review', backgroundColor: Colors.green.shade100, colorText: Colors.green.shade800, icon: Icon(Icons.check_circle, color: Colors.green.shade600), duration: Duration(seconds: 4), ); // Reload review queue await _loadReviewQueue(); } else { setState(() { _isSyncing = false; }); Get.snackbar( 'Sync Failed', response['message'] ?? 'Failed to sync transactions', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, ); } } catch (e) { setState(() { _isSyncing = false; }); Get.snackbar( 'Error', 'Failed to sync: $e', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, ); } } Future _approveMatch(dynamic item) async { final transaction = item['transaction']; final match = item['suggestedMatch']; try { final txnDate = DateTime.parse(transaction['transactionDate']); final response = await _apiService.post( '/transaction-sync/approve', { 'transaction_id': transaction['id'], 'group_id': match['group']['id'], 'user_id': match['member']['User']['id'], 'month': txnDate.month, 'year': txnDate.year, 'amount': transaction['amount'] / 100, }, ); if (response['success'] == true) { Get.snackbar( 'Approved!', 'Payment recorded successfully', backgroundColor: Colors.green.shade100, colorText: Colors.green.shade800, icon: Icon(Icons.check_circle, color: Colors.green.shade600), ); // Remove from review queue setState(() { _reviewQueue.remove(item); }); } else { Get.snackbar( 'Failed', response['message'] ?? 'Could not approve match', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, ); } } catch (e) { Get.snackbar( 'Error', 'Failed to approve: $e', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, ); } } Future _rejectMatch(dynamic item) async { final transaction = item['transaction']; try { await _apiService.post( '/transaction-sync/reject', { 'transaction_id': transaction['id'], 'reason': 'Manager rejected', }, ); // Remove from review queue setState(() { _reviewQueue.remove(item); }); Get.snackbar( 'Rejected', 'Match rejected', backgroundColor: Colors.grey.shade100, colorText: Colors.grey.shade800, ); } catch (e) { Get.snackbar( 'Error', 'Failed to reject: $e', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, ); } } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), ), child: Container( width: double.infinity, constraints: BoxConstraints( maxWidth: 600.w, maxHeight: 0.85.sh, ), child: Column( children: [ // Header Container( padding: EdgeInsets.all(20.w), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.blue.shade600, Colors.blue.shade700, ], ), borderRadius: BorderRadius.only( topLeft: Radius.circular(16.r), topRight: Radius.circular(16.r), ), ), child: Row( children: [ Container( padding: EdgeInsets.all(8.w), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8.r), ), child: Icon( Icons.sync, color: Colors.white, size: 24.w, ), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Auto-Sync Payments', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, color: Colors.white, ), ), Text( 'Import PhonePe transactions', style: TextStyle( fontSize: 14.sp, color: Colors.white.withOpacity(0.9), ), ), ], ), ), IconButton( onPressed: () => Navigator.pop(context), icon: Icon(Icons.close, color: Colors.white, size: 24.w), ), ], ), ), // Content Expanded( child: SingleChildScrollView( padding: EdgeInsets.all(20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Sync Controls _buildSyncControls(), if (_syncResults != null) ...[ SizedBox(height: 20.h), _buildSyncResults(), ], if (_reviewQueue.isNotEmpty) ...[ SizedBox(height: 24.h), _buildReviewQueueSection(), ], if (_reviewQueue.isEmpty && !_isLoadingReview && _syncResults == null) ...[ SizedBox(height: 20.h), _buildEmptyState(), ], ], ), ), ), ], ), ), ); } Widget _buildSyncControls() { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12.r), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.cloud_download, color: Colors.blue.shade700, size: 20.w), SizedBox(width: 8.w), Text( 'Pull Transactions from PhonePe', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.blue.shade900, ), ), ], ), SizedBox(height: 12.h), // Days selection Row( children: [ Text( 'Last', style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade700), ), SizedBox(width: 8.w), DropdownButton( value: _daysBack, items: [7, 14, 30, 60, 90].map((days) { return DropdownMenuItem( value: days, child: Text('$days days', style: TextStyle(fontSize: 14.sp)), ); }).toList(), onChanged: (value) { setState(() { _daysBack = value!; }); }, ), ], ), SizedBox(height: 16.h), // Sync button SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isSyncing ? null : _syncTransactions, icon: _isSyncing ? SizedBox( width: 20.w, height: 20.h, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Icon(Icons.sync, size: 20.w), label: Text( _isSyncing ? 'Syncing...' : 'Sync Transactions', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 14.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), ), ), ), SizedBox(height: 8.h), Text( '• Auto-matches by phone number, name, and amount\n' '• High-confidence matches auto-recorded\n' '• Low-confidence matches need your review', style: TextStyle( fontSize: 12.sp, color: Colors.blue.shade700, height: 1.5, ), ), ], ), ); } Widget _buildSyncResults() { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(12.r), border: Border.all(color: Colors.green.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.check_circle, color: Colors.green.shade700, size: 20.w), SizedBox(width: 8.w), Text( 'Sync Results', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.green.shade900, ), ), ], ), SizedBox(height: 12.h), _buildResultRow('Total Transactions', _syncResults!['total'].toString()), _buildResultRow('Auto-Recorded', _syncResults!['autoRecorded'].toString(), color: Colors.green.shade700), _buildResultRow('Need Review', _syncResults!['needsReview'].toString(), color: Colors.orange.shade700), _buildResultRow('Could Not Match', _syncResults!['failed'].toString(), color: Colors.red.shade700), ], ), ); } Widget _buildResultRow(String label, String value, {Color? color}) { return Padding( padding: EdgeInsets.symmetric(vertical: 4.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade700), ), Text( value, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w700, color: color ?? Colors.grey.shade900, ), ), ], ), ); } Widget _buildReviewQueueSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Review Queue', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, color: Colors.grey.shade800, ), ), Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12.r), ), child: Text( '${_reviewQueue.length}', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w700, color: Colors.orange.shade800, ), ), ), ], ), SizedBox(height: 12.h), Text( 'These transactions need your confirmation:', style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade600), ), SizedBox(height: 12.h), ..._reviewQueue.map((item) => _buildReviewCard(item)), ], ); } Widget _buildReviewCard(dynamic item) { final transaction = item['transaction']; final match = item['suggestedMatch']; final amount = (transaction['amount'] / 100).toStringAsFixed(2); final payerName = transaction['payerName'] ?? 'Unknown'; final payerPhone = transaction['payerPhone'] ?? ''; final date = DateTime.parse(transaction['transactionDate']); final confidence = match['confidence']; final matchedBy = match['matchedBy']; final groupName = match['group']['name']; final memberName = match['member']?['User']?['full_name'] ?? 'Unknown'; final memberPhone = match['member']?['User']?['mobile_number'] ?? ''; return Container( margin: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), border: Border.all(color: Colors.orange.shade200), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4.r, offset: Offset(0, 2.h), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Transaction Details Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(Icons.account_balance_wallet, size: 18.w, color: Colors.blue.shade600), SizedBox(width: 6.w), Text( '₹$amount', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w800, color: Colors.blue.shade700, ), ), ], ), Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), decoration: BoxDecoration( color: _getConfidenceColor(confidence).withOpacity(0.1), borderRadius: BorderRadius.circular(12.r), border: Border.all(color: _getConfidenceColor(confidence)), ), child: Text( confidence.toUpperCase().replaceAll('_', ' '), style: TextStyle( fontSize: 11.sp, fontWeight: FontWeight.w600, color: _getConfidenceColor(confidence), ), ), ), ], ), SizedBox(height: 8.h), // Payer info Row( children: [ Icon(Icons.person, size: 14.w, color: Colors.grey.shade600), SizedBox(width: 6.w), Expanded( child: Text( '$payerName ${payerPhone.isNotEmpty ? "• $payerPhone" : ""}', style: TextStyle(fontSize: 13.sp, color: Colors.grey.shade700), overflow: TextOverflow.ellipsis, ), ), ], ), SizedBox(height: 4.h), Row( children: [ Icon(Icons.calendar_today, size: 14.w, color: Colors.grey.shade600), SizedBox(width: 6.w), Text( '${date.day}/${date.month}/${date.year}', style: TextStyle(fontSize: 13.sp, color: Colors.grey.shade700), ), ], ), SizedBox(height: 12.h), Divider(height: 1, color: Colors.grey.shade300), SizedBox(height: 12.h), // Suggested Match Row( children: [ Icon(Icons.auto_fix_high, size: 16.w, color: Colors.orange.shade600), SizedBox(width: 6.w), Text( 'Suggested Match:', style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w600, color: Colors.grey.shade700, ), ), ], ), SizedBox(height: 8.h), Container( padding: EdgeInsets.all(12.w), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8.r), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( groupName, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w700, color: Colors.grey.shade900, ), ), SizedBox(height: 6.h), Text( '$memberName ${memberPhone.isNotEmpty ? "• $memberPhone" : ""}', style: TextStyle(fontSize: 13.sp, color: Colors.grey.shade700), ), SizedBox(height: 6.h), Text( 'Matched by: $matchedBy', style: TextStyle( fontSize: 11.sp, color: Colors.grey.shade600, fontStyle: FontStyle.italic, ), ), ], ), ), SizedBox(height: 16.h), // Action Buttons Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _rejectMatch(item), icon: Icon(Icons.close, size: 18.w), label: Text('Reject', style: TextStyle(fontSize: 14.sp)), style: OutlinedButton.styleFrom( foregroundColor: Colors.red.shade600, side: BorderSide(color: Colors.red.shade600), padding: EdgeInsets.symmetric(vertical: 10.h), ), ), ), SizedBox(width: 12.w), Expanded( flex: 2, child: ElevatedButton.icon( onPressed: () => _approveMatch(item), icon: Icon(Icons.check, size: 18.w), label: Text('Approve & Record', style: TextStyle(fontSize: 14.sp)), style: ElevatedButton.styleFrom( backgroundColor: Colors.green.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 10.h), ), ), ), ], ), ], ), ); } Widget _buildEmptyState() { return Center( child: Padding( padding: EdgeInsets.all(32.w), child: Column( children: [ Icon( Icons.check_circle_outline, size: 64.w, color: Colors.green.shade400, ), SizedBox(height: 16.h), Text( 'All Caught Up!', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w600, color: Colors.green.shade700, ), ), SizedBox(height: 8.h), Text( 'No transactions need review.\nTap "Sync Transactions" to check for new payments.', textAlign: TextAlign.center, style: TextStyle( fontSize: 14.sp, color: Colors.grey.shade600, ), ), ], ), ), ); } Color _getConfidenceColor(String confidence) { switch (confidence) { case 'very_high': return Colors.green.shade600; case 'high': return Colors.lightGreen.shade600; case 'medium': return Colors.orange.shade600; case 'low': return Colors.red.shade600; default: return Colors.grey.shade600; } } }