ts-tat/lib/Screens/plans/create_plans.dart
2025-06-06 18:05:58 +05:30

3535 lines
126 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:google_fonts/google_fonts.dart';
import 'package:super_tooltip/super_tooltip.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 '../../widgets/saving_loader.dart';
import '../approvals/approval_dialogs.dart';
import '../dialog/user_selection_dialog.dart';
import '../itnerary/flights.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;
final FocusNode focusNode = FocusNode();
bool isFocused = false;
@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,
backgroundColor: Color(0xFFf5f5f5),
// backgroundColor: Color(0xFFFCFCFC),
appBar: CustomAppBar(isDesktop: isDesktop),
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(0),
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 String approverId = args['approverId'] ?? "";
final String delegaterId = args['delegaterId'] ?? "";
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 approverId - $approverId");
print("RECived delegateId - $delegaterId");
return Container(
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
// color: Colors.amber,
// color: bodyColor,
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
// 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,
approverId: approverId,
delegaterId: delegaterId,
),
),
),
),
),
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: () {
final currentUri =
GoRouterState.of(
context,
).uri.toString(); // ✅ safer than `.location`
print("currentUri - $currentUri");
if (currentUri == "/allTrips/trips") {
context.go('/listAllPlan');
} else if (currentUri == "/createPlan") {
context.go('/listPlan');
} else {
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;
final String approverId;
final String delegaterId;
const CreateNewPlan({
super.key,
required this.isDesktop,
required this.bodyColor,
required this.layoutColor,
required this.selectedPlanData,
required this.isViewMode,
required this.isApprover,
required this.approverId,
required this.delegaterId,
});
@override
CreateNewPlansState createState() => CreateNewPlansState();
}
class CreateNewPlansState extends State<CreateNewPlan> {
final GlobalKey<DynamicItineraryState> dynamicItineraryKey =
GlobalKey<DynamicItineraryState>();
final GlobalKey<FlightScreenState> flightScreenKey =
GlobalKey<FlightScreenState>();
late final ValueNotifier<String?> flightTripTypeNotifier;
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 _isdescriptionFocused = false;
bool _isTripTitleFocused = 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 = [];
// Declare tooltip controller
late SuperTooltip tooltip;
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;
String? temporaryMessage;
//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();
flightTripTypeNotifier = ValueNotifier(null);
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 handleSelectedUser() {
if (widget.selectedPlanData != null) {
setState(() {
String userId = widget.selectedPlanData['user_id'] ?? '';
String userName = widget.selectedPlanData['user_name'] ?? '';
String travellerId = widget.selectedPlanData['traveller_id'] ?? '';
String travellerName = widget.selectedPlanData['traveller_name'] ?? '';
print("TestplanUsrId $userId --- $travellerId");
if (travellerId.isNotEmpty && travellerId != "0") {
print("TestplanUsrId1: $travellerId");
planTravlrId = travellerId;
_selectedOption = "Option 3";
otherUserName = travellerName;
} else if (userId.isNotEmpty && userId != "0") {
print("TestplanUsrId : $userId");
print("TestplanUsrIdSelf : $selfId");
_selectedOption = (selfId != userId) ? "Option 2" : "Option 1";
otherUserName = userName;
}
});
}
}
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'];
flightTripTypeNotifier.value = 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');
}
}
Future<void> getSelectedPlanFor() async {
var userTripId;
// if (!mounted) return;
print("getSelectedPlanFor");
setState(() {
if (selectedplanUserId != null) {
print("Is Not USER ID - $planUsrId ");
if (selectedIstravelUser!) {
planUsrId = "";
planTravlrId = selectedplanUserId;
userTripId = selectedplanUserId;
} else {
planUsrId = selectedplanUserId;
planTravlrId = "";
userTripId = selectedplanUserId;
}
} else {
print("Is USER ID - $planUsrId ");
planUsrId = selfId;
planTravlrId = "";
_selectedOption = "Option 1";
userTripId = selfId;
}
});
final prefs = await SharedPreferences.getInstance();
await prefs.setString('trip_planned_user', userTripId);
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");
// handleSelectedUser();
getSelectedPlanFor();
setTripPlanAction();
handleSelectedUser();
// handleUpdateData();
}
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("CostCenterdropdoen - $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['cost_center_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,
"trip_title": _tripTitleController.text,
"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] = "Required";
// "${entry.key.replaceAll('_', ' ').toUpperCase()} 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,
// );
bool anyServiceSelected = serviceLists.any((list) {
return list != null &&
list.any((entry) => entry['is_active'].toString() == "1");
});
if (!anyServiceSelected) {
validationErrors["services"] = "Please select at least one service";
setState(() {
temporaryMessage = "Please select at least one service";
});
// Clear message after 3 seconds
Future.delayed(Duration(seconds: 3), () {
if (mounted) {
setState(() {
temporaryMessage = null;
});
}
});
}
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": widget.approverId,
"delegation_user_id": widget.delegaterId,
},
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": widget.approverId,
"delegation_user_id": widget.delegaterId,
"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'],
data['approverId'],
data['delegaterId'],
isViewMode: false,
isApprover: true,
);
context.go('/approvallist');
} else {
print("$methodName failed. Status: ${response.statusCode}");
print("Error: ${response.body}");
}
} catch (e) {
print("Error in $methodName: $e");
}
}
void handleSubmit() {
setState(() async {
if (validateForm() && temporaryMessage == null) {
// 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}, "
// " ");
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const SavingLoader(),
);
// postPlanData(planData);
await postPlanData(planData);
// Close loading dialog (ONLY if still mounted)
if (mounted) Navigator.of(context, rootNavigator: true).pop();
}
});
}
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}");
final currentUri =
GoRouterState.of(
context,
).uri.toString(); // ✅ safer than `.location`
print("currentUri - $currentUri");
if (currentUri == "/allTrips/trips") {
context.go('/listAllPlan');
} else if (currentUri == "/createPlan") {
context.go('/listPlan');
} else {
widget.isApprover
? context.go('/approvallist')
: context.go('/listPlan');
}
} 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,
// mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildTripName(isDesktop),
if (isDesktop) Spacer(),
..._buildApproverControls(isDesktop),
// Text(
// widget.isViewMode
// ? "View Plan"
// : (selectedPlanId != null &&
// selectedPlanId!.isNotEmpty
// ? "Update Plan"
// : "New Plan"),
// style: TextStyle(fontSize: 18),
// ),
// Spacer(),
if (statusValue != "")
..._buildPlanPdf(isDesktop),
],
)
: Column(
children: [
Row(
// crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.end,
children: [
..._buildApproverControls(
isDesktop,
),
if (statusValue != "")
..._buildPlanPdf(isDesktop),
],
),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [_buildTripName(isDesktop)],
),
],
),
);
},
),
// 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: InputDecoration(
hintText: "Please enter remarks...",
hintStyle: GoogleFonts.poppins(
fontSize: 13,
color: Colors.grey,
),
border: InputBorder.none,
isDense: true,
),
maxLines: null,
),
),
),
],
),
// if (widget.isApprover || isStatusExpanded)
Divider(thickness: 0.1, color: Colors.blueGrey),
if (widget.isApprover) SizedBox(height: 5),
Text(
"Planning This Trip For*", // Your label
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 6),
isDesktop
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: _buildPlanTrip(isDesktop)),
)
: Row(children: _buildPlanTrip(isDesktop)),
SizedBox(height: 7),
// Text(
// otherUserName ?? userName ?? " ", // Your label
//
// style: GoogleFonts.poppins(
// fontSize: 11,
// fontWeight: FontWeight.w400,
// color: widget.layoutColor,
// ),
// ),
Text.rich(
TextSpan(
text: "Trip Planned User : ", // Static text
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
// color: Color(0xFF575A74), // Default color
),
children: [
TextSpan(
text:
otherUserName ??
userName ??
" ", // Dynamic username
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w400,
color:
widget
.layoutColor, // Change this to any color
// color: Colors.blueAccent, // Change this to any color
),
),
],
),
),
SizedBox(height: 5),
],
),
),
],
),
// 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 (temporaryMessage != null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
// validationErrors["services"]!,
temporaryMessage!,
style: GoogleFonts.poppins(
color: Colors.red,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
if (temporaryMessage != null) SizedBox(height: 10),
Row(
children: [
Expanded(
child: DynamicItinerary(
key: dynamicItineraryKey,
flightScreenKey: flightScreenKey,
tripTypeNotifier: flightTripTypeNotifier,
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'];
// }
final List<Map<String, dynamic>> allTripTypes = [
{"dropdown_key": "1", "dropdown_value": "Domestic"},
{"dropdown_key": "2", "dropdown_value": "International"},
];
List<Map<String, dynamic>> getFilteredTripTypes() {
if (!showDomestic && !showInternational) return [];
return allTripTypes.where((item) {
if (item['dropdown_key'] == "1" && showDomestic) return true;
if (item['dropdown_key'] == "2" && showInternational) return true;
return false;
}).toList();
}
List<Widget> _buildTripRow(bool isMobile) {
List<dynamic> purposeList = apiData?['plan_is_billable'] ?? [];
List<Map<String, dynamic>> tripTypeList = [
{"dropdown_key": "1", "dropdown_value": "Domestic"},
{"dropdown_key": "2", "dropdown_value": "International"},
];
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Trip Type *", // Your label
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false,
isDesktop: widget.isDesktop,
child: SizedBox(
height: 35,
width: double.infinity,
child: DropdownSearch<Map<String, dynamic>>(
popupProps: PopupProps.menu(
showSearchBox: false, // Optionally enable search
fit: FlexFit.loose,
menuProps: const MenuProps(backgroundColor: Colors.white),
itemBuilder: (context, item, isSelected) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
child: Text(
item['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize: 12, // 👈 Smaller font size
color: Colors.black,
),
),
);
},
),
dropdownDecoratorProps: const DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none, // No underline
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
),
),
dropdownBuilder: (context, selectedItem) {
if (selectedItem == null || selectedItem.isEmpty) {
return Text(
"Select Trip Type",
style: GoogleFonts.poppins(
color: Colors.grey,
fontSize: 13,
),
);
}
return Text(
selectedItem['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
),
);
},
selectedItem: tripTypeList.firstWhere(
(item) => item['dropdown_key'] == _selectedTripType,
orElse: () => {},
),
itemAsString: (item) => item['dropdown_value'] ?? '',
items: getFilteredTripTypes(),
// items:
// [
// {"dropdown_key": "1", "dropdown_value": "Domestic"},
// {"dropdown_key": "2", "dropdown_value": "International"},
// ].map<Map<String, dynamic>>((item) {
// return Map<String, dynamic>.from(item);
// }).toList(),
onChanged:
widget.isViewMode
? null
: (Map<String, dynamic>? newItem) {
if (newItem != null) {
setState(() {
_selectedTripType =
newItem['dropdown_key'].toString();
// Notify FlightScreen
flightTripTypeNotifier.value = _selectedTripType;
print(
"flightTripTypeNotifier- ${flightTripTypeNotifier.value}",
);
fetchTrainFlightClass(
int.parse(_selectedTripType!),
);
dynamicItineraryKey.currentState
?.updateSelectedServices();
});
}
},
),
),
),
// 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: GoogleFonts.poppins(fontSize: 12, color: Colors.red),
),
),
],
),
SizedBox(width: 25, height: 5),
// SizedBox(
// width: 25,
// height: 5,
// ),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Is Billable ", // Your label
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
width:
widget.isDesktop
? MediaQuery.of(context).size.width * 0.2
: null,
isFocused: false,
isDesktop: widget.isDesktop,
child: SizedBox(
height: 35,
width: double.infinity,
child: DropdownSearch<Map<String, dynamic>>(
popupProps: PopupProps.menu(
showSearchBox: false,
fit: FlexFit.loose,
menuProps: const MenuProps(backgroundColor: Colors.white),
itemBuilder: (context, item, isSelected) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
child: Text(
item['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
),
),
);
},
),
dropdownDecoratorProps: const DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
),
),
selectedItem: purposeList
.cast<Map<String, dynamic>>()
.firstWhere(
(item) => item['dropdown_key'] == _selectedIsBillable,
orElse: () => {},
),
dropdownButtonProps: const DropdownButtonProps(
icon: Icon(Icons.arrow_drop_down),
),
// itemAsString: (item) => item['dropdown_value'] ?? '',
// items: purposeList.cast<Map<String, dynamic>>(),
dropdownBuilder: (context, selectedItem) {
if (selectedItem == null || selectedItem.isEmpty) {
return Text(
"Select Billable", // fallback text
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.grey,
),
);
}
return Text(
selectedItem['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
),
);
},
items:
purposeList.map<Map<String, dynamic>>((item) {
return Map<String, dynamic>.from(
item,
); // Ensuring each item is properly cast
}).toList(),
onChanged:
widget.isViewMode
? null
: (Map<String, dynamic>? newItem) {
if (newItem != null) {
setState(() {
_selectedIsBillable =
newItem['dropdown_key'].toString();
});
}
},
),
),
),
// 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(),
// ),
],
),
];
}
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: GoogleFonts.poppins(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: GoogleFonts.poppins(
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),
hintStyle: GoogleFonts.poppins(
fontSize: 13,
color: Colors.grey,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
),
),
style: GoogleFonts.poppins(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: GoogleFonts.poppins(
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),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
),
),
dropdownBuilder: (context, selectedItem) {
if (selectedItem == null || selectedItem.isEmpty) {
return Text(
"Select Purpose", // fallback text
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.grey,
),
);
}
return Text(
selectedItem ?? '',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
),
);
},
// 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(),
// ),
// ),
),
if (validationErrors["cost_center_id"] != null)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
validationErrors["cost_center_id"]!,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.red),
),
),
],
),
SizedBox(width: 25, height: 5),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Purpose of Trip *", // Your label
style: GoogleFonts.poppins(
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: GoogleFonts.poppins(
fontSize: 13,
color: Colors.grey,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
),
),
style: GoogleFonts.poppins(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: GoogleFonts.poppins(
fontSize: 13,
), // 👈 Set your desired text size here
),
),
),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
border: InputBorder.none,
),
),
onChanged:
widget.isViewMode
? null
: (Map<String, dynamic>? newValue) {
setState(() {
selectedPurpose =
newValue?['dropdown_key'].toString();
});
print("selectedPurpose - $selectedPurpose");
},
dropdownBuilder: (context, selectedItem) {
if (selectedItem == null || selectedItem.isEmpty) {
return Text(
"Select Purpose", // fallback text
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.grey,
),
);
}
return Text(
selectedItem['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize:
12, // 👈 Small font size for selected item
color: Colors.black,
),
);
},
// dropdownBuilder: (context, selectedItem) => Align(
// alignment: Alignment.centerLeft,
// child: Text(
// selectedItem?['dropdown_value'] ?? '',
// style: TextStyle(fontSize: 12),
// ),
// ),
),
),
),
if (validationErrors["purpose_of_travel"] != null)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
validationErrors["purpose_of_travel"]!,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.red),
),
),
// 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: GoogleFonts.poppins(
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: GoogleFonts.poppins(
fontSize: 13,
color: Colors.grey,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
),
),
style: GoogleFonts.poppins(
fontSize:
13, // 👈 Small font size for selected item
),
),
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: GoogleFonts.poppins(
fontSize:
13, // 👈 Small font size for selected item
), // 👈 Set your desired text size here
),
),
),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
border: InputBorder.none,
),
),
onChanged:
widget.isViewMode
? null
: (Map<String, dynamic>? newValue) {
setState(() {
selectedFuncDept =
newValue?['dropdown_key'].toString();
});
print("selectedFuncDept - $selectedFuncDept");
},
dropdownBuilder: (context, selectedItem) {
if (selectedItem == null || selectedItem.isEmpty) {
return const Text(
"Select Purpose", // fallback text
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
);
}
return Text(
selectedItem['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize:
12, // 👈 Small font size for selected item
color: Colors.black,
),
);
},
// dropdownBuilder: (context, selectedItem) => Align(
// alignment: Alignment.centerLeft,
// child: Text(
// selectedItem?['dropdown_value'] ?? '',
// style: TextStyle(fontSize: 12),
// ),
// ),
),
),
),
if (validationErrors["functional_department"] != null)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
validationErrors["functional_department"]!,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.red),
),
),
],
),
];
}
List<Widget> _buildPlanTrip(bool isDesktop) {
List<Map<String, String>> options = [
{"title": "Self", "value": "Option 1"},
{"title": "Other Employee", "value": "Option 2"},
{"title": "Others (Non Employee)", "value": "Option 3"},
];
return [
CustomTextFieldWrapper(
isFocused: false,
isDesktop: isDesktop,
layoutColor: widget.layoutColor,
child: SizedBox(
height: 35,
width: double.infinity,
child: DropdownSearch<String>(
selectedItem:
options.firstWhere(
(opt) => opt['value'] == _selectedOption,
)['title'],
enabled: !widget.isViewMode,
items: options.map((opt) => opt['title']!).toList(),
popupProps: PopupProps.menu(
showSearchBox: false,
menuProps: const MenuProps(backgroundColor: Colors.white),
constraints: BoxConstraints(maxHeight: 100),
itemBuilder:
(context, item, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 6.0,
),
child: Text(item, style: GoogleFonts.poppins(fontSize: 13)),
),
),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
),
),
dropdownBuilder: (context, selectedItem) {
return Text(
selectedItem ?? "Select Purpose",
style: GoogleFonts.poppins(
fontSize: 12,
color: selectedItem == null ? Colors.grey : Colors.black,
),
);
},
onChanged: (String? newTitle) {
if (newTitle == null) return;
final selected = options.firstWhere(
(opt) => opt["title"] == newTitle,
);
setState(() {
_selectedOption = selected["value"]!;
if (_selectedOption == "Option 2" ||
_selectedOption == "Option 3") {
_showInputDialog(selected["title"]!);
} else if (_selectedOption == "Option 1") {
otherUserName = userName;
}
});
},
),
),
),
];
}
List<Widget> _buildPlanTrip1(bool isDesktop) {
List<Map<String, String>> options = [
{"title": "Self", "value": "Option 1"},
{"title": "Other Employee", "value": "Option 2"},
{"title": "Others (Non Employee)", "value": "Option 3"},
];
print(" layoutColor: ${widget.layoutColor}");
return options.map((option) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, 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(10),
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: GoogleFonts.poppins(
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: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
// CustomTextFieldWrapper(
// isFocused: false,
// // isFocused: _isdescriptionFocused,
// isDesktop: widget.isDesktop,
// width: isDesktop
// ? MediaQuery.of(context).size.width * 0.4
// : 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: false,
isFocused: _isdescriptionFocused,
// width: isDesktop
// ? MediaQuery.of(context).size.width * 0.38
// : MediaQuery.of(context).size.width * 0.85,
isDesktop: widget.isDesktop,
child: SizedBox(
height: 55,
child: TextField(
focusNode: _descriptionFocusNode,
controller: _descriptionController,
maxLines: 2,
keyboardType: TextInputType.multiline,
style: TextStyle(fontSize: 12),
enabled: !widget.isViewMode,
decoration: InputDecoration(
labelText: "Description",
labelStyle: GoogleFonts.poppins(
fontSize: 12,
color: Colors.grey,
),
// 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 _buildTripName(bool isDesktop) {
return SizedBox(
width: isDesktop ? MediaQuery.of(context).size.width * 0.2 : 180,
height: 35,
child: TextField(
focusNode: _tripTitleFocusNode,
controller: _tripTitleController,
// style: TextStyle(fontSize: 12),
style: GoogleFonts.poppins(
fontSize: isDesktop ? 16 : 14,
fontWeight: FontWeight.w600,
color: Colors.black,
),
enabled: !widget.isViewMode,
decoration: InputDecoration(
labelText: "Trip Name *",
labelStyle: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
errorText: validationErrors["trip_title"], // <--- add this line
errorStyle: GoogleFonts.poppins(fontSize: 12, color: Colors.red),
),
),
);
}
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 [
// 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;
//
// });
// },
onTap: () async {
final result = await showApprovalDialog(
context,
widget.layoutColor ?? Colors.grey,
);
if (result == 'approved') {
// User approved
setState(() {
isApproverApproved = true;
isApproverRejected = false;
statusValue = "Approved";
});
callApproveAPI(selectedPlanId!, planData['user_id']);
} else if (result is String) {
// User rejected with remarks
_remarksController.text = result;
setState(() {
isApproverApproved = false;
isApproverRejected = true;
statusValue = "Rejected";
});
callRejectAPI(selectedPlanId!, planData['user_id'], result);
}
},
child: Icon(
Icons.edit,
size: 18,
color: isShowApprovalAction ? Colors.green : Colors.grey,
),
),
// const SizedBox(height: 10, width: 4),
// 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),
// ),
// ),
// ),
// ],
// ),
const SizedBox(height: 10, width: 10),
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: 6.0,
bottom: 6.0,
left: 15,
right: 15,
),
child: Text(
statusText,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w400,
color: getStatusColor(
isApproverApproved
? "Approved"
: isApproverRejected
? "Rejected"
: (statusValue ?? ""),
),
),
// style: TextStyle(
// fontFamily: "Roboto",
// fontWeight: FontWeight.w400,
// fontSize: 12,
//
// ),
),
),
],
),
),
),
// Tooltip
if (isStatusExpanded)
Positioned(
right: 0,
top: 45, // 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),
// ],
// ],
// ],
// ),
// ),
// ),
],
),
const SizedBox(height: 10, width: 10),
];
}
List<Widget> _buildPlanPdf(isDesktop) {
return [
Column(
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF114D8B),
foregroundColor: Colors.white,
disabledBackgroundColor: Color(0xFF114D8B),
disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Color(0xFF114D8B), width: 2),
),
padding:
isDesktop
? EdgeInsets.only(
top: 6.0,
bottom: 6.0,
left: 15,
right: 15,
)
: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
),
onPressed: () {
getPdfDownload();
},
child: Row(
mainAxisSize: MainAxisSize.min, // Ensures content fits nicely
children: [
if (isDesktop)
Text(
"Download PDF",
style: GoogleFonts.poppins(
fontSize: isDesktop ? 13 : 11,
// fontWeight:
// FontWeight.w500,
),
// style: TextStyle(fontSize: isDesktop ? 13 : 11),
),
if (isDesktop)
SizedBox(width: 8), // spacing between icon and text
Icon(Icons.download_rounded, size: 15, color: Colors.white),
],
),
),
// 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();
},
onClose: () {
print("Choosede Clsoes");
fetchUserDetails();
},
layoutColorForUser: widget.layoutColor!,
);
},
);
}
}