ts-tat/lib/Screens/plans/create_plans.dart
2025-05-03 17:59:17 +05:30

2767 lines
97 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:web/web.dart' as web;
import 'package:flutter/material.dart';
import 'package:frontend/Screens/plans/dynamic_itinerary_stepper.dart';
import 'package:frontend/utils/auth_utils.dart';
import 'package:go_router/go_router.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'package:universal_html/html.dart' as html;
import '../../config/apiUrl.dart';
import '../../data/models/plan.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../widgets/custom_radio_button.dart';
import '../../widgets/custom_text_field.dart';
import '../approvals/approval_dialogs.dart';
import '../dialog/user_selection_dialog.dart';
class CreatePlan extends StatefulWidget {
CreatePlan({super.key});
@override
_CreatePlansState createState() => _CreatePlansState();
}
class _CreatePlansState extends State<CreatePlan> {
final GlobalKey<CreateNewPlansState> _createPlanKey =
GlobalKey<CreateNewPlansState>();
Color layoutColor = Colors.redAccent;
Color bodyColor = Colors.white;
@override
void initState() {
super.initState();
loadInitialData();
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor = layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor = bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
});
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Scaffold(
backgroundColor: Colors.white,
appBar: const CustomAppBar(),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding: isDesktop
? EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical: MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(8),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
Expanded(
child: buildUserTable(
isDesktop, context, bodyColor, layoutColor)),
],
),
),
);
});
}
Widget buildUserTable(
bool isDesktop, context, Color? bodyColor, Color layoutColor) {
final args = GoRouterState.of(context).extra as Map<String, dynamic>? ?? {};
// final planData = args?['planData'];
final bool isViewMode = args?['isViewMode'] ?? false;
final bool isApprover = args?['isApprover'] ?? false;
final Map<String, dynamic> planData =
args['planData'] as Map<String, dynamic>? ?? {};
// print("isViewMode: $isViewMode");
// final bool isViewMode = true;
// final planData = GoRouterState.of(context).extra as Map<String, dynamic>? ?? {};
print("RECived palndata");
// print("RECived palndata - ${planData}");
return Container(
// margin: const EdgeInsets.only(left: 10.0, right: 15.0, top: 10.0, bottom: 10.0),
// decoration: BoxDecoration(
// // color: Colors.amber,
// // color: bodyColor,
// color: Color(0xFFE1F5FE),
// border: Border.all(
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
//
// width: 3.5)),
child: Column(
children: [
// Container(
// color: Color(0xFFF4F4FB),
// padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16),
// child: Row(children: [
// Row(
// children: [
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: Icon(
// Icons.create_new_folder_outlined,
// color: Color(0xFF84869A),
// size: 23,
// ),
// ),
// Text(
// isViewMode
// ? "View Plan"
// : (planData.isNotEmpty ? "Update Plan" : "New Plan"),
// style: TextStyle(fontSize: 18),
// ),
// ],
// ),
// Spacer(),
// ]),
// ),
Expanded(
child: Container(
// margin: isDesktop
// ? EdgeInsets.all(10.0)
// : EdgeInsets.only(
// left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
// padding: const EdgeInsets.all(10),
height: isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
// decoration: BoxDecoration(
// border: isDesktop
// ? Border.all(
// width: 2,
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
// )
// : null,
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
//
// // color: Colors.amber,
// ),
// color: bodyColor,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10.0),
child: CreateNewPlan(
key: _createPlanKey,
bodyColor: bodyColor,
layoutColor: layoutColor,
isDesktop: isDesktop,
selectedPlanData: planData,
isViewMode: isViewMode,
isApprover: isApprover,
),
),
),
),
),
Container(
padding: const EdgeInsets.all(10),
color: Colors.white,
child: isDesktop
? Row(
mainAxisAlignment: MainAxisAlignment.end,
// children: [Text("Button")],
children: _buildSubmit(
isDesktop, isViewMode, layoutColor, isApprover),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildSubmit(
isDesktop, isViewMode, layoutColor, isApprover),
))
],
),
);
}
List<Widget> _buildSubmit(
isDesktop, bool isViewMode, Color layoutColor, bool isApprover) {
return [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor ?? Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
isApprover ? context.go('/approvallist') : context.go('/listPlan');
},
child: Text("Cancel")),
SizedBox(
width: 20,
),
MouseRegion(
cursor: isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
isViewMode ? layoutColor : layoutColor, // Keep original color
foregroundColor:
isViewMode ? Colors.white : Colors.white, // Keep original color
disabledBackgroundColor:
layoutColor, // Ensure color remains when disabled
disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: isViewMode
? null
: () {
_createPlanKey.currentState?.handleSubmit();
}, // Disable when in view mode
child: Text("Submit"),
),
)
];
}
}
class CreateNewPlan extends StatefulWidget {
final bool isDesktop;
final bool isViewMode;
final bool isApprover;
final Color? bodyColor;
final Color? layoutColor;
final Map<String, dynamic> selectedPlanData;
const CreateNewPlan({
super.key,
required this.isDesktop,
required this.bodyColor,
required this.layoutColor,
required this.selectedPlanData,
required this.isViewMode,
required this.isApprover,
});
@override
CreateNewPlansState createState() => CreateNewPlansState();
}
class CreateNewPlansState extends State<CreateNewPlan> {
final GlobalKey<DynamicItineraryState> dynamicItineraryKey =
GlobalKey<DynamicItineraryState>();
final ApiService apiService = ApiService();
final TextEditingController _tripTitleController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final TextEditingController _remarksController = TextEditingController();
final FocusNode _tripTitleFocusNode = FocusNode();
final FocusNode _descriptionFocusNode = FocusNode(); // Declare FocusNode
bool _isTripTitleFocused = false;
bool _isdescriptionFocused = false;
late String _selectedOption = "Option 1";
// late String? _selectedIsBillable = "Billable";
bool isStatusExpanded = false;
bool isShowApprovalAction = false;
String? selectedPlanId;
String? userDetails;
String? userName;
String? selfId;
String? otherUserName;
String? selectedplanUserId;
bool? selectedIstravelUser;
late Color layoutColorForUser;
Map<String, dynamic>? apiData; // Store API response here
Map<String, dynamic>? apiDataForClass; // Store API response here
List<dynamic>? apiCountryData;
List<dynamic>? apiCostData; // Store API response here
bool isLoading = true; // Track loading state
String? TripPlanAction;
bool showDomestic = false;
bool showInternational = false;
bool hasAction = true;
late Map<String, String> costCenterMap;
List<String> costCenterIds = [];
// List<dynamic> apiCostData = []; // if youre not already using this
late Map<String, String> purposeMap;
List<String> purposeKeys = [];
String? orgId;
String? planUsrId;
String? planTravlrId;
String? statusValue;
String? _selectedTripType;
String? selectedCostCenterId;
String? _selectedIsBillable;
String? selectedFuncDept;
String? selectedPurpose;
Map<String, String?> validationErrors = {};
List<Map<String, dynamic>> miscellaneousList = [];
// List<Map<String, dynamic>> miscellaneousList = [{"special_request": 1, "comments": "posta", "indx": 1}];
List<Map<String, dynamic>> visaList = [];
List<Map<String, dynamic>> insuranceList = [];
List<Map<String, dynamic>> accommodationList = [];
List<Map<String, dynamic>> trainList = [];
List<Map<String, dynamic>> busList = [];
List<Map<String, dynamic>> taxiList = [];
List<Map<String, dynamic>> forexList = [];
List<Map<String, dynamic>> flightList = [];
List<Map<String, dynamic>> planStatusList = [];
late bool isApproverApproved = false;
late bool isApproverRejected = false;
//Getter Method
Map<String, dynamic> get planData => {
"org_id": orgId,
"user_id": planUsrId,
"traveller_id": planTravlrId,
"trip_title": _tripTitleController.text,
"trip_type": _selectedTripType,
"cost_center_id": selectedCostCenterId,
"is_billable": _selectedIsBillable,
"purpose_of_travel": selectedPurpose,
"description": _descriptionController.text,
"functional_department": selectedFuncDept,
"so_number": "12345",
"created_by": selfId,
"updated_by": selfId,
"is_active": "1",
"flight": flightList,
"accomodation": accommodationList,
"bus": busList,
"taxi": taxiList,
"train": trainList,
"visa": visaList,
"forex": forexList,
"insurance": insuranceList,
"miscellaneous": miscellaneousList,
// "status_value": statusValue,
};
void handleItineraryUpdate(String type, List<Map<String, dynamic>> newList) {
setState(() {
switch (type) {
case "Miscellaneous":
miscellaneousList = newList;
break;
case "Visa":
visaList = newList;
break;
case "Insurance":
insuranceList = newList;
break;
case "Train":
trainList = newList;
break;
case "Bus":
busList = newList;
break;
case "Taxi":
taxiList = newList;
break;
case "Forex":
forexList = newList;
break;
case "Flight":
flightList = newList;
break;
case "Accomodation":
accommodationList = newList;
break;
default:
print("Unknown itinerary type: $type");
}
});
print("Updated $type List: $newList");
}
@override
void initState() {
super.initState();
fetchUserDetails();
fetchPlans();
fetchCostCenter();
fetchCountryList();
_tripTitleFocusNode.addListener(() {
setState(() {
_isTripTitleFocused = _tripTitleFocusNode.hasFocus;
});
});
_descriptionFocusNode.addListener(() {
setState(() {
_isdescriptionFocused = _descriptionFocusNode.hasFocus;
});
});
handleUpdateData();
}
@override
void dispose() {
_tripTitleFocusNode.dispose();
_descriptionFocusNode.dispose();
super.dispose();
}
void handleUpdateData() {
if (widget.selectedPlanData != null) {
setState(() {
statusValue = widget.selectedPlanData['status_value'] ?? '';
print("STATUS____ : $statusValue");
planUsrId = widget.selectedPlanData['user_id'] ?? '';
_tripTitleController.text = widget.selectedPlanData['trip_title'] ?? '';
_descriptionController.text =
widget.selectedPlanData['description'] ?? '';
_selectedTripType = widget.selectedPlanData['trip_type'];
_selectedIsBillable =
widget.selectedPlanData['is_billable'] == "1" ? "1" : "2";
// selectedCostCenterId = widget.selectedPlanData['cost_center_id']?.toString() ;
// selectedPurpose = widget.selectedPlanData['purpose_of_travel']?.toString();
// selectedFuncDept =widget.selectedPlanData['functional_department']?.toString();
if (widget.selectedPlanData!["cost_center_id"] != null) {
selectedCostCenterId =
widget.selectedPlanData!["cost_center_id"].toString();
}
if (widget.selectedPlanData['plan_status'] != null) {
planStatusList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['plan_status'] ?? []);
}
//
if (widget.selectedPlanData!["purpose_of_travel"] != null) {
selectedPurpose =
widget.selectedPlanData!["purpose_of_travel"].toString();
}
if (widget.selectedPlanData!["functional_department"] != null) {
// selectedFuncDept = widget.selectedPlanData!["functional_department"].toString();
selectedFuncDept = widget.selectedPlanData!["functional_department"];
}
// Assign lists from selectedPlanData, ensuring they are properly formatted
flightList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['flight'] ?? []);
accommodationList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['accomodation'] ?? []);
busList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['bus'] ?? []);
taxiList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['taxi'] ?? []);
trainList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['train'] ?? []);
visaList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['visa'] ?? []);
forexList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['forex'] ?? []);
insuranceList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['insurance'] ?? []);
miscellaneousList = List<Map<String, dynamic>>.from(
widget.selectedPlanData['miscellaneous'] ?? []);
});
if (widget.selectedPlanData['trip_type'] != null) {
int? tripId =
int.tryParse(widget.selectedPlanData['trip_type'].toString());
fetchTrainFlightClass(tripId!);
}
if (widget.selectedPlanData.containsKey('plan_id') &&
widget.selectedPlanData['plan_id'] != null) {
print("Plan ID exists: ${widget.selectedPlanData['plan_id']}");
selectedPlanId = widget.selectedPlanData['plan_id']?.toString();
} else {
print("Plan ID is missing or null");
}
print("updatedPlanDAta - $planData");
}
}
void setTripPlanAction() {
setState(() {
if (TripPlanAction == "Plan Creation Not Allowed") {
showDomestic = false;
showInternational = false;
hasAction = false;
} else if (TripPlanAction == "Only Domestic Plan Creation Allowed") {
showDomestic = true;
showInternational = false;
hasAction = true;
} else if (TripPlanAction == "Only International Plan Creation Allowed") {
showDomestic = false;
showInternational = true;
hasAction = true;
} else if (TripPlanAction == "Both Type Plan Creation Allowed") {
showDomestic = true;
showInternational = true;
hasAction = true;
}
});
}
Future<void> getPdfDownload() async {
final String apiUrldata =
'$apiUrl/api/plans/download?plan_id=$selectedPlanId';
// final String apiUrldata = '$apiUrl/auth/googlelogin';
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
try {
print("PDf Dowloaded");
// Create a blob from the response body
final blob = html.Blob([response.bodyBytes]);
// Generate a download URL for the blob
final url = html.Url.createObjectUrlFromBlob(blob);
// Create a link element to trigger the download
final anchor = html.AnchorElement(href: url)
..setAttribute('download', 'trip_plan_$selectedPlanId.pdf')
..click();
// Revoke the download URL to free up resources
html.Url.revokeObjectUrl(url);
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else if (response.statusCode == 404) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('File not found.'),
// content: Text('File not found.'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
),
],
);
},
);
} else {
throw Exception('Failed to load plans');
}
}
void getSelectedPlanFor() {
if (!mounted) return;
setState(() {
if (selectedplanUserId != null) {
if (selectedIstravelUser!) {
planUsrId = "";
planTravlrId = selectedplanUserId;
} else {
planUsrId = selectedplanUserId;
planTravlrId = "";
}
} else {
planUsrId = selfId;
planTravlrId = "";
}
});
print("USER ID - $planUsrId , TRAVELLER ID - $planTravlrId");
}
void fetchUserDetails() async {
final details = await getUserDetails();
TripPlanAction = await getTripPlanAction();
print("TripPlanAction- $TripPlanAction");
print("details- $details");
if (details != null) {
setState(() {
userDetails = details.toString(); // Store the full Map
userName = details['name']; // Extract the name
selfId = details['user_id'];
});
}
orgId = await getOrgId();
print("userDetails - $selfId");
getSelectedPlanFor();
setTripPlanAction();
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}
Future<String?> getUserId() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('userId');
}
Future<Map<String, String>?> getUserDetails() async {
final prefs = await SharedPreferences.getInstance();
final userData = prefs.getString('user_data');
if (userData != null) {
final decodedData = jsonDecode(userData);
return {
'user_id': decodedData['user_id'].toString(),
'name': "${decodedData['first_name']} ${decodedData['last_name']}",
};
}
return null;
}
Future<void> fetchPlans() async {
final String apiUrldata = '$apiUrl/api/getDropdownMaster';
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
try {
final data = json.decode(response.body);
print(data);
if (!data.containsKey('data') || data['data'] is! Map) {
throw Exception(
"Invalid response format: 'data' field is missing or not a Map");
}
Map<String, dynamic> plansJson =
data['data']; // 'data' is a Map, not a List
setState(() {
apiData = plansJson; // Store API response in state
isLoading = false;
});
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else {
throw Exception('Failed to load plans');
}
}
Future<void> fetchCostCenter() async {
final String apiUrldata = '$apiUrl/api/getCostCenterMaster';
final token = await getToken();
final userId = await getUserId();
// print("SUSRTRT- $userId");
//
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
try {
final data = json.decode(response.body);
print(data);
if (!data.containsKey('data') || data['data'] is! List) {
throw Exception(
"Invalid response format: 'data' field is missing or not a List");
}
List<dynamic> plansJson = data['data']; // 'data' is a Map, not a List
// setState(() {
// apiCostData = plansJson; // Store API response in state
// // if(apiCostData!.isNotEmpty){
// // selectedCostCenterId =apiCostData?.first['department_id'];
// // }
//
// if (apiCostData != null && apiCostData!.isNotEmpty) {
// selectedCostCenterId ??= apiCostData!.first['name']?.toString();
// }
// });
setState(() {
apiCostData = plansJson;
costCenterMap = {
for (var item in apiCostData!)
item['department_id'].toString(): item['name'].toString()
};
costCenterIds = costCenterMap.keys.toList();
// Optionally auto-select the first item if not already selected
selectedCostCenterId ??=
costCenterIds.isNotEmpty ? costCenterIds.first : null;
});
print('plansJSON');
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else {
throw Exception('Failed to load plans');
}
}
Future<void> fetchCountryList() async {
final String apiUrldata = '$apiUrl/api/getcountryMaster';
final token = await getToken();
final userId = await getUserId();
// print("SUSRTRT- $userId");
//
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
try {
final data = json.decode(response.body);
print("Country - $data");
if (!data.containsKey('data') || data['data'] is! List) {
throw Exception(
"Invalid response format: 'data' field is missing or not a List");
}
List<dynamic> plansJson = data['data']; // 'data' is a Map, not a List
if (data['data'] is List) {
List<dynamic> plansJson = data['data'];
print("plansJson.length - ${plansJson.length}");
} else {
print("The 'data' key does not contain a list.");
}
setState(() {
apiCountryData = plansJson; // Store API response in state
});
print('plansJSONContry - $plansJson');
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else {
throw Exception('Failed to load plans');
}
}
Future<void> fetchTrainFlightClass(int tripId) async {
final userId = (planUsrId?.toString().isNotEmpty == true)
? planUsrId.toString()
: (planTravlrId?.toString().isNotEmpty == true)
? planTravlrId.toString()
: '';
// final String apiUrldata = '$apiUrl/api/getDropdownMaster';
final String apiUrldata =
'$apiUrl/api/getFlightAndTrainClass?user_id=$userId&trip_type=$tripId';
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
try {
final data = json.decode(response.body);
print(data);
if (!data.containsKey('data') || data['data'] is! Map) {
throw Exception(
"Invalid response format: 'data' field is missing or not a Map");
}
Map<String, dynamic> plansJson =
data['data']; // 'data' is a Map, not a List
setState(() {
apiDataForClass = plansJson; // Store API response in state
isLoading = false;
});
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else {
throw Exception('Failed to load plans');
}
}
// Handle Submit
bool validateForm() {
validationErrors.clear(); // Clear previous errors
// Ensure either "user_id" or "traveller_id" is provided
if ((planUsrId == null || planUsrId!.isEmpty) &&
(planTravlrId == null || planTravlrId!.isEmpty)) {
validationErrors["user_id"] =
"Either User ID or Traveller ID is required";
validationErrors["traveller_id"] =
"Either User ID or Traveller ID is required";
}
final requiredFields = {
if (TripPlanAction != "Plan Creation Not Allowed") //
"trip_type": _selectedTripType,
"cost_center_id": selectedCostCenterId,
"functional_department": selectedFuncDept,
"purpose_of_travel": selectedPurpose,
};
for (var entry in requiredFields.entries) {
if (entry.value == null || entry.value!.isEmpty) {
validationErrors[entry.key] =
"${entry.key.replaceAll('_', ' ').toUpperCase()} is required";
}
}
// Validate at least one service is selected
final serviceLists = [
flightList,
accommodationList,
busList,
taxiList,
trainList,
visaList,
forexList,
insuranceList,
miscellaneousList,
];
bool anyServiceSelected =
serviceLists.any((list) => list != null && list.isNotEmpty);
if (!anyServiceSelected) {
validationErrors["services"] = "Please select at least one service";
}
return validationErrors.isEmpty; // Returns true if no errors
}
Future<void> callApproveAPI(String planId, String userId) async {
await postToAPI(
endpoint: '/api/plans/approvePlan',
data: {
"plan_id": planId,
"user_id": userId,
},
methodName: 'Plan Approval',
);
}
Future<void> callRejectAPI(
String planId, String userId, String remarks) async {
await postToAPI(
endpoint: '/api/plans/rejectPlan',
data: {
"plan_id": planId,
"user_id": userId,
"reason": remarks,
},
methodName: 'Plan Rejection',
);
}
Future<void> postToAPI({
required String endpoint,
required Map<String, dynamic> data,
String methodName = '',
}) async {
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final response = await http.post(
Uri.parse('$apiUrl$endpoint'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode(data),
);
if (response.statusCode == 200) {
print("$methodName successful!");
print("Response: ${response.body}");
// await apiService.getViewPlan(
// data['planId'],planData
// );
print("ViewAAA - $planData");
print("Call viewPlanForApprover");
ApiService.viewPlanForApprover(context, data['plan_id'],
isViewMode: false, isApprover: true);
} else {
print("$methodName failed. Status: ${response.statusCode}");
print("Error: ${response.body}");
}
} catch (e) {
print("Error in $methodName: $e");
}
}
void handleSubmit() {
setState(() {
if (validateForm()) {
// if (selectedPlanId != null && selectedPlanId!.isNotEmpty) {
// planData['plan_id'] = selectedPlanId; // Add plan_id for update
// }
//
// print("Form submitted successfully:"
// " ${_remarksController.text}, ${planData['user_id']}, ${planData['traveller_id']}, "
// "${planData['traveller_id']},"
// " ${selectedPlanId}, "
// " ");
postPlanData(planData);
widget.isApprover
? context.go('/approvallist')
: context.go('/listPlan');
}
});
}
Future<void> postPlanData(planData) async {
final String apiUrldata = '$apiUrl/api/plans/createOrEditPlan';
final token = await getToken(); // Fetch token
if (token == null) {
throw Exception('Token not found. Please log in.');
}
if (selectedPlanId != null && selectedPlanId!.isNotEmpty) {
planData['plan_id'] = selectedPlanId; // Add plan_id for update
}
print("POSTPlanTesting------- $planData}");
try {
final response = await http.post(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode(planData), // Convert map to JSON
);
if (response.statusCode == 200) {
print("Plan submitted successfully!");
print("Response: ${response.body}");
} else {
print("Failed to submit plan. Status: ${response.statusCode}");
print("Error: ${response.body}");
}
} catch (e) {
print(" Error submitting plan: $e");
}
}
Widget build(BuildContext context) {
bool hasApprovals = planStatusList.any(
(item) => item.entries.any(
(entry) =>
entry.key.contains('status') &&
entry.value != null &&
entry.value.toString().isNotEmpty,
),
);
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ResponsiveBuilder(builder: (context, sizingInfo) {
bool isDesktop = sizingInfo.deviceScreenType ==
DeviceScreenType.desktop;
return Container(
padding: const EdgeInsets.all(5),
child: isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildApproverControls(isDesktop),
Spacer(),
Text(
widget.isViewMode
? "View Plan"
: (selectedPlanId != null &&
selectedPlanId!.isNotEmpty
? "Update Plan"
: "New Plan"),
style: TextStyle(fontSize: 18),
),
Spacer(),
..._buildPlanPdf()
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildApproverControls(isDesktop),
Text(
widget.isViewMode
? "View Plan"
: (planData.isNotEmpty
? "Update Plan"
: "New Plan"),
style: TextStyle(fontSize: 18),
),
..._buildPlanPdf()
],
),
);
}),
// if (isStatusExpanded)
// Container(
// margin: isDesktop
// ? const EdgeInsets.only(left: 0, top: 0)
// : const EdgeInsets.only(left: 5, top: 2),
// padding: const EdgeInsets.all(12),
// width: isDesktop
// ? MediaQuery.of(context).size.width * 0.2
// : MediaQuery.of(context).size.width,
// decoration: BoxDecoration(
// color: Color(0xFFF5F5F5),
// border: Border.all(
// // color: Colors.grey.shade300,
// color: Colors.white,
// width: 0.2),
// borderRadius: BorderRadius.circular(8),
// // boxShadow: [
// // BoxShadow(
// // // color: Colors.grey.withAlpha(20),
// // color: Colors.grey.withAlpha(20),
// // spreadRadius: 1.5,
// // blurRadius: 7,
// // offset: Offset(0, 4), // shadow direction: bottom
// // ),
// // ],
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// if (!hasApprovals)
// Center(
// child: Text(
// "--- No Approvals ---",
// style: TextStyle(
// fontFamily: "Archivo",
// fontSize: 11,
// fontWeight: FontWeight.w500,
// color: Colors.black87,
// ),
// )),
// for (int i = 0; i < planStatusList.length; i++) ...[
// if (planStatusList[i].entries.any((entry) =>
// entry.key.contains('status') &&
// entry.value != null &&
// entry.value.toString().isNotEmpty)) ...[
// _buildApprovalItem(
// "Approver ${i + 1}",
// planStatusList[i]
// .entries
// .firstWhere(
// (entry) => entry.key.contains('status'),
// orElse: () => MapEntry('', ''),
// )
// .value
// .toString(),
// ),
// SizedBox(height: 6),
// ],
// ],
// ],
// ),
// ),
if (isApproverRejected)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 4, left: 8),
child: Text("Remarks : ",
style: TextStyle(
fontFamily: "Archivo",
fontWeight: FontWeight.w600,
fontSize: 11)),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
top: 0), // tweak if needed
child: TextFormField(
controller: _remarksController,
style: TextStyle(
fontFamily: "Archivo",
fontWeight: FontWeight.w600,
fontSize: 11),
decoration: const InputDecoration(
hintText: "Please enter remarks...",
hintStyle: TextStyle(
color: Colors.grey, fontSize: 15),
border: InputBorder.none,
isDense: true,
),
maxLines: null,
),
),
),
],
),
// if (widget.isApprover || isStatusExpanded)
Divider(
thickness: 0.1,
color: Colors.blueGrey,
),
if (widget.isApprover)
SizedBox(
height: 5,
),
isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _buildPlanTrip(isMobile),
),
)
: Row(
children: _buildPlanTrip(isMobile),
),
SizedBox(height: 13),
Text.rich(
TextSpan(
text: "Planning this Trip For : ", // Static text
style: TextStyle(
fontFamily: "Archivo",
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
// color: Color(0xFF575A74), // Default color
),
children: [
TextSpan(
text: otherUserName ??
userName ??
" ", // Dynamic username
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: widget
.layoutColor, // Change this to any color
// color: Colors.blueAccent, // Change this to any color
),
),
],
),
),
],
),
),
],
),
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: Divider(
// color: Color(0xFFE6E7F5), // Change color
// thickness: 0.5,
// ),
// ),
SizedBox(height: 10),
// Row(
// children: [
// Expanded(
// child:
// ),
// ],
// ),
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildTripRow(isMobile))
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildTripRow(isMobile)),
SizedBox(
height: 15,
),
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildCostCenter(isDesktop))
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildCostCenter(isDesktop)),
SizedBox(
height: 8,
),
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNonDescriptionColumn(),
SizedBox(width: 25),
_buildDescriptionColumn(isDesktop),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNonDescriptionColumn(),
SizedBox(height: 15),
_buildDescriptionColumn(isDesktop),
],
),
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: Divider(
// color: Color(0xFFE6E7F5), // Change color
// thickness: 0.5,
// ),
// ),
SizedBox(
height: 20,
),
if (validationErrors["services"] != null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
validationErrors["services"]!,
style: TextStyle(
color: Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold),
),
],
),
Row(
children: [
Expanded(
child: DynamicItinerary(
key: dynamicItineraryKey,
hasAction: hasAction,
tripType: _selectedTripType,
apiData: apiData,
apiDataForClass: apiDataForClass,
apiCountryData: apiCountryData,
onItineraryUpdate: handleItineraryUpdate,
loginUser: selfId,
selectedPlanData: planData,
isViewMode: widget.isViewMode,
)), // Wrap with Expanded if needed
],
),
SizedBox(height: 15),
],
);
// Row(
// children: [
// isDesktop
// ? Row(
// mainAxisAlignment: MainAxisAlignment.end,
// children: _buildSubmit(isDesktop),
// )
// : Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: _buildSubmit(isDesktop),
// )
// ],
// )
});
}
String? getSelectedCostCenterName() {
if (selectedCostCenterId == null || apiCostData == null) return null;
return apiCostData!.firstWhere(
(item) => item['department_id'] == selectedCostCenterId,
orElse: () => null,
)?['name'];
}
/// Extracted helper function
///
List<Widget> _buildTripRow(bool isMobile) {
List<dynamic> purposeList = apiData?['plan_is_billable'] ?? [];
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Trip Type *", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _buildTripType(isMobile),
),
)
: Row(
children: _buildTripType(isMobile),
),
if (validationErrors["trip_type"] != null)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
validationErrors["trip_type"]!,
style: TextStyle(color: Colors.red, fontSize: 10),
),
),
],
),
SizedBox(
width: 150,
height: 5,
),
// SizedBox(
// width: 25,
// height: 5,
// ),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(
"Is Billable ", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
Row(
children: purposeList.map<Widget>((item) {
final isSelected =
_selectedIsBillable == item['dropdown_key'].toString();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: CustomTextFieldWrapper(
color: Color(0xFFF5F5F5),
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
layoutColor: widget.layoutColor,
width: item['dropdown_key'] == 'some_key' ? 185 : 125,
borderRadius: BorderRadius.circular(25),
isFocused: isSelected,
isDesktop: widget.isDesktop,
child: GestureDetector(
onTap: widget.isViewMode
? null
: () {
setState(() {
_selectedIsBillable =
item['dropdown_key'].toString();
});
print("SELECBILL - $_selectedIsBillable");
},
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item['dropdown_value'] ?? '',
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: isSelected ? FontWeight.w500 : null,
fontSize: 13),
),
const SizedBox(width: 8),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected ? Colors.white : Colors.black,
width: isSelected ? 2 : 1,
),
),
child: isSelected
? Icon(Icons.rectangle,
size: 8, color: Colors.white)
: null,
),
],
),
),
),
);
}).toList(),
),
// Row(
// children: purposeList.map<Widget>((item) {
// // return Row(
// // children: [
// // Radio<String>(
// // // value: item['dropdown_key'], // Use dropdown_value as value
// //
// // value: item['dropdown_key'].toString(), // Convert to String
// // groupValue: _selectedIsBillable,
// // activeColor: Colors.blueAccent,
// // onChanged: widget.isViewMode
// // ? null
// // : (value) {
// // setState(() {
// // _selectedIsBillable = value;
// // });
// // print("SELECBILL - $_selectedIsBillable");
// // },
// // ),
// //
// // Text(item['dropdown_value'] ?? ''), // Display dropdown_value
// // SizedBox(width: 20), // Spacing
// // ],
// // );
// }).toList(),
//
//
// ),
])
];
}
List<Widget> _buildCostCenter(bool isDesktop) {
// if (apiData == null) {
// return Center(child: CircularProgressIndicator()); // Show loading indicator
// }
// 'plan_purpose_of_travel' Starts ------------------------------------------------------
// List<dynamic> purposeList = apiData?['plan_purpose_of_travel'] ?? [];
List<Map<String, dynamic>> purposeList = List<Map<String, dynamic>>.from(
apiData?['plan_purpose_of_travel'] ?? []);
List<DropdownMenuItem<String>> dropdownItems = purposeList
.map((item) => DropdownMenuItem<String>(
// value: item['dropdown_key'],
value: item['dropdown_key']?.toString(),
// value: item['dropdown_key'].toString(),
child: Text(item['dropdown_value']),
))
.toList();
if (dropdownItems.isEmpty) {
dropdownItems.add(
DropdownMenuItem<String>(
// value: null,
value: "1",
child: Text("No options available",
style: TextStyle(color: Colors.grey)),
),
);
}
selectedPurpose ??= dropdownItems.isNotEmpty
? dropdownItems.first.value.toString()
: "No options";
print(
"Dropdown Purpose List: ${dropdownItems.map((e) => e.value).toList()}");
print("Selected Purpose: $selectedPurpose");
// 'plan_functional_department' Starts ---------------------------------------------
// List<dynamic> funcDeptList = apiData?['plan_functional_department'] ?? [];
List<Map<String, dynamic>> funcDeptList = List<Map<String, dynamic>>.from(
apiData?['plan_functional_department'] ?? []);
List<DropdownMenuItem<String>> dropdownFuncDeptItems = funcDeptList
.map((item) => DropdownMenuItem<String>(
// value: item['dropdown_key'],
value: item['dropdown_key']?.toString(),
child: Text(item['dropdown_value']),
))
.toList();
if (dropdownFuncDeptItems.isEmpty) {
dropdownFuncDeptItems.add(
DropdownMenuItem<String>(
// value: null,
value: "1",
child: Text("No options available",
style: TextStyle(color: Colors.grey)),
),
);
}
// selectedFuncDept ??= dropdownFuncDeptItems.isNotEmpty ? dropdownFuncDeptItems.first.value.toString() : null;
selectedFuncDept ??= dropdownFuncDeptItems.isNotEmpty
? dropdownFuncDeptItems.first.value.toString()
: "No options";
print(
"Dropdown Functional Department List: ${dropdownFuncDeptItems.map((e) => e.value).toList()}");
print("Selected Functional Department: $selectedFuncDept");
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Cost Center *", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false, // Dropdown doesn't use focus
isDesktop: widget.isDesktop,
child: SizedBox(
height: 35,
width: double.infinity,
child: apiCostData == null
? Center(
child: Transform.scale(
scale: 0.5,
child: CircularProgressIndicator(),
),
)
: DropdownSearch<String>(
selectedItem: costCenterMap[selectedCostCenterId],
enabled: !widget.isViewMode,
popupProps: PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search Cost Center...",
hintStyle:
TextStyle(fontSize: 13, color: Colors.grey),
contentPadding:
EdgeInsets.symmetric(horizontal: 10),
),
style: TextStyle(fontSize: 13)),
menuProps: MenuProps(
backgroundColor: Colors.white,
),
itemBuilder: (context, item, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
item,
style: TextStyle(
fontSize:
13), // 👈 Set your desired text size here
),
),
),
items: costCenterMap.values.toList(), // just names
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1),
),
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
setState(() {
selectedCostCenterId = costCenterMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
});
},
),
),
// child: SizedBox(
// height: 45, // Set appropriate height
// child: DropdownButtonFormField<String>(
// value: selectedCostCenterId,
// style: TextStyle(fontSize: 12),
// decoration: InputDecoration(
// border: InputBorder.none,
// contentPadding:
// EdgeInsets.symmetric(horizontal: 10), // Proper padding
// ),
// onChanged: widget.isViewMode
// ? null
// : (newValue) {
// setState(() {
// selectedCostCenterId = newValue;
// });
// },
// items: apiCostData?.map<DropdownMenuItem<String>>((item) {
// return DropdownMenuItem(
// value: item['department_id'], // ID as value
// child: Text(item['name'] ?? "Unknown"),
// );
// }).toList(),
// ),
// ),
),
],
),
SizedBox(
width: 25,
height: 5,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Purpose of Trip *", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
width: isDesktop
? MediaQuery.of(context).size.width * 0.2
: MediaQuery.of(context).size.width * 0.85,
isFocused: false, // Dropdown doesn't use focus
isDesktop: widget.isDesktop,
child: SizedBox(
height: 35, // Set appropriate height
child: apiData == null
? Center(
child:
CircularProgressIndicator()) // Show loading inside dropdown
: DropdownSearch<Map<String, dynamic>>(
selectedItem: purposeList.firstWhere(
(item) =>
item['dropdown_key'].toString() == selectedPurpose,
orElse: () => <String, dynamic>{}, // ✅ Safe fallback
),
items: purposeList,
itemAsString: (Map<String, dynamic> item) =>
item['dropdown_value'],
popupProps: PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search Purpose...",
hintStyle:
TextStyle(fontSize: 13, color: Colors.grey),
contentPadding:
EdgeInsets.symmetric(horizontal: 10),
),
style: TextStyle(fontSize: 13)),
menuProps: MenuProps(
backgroundColor: Colors.white,
),
itemBuilder: (context, item, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
item['dropdown_value'],
style: TextStyle(
fontSize:
13), // 👈 Set your desired text size here
),
),
),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
horizontal: 1,
),
border: InputBorder.none,
),
),
onChanged: widget.isViewMode
? null
: (Map<String, dynamic>? newValue) {
setState(() {
selectedPurpose =
newValue?['dropdown_key'].toString();
});
print("selectedPurpose - $selectedPurpose");
},
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem?['dropdown_value'] ?? '',
style: TextStyle(fontSize: 12),
),
),
),
),
),
// CustomTextFieldWrapper(
// isFocused: false, // Dropdown doesn't use focus
// isDesktop: widget.isDesktop,
// child: SizedBox(
// height: 45, // Set appropriate height
// child: apiData == null
// ? Center(
// child:
// CircularProgressIndicator()) // Show loading inside dropdown
// : DropdownButtonFormField<String>(
// value: selectedPurpose,
// style: TextStyle(fontSize: 12),
// decoration: InputDecoration(
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(
// horizontal: 10), // Proper padding
// ),
// onChanged: widget.isViewMode
// ? null
// : purposeList.isNotEmpty
// ? (newValue) {
// setState(() {
// selectedPurpose = newValue;
// });
// print(
// "selectedPurpose - $selectedPurpose");
// }
// : null,
// items: dropdownItems,
// ),
// ),
// ),
],
),
SizedBox(
width: 25,
height: 5,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Functional Department", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false, // Dropdown doesn't use focus
isDesktop: widget.isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.2
: MediaQuery.of(context).size.width * 0.85,
// width: MediaQuery.of(context).size.width * 0.2,
child: SizedBox(
height: 35, // Set appropriate height
child: apiData == null
? Center(
child:
CircularProgressIndicator()) // Show loading inside dropdown
: DropdownSearch<Map<String, dynamic>>(
selectedItem: funcDeptList.firstWhere(
(item) =>
item['dropdown_key'].toString() == selectedFuncDept,
orElse: () => <String, dynamic>{}, // ✅ Safe fallback
),
items: funcDeptList,
itemAsString: (Map<String, dynamic> item) =>
item['dropdown_value'],
popupProps: PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search department...",
hintStyle:
TextStyle(fontSize: 13, color: Colors.grey),
contentPadding:
EdgeInsets.symmetric(horizontal: 10),
),
style: TextStyle(fontSize: 13)),
menuProps: MenuProps(
backgroundColor: Colors.white,
),
itemBuilder: (context, item, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
item['dropdown_value'],
style: TextStyle(
fontSize:
13), // 👈 Set your desired text size here
),
),
),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 1),
border: InputBorder.none,
),
),
onChanged: widget.isViewMode
? null
: (Map<String, dynamic>? newValue) {
setState(() {
selectedFuncDept =
newValue?['dropdown_key'].toString();
});
print("selectedFuncDept - $selectedFuncDept");
},
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem?['dropdown_value'] ?? '',
style: TextStyle(fontSize: 12),
),
),
),
),
),
],
),
];
}
List<Widget> _buildPlanTrip(bool isDesktop) {
List<Map<String, String>> options = [
{"title": "Self", "value": "Option 1"},
{"title": "Other Employee", "value": "Option 2"},
{"title": "Others", "value": "Option 3"},
];
print(" layoutColor: ${widget.layoutColor}");
return options.map((option) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
child: CustomTextFieldWrapper(
// color: Color(0xFFF4F4FB),
color: Color(0xFFF5F5F5),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
layoutColor: widget.layoutColor,
width: option["value"] == "Option 2"
? 185
: 125, // Adjust width conditionally
borderRadius: BorderRadius.circular(25),
isFocused: _selectedOption == option["value"],
isDesktop: isDesktop,
child: GestureDetector(
onTap: widget.isViewMode
? null
: () {
setState(() {
_selectedOption = option["value"]!;
if (option["value"] == 'Option 2' ||
option["value"] == 'Option 3') {
_showInputDialog(option["title"]!);
}
});
},
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
option["title"]!,
style: TextStyle(
fontSize: 13,
color: _selectedOption == option["value"]
? Colors.white
: Colors.black,
fontWeight: _selectedOption == option["value"]
? FontWeight.w500
: null,
),
),
Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
// color: _selectedOption == option["value"]
// ? Colors.blueAccent
// : Colors.transparent,
borderRadius: BorderRadius.circular(4), // Rounded rectangle
border: Border.all(
color: _selectedOption == option["value"]
? Colors.white
: Colors.black,
width: _selectedOption == option["value"] ? 2 : 1,
),
),
child: _selectedOption == option["value"]
? Icon(Icons.rectangle, size: 8, color: Colors.white)
: null, // Add checkmark if selected
),
],
),
),
),
);
}).toList();
}
List<Widget> _buildTripType(bool isMobile) {
return [
if (showDomestic == true)
GestureDetector(
onTap: widget.isViewMode
? null
: () {
setState(() {
_selectedTripType = "1";
fetchTrainFlightClass(1);
dynamicItineraryKey.currentState?.updateSelectedServices();
});
},
child: CustomTextFieldWrapper(
color: Color(0xFFF4F4FB),
layoutColor: widget.layoutColor,
borderRadius: BorderRadius.circular(25),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
width: 130,
isFocused: _selectedTripType == "1",
isDesktop: widget.isDesktop,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Domestic",
style: TextStyle(
color: _selectedTripType == "1"
? Colors.white
: Colors.black,
fontWeight:
_selectedTripType == "1" ? FontWeight.w500 : null,
fontSize: 13),
),
Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
// color: _selectedOption == option["value"]
// ? Colors.blueAccent
// : Colors.transparent,
borderRadius: BorderRadius.circular(4), // Rounded rectangle
border: Border.all(
color: _selectedTripType == "1"
? Colors.white
: Colors.black,
width: _selectedTripType == "1" ? 2 : 1,
),
),
child: _selectedTripType == "1"
? Icon(Icons.rectangle, size: 8, color: Colors.white)
: null, // Add checkmark if selected
),
],
),
),
),
SizedBox(width: 20),
if (showInternational)
GestureDetector(
onTap: widget.isViewMode
? null
: () {
setState(() {
_selectedTripType = "2";
fetchTrainFlightClass(2);
dynamicItineraryKey.currentState?.updateSelectedServices();
});
},
child: CustomTextFieldWrapper(
color: Color(0xFFF4F4FB),
layoutColor: widget.layoutColor,
borderRadius: BorderRadius.circular(25),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
width: 150,
// padding: EdgeInsets.symmetric(horizontal: 5, vertical: 2),
isFocused: _selectedTripType == "2",
isDesktop: widget.isDesktop,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"International",
style: TextStyle(
fontSize: 13,
color:
_selectedTripType == "2" ? Colors.white : Colors.black,
fontWeight:
_selectedTripType == "2" ? FontWeight.w500 : null,
),
),
Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
// color: _selectedOption == option["value"]
// ? Colors.blueAccent
// : Colors.transparent,
borderRadius: BorderRadius.circular(4), // Rounded rectangle
border: Border.all(
color: _selectedTripType == "2"
? Colors.white
: Colors.black,
width: _selectedTripType == "2" ? 2 : 1,
),
),
child: _selectedTripType == "2"
? Icon(Icons.rectangle, size: 8, color: Colors.white)
: null, // Add checkmark if selected
),
],
),
// RadioListTile<String>(
// activeColor: Colors.blueAccent,
// contentPadding: EdgeInsets.zero,
// dense: true,
// title: Text("International"),
// value: "2",
// groupValue: _selectedTripType,
// onChanged: widget.isViewMode
// ? null
// : (value) {
// setState(() {
// _selectedTripType = value!;
// });
// },
// ),
),
),
];
}
Widget _buildNonDescriptionColumn() {
return Column(children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Trip Name", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: _isTripTitleFocused,
isDesktop: widget.isDesktop,
child: SizedBox(
height: 35,
child: TextField(
focusNode: _tripTitleFocusNode,
controller: _tripTitleController,
style: TextStyle(fontSize: 12),
enabled: !widget.isViewMode,
decoration: InputDecoration(
labelText: "Trip Name",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
],
),
SizedBox(
height: 8,
),
]);
}
Widget _buildDescriptionColumn(isDesktop) {
return Column(
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Description", // Your label
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: _isdescriptionFocused,
isDesktop: widget.isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.42
: MediaQuery.of(context).size.width * 0.85,
child: SizedBox(
height: 35,
child: TextField(
focusNode: _descriptionFocusNode,
controller: _descriptionController,
style: TextStyle(fontSize: 12),
enabled: !widget.isViewMode,
decoration: InputDecoration(
labelText: "Description",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
// CustomTextFieldWrapper(
// isFocused: _isdescriptionFocused,
// width: isDesktop
// ? MediaQuery.of(context).size.width * 0.4
// : MediaQuery.of(context).size.width * 0.85,
// isDesktop: widget.isDesktop,
// child: SizedBox(
// height: 35,
// child:
// TextField(
// focusNode: _descriptionFocusNode,
// controller: _descriptionController,
// // maxLines: 1,
// // keyboardType: TextInputType.multiline,
// style: TextStyle(fontSize: 12),
// enabled: !widget.isViewMode,
// decoration: InputDecoration(
// labelText: "Description",
// labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
// floatingLabelBehavior: FloatingLabelBehavior.never,
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(vertical: 1),
// ),
// ),
// ),
// ),
],
),
],
),
],
);
}
List<Widget> _buildSubmit1(isDesktop) {
return [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.blueAccent, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
widget.isApprover
? context.go('/approvallist')
: context.go('/listPlan');
},
child: Text("Cancel")),
SizedBox(
width: 20,
),
MouseRegion(
cursor: widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: widget.isViewMode
? Colors.blueAccent
: Colors.blueAccent, // Keep original color
foregroundColor: widget.isViewMode
? Colors.white
: Colors.white, // Keep original color
disabledBackgroundColor:
Colors.blueAccent, // Ensure color remains when disabled
disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.blueAccent, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: widget.isViewMode
? null
: handleSubmit, // Disable when in view mode
child: Text("Submit"),
),
)
];
}
Widget _buildApprovalItem(String title, String name) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"$title : ",
style: TextStyle(
fontFamily: "Archivo",
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
Expanded(
child: Text(
name,
style: TextStyle(
fontFamily: "Archivo",
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
],
);
}
Color getStatusColor(String status) {
if (status == "Partially Approved") return Colors.yellow;
if (status == "Approved") return Colors.green;
if (status == "Completed") return Colors.green;
if (status == "Rejected") return Colors.red;
return Colors.grey;
}
List<Widget> _buildApproverControls(bool isDesktop) {
final statusText = isApproverApproved
? "Approved"
: isApproverRejected
? "Rejected"
: (statusValue ?? "");
bool hasApprovals = planStatusList.any(
(item) => item.entries.any(
(entry) =>
entry.key.contains('status') &&
entry.value != null &&
entry.value.toString().isNotEmpty,
),
);
return [
Stack(
clipBehavior: Clip.none, // allow tooltip to overflow
children: [
if (statusValue != "")
MouseRegion(
onEnter: (_) => setState(() => isStatusExpanded = true),
onExit: (_) => setState(() => isStatusExpanded = false),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: getStatusColor(statusText)),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: 8.0, bottom: 8.0, left: 15, right: 15),
child: Text(
statusText,
style: TextStyle(
fontFamily: "Roboto",
fontWeight: FontWeight.w400,
fontSize: 12,
color: getStatusColor(
isApproverApproved
? "Approved"
: isApproverRejected
? "Rejected"
: (statusValue ?? ""),
),
),
),
),
],
),
),
),
// Tooltip
if (isStatusExpanded)
Positioned(
top: -20, // Ensure the tooltip is above the container
left:
MediaQuery.of(context).size.width * 0.06, // align with label
child: Material(
// important: avoid clipping, give elevation
elevation: 4,
borderRadius: BorderRadius.circular(8),
child: SizedBox(
child: Container(
width: isDesktop
? MediaQuery.of(context).size.width * 0.25
: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
// color: Colors.transparent,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!hasApprovals)
Center(
child: Text(
"--- No Approvals ---",
style: TextStyle(
fontFamily: "Archivo",
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
)),
for (int i = 0; i < planStatusList.length; i++) ...[
if (planStatusList[i].entries.any((entry) =>
entry.key.contains('status') &&
entry.value != null &&
entry.value.toString().isNotEmpty)) ...[
_buildApprovalItem(
"Approver ${i + 1}",
planStatusList[i]
.entries
.firstWhere(
(entry) => entry.key.contains('status'),
orElse: () => MapEntry('', ''),
)
.value
.toString(),
),
SizedBox(height: 6),
],
],
],
),
),
),
),
),
// if (isStatusExpanded)
// Positioned(
// top: 20, // adjust how much above you want
// left: 100,
// child: Container(
// margin: isDesktop
// ? const EdgeInsets.only(left: 0, top: 0)
// : const EdgeInsets.only(left: 5, top: 2),
// padding: const EdgeInsets.all(12),
// width: isDesktop
// ? MediaQuery.of(context).size.width * 0.2
// : MediaQuery.of(context).size.width,
// decoration: BoxDecoration(
// // color: Color(0xFFF5F5F5),
// color: Colors.white,
// border: Border.all(color: Colors.white, width: 0.2),
// borderRadius: BorderRadius.circular(8),
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// if (!hasApprovals)
// Center(
// child: Text(
// "--- No Approvals ---",
// style: TextStyle(
// fontFamily: "Archivo",
// fontSize: 11,
// fontWeight: FontWeight.w500,
// color: Colors.black87,
// ),
// )),
// for (int i = 0; i < planStatusList.length; i++) ...[
// if (planStatusList[i].entries.any((entry) =>
// entry.key.contains('status') &&
// entry.value != null &&
// entry.value.toString().isNotEmpty)) ...[
// _buildApprovalItem(
// "Approver ${i + 1}",
// planStatusList[i]
// .entries
// .firstWhere(
// (entry) => entry.key.contains('status'),
// orElse: () => MapEntry('', ''),
// )
// .value
// .toString(),
// ),
// SizedBox(height: 6),
// ],
// ],
// ],
// ),
// ),
// ),
],
),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(
// isApproverApproved
// ? "Approved"
// : isApproverRejected
// ? "Rejected"
// : (statusValue ?? ""),
// style: TextStyle(
// fontFamily: "Archivo",
// fontWeight: FontWeight.bold,
// color: widget.layoutColor ?? Colors.grey,
// ),
// ),
// SizedBox(width: 5),
// MouseRegion(
// onEnter: (_) => setState(() => isStatusExpanded = true),
// onExit: (_) => setState(() => isStatusExpanded = false),
// child: Icon(
// Icons.approval_outlined,
// size: 18,
// color: isStatusExpanded ? Colors.green : Colors.grey,
// ),
// ),
// ],
// ),
const SizedBox(height: 10, width: 10),
if (widget.isApprover)
GestureDetector(
onTap: () {
setState(() {
isShowApprovalAction = !isShowApprovalAction;
});
},
child: Icon(
Icons.edit,
size: 18,
color: isShowApprovalAction ? Colors.green : Colors.grey,
),
),
const SizedBox(height: 10, width: 10),
if (isShowApprovalAction)
Row(
mainAxisSize: MainAxisSize.min,
children: [
MouseRegion(
cursor: widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
disabledBackgroundColor:
statusValue == "Approved" ? Colors.green.shade100 : null,
disabledForegroundColor:
statusValue == "Approved" ? Colors.white : Colors.black,
backgroundColor: isApproverApproved ||
(!isApproverApproved &&
!isApproverRejected &&
statusValue == "Approved")
? Colors.green
: Colors.grey.shade100,
foregroundColor: isApproverApproved ||
(!isApproverApproved &&
!isApproverRejected &&
statusValue == "Approved")
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: 18, vertical: 10),
),
onPressed: widget.isViewMode
? null
: () async {
final confirmed = await showApproveDialog(
context, widget.layoutColor ?? Colors.grey);
if (confirmed == true) {
setState(() {
isApproverApproved = true;
isApproverRejected = false;
statusValue = "Approved";
});
callApproveAPI(
selectedPlanId!,
planData['user_id'],
);
}
},
child: Text(
isApproverApproved || statusValue == "Approved"
? "Approved"
: "Approve",
style: TextStyle(fontSize: 12),
),
),
),
const SizedBox(width: 10),
MouseRegion(
cursor: widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
disabledBackgroundColor: statusValue == "Rejected"
? Colors.redAccent.shade100
: null,
disabledForegroundColor:
statusValue == "Rejected" ? Colors.white : Colors.black,
backgroundColor: isApproverRejected ||
(!isApproverApproved &&
!isApproverRejected &&
statusValue == "Rejected")
? Colors.redAccent
: Colors.grey.shade100,
foregroundColor: isApproverRejected ||
(!isApproverApproved &&
!isApproverRejected &&
statusValue == "Rejected")
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: 18, vertical: 10),
),
onPressed: widget.isViewMode
? null
: () async {
final remarks = await showRejectDialog(
context, widget.layoutColor ?? Colors.grey);
if (remarks != null) {
_remarksController.text = remarks;
setState(() {
isApproverApproved = false;
isApproverRejected = true;
statusValue = "Rejected";
});
callRejectAPI(
selectedPlanId!, planData['user_id'], remarks);
}
},
child: Text(
isApproverRejected || statusValue == "Rejected"
? "Rejected"
: "Reject",
style: TextStyle(fontSize: 12),
),
),
),
],
),
];
}
List<Widget> _buildPlanPdf() {
return [
Column(
children: [
InkWell(
hoverColor: Colors.white,
onTap: () {
print('📄 PDF icon clicked!');
getPdfDownload();
},
child: Row(
children: [
Text("Download PDF"),
SizedBox(
width: 10,
),
Transform.scale(
scale: 1.5, // 1.0 = normal, 1.5 = 50% bigger
child: Image.asset(
'assets/images/IconsImg/planPdf_icon.png',
width: 25,
height: 25, // keep the real height small
),
),
],
)),
// SizedBox(height: 10), // small spacing
],
),
];
}
void _showInputDialog(String title) {
showDialog(
context: context,
builder: (BuildContext context) {
return UserSelectionDialog(
title: title,
onSubmit: (input, userId, isTraveller) {
setState(() {
otherUserName = input;
selectedplanUserId = userId;
selectedIstravelUser = isTraveller;
});
print("USer entered : $otherUserName $userId $isTraveller");
getSelectedPlanFor();
},
layoutColorForUser: widget.layoutColor!,
);
});
}
}