721 lines
24 KiB
Dart
721 lines
24 KiB
Dart
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<PayoutUploadScreen> createState() => _PayoutUploadScreenState();
|
|
}
|
|
|
|
class _PayoutUploadScreenState extends ConsumerState<PayoutUploadScreen> {
|
|
late ApiService apiService;
|
|
bool isLoading = false;
|
|
String? uploadedFileName;
|
|
List<Map<String, dynamic>> failedRows = [];
|
|
/// Present when upload succeeded but `data.errors` was non-empty (partial success).
|
|
Map<String, dynamic>? 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<Map<String, dynamic>> _extractFailedRows(Map<String, dynamic> 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<String, dynamic>) return Map<String, dynamic>.from(e);
|
|
if (e is Map) return Map<String, dynamic>.from(e);
|
|
return <String, dynamic>{'message': e.toString()};
|
|
}).toList();
|
|
}
|
|
}
|
|
final top = response['errors'] ?? response['failed_rows'];
|
|
if (top is List) {
|
|
return top.map((e) {
|
|
if (e is Map<String, dynamic>) return Map<String, dynamic>.from(e);
|
|
if (e is Map) return Map<String, dynamic>.from(e);
|
|
return <String, dynamic>{'message': e.toString()};
|
|
}).toList();
|
|
}
|
|
return [];
|
|
}
|
|
|
|
String _failureMessage(Map<String, dynamic> 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<String, dynamic> data) {
|
|
final updated = data['updated_rows'];
|
|
final skipped = data['skipped_empty_payout'];
|
|
final parts = <String>[];
|
|
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<String, dynamic> 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<String, dynamic> 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<String, dynamic>.from(data);
|
|
final errors = map['errors'];
|
|
if (errors is List && errors.isNotEmpty) {
|
|
setState(() {
|
|
partialSuccessData = map;
|
|
failedRows = errors.map((e) {
|
|
if (e is Map<String, dynamic>) return Map<String, dynamic>.from(e);
|
|
if (e is Map) return Map<String, dynamic>.from(e);
|
|
return <String, dynamic>{'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<void> _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<String> _visibleColumns() {
|
|
if (failedRows.isEmpty) return [];
|
|
const preferred = ['row', 'policy_number', 'agent_code', 'expected_agent_code', 'message'];
|
|
const hidden = <String>{};
|
|
final keySet = <String>{};
|
|
for (final row in failedRows) {
|
|
keySet.addAll(row.keys);
|
|
}
|
|
final ordered = <String>[];
|
|
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<String>? 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<void> _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>(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(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|