nhance_partner/lib/presentation/screens/Grid/gridUpload.dart
2026-04-08 15:55:08 +05:30

885 lines
31 KiB
Dart

// import 'dart:io' as html;
// import 'dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart';
import 'dart:convert';
import 'dart:typed_data'; // Import for Uint8List
import 'package:dropdown_search/dropdown_search.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:http/http.dart' as http;
import 'package:nhance_partner/data/utils/toastNotification.dart';
import 'package:nhance_partner/presentation/themes/indicators/text_field_theme.dart';
import 'package:toastification/toastification.dart';
import 'package:universal_html/html.dart' as html;
import 'package:nhance_partner/core/config/env.dart';
import 'package:nhance_partner/core/services/api_service.dart';
import 'package:nhance_partner/data/services/auth_service.dart';
import 'package:nhance_partner/data/utils/validators.dart';
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
import 'package:nhance_partner/presentation/providers/manager_provider.dart';
import 'package:nhance_partner/presentation/themes/indicators/customizd_file_upload.dart';
import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart';
import 'package:nhance_partner/presentation/themes/indicators/month_field_theme.dart';
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
// import '../../../themes/indicators/upload_doc_theme.dart';
/// function to open the modal
void showUploadGridModal(BuildContext context) {
showDialog(
context: context,
builder: (context) => const UploadGridModal(),
);
}
class UploadGridModal extends ConsumerStatefulWidget {
const UploadGridModal({super.key});
@override
ConsumerState<UploadGridModal> createState() =>
UploadGridModalState();
}
class UploadGridModalState extends ConsumerState<UploadGridModal> {
late ApiService apiService;
bool isLoading = false;
final _formKey = GlobalKey<FormState>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKey =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
List<Map<String, dynamic>> getAgentData = [];
List<Map<String, dynamic>> filteredData = [];
List<Map<String, dynamic>> getAgentGridFileData = [];
List<Map<String, dynamic>> filteredGridData = [];
Map<String, TextEditingController> controllers = {};
// html.File? passportFile;
List<PlatformFile> selectedFiles = [];
String? selectedFileNames;
List<String> tabHeader = ['name', 'agentId', 'date', 'search'];
String? _token;
dynamic userId;
dynamic managerId;
dynamic agentId;
DateTime? _validFrom;
String _formatDateForApi(DateTime date) =>
date.toIso8601String().split('T').first;
String _formatDateForUi(DateTime date) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
Future<void> _pickValidFrom() async {
final today = DateTime.now();
final minDate = DateTime(today.year, today.month, today.day);
final safeInitial = (_validFrom != null && _validFrom!.isAfter(minDate))
? _validFrom!
: minDate;
final picked = await showDatePicker(
context: context,
firstDate: minDate,
lastDate: DateTime(2100),
initialDate: safeInitial,
);
if (picked == null) return;
setState(() => _validFrom = picked);
// Backend expects `incentive_month` but payout-grid upload uses a full date.
controllers['date']?.text = _formatDateForApi(picked);
}
Map<String, dynamic> dataDetails() {
final selectedDate = controllers["date"]?.text;
final data = {
"agent_id": agentId,
"incentive_month": selectedDate,
"created_by": userId,
"file_type": "grid",
};
return data;
}
@override
void initState() {
super.initState();
apiService = ApiService();
for (String field in tabHeader) {
controllers[field] = TextEditingController();
}
Future.microtask(_loadInitialModalData);
_initializeToken();
// getAgentList();
}
Future<void> _loadInitialModalData() async {
managerId = ref.read(managerIdProvider);
userId = ref.read(userIdProvider);
agentId = null;
if (managerId != null) {
await getAgentList(managerId);
}
// Always fetch history list when modal opens.
// if (agentId != null) {
// await getAgentIncenctiveFileList(agentId);
// }
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
print("APISERTOKEN - $_token");
}
@override
void dispose() {
for (var controller in controllers.values) {
controller.dispose();
}
controllers['date']?.dispose();
controllers['code']?.dispose();
super.dispose();
}
Future<void> refresh() async {
setState(() {
// reset file fields
selectedFiles = [];
// selectedFileNames = "";
selectedFileNames = null;
// getAgentData = [];
// filteredData = [];
getAgentGridFileData = [];
filteredGridData = [];
// clear all text controllers
for (var controller in controllers.values) {
controller.clear();
}
_validFrom = null;
dropDownKey.currentState?.clear(); // 👈 clear selected agent
controllers['agentId']?.clear(); // clear text field value too
controllers['uploadFile']?.clear();
});
// Re-fetch history after clearing UI fields.
// if (agentId != null) {
// await getAgentIncenctiveFileList(agentId);
// }
}
void filterData(String query) {
print("FilterDAta - $query");
setState(() {
filteredGridData = getAgentGridFileData.where((item) {
return (item['incentive_month'] ?? '-').toLowerCase().contains(
query.toLowerCase(),
);
}).toList();
});
}
Future<void> getAgentList(int id) async {
print('getClaimList called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchAgentUserList(managerId);
if (response['status'] == 'success') {
print('getAgentListData - ${response['data']}');
setState(() {
getAgentData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getAgentData');
filteredData = List.from(getAgentData);
// print('originalData - $filteredData');
});
} else {
getAgentData = [];
filteredData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getAgentIncenctiveFileList(agentId) async {
print('getClaimList agentId - $agentId');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchAgentIncentiveList(agentId, type: 'grid');
if (response['status'] == 'success') {
print('AgentIncentive - ${response['data']}');
setState(() {
getAgentGridFileData = List<Map<String, dynamic>>.from(
response['data'],
);
print('API AgentIncentive - $getAgentGridFileData');
filteredGridData = List.from(getAgentGridFileData);
// print('originalData - $filteredGridData');
});
} else {
getAgentGridFileData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> handleSave() async {
final dateText = controllers['date']?.text.trim() ?? '';
if (dateText.isEmpty) {
ToastHelper.showErrorToast(context, 'Select valid from date');
return;
}
if (!_formKey.currentState!.validate()) return;
print("Handl1");
final dataSet = dataDetails();
print("dataSetAgent - $dataSet");
print("Handl13f");
if (selectedFiles.isEmpty) {
print("❌ No file selected!");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a file before submitting"),
),
);
return;
}
final invalidFiles = selectedFiles.where((file) {
final name = file.name.toLowerCase();
return !(name.endsWith('.xls') || name.endsWith('.xlsx'));
}).toList();
if (invalidFiles.isNotEmpty) {
ToastHelper.showErrorToast(
context,
'Upload only Excel files (.xls, .xlsx)',
);
return;
}
setState(() {
uploadIncentiveData(dataSet);
});
print("Handl2");
}
Future<void> uploadIncentiveData(Map<String, dynamic> dataSet) async {
final uri = Uri.parse('${Env.apiUrl}grid/upload');
if (_token == null) {
throw Exception('Token not found. Please log in.');
}
// Use MultipartRequest (POST only)
final request = http.MultipartRequest('POST', uri);
request.headers['Authorization'] = 'Bearer $_token';
request.headers['app-signature'] = Env.App_Signature;
print("USerDAta - $dataSet");
// dataSet.forEach((key, value) {
// request.fields[key] = value.toString();
// print("✅ Encoded travel_details2: ${request.fields[key]}");
// });
dataSet.forEach((key, value) {
if (key != 'incentive_file_name') {
request.fields[key] = value.toString();
print("✅ Encoded $key: ${request.fields[key]}");
}
});
if (selectedFiles.isNotEmpty) {
for (final file in selectedFiles) {
try {
if (file.bytes != null) {
final multipartFile = http.MultipartFile.fromBytes(
'incentive_file_name',
file.bytes!,
filename: file.name,
);
request.files.add(multipartFile);
} else if (file.path != null) {
final multipartFile = await http.MultipartFile.fromPath(
'incentive_file_name',
file.path!,
filename: file.name,
);
request.files.add(multipartFile);
}
print("📎 File attached: ${file.name}");
} catch (e) {
print("❌ Failed to attach file ${file.name}: $e");
}
}
}
// Attach file if selected
// if (passportFile != null) {
// try {
// final reader = html.FileReader();
// reader.readAsArrayBuffer(passportFile! as html.Blob);
// await reader.onLoad.first;
//
// final data = reader.result as Uint8List;
//
// final multipartFile = http.MultipartFile.fromBytes(
// 'incentive_file_name',
// data,
// filename: passportFile!.name,
// );
//
// request.files.add(multipartFile);
// print("📎 File attached: ${passportFile!.name}");
// } catch (e) {
// print("❌ Failed to read file: $e");
// }
// } else {
// print("⚠️ No passport file selected.");
// }
print("🚀 Sending request with fields: ${request.fields}");
try {
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
print("Response status: ${response.statusCode}");
print("Response body: ${response.body}");
final responseBody = json.decode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
// dispose();
print("✅ Partner submitted successfully!");
print("📨 Response: ${response.body}");
final data = dataDetails();
final agentId = null;
final status = responseBody['status'];
final message = responseBody['data'] ?? '';
if (status == 'success') {
ToastHelper.showSuccessToast(context, 'File uploaded successfully!');
// await getAgentIncenctiveFileList(agentId);
// Close the modal and signal the parent to refresh.
Navigator.pop(context, true);
} else {
ToastHelper.showErrorToast(
context,
message.isNotEmpty ? message : 'Something went wrong!',
);
}
// context.go(AppRoutes.agentLst);
} else if (response.statusCode == 403) {
await apiService.clearLocalStorageAndRedirect();
} else {
print("❌ Submission failed. Status: ${response.statusCode}");
print("📨 Body: ${response.body}");
String errorMessage = 'There was a problem in creating user. Please try again.';
try {
if (responseBody['data'] is String &&
(responseBody['data'] as String).isNotEmpty) {
final parsedData = json.decode(responseBody['data']);
if (parsedData is Map<String, dynamic> &&
parsedData['message'] != null) {
errorMessage = parsedData['message'].toString();
}
} else if (responseBody['message'] != null) {
errorMessage = responseBody['message'].toString();
}
} catch (_) {
// keep fallback message
}
ToastHelper.showErrorToast(context, errorMessage);
}
} catch (e) {
print("🔥 Error submitting user: $e");
}
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final isCompact = screenWidth < 1000;
final dialogWidth = isCompact ? screenWidth * 0.95 : screenWidth * 0.55;
final fieldWidth = isCompact ? dialogWidth * 0.9 : screenWidth * 0.27;
return SelectionArea(
child:AlertDialog(
backgroundColor: Colors.white,
content: Container(
width: dialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"Payout Files",
style: GoogleFonts.poppins(
fontSize: 14,
color: Color(0xFF50A398),
fontWeight: FontWeight.w500,
),
),
Spacer(),
Tooltip(
message: 'Refresh',
child: IconButton(
icon: const Icon(
Icons.refresh,
size: 18,
color: Color(0xFF2E7D6E),
// color: Color(0xFF425B5B),
),
onPressed: () {
refresh();
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
Tooltip(
message: 'Close',
child: IconButton(
icon: const Icon(
Icons.close,
size: 18,
// color: Color(0xFF425B5B),
color: Color(0xFF2E7D6E),
),
onPressed: () {
Navigator.pop(context);
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Form(
key: _formKey,
child: Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Valid From',
style: _labelStyle,
),
SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width * 0.27,
height: 40,
child: InkWell(
onTap: _pickValidFrom,
borderRadius: BorderRadius.circular(6),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFE2E8F0)),
color: Colors.white,
),
child: Row(
children: [
Expanded(
child: Text(
_validFrom == null
? 'Select Valid From'
: _formatDateForUi(_validFrom!),
style: GoogleFonts.inter(
fontSize: 12,
color: _validFrom == null
? const Color(0xFFCBD5E1)
: Colors.black,
),
),
),
const Icon(
Icons.calendar_today_outlined,
size: 16,
color: Color(0xFF94A3B8),
),
],
),
),
),
),
],
),
SizedBox(width: 15),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Upload Files', style: _labelStyle),
],
),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedUploadField(
key: ValueKey(selectedFileNames),
allowMultiple: true,
// hintText: "Upload Document",
txtheight: 40,
// validator: (value) => Validators.requiredField(
// value,
// "Upload Document",
// ),
// validator: (value) {
// if (passportFile == null) {
// return "Please upload a file";
// }
// return null;
// },
// backgroundColor: Color(0xFFECECEC),
backgroundColor: Colors.white,
borderColor: Color(0xFFE2E8F0),
allowedExtensions: const ['xls', 'xlsx'],
hintText:
(selectedFileNames == null ||
selectedFileNames!.isEmpty)
? "Upload Document"
: selectedFileNames,
txtwidth: isCompact
? fieldWidth * 0.71
: MediaQuery.of(context).size.width * 0.19,
onFilesSelected: (fileNames, files) {
print("Files picked: ${fileNames.join(', ')}");
// print("Size: ${file.size}");
// print(
// "Path: ${file.path}",
// ); // works on mobile/desktop
// print("Bytes: ${file.bytes}");
setState(() {
selectedFiles = files;
selectedFileNames = fileNames.join(', ');
});
},
),
SizedBox(width: 15),
Container(
width: isCompact
? fieldWidth * 0.25
: MediaQuery.of(context).size.width * 0.068,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFF425B5B),
backgroundColor: Color(0xFF2E7D6E),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
6,
), // 👈 reduce radius (default ~20)
),
),
onPressed: () {
handleSave();
// Navigator.pop(context);
},
child: Text(
"Submit",
style: GoogleFonts.poppins(fontSize: 12),
),
),
),
],
),
],
),
],
),
),
],
),
const SizedBox(height: 15),
// fILE LIST section
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text('Files', style: _labelHeaderStyle),
// ThemedSearchField(
// hintText: 'Search',
// backgroundColor: Colors.white,
// // backgroundColor: Color(0xFFECECEC),
// onChanged: filterData,
// // onChanged: null,
// controller: controllers['search']!,
// txtwidth: MediaQuery.of(context).size.width * 0.15,
// txtHeight: 30,
// ),
// ],
// ),
// const SizedBox(height: 10),
//
// Expanded(
// child: isLoading
// ? const Center(child: CircularProgressIndicator())
// : filteredGridData.isEmpty
// ? const Center(child: Text("No files found"))
// : LayoutBuilder(
// builder: (context, constraints) {
// final fileAreaWidth = constraints.maxWidth;
// final crossAxisCount =
// fileAreaWidth < 520 ? 1 : 2;
// const spacing = 10.0;
// final tileWidth = (fileAreaWidth -
// spacing * (crossAxisCount - 1) -
// 16) /
// crossAxisCount;
// const targetTileHeight = 70.0;
// final aspectRatio = (tileWidth / targetTileHeight)
// .clamp(3.2, 12.0);
//
// return GridView.builder(
// padding: const EdgeInsets.all(8),
// gridDelegate:
// SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: crossAxisCount,
// crossAxisSpacing: spacing,
// mainAxisSpacing: spacing,
// childAspectRatio: aspectRatio,
// ),
// itemCount: filteredGridData.length,
// itemBuilder: (context, index) {
// final file = filteredGridData[index];
// return IncentiveFileRow(
// fileName:
// file['incentive_file_name'] ?? "Unknown",
// date: file['incentive_month'] ?? "-",
// onUpload: () {
// print("Upload ${file['id']}");
// final selectedId = file['id'];
// final path =
// 'agent/downloadAgentIncentiveFile?type=grid&id=$selectedId';
// apiService.getPdfDownload(path, selectedId);
// },
// onDelete: () async {
// print("Delete ${file['id']}");
// final selectedId = file['id'];
// final response = await apiService
// .deleteAgentIncentiveFile(selectedId);
// print("res - $response");
//
// if (response['status'] == 'success') {
// print("DELTED");
//
// final data =
// dataDetails(); // Map<String, dynamic>
// final agentId = null;
//
// // getAgentIncenctiveFileList(agentId);
// } else {
// // Fluttertoast.showToast(
// // msg: "Something went wrong",
// // toastLength: Toast.LENGTH_SHORT,
// // gravity: ToastGravity.BOTTOM,
// // );
// }
// },
// );
// },
// );
// },
// ),
// ),
],
),
),
),);
}
}
final _labelStyle = GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 12,
);
final _labelHeaderStyle = GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
);
class IncentiveFileRow extends StatelessWidget {
final String fileName;
final String date;
final VoidCallback? onUpload;
final VoidCallback? onDelete;
const IncentiveFileRow({
super.key,
required this.fileName,
required this.date,
this.onUpload,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE3E3E3)),
borderRadius: BorderRadius.circular(6),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(6.0),
decoration: BoxDecoration(
color: const Color(0xFFF4F6F8),
borderRadius: BorderRadius.circular(6.0),
border: Border.all(color: const Color(0xFFE3E3E3)),
),
child: const Icon(
Icons.file_present,
color: Color(0xFF838587),
size: 18,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
fileName,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w400,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 2),
Text(
date,
style: GoogleFonts.poppins(
color: const Color(0xFF6E6E6E),
fontSize: 10,
),
),
],
),
),
const SizedBox(width: 8),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onUpload,
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(
Icons.file_download_outlined,
color: Color(0xFF6E6E6E),
size: 20,
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onDelete,
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(
Icons.delete_outlined,
color: Color(0xFF6E6E6E),
size: 20,
),
),
),
),
],
),
);
}
}
class GridUploadScreen extends StatelessWidget {
const GridUploadScreen({super.key});
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'Payout Upload',
body: Center(
child: ElevatedButton.icon(
onPressed: () => showUploadGridModal(context),
icon: const Icon(Icons.upload_file_outlined),
label: const Text('Open Payout Upload'),
),
),
);
}
}