import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:cashumit/models/submission_task.dart'; import 'package:cashumit/models/group_submission_result.dart'; import 'package:cashumit/services/receipt_service.dart'; import 'package:cashumit/utils/currency_format.dart'; /// Popup card yang menampilkan daftar transaksi yang sedang dikirim ke /// Firefly III (satu baris per task: grup deposit atau tip). /// /// Menerima [tasks] (sudah berisi konteks lengkap per-grup: group, destination, /// date, tip) plus [baseUrl] & [accessToken]. /// Pengiriman dilakukan satu per satu (sequential). Jika ada yang gagal, /// user dapat me-retry per-item atau "Retry Semua yang Gagal". /// Tombol "Selesai" menutup dialog dan mengembalikan list hasil. class TransactionSubmissionDialog extends StatefulWidget { final List tasks; final String baseUrl; final String accessToken; const TransactionSubmissionDialog({ super.key, required this.tasks, required this.baseUrl, required this.accessToken, }); @override State createState() => _TransactionSubmissionDialogState(); } class _TransactionSubmissionDialogState extends State { late List _results; bool _isSubmitting = false; @override void initState() { super.initState(); _results = _buildInitialResults(); // Mulai kirim semua otomatis WidgetsBinding.instance.addPostFrameCallback((_) => _submitAll()); } List _buildInitialResults() { return widget.tasks.map((t) { return GroupSubmissionResult( groupId: t.taskId, label: t.label, amount: t.amount, isTip: t.isTip, status: SubmissionStatus.pending, ); }).toList(); } /// Kirim satu task, return hasilnya. Future _submitTask(SubmissionTask task) async { if (task.isTip) { return ReceiptService.submitTip( tipSourceAccountId: task.tipSourceAccountId!, tipSourceAccountName: task.tipSourceAccountName ?? '', tipAmount: task.tipAmount!, destinationAccountId: task.destinationAccountId, transactionDate: task.transactionDate, baseUrl: widget.baseUrl, accessToken: widget.accessToken, tags: task.tags, ); } return ReceiptService.submitSingleGroup( group: task.group!, destinationAccountId: task.destinationAccountId, transactionDate: task.transactionDate, baseUrl: widget.baseUrl, accessToken: widget.accessToken, tags: task.tags, ); } /// Kirim semua entry yang masih pending/gagal, sequential. Future _submitAll() async { if (_isSubmitting) return; setState(() => _isSubmitting = true); for (int i = 0; i < _results.length; i++) { if (_results[i].status != SubmissionStatus.pending && _results[i].status != SubmissionStatus.failed) { continue; } // Set sending setState(() { _results[i] = _results[i].copyWith( status: SubmissionStatus.sending, error: null, ); }); final task = widget.tasks[i]; final res = await _submitTask(task); if (!mounted) return; setState(() { _results[i] = GroupSubmissionResult( groupId: _results[i].groupId, label: _results[i].label, amount: _results[i].amount, isTip: _results[i].isTip, status: res.status, transactionId: res.transactionId, error: res.error, ); }); } setState(() => _isSubmitting = false); } /// Retry satu entry. Future _retryOne(int index) async { if (_isSubmitting) return; setState(() => _isSubmitting = true); setState(() { _results[index] = _results[index].copyWith( status: SubmissionStatus.sending, error: null, ); }); final task = widget.tasks[index]; final res = await _submitTask(task); if (!mounted) return; setState(() { _results[index] = GroupSubmissionResult( groupId: _results[index].groupId, label: _results[index].label, amount: _results[index].amount, isTip: _results[index].isTip, status: res.status, transactionId: res.transactionId, error: res.error, ); _isSubmitting = false; }); } /// Retry semua yang gagal. Future _retryAllFailed() async { setState(() { for (int i = 0; i < _results.length; i++) { if (_results[i].isFailed) { _results[i] = _results[i].copyWith(status: SubmissionStatus.pending); } } }); await _submitAll(); } int get _failedCount => _results.where((r) => r.isFailed).length; int get _successCount => _results.where((r) => r.isSuccess).length; bool get _allDone => _results.every((r) => r.isSuccess || r.isFailed); @override Widget build(BuildContext context) { return Dialog( insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420, maxHeight: 560), child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header Row( children: [ Icon(Icons.cloud_upload, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), Expanded( child: Text( _isSubmitting ? 'Mengirim transaksi...' : _allDone ? 'Pengiriman selesai ($_successCount/${_successCount + _failedCount})' : 'Pengiriman transaksi', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), if (_isSubmitting) const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), const Divider(height: 16), // Daftar transaksi Flexible( child: ListView.separated( shrinkWrap: true, itemCount: _results.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final r = _results[index]; return _SubmissionTile( result: r, baseUrl: widget.baseUrl, onRetry: r.isFailed && !_isSubmitting ? () => _retryOne(index) : null, ); }, ), ), const SizedBox(height: 12), // Aksi bawah Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (_allDone && _failedCount > 0 && !_isSubmitting) TextButton.icon( onPressed: _retryAllFailed, icon: const Icon(Icons.refresh, size: 18), label: const Text('Retry Semua Gagal'), ), const SizedBox(width: 8), ElevatedButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(_results), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), child: const Text('Selesai'), ), ], ), ], ), ), ), ); } } /// Satu baris transaksi dalam popup. class _SubmissionTile extends StatelessWidget { final GroupSubmissionResult result; final String baseUrl; final VoidCallback? onRetry; const _SubmissionTile({ required this.result, required this.baseUrl, this.onRetry, }); String get _url => '$baseUrl/transactions/show/${result.transactionId}'; Future _copyLink(BuildContext context) async { await Clipboard.setData(ClipboardData(text: _url)); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Link disalin'), duration: Duration(seconds: 1), ), ); } Future _openLink(BuildContext context) async { try { final uri = Uri.parse(_url); final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); if (!launched) { throw 'Tidak dapat membuka URL'; } } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal membuka link: $e')), ); } } @override Widget build(BuildContext context) { return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), leading: _buildLeading(), title: Text( result.label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( CurrencyFormat.formatRupiah(result.amount), style: TextStyle( fontSize: 12, color: Colors.grey[700], ), ), if (result.isFailed && result.error != null) Text( result.error!.replaceFirst('Exception: ', ''), style: const TextStyle(fontSize: 11, color: Colors.red), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (result.isSuccess && result.transactionId != null) Text( _url, style: TextStyle( fontSize: 10, color: Colors.grey[500], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), trailing: _buildTrailing(context), ); } Widget _buildLeading() { if (result.isSending) { return const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ); } if (result.isSuccess) { return const Icon(Icons.check_circle, color: Colors.green, size: 22); } if (result.isFailed) { return const Icon(Icons.cancel, color: Colors.red, size: 22); } return Icon(Icons.hourglass_empty, color: Colors.grey[400], size: 22); } Widget? _buildTrailing(BuildContext context) { // Baris sukses dengan ID: tombol copy + open link if (result.isSuccess && result.transactionId != null) { return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.copy, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(), tooltip: 'Salin link', onPressed: () => _copyLink(context), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.open_in_new, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(), tooltip: 'Buka di browser', color: Theme.of(context).colorScheme.primary, onPressed: () => _openLink(context), ), ], ); } // Baris gagal: tombol retry if (result.isFailed && onRetry != null) { return TextButton( onPressed: onRetry, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8), minimumSize: const Size(0, 30), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text('Retry', style: TextStyle(fontSize: 12)), ); } return null; } }