import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:nhance_partner/data/utils/toastNotification.dart'; import '../../../core/services/api_service.dart'; import '../../layouts/main_layout.dart'; class PayoutUploadScreen extends ConsumerStatefulWidget { const PayoutUploadScreen({super.key}); @override ConsumerState createState() => _PayoutUploadScreenState(); } class _PayoutUploadScreenState extends ConsumerState { late ApiService apiService; bool isLoading = false; String? uploadedFileName; List> failedRows = []; /// Present when upload succeeded but `data.errors` was non-empty (partial success). Map? partialSuccessData; @override void initState() { super.initState(); apiService = ApiService(); } void _resetAll() { setState(() { uploadedFileName = null; failedRows = []; partialSuccessData = null; }); } /// Parses `data.errors` (success-with-errors) or top-level error arrays. List> _extractFailedRows(Map response) { final dynamic data = response['data']; if (data is Map) { final err = data['errors']; if (err is List) { return err.map((e) { if (e is Map) return Map.from(e); if (e is Map) return Map.from(e); return {'message': e.toString()}; }).toList(); } } final top = response['errors'] ?? response['failed_rows']; if (top is List) { return top.map((e) { if (e is Map) return Map.from(e); if (e is Map) return Map.from(e); return {'message': e.toString()}; }).toList(); } return []; } String _failureMessage(Map response) { final m = response['message']?.toString().trim(); if (m != null && m.isNotEmpty) return m; final data = response['data']; if (data is String && data.trim().isNotEmpty) return data.trim(); return 'Upload failed'; } String _successSummary(Map data) { final updated = data['updated_rows']; final skipped = data['skipped_empty_payout']; final parts = []; if (updated != null) parts.add('Updated $updated row(s)'); if (skipped != null) parts.add('Skipped empty payout: $skipped'); return parts.isEmpty ? 'Upload successful' : parts.join(' · '); } String _partialSummary(Map data) { final base = _successSummary(data); final ec = data['error_count']; if (ec != null) return '$base · $ec row(s) with errors'; return base; } static const _errorIcon = Icon(Icons.error_outline, color: Colors.red, size: 16); Widget _messageWithLeadingIcon(String text) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 2), child: _errorIcon, ), const SizedBox(width: 8), Expanded( child: Text( text, style: GoogleFonts.inter(fontSize: 12, height: 1.35), ), ), ], ); } void _handleUploadResponse(Map response) { final status = response['status']?.toString(); final code = response['code']; if (status == 'error' && (response['message']?.toString().toLowerCase().contains('session') ?? false)) { ToastHelper.showErrorToast(context, response['message']?.toString() ?? 'Session expired'); return; } final isHttpSuccess = status == 'success' || status == '200' || code == 200 || code == '200'; if (isHttpSuccess) { final data = response['data']; if (data is Map) { final map = Map.from(data); final errors = map['errors']; if (errors is List && errors.isNotEmpty) { setState(() { partialSuccessData = map; failedRows = errors.map((e) { if (e is Map) return Map.from(e); if (e is Map) return Map.from(e); return {'message': e.toString()}; }).toList(); }); ToastHelper.showWarningToast(context, _partialSummary(map)); return; } ToastHelper.showSuccessToast(context, _successSummary(map)); _resetAll(); return; } ToastHelper.showSuccessToast( context, response['message']?.toString() ?? 'Upload successful', ); _resetAll(); return; } if (status == 'failed' || status == 'error' || !isHttpSuccess) { final rows = _extractFailedRows(response); ToastHelper.showErrorToast(context, _failureMessage(response)); if (rows.isNotEmpty) { setState(() { failedRows = rows; partialSuccessData = null; }); } return; } ToastHelper.showErrorToast(context, 'Unexpected response'); } Future _uploadFile() async { final picked = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['xlsx', 'xls'], withData: true, ); if (picked == null || picked.files.isEmpty) return; final file = picked.files.single; if (file.bytes == null || file.bytes!.isEmpty) { ToastHelper.showWarningToast(context, 'Unable to read selected file'); return; } setState(() { isLoading = true; uploadedFileName = file.name; }); try { final response = await apiService.uploadPolicyCommissionExcel( file: file, ); _handleUploadResponse(response); } catch (e) { ToastHelper.showErrorToast(context, e.toString()); } finally { if (mounted) setState(() => isLoading = false); } } bool get _isStandardErrorShape { if (failedRows.isEmpty) return false; for (final r in failedRows) { if (!r.containsKey('row') && !r.containsKey('policy_number')) return false; } return true; } List _visibleColumns() { if (failedRows.isEmpty) return []; const preferred = ['row', 'policy_number', 'agent_code', 'expected_agent_code', 'message']; const hidden = {}; final keySet = {}; for (final row in failedRows) { keySet.addAll(row.keys); } final ordered = []; for (final p in preferred) { if (keySet.contains(p) && !hidden.contains(p)) ordered.add(p); } for (final k in keySet) { if (!ordered.contains(k) && !hidden.contains(k)) ordered.add(k); } return ordered; } Widget _buildPartialSummaryBanner() { final d = partialSuccessData; if (d == null) return const SizedBox.shrink(); final updated = d['updated_rows']; final skipped = d['skipped_empty_payout']; final errCount = d['error_count']; return Padding( padding: const EdgeInsets.only(bottom: 10), child: Wrap( spacing: 8, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (updated != null) _summaryChip('Updated rows', updated.toString(), const Color(0xFF059669)), if (skipped != null) _summaryChip('Skipped empty payout', skipped.toString(), const Color(0xFF64748B)), if (errCount != null) _summaryChip('Errors', errCount.toString(), const Color(0xFFDC2626)), ], ), ); } Widget _summaryChip(String label, String value, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.35)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '$label: ', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF475569)), ), Text( value, style: GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.w700, color: color), ), ], ), ); } Widget _buildErrorsTable() { if (_isStandardErrorShape) { return Column( children: [ _buildTableHeaderRow(isStandard: true), Expanded( child: ListView.builder( itemCount: failedRows.length, itemBuilder: (context, index) { final row = failedRows[index]; return Container( decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: Color(0xFFE5E7EB)), ), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 52, child: Text( '${row['row'] ?? '-'}', style: GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.w600), ), ), SizedBox( width: 160, child: Text( (row['policy_number'] ?? '-').toString(), style: GoogleFonts.inter(fontSize: 12), ), ), SizedBox( width: 100, child: Text( (row['agent_code'] ?? '-').toString().isEmpty ? '—' : (row['agent_code'] ?? '').toString(), style: GoogleFonts.inter(fontSize: 12), ), ), SizedBox( width: 120, child: Text( row['expected_agent_code'] != null && row['expected_agent_code'].toString().trim().isNotEmpty ? row['expected_agent_code'].toString() : '—', style: GoogleFonts.inter(fontSize: 12, color: const Color(0xFF64748B)), ), ), Expanded( child: _messageWithLeadingIcon((row['message'] ?? '-').toString()), ), ], ), ); }, ), ), ], ); } final columns = _visibleColumns(); return Column( children: [ _buildTableHeaderRow(isStandard: false, columns: columns), Expanded( child: ListView.builder( itemCount: failedRows.length, itemBuilder: (context, index) { final row = failedRows[index]; return Container( decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: Color(0xFFE5E7EB)), ), ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final c in columns) if (c == 'message') Expanded( child: _messageWithLeadingIcon((row[c] ?? '-').toString()), ) else SizedBox( width: 140, child: Text( (row[c] ?? '-').toString(), maxLines: 3, overflow: TextOverflow.ellipsis, style: GoogleFonts.inter(fontSize: 11), ), ), ], ), ); }, ), ), ], ); } Widget _buildTableHeaderRow({required bool isStandard, List? columns}) { TextStyle h() => GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w700, color: const Color(0xFF334155), letterSpacing: 0.3, ); if (isStandard) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: const BoxDecoration( color: Color(0xFFF1F5F9), border: Border(bottom: BorderSide(color: Color(0xFFE2E8F0))), ), child: Row( children: [ SizedBox(width: 52, child: Text('ROW', style: h())), SizedBox(width: 160, child: Text('POLICY NUMBER', style: h())), SizedBox(width: 100, child: Text('AGENT CODE', style: h())), SizedBox(width: 120, child: Text('EXPECTED', style: h())), Expanded(child: Text('MESSAGE', style: h())), ], ), ); } final cols = columns ?? []; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: const BoxDecoration( color: Color(0xFFF1F5F9), border: Border(bottom: BorderSide(color: Color(0xFFE2E8F0))), ), child: Row( children: [ for (final c in cols) if (c == 'message') Expanded( child: Text( c.toUpperCase().replaceAll('_', ' '), style: h(), ), ) else SizedBox( width: 140, child: Text( c.toUpperCase().replaceAll('_', ' '), style: h(), ), ), ], ), ); } Future _onTemplateFileTap() async { try { await apiService.downloadCommissionUploadSampleExcel(); } catch (e) { if (mounted) { ToastHelper.showErrorToast( context, 'Could not download template. Try again.', ); } } } Widget _buildTemplateDownloadHint() { return Padding( padding: const EdgeInsets.only(top: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( 'Please download the sample file to review the format.', textAlign: TextAlign.center, style: GoogleFonts.inter( fontSize: 12, height: 1.4, color: const Color(0xFF6B7280), ), ), const SizedBox(height: 8), MouseRegion( cursor: SystemMouseCursors.click, child: InkWell( onTap: () => _onTemplateFileTap(), borderRadius: BorderRadius.circular(4), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( 'Template File', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: const Color(0xFF2E7D6E), decoration: TextDecoration.underline, decorationColor: const Color(0xFF2E7D6E), ), ), ), ), ), ], ), ); } Widget _buildUploadCard() { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ InkWell( onTap: isLoading ? null : _uploadFile, borderRadius: BorderRadius.circular(10), child: Container( width: double.infinity, constraints: const BoxConstraints(maxWidth: 1050), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 26), decoration: BoxDecoration( color: const Color(0xFFF8FAFA), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF3AA69A), width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 30, height: 30, decoration: BoxDecoration( color: const Color(0xFFE6F6F4), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF7BCBC3)), ), child: isLoading ? const Padding( padding: EdgeInsets.all(6), child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Color(0xFF2E7D6E)), ), ) : const Icon( Icons.upload_file_rounded, size: 16, color: Color(0xFF2E7D6E), ), ), const SizedBox(height: 10), Text( isLoading ? 'Uploading file...' : 'Upload Your Payout Documents', style: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF111827), ), ), const SizedBox(height: 4), Text( '(Supported Format: XLSX, XLS)', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w400, color: const Color(0xFF6B7280), ), ), const SizedBox(height: 12), Text( 'Click here to choose an Excel file and upload.', style: GoogleFonts.inter( fontSize: 12, color: const Color(0xFF4B5563), ), ), ], ), ), ), const SizedBox(height: 16), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1050), child: SizedBox( width: double.infinity, child: _buildTemplateDownloadHint(), ), ), ], ); } Widget _buildFailureGrid() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( uploadedFileName == null ? 'Validation errors' : 'File: $uploadedFileName', style: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w600), ), ), OutlinedButton.icon( onPressed: isLoading ? null : _resetAll, icon: const Icon(Icons.refresh, size: 16), label: const Text('Re-upload'), ), ], ), _buildPartialSummaryBanner(), const SizedBox(height: 4), Text( 'Error rows (${failedRows.length})', style: GoogleFonts.inter(fontSize: 12, color: const Color(0xFF64748B)), ), const SizedBox(height: 8), Expanded( child: Container( width: double.infinity, decoration: BoxDecoration( border: Border.all(color: const Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8), ), clipBehavior: Clip.antiAlias, child: LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < 720 && _isStandardErrorShape) { return ListView.builder( padding: const EdgeInsets.all(12), itemCount: failedRows.length, itemBuilder: (context, index) { final row = failedRows[index]; return Container( decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(8), border: const Border( bottom: BorderSide(color: Color(0xFFE5E7EB)), ), ), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Row ${row['row'] ?? '-'}', style: GoogleFonts.inter( fontWeight: FontWeight.w700, fontSize: 12, ), ), const SizedBox(height: 6), _mobileLine('Policy', row['policy_number']), _mobileLine('Agent code', row['agent_code']), if (row['expected_agent_code'] != null && row['expected_agent_code'].toString().trim().isNotEmpty) _mobileLine('Expected', row['expected_agent_code']), const SizedBox(height: 6), _messageWithLeadingIcon((row['message'] ?? '').toString()), ], ), ), ); }, ); } return _buildErrorsTable(); }, ), ), ), ], ); } Widget _mobileLine(String label, dynamic value) { final v = value?.toString().trim() ?? ''; return Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 88, child: Text( '$label:', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF64748B)), ), ), Expanded( child: Text( v.isEmpty ? '—' : v, style: GoogleFonts.inter(fontSize: 12), ), ), ], ), ); } @override Widget build(BuildContext context) { return MainLayout( title: 'Payout Upload', body: Container( width: double.infinity, color: Colors.white, padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Payout Upload', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: const Color(0xFF111827), ), ), const SizedBox(height: 10), Expanded( child: failedRows.isEmpty ? Center(child: _buildUploadCard()) : _buildFailureGrid(), ), ], ), ), ); } }