payout reports and payout upload

This commit is contained in:
Surendiran 2026-04-04 10:04:15 +05:30
parent 9a6685382a
commit c121c127df
9 changed files with 2087 additions and 8 deletions

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,8 @@ import '../../presentation/screens/login/login_screen.dart';
import '../../presentation/screens/payout/payout_screen.dart';
import '../../presentation/screens/payout/payout_details.dart';
import '../../presentation/screens/payout/payout_list.dart';
import '../../presentation/screens/payout/payout_upload.dart';
import '../../presentation/screens/payout/payout_report.dart';
import '../../presentation/screens/splash/splash_screen.dart';
import '../../presentation/screens/staff/Enquiry/enquiry_inline_Edit_17Nov.dart';
import '../../presentation/screens/staff/Enquiry/enquiry_inline_list.dart';
@ -102,6 +104,14 @@ final GoRouter appRouter = GoRouter(
return PayOutDetails(editItem: editItem);
},
),
GoRoute(
path: AppRoutes.payoutUpload,
builder: (context, state) => const PayoutUploadScreen(),
),
GoRoute(
path: AppRoutes.payoutReport,
builder: (context, state) => const PayoutReportScreen(),
),
GoRoute(
path: AppRoutes.profile,

View File

@ -52,6 +52,9 @@ class AppRoutes {
static const String vehicleTypeLst = '/vehicleTypeLst';
static const String payoutList = '/payoutList';
static const String payoutDetails = '/payoutDetails';
static const String payoutUpload = '/payoutUpload';
/// Accounts + agent: commission payout report (list API TBD).
static const String payoutReport = '/payoutReport';
static const String payoutGrid = '/payoutGrid';
static const String gridList = '/gridList';
static const String gridUpload = '/gridUpload';

View File

@ -2050,7 +2050,7 @@ class ApiService {
// --------------------------------- PayOut Module----------------------------------------------
Future<Map<String, dynamic>> getCommissionRateList(data) async {
print("getCommissionRateList------- $data}");
// final url = Uri.parse('${Env.apiUrl}/invoice/commission-rate-list');
final url = Uri.parse('${Env.apiUrl}/invoice/commission-rate-list');
// final url = Uri.parse('http://localhost/nhance_partner_be/invoice/commission-rate-list');
print("getCommissionRateList 1");
// final token = await getToken(); // Fetch token
@ -2350,7 +2350,6 @@ class ApiService {
}
Future<Map<String, dynamic>> uploadPolicyCommissionExcel({
required dynamic id,
required PlatformFile file,
bool proceedPartnerMismatch = false,
}) async {
@ -2370,12 +2369,11 @@ class ApiService {
request.files.add(
http.MultipartFile.fromBytes(
'file',
'commission_excel',
file.bytes!,
filename: file.name,
),
);
request.fields['id'] = id.toString();
if (proceedPartnerMismatch) {
request.fields['proceed_partner_mismatch'] = '1';
}
@ -2390,9 +2388,21 @@ class ApiService {
}
if (streamedResponse.statusCode != 200) {
try {
final decoded = jsonDecode(responseBody);
if (decoded is Map<String, dynamic>) {
return {
...decoded,
if (!decoded.containsKey('code')) 'code': streamedResponse.statusCode,
};
}
} catch (_) {}
return {
'status': 'error',
'message': 'Server Error: ${streamedResponse.statusCode}',
'status': 'failed',
'code': streamedResponse.statusCode,
'message': responseBody.isNotEmpty
? responseBody
: 'Server Error: ${streamedResponse.statusCode}',
};
}
@ -2613,6 +2623,52 @@ class ApiService {
);
}
/// Static sample Excel for commission upload (template).
Future<void> downloadCommissionUploadSampleExcel() async {
await getPdfDownload(
'policy/downloadCommissionUploadSampleExcel',
'commission_upload_sample',
);
}
/// GET `policy/commissionPayoutReport` optional query: `from_date`, `to_date` (yyyy-MM-dd), `agent_id`, `payout_raised` (`all` / `yes` / `no`).
Future<Map<String, dynamic>> fetchPayoutReportList({
String? fromDate,
String? toDate,
String? agentId,
String? payoutRaised,
}) async {
if (_token == null) {
await _initializeToken();
}
final query = <String, String>{};
if (fromDate != null && fromDate.isNotEmpty) {
query['from_date'] = fromDate;
}
if (toDate != null && toDate.isNotEmpty) {
query['to_date'] = toDate;
}
if (agentId != null && agentId.trim().isNotEmpty) {
query['agent_id'] = agentId.trim();
}
if (payoutRaised != null && payoutRaised.isNotEmpty) {
query['payout_raised'] = payoutRaised;
}
final url = Uri.parse('${Env.apiUrl}policy/commissionPayoutReport').replace(
queryParameters: query.isEmpty ? null : query,
);
final headers = {
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
Future<void> generatePolicyExcel(managerId, fromDate, toDate, searchValue,flag,insurer) async {
final url = Uri.parse(
'${Env.apiUrl}reports/policy-excel?manager_id=$managerId&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&search=${searchValue ?? ''}&show_policy_report=${flag ?? ''}&insurer_id=${insurer ?? ''}',

View File

@ -105,6 +105,12 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
if (route.contains('payoutgrid')) {
return 'PayoutGrid';
}
if (route.contains('payoutupload')) {
return 'PayoutUpload';
}
if (route.contains('payoutreport')) {
return 'PayoutReport';
}
if (route.contains('invoice') ||
(route.contains('payout') && !route.contains('payoutgrid'))) {
return 'Payout';
@ -509,6 +515,43 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
context.go(AppRoutes.payoutList);
},
),
const SizedBox(width: 8),
_buildMenuItem(
icon: Icons.upload_file_outlined,
label: "Payout Upload",
isActive: activeMenu == 'PayoutUpload',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutUpload');
context.go(AppRoutes.payoutUpload);
},
),
const SizedBox(width: 8),
_buildMenuItem(
icon: Icons.table_chart_outlined,
label: "Payout Report",
isActive: activeMenu == 'PayoutReport',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutReport');
context.go(AppRoutes.payoutReport);
},
),
],
// Partner (agent): Payout Report same route as Accounts
if (role == 'agent') ...[
const SizedBox(width: 8),
_buildMenuItem(
icon: Icons.table_chart_outlined,
label: "Payout Report",
isActive: activeMenu == 'PayoutReport',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutReport');
context.go(AppRoutes.payoutReport);
},
),
],
const Spacer(),

View File

@ -1028,7 +1028,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
}
final response = await apiService.uploadPolicyCommissionExcel(
id: selectedAgentId!.first,
// id: selectedAgentId!.first,
file: selectedFile,
proceedPartnerMismatch: partnerMismatchCount > 0,
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,720 @@
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(),
),
],
),
),
);
}
}

View File

@ -205,6 +205,39 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
context.go(AppRoutes.allStaffAttendance);
},
),
if (roleId == 'Accounts') ...[
_buildMenuItem(
context: context,
icon: const Icon(Icons.payments_outlined, size: 30, color: Colors.black),
label: "Payout",
isActive: currentPath == AppRoutes.payoutList,
onTap: () => context.go(AppRoutes.payoutList),
),
_buildMenuItem(
context: context,
icon: const Icon(Icons.upload_file_outlined, size: 30, color: Colors.black),
label: "Payout Upload",
isActive: currentPath == AppRoutes.payoutUpload,
onTap: () => context.go(AppRoutes.payoutUpload),
),
_buildMenuItem(
context: context,
icon: const Icon(Icons.table_chart_outlined, size: 30, color: Colors.black),
label: "Payout Report",
isActive: currentPath == AppRoutes.payoutReport,
onTap: () => context.go(AppRoutes.payoutReport),
),
],
if (roleId == 'agent')
_buildMenuItem(
context: context,
icon: const Icon(Icons.table_chart_outlined, size: 30, color: Colors.black),
label: "Payout Report",
isActive: currentPath == AppRoutes.payoutReport,
onTap: () => context.go(AppRoutes.payoutReport),
),
],
),
);