user mangement client chnages
This commit is contained in:
parent
154f18a59b
commit
b1d19aec84
@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../routes/mainLayout.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../utils/pagination.dart';
|
||||
@ -62,7 +63,6 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
// futurePlans = fetchPlans();
|
||||
}
|
||||
|
||||
@ -70,16 +70,20 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
print("allPlans before filtering: $allPlans");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredPlans = allPlans.where((plan) {
|
||||
filteredPlans =
|
||||
allPlans.where((plan) {
|
||||
return (plan.planId?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.employeeCode?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.employeeCode?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(plan.tripTitle?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.userName?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.travellerName?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.travellerName?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(plan.tripType?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.createdOn?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.statusValue?.toLowerCase().contains(lowerQuery) ?? false);
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredPlans: $filteredPlans");
|
||||
}
|
||||
@ -89,11 +93,13 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -202,6 +208,7 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
// List<dynamic> plansJson = [];
|
||||
List<dynamic> plansJson = data['data'];
|
||||
return plansJson.map((json) => Plan.fromJson(json)).toList();
|
||||
} else {
|
||||
@ -246,8 +253,10 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
}
|
||||
|
||||
void deletePlan(String planId) async {
|
||||
bool confirmed =
|
||||
await apiService.showCancelConfirmationDialog(context, layoutColor);
|
||||
bool confirmed = await apiService.showCancelConfirmationDialog(
|
||||
context,
|
||||
layoutColor,
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
@ -264,8 +273,10 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -276,23 +287,27 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
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: buildGroupListLayout(isDesktop))
|
||||
Expanded(child: buildGroupListLayout(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupListLayout(bool isDesktop) {
|
||||
@ -323,8 +338,9 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
String _formatDate(String rawDate) {
|
||||
try {
|
||||
final dateTime = DateTime.parse(rawDate);
|
||||
return DateFormat('dd, MMM yyyy HH:mm')
|
||||
.format(dateTime); // 24-hour format
|
||||
return DateFormat(
|
||||
'dd, MMM yyyy HH:mm',
|
||||
).format(dateTime); // 24-hour format
|
||||
} catch (e) {
|
||||
return rawDate; // fallback if parsing fails
|
||||
}
|
||||
@ -333,11 +349,13 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
return Container(
|
||||
margin: isDesktop ? EdgeInsets.all(10.0) : null,
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 15, left: 20, right: 20),
|
||||
height: isDesktop
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
decoration: BoxDecoration(
|
||||
border: isDesktop
|
||||
border:
|
||||
isDesktop
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: Colors.white,
|
||||
@ -383,9 +401,7 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: 1,
|
||||
),
|
||||
SizedBox(width: 1),
|
||||
Spacer(),
|
||||
if (isDesktop)
|
||||
Container(
|
||||
@ -396,8 +412,10 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
onChanged: filterPlans,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search...",
|
||||
hintStyle:
|
||||
TextStyle(fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -409,22 +427,23 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
|
||||
// SizedBox(width: 16),
|
||||
|
||||
Spacer(),
|
||||
// ElevatedButton(
|
||||
// style: ElevatedButton.styleFrom(
|
||||
@ -522,8 +541,10 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
onChanged: filterPlans,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search...",
|
||||
hintStyle:
|
||||
TextStyle(fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -535,17 +556,19 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -558,6 +581,8 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
} else if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty) {
|
||||
final adjHgt = MediaQuery.of(context).size.height;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@ -575,21 +600,15 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
// color: Colors.redAccent),
|
||||
//
|
||||
// ),
|
||||
const SizedBox(height: 15),
|
||||
SizedBox(height: adjHgt / 4),
|
||||
Text(
|
||||
"No Trips",
|
||||
" No Trips Found",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black54,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Please Create Trip",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -605,10 +624,13 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
List<Plan> plans =
|
||||
searchController.text.isEmpty ? allPlans : filteredPlans;
|
||||
|
||||
plans.sort((a, b) =>
|
||||
int.parse(b.planId).compareTo(int.parse(a.planId)));
|
||||
plans.sort(
|
||||
(a, b) =>
|
||||
int.parse(b.planId).compareTo(int.parse(a.planId)),
|
||||
);
|
||||
|
||||
List<Plan> paginatedPlans = plans
|
||||
List<Plan> paginatedPlans =
|
||||
plans
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
@ -624,112 +646,169 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5, color: Colors.grey.shade200),
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Trip ID',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Trip Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Emp Code',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Traveller',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Trip Type',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Created On',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: paginatedPlans.map((plan) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(plan.planId,
|
||||
rows:
|
||||
paginatedPlans.map((plan) {
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
plan.planId,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(plan.tripTitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
plan.tripTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
DataCell(Text(plan.employeeCode ?? " - ",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
plan.employeeCode ?? " - ",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
plan.userName.isNotEmpty
|
||||
? plan.userName
|
||||
: plan.travellerName,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(plan.tripType,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
plan.tripType,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(_formatDate(plan.createdOn),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
_formatDate(plan.createdOn),
|
||||
// plan.createdOn,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
width: double
|
||||
width:
|
||||
double
|
||||
.infinity, // Set your desired fixed size (equal width and height)
|
||||
height: 25,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: getStatusColor(plan.statusValue),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: getStatusColor(
|
||||
plan.statusValue,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
10,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
plan.statusValue,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color:
|
||||
getStatusTextColor(plan.statusValue),
|
||||
color: getStatusTextColor(
|
||||
plan.statusValue,
|
||||
),
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: FontWeight.w400,
|
||||
@ -749,80 +828,132 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
color: Color(0xFF475569),
|
||||
size: 14,
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
CustomPopupMenuEntry(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 8),
|
||||
padding:
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
MainAxisAlignment
|
||||
.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove_red_eye,
|
||||
color: Color(0xFF475569),
|
||||
size: 18),
|
||||
Icons
|
||||
.remove_red_eye,
|
||||
color: Color(
|
||||
0xFF475569,
|
||||
),
|
||||
size: 18,
|
||||
),
|
||||
tooltip:
|
||||
'View The Trip Details',
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context); // Close popup manually
|
||||
context,
|
||||
); // Close popup manually
|
||||
ApiService.viewPlan(
|
||||
context, plan.planId,
|
||||
isViewMode: true);
|
||||
context,
|
||||
plan.planId,
|
||||
isViewMode:
|
||||
true,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
tooltip:
|
||||
'Edit The Trip Details',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
ApiService.viewPlan(
|
||||
context, plan.planId,
|
||||
isViewMode: false);
|
||||
context,
|
||||
plan.planId,
|
||||
isViewMode:
|
||||
false,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 18),
|
||||
Icons
|
||||
.cancel_rounded,
|
||||
size: 18,
|
||||
),
|
||||
tooltip:
|
||||
'Cancellation The Trip Details',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
deletePlan(plan.planId);
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
deletePlan(
|
||||
plan.planId,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.download,
|
||||
color: Color(0xFF114D8B),
|
||||
size: 18),
|
||||
icon: Icon(
|
||||
Icons.download,
|
||||
color: Color(
|
||||
0xFF114D8B,
|
||||
),
|
||||
size: 18,
|
||||
),
|
||||
tooltip:
|
||||
'Download The Trip Details',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
apiService.getPdfDownload(
|
||||
plan.planId);
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
apiService
|
||||
.getPdfDownload(
|
||||
plan.planId,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.comment,
|
||||
color: Color(0xFF475569),
|
||||
color: Color(
|
||||
0xFF475569,
|
||||
),
|
||||
size: 11,
|
||||
),
|
||||
tooltip:
|
||||
'Trip Comments',
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
CommentModalList(
|
||||
context:
|
||||
context,
|
||||
builder:
|
||||
(
|
||||
context,
|
||||
) => CommentModalList(
|
||||
// planId: plan.planId,
|
||||
planId: plan
|
||||
.planId
|
||||
planId:
|
||||
plan.planId
|
||||
.toString(),
|
||||
layoutColorForUser:
|
||||
layoutColor!,
|
||||
role: "Admin"),
|
||||
role:
|
||||
"Admin",
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -832,7 +963,8 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@ -846,8 +978,10 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
final plan = paginatedPlans[index];
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin:
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -872,18 +1006,23 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getStatusColor(
|
||||
plan.statusValue),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
plan.statusValue,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
plan.statusValue,
|
||||
style: TextStyle(
|
||||
color: getStatusTextColor(
|
||||
plan.statusValue),
|
||||
plan.statusValue,
|
||||
),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@ -912,11 +1051,14 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(' ${plan.tripTitle}',
|
||||
Text(
|
||||
' ${plan.tripTitle}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: FontWeight.bold)),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -931,24 +1073,28 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(' ${plan.tripType}',
|
||||
Text(
|
||||
' ${plan.tripType}',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.black87,
|
||||
fontFamily: "Inter",
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${_formatDate(plan.createdOn)}',
|
||||
Text(
|
||||
'${_formatDate(plan.createdOn)}',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.black87,
|
||||
fontFamily: "Inter",
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -968,7 +1114,8 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: "Inter",
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -982,7 +1129,8 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: "Inter",
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -1002,14 +1150,17 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredPlans.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.grey),
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@ -1022,7 +1173,9 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.grey),
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(paginatedPlans)),
|
||||
@ -1055,7 +1208,7 @@ class _ListAllPlansState extends State<ListAllPlans> {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -69,12 +69,15 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
print("allPlans before filtering: $allPlans");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredPlans = allPlans.where((plan) {
|
||||
filteredPlans =
|
||||
allPlans.where((plan) {
|
||||
return (plan.planId?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.employeeCode?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.employeeCode?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(plan.tripTitle?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.userName?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.travellerName?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.travellerName?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(plan.tripType?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.createdOn?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(plan.statusValue?.toLowerCase().contains(lowerQuery) ?? false);
|
||||
@ -88,11 +91,13 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -195,7 +200,8 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
List<dynamic> plansJson = data['data'];
|
||||
List<dynamic> plansJson = [];
|
||||
// List<dynamic> plansJson = data['data'];
|
||||
return plansJson.map((json) => Plan.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to load plans');
|
||||
@ -239,8 +245,10 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
}
|
||||
|
||||
void deletePlan(String planId) async {
|
||||
bool confirmed =
|
||||
await apiService.showCancelConfirmationDialog(context, layoutColor);
|
||||
bool confirmed = await apiService.showCancelConfirmationDialog(
|
||||
context,
|
||||
layoutColor,
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
@ -257,8 +265,10 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -267,23 +277,27 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
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: buildGroupListLayout(isDesktop))
|
||||
Expanded(child: buildGroupListLayout(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupListLayout(bool isDesktop) {
|
||||
@ -313,8 +327,9 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
String _formatDate(String rawDate) {
|
||||
try {
|
||||
final dateTime = DateTime.parse(rawDate);
|
||||
return DateFormat('dd, MMM yyyy HH:mm')
|
||||
.format(dateTime); // 24-hour format
|
||||
return DateFormat(
|
||||
'dd, MMM yyyy HH:mm',
|
||||
).format(dateTime); // 24-hour format
|
||||
} catch (e) {
|
||||
return rawDate; // fallback if parsing fails
|
||||
}
|
||||
@ -323,11 +338,13 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
return Container(
|
||||
margin: isDesktop ? EdgeInsets.all(10.0) : null,
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 15, left: 20, right: 20),
|
||||
height: isDesktop
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
decoration: BoxDecoration(
|
||||
border: isDesktop
|
||||
border:
|
||||
isDesktop
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: Colors.white,
|
||||
@ -382,8 +399,10 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
onChanged: filterPlans,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search...",
|
||||
hintStyle:
|
||||
TextStyle(fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -395,23 +414,23 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
style: TextStyle(fontSize: 12, fontFamily: "Inter"),
|
||||
),
|
||||
),
|
||||
|
||||
// SizedBox(width: 16),
|
||||
|
||||
Spacer(),
|
||||
// ElevatedButton(
|
||||
// style: ElevatedButton.styleFrom(
|
||||
@ -447,8 +466,10 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
onChanged: filterPlans,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search...",
|
||||
hintStyle:
|
||||
TextStyle(fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -460,17 +481,19 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -479,6 +502,8 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
FutureBuilder<List<Plan>>(
|
||||
future: futurePlans,
|
||||
builder: (context, snapshot) {
|
||||
final adjHgt = MediaQuery.of(context).size.height;
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError ||
|
||||
@ -490,7 +515,7 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 20),
|
||||
// SizedBox(height: 20),
|
||||
// Icon(Icons.error_outline,
|
||||
// color: Colors.redAccent, size: 60),
|
||||
// SizedBox(height: 1),
|
||||
@ -499,21 +524,23 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
// fontSize: 22,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// color: Colors.redAccent)),
|
||||
SizedBox(height: 20),
|
||||
isDesktop
|
||||
? SizedBox(
|
||||
width:
|
||||
MediaQuery.of(context).size.width * 5.5,
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
SizedBox(height: adjHgt / 4),
|
||||
|
||||
Text("No Trips Pending For Your Approvals",
|
||||
Text(
|
||||
" No Trips Found",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54)),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
// Text("Please Create Trip",
|
||||
// textAlign: TextAlign.center,
|
||||
@ -536,10 +563,13 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
List<Plan> plans =
|
||||
searchController.text.isEmpty ? allPlans : filteredPlans;
|
||||
|
||||
plans.sort((a, b) =>
|
||||
int.parse(b.planId).compareTo(int.parse(a.planId)));
|
||||
plans.sort(
|
||||
(a, b) =>
|
||||
int.parse(b.planId).compareTo(int.parse(a.planId)),
|
||||
);
|
||||
|
||||
List<Plan> paginatedPlans = plans
|
||||
List<Plan> paginatedPlans =
|
||||
plans
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
@ -555,117 +585,174 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5, color: Colors.grey.shade200),
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Trip ID',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Trip Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Emp Code',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Traveller',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Trip Type',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Created On',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: paginatedPlans.map((plan) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(plan.planId,
|
||||
rows:
|
||||
paginatedPlans.map((plan) {
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
plan.planId,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
DataCell(Text(plan.tripTitle,
|
||||
DataCell(
|
||||
Text(
|
||||
plan.tripTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
DataCell(Text(plan.employeeCode ?? " - ",
|
||||
DataCell(
|
||||
Text(
|
||||
plan.employeeCode ?? " - ",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
plan.userName.isNotEmpty
|
||||
? plan.userName
|
||||
: plan.travellerName,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(plan.tripType,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
plan.tripType,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(_formatDate(plan.createdOn),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
_formatDate(plan.createdOn),
|
||||
// plan.createdOn,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
// width:
|
||||
// 150, // Set your desired fixed size (equal width and height)
|
||||
width: double
|
||||
width:
|
||||
double
|
||||
.infinity, // Set your desired fixed size (equal width and height)
|
||||
height: 25,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: getStatusColor(plan.statusValue),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: getStatusColor(
|
||||
plan.statusValue,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
10,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
plan.statusValue,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color:
|
||||
getStatusTextColor(plan.statusValue),
|
||||
color: getStatusTextColor(
|
||||
plan.statusValue,
|
||||
),
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: FontWeight.w400,
|
||||
@ -681,83 +768,120 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.zero,
|
||||
offset: Offset(0, 30),
|
||||
icon: Icon(Icons.more_vert,
|
||||
color: Color(0xFF475569)),
|
||||
itemBuilder: (context) => [
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
CustomPopupMenuEntry(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 8),
|
||||
padding:
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
MainAxisAlignment
|
||||
.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove_red_eye,
|
||||
color: Color(0xFF475569),
|
||||
size: 16),
|
||||
Icons
|
||||
.remove_red_eye,
|
||||
color: Color(
|
||||
0xFF475569,
|
||||
),
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context); // Close popup manually
|
||||
context,
|
||||
); // Close popup manually
|
||||
ApiService.viewPlan(
|
||||
context, plan.planId,
|
||||
isViewMode: true);
|
||||
context,
|
||||
plan.planId,
|
||||
isViewMode:
|
||||
true,
|
||||
);
|
||||
},
|
||||
),
|
||||
// IconButton(
|
||||
// icon: Image.asset(
|
||||
// 'assets/images/IconsImg/edit.png',
|
||||
// width: 20,
|
||||
// height: 15),
|
||||
// onPressed: () {
|
||||
// Navigator.pop(context);
|
||||
// ApiService.viewPlan(
|
||||
// context, plan.planId,
|
||||
// isViewMode: false);
|
||||
// },
|
||||
// ),
|
||||
IconButton(
|
||||
icon: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
icon: Icon(
|
||||
Icons
|
||||
.cancel_rounded,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
ApiService.viewPlan(
|
||||
context, plan.planId,
|
||||
isViewMode: false);
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
deletePlan(
|
||||
plan.planId,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 18),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
deletePlan(plan.planId);
|
||||
},
|
||||
Icons.download,
|
||||
color: Color(
|
||||
0xFF114D8B,
|
||||
),
|
||||
size: 18,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.download,
|
||||
color: Color(0xFF114D8B),
|
||||
size: 18),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
apiService.getPdfDownload(
|
||||
plan.planId);
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
apiService
|
||||
.getPdfDownload(
|
||||
plan.planId,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.comment,
|
||||
color: Color(0xFF475569),
|
||||
color: Color(
|
||||
0xFF475569,
|
||||
),
|
||||
size: 11,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
CommentModal(
|
||||
context:
|
||||
context,
|
||||
builder:
|
||||
(
|
||||
context,
|
||||
) => CommentModal(
|
||||
// planId: plan.planId,
|
||||
planId: plan
|
||||
.planId
|
||||
planId:
|
||||
plan.planId
|
||||
.toString(),
|
||||
layoutColorForUser:
|
||||
layoutColor!,
|
||||
role:
|
||||
"Travel Agent"),
|
||||
"Travel Agent",
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -807,7 +931,8 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
// apiService.getPdfDownload(plan.planId);
|
||||
// }),
|
||||
// ])),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@ -821,8 +946,10 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
final plan = paginatedPlans[index];
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin:
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -839,7 +966,9 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getStatusColor(plan.statusValue),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@ -848,7 +977,8 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
plan.statusValue,
|
||||
style: TextStyle(
|
||||
color: getStatusTextColor(
|
||||
plan.statusValue),
|
||||
plan.statusValue,
|
||||
),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@ -871,11 +1001,14 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(' ${plan.tripTitle}',
|
||||
Text(
|
||||
' ${plan.tripTitle}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: FontWeight.bold)),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -890,24 +1023,28 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(' ${plan.tripType}',
|
||||
Text(
|
||||
' ${plan.tripType}',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.black87,
|
||||
fontFamily: "Inter",
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${_formatDate(plan.createdOn)}',
|
||||
Text(
|
||||
'${_formatDate(plan.createdOn)}',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.black87,
|
||||
fontFamily: "Inter",
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -927,7 +1064,8 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: "Inter",
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -941,7 +1079,8 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: "Inter",
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -963,14 +1102,17 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredPlans.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.grey),
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@ -983,7 +1125,9 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.grey),
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(paginatedPlans)),
|
||||
@ -1016,7 +1160,7 @@ class _TravelAgentListPlansState extends State<TravelAgentListPlans> {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<dynamic> showApprovalDialog(
|
||||
BuildContext context, Color layoutColor) async {
|
||||
BuildContext context,
|
||||
Color layoutColor,
|
||||
) async {
|
||||
String selectedAction = ""; // "", "accept", "reject"
|
||||
String remarks = "";
|
||||
|
||||
@ -24,13 +26,12 @@ Future<dynamic> showApprovalDialog(
|
||||
Text(
|
||||
"To Approve or Reject Trip",
|
||||
style: TextStyle(
|
||||
fontFamily: "Inter", fontWeight: FontWeight.w500),
|
||||
fontFamily: "Inter",
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 15,
|
||||
),
|
||||
icon: const Icon(Icons.close, size: 15),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, null); // Close the dialog
|
||||
},
|
||||
@ -45,10 +46,12 @@ Future<dynamic> showApprovalDialog(
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: selectedAction == "accept"
|
||||
? layoutColor
|
||||
backgroundColor:
|
||||
selectedAction == "accept"
|
||||
? Colors.green
|
||||
: Colors.grey.shade200,
|
||||
foregroundColor: selectedAction == "accept"
|
||||
foregroundColor:
|
||||
selectedAction == "accept"
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
shape: RoundedRectangleBorder(
|
||||
@ -68,10 +71,12 @@ Future<dynamic> showApprovalDialog(
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: selectedAction == "reject"
|
||||
backgroundColor:
|
||||
selectedAction == "reject"
|
||||
? Colors.redAccent
|
||||
: Colors.grey.shade200,
|
||||
foregroundColor: selectedAction == "reject"
|
||||
foregroundColor:
|
||||
selectedAction == "reject"
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
shape: RoundedRectangleBorder(
|
||||
@ -122,12 +127,16 @@ Future<dynamic> showApprovalDialog(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.blueGrey, width: 0.5),
|
||||
color: Colors.blueGrey,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.blueGrey, width: 0.5),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -135,7 +144,7 @@ Future<dynamic> showApprovalDialog(
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
@ -187,14 +196,12 @@ Future<dynamic> showApprovalDialog(
|
||||
Future<bool?> showApproveDialog1(BuildContext context, Color layoutColor) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
title: const Text(
|
||||
"Confirm Approval",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: const Text("Are you sure you want to approve this plan?"),
|
||||
actions: [
|
||||
@ -231,14 +238,17 @@ Future<bool?> showApproveDialog1(BuildContext context, Color layoutColor) {
|
||||
|
||||
/// Show confirm dialog for rejection with remarks input
|
||||
Future<String?> showRejectDialog1(
|
||||
BuildContext context, Color layoutColor) async {
|
||||
BuildContext context,
|
||||
Color layoutColor,
|
||||
) async {
|
||||
String remarks = "";
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
builder:
|
||||
(context, setState) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.all(36),
|
||||
// title: const Text("Confirm Rejection"),
|
||||
@ -260,13 +270,20 @@ Future<String?> showRejectDialog1(
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 10, // 👈 Set your desired font size here
|
||||
color: Colors.grey,
|
||||
fontFamily: "Inter", // optional if you want consistent font
|
||||
fontFamily:
|
||||
"Inter", // optional if you want consistent font
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.blueGrey, width: 0.5),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.blueGrey, width: 0.5),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blueGrey,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey, width: 1),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,8 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../services/apiService.dart';
|
||||
|
||||
class LoginWidget extends StatefulWidget {
|
||||
final bool isDesktop;
|
||||
final bool isTablet;
|
||||
@ -25,6 +27,9 @@ class LoginWidget extends StatefulWidget {
|
||||
enum LoginStep { login, forgotEmail, otpReset }
|
||||
|
||||
class _LoginWidgetState extends State<LoginWidget> {
|
||||
final ApiService apiService = ApiService();
|
||||
bool _moved = false;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
@ -39,6 +44,17 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
// 🔹 Login Step Enum and State Variable
|
||||
LoginStep _loginStep = LoginStep.login;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_moved = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
@ -54,15 +70,18 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
final parts = token.split('.');
|
||||
if (parts.length != 3) throw Exception('Invalid token format');
|
||||
|
||||
final payload = json
|
||||
.decode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
|
||||
final payload = json.decode(
|
||||
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
|
||||
);
|
||||
|
||||
final userData = payload['data'];
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('auth_token', token);
|
||||
await prefs.setString(
|
||||
'user_data', jsonEncode(userData)); // Store full user data
|
||||
'user_data',
|
||||
jsonEncode(userData),
|
||||
); // Store full user data
|
||||
|
||||
if (userData != null) {
|
||||
final pref = await SharedPreferences.getInstance();
|
||||
@ -75,6 +94,8 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
print("userData11 - ${userData['role']}");
|
||||
print("userData12 - $userRole");
|
||||
}
|
||||
|
||||
await apiService.getOrganizationData();
|
||||
} catch (e) {
|
||||
print('Error decoding token: $e');
|
||||
}
|
||||
@ -89,7 +110,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
Uri.parse(url),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'email': _emailController.text.trim(),
|
||||
@ -135,6 +156,8 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
// Fluttertoast.showToast(
|
||||
// msg: "Login Failed: $errorMessage",
|
||||
// toastLength: Toast.LENGTH_LONG,
|
||||
@ -145,9 +168,9 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
// );
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error: $e")),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Error: $e")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -175,19 +198,27 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
// _clearAllFields();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("OTP sent to your email")),
|
||||
SnackBar(
|
||||
content: Text("OTP sent to your email"),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print(response);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text("${jsonDecode(response.body)['messages']['error']}")),
|
||||
content: Text(
|
||||
"${jsonDecode(response.body)['messages']['error']}",
|
||||
),
|
||||
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text("Error: $e")));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error: $e"), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} else if (!_isForgotPassword && _showOtpResetFields) {
|
||||
print('22');
|
||||
@ -214,20 +245,32 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
_clearAllFields();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Password reset successfully")),
|
||||
SnackBar(
|
||||
content: Text("Password reset successfully"),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print('23');
|
||||
print('otp wrong');
|
||||
final responseBody = json.decode(response.body);
|
||||
final errorMessage =
|
||||
responseBody['messages']?['error'] ?? 'An unknown error occurred';
|
||||
print(errorMessage);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Reset failed: ${jsonDecode(response.body)['message']}")),
|
||||
"Reset failed: $errorMessage",
|
||||
// "Reset failed: ${jsonDecode(response.body)['message']}",
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text("Error: $e")));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error: $e"), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Login flow
|
||||
@ -255,15 +298,15 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
/// Layout
|
||||
Widget build(BuildContext context) {
|
||||
double formWidth = widget.isTablet ? 400 : 300;
|
||||
|
||||
return Container(
|
||||
// color: Color(0xFF114D8B),
|
||||
color: Color(0xFFf5f5f5),
|
||||
color: Colors.white,
|
||||
|
||||
// color: Color(0xFFf5f5f5),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
children: [
|
||||
@ -272,21 +315,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
flex: 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// image: DecorationImage(
|
||||
// image: AssetImage(
|
||||
// 'assets/images/login/login_travel.png',
|
||||
// ),
|
||||
// // fit: BoxFit.fill
|
||||
// fit: BoxFit.contain
|
||||
// // fit: BoxFit.cover, // or BoxFit.contain, BoxFit.fill, etc.
|
||||
// ),
|
||||
color: Color(0xFFE6F0FA),
|
||||
// color: Color(0xFFF0F7FF),
|
||||
// color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(250), // Rounded top-left corner
|
||||
bottomRight:
|
||||
Radius.circular(250), // Rounded bottom-left corner
|
||||
bottomRight: Radius.circular(
|
||||
250,
|
||||
), // Rounded bottom-left corner
|
||||
),
|
||||
),
|
||||
// child: Padding(
|
||||
@ -305,11 +341,15 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/login/logoNew.jpg',
|
||||
// Image.asset(
|
||||
// 'assets/images/login/logoNew.jpg',
|
||||
// width: 200, // Optional: control size
|
||||
// height: 100,
|
||||
// fit: BoxFit.contain,
|
||||
// ),
|
||||
Container(
|
||||
width: 200, // Optional: control size
|
||||
height: 100,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
@ -320,9 +360,10 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
// width: 200, // Optional: control size
|
||||
// height: 100,
|
||||
fit: BoxFit.contain,
|
||||
)),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -332,20 +373,18 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// color: Colors.white,
|
||||
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(25), // Rounded top-left corner
|
||||
bottomLeft: Radius.circular(25), // Rounded bottom-left corner
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildForm(width: formWidth), // Fixed form width
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -353,29 +392,57 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
|
||||
/// **Reusable Login Form**
|
||||
Widget _buildForm({required double width}) {
|
||||
return SizedBox(
|
||||
return Column(
|
||||
children: [
|
||||
// width: width,
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
// children: [
|
||||
//
|
||||
// ],
|
||||
// ),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: width,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/login/logoNew.jpg',
|
||||
width: 180, // Optional: control size
|
||||
height: 70,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"Sign In",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF212121)),
|
||||
fontSize: widget.isDesktop ? 20 : 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.green,
|
||||
// color: Color(0xFF212121),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
"Welcome To TripApprovalTool",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF212121)),
|
||||
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
/// **Email Field**
|
||||
if (!_isForgotPassword && !_showOtpResetFields) ...[
|
||||
@ -383,47 +450,100 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
decoration:
|
||||
_inputDecoration("Enter your email address").copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
decoration: _inputDecoration(
|
||||
"Enter your email address",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? 'Required Email' : null,
|
||||
validator:
|
||||
(value) =>
|
||||
value == null || value.isEmpty
|
||||
? 'Required Email'
|
||||
: null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
/// **Password Field**
|
||||
_buildLabel("Password"),
|
||||
// TextFormField(
|
||||
// controller: _passwordController,
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontWeight: FontWeight.w600,
|
||||
// fontSize: 11,
|
||||
// ),
|
||||
// obscureText: _obscureText,
|
||||
//
|
||||
// decoration: _inputDecoration(
|
||||
// "Enter your password",
|
||||
// ).copyWith(
|
||||
// prefixIcon: Icon(Icons.key, size: 16),
|
||||
// suffixIcon: IconButton(
|
||||
// icon: Icon(
|
||||
// _obscureText
|
||||
// ? Icons.visibility_off
|
||||
// : Icons.visibility,
|
||||
// color: Color(0xFF12B24B),
|
||||
// size: 16,
|
||||
// ),
|
||||
//
|
||||
// onPressed:
|
||||
// () => setState(
|
||||
// () => _obscureText = !_obscureText,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// validator:
|
||||
// (value) =>
|
||||
// value == null || value.isEmpty
|
||||
// ? 'Required Password'
|
||||
// : null,
|
||||
// ),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
obscureText: _obscureText,
|
||||
decoration: _inputDecoration("Enter your password").copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.key,
|
||||
size: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
obscureText: _obscureText,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: _inputDecoration(
|
||||
"Enter your password",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(Icons.key, size: 16),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||
_obscureText
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: Color(0xFF12B24B),
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscureText = !_obscureText),
|
||||
onPressed:
|
||||
() => setState(
|
||||
() => _obscureText = !_obscureText,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? 'Required Password' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
onFieldSubmitted: (_) {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_login(context);
|
||||
}
|
||||
},
|
||||
validator:
|
||||
(value) =>
|
||||
value == null || value.isEmpty
|
||||
? 'Required Password'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
/// **Login Button**
|
||||
Row(
|
||||
@ -432,31 +552,39 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _login(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF12B24B), // Button color
|
||||
foregroundColor: Colors.white, // Text color
|
||||
backgroundColor: Color(
|
||||
0xFF12B24B,
|
||||
), // Button color
|
||||
foregroundColor:
|
||||
Colors.white, // Text color
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 10),
|
||||
horizontal: 24,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Sign In",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w800, fontSize: 15),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13.5,
|
||||
),
|
||||
SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
SizedBox(width: 3),
|
||||
Icon(
|
||||
Icons.arrow_forward_sharp,
|
||||
color: Colors.white,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -464,54 +592,69 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
),
|
||||
],
|
||||
),
|
||||
] else if (_isForgotPassword && !_showOtpResetFields) ...[
|
||||
] else if (_isForgotPassword &&
|
||||
!_showOtpResetFields) ...[
|
||||
_buildLabel("Email Address"),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
decoration:
|
||||
_inputDecoration("Enter your email address").copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
decoration: _inputDecoration(
|
||||
"Enter your email address",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? 'Required Email' : null,
|
||||
validator:
|
||||
(value) =>
|
||||
value == null || value.isEmpty
|
||||
? 'Required Email'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _onSubmit(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF12B24B), // Button color
|
||||
foregroundColor: Colors.white, // Text color
|
||||
backgroundColor: Color(
|
||||
0xFF12B24B,
|
||||
), // Button color
|
||||
foregroundColor:
|
||||
Colors.white, // Text color
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 10),
|
||||
horizontal: 24,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Submit",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w800, fontSize: 15),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13.5,
|
||||
),
|
||||
SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
SizedBox(width: 3),
|
||||
Icon(
|
||||
Icons.arrow_forward_sharp,
|
||||
color: Colors.white,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -525,80 +668,103 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
controller: _emailController,
|
||||
readOnly: true,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
decoration:
|
||||
_inputDecoration("Enter your email address").copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
decoration: _inputDecoration(
|
||||
"Enter your email address",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? 'Required Email' : null,
|
||||
validator:
|
||||
(value) =>
|
||||
value == null || value.isEmpty
|
||||
? 'Required Email'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
_buildLabel("OTP"),
|
||||
TextFormField(
|
||||
controller: _otpController,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
decoration: _inputDecoration("Enter your OTP").copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
decoration: _inputDecoration(
|
||||
"Enter your OTP",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? 'Required OTP' : null,
|
||||
validator:
|
||||
(value) =>
|
||||
value == null || value.isEmpty
|
||||
? 'Required OTP'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
_buildLabel("New Password"),
|
||||
TextFormField(
|
||||
controller: _newPasswordController,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
obscureText: _obscureText,
|
||||
decoration:
|
||||
_inputDecoration("Enter your new password").copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.key,
|
||||
size: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
obscureText: _obscureText,
|
||||
decoration: _inputDecoration(
|
||||
"Enter your new password",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(Icons.key, size: 16),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||
_obscureText
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: Color(0xFF12B24B),
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscureText = !_obscureText),
|
||||
onPressed:
|
||||
() => setState(
|
||||
() => _obscureText = !_obscureText,
|
||||
),
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
),
|
||||
validator:
|
||||
(value) =>
|
||||
value == null || value.isEmpty
|
||||
? 'Required New Password'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
_buildLabel("Confirm Password"),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600, fontSize: 11),
|
||||
obscureText: _obscureText,
|
||||
decoration:
|
||||
_inputDecoration("Enter your confirm password").copyWith(
|
||||
prefixIcon: Icon(
|
||||
Icons.key,
|
||||
size: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
obscureText: _obscureText,
|
||||
decoration: _inputDecoration(
|
||||
"Enter your confirm password",
|
||||
).copyWith(
|
||||
prefixIcon: Icon(Icons.key, size: 16),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||
_obscureText
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: Color(0xFF12B24B),
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscureText = !_obscureText),
|
||||
onPressed:
|
||||
() => setState(
|
||||
() => _obscureText = !_obscureText,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
@ -611,13 +777,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (!_formKey.currentState!.validate())
|
||||
return;
|
||||
setState(() {
|
||||
_isForgotPassword = false;
|
||||
_showOtpResetFields = true;
|
||||
@ -626,31 +793,39 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
_onSubmit(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF12B24B), // Button color
|
||||
foregroundColor: Colors.white, // Text color
|
||||
backgroundColor: Color(
|
||||
0xFF12B24B,
|
||||
), // Button color
|
||||
foregroundColor:
|
||||
Colors.white, // Text color
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 10),
|
||||
horizontal: 24,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Submit",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w800, fontSize: 15),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13.5,
|
||||
),
|
||||
SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
SizedBox(width: 3),
|
||||
Icon(
|
||||
Icons.arrow_forward_sharp,
|
||||
color: Colors.white,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -659,7 +834,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
if (!_isForgotPassword && !_showOtpResetFields)
|
||||
Center(
|
||||
child: TextButton(
|
||||
@ -678,7 +853,8 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
|
||||
color: Color(0xFF212121), // Text color
|
||||
decoration:
|
||||
TextDecoration.underline, // Underline the text
|
||||
TextDecoration
|
||||
.underline, // Underline the text
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -701,13 +877,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
|
||||
color: Color(0xFF212121), // Text color
|
||||
decoration:
|
||||
TextDecoration.underline, // Underline the text
|
||||
TextDecoration
|
||||
.underline, // Underline the text
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
if (!_isForgotPassword && !_showOtpResetFields)
|
||||
Row(
|
||||
children: [
|
||||
@ -717,10 +894,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
handleMS();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white, // Button color
|
||||
foregroundColor: Colors.black, // Text color
|
||||
backgroundColor:
|
||||
Colors.white, // Button color
|
||||
foregroundColor:
|
||||
Colors.black, // Text color
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 5),
|
||||
horizontal: 24,
|
||||
vertical: 5,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
@ -731,18 +912,20 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 5),
|
||||
horizontal: 24,
|
||||
vertical: 3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Sign In With Microsoft",
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500, fontSize: 14),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
SizedBox(width: 3),
|
||||
Image.asset(
|
||||
'assets/images/login/microsoft.png',
|
||||
width: 30, // Optional: control size
|
||||
@ -783,6 +966,13 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -797,7 +987,8 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121)),
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -826,8 +1017,10 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||
final url = '$apiUrl/auth/mslogin';
|
||||
print(url);
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse(url), headers: {'Content-Type': 'application/json'});
|
||||
final response = await http.get(
|
||||
Uri.parse(url),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
print("inside try method");
|
||||
if (response.statusCode == 200) {
|
||||
final authUrl = json.decode(response.body)['auth_url'];
|
||||
|
||||
@ -19,13 +19,14 @@ class CostCenterData extends StatefulWidget {
|
||||
final int? costcenterId; // <-- Add this
|
||||
final Map<String, dynamic>? costcenterData;
|
||||
|
||||
const CostCenterData(
|
||||
{super.key,
|
||||
const CostCenterData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetCostCenter,
|
||||
this.costcenterId,
|
||||
this.costcenterData});
|
||||
this.costcenterData,
|
||||
});
|
||||
|
||||
@override
|
||||
CostCenterDataState createState() => CostCenterDataState();
|
||||
@ -49,10 +50,7 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
int? costcenterDataId;
|
||||
late String isActive = "1";
|
||||
|
||||
List<String> dataHeader = [
|
||||
"name",
|
||||
"description",
|
||||
];
|
||||
List<String> dataHeader = ["name", "description"];
|
||||
|
||||
Map<String, dynamic> costcenterDetails() {
|
||||
final data = {
|
||||
@ -69,7 +67,6 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
|
||||
apiData = null;
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
@ -110,7 +107,6 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void toggleStatus() {
|
||||
setState(() {
|
||||
isActive = isActive == "1" ? "0" : "1";
|
||||
@ -171,7 +167,9 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
apiUrldata = '$apiUrl/api/updateCostCenter/$costcenterDataId';
|
||||
costcenterData["cost_center_id"] = costcenterDataId.toString();
|
||||
costcenterData["updated_by"] = userId;
|
||||
(costcenterData.containsKey("created_by")) ? costcenterData.remove("created_by") : '' ;
|
||||
(costcenterData.containsKey("created_by"))
|
||||
? costcenterData.remove("created_by")
|
||||
: '';
|
||||
} else {
|
||||
print("for add CostCenter id - null");
|
||||
apiUrldata = '$apiUrl/api/createCostCenter';
|
||||
@ -193,11 +191,11 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
};
|
||||
final body = jsonEncode(costcenterData);
|
||||
|
||||
final response = costcenterDataId != null
|
||||
final response =
|
||||
costcenterDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
print("Update - Response: ${response.body}");
|
||||
@ -217,7 +215,6 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
print("Failed to submit costcenter. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print(" Error submitting plan: $e");
|
||||
}
|
||||
@ -225,7 +222,6 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
@ -238,27 +234,27 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
(costcenterDataId != null) ? 'Edit CostCenter' : 'Create CostCenter',
|
||||
(costcenterDataId != null)
|
||||
? 'Edit CostCenter'
|
||||
: 'Create CostCenter',
|
||||
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 5),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Name",
|
||||
"Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -278,7 +274,8 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -289,18 +286,17 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Description",
|
||||
"Description *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -334,9 +330,7 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
if (costcenterDataId != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -346,7 +340,8 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
@ -358,17 +353,14 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color: isActive == "1" ? Colors.green : Colors.red,
|
||||
color: isActive == "1" ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (costcenterDataId != null)
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
if (costcenterDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -405,13 +397,17 @@ class CostCenterDataState extends State<CostCenterData> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
@ -67,11 +67,13 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -97,8 +99,7 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchGetCostCenter() async {
|
||||
|
||||
final String apiUrlData = '$apiUrl/api/getCostCenterMaster';
|
||||
final String apiUrlData = '$apiUrl/api/getCostCenterMaster?for=table_view';
|
||||
|
||||
final String? token = await getToken();
|
||||
|
||||
@ -143,25 +144,30 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
print("all before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredCostCenter = allCostCenter.where((object) {
|
||||
filteredCostCenter =
|
||||
allCostCenter.where((object) {
|
||||
final isActiveStatus =
|
||||
object['is_active'] == "1" ? "active" : "inactive";
|
||||
return (object['cost_center_id']?.toLowerCase().contains(lowerQuery) ??
|
||||
return (object['cost_center_id']?.toLowerCase().contains(
|
||||
lowerQuery,
|
||||
) ??
|
||||
false) ||
|
||||
(object['name']?.toLowerCase().contains(lowerQuery) ??
|
||||
(object['name']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(object['description']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['description']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(isActiveStatus.contains(lowerQuery));
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredCostCenter: $filteredCostCenter");
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -170,11 +176,14 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
@ -187,7 +196,8 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
@ -215,7 +225,8 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
// ? 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
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
|
||||
@ -247,9 +258,7 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.16,
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.16),
|
||||
|
||||
if (isDesktop)
|
||||
Container(
|
||||
@ -261,7 +270,9 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -273,17 +284,19 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -297,16 +310,18 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
disabledForegroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side:
|
||||
BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
side: BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CostCenterData(
|
||||
builder:
|
||||
(context) => CostCenterData(
|
||||
isDesktop: isDesktop,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetCostCenter: refreshData,
|
||||
@ -338,10 +353,7 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
],
|
||||
),
|
||||
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
if (!isDesktop) SizedBox(height: 5),
|
||||
isDesktop
|
||||
? SizedBox.shrink()
|
||||
: Row(
|
||||
@ -356,7 +368,9 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -369,17 +383,18 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -417,14 +432,17 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Please Create CostCenter Details",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16, color: Colors.grey),
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
@ -434,19 +452,21 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
}
|
||||
/* Here collect the list to displayed the data in table or card Used */
|
||||
List<dynamic> object =
|
||||
filteredCostCenter.isNotEmpty ? filteredCostCenter : allCostCenter;
|
||||
filteredCostCenter.isNotEmpty
|
||||
? filteredCostCenter
|
||||
: allCostCenter;
|
||||
|
||||
/* List is Sorting here */
|
||||
object.sort((a, b) {
|
||||
DateTime dateA = DateTime.parse(a['created_on']);
|
||||
DateTime dateB = DateTime.parse(b['created_on']);
|
||||
|
||||
return dateB
|
||||
.compareTo(dateA); // Descending: newest first
|
||||
return dateB.compareTo(dateA); // Descending: newest first
|
||||
});
|
||||
|
||||
/* For pagination for list ... */
|
||||
List paginatedCostCenter = object
|
||||
List paginatedCostCenter =
|
||||
object
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
@ -454,8 +474,7 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
/* Table ... */
|
||||
Widget table = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double minWidth =
|
||||
isDesktop ? constraints.maxWidth : 1300;
|
||||
double minWidth = isDesktop ? constraints.maxWidth : 1300;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: minWidth),
|
||||
@ -464,7 +483,9 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5, color: Colors.grey.shade200),
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
@ -472,48 +493,68 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
'Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Description',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: paginatedCostCenter.map((tableObject) {
|
||||
String costcenterId = tableObject['cost_center_id']
|
||||
rows:
|
||||
paginatedCostCenter.map((tableObject) {
|
||||
String costcenterId =
|
||||
tableObject['cost_center_id']
|
||||
.toString(); // Get user ID
|
||||
bool isSelected = selectedCostCenterId == costcenterId;
|
||||
bool isSelected =
|
||||
selectedCostCenterId == costcenterId;
|
||||
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(tableObject['name'] ?? '',
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['name'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(tableObject['description'] ?? 'N/A',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['description'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['is_active'] == "1"
|
||||
@ -522,7 +563,10 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color: tableObject['is_active'] == "1" ? Colors.green : Colors.red,
|
||||
color:
|
||||
tableObject['is_active'] == "1"
|
||||
? Colors.green
|
||||
: Colors.grey,
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -535,32 +579,45 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
// apiService.getSingleUser(id),
|
||||
// ),
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit CostCenter Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final costcenterId = int.tryParse(
|
||||
tableObject['cost_center_id']
|
||||
.toString());
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (costcenterId != null) {
|
||||
print("Table cell - costcenter Id -- $costcenterId");
|
||||
final data = await apiService.getCostCenterDetailsFind(costcenterId);
|
||||
print(
|
||||
"Table cell - costcenter Id -- $costcenterId",
|
||||
);
|
||||
final data = await apiService
|
||||
.getCostCenterDetailsFind(
|
||||
costcenterId,
|
||||
);
|
||||
print("CostCenterId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CostCenterData(
|
||||
builder:
|
||||
(context) => CostCenterData(
|
||||
isDesktop: isDesktop,
|
||||
costcenterId: costcenterId, // Pass the ID
|
||||
costcenterId:
|
||||
costcenterId, // Pass the ID
|
||||
costcenterData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetForex: fetchGetForex,
|
||||
fetchGetCostCenter: refreshData,
|
||||
fetchGetCostCenter:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
@ -571,7 +628,8 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@ -587,7 +645,9 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -607,37 +667,50 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit CostCenter Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final costcenterId = int.tryParse(
|
||||
cardObject['cost_center_id']
|
||||
.toString());
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (costcenterId != null) {
|
||||
print("costcenterId -- $costcenterId");
|
||||
print(
|
||||
"costcenterId -- $costcenterId",
|
||||
);
|
||||
final data = await apiService
|
||||
.getCostCenterDetailsFind(costcenterId);
|
||||
.getCostCenterDetailsFind(
|
||||
costcenterId,
|
||||
);
|
||||
print("CostCenterId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CostCenterData(
|
||||
builder:
|
||||
(context) => CostCenterData(
|
||||
isDesktop: isDesktop,
|
||||
costcenterId:costcenterId, // Pass the ID
|
||||
costcenterId:
|
||||
costcenterId, // Pass the ID
|
||||
costcenterData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetCostCenter: fetchGetCostCenter,
|
||||
fetchGetCostCenter: refreshData,
|
||||
fetchGetCostCenter:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
@ -736,13 +809,12 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
cardObject['description'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
@ -751,7 +823,8 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
cardObject['description'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -767,13 +840,13 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredCostCenter.isEmpty
|
||||
? Center(
|
||||
@ -781,7 +854,8 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@ -795,10 +869,13 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(paginatedCostCenter)),
|
||||
: buildMobileCardView(
|
||||
paginatedCostCenter,
|
||||
)),
|
||||
),
|
||||
// Expanded(
|
||||
// child: isDesktop
|
||||
@ -829,9 +906,11 @@ class CostCenterListState extends State<CostCenterList> {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -197,18 +197,23 @@ class StatusDashboardState extends State<StatusDashboard> {
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
horizontal: MediaQuery.of(context).size.width * 0.1, // 30% of screen width as horizontal padding
|
||||
vertical: 10, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child : LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight( // Only needed if child layout depends on height
|
||||
child: Container(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : const Color(0xFFFCFCFC),
|
||||
borderRadius: BorderRadius.circular(12), // 👈 Set your desired radius
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
// color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -253,7 +258,15 @@ class StatusDashboardState extends State<StatusDashboard> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@ -19,13 +19,14 @@ class DepartmentData extends StatefulWidget {
|
||||
final int? departmentId; // <-- Add this
|
||||
final Map<String, dynamic>? departmentData;
|
||||
|
||||
const DepartmentData(
|
||||
{super.key,
|
||||
const DepartmentData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetDepartment,
|
||||
this.departmentId,
|
||||
this.departmentData});
|
||||
this.departmentData,
|
||||
});
|
||||
|
||||
@override
|
||||
DepartmentDataState createState() => DepartmentDataState();
|
||||
@ -49,10 +50,7 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
int? departmentDataId;
|
||||
late String isActive = "1";
|
||||
|
||||
List<String> dataHeader = [
|
||||
"name",
|
||||
"description",
|
||||
];
|
||||
List<String> dataHeader = ["name", "description"];
|
||||
|
||||
Map<String, dynamic> departmentDetails() {
|
||||
final data = {
|
||||
@ -69,7 +67,6 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
|
||||
apiData = null;
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
@ -110,7 +107,6 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void toggleStatus() {
|
||||
setState(() {
|
||||
isActive = isActive == "1" ? "0" : "1";
|
||||
@ -171,7 +167,9 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
apiUrldata = '$apiUrl/api/updateDepartment/$departmentDataId';
|
||||
departmentData["department_id"] = departmentDataId.toString();
|
||||
departmentData["updated_by"] = userId;
|
||||
(departmentData.containsKey("created_by")) ? departmentData.remove("created_by") : '' ;
|
||||
(departmentData.containsKey("created_by"))
|
||||
? departmentData.remove("created_by")
|
||||
: '';
|
||||
} else {
|
||||
print("for add Department id - null");
|
||||
apiUrldata = '$apiUrl/api/createDepartment';
|
||||
@ -193,11 +191,11 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
};
|
||||
final body = jsonEncode(departmentData);
|
||||
|
||||
final response = departmentDataId != null
|
||||
final response =
|
||||
departmentDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
print("Update - Response: ${response.body}");
|
||||
@ -217,7 +215,6 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
print("Failed to submit department. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print(" Error submitting plan: $e");
|
||||
}
|
||||
@ -225,7 +222,6 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
@ -238,27 +234,27 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
(departmentDataId != null) ? 'Edit Department' : 'Create Department',
|
||||
(departmentDataId != null)
|
||||
? 'Edit Department'
|
||||
: 'Create Department',
|
||||
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 5),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Name",
|
||||
"Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -278,7 +274,8 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -289,18 +286,17 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Description",
|
||||
"Description *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -334,9 +330,7 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
if (departmentDataId != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -346,7 +340,8 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
@ -362,13 +357,10 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
if (departmentDataId != null)
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
if (departmentDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -405,13 +397,17 @@ class DepartmentDataState extends State<DepartmentData> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
@ -67,11 +67,13 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -97,7 +99,7 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchGetDepartment() async {
|
||||
final String apiUrlData = '$apiUrl/api/getDepartmentList';
|
||||
final String apiUrlData = '$apiUrl/api/getDepartmentList?for=table_view';
|
||||
|
||||
final String? token = await getToken();
|
||||
|
||||
@ -141,24 +143,30 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
print("all before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredDepartment = allDepartment.where((object) {
|
||||
filteredDepartment =
|
||||
allDepartment.where((object) {
|
||||
final isActiveStatus =
|
||||
object['is_active'] == "1" ? "active" : "inactive";
|
||||
return (object['department_id']?.toLowerCase().contains(lowerQuery) ??
|
||||
return (object['department_id']?.toLowerCase().contains(
|
||||
lowerQuery,
|
||||
) ??
|
||||
false) ||
|
||||
(object['name']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(object['description']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(isActiveStatus.contains(lowerQuery));
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredDepartment: $filteredDepartment");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -167,11 +175,14 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
@ -184,7 +195,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
@ -212,7 +224,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
// ? 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
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
|
||||
@ -244,9 +257,7 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.16,
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.16),
|
||||
|
||||
if (isDesktop)
|
||||
Container(
|
||||
@ -258,7 +269,9 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -270,17 +283,19 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -294,16 +309,18 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
disabledForegroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side:
|
||||
BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
side: BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => DepartmentData(
|
||||
builder:
|
||||
(context) => DepartmentData(
|
||||
isDesktop: isDesktop,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetDepartment: refreshData,
|
||||
@ -335,10 +352,7 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
],
|
||||
),
|
||||
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
if (!isDesktop) SizedBox(height: 5),
|
||||
isDesktop
|
||||
? SizedBox.shrink()
|
||||
: Row(
|
||||
@ -353,7 +367,9 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -366,17 +382,18 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -414,14 +431,17 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Please Create Department Details",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16, color: Colors.grey),
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
@ -430,7 +450,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
);
|
||||
}
|
||||
/* Here collect the list to displayed the data in table or card Used */
|
||||
List<dynamic> object = filteredDepartment.isNotEmpty
|
||||
List<dynamic> object =
|
||||
filteredDepartment.isNotEmpty
|
||||
? filteredDepartment
|
||||
: allDepartment;
|
||||
|
||||
@ -439,12 +460,12 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
DateTime dateA = DateTime.parse(a['created_on']);
|
||||
DateTime dateB = DateTime.parse(b['created_on']);
|
||||
|
||||
return dateB
|
||||
.compareTo(dateA); // Descending: newest first
|
||||
return dateB.compareTo(dateA); // Descending: newest first
|
||||
});
|
||||
|
||||
/* For pagination for list ... */
|
||||
List paginatedDepartment = object
|
||||
List paginatedDepartment =
|
||||
object
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
@ -452,8 +473,7 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
/* Table ... */
|
||||
Widget table = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double minWidth =
|
||||
isDesktop ? constraints.maxWidth : 1300;
|
||||
double minWidth = isDesktop ? constraints.maxWidth : 1300;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: minWidth),
|
||||
@ -462,7 +482,9 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5, color: Colors.grey.shade200),
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
@ -470,51 +492,68 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
'Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Description',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: paginatedDepartment.map((tableObject) {
|
||||
rows:
|
||||
paginatedDepartment.map((tableObject) {
|
||||
String departmentId =
|
||||
tableObject['department_id']
|
||||
.toString(); // Get user ID
|
||||
bool isSelected =
|
||||
selectedDepartmentId == departmentId;
|
||||
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(tableObject['name'] ?? '',
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['name'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(tableObject['description'] ?? 'N/A',
|
||||
Text(
|
||||
tableObject['description'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['is_active'] == "1"
|
||||
@ -523,9 +562,10 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color: tableObject['is_active'] == "1"
|
||||
color:
|
||||
tableObject['is_active'] == "1"
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
: Colors.grey,
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -538,37 +578,45 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
// apiService.getSingleUser(id),
|
||||
// ),
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit Department Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final departmentId = int.tryParse(
|
||||
tableObject['department_id']
|
||||
.toString());
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (departmentId != null) {
|
||||
print(
|
||||
"Table cell - department Id -- $departmentId");
|
||||
"Table cell - department Id -- $departmentId",
|
||||
);
|
||||
final data = await apiService
|
||||
.getDepartmentDetailsFind(
|
||||
departmentId);
|
||||
departmentId,
|
||||
);
|
||||
print("DepartmentId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
DepartmentData(
|
||||
builder:
|
||||
(context) => DepartmentData(
|
||||
isDesktop: isDesktop,
|
||||
departmentId:
|
||||
departmentId, // Pass the ID
|
||||
departmentData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetForex: fetchGetForex,
|
||||
fetchGetDepartment: refreshData,
|
||||
fetchGetDepartment:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
@ -579,7 +627,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@ -595,7 +644,9 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -615,34 +666,42 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit Department Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final departmentId = int.tryParse(
|
||||
cardObject['department_id']
|
||||
.toString());
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (departmentId != null) {
|
||||
print(
|
||||
"departmentId -- $departmentId");
|
||||
"departmentId -- $departmentId",
|
||||
);
|
||||
final data = await apiService
|
||||
.getDepartmentDetailsFind(
|
||||
departmentId);
|
||||
departmentId,
|
||||
);
|
||||
print("DepartmentId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
DepartmentData(
|
||||
builder:
|
||||
(context) => DepartmentData(
|
||||
isDesktop: isDesktop,
|
||||
departmentId:
|
||||
departmentId, // Pass the ID
|
||||
@ -749,13 +808,12 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
cardObject['description'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
@ -764,7 +822,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
cardObject['description'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -785,7 +844,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredDepartment.isEmpty
|
||||
? Center(
|
||||
@ -793,7 +853,8 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@ -807,11 +868,13 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(
|
||||
paginatedDepartment)),
|
||||
paginatedDepartment,
|
||||
)),
|
||||
),
|
||||
// Expanded(
|
||||
// child: isDesktop
|
||||
@ -842,9 +905,11 @@ class DepartmentListState extends State<DepartmentList> {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,11 +100,13 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
"Unexpected response format: Expected a List but got ${responseBody.runtimeType}");
|
||||
"Unexpected response format: Expected a List but got ${responseBody.runtimeType}",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
'Failed to load users. Status Code: ${response.statusCode}');
|
||||
'Failed to load users. Status Code: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching users: $e");
|
||||
@ -138,7 +140,8 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
List<dynamic> travellerList = responseBody['data'];
|
||||
|
||||
setState(() {
|
||||
_traveller = travellerList
|
||||
_traveller =
|
||||
travellerList
|
||||
.map((user) => SearchTraveler.fromJson(user))
|
||||
.toList();
|
||||
_filteredTraveller = List.from(_traveller);
|
||||
@ -147,47 +150,53 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
print("Users fetched: ${_users.length}");
|
||||
for (var travvelr in _traveller) {
|
||||
print(
|
||||
"${travvelr.firstName} ${travvelr.lastName} ${travvelr.mobileNo}");
|
||||
"${travvelr.firstName} ${travvelr.lastName} ${travvelr.mobileNo}",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
"Unexpected response format: Expected a List but got ${responseBody.runtimeType}");
|
||||
"Unexpected response format: Expected a List but got ${responseBody.runtimeType}",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
'Failed to load users. Status Code: ${response.statusCode}');
|
||||
'Failed to load users. Status Code: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching traveller: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _filterUsers1(String query) {
|
||||
print("Filtering users...");
|
||||
setState(() {
|
||||
if (query.isEmpty) {
|
||||
_filteredUsers = List.from(_users);
|
||||
} else {
|
||||
_filteredUsers = _users.where((user) {
|
||||
List<String> searchFields = [
|
||||
"${user.firstName} ${user.lastName}".toLowerCase(),
|
||||
user.email.toLowerCase() ?? "",
|
||||
user.userId.toLowerCase() ?? "",
|
||||
user.mobileNo ?? "",
|
||||
user.alternateMobileNo ?? ""
|
||||
];
|
||||
|
||||
return searchFields
|
||||
.any((field) => field.contains(query.toLowerCase()));
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
|
||||
print("Filtered Users:");
|
||||
for (var user in _filteredUsers) {
|
||||
print("${user.firstName} ${user.lastName}");
|
||||
}
|
||||
}
|
||||
// void _filterUsers1(String query) {
|
||||
// print("Filtering users...");
|
||||
// setState(() {
|
||||
// if (query.isEmpty) {
|
||||
// _filteredUsers = List.from(_users);
|
||||
// } else {
|
||||
// _filteredUsers =
|
||||
// _users.where((user) {
|
||||
// List<String> searchFields = [
|
||||
// "${user.firstName} ${user.lastName}".toLowerCase(),
|
||||
// user.email.toLowerCase() ?? "",
|
||||
// user.empCode?.toLowerCase() ?? "",
|
||||
// user.userId.toLowerCase() ?? "",
|
||||
// user.mobileNo ?? "",
|
||||
// user.alternateMobileNo ?? "",
|
||||
// ];
|
||||
//
|
||||
// return searchFields.any(
|
||||
// (field) => field.contains(query.toLowerCase()),
|
||||
// );
|
||||
// }).toList();
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// print("Filtered Users:");
|
||||
// for (var user in _filteredUsers) {
|
||||
// print("${user.firstName} ${user.lastName}");
|
||||
// }
|
||||
// }
|
||||
|
||||
void _filterUsers(String query) {
|
||||
print("Filtering _filterUsersTravellers...");
|
||||
@ -200,7 +209,8 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
];
|
||||
} else {
|
||||
_filteredList = [
|
||||
..._users.where((user) {
|
||||
..._users
|
||||
.where((user) {
|
||||
print("usersLLL : ${user}");
|
||||
|
||||
List<String> searchFields = [
|
||||
@ -208,11 +218,14 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
user.email.toLowerCase() ?? "",
|
||||
user.userId.toLowerCase() ?? "",
|
||||
user.mobileNo ?? "",
|
||||
user.alternateMobileNo ?? ""
|
||||
user.alternateMobileNo ?? "",
|
||||
user.empCode?.toLowerCase() ?? "",
|
||||
];
|
||||
return searchFields
|
||||
.any((field) => field.contains(query.toLowerCase()));
|
||||
}).map((user) => {"type": "user", "data": user}),
|
||||
return searchFields.any(
|
||||
(field) => field.contains(query.toLowerCase()),
|
||||
);
|
||||
})
|
||||
.map((user) => {"type": "user", "data": user}),
|
||||
];
|
||||
}
|
||||
});
|
||||
@ -221,7 +234,8 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
for (var item in _filteredList) {
|
||||
var user = item["data"];
|
||||
print(
|
||||
"${item["type"].toUpperCase()} - ${user.firstName} ${user.lastName}");
|
||||
"${item["type"].toUpperCase()} - ${user.firstName} ${user.lastName}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,16 +250,19 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
];
|
||||
} else {
|
||||
_filteredList = [
|
||||
..._traveller.where((traveller) {
|
||||
..._traveller
|
||||
.where((traveller) {
|
||||
List<String> searchFields = [
|
||||
"${traveller.firstName} ${traveller.lastName}".toLowerCase(),
|
||||
traveller.email.toLowerCase() ?? "",
|
||||
traveller.travellerId.toLowerCase() ?? "",
|
||||
traveller.mobileNo ?? "",
|
||||
];
|
||||
return searchFields
|
||||
.any((field) => field.contains(query.toLowerCase()));
|
||||
}).map((traveller) => {"type": "traveller", "data": traveller}),
|
||||
return searchFields.any(
|
||||
(field) => field.contains(query.toLowerCase()),
|
||||
);
|
||||
})
|
||||
.map((traveller) => {"type": "traveller", "data": traveller}),
|
||||
];
|
||||
}
|
||||
});
|
||||
@ -254,7 +271,8 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
for (var item in _filteredList) {
|
||||
var user = item["data"];
|
||||
print(
|
||||
"${item["type"].toUpperCase()} - ${user.firstName} ${user.lastName}");
|
||||
"${item["type"].toUpperCase()} - ${user.firstName} ${user.lastName}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,32 +284,39 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
if (query.isEmpty) {
|
||||
_filteredList = [
|
||||
..._users.map((user) => {"type": "user", "data": user}),
|
||||
..._traveller
|
||||
.map((traveller) => {"type": "traveller", "data": traveller}),
|
||||
..._traveller.map(
|
||||
(traveller) => {"type": "traveller", "data": traveller},
|
||||
),
|
||||
];
|
||||
} else {
|
||||
_filteredList = [
|
||||
..._users.where((user) {
|
||||
..._users
|
||||
.where((user) {
|
||||
List<String> searchFields = [
|
||||
"${user.firstName} ${user.lastName}".toLowerCase(),
|
||||
user.email.toLowerCase() ?? "",
|
||||
user.userId.toLowerCase() ?? "",
|
||||
user.mobileNo ?? "",
|
||||
user.alternateMobileNo ?? ""
|
||||
user.alternateMobileNo ?? "",
|
||||
];
|
||||
return searchFields
|
||||
.any((field) => field.contains(query.toLowerCase()));
|
||||
}).map((user) => {"type": "user", "data": user}),
|
||||
..._traveller.where((traveller) {
|
||||
return searchFields.any(
|
||||
(field) => field.contains(query.toLowerCase()),
|
||||
);
|
||||
})
|
||||
.map((user) => {"type": "user", "data": user}),
|
||||
..._traveller
|
||||
.where((traveller) {
|
||||
List<String> searchFields = [
|
||||
"${traveller.firstName} ${traveller.lastName}".toLowerCase(),
|
||||
traveller.email.toLowerCase() ?? "",
|
||||
traveller.travellerId.toLowerCase() ?? "",
|
||||
traveller.mobileNo ?? "",
|
||||
];
|
||||
return searchFields
|
||||
.any((field) => field.contains(query.toLowerCase()));
|
||||
}).map((traveller) => {"type": "traveller", "data": traveller}),
|
||||
return searchFields.any(
|
||||
(field) => field.contains(query.toLowerCase()),
|
||||
);
|
||||
})
|
||||
.map((traveller) => {"type": "traveller", "data": traveller}),
|
||||
];
|
||||
}
|
||||
});
|
||||
@ -300,7 +325,8 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
for (var item in _filteredList) {
|
||||
var user = item["data"];
|
||||
print(
|
||||
"${item["type"].toUpperCase()} - ${user.firstName} ${user.lastName}");
|
||||
"${item["type"].toUpperCase()} - ${user.firstName} ${user.lastName}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,11 +349,15 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
mainAxisSize:
|
||||
MainAxisSize.min, // Ensures content doesn't expand unnecessarily
|
||||
children: [
|
||||
widget.title == "Others"
|
||||
? Text("Please Select Other User",
|
||||
style: GoogleFonts.poppins(fontSize: 14))
|
||||
: Text("Please Select Other Employee",
|
||||
style: GoogleFonts.poppins(fontSize: 14)),
|
||||
widget.title == "Others (Non Employee)"
|
||||
? Text(
|
||||
"Please Select Other User",
|
||||
style: GoogleFonts.poppins(fontSize: 14),
|
||||
)
|
||||
: Text(
|
||||
"Please Select Other Employee",
|
||||
style: GoogleFonts.poppins(fontSize: 14),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
|
||||
// Search Field
|
||||
@ -337,18 +367,21 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
setState(() {
|
||||
_showTravellerForm = false;
|
||||
});
|
||||
widget.title == "Others"
|
||||
widget.title == "Others (Non Employee)"
|
||||
? _filterTravellers(query)
|
||||
: _filterUsers(query);
|
||||
},
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search for a user",
|
||||
hintStyle:
|
||||
GoogleFonts.poppins(fontSize: 14, color: Colors.grey),
|
||||
hintStyle: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey.shade200, width: 1),
|
||||
// borderSide: BorderSide(color: Color(0xFFF5F5F5), width: 2),
|
||||
@ -362,13 +395,17 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
|
||||
SizedBox(height: 10),
|
||||
|
||||
if (widget.title == "Others") ...[
|
||||
if (widget.title == "Others (Non Employee)") ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("or create a new traveler",
|
||||
Text(
|
||||
"or create a new traveler",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Color(0xFF575A74))),
|
||||
fontSize: 14,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -376,9 +413,13 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
child: Text("Create",
|
||||
child: Text(
|
||||
"Create",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: widget.layoutColorForUser)),
|
||||
fontSize: 14,
|
||||
color: widget.layoutColorForUser,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -391,12 +432,15 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
? SizedBox(
|
||||
height: 300, // Limit height to avoid overflow
|
||||
// child: _filteredUsers.isEmpty
|
||||
child: _filteredList.isEmpty
|
||||
child:
|
||||
_filteredList.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No users found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.grey),
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
@ -411,7 +455,8 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
item["type"]; // "user" or "traveller"
|
||||
if (user is Map<String, dynamic>) {
|
||||
print(
|
||||
"userLsirer - ${jsonEncode(user)}"); // pretty JSON-like string
|
||||
"userLsirer - ${jsonEncode(user)}",
|
||||
); // pretty JSON-like string
|
||||
} else {
|
||||
print("userLsirer - $user"); // fallback
|
||||
}
|
||||
@ -420,32 +465,37 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
"${user.firstName ?? "Unknown"} ${user.lastName ?? ""}",
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
subtitle: userType == "user"
|
||||
subtitle:
|
||||
userType == "user"
|
||||
? Text(
|
||||
"Employee ID: ${user.empCode ?? ""} ",
|
||||
// "Employee ID: ${userType == "user" ? user.userId : user.travellerId}",
|
||||
style:
|
||||
GoogleFonts.poppins(fontSize: 10),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Mobile : ${user.mobileNo ?? ""} ",
|
||||
// "Employee ID: ${userType == "user" ? user.userId : user.travellerId}",
|
||||
style:
|
||||
GoogleFonts.poppins(fontSize: 10),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
String selectedUser =
|
||||
"${user.firstName ?? "Unknown"} ${user.lastName ?? ""}";
|
||||
setState(() {
|
||||
_searchController.text = selectedUser;
|
||||
userIdSelected = userType == "user"
|
||||
userIdSelected =
|
||||
userType == "user"
|
||||
? user.userId
|
||||
: user.travellerId;
|
||||
isTraveller = userType == "traveller";
|
||||
});
|
||||
print(
|
||||
"Selected: $selectedUser, ID: ${userType == "user" ? user.userId : user.travellerId},"
|
||||
" isTraveller: $userIdSelected");
|
||||
" isTraveller: $userIdSelected",
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -462,10 +512,16 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TravelerForm(
|
||||
formKey: _formKey,
|
||||
onSubmit: (String fullName, String travellerId,
|
||||
bool isTraveller) {
|
||||
widget.onSubmit(fullName, travellerId,
|
||||
isTraveller); // Pass the data up
|
||||
onSubmit: (
|
||||
String fullName,
|
||||
String travellerId,
|
||||
bool isTraveller,
|
||||
) {
|
||||
widget.onSubmit(
|
||||
fullName,
|
||||
travellerId,
|
||||
isTraveller,
|
||||
); // Pass the data up
|
||||
},
|
||||
firstNameController: TextEditingController(),
|
||||
lastNameController: TextEditingController(),
|
||||
@ -487,7 +543,9 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: widget.layoutColorForUser, width: 2),
|
||||
color: widget.layoutColorForUser,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
@ -508,15 +566,21 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: widget.layoutColorForUser, width: 2),
|
||||
color: widget.layoutColorForUser,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: () {
|
||||
print(
|
||||
"Submitting: ${_searchController.text}, ID: $userIdSelected");
|
||||
"Submitting: ${_searchController.text}, ID: $userIdSelected",
|
||||
);
|
||||
widget.onSubmit(
|
||||
_searchController.text, userIdSelected, isTraveller);
|
||||
_searchController.text,
|
||||
userIdSelected,
|
||||
isTraveller,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
@ -542,14 +606,15 @@ class TravelerForm extends StatefulWidget {
|
||||
final GlobalKey<FormState> formKey;
|
||||
final void Function(String, String, bool) onSubmit;
|
||||
|
||||
TravelerForm(
|
||||
{required this.formKey,
|
||||
TravelerForm({
|
||||
required this.formKey,
|
||||
required this.orgId,
|
||||
required this.firstNameController,
|
||||
required this.lastNameController,
|
||||
required this.emailController,
|
||||
required this.mobileController,
|
||||
required this.onSubmit});
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
_TravelerFormState createState() => _TravelerFormState();
|
||||
@ -582,8 +647,9 @@ class _TravelerFormState extends State<TravelerForm> {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email is required';
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
||||
.hasMatch(value)) {
|
||||
if (!RegExp(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||
).hasMatch(value)) {
|
||||
return 'Enter a valid email address';
|
||||
}
|
||||
return null;
|
||||
@ -642,7 +708,8 @@ class _TravelerFormState extends State<TravelerForm> {
|
||||
String lastName = travellerData["last_name"];
|
||||
|
||||
print(
|
||||
"Traveller Added: ID: $travellerId, Name: $firstName $lastName");
|
||||
"Traveller Added: ID: $travellerId, Name: $firstName $lastName",
|
||||
);
|
||||
|
||||
// // Pass data to callback
|
||||
// widget.onSubmit("$firstName $lastName", travellerId, true);
|
||||
@ -658,21 +725,22 @@ class _TravelerFormState extends State<TravelerForm> {
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Traveller added successfully!",
|
||||
style:
|
||||
GoogleFonts.poppins(color: Colors.white), // ✅ Set text color
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
), // ✅ Set text color
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error: ${response.body}")),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Error: ${response.body}")));
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to connect to server.")),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Failed to connect to server.")));
|
||||
}
|
||||
}
|
||||
|
||||
@ -697,8 +765,10 @@ class _TravelerFormState extends State<TravelerForm> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text("Create Traveler",
|
||||
style: GoogleFonts.poppins(color: Colors.black54)),
|
||||
Text(
|
||||
"Create Traveler",
|
||||
style: GoogleFonts.poppins(color: Colors.black54),
|
||||
),
|
||||
SizedBox(height: 7),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
@ -714,13 +784,17 @@ class _TravelerFormState extends State<TravelerForm> {
|
||||
onPressed: () {
|
||||
widget.formKey.currentState?.reset();
|
||||
},
|
||||
child: Text("Clear",
|
||||
style: GoogleFonts.poppins(color: Colors.grey)),
|
||||
child: Text(
|
||||
"Clear",
|
||||
style: GoogleFonts.poppins(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _onSubmit(context),
|
||||
child: Text("Add",
|
||||
style: GoogleFonts.poppins(color: Color(0xFF114D8B))),
|
||||
child: Text(
|
||||
"Add",
|
||||
style: GoogleFonts.poppins(color: Color(0xFF114D8B)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -21,13 +21,14 @@ class ForexData extends StatefulWidget {
|
||||
final int? forexId; // <-- Add this
|
||||
final Map<String, dynamic>? forexData;
|
||||
|
||||
const ForexData(
|
||||
{super.key,
|
||||
const ForexData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetForex,
|
||||
this.forexId,
|
||||
this.forexData});
|
||||
this.forexData,
|
||||
});
|
||||
|
||||
@override
|
||||
ForexDataState createState() => ForexDataState();
|
||||
@ -61,7 +62,7 @@ class ForexDataState extends State<ForexData> {
|
||||
"currency",
|
||||
"perdiemAmount",
|
||||
"cash",
|
||||
"card"
|
||||
"card",
|
||||
];
|
||||
|
||||
Map<String, dynamic> forex_Detials() {
|
||||
@ -197,7 +198,7 @@ class ForexDataState extends State<ForexData> {
|
||||
"currency",
|
||||
"perdiemAmount",
|
||||
"cash_percentage",
|
||||
"card_percentage"
|
||||
"card_percentage",
|
||||
];
|
||||
|
||||
// Check validation for each field
|
||||
@ -273,7 +274,8 @@ class ForexDataState extends State<ForexData> {
|
||||
};
|
||||
final body = jsonEncode(forexData);
|
||||
|
||||
final response = forexDataId != null
|
||||
final response =
|
||||
forexDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
@ -326,7 +328,7 @@ class ForexDataState extends State<ForexData> {
|
||||
// Map country codes to country names
|
||||
countryMap = {
|
||||
for (var item in countryList)
|
||||
item['country_code'] as String: item['country_name'] as String
|
||||
item['country_code'] as String: item['country_name'] as String,
|
||||
};
|
||||
|
||||
// Extract only country codes for processing
|
||||
@ -355,21 +357,19 @@ class ForexDataState extends State<ForexData> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Country",
|
||||
"Country *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -381,13 +381,14 @@ class ForexDataState extends State<ForexData> {
|
||||
selectedItem: countryMap[selectedCountry],
|
||||
popupProps: PopupProps.menu(
|
||||
showSearchBox: true, // Enables search functionality
|
||||
menuProps: const MenuProps(
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
menuProps: const MenuProps(backgroundColor: Colors.white),
|
||||
constraints: BoxConstraints(maxHeight: 250),
|
||||
itemBuilder: (context, item, isSelected) => Padding(
|
||||
itemBuilder:
|
||||
(context, item, isSelected) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 6.0),
|
||||
horizontal: 8.0,
|
||||
vertical: 6.0,
|
||||
),
|
||||
child: Text(
|
||||
item,
|
||||
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||
@ -405,12 +406,11 @@ class ForexDataState extends State<ForexData> {
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownBuilder: (context, selectedItem) => Align(
|
||||
dropdownBuilder:
|
||||
(context, selectedItem) => Align(
|
||||
// Center-align selected item
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
@ -421,7 +421,8 @@ class ForexDataState extends State<ForexData> {
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
// Find the country_code based on selected country_name
|
||||
selectedCountry = countryMap.entries
|
||||
selectedCountry =
|
||||
countryMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
.key;
|
||||
selectedCountryName = newValue;
|
||||
@ -444,11 +445,12 @@ class ForexDataState extends State<ForexData> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Currency",
|
||||
"Currency *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -470,7 +472,8 @@ class ForexDataState extends State<ForexData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["currency"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -481,11 +484,9 @@ class ForexDataState extends State<ForexData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
|
||||
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -493,18 +494,20 @@ class ForexDataState extends State<ForexData> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Cash",
|
||||
"Cash (%) *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
width: widget.isDesktop
|
||||
width:
|
||||
widget.isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.09
|
||||
: MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
@ -518,13 +521,16 @@ class ForexDataState extends State<ForexData> {
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Cash",
|
||||
labelStyle:
|
||||
TextStyle(fontSize: 11, color: Colors.grey),
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["cash_percentage"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -540,18 +546,20 @@ class ForexDataState extends State<ForexData> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Card",
|
||||
"Card (%) *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
width: widget.isDesktop
|
||||
width:
|
||||
widget.isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.09
|
||||
: MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
@ -565,13 +573,16 @@ class ForexDataState extends State<ForexData> {
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Card",
|
||||
labelStyle:
|
||||
TextStyle(fontSize: 11, color: Colors.grey),
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["card_percentage"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -581,21 +592,20 @@ class ForexDataState extends State<ForexData> {
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Perdiem Amount",
|
||||
"Perdiem Amount *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -614,7 +624,8 @@ class ForexDataState extends State<ForexData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["perdiemAmount"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -625,9 +636,7 @@ class ForexDataState extends State<ForexData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
|
||||
if (forexDataId != null)
|
||||
Row(
|
||||
@ -638,7 +647,8 @@ class ForexDataState extends State<ForexData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
@ -654,13 +664,10 @@ class ForexDataState extends State<ForexData> {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
if (forexDataId != null)
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
if (forexDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -697,13 +704,17 @@ class ForexDataState extends State<ForexData> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
@ -83,11 +83,13 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -100,7 +102,7 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
|
||||
Future<List<dynamic>> fetchGetForex() async {
|
||||
orgId = await getOrgId();
|
||||
final String apiUrlData = '$apiUrl/api/getForexPerdiemList';
|
||||
final String apiUrlData = '$apiUrl/api/getForexPerdiemList?for=table_view';
|
||||
|
||||
final String? token = await getToken();
|
||||
|
||||
@ -151,7 +153,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
|
||||
if (!data.containsKey('data') || data['data'] is! List) {
|
||||
throw Exception(
|
||||
"Invalid response format: 'data' field is missing or not a List");
|
||||
"Invalid response format: 'data' field is missing or not a List",
|
||||
);
|
||||
}
|
||||
|
||||
List<dynamic> plansJson = data['data']; // 'data' is a Map, not a List
|
||||
@ -180,7 +183,10 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
}
|
||||
|
||||
Future<void> createUserData(
|
||||
Map<String, dynamic> userData, String userId, String newStatus) async {
|
||||
Map<String, dynamic> userData,
|
||||
String userId,
|
||||
String newStatus,
|
||||
) async {
|
||||
final uri = Uri.parse('$apiUrl/api/users/update/$userId');
|
||||
|
||||
final String? token = await getToken();
|
||||
@ -230,8 +236,11 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
}
|
||||
}
|
||||
|
||||
void handleToggleUserStatus(String userId, String currentStatus,
|
||||
Map<String, dynamic> userData) async {
|
||||
void handleToggleUserStatus(
|
||||
String userId,
|
||||
String currentStatus,
|
||||
Map<String, dynamic> userData,
|
||||
) async {
|
||||
print("Toggling user status - $userId (Current: $currentStatus)");
|
||||
|
||||
final String apiUrlData =
|
||||
@ -282,47 +291,35 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
});
|
||||
}
|
||||
|
||||
void filterForex1(String query) {
|
||||
print("allUsers before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredForex = allForex.where((forex) {
|
||||
return (forex['country_code']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(forex['country_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(forex['currency']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(forex['perdiem_amount']?.toLowerCase().contains(lowerQuery) ??
|
||||
false);
|
||||
}).toList();
|
||||
});
|
||||
print("filteredPlans: $filteredForex");
|
||||
}
|
||||
|
||||
void filterForex(String query) {
|
||||
print("allForex before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredForex = allForex.where((forex) {
|
||||
filteredForex =
|
||||
allForex.where((forex) {
|
||||
final isActiveStatus =
|
||||
forex['is_active'] == "1" ? "active" : "inactive";
|
||||
return (forex['country_code']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(forex['country_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(forex['currency']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(forex['currency']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(forex['perdiem_amount']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(isActiveStatus.contains(lowerQuery));
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredForex: $filteredForex");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -331,11 +328,14 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
@ -348,7 +348,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
@ -376,7 +377,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
// ? 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
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
|
||||
@ -408,9 +410,7 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.16,
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.16),
|
||||
|
||||
if (isDesktop)
|
||||
Container(
|
||||
@ -422,7 +422,9 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -434,17 +436,19 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -458,16 +462,18 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
disabledForegroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side:
|
||||
BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
side: BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ForexData(
|
||||
builder:
|
||||
(context) => ForexData(
|
||||
isDesktop: isDesktop,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetForex: refreshData,
|
||||
@ -498,10 +504,7 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
],
|
||||
),
|
||||
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
if (!isDesktop) SizedBox(height: 5),
|
||||
isDesktop
|
||||
? SizedBox.shrink()
|
||||
: Row(
|
||||
@ -516,7 +519,9 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -529,17 +534,18 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -577,14 +583,17 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Please Create Perdiem Amount",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16, color: Colors.grey),
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
@ -600,19 +609,18 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
DateTime dateA = DateTime.parse(a['created_on']);
|
||||
DateTime dateB = DateTime.parse(b['created_on']);
|
||||
|
||||
return dateB
|
||||
.compareTo(dateA); // Descending: newest first
|
||||
return dateB.compareTo(dateA); // Descending: newest first
|
||||
});
|
||||
|
||||
List paginatedForex = forex
|
||||
List paginatedForex =
|
||||
forex
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
|
||||
Widget table = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double minWidth =
|
||||
isDesktop ? constraints.maxWidth : 1300;
|
||||
double minWidth = isDesktop ? constraints.maxWidth : 1300;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: minWidth),
|
||||
@ -621,7 +629,9 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5, color: Colors.grey.shade200),
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
@ -629,76 +639,105 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
'Country Code',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Country',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Currency',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Perdiem Amount',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: paginatedForex.map((forex) {
|
||||
String forexId = forex['forex_perdiem_id']
|
||||
rows:
|
||||
paginatedForex.map((forex) {
|
||||
String forexId =
|
||||
forex['forex_perdiem_id']
|
||||
.toString(); // Get user ID
|
||||
bool isSelected = selectedUserId == forexId;
|
||||
|
||||
return DataRow(cells: [
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text("${forex['country_code'] ?? ''}",
|
||||
Text(
|
||||
"${forex['country_code'] ?? ''}",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(forex['country_name'] ?? '',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
forex['country_name'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(forex['currency'] ?? 'N/A',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
forex['currency'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(forex['perdiem_amount'] ?? 'N/A',
|
||||
Text(
|
||||
forex['perdiem_amount'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
forex['is_active'] == "1"
|
||||
@ -722,17 +761,22 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
// apiService.getSingleUser(id),
|
||||
// ),
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit ForEx Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final forexId = int.tryParse(
|
||||
forex['forex_perdiem_id']
|
||||
.toString());
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (forexId != null) {
|
||||
print("ForexId -- $forexId");
|
||||
@ -742,9 +786,11 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ForexData(
|
||||
builder:
|
||||
(context) => ForexData(
|
||||
isDesktop: isDesktop,
|
||||
forexId: forexId, // Pass the ID
|
||||
forexId:
|
||||
forexId, // Pass the ID
|
||||
forexData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetForex: fetchGetForex,
|
||||
@ -759,7 +805,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@ -774,7 +821,9 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -794,21 +843,26 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit Forex Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final forexId = int.tryParse(
|
||||
forex['forex_perdiem_id']
|
||||
.toString());
|
||||
forex['forex_perdiem_id'].toString(),
|
||||
);
|
||||
|
||||
if (forexId != null) {
|
||||
print("ForexId -- $forexId");
|
||||
@ -818,7 +872,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ForexData(
|
||||
builder:
|
||||
(context) => ForexData(
|
||||
isDesktop: isDesktop,
|
||||
forexId:
|
||||
forexId, // Pass the ID
|
||||
@ -924,7 +979,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -943,13 +999,12 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
forex['currency'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
@ -958,7 +1013,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
forex['perdiem_amount'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -979,7 +1035,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredForex.isEmpty
|
||||
? Center(
|
||||
@ -987,7 +1044,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@ -1001,7 +1059,8 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(paginatedForex)),
|
||||
@ -1035,9 +1094,11 @@ class ForexDataListState extends State<ForexDataList> {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,13 +21,14 @@ class GroupData extends StatefulWidget {
|
||||
final int? groupId; // <-- Add this
|
||||
final Map<String, dynamic>? groupData;
|
||||
|
||||
const GroupData(
|
||||
{super.key,
|
||||
const GroupData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetGroup,
|
||||
this.groupId,
|
||||
this.groupData});
|
||||
this.groupData,
|
||||
});
|
||||
|
||||
@override
|
||||
GroupDataState createState() => GroupDataState();
|
||||
@ -46,7 +47,6 @@ class GroupDataState extends State<GroupData> {
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
Map<String, String> errorMessages = {};
|
||||
|
||||
|
||||
List<dynamic> domesticList = [];
|
||||
List<dynamic> internationalList = [];
|
||||
|
||||
@ -166,7 +166,6 @@ class GroupDataState extends State<GroupData> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
bool validateData() {
|
||||
errorMessages.clear();
|
||||
|
||||
@ -179,10 +178,7 @@ class GroupDataState extends State<GroupData> {
|
||||
"international_policy_name": selectedInternationalPolicyName,
|
||||
};
|
||||
|
||||
final requiredFields = [
|
||||
"name",
|
||||
"description",
|
||||
];
|
||||
final requiredFields = ["name", "description"];
|
||||
|
||||
// Check validation for each field
|
||||
for (String field in requiredFields) {
|
||||
@ -240,7 +236,8 @@ class GroupDataState extends State<GroupData> {
|
||||
};
|
||||
final body = jsonEncode(groupData);
|
||||
|
||||
final response = groupDataId != null
|
||||
final response =
|
||||
groupDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
@ -288,7 +285,7 @@ class GroupDataState extends State<GroupData> {
|
||||
// Map id to names
|
||||
DomesticMap = {
|
||||
for (var object in domesticList)
|
||||
object['policy_id'] as String: object['name'] as String
|
||||
object['policy_id'] as String: object['name'] as String,
|
||||
};
|
||||
|
||||
// print("domestic -- map--$DomesticMap");
|
||||
@ -302,7 +299,7 @@ class GroupDataState extends State<GroupData> {
|
||||
|
||||
InternationalMap = {
|
||||
for (var item in internationalList)
|
||||
item['policy_id'] as String: item['name'] as String
|
||||
item['policy_id'] as String: item['name'] as String,
|
||||
};
|
||||
|
||||
// Extract only id for processing
|
||||
@ -330,20 +327,18 @@ class GroupDataState extends State<GroupData> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 5),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Name",
|
||||
"Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -362,7 +357,8 @@ class GroupDataState extends State<GroupData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -382,7 +378,8 @@ class GroupDataState extends State<GroupData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -391,16 +388,18 @@ class GroupDataState extends State<GroupData> {
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: DropdownSearch<String>(
|
||||
selectedItem: InternationalMap[selectedInternationalPolicyID],
|
||||
selectedItem:
|
||||
InternationalMap[selectedInternationalPolicyID],
|
||||
popupProps: PopupProps.menu(
|
||||
showSearchBox: true, // Enables search functionality
|
||||
menuProps: const MenuProps(
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
menuProps: const MenuProps(backgroundColor: Colors.white),
|
||||
constraints: BoxConstraints(maxHeight: 250),
|
||||
itemBuilder: (context, item, isSelected) => Padding(
|
||||
itemBuilder:
|
||||
(context, item, isSelected) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 6.0),
|
||||
horizontal: 8.0,
|
||||
vertical: 6.0,
|
||||
),
|
||||
child: Text(
|
||||
item,
|
||||
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||
@ -418,12 +417,11 @@ class GroupDataState extends State<GroupData> {
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownBuilder: (context, selectedItem) => Align(
|
||||
dropdownBuilder:
|
||||
(context, selectedItem) => Align(
|
||||
// Center-align selected item
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
@ -434,7 +432,8 @@ class GroupDataState extends State<GroupData> {
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
// Find the country_code based on selected country_name
|
||||
selectedInternationalPolicyID = InternationalMap.entries
|
||||
selectedInternationalPolicyID =
|
||||
InternationalMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
.key;
|
||||
selectedInternationalPolicyName = newValue;
|
||||
@ -454,7 +453,8 @@ class GroupDataState extends State<GroupData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -466,13 +466,14 @@ class GroupDataState extends State<GroupData> {
|
||||
selectedItem: DomesticMap[selectedDomesticPolicyID],
|
||||
popupProps: PopupProps.menu(
|
||||
showSearchBox: true, // Enables search functionality
|
||||
menuProps: const MenuProps(
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
menuProps: const MenuProps(backgroundColor: Colors.white),
|
||||
constraints: BoxConstraints(maxHeight: 250),
|
||||
itemBuilder: (context, object, isSelected) => Padding(
|
||||
itemBuilder:
|
||||
(context, object, isSelected) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 6.0),
|
||||
horizontal: 8.0,
|
||||
vertical: 6.0,
|
||||
),
|
||||
child: Text(
|
||||
object,
|
||||
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||
@ -490,12 +491,11 @@ class GroupDataState extends State<GroupData> {
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownBuilder: (context, selectedItem) => Align(
|
||||
dropdownBuilder:
|
||||
(context, selectedItem) => Align(
|
||||
// Center-align selected item
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
@ -506,7 +506,8 @@ class GroupDataState extends State<GroupData> {
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
// Find the country_code based on selected country_name
|
||||
selectedDomesticPolicyID = DomesticMap.entries
|
||||
selectedDomesticPolicyID =
|
||||
DomesticMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
.key;
|
||||
selectedDomesticPolicyName = newValue;
|
||||
@ -522,11 +523,12 @@ class GroupDataState extends State<GroupData> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Description",
|
||||
"Description *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -559,9 +561,7 @@ class GroupDataState extends State<GroupData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
if (groupDataId != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -571,7 +571,8 @@ class GroupDataState extends State<GroupData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
@ -587,13 +588,10 @@ class GroupDataState extends State<GroupData> {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
if (groupDataId != null)
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
if (groupDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -630,13 +628,17 @@ class GroupDataState extends State<GroupData> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
433
lib/Screens/group/groupListBackUp.dart
Normal file
433
lib/Screens/group/groupListBackUp.dart
Normal file
@ -0,0 +1,433 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/Screens/group/group.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import 'groupDetails.dart';
|
||||
|
||||
class GroupListBackUp extends StatefulWidget {
|
||||
@override
|
||||
_GroupListBackUpState createState() => _GroupListBackUpState();
|
||||
}
|
||||
|
||||
class _GroupListBackUpState extends State<GroupListBackUp> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
List<dynamic>? apiAllGroups;
|
||||
Color? layoutColor;
|
||||
Color? bodyColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadAllGroups();
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadAllGroups() async {
|
||||
try {
|
||||
final result = await apiService.fetchAllGroup();
|
||||
setState(() {
|
||||
apiAllGroups = result;
|
||||
});
|
||||
print("Fetched services: $apiAllGroups");
|
||||
} catch (e) {
|
||||
print('Error fetching role list: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void handleActiveStatus(
|
||||
Map<String, dynamic> groupData,
|
||||
String groupId,
|
||||
String currentStatus,
|
||||
) async {
|
||||
print("Toggling user status - $groupId (Current: $currentStatus)");
|
||||
|
||||
final String apiUrlData =
|
||||
'$apiUrl/api/groups/update/$groupId'; // API for updating user
|
||||
final String? token = await getToken();
|
||||
|
||||
if (token == null) {
|
||||
print("Error: Token not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1")
|
||||
String newStatus = (currentStatus == "1") ? "0" : "1";
|
||||
|
||||
print("STatus 1 - $newStatus");
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrlData),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
"is_active": newStatus // Set new status dynamically
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
print("User status updated successfully to $newStatus!");
|
||||
loadAllGroups(); // Refresh users list after update
|
||||
} else {
|
||||
print("Failed to update user status. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error updating user status: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void deleteGroup(Map<String, dynamic> groupdata, groupId, status) {
|
||||
print("GroupId : $groupId");
|
||||
print("Groupstatus: $status");
|
||||
print("GroupsData: $groupdata");
|
||||
|
||||
// handleActiveStatus(groupdata, groupId, status);
|
||||
print("Calling handleActiveStatus with: id=$groupId, status=$status");
|
||||
handleActiveStatus(groupdata, groupId.toString(), status.toString());
|
||||
}
|
||||
|
||||
Future<void> refreshData() async {
|
||||
loadAllGroups();
|
||||
}
|
||||
|
||||
// Future<void> deleteGroupFromApi(int groupId) async {
|
||||
// try {
|
||||
// await apiService.deleteGroup(groupId); // your delete API call
|
||||
// deleteGroup(groupId); // remove from UI list
|
||||
// } catch (e) {
|
||||
// print('Error deleting group: $e');
|
||||
// }
|
||||
// }'
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
// backgroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
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(8),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(child: buildGroupListLayout(isDesktop))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildGroupListLayout(bool isDesktop) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
// decoration: BoxDecoration(
|
||||
// // color: Colors.amber,
|
||||
// // color: bodyColor,
|
||||
// color: Color(0xFFE1F5FE),
|
||||
// border: Border.all(
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// width: 3.5)),
|
||||
child: buildGroupData(isDesktop),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget buildGroupListView(bool isDesktop) {
|
||||
// return Container(
|
||||
// child: Text("DAta"),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget buildGroupData(isDesktop) {
|
||||
return 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,
|
||||
// ),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Group List',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFF114D8B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// side: BorderSide(color: , width: 1),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: () async {
|
||||
// List<dynamic> users = await futureUsers;
|
||||
// context.go('/CreateGroup');
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => GroupData(
|
||||
isDesktop: isDesktop,
|
||||
groupId: null,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetGroup: refreshData
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text('New Group', style: GoogleFonts.poppins(fontSize: 12)),
|
||||
SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Icon(
|
||||
Icons.add_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
padding: const EdgeInsets.all(10),
|
||||
// margin: const EdgeInsets.only(bottom: 10),
|
||||
color: Colors.white,
|
||||
// color: Colors.red.shade100,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
children: [
|
||||
buildGroupListView(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupListView(bool isDesktop) {
|
||||
if (apiAllGroups == null || apiAllGroups!.isEmpty) {
|
||||
return Center(child: Text("No groups found."));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: apiAllGroups!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final group = apiAllGroups![index];
|
||||
return Card(
|
||||
// color: bodyColor,
|
||||
// color: Color(0xFFF5F5F5),
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text("Group Name",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("Domestic Policy",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("International Policy",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("Description",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text("${group['name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("${group['domestic_policy_name'] ?? 'N/A'}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("${group['international_policy_name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600))),
|
||||
Expanded(
|
||||
child: Text("${group['description'] ?? 'N/A'}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600))),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (group['group_id'] != null) {
|
||||
final newGroupID = int.tryParse(
|
||||
group['group_id'].toString());
|
||||
if (newGroupID != null) {
|
||||
final data = await apiService.getGroupDetailsFind(
|
||||
newGroupID); // ✅ Always an int
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
GroupData(
|
||||
isDesktop: isDesktop,
|
||||
groupId: newGroupID,
|
||||
// Pass the ID
|
||||
groupData: data,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetGroup: refreshData
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
print("something went wrong check properly");
|
||||
}
|
||||
},
|
||||
|
||||
// onTap: () {
|
||||
// context.go("/CreateGroup", extra: group);
|
||||
// },
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
),
|
||||
SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final idStr = group['group_id'];
|
||||
final id = int.tryParse(idStr.toString());
|
||||
|
||||
if (id == null) {
|
||||
print("group_id is null");
|
||||
return;
|
||||
}
|
||||
final status = group['is_active'];
|
||||
// print("GroupId : ${group['group_id']} ");
|
||||
deleteGroup(group, id, status);
|
||||
},
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -20,13 +20,14 @@ class HotelsData extends StatefulWidget {
|
||||
final int? hotelsId; // <-- Add this
|
||||
final Map<String, dynamic>? hotelsData;
|
||||
|
||||
const HotelsData(
|
||||
{super.key,
|
||||
const HotelsData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetHotels,
|
||||
this.hotelsId,
|
||||
this.hotelsData});
|
||||
this.hotelsData,
|
||||
});
|
||||
|
||||
@override
|
||||
HotelsDataState createState() => HotelsDataState();
|
||||
@ -110,7 +111,8 @@ class HotelsDataState extends State<HotelsData> {
|
||||
if (data == null) return;
|
||||
setState(() {
|
||||
selectedCountry = data['country_code']; // For dropdown
|
||||
selectedCountryName = data['country_name']; // For dropdown label or display
|
||||
selectedCountryName =
|
||||
data['country_name']; // For dropdown label or display
|
||||
controllers['city']?.text = data['city'] ?? '';
|
||||
controllers['hotel_chain']?.text = data['hotel_chain'] ?? '';
|
||||
controllers['hotel_name']?.text = data['hotel_name'] ?? '';
|
||||
@ -148,7 +150,12 @@ class HotelsDataState extends State<HotelsData> {
|
||||
"city": controllers["city"]?.text,
|
||||
};
|
||||
|
||||
final requiredFields = ["hotel_name","hotel_chain","country_code","city"];
|
||||
final requiredFields = [
|
||||
"hotel_name",
|
||||
"hotel_chain",
|
||||
"country_code",
|
||||
"city",
|
||||
];
|
||||
|
||||
// Check validation for each field
|
||||
for (String field in requiredFields) {
|
||||
@ -174,7 +181,6 @@ class HotelsDataState extends State<HotelsData> {
|
||||
}
|
||||
|
||||
Future<void> postHotelsData({int isActive = 1}) async {
|
||||
|
||||
final hotelsData = hotels_Details();
|
||||
|
||||
final String apiUrldata;
|
||||
@ -184,16 +190,20 @@ class HotelsDataState extends State<HotelsData> {
|
||||
apiUrldata = '$apiUrl/api/updateHotels/$hotelsDataId';
|
||||
hotelsData["hotel_id"] = hotelsDataId.toString();
|
||||
hotelsData["updated_by"] = userId;
|
||||
(hotelsData.containsKey("created_by")) ? hotelsData.remove("created_by") : '' ;
|
||||
(hotelsData.containsKey("country_name")) ? hotelsData.remove("country_name") : '' ;
|
||||
|
||||
|
||||
(hotelsData.containsKey("created_by"))
|
||||
? hotelsData.remove("created_by")
|
||||
: '';
|
||||
(hotelsData.containsKey("country_name"))
|
||||
? hotelsData.remove("country_name")
|
||||
: '';
|
||||
} else {
|
||||
print("for add Hotel id - null");
|
||||
apiUrldata = '$apiUrl/api/createHotels';
|
||||
print("called apiUrl - $apiUrldata");
|
||||
hotelsData["created_by"] = userId;
|
||||
(hotelsData.containsKey("country_name")) ? hotelsData.remove("country_name") : '' ;
|
||||
(hotelsData.containsKey("country_name"))
|
||||
? hotelsData.remove("country_name")
|
||||
: '';
|
||||
}
|
||||
|
||||
final token = await getToken(); // Fetch token
|
||||
@ -210,7 +220,8 @@ class HotelsDataState extends State<HotelsData> {
|
||||
};
|
||||
final body = jsonEncode(hotelsData);
|
||||
|
||||
final response = hotelsDataId != null
|
||||
final response =
|
||||
hotelsDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
@ -254,7 +265,7 @@ class HotelsDataState extends State<HotelsData> {
|
||||
// Map country codes to country names
|
||||
countryMap = {
|
||||
for (var item in countryList)
|
||||
item['country_code'] as String: item['country_name'] as String
|
||||
item['country_code'] as String: item['country_name'] as String,
|
||||
};
|
||||
|
||||
// Extract only country codes for processing
|
||||
@ -281,20 +292,18 @@ class HotelsDataState extends State<HotelsData> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Hotel Name",
|
||||
"Hotel Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -313,7 +322,8 @@ class HotelsDataState extends State<HotelsData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["hotel_name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -329,11 +339,12 @@ class HotelsDataState extends State<HotelsData> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Hotel Chain",
|
||||
"Hotel Chain *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -352,7 +363,8 @@ class HotelsDataState extends State<HotelsData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["hotel_chain"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -363,16 +375,104 @@ class HotelsDataState extends State<HotelsData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
|
||||
// - It has been observed that many of the dropdowns have overlapping issues, causing label names to be hidden - just copied searchable dropdown - still not completed (user mangement screen only ) master page except policy - my trips - flight taxi train insurance, visa misscenllo color white size padding data
|
||||
// - Delete option is not working in the policy list page - completed
|
||||
// - Label Names for all the modules should be set bold as it is looking like normal text in user management compared to trips page - completed
|
||||
// - In the masters org mangement search option not working for traveller - particular 4 master page - - issues occur - commpleted - email master working fine, traveller master working fine, amount master working fine, group - completed
|
||||
// - QC- Authentication - - completed - ask to check
|
||||
const SizedBox(height: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"City",
|
||||
"Country *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: DropdownSearch<String>(
|
||||
selectedItem: countryMap[selectedCountry],
|
||||
popupProps: PopupProps.menu(
|
||||
showSearchBox: true, // Enables search functionality
|
||||
menuProps: const MenuProps(backgroundColor: Colors.white),
|
||||
constraints: BoxConstraints(maxHeight: 250),
|
||||
itemBuilder:
|
||||
(context, item, isSelected) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 6.0,
|
||||
),
|
||||
child: Text(
|
||||
item,
|
||||
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||
),
|
||||
),
|
||||
searchFieldProps: TextFieldProps(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search Country...",
|
||||
hintStyle: GoogleFonts.poppins(fontSize: 11),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
items: countryMap.values.toList(),
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 1),
|
||||
),
|
||||
),
|
||||
dropdownBuilder:
|
||||
(context, selectedItem) => Align(
|
||||
// Center-align selected item
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
selectedItem ?? "Select Country",
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
// Find the country_code based on selected country_name
|
||||
selectedCountry =
|
||||
countryMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
.key;
|
||||
selectedCountryName = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["country_code"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["country_code"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"City *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
@ -391,7 +491,8 @@ class HotelsDataState extends State<HotelsData> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["city"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
@ -402,86 +503,7 @@ class HotelsDataState extends State<HotelsData> {
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Country",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: DropdownSearch<String>(
|
||||
selectedItem: countryMap[selectedCountry],
|
||||
popupProps: PopupProps.menu(
|
||||
showSearchBox: true, // Enables search functionality
|
||||
menuProps: const MenuProps(
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: 250),
|
||||
itemBuilder: (context, item, isSelected) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 6.0),
|
||||
child: Text(
|
||||
item,
|
||||
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||
),
|
||||
),
|
||||
searchFieldProps: TextFieldProps(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search Country...",
|
||||
hintStyle: GoogleFonts.poppins(fontSize: 11),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
items: countryMap.values.toList(),
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownBuilder: (context, selectedItem) => Align(
|
||||
// Center-align selected item
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
selectedItem ?? "Select Country",
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
// Find the country_code based on selected country_name
|
||||
selectedCountry = countryMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
.key;
|
||||
selectedCountryName = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["country_code"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["country_code"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox( height: 15 ),
|
||||
SizedBox(height: 10),
|
||||
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
|
||||
if (hotelsDataId != null)
|
||||
Row(
|
||||
@ -492,7 +514,8 @@ class HotelsDataState extends State<HotelsData> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
@ -508,13 +531,10 @@ class HotelsDataState extends State<HotelsData> {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hotelsDataId != null)
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
if (hotelsDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -551,13 +571,17 @@ class HotelsDataState extends State<HotelsData> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
@ -83,11 +83,13 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -100,7 +102,7 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
|
||||
Future<List<dynamic>> fetchGetHotels() async {
|
||||
orgId = await getOrgId();
|
||||
final String apiUrlData = '$apiUrl/api/getHotels';
|
||||
final String apiUrlData = '$apiUrl/api/getHotels?for=table_view';
|
||||
|
||||
final String? token = await getToken();
|
||||
|
||||
@ -150,7 +152,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
|
||||
if (!data.containsKey('data') || data['data'] is! List) {
|
||||
throw Exception(
|
||||
"Invalid response format: 'data' field is missing or not a List");
|
||||
"Invalid response format: 'data' field is missing or not a List",
|
||||
);
|
||||
}
|
||||
|
||||
List<dynamic> plansJson = data['data']; // 'data' is a Map, not a List
|
||||
@ -174,7 +177,6 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Refresh user list after update
|
||||
void refreshUserList() {
|
||||
setState(() {
|
||||
@ -187,27 +189,34 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
print("allHotels before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredHotels = allHotels.where((hotels) {
|
||||
filteredHotels =
|
||||
allHotels.where((hotels) {
|
||||
final isActiveStatus =
|
||||
hotels['is_active'] == "1" ? "active" : "inactive";
|
||||
return (hotels['country_code']?.toLowerCase().contains(lowerQuery) ??
|
||||
return (hotels['country_code']?.toLowerCase().contains(
|
||||
lowerQuery,
|
||||
) ??
|
||||
false) ||
|
||||
(hotels['country_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(hotels['city']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(hotels['hotel_chain']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
(hotels['hotel_chain']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(hotels['hotel_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(isActiveStatus.contains(lowerQuery));
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredHotels: $filteredHotels");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -216,11 +225,14 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
@ -233,7 +245,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
@ -261,7 +274,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
// ? 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
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
|
||||
@ -293,9 +307,7 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.16,
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.16),
|
||||
|
||||
if (isDesktop)
|
||||
Container(
|
||||
@ -307,7 +319,9 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -319,17 +333,19 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200, width: 0.5),
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -343,16 +359,18 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
disabledForegroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side:
|
||||
BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
side: BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => HotelsData(
|
||||
builder:
|
||||
(context) => HotelsData(
|
||||
isDesktop: isDesktop,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetHotels: refreshData,
|
||||
@ -383,10 +401,7 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
],
|
||||
),
|
||||
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
if (!isDesktop) SizedBox(height: 5),
|
||||
isDesktop
|
||||
? SizedBox.shrink()
|
||||
: Row(
|
||||
@ -401,7 +416,9 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
@ -414,17 +431,18 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
@ -462,14 +480,17 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Please Create Hotels",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16, color: Colors.grey),
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
@ -485,19 +506,18 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
DateTime dateA = DateTime.parse(a['created_on']);
|
||||
DateTime dateB = DateTime.parse(b['created_on']);
|
||||
|
||||
return dateB
|
||||
.compareTo(dateA); // Descending: newest first
|
||||
return dateB.compareTo(dateA); // Descending: newest first
|
||||
});
|
||||
|
||||
List paginatedHotels = hotels
|
||||
List paginatedHotels =
|
||||
hotels
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
|
||||
Widget table = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double minWidth =
|
||||
isDesktop ? constraints.maxWidth : 1300;
|
||||
double minWidth = isDesktop ? constraints.maxWidth : 1300;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: minWidth),
|
||||
@ -506,7 +526,9 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5, color: Colors.grey.shade200),
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
@ -514,74 +536,105 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
'Hotel Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Hotel Chain',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'City',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Country',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: paginatedHotels.map((hotels) {
|
||||
String hotelsId = hotels['hotel_id']
|
||||
rows:
|
||||
paginatedHotels.map((hotels) {
|
||||
String hotelsId =
|
||||
hotels['hotel_id']
|
||||
.toString(); // Get user ID
|
||||
bool isSelected = selectedUserId == hotelsId;
|
||||
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(hotels['hotel_name'] ?? 'N/A',
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
hotels['hotel_name'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
DataCell(Text(hotels['hotel_chain'] ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
hotels['hotel_chain'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
DataCell(Text(hotels['city'] ?? 'N/A',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
hotels['city'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
DataCell(Text(hotels['country_name'] ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
hotels['country_name'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
))),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
hotels['is_active'] == "1"
|
||||
@ -590,7 +643,10 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color: hotels['is_active'] == "1" ? Colors.green : Colors.red,
|
||||
color:
|
||||
hotels['is_active'] == "1"
|
||||
? Colors.green
|
||||
: Colors.grey,
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -603,17 +659,21 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
// apiService.getSingleUser(id),
|
||||
// ),
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit Hotel Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final hotelsId = int.tryParse(
|
||||
hotels['hotel_id']
|
||||
.toString());
|
||||
hotels['hotel_id'].toString(),
|
||||
);
|
||||
|
||||
if (hotelsId != null) {
|
||||
print("HotelsId -- $hotelsId");
|
||||
@ -623,9 +683,11 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => HotelsData(
|
||||
builder:
|
||||
(context) => HotelsData(
|
||||
isDesktop: isDesktop,
|
||||
hotelsId: hotelsId, // Pass the ID
|
||||
hotelsId:
|
||||
hotelsId, // Pass the ID
|
||||
hotelsData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetHotels: fetchGetHotels,
|
||||
@ -640,7 +702,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@ -655,7 +718,9 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -675,21 +740,26 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
child: Tooltip(
|
||||
message: 'Edit Hotel Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15),
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final hotelsId = int.tryParse(
|
||||
hotels['hotel_id']
|
||||
.toString());
|
||||
hotels['hotel_id'].toString(),
|
||||
);
|
||||
|
||||
if (hotelsId != null) {
|
||||
print("HotelsId -- $hotelsId");
|
||||
@ -699,7 +769,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => HotelsData(
|
||||
builder:
|
||||
(context) => HotelsData(
|
||||
isDesktop: isDesktop,
|
||||
hotelsId:
|
||||
hotelsId, // Pass the ID
|
||||
@ -732,7 +803,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -750,7 +822,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -767,7 +840,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
hotels['country_name'] ?? '',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87),
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -787,7 +861,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredHotels.isEmpty
|
||||
? Center(
|
||||
@ -795,7 +870,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@ -809,7 +885,8 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(paginatedHotels)),
|
||||
@ -843,9 +920,11 @@ class HotelsDataListState extends State<HotelsDataList> {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -14,12 +14,13 @@ class TaxiScreen extends StatefulWidget {
|
||||
final Map<String, dynamic>? selectedItem;
|
||||
final String? loginUser;
|
||||
|
||||
TaxiScreen(
|
||||
{required this.onClose,
|
||||
TaxiScreen({
|
||||
required this.onClose,
|
||||
this.apiData,
|
||||
required this.onSavetaxi,
|
||||
required this.selectedItem,
|
||||
required this.loginUser});
|
||||
required this.loginUser,
|
||||
});
|
||||
|
||||
@override
|
||||
_TaxiScreenState createState() => _TaxiScreenState();
|
||||
@ -97,13 +98,17 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
super.initState();
|
||||
|
||||
_addFocusListener(
|
||||
_destinationFocusNode, (focus) => _destinationFocus = focus);
|
||||
_destinationFocusNode,
|
||||
(focus) => _destinationFocus = focus,
|
||||
);
|
||||
_addFocusListener(_locationFocusNode, (focus) => _locationFocus = focus);
|
||||
_addFocusListener(_dateFocusNode, (focus) => _dateFocus = focus);
|
||||
_addFocusListener(_timeFocusNode, (focus) => _timeFocus = focus);
|
||||
_addFocusListener(_taxiReqFocusNode, (focus) => _taxiReqFocused = focus);
|
||||
_addFocusListener(
|
||||
_numPassengerFocusNode, (focus) => _numPassengerFocus = focus);
|
||||
_numPassengerFocusNode,
|
||||
(focus) => _numPassengerFocus = focus,
|
||||
);
|
||||
_addFocusListener(_commentsFocusNode, (focus) => _commentsFocus = focus);
|
||||
|
||||
_destinationController = initController("destination_city");
|
||||
@ -168,7 +173,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
"location_of_pickup",
|
||||
"no_of_passengers",
|
||||
"date",
|
||||
"time"
|
||||
"time",
|
||||
];
|
||||
|
||||
// Check validation for each field
|
||||
@ -199,9 +204,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isMobile = sizingInfo.isMobile;
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Container(
|
||||
// color: Color(0xFFF4F4FB),
|
||||
@ -216,13 +223,14 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
child: Center(
|
||||
child: Column(children: _buildAccomadtionForm(isDesktop)),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildAccomadtionForm(bool isDesktop) {
|
||||
@ -235,7 +243,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
|
||||
List<List<Widget>> rowBuilders = [
|
||||
// _builClassType(isDesktop),
|
||||
_buildSecondRow(isDesktop)
|
||||
_buildSecondRow(isDesktop),
|
||||
];
|
||||
|
||||
return [
|
||||
@ -253,19 +261,24 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
List<Widget> _buildFirstRow(isDesktop) {
|
||||
List<dynamic> purposeList = widget.apiData?['taxt_car_type'] ?? [];
|
||||
|
||||
List<DropdownMenuItem<String>> dropdownItems = purposeList
|
||||
.map((item) => DropdownMenuItem<String>(
|
||||
List<DropdownMenuItem<String>> dropdownItems =
|
||||
purposeList
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item['dropdown_key'],
|
||||
child: Text(item['dropdown_value']),
|
||||
))
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (dropdownItems.isEmpty) {
|
||||
dropdownItems.add(
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text("No options available",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
child: Text(
|
||||
"No options available",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -283,20 +296,16 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
isDesktop
|
||||
? Row(children: _buildTripType(isDesktop))
|
||||
: Column(children: _buildTripType(isDesktop))
|
||||
: Column(children: _buildTripType(isDesktop)),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -305,7 +314,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -319,8 +329,9 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: const TextStyle(fontSize: 12),
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
r'^\d*\.?\d*$')), // Allow only positive numbers with optional decimal
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d*\.?\d*$'),
|
||||
), // Allow only positive numbers with optional decimal
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Number of Passenger",
|
||||
@ -334,19 +345,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
if (errorMessages["no_of_passengers"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -355,7 +358,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -369,16 +373,19 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10), // Proper padding
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
), // Proper padding
|
||||
),
|
||||
onChanged: purposeList.isNotEmpty
|
||||
onChanged:
|
||||
purposeList.isNotEmpty
|
||||
? (newValue) {
|
||||
setState(() {
|
||||
selectedCarType = newValue;
|
||||
});
|
||||
print(
|
||||
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
|
||||
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}",
|
||||
);
|
||||
}
|
||||
: null,
|
||||
|
||||
@ -388,31 +395,31 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox.shrink()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) SizedBox.shrink() else SizedBox(height: 8),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildTripType(bool isDesktop) {
|
||||
List<dynamic> purposeList = widget.apiData?['taxi_car_required_for'] ?? [];
|
||||
|
||||
List<DropdownMenuItem<String>> dropdownItems = purposeList
|
||||
.map((item) => DropdownMenuItem<String>(
|
||||
List<DropdownMenuItem<String>> dropdownItems =
|
||||
purposeList
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item['dropdown_key'],
|
||||
child: Text(item['dropdown_value']),
|
||||
))
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (dropdownItems.isEmpty) {
|
||||
dropdownItems.add(
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text("No options available",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
child: Text(
|
||||
"No options available",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -425,7 +432,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
CustomTextFieldWrapper(
|
||||
isFocused: _taxiReqFocused,
|
||||
isDesktop: isDesktop,
|
||||
width: isDesktop
|
||||
width:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.34
|
||||
: MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
@ -436,16 +444,19 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10), // Proper padding
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
), // Proper padding
|
||||
),
|
||||
onChanged: purposeList.isNotEmpty
|
||||
onChanged:
|
||||
purposeList.isNotEmpty
|
||||
? (newValue) {
|
||||
setState(() {
|
||||
selectedReqTaxi = newValue;
|
||||
});
|
||||
print(
|
||||
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
|
||||
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}",
|
||||
);
|
||||
}
|
||||
: null,
|
||||
|
||||
@ -466,7 +477,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
|
||||
DateTime? pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedCheckOutDate != null &&
|
||||
initialDate:
|
||||
_selectedCheckOutDate != null &&
|
||||
_selectedCheckOutDate!.isAfter(today)
|
||||
? _selectedCheckOutDate!
|
||||
: today,
|
||||
@ -494,8 +506,13 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
// Formatting time to HH:mm (24-hour format)
|
||||
final now = DateTime.now();
|
||||
final formattedTime = DateFormat('HH:mm').format(
|
||||
DateTime(now.year, now.month, now.day, pickedTime.hour,
|
||||
pickedTime.minute),
|
||||
DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
pickedTime.hour,
|
||||
pickedTime.minute,
|
||||
),
|
||||
);
|
||||
_timeController.text = formattedTime;
|
||||
});
|
||||
@ -511,7 +528,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -535,28 +553,21 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
if (errorMessages["destination_city"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Location of Pickup",
|
||||
"Pickup Location",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -569,7 +580,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
controller: _locationController,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Location of Pickup",
|
||||
labelText: "Pickup Location",
|
||||
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
@ -580,19 +591,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
if (errorMessages["location_of_pickup"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -601,7 +604,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -622,8 +626,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
suffixIcon: Icon(Icons.calendar_today,
|
||||
size: 16, color: Colors.grey),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -632,19 +639,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
if (errorMessages["date"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -653,7 +652,8 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -674,8 +674,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
suffixIcon:
|
||||
Icon(Icons.access_time, size: 16, color: Colors.grey),
|
||||
suffixIcon: Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -684,10 +687,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
if (errorMessages["time"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
@ -704,13 +704,15 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldWrapper(
|
||||
isFocused: _commentsFocus, // Dropdown doesn't use focus
|
||||
isDesktop: isDesktop,
|
||||
width: isDesktop
|
||||
width:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.34
|
||||
: MediaQuery.of(context).size.width * 0.66,
|
||||
|
||||
@ -733,9 +735,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
],
|
||||
),
|
||||
if (isDesktop) Spacer(),
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
@ -756,9 +756,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[400], // Light grey color
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
@ -767,7 +765,6 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10), // Space between buttons
|
||||
|
||||
// Save Changes Button
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
@ -775,9 +772,7 @@ class _TaxiScreenState extends State<TaxiScreen> {
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF114D8B), // Primary color for save
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
@ -17,14 +17,15 @@ class TrainScreen extends StatefulWidget {
|
||||
final String? loginUser;
|
||||
final String? tripType;
|
||||
|
||||
TrainScreen(
|
||||
{required this.onClose,
|
||||
TrainScreen({
|
||||
required this.onClose,
|
||||
this.apiData,
|
||||
required this.onSavetrain,
|
||||
required this.selectedItem,
|
||||
required this.loginUser,
|
||||
this.apiDataForClass,
|
||||
this.tripType});
|
||||
this.tripType,
|
||||
});
|
||||
|
||||
@override
|
||||
_TrainScreenState createState() => _TrainScreenState();
|
||||
@ -232,7 +233,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
"from_station",
|
||||
"to_station",
|
||||
"date",
|
||||
"time"
|
||||
"time",
|
||||
];
|
||||
|
||||
// Check validation for each field
|
||||
@ -287,9 +288,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isMobile = sizingInfo.isMobile;
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Container(
|
||||
// color: Color(0xFFF4F4FB),
|
||||
@ -325,13 +328,14 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
child: Center(
|
||||
child: Column(children: _buildAccomadtionForm(isDesktop)),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildAccomadtionForm(bool isDesktop) {
|
||||
@ -344,7 +348,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
|
||||
List<List<Widget>> rowBuilders = [
|
||||
_builClassType(isDesktop),
|
||||
_buildSecondRow(isDesktop)
|
||||
_buildSecondRow(isDesktop),
|
||||
];
|
||||
|
||||
return [
|
||||
@ -369,7 +373,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
isDesktop
|
||||
@ -377,38 +382,35 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
: Column(children: _buildTripType(isDesktop)),
|
||||
if (errorMessages["train_no"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildTripType(bool isDesktop) {
|
||||
List<dynamic> purposeList = widget.apiData?['flight_trip_type'] ?? [];
|
||||
|
||||
List<DropdownMenuItem<String>> dropdownItems = purposeList
|
||||
.map((item) => DropdownMenuItem<String>(
|
||||
List<DropdownMenuItem<String>> dropdownItems =
|
||||
purposeList
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item['dropdown_value'],
|
||||
child: Text(item['dropdown_value']),
|
||||
))
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (dropdownItems.isEmpty) {
|
||||
dropdownItems.add(
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text("No options available",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
child: Text(
|
||||
"No options available",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -455,7 +457,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
|
||||
DateTime? pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedCheckOutDate != null &&
|
||||
initialDate:
|
||||
_selectedCheckOutDate != null &&
|
||||
_selectedCheckOutDate!.isAfter(today)
|
||||
? _selectedCheckOutDate!
|
||||
: today,
|
||||
@ -483,8 +486,13 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
// Formatting time to HH:mm (24-hour format)
|
||||
final now = DateTime.now();
|
||||
final formattedTime = DateFormat('HH:mm').format(
|
||||
DateTime(now.year, now.month, now.day, pickedTime.hour,
|
||||
pickedTime.minute),
|
||||
DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
pickedTime.hour,
|
||||
pickedTime.minute,
|
||||
),
|
||||
);
|
||||
_timeController.text = formattedTime;
|
||||
});
|
||||
@ -495,19 +503,24 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
|
||||
List<dynamic> purposeList = widget.apiDataForClass?['train_class'] ?? [];
|
||||
|
||||
List<DropdownMenuItem<String>> dropdownItems = purposeList
|
||||
.map((item) => DropdownMenuItem<String>(
|
||||
List<DropdownMenuItem<String>> dropdownItems =
|
||||
purposeList
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item['dropdown_key'],
|
||||
child: Text(item['dropdown_value']),
|
||||
))
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (dropdownItems.isEmpty) {
|
||||
dropdownItems.add(
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text("No options available",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
child: Text(
|
||||
"No options available",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -525,7 +538,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -543,10 +557,12 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10), // Proper padding
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
), // Proper padding
|
||||
),
|
||||
onChanged: purposeList.isNotEmpty
|
||||
onChanged:
|
||||
purposeList.isNotEmpty
|
||||
? (newValue) {
|
||||
setState(() {
|
||||
selectedClass = newValue;
|
||||
@ -559,19 +575,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
if (errorMessages["class"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -580,7 +588,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -588,41 +597,51 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: isCountryLoading
|
||||
child:
|
||||
isCountryLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: DropdownSearch<String>(
|
||||
// selectedItem: selectedFrom != null
|
||||
// ? countryMap[selectedFrom]
|
||||
// : null,
|
||||
|
||||
selectedItem: selectedFrom != null
|
||||
? countryMap[
|
||||
selectedFrom] // get the display value from code
|
||||
selectedItem:
|
||||
selectedFrom != null
|
||||
? countryMap[selectedFrom] // get the display value from code
|
||||
: null,
|
||||
popupProps: PopupProps.menu(
|
||||
menuProps: MenuProps(backgroundColor: Colors.white),
|
||||
constraints: BoxConstraints(maxHeight: 230),
|
||||
showSearchBox: true,
|
||||
searchFieldProps: TextFieldProps(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
items: countryMap.values.toList(),
|
||||
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 1),
|
||||
),
|
||||
),
|
||||
dropdownBuilder: (context, selectedItem) => Align(
|
||||
dropdownBuilder:
|
||||
(context, selectedItem) => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
selectedItem ?? "Select",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
|
||||
// onChanged: (String? newValue) {
|
||||
// setState(() {
|
||||
// // selectedFrom[index] = countryMap.entries
|
||||
@ -636,16 +655,18 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
// print(selectedFrom);
|
||||
// });
|
||||
// },
|
||||
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
selectedFrom = countryMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
selectedFrom =
|
||||
countryMap.entries
|
||||
.firstWhere(
|
||||
(entry) => entry.value == newValue,
|
||||
)
|
||||
.key;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
// child: SizedBox(
|
||||
// height: 40,
|
||||
// child: TextField(
|
||||
@ -664,19 +685,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
if (errorMessages["from_station"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -685,7 +698,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -693,20 +707,24 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: isCountryLoading
|
||||
child:
|
||||
isCountryLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: DropdownSearch<String>(
|
||||
selectedItem: selectedTo != null
|
||||
? countryMap[
|
||||
selectedTo] // get the display value from code
|
||||
selectedItem:
|
||||
selectedTo != null
|
||||
? countryMap[selectedTo] // get the display value from code
|
||||
: null,
|
||||
popupProps: PopupProps.menu(
|
||||
menuProps: MenuProps(backgroundColor: Colors.white),
|
||||
constraints: BoxConstraints(maxHeight: 230),
|
||||
showSearchBox: true,
|
||||
searchFieldProps: TextFieldProps(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -717,7 +735,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 1),
|
||||
),
|
||||
),
|
||||
dropdownBuilder: (context, selectedItem) => Align(
|
||||
dropdownBuilder:
|
||||
(context, selectedItem) => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
selectedItem ?? "Select",
|
||||
@ -726,8 +745,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
selectedTo = countryMap.entries
|
||||
.firstWhere((entry) => entry.value == newValue)
|
||||
selectedTo =
|
||||
countryMap.entries
|
||||
.firstWhere(
|
||||
(entry) => entry.value == newValue,
|
||||
)
|
||||
.key;
|
||||
});
|
||||
},
|
||||
@ -736,19 +758,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
if (errorMessages["to_station"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -757,7 +771,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -779,8 +794,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
suffixIcon: Icon(Icons.calendar_today,
|
||||
size: 16, color: Colors.grey),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -789,19 +807,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
if (errorMessages["date"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isDesktop) Spacer() else SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -810,7 +820,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldItnerarySubWrapper(
|
||||
@ -832,8 +843,11 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
suffixIcon:
|
||||
Icon(Icons.access_time, size: 16, color: Colors.grey),
|
||||
suffixIcon: Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -842,10 +856,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
if (errorMessages["time"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
@ -862,7 +873,8 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldWrapper(
|
||||
@ -890,9 +902,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
],
|
||||
),
|
||||
if (isDesktop) Spacer(),
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
@ -900,7 +910,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
children: _handleAction(isDesktop),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@ -913,9 +923,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[400], // Light grey color
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
@ -924,7 +932,6 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10), // Space between buttons
|
||||
|
||||
// Save Changes Button
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
@ -932,9 +939,7 @@ class _TrainScreenState extends State<TrainScreen> {
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF114D8B), // Primary color for save
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
@ -438,63 +438,6 @@ class _VisaScreenState extends State<VisaScreen> {
|
||||
selectedPurpose ??=
|
||||
dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
|
||||
return [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Type of Visa",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldWrapper(
|
||||
isFocused: _tripTypeFocused,
|
||||
isDesktop: isDesktop,
|
||||
width: isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.34
|
||||
: MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: DropdownButtonFormField<String>(
|
||||
focusNode: _tripTypeFocusNode, // Assign the correct focus node
|
||||
value: selectedPurpose,
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10), // Proper padding
|
||||
),
|
||||
onChanged: purposeList.isNotEmpty
|
||||
? (newValue) {
|
||||
setState(() {
|
||||
selectedPurpose = newValue;
|
||||
});
|
||||
print(
|
||||
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
|
||||
}
|
||||
: null,
|
||||
|
||||
items: dropdownItems,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["type_of_visa"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -509,7 +452,10 @@ class _VisaScreenState extends State<VisaScreen> {
|
||||
CustomTextFieldWrapper(
|
||||
isFocused: _isHotelNameFocused,
|
||||
isDesktop: isDesktop,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.15 : null,
|
||||
// width: isDesktop ? MediaQuery.of(context).size.width * 0.15 : null,
|
||||
width: isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.34
|
||||
: MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: DropdownSearch<String>(
|
||||
@ -564,6 +510,65 @@ class _VisaScreenState extends State<VisaScreen> {
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Type of Visa",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldWrapper(
|
||||
isFocused: _tripTypeFocused,
|
||||
isDesktop: isDesktop,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.15 : null,
|
||||
// width: isDesktop
|
||||
// ? MediaQuery.of(context).size.width * 0.34
|
||||
// : MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: DropdownButtonFormField<String>(
|
||||
focusNode: _tripTypeFocusNode, // Assign the correct focus node
|
||||
value: selectedPurpose,
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10), // Proper padding
|
||||
),
|
||||
onChanged: purposeList.isNotEmpty
|
||||
? (newValue) {
|
||||
setState(() {
|
||||
selectedPurpose = newValue;
|
||||
});
|
||||
print(
|
||||
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
|
||||
}
|
||||
: null,
|
||||
|
||||
items: dropdownItems,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["type_of_visa"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
"Required",
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
Spacer()
|
||||
else
|
||||
|
||||
@ -245,14 +245,26 @@ class AccomodationListWidget extends StatelessWidget {
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Accomodation"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Accomodation Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onDeleteAccommodation(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Accommodation Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -214,16 +214,36 @@ class BusListWidget extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${item["from"]} - ${item["to"]}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Bus"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Edit Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onDeleteBus(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Bus Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -85,7 +85,8 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
return AlertDialog(
|
||||
title: const Text('Select Trip Type'),
|
||||
content: const Text(
|
||||
'Please select a trip type before adding a flight.'),
|
||||
'Please select a trip type before adding a flight.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@ -118,11 +119,13 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
// style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
MouseRegion(
|
||||
cursor: widget.isViewMode
|
||||
cursor:
|
||||
widget.isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: widget.isViewMode
|
||||
onTap:
|
||||
widget.isViewMode
|
||||
? null
|
||||
: () {
|
||||
checkClass();
|
||||
@ -141,7 +144,7 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
Icons.add_circle_sharp,
|
||||
size: 30,
|
||||
color: Color(0xFF114D8B),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -149,9 +152,9 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Scroll behavior based on device
|
||||
|
||||
_buildData(context, isDesktop)
|
||||
// Scroll behavior based on device
|
||||
_buildData(context, isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -224,8 +227,11 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
|
||||
if (hour == 24 && minute == 0) {
|
||||
// 24:00 is treated as 00:00 on the next day
|
||||
dateTime = DateTime(now.year, now.month, now.day)
|
||||
.add(const Duration(days: 1));
|
||||
dateTime = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).add(const Duration(days: 1));
|
||||
} else {
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
throw FormatException("Invalid hour or minute");
|
||||
@ -245,7 +251,21 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
itemCount: filteredList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredList[index];
|
||||
|
||||
String? tripTypeName = '';
|
||||
switch (item["trip_type"]?.toString()) {
|
||||
case "Roundtrip":
|
||||
tripTypeName = "Round Trip";
|
||||
break;
|
||||
case "Multitrip":
|
||||
tripTypeName = "Multi-Trip";
|
||||
break;
|
||||
case "Oneway":
|
||||
tripTypeName = "One-Way";
|
||||
break;
|
||||
default:
|
||||
tripTypeName = item["trip_type"]?.toString();
|
||||
break;
|
||||
}
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
@ -281,7 +301,7 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
item["trip_type"]?.toString() ?? "N/A",
|
||||
tripTypeName ?? "N/A",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
@ -290,20 +310,30 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => widget.onOpen(true, item, "Flight"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Flight Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => widget.onDeleteFlight(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Flight Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
color: Colors.blueGrey.shade50,
|
||||
),
|
||||
Divider(color: Colors.blueGrey.shade50),
|
||||
SizedBox(height: 4),
|
||||
if (isDesktop)
|
||||
Row(
|
||||
@ -312,39 +342,35 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Class",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
"Sector",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Date",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Time",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
// Trip Rows
|
||||
|
||||
// Trip Rows
|
||||
if (isDesktop)
|
||||
if (item["trips"] != null && item["trips"].isNotEmpty)
|
||||
...item["trips"].map<Widget>((trip) {
|
||||
@ -373,7 +399,7 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
"$fromPlaceCountry (from) - (to) $toPlaceCountry",
|
||||
"$fromPlaceCountry (From) - (To) $toPlaceCountry",
|
||||
// "${trip["from_place"]?.toString()} - ${trip["to_place"]?.toString()}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
@ -425,13 +451,20 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
_buildKeyValueRow(
|
||||
"Class",
|
||||
getRequestForClass(trip["class"].toString()) ??
|
||||
"N/A"),
|
||||
_buildKeyValueRow("Sector",
|
||||
"${trip["from_place"] ?? "N/A"} - ${trip["to_place"] ?? "N/A"}"),
|
||||
"N/A",
|
||||
),
|
||||
_buildKeyValueRow(
|
||||
"Date", formatDate(trip["date"] ?? "")),
|
||||
"Sector",
|
||||
"${trip["from_place"] ?? "N/A"} - ${trip["to_place"] ?? "N/A"}",
|
||||
),
|
||||
_buildKeyValueRow(
|
||||
"Time", formatTime(trip["time"] ?? "")),
|
||||
"Date",
|
||||
formatDate(trip["date"] ?? ""),
|
||||
),
|
||||
_buildKeyValueRow(
|
||||
"Time",
|
||||
formatTime(trip["time"] ?? ""),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -462,12 +495,7 @@ class _FlightListWidgetState extends State<FlightListWidget> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
child: Text(value, style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -261,14 +261,26 @@ class ForexListWidget extends StatelessWidget {
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Forex"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Forex Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onDeleteForex(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Forex Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
item['forex_id'] != null
|
||||
? IconButton(
|
||||
|
||||
@ -192,16 +192,36 @@ class InsuranceListWidget extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
getRequestForInsuranceType(item["type_of_insurance"]!.toString()),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Insurance"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Insurance Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onDeleteInsurance(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Insurance Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -437,14 +457,26 @@ class InsuranceListWidget extends StatelessWidget {
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Insurance"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Insurance Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onDeleteInsurance(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Insurance Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.keyboard_arrow_down_outlined,
|
||||
|
||||
@ -9,14 +9,15 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
final Function(String, bool) onAddNew;
|
||||
final bool isViewMode;
|
||||
|
||||
const MiscellaneousListWidget(
|
||||
{super.key,
|
||||
const MiscellaneousListWidget({
|
||||
super.key,
|
||||
required this.miscellaneousList,
|
||||
required this.onOpen,
|
||||
required this.onDeleteMiscellaneous,
|
||||
required this.apiData,
|
||||
required this.onAddNew,
|
||||
required this.isViewMode});
|
||||
required this.isViewMode,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -34,11 +35,13 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MouseRegion(
|
||||
cursor: isViewMode
|
||||
cursor:
|
||||
isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: isViewMode
|
||||
onTap:
|
||||
isViewMode
|
||||
? null
|
||||
: () {
|
||||
print("New data");
|
||||
@ -57,7 +60,7 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
Icons.add_circle_sharp,
|
||||
size: 30,
|
||||
color: Color(0xFF114D8B),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -95,7 +98,7 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Scroll behavior based on device
|
||||
_buildData(context, isDesktop)
|
||||
_buildData(context, isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -152,22 +155,41 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
getRequestValue(item["special_request"]?.toString()),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Miscellaneous"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Miscellaneous Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Miscellaneous"),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
// onTap: () => onOpen(true, item, "Miscellaneous"),
|
||||
onTap: () => onDeleteMiscellaneous(item),
|
||||
child: Tooltip(
|
||||
message: 'Delete Miscellaneous Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
color: Colors.blueGrey.shade50,
|
||||
),
|
||||
Divider(color: Colors.blueGrey.shade50),
|
||||
SizedBox(height: 4),
|
||||
isDesktop
|
||||
? Row(
|
||||
@ -176,18 +198,16 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Special Request",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
" Comments",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
@ -198,7 +218,8 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: Text(
|
||||
getRequestValue(
|
||||
item["special_request"]?.toString()),
|
||||
item["special_request"]?.toString(),
|
||||
),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -221,8 +242,8 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
children: [
|
||||
_buildRow(
|
||||
"Special Request:",
|
||||
getRequestValue(
|
||||
item["special_request"]?.toString())),
|
||||
getRequestValue(item["special_request"]?.toString()),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
_buildRow("Comments:", item["comments"] ?? "N/A"),
|
||||
],
|
||||
@ -255,23 +276,27 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: exceedsLimit
|
||||
child:
|
||||
exceedsLimit
|
||||
? Tooltip(
|
||||
message: wrapText(value, 50),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200, // Black background
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
textStyle:
|
||||
TextStyle(color: Colors.black), // Tooltip text color
|
||||
textStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
), // Tooltip text color
|
||||
padding: EdgeInsets.all(8),
|
||||
preferBelow: false,
|
||||
child: Text(value.substring(0, limit) + "...",
|
||||
child: Text(
|
||||
value.substring(0, limit) + "...",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
value,
|
||||
@ -306,14 +331,12 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: exceedsLimit
|
||||
child:
|
||||
exceedsLimit
|
||||
? Tooltip(
|
||||
message: wrapText(value, 50),
|
||||
decoration: BoxDecoration(
|
||||
@ -321,7 +344,9 @@ class MiscellaneousListWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
color: Colors.black, fontSize: 12), // Tooltip text color
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
), // Tooltip text color
|
||||
padding: EdgeInsets.all(8),
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
|
||||
@ -11,14 +11,15 @@ class TaxiListWidget extends StatelessWidget {
|
||||
final Function(String, bool) onAddNew;
|
||||
final bool isViewMode;
|
||||
|
||||
const TaxiListWidget(
|
||||
{super.key,
|
||||
const TaxiListWidget({
|
||||
super.key,
|
||||
required this.taxiList,
|
||||
required this.apiData,
|
||||
required this.onOpen,
|
||||
required this.onDeleteTaxi,
|
||||
required this.onAddNew,
|
||||
required this.isViewMode});
|
||||
required this.isViewMode,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -36,12 +37,14 @@ class TaxiListWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MouseRegion(
|
||||
cursor: isViewMode
|
||||
cursor:
|
||||
isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
|
||||
child: GestureDetector(
|
||||
onTap: isViewMode
|
||||
onTap:
|
||||
isViewMode
|
||||
? null
|
||||
: () {
|
||||
print("New data");
|
||||
@ -60,7 +63,7 @@ class TaxiListWidget extends StatelessWidget {
|
||||
Icons.add_circle_sharp,
|
||||
size: 30,
|
||||
color: Color(0xFF114D8B),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -99,9 +102,9 @@ class TaxiListWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Scroll behavior based on device
|
||||
|
||||
_buildData(context, isDesktop)
|
||||
// Scroll behavior based on device
|
||||
_buildData(context, isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -187,8 +190,11 @@ class TaxiListWidget extends StatelessWidget {
|
||||
|
||||
if (hour == 24 && minute == 0) {
|
||||
// 24:00 is treated as 00:00 on the next day
|
||||
dateTime = DateTime(now.year, now.month, now.day)
|
||||
.add(const Duration(days: 1));
|
||||
dateTime = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).add(const Duration(days: 1));
|
||||
} else {
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
throw FormatException("Invalid hour or minute");
|
||||
@ -242,24 +248,35 @@ class TaxiListWidget extends StatelessWidget {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Taxi"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Taxi Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => (item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
onTap: () => onDeleteTaxi(item),
|
||||
child: Tooltip(
|
||||
message: 'Delete Taxi Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
color: Colors.blueGrey.shade50,
|
||||
),
|
||||
Divider(color: Colors.blueGrey.shade50),
|
||||
SizedBox(height: 4),
|
||||
isDesktop
|
||||
? Row(
|
||||
@ -268,34 +285,30 @@ class TaxiListWidget extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"City",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
" Location",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
" Date",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Comments",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
style: GoogleFonts.poppins(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
@ -339,7 +352,9 @@ class TaxiListWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildComments(
|
||||
" Comments:", item["comments"] ?? "N/A"),
|
||||
" Comments:",
|
||||
item["comments"] ?? "N/A",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -358,7 +373,7 @@ class TaxiListWidget extends StatelessWidget {
|
||||
_buildRow("TaxiFor:", item["car_required_for"]!),
|
||||
_buildRow("Comments:", item["comments"] ?? "N/A"),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -387,23 +402,27 @@ class TaxiListWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: exceedsLimit
|
||||
child:
|
||||
exceedsLimit
|
||||
? Tooltip(
|
||||
message: wrapText(value, 50),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200, // Black background
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
textStyle:
|
||||
TextStyle(color: Colors.black), // Tooltip text color
|
||||
textStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
), // Tooltip text color
|
||||
padding: EdgeInsets.all(8),
|
||||
preferBelow: false,
|
||||
child: Text(value.substring(0, limit) + "...",
|
||||
child: Text(
|
||||
value.substring(0, limit) + "...",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
value,
|
||||
@ -438,34 +457,30 @@ class TaxiListWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 12),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: exceedsLimit
|
||||
child:
|
||||
exceedsLimit
|
||||
? Tooltip(
|
||||
message: wrapText(value, 50),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200, // Black background
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
textStyle:
|
||||
TextStyle(color: Colors.black), // Tooltip text color
|
||||
textStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
), // Tooltip text color
|
||||
padding: EdgeInsets.all(8),
|
||||
preferBelow: false,
|
||||
child: Text(value.substring(0, limit) + "...",
|
||||
child: Text(
|
||||
value.substring(0, limit) + "...",
|
||||
style: TextStyle(fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
value,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
: Text(value, style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -477,19 +492,11 @@ class TaxiListWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"$date, $time",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
child: Text("$date, $time", style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -333,14 +333,26 @@ class _TrainListWidgetState extends State<TrainListWidget> {
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => widget.onOpen(true, item, "Train"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Train Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => widget.onDeleteTrain(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Train Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -291,16 +291,36 @@ class VisaListWidget extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
getRequestForVisa(item["type_of_visa"]!.toString()),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => onOpen(true, item, "Visa"),
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Edit Visa Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () => onDeleteMiscellaneous(item),
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Tooltip(
|
||||
message: 'Delete Visa Details',
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
562
lib/Screens/myTemplates/Tempale
Normal file
562
lib/Screens/myTemplates/Tempale
Normal file
@ -0,0 +1,562 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const Template({super.key, required this.templateData});
|
||||
|
||||
static Template fromState(GoRouterState state) {
|
||||
return Template(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateState createState() => TemplateState();
|
||||
}
|
||||
|
||||
class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// String convertQuillDocToHtml(quill.Document doc) {
|
||||
// final buffer = StringBuffer();
|
||||
//
|
||||
// print("convertQuillDocToHtml");
|
||||
// for (final op in doc.toDelta().toList()) {
|
||||
// final insert = op.data;
|
||||
// final attrs = op.attributes ?? {};
|
||||
//
|
||||
// if (insert is String) {
|
||||
// var content = insert;
|
||||
//
|
||||
// // Handle formatting (bold, italic, etc.)
|
||||
// if (attrs.containsKey('bold')) {
|
||||
// content = '<strong>$content</strong>';
|
||||
// }
|
||||
// if (attrs.containsKey('italic')) {
|
||||
// content = '<em>$content</em>';
|
||||
// }
|
||||
//
|
||||
// // Wrap each paragraph with <p>
|
||||
// if (content.trim().isNotEmpty) {
|
||||
// buffer.write('<p>${content.trim()}</p>');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String convertQuillDocToHtml2(quill.Document doc) {
|
||||
// final buffer = StringBuffer();
|
||||
// final lines = <String>[];
|
||||
// final delta = doc.toDelta();
|
||||
//
|
||||
// String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
// if (attrs == null) return text;
|
||||
// if (attrs.containsKey('bold')) {
|
||||
// text = '<strong>$text</strong>';
|
||||
// }
|
||||
// if (attrs.containsKey('italic')) {
|
||||
// text = '<em>$text</em>';
|
||||
// }
|
||||
// return text;
|
||||
// }
|
||||
//
|
||||
// for (final op in delta.toList()) {
|
||||
// final insert = op.data;
|
||||
// final attrs = op.attributes;
|
||||
//
|
||||
// if (insert is String) {
|
||||
// final parts = insert.split('\n');
|
||||
// for (int i = 0; i < parts.length; i++) {
|
||||
// final part = applyStyles(parts[i], attrs);
|
||||
// lines.add(part);
|
||||
//
|
||||
// if (i < parts.length - 1) {
|
||||
// // End of line: wrap accumulated content into <p>
|
||||
// final joined = lines.join('');
|
||||
// if (joined.trim().isNotEmpty) {
|
||||
// buffer.writeln('<p>${joined.trim()}</p>');
|
||||
// }
|
||||
// lines.clear();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Add remaining lines
|
||||
// final joined = lines.join('');
|
||||
// if (joined.trim().isNotEmpty) {
|
||||
// buffer.writeln('<p>${joined.trim()}</p>');
|
||||
// }
|
||||
//
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String extractPlainTextFromHtml(String html) {
|
||||
// final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
// final matches = regex.allMatches(html);
|
||||
//
|
||||
// final buffer = StringBuffer();
|
||||
// for (final match in matches) {
|
||||
// final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
// buffer.writeln(text.trim());
|
||||
// }
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String decodeHtmlEntities(String text) {
|
||||
// return text
|
||||
// .replaceAll(' ', ' ')
|
||||
// .replaceAll('&', '&')
|
||||
// .replaceAll('<', '<')
|
||||
// .replaceAll('>', '>')
|
||||
// .replaceAll('"', '"')
|
||||
// .replaceAll(''', "'"); // add more as needed
|
||||
// }
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
// /*inal converter = DeltaFromHTML();
|
||||
// final delta = converter.convert(bodyHtml); // Convert HTML → Delta
|
||||
// final quillDoc = quill.Document.fromDelta(delta);
|
||||
// */
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
//
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// // final quillDoc = convertBasicHtmlToQuill(bodyHtml);
|
||||
// // final quillDoc = quill.Document()..insert(0, plainText);
|
||||
// // final quillDoc = quill.Document.fromDelta(delta);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
final deltaJsonString =
|
||||
widget.templateData?["templateData"]?["body_delta"];
|
||||
if (deltaJsonString != null) {
|
||||
final deltaJson = jsonDecode(deltaJsonString);
|
||||
final quillDoc = quill.Document.fromJson(deltaJson);
|
||||
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
}
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
setState(() {
|
||||
// updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
|
||||
final TemplateData1 = TemplateData;
|
||||
print("TemplateData - $TemplateData1");
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(Map<String, dynamic> policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
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,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(height: 10),
|
||||
buildTempalteSubject(isDesktop),
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(child: QuillSimpleToolbar(controller: _controller)),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/templateList');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
const kScreenshot1 = 'assets/images/screenshot_1.png';
|
||||
const kScreenshot2 = 'assets/images/screenshot_2.png';
|
||||
const kScreenshot3 = 'assets/images/screenshot_3.png';
|
||||
const kScreenshot4 = 'assets/images/screenshot_4.png';
|
||||
const kScreenshot4 =
|
||||
'assets/images/screenshot_4.png'; // TODO Implement this library.
|
||||
|
||||
@ -16,14 +16,8 @@ class CustomToolbar extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
children: [
|
||||
QuillToolbarHistoryButton(
|
||||
isUndo: true,
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarHistoryButton(
|
||||
isUndo: false,
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarHistoryButton(isUndo: true, controller: controller),
|
||||
QuillToolbarHistoryButton(isUndo: false, controller: controller),
|
||||
QuillToolbarToggleStyleButton(
|
||||
options: const QuillToolbarToggleStyleButtonOptions(),
|
||||
controller: controller,
|
||||
@ -38,40 +32,22 @@ class CustomToolbar extends StatelessWidget {
|
||||
controller: controller,
|
||||
attribute: Attribute.underline,
|
||||
),
|
||||
QuillToolbarClearFormatButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarClearFormatButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarImageButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarCameraButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarVideoButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarImageButton(controller: controller),
|
||||
QuillToolbarCameraButton(controller: controller),
|
||||
QuillToolbarVideoButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarColorButton(
|
||||
controller: controller,
|
||||
isBackground: false,
|
||||
),
|
||||
QuillToolbarColorButton(
|
||||
controller: controller,
|
||||
isBackground: true,
|
||||
),
|
||||
QuillToolbarColorButton(controller: controller, isBackground: false),
|
||||
QuillToolbarColorButton(controller: controller, isBackground: true),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarSelectHeaderStyleDropdownButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarSelectHeaderStyleDropdownButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarSelectLineHeightStyleDropdownButton(
|
||||
controller: controller,
|
||||
),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarToggleCheckListButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarToggleCheckListButton(controller: controller),
|
||||
QuillToolbarToggleStyleButton(
|
||||
controller: controller,
|
||||
attribute: Attribute.ol,
|
||||
@ -88,14 +64,8 @@ class CustomToolbar extends StatelessWidget {
|
||||
controller: controller,
|
||||
attribute: Attribute.blockQuote,
|
||||
),
|
||||
QuillToolbarIndentButton(
|
||||
controller: controller,
|
||||
isIncrease: true,
|
||||
),
|
||||
QuillToolbarIndentButton(
|
||||
controller: controller,
|
||||
isIncrease: false,
|
||||
),
|
||||
QuillToolbarIndentButton(controller: controller, isIncrease: true),
|
||||
QuillToolbarIndentButton(controller: controller, isIncrease: false),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarLinkStyleButton(controller: controller),
|
||||
],
|
||||
|
||||
88
lib/Screens/myTemplates/dialog_placeholders.dart
Normal file
88
lib/Screens/myTemplates/dialog_placeholders.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:frontend/utils/auth_utils.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class PlaceholdersModal extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> placeholders;
|
||||
const PlaceholdersModal({Key? key, required this.placeholders})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_PlaceholdersModalState createState() => _PlaceholdersModalState();
|
||||
}
|
||||
|
||||
class _PlaceholdersModalState extends State<PlaceholdersModal> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String templLabel(String placeholder) {
|
||||
var label = placeholder.replaceAll('%', '').replaceAll('_', ' ');
|
||||
return label
|
||||
.split(' ')
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? word[0].toUpperCase() + word.substring(1)
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text(
|
||||
"Available Placeholders",
|
||||
style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
content: Container(
|
||||
width:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.4
|
||||
: double.maxFinite,
|
||||
// Set max height so ListView knows constraints
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.placeholders.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = widget.placeholders[index]['value'] ?? '';
|
||||
return ListTile(
|
||||
hoverColor: Colors.white,
|
||||
focusColor: Colors.white,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
templLabel(value),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SelectableText(
|
||||
value,
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
// onTap: () {
|
||||
// Navigator.of(context).pop(value);
|
||||
// },
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text("Close", style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../editor/image/image_embed_types.dart';
|
||||
import 'extensions/controller_ext.dart';
|
||||
|
||||
OnImageInsertCallback _defaultOnImageInsert() {
|
||||
return (imageUrl, controller) async {
|
||||
controller
|
||||
..skipRequestKeyboard = true
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
..insertImageBlock(imageSource: imageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
@internal
|
||||
Future<void> handleImageInsert(
|
||||
String imageUrl, {
|
||||
required QuillController controller,
|
||||
required OnImageInsertCallback? onImageInsertCallback,
|
||||
required OnImageInsertedCallback? onImageInsertedCallback,
|
||||
}) async {
|
||||
final customOnImageInsert = onImageInsertCallback;
|
||||
if (customOnImageInsert != null) {
|
||||
await customOnImageInsert.call(imageUrl, controller);
|
||||
} else {
|
||||
await _defaultOnImageInsert().call(imageUrl, controller);
|
||||
}
|
||||
await onImageInsertedCallback?.call(imageUrl);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../toolbar/video/config/video.dart';
|
||||
import 'extensions/controller_ext.dart';
|
||||
|
||||
OnVideoInsertCallback _defaultOnVideoInsert() {
|
||||
return (imageUrl, controller) async {
|
||||
controller
|
||||
..skipRequestKeyboard = true
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
..insertVideoBlock(videoUrl: imageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
@internal
|
||||
Future<void> handleVideoInsert(
|
||||
String videoUrl, {
|
||||
required QuillController controller,
|
||||
required OnVideoInsertCallback? onVideoInsertCallback,
|
||||
required OnVideoInsertedCallback? onVideoInsertedCallback,
|
||||
}) async {
|
||||
final customOnVideoInsert = onVideoInsertCallback;
|
||||
if (customOnVideoInsert != null) {
|
||||
await customOnVideoInsert.call(videoUrl, controller);
|
||||
} else {
|
||||
await _defaultOnVideoInsert().call(videoUrl, controller);
|
||||
}
|
||||
await onVideoInsertedCallback?.call(videoUrl);
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart'
|
||||
show Attribute, AttributeScope;
|
||||
|
||||
class FlutterAlignmentAttribute extends Attribute<String?> {
|
||||
const FlutterAlignmentAttribute(String? val)
|
||||
: super('flutterAlignment', AttributeScope.ignore, val);
|
||||
}
|
||||
|
||||
extension AttributeExt on Attribute {
|
||||
static const FlutterAlignmentAttribute flutterAlignment =
|
||||
FlutterAlignmentAttribute(null);
|
||||
}
|
||||
@ -1,36 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
@Deprecated('Invalid extension')
|
||||
extension QuillControllerExt on QuillController {
|
||||
@Deprecated(
|
||||
'Invalid extension property and will be removed, use selection.baseOffset instead')
|
||||
int get index => selection.baseOffset;
|
||||
@Deprecated(
|
||||
'Invalid extension property and will be removed, use selection.extentOffset - selection.baseOffset instead')
|
||||
int get length => selection.extentOffset - index;
|
||||
|
||||
@Deprecated('Invalid extension method and will be removed.')
|
||||
void insertImageBlock({
|
||||
required String imageSource,
|
||||
}) {
|
||||
this
|
||||
..skipRequestKeyboard = true
|
||||
..replaceText(
|
||||
index,
|
||||
length,
|
||||
BlockEmbed.image(imageSource),
|
||||
null,
|
||||
)
|
||||
..moveCursorToPosition(index + 1);
|
||||
}
|
||||
|
||||
@Deprecated('Invalid extension method and will be removed.')
|
||||
void insertVideoBlock({
|
||||
required String videoUrl,
|
||||
}) {
|
||||
this
|
||||
..skipRequestKeyboard = true
|
||||
..replaceText(index, length, BlockEmbed.video(videoUrl), null)
|
||||
..moveCursorToPosition(index + 1);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'utils/patterns.dart';
|
||||
|
||||
enum LinkType {
|
||||
video,
|
||||
image,
|
||||
}
|
||||
|
||||
class TypeLinkDialog extends StatefulWidget {
|
||||
const TypeLinkDialog({
|
||||
required this.linkType,
|
||||
this.dialogTheme,
|
||||
this.link,
|
||||
this.linkRegExp,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final QuillDialogTheme? dialogTheme;
|
||||
final String? link;
|
||||
final RegExp? linkRegExp;
|
||||
final LinkType linkType;
|
||||
|
||||
@override
|
||||
TypeLinkDialogState createState() => TypeLinkDialogState();
|
||||
}
|
||||
|
||||
class TypeLinkDialogState extends State<TypeLinkDialog> {
|
||||
late String _link;
|
||||
late TextEditingController _controller;
|
||||
RegExp? _linkRegExp;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_link = widget.link ?? '';
|
||||
_controller = TextEditingController(text: _link);
|
||||
|
||||
_linkRegExp = widget.linkRegExp;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
|
||||
content: TextField(
|
||||
keyboardType: TextInputType.url,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
style: widget.dialogTheme?.inputTextStyle,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.loc.pasteLink,
|
||||
hintText: widget.linkType == LinkType.image
|
||||
? context.loc.pleaseEnterAValidImageURL
|
||||
: context.loc.pleaseEnterAValidVideoURL,
|
||||
labelStyle: widget.dialogTheme?.labelTextStyle,
|
||||
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
|
||||
),
|
||||
autofocus: true,
|
||||
onChanged: _linkChanged,
|
||||
controller: _controller,
|
||||
onEditingComplete: () {
|
||||
if (!_canPress()) {
|
||||
return;
|
||||
}
|
||||
_applyLink();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _canPress() ? _applyLink : null,
|
||||
child: Text(
|
||||
context.loc.ok,
|
||||
style: widget.dialogTheme?.labelTextStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _linkChanged(String value) {
|
||||
setState(() {
|
||||
_link = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _applyLink() {
|
||||
Navigator.pop(context, _link.trim());
|
||||
}
|
||||
|
||||
RegExp get linkRegExp {
|
||||
final customRegExp = _linkRegExp;
|
||||
if (customRegExp != null) {
|
||||
return customRegExp;
|
||||
}
|
||||
switch (widget.linkType) {
|
||||
case LinkType.video:
|
||||
if (youtubeRegExp.hasMatch(_link)) {
|
||||
return youtubeRegExp;
|
||||
}
|
||||
return videoRegExp;
|
||||
case LinkType.image:
|
||||
return imageRegExp;
|
||||
}
|
||||
}
|
||||
|
||||
bool _canPress() {
|
||||
if (_link.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (widget.linkType == LinkType.image) {}
|
||||
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
// import 'package:universal_html/html.dart' as html;
|
||||
|
||||
// Fake interface for the logic that this package needs from (web-only) dart:ui.
|
||||
// This is conditionally exported so the analyzer sees these methods as
|
||||
// available.
|
||||
|
||||
// typedef PlatroformViewFactory = html.Element Function(int viewId);
|
||||
|
||||
// /// Shim for web_ui engine.PlatformViewRegistry
|
||||
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
|
||||
// class PlatformViewRegistry {
|
||||
// /// Shim for registerViewFactory
|
||||
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
|
||||
// static dynamic registerViewFactory(
|
||||
// String viewTypeId, PlatroformViewFactory viewFactory) {}
|
||||
// }
|
||||
|
||||
// /// Shim for web_ui engine.AssetManager
|
||||
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12
|
||||
// class WebOnlyAssetManager {
|
||||
// static dynamic getAssetUrl(String asset) {}
|
||||
// }
|
||||
|
||||
class PlatformViewRegistry {
|
||||
/// Register [viewType] as being created by the given [viewFactory].
|
||||
///
|
||||
/// [viewFactory] can be any function that takes an integer and optional
|
||||
/// `params` and returns an `HTMLElement` DOM object.
|
||||
bool registerViewFactory(
|
||||
String viewType,
|
||||
Function viewFactory, {
|
||||
bool isVisible = true,
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the view previously created for [viewId].
|
||||
///
|
||||
/// Throws if no view has been created for [viewId].
|
||||
Object getViewById(int viewId) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export 'dart:ui' if (dart.library.js_interop) 'dart:ui_web';
|
||||
@ -1,84 +0,0 @@
|
||||
import 'package:flutter/widgets.dart' show BuildContext, MediaQuery;
|
||||
|
||||
Map<String, String> parseCssString(String cssString) {
|
||||
final result = <String, String>{};
|
||||
final declarations = cssString.split(';');
|
||||
|
||||
for (final declaration in declarations) {
|
||||
final parts = declaration.split(':');
|
||||
if (parts.length == 2) {
|
||||
final property = parts[0].trim();
|
||||
final value = parts[1].trim();
|
||||
result[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
enum _CssUnit {
|
||||
px('px'),
|
||||
percentage('%'),
|
||||
viewportWidth('vw'),
|
||||
viewportHeight('vh'),
|
||||
em('em'),
|
||||
rem('rem'),
|
||||
invalid('invalid');
|
||||
|
||||
const _CssUnit(this.cssName);
|
||||
|
||||
final String cssName;
|
||||
}
|
||||
|
||||
double? parseCssPropertyAsDouble(
|
||||
String value, {
|
||||
required BuildContext context,
|
||||
}) {
|
||||
if (value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to parse it in case it's a valid double already
|
||||
var doubleValue = double.tryParse(value);
|
||||
|
||||
if (doubleValue != null) {
|
||||
return doubleValue;
|
||||
}
|
||||
|
||||
// If not then if it's a css numberic value then we will try to parse it
|
||||
final unit = _CssUnit.values
|
||||
.where((element) => value.endsWith(element.cssName))
|
||||
.firstOrNull;
|
||||
if (unit == null) {
|
||||
return null;
|
||||
}
|
||||
value = value.replaceFirst(unit.cssName, '');
|
||||
doubleValue = double.tryParse(value);
|
||||
if (doubleValue != null) {
|
||||
switch (unit) {
|
||||
case _CssUnit.px:
|
||||
// Do nothing
|
||||
break;
|
||||
case _CssUnit.percentage:
|
||||
// Not supported yet
|
||||
doubleValue = null;
|
||||
break;
|
||||
case _CssUnit.viewportWidth:
|
||||
doubleValue = (doubleValue / 100) * MediaQuery.sizeOf(context).width;
|
||||
break;
|
||||
case _CssUnit.viewportHeight:
|
||||
doubleValue = (doubleValue / 100) * MediaQuery.sizeOf(context).height;
|
||||
break;
|
||||
case _CssUnit.em:
|
||||
doubleValue = MediaQuery.textScalerOf(context).scale(doubleValue);
|
||||
break;
|
||||
case _CssUnit.rem:
|
||||
doubleValue = MediaQuery.textScalerOf(context).scale(doubleValue);
|
||||
break;
|
||||
case _CssUnit.invalid:
|
||||
doubleValue = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return doubleValue;
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
import 'package:flutter/foundation.dart' show immutable;
|
||||
import 'package:flutter/widgets.dart' show Alignment, BuildContext;
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'element_shared_utils.dart';
|
||||
|
||||
/// Theses properties are not officialy supported by quill js
|
||||
/// but they are only used in all platforms other than web
|
||||
/// and they will be stored in css style property so quill js ignore them
|
||||
enum ExtraElementProperties {
|
||||
deletable,
|
||||
}
|
||||
|
||||
(
|
||||
ElementSize elementSize,
|
||||
double? margin,
|
||||
Alignment alignment,
|
||||
) getElementAttributes(
|
||||
Node node,
|
||||
BuildContext context,
|
||||
) {
|
||||
var elementSize = const ElementSize(null, null);
|
||||
var elementAlignment = Alignment.center;
|
||||
double? elementMargin;
|
||||
|
||||
final heightValue = parseCssPropertyAsDouble(
|
||||
node.style.attributes[Attribute.height.key]?.value.toString() ?? '',
|
||||
context: context,
|
||||
);
|
||||
final widthValue = parseCssPropertyAsDouble(
|
||||
node.style.attributes[Attribute.width.key]?.value.toString() ?? '',
|
||||
context: context,
|
||||
);
|
||||
|
||||
if (heightValue != null) {
|
||||
elementSize = elementSize.copyWith(
|
||||
height: heightValue,
|
||||
);
|
||||
}
|
||||
if (widthValue != null) {
|
||||
elementSize = elementSize.copyWith(
|
||||
width: widthValue,
|
||||
);
|
||||
}
|
||||
|
||||
final cssStyle = node.style.attributes['style'];
|
||||
|
||||
if (cssStyle != null) {
|
||||
// It css value as string but we will try to support it anyway
|
||||
|
||||
final cssAttrs = parseCssString(cssStyle.value.toString());
|
||||
|
||||
final cssHeightValue = parseCssPropertyAsDouble(
|
||||
(cssAttrs[Attribute.height.key]) ?? '',
|
||||
context: context,
|
||||
);
|
||||
final cssWidthValue = parseCssPropertyAsDouble(
|
||||
(cssAttrs[Attribute.width.key]) ?? '',
|
||||
context: context,
|
||||
);
|
||||
|
||||
// cssHeightValue != null && elementSize.height == null
|
||||
if (cssHeightValue != null) {
|
||||
elementSize = elementSize.copyWith(height: cssHeightValue);
|
||||
}
|
||||
if (cssWidthValue != null) {
|
||||
elementSize = elementSize.copyWith(width: cssWidthValue);
|
||||
}
|
||||
|
||||
elementAlignment = getAlignment(cssAttrs['alignment']);
|
||||
|
||||
final margin = double.tryParse('margin');
|
||||
if (margin != null) {
|
||||
elementMargin = margin;
|
||||
}
|
||||
}
|
||||
|
||||
return (elementSize, elementMargin, elementAlignment);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ElementSize {
|
||||
const ElementSize(
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
|
||||
/// If non-null, requires the child to have exactly this width.
|
||||
/// If null, the child is free to choose its own width.
|
||||
final double? width;
|
||||
|
||||
/// If non-null, requires the child to have exactly this height.
|
||||
/// If null, the child is free to choose its own height.
|
||||
final double? height;
|
||||
|
||||
ElementSize copyWith({
|
||||
double? width,
|
||||
double? height,
|
||||
}) {
|
||||
return ElementSize(
|
||||
width ?? this.width,
|
||||
height ?? this.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
|
||||
|
||||
import 'element_shared_utils.dart';
|
||||
|
||||
/// Prefer the width, and height from the css style attribute if exits
|
||||
/// it can be `auto` or `100px` so it's specific to HTML && CSS
|
||||
/// if not, we will use the one from attributes which is usually just an double
|
||||
(
|
||||
String height,
|
||||
String width,
|
||||
String margin,
|
||||
String alignment,
|
||||
) getWebElementAttributes(
|
||||
Node node,
|
||||
) {
|
||||
var height = 'auto';
|
||||
var width = 'auto';
|
||||
// TODO(): Add support for margin and alignment
|
||||
var margin = 'auto';
|
||||
const alignment = 'center';
|
||||
|
||||
final cssStyle = node.style.attributes['style'];
|
||||
|
||||
final heightValue = node.style.attributes[Attribute.height.key]?.value;
|
||||
final widthValue = node.style.attributes[Attribute.width.key]?.value;
|
||||
|
||||
if (cssStyle != null) {
|
||||
final attrs = parseCssString(cssStyle.value.toString());
|
||||
|
||||
final cssHeightValue = attrs[Attribute.height.key];
|
||||
|
||||
if (cssHeightValue != null) {
|
||||
height = cssHeightValue;
|
||||
} else {
|
||||
height = '${heightValue}px';
|
||||
}
|
||||
final cssWidthValue = attrs[Attribute.width.key];
|
||||
if (cssWidthValue != null) {
|
||||
width = cssWidthValue;
|
||||
} else if (widthValue != null) {
|
||||
width = '${widthValue}px';
|
||||
}
|
||||
|
||||
final cssMarginValue = attrs['margin'];
|
||||
if (cssMarginValue != null) {
|
||||
margin = cssMarginValue;
|
||||
}
|
||||
|
||||
return (height, width, margin, alignment);
|
||||
}
|
||||
|
||||
if (heightValue != null) {
|
||||
height = '${heightValue}px';
|
||||
}
|
||||
if (widthValue != null) {
|
||||
width = '${widthValue}px';
|
||||
}
|
||||
|
||||
return (height, width, margin, alignment);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
RegExp base64RegExp = RegExp(
|
||||
r'^(?:[A-Za-z0-9+\/][A-Za-z0-9+\/][A-Za-z0-9+\/][A-Za-z0-9+\/])*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
|
||||
);
|
||||
|
||||
final imageRegExp = RegExp(
|
||||
r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final videoRegExp = RegExp(
|
||||
r'\bhttps?://\S+\.(mp4|mov|avi|mkv|flv|wmv|webm)\b',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final youtubeRegExp = RegExp(
|
||||
r'^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
@ -1,30 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Attribute;
|
||||
|
||||
String replaceStyleStringWithSize(
|
||||
String cssStyle, {
|
||||
required double width,
|
||||
required double height,
|
||||
}) {
|
||||
final result = <String, String>{};
|
||||
final pairs = cssStyle.split(';');
|
||||
for (final pair in pairs) {
|
||||
final index = pair.indexOf(':');
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
final key = pair.substring(0, index).trim();
|
||||
result[key] = pair.substring(index + 1).trim();
|
||||
}
|
||||
|
||||
result[Attribute.width.key] = width.toString();
|
||||
result[Attribute.height.key] = height.toString();
|
||||
final sb = StringBuffer();
|
||||
for (final pair in result.entries) {
|
||||
sb
|
||||
..write(pair.key)
|
||||
..write(': ')
|
||||
..write(pair.value)
|
||||
..write('; ');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import 'patterns.dart';
|
||||
|
||||
bool isBase64(String str) {
|
||||
return base64RegExp.hasMatch(str);
|
||||
}
|
||||
|
||||
bool isHttpUrl(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url.trim());
|
||||
return uri.isScheme('HTTP') || uri.isScheme('HTTPS');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isImageBase64(String imageUrl) {
|
||||
return !isHttpUrl(imageUrl) && isBase64(imageUrl);
|
||||
}
|
||||
|
||||
bool isYouTubeUrl(String videoUrl) {
|
||||
try {
|
||||
final uri = Uri.parse(videoUrl);
|
||||
return uri.host == 'www.youtube.com' ||
|
||||
uri.host == 'youtube.com' ||
|
||||
uri.host == 'youtu.be' ||
|
||||
uri.host == 'www.youtu.be';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export './web_stub.dart' if (dart.library.js_interop) './web_real.dart';
|
||||
@ -1,46 +0,0 @@
|
||||
import 'package:web/web.dart';
|
||||
import '../dart_ui/dart_ui_fake.dart'
|
||||
if (dart.library.js_interop) '../dart_ui/dart_ui_real.dart' as ui;
|
||||
|
||||
void main(List<String> args) {
|
||||
HTMLImageElement;
|
||||
}
|
||||
|
||||
void createHtmlImageElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) {
|
||||
ui.PlatformViewRegistry().registerViewFactory(src, (viewId) {
|
||||
return createHtmlImageElement(
|
||||
src: src,
|
||||
alignSelf: alignSelf,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void createHtmlIFrameElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) {
|
||||
ui.PlatformViewRegistry().registerViewFactory(
|
||||
src,
|
||||
(id) {
|
||||
return HTMLIFrameElement()
|
||||
..style.width = width
|
||||
..style.height = height
|
||||
..src = src
|
||||
..style.border = 'none'
|
||||
..style.margin = margin
|
||||
..style.alignSelf = alignSelf;
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
void createHtmlImageElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) =>
|
||||
throw UnimplementedError(
|
||||
'A stub method is called, createHtmlImageElement is for web platforms only.');
|
||||
|
||||
void createHtmlIFrameElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) =>
|
||||
throw UnimplementedError(
|
||||
'A stub method is called, createHtmlIFrameElement is for web platforms only.');
|
||||
@ -1,165 +1 @@
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import '../image_embed_types.dart';
|
||||
|
||||
/// [QuillEditorImageEmbedConfig] for desktop, mobile and
|
||||
/// other platforms
|
||||
/// excluding web, it's configurations that is needed for the editor
|
||||
///
|
||||
@immutable
|
||||
class QuillEditorImageEmbedConfig {
|
||||
const QuillEditorImageEmbedConfig({
|
||||
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
|
||||
this.shouldRemoveImageCallback,
|
||||
this.imageProviderBuilder,
|
||||
this.imageErrorWidgetBuilder,
|
||||
this.onImageClicked,
|
||||
}) : _onImageRemovedCallback = onImageRemovedCallback;
|
||||
|
||||
/// [onImageRemovedCallback] is called when an image is
|
||||
/// removed from the editor.
|
||||
/// By default, [onImageRemovedCallback] deletes the
|
||||
/// temporary image file if
|
||||
/// the platform is mobile and if it still exists. You
|
||||
/// can customize this behavior
|
||||
/// by passing your own function that handles the removal process.
|
||||
///
|
||||
/// Example of [onImageRemovedCallback] customization:
|
||||
/// ```dart
|
||||
/// afterRemoveImageFromEditor: (imageFile) async {
|
||||
/// // Your custom logic here
|
||||
/// // or leave it empty to do nothing
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Default value if the passed value is null:
|
||||
/// [QuillEditorImageEmbedConfig.defaultOnImageRemovedCallback]
|
||||
///
|
||||
/// so if you want to do nothing make sure to pass a empty callback
|
||||
/// instead of passing null as value
|
||||
final ImageEmbedBuilderOnRemovedCallback? _onImageRemovedCallback;
|
||||
|
||||
ImageEmbedBuilderOnRemovedCallback get onImageRemovedCallback {
|
||||
return _onImageRemovedCallback ??
|
||||
QuillEditorImageEmbedConfig.defaultOnImageRemovedCallback;
|
||||
}
|
||||
|
||||
/// [shouldRemoveImageCallback] is a callback
|
||||
/// function that is invoked when the
|
||||
/// user attempts to remove an image from the editor. It allows you to control
|
||||
/// whether the image should be removed based on your custom logic.
|
||||
///
|
||||
/// Example of [shouldRemoveImageCallback] customization:
|
||||
/// ```dart
|
||||
/// shouldRemoveImageFromEditor: (imageFile) async {
|
||||
/// // Show a confirmation dialog before removing the image
|
||||
/// final isShouldRemove = await showYesCancelDialog(
|
||||
/// context: context,
|
||||
/// options: const YesOrCancelDialogOptions(
|
||||
/// title: 'Deleting an image',
|
||||
/// message: 'Are you sure you want' ' to delete this
|
||||
/// image from the editor?',
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// // Return `true` to allow image removal if the user confirms, otherwise
|
||||
/// `false`
|
||||
/// return isShouldRemove;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback;
|
||||
|
||||
/// Allows to override the default handling and fallback to the default if `null` was returned.
|
||||
///
|
||||
/// Example of [imageProviderBuilder] customization:
|
||||
/// ```dart
|
||||
/// imageProviderBuilder: (imageUrl) async {
|
||||
/// if (imageUrl.startsWith('assets/')) {
|
||||
/// // Supports Image assets
|
||||
/// return AssetImage(imageUrl);
|
||||
/// }
|
||||
/// if (imageUrl.startsWith('http')) {
|
||||
/// // Use https://pub.dev/packages/cached_network_image
|
||||
/// // for network images to cache them.
|
||||
/// return CachedNetworkImageProvider(imageUrl);
|
||||
/// }
|
||||
///
|
||||
/// // Return null to fallback to default handling
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
final ImageEmbedBuilderProviderBuilder? imageProviderBuilder;
|
||||
|
||||
/// [imageErrorWidgetBuilder] if you want to show a custom widget based on the
|
||||
/// exception that happen while loading the image, if it network image or
|
||||
/// local one, and it will get called on all the images even in the photo
|
||||
/// preview widget and not just in the quill editor
|
||||
/// by default the default error from flutter framework will thrown
|
||||
///
|
||||
final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder;
|
||||
|
||||
/// What should happen when the image is pressed?
|
||||
///
|
||||
/// By default will show `ImageOptionsMenu` dialog. If you want to handle what happens
|
||||
/// to the image when it's clicked, you can pass a callback to this property.
|
||||
final void Function(String imageSource)? onImageClicked;
|
||||
|
||||
static ImageEmbedBuilderOnRemovedCallback get defaultOnImageRemovedCallback {
|
||||
return (imageUrl) async {
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mobile = isMobileApp;
|
||||
// If the platform is not mobile, return void;
|
||||
// Since the mobile OS gives us a copy of the image
|
||||
|
||||
// Note: We should remove the image on Flutter web
|
||||
// since the behavior is similar to how it is on mobile,
|
||||
// but since this builder is not for web, we will ignore it
|
||||
if (!mobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On mobile OS (Android, iOS), the system will not give us
|
||||
// direct access to the image; instead,
|
||||
// it will give us the image
|
||||
// in the temp directory of the application. So, we want to
|
||||
// remove it when we no longer need it.
|
||||
|
||||
// but on desktop we don't want to touch user files
|
||||
// especially on macOS, where we can't even delete
|
||||
// it without
|
||||
// permission
|
||||
|
||||
final dartIoImageFile = File(imageUrl);
|
||||
|
||||
final isFileExists = await dartIoImageFile.exists();
|
||||
if (isFileExists) {
|
||||
await dartIoImageFile.delete();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
QuillEditorImageEmbedConfig copyWith({
|
||||
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
|
||||
ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback,
|
||||
ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||
ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder,
|
||||
bool? forceUseMobileOptionMenuForImageClick,
|
||||
}) {
|
||||
return QuillEditorImageEmbedConfig(
|
||||
onImageRemovedCallback: onImageRemovedCallback ?? _onImageRemovedCallback,
|
||||
shouldRemoveImageCallback:
|
||||
shouldRemoveImageCallback ?? this.shouldRemoveImageCallback,
|
||||
imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder,
|
||||
imageErrorWidgetBuilder:
|
||||
imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,11 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BoxConstraints;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorWebImageEmbedConfig {
|
||||
const QuillEditorWebImageEmbedConfig({
|
||||
this.constraints,
|
||||
});
|
||||
|
||||
final BoxConstraints? constraints;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,77 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_utils.dart';
|
||||
import 'config/image_config.dart';
|
||||
import 'image_menu.dart';
|
||||
import 'widgets/image.dart';
|
||||
|
||||
class QuillEditorImageEmbedBuilder extends EmbedBuilder {
|
||||
QuillEditorImageEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
final QuillEditorImageEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.imageType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
final imageSource = standardizeImageUrl(embedContext.node.value.data);
|
||||
final ((imageSize), margin, alignment) = getElementAttributes(
|
||||
embedContext.node,
|
||||
context,
|
||||
);
|
||||
|
||||
final width = imageSize.width;
|
||||
final height = imageSize.height;
|
||||
|
||||
final imageWidget = getImageWidgetByImageSource(
|
||||
context: context,
|
||||
imageSource,
|
||||
imageProviderBuilder: config.imageProviderBuilder,
|
||||
imageErrorWidgetBuilder: config.imageErrorWidgetBuilder,
|
||||
alignment: alignment,
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final onImageClicked = config.onImageClicked;
|
||||
if (onImageClicked != null) {
|
||||
onImageClicked(imageSource);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => ImageOptionsMenu(
|
||||
controller: embedContext.controller,
|
||||
config: config,
|
||||
imageSource: imageSource,
|
||||
imageSize: imageSize,
|
||||
readOnly: embedContext.readOnly,
|
||||
imageProvider: imageWidget.image,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (margin != null) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(margin),
|
||||
child: imageWidget,
|
||||
);
|
||||
}
|
||||
return imageWidget;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,67 +1 @@
|
||||
import 'package:flutter/widgets.dart'
|
||||
show ImageErrorWidgetBuilder, ImageProvider;
|
||||
import 'package:flutter/widgets.dart' show BuildContext;
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
/// When request picking an image, for example when the image button toolbar
|
||||
/// clicked, it should be null in case the user didn't choose any image or
|
||||
/// any other reasons, and it should be the image file path as string that is
|
||||
/// exists in case the user picked the image successfully
|
||||
///
|
||||
/// by default we already have a default implementation that show a dialog
|
||||
/// request the source for picking the image, from gallery, link or camera
|
||||
typedef OnRequestPickImage = Future<String?> Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
/// A callback will called when inserting a image in the editor
|
||||
/// it have the logic that will insert the image block using the controller
|
||||
typedef OnImageInsertCallback = Future<void> Function(
|
||||
String image,
|
||||
QuillController controller,
|
||||
);
|
||||
|
||||
/// When a new image picked this callback will called and you might want to
|
||||
/// do some logic depending on your use case
|
||||
typedef OnImageInsertedCallback = Future<void> Function(
|
||||
String image,
|
||||
);
|
||||
|
||||
enum InsertImageSource {
|
||||
gallery,
|
||||
camera,
|
||||
link,
|
||||
}
|
||||
|
||||
/// Configurations for dealing with images, on insert a image
|
||||
/// on request picking a image
|
||||
@immutable
|
||||
class QuillToolbarImageConfig {
|
||||
const QuillToolbarImageConfig({
|
||||
this.onRequestPickImage,
|
||||
this.onImageInsertedCallback,
|
||||
this.onImageInsertCallback,
|
||||
});
|
||||
|
||||
final OnRequestPickImage? onRequestPickImage;
|
||||
|
||||
final OnImageInsertedCallback? onImageInsertedCallback;
|
||||
|
||||
final OnImageInsertCallback? onImageInsertCallback;
|
||||
}
|
||||
|
||||
typedef ImageEmbedBuilderWillRemoveCallback = Future<bool> Function(
|
||||
String imageUrl,
|
||||
);
|
||||
|
||||
typedef ImageEmbedBuilderOnRemovedCallback = Future<void> Function(
|
||||
String imageUrl,
|
||||
);
|
||||
|
||||
typedef ImageEmbedBuilderProviderBuilder = ImageProvider? Function(
|
||||
BuildContext context,
|
||||
String imageUrl,
|
||||
);
|
||||
|
||||
typedef ImageEmbedBuilderErrorWidgetBuilder = ImageErrorWidgetBuilder;
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import 'dart:async' show Completer;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ImageLoader {
|
||||
static ImageLoader _instance = ImageLoader();
|
||||
|
||||
static ImageLoader get instance => _instance;
|
||||
|
||||
/// Allows overriding the instance for testing
|
||||
@visibleForTesting
|
||||
static set instance(ImageLoader newInstance) => _instance = newInstance;
|
||||
|
||||
// TODO(performance): This will load the image again. In case
|
||||
// this is a network image, then this will be inefficient.
|
||||
Future<Uint8List?> loadImageBytesFromImageProvider({
|
||||
required ImageProvider imageProvider,
|
||||
}) async {
|
||||
final stream = imageProvider.resolve(ImageConfiguration.empty);
|
||||
final completer = Completer<ui.Image>();
|
||||
|
||||
ImageStreamListener? listener;
|
||||
listener = ImageStreamListener((info, _) {
|
||||
completer.complete(info.image);
|
||||
stream.removeListener(listener!);
|
||||
});
|
||||
|
||||
stream.addListener(listener);
|
||||
|
||||
final image = await completer.future;
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData?.buffer.asUint8List();
|
||||
}
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart' show showCupertinoModalPopup;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart'
|
||||
show ImageUrl, QuillController, StyleAttribute, getEmbedNode;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_utils.dart';
|
||||
import '../../common/utils/string.dart';
|
||||
import 'config/image_config.dart';
|
||||
import 'image_load_utils.dart';
|
||||
import 'image_save_utils.dart';
|
||||
import 'widgets/image.dart' show ImageTapWrapper, getImageStyleString;
|
||||
import 'widgets/image_resizer.dart' show ImageResizer;
|
||||
|
||||
class ImageOptionsMenu extends StatelessWidget {
|
||||
const ImageOptionsMenu({
|
||||
required this.controller,
|
||||
required this.config,
|
||||
required this.imageSource,
|
||||
required this.imageSize,
|
||||
required this.readOnly,
|
||||
required this.imageProvider,
|
||||
this.prefersGallerySave = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final QuillController controller;
|
||||
final QuillEditorImageEmbedConfig config;
|
||||
final String imageSource;
|
||||
final ElementSize imageSize;
|
||||
final bool readOnly;
|
||||
final ImageProvider imageProvider;
|
||||
|
||||
// TODO(quill_native_bridge): Update this doc comment once saveImageToGallery()
|
||||
// is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features
|
||||
/// Determines if the image should be saved to the gallery instead of using the
|
||||
/// system file save dialog for platforms that support both.
|
||||
///
|
||||
/// Currently, the only platform where this applies is macOS.
|
||||
///
|
||||
/// This is silently ignored on platforms that only support gallery save (Android and iOS)
|
||||
/// or only image save.
|
||||
///
|
||||
/// For more details, refer to [quill_native_bridge Saving images](https://pub.dev/packages/quill_native_bridge#-saving-images).
|
||||
final bool prefersGallerySave;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final materialTheme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
|
||||
child: SimpleDialog(
|
||||
title: Text(context.loc.image),
|
||||
children: [
|
||||
if (!readOnly)
|
||||
ListTile(
|
||||
title: Text(context.loc.resize),
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (modalContext) {
|
||||
final screenSize = MediaQuery.sizeOf(modalContext);
|
||||
return ImageResizer(
|
||||
onImageResize: (width, height) {
|
||||
final res = getEmbedNode(
|
||||
controller,
|
||||
controller.selection.start,
|
||||
);
|
||||
|
||||
final attr = replaceStyleStringWithSize(
|
||||
getImageStyleString(controller),
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
controller
|
||||
..skipRequestKeyboard = true
|
||||
..formatText(
|
||||
res.offset,
|
||||
1,
|
||||
StyleAttribute(attr),
|
||||
);
|
||||
},
|
||||
imageWidth: imageSize.width,
|
||||
imageHeight: imageSize.height,
|
||||
maxWidth: screenSize.width,
|
||||
maxHeight: screenSize.height,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy_all_outlined),
|
||||
title: Text(context.loc.copy),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
controller.copiedImageUrl = ImageUrl(
|
||||
imageSource,
|
||||
getImageStyleString(controller),
|
||||
);
|
||||
|
||||
final imageBytes = await ImageLoader.instance
|
||||
.loadImageBytesFromImageProvider(
|
||||
imageProvider: imageProvider);
|
||||
if (imageBytes != null) {
|
||||
await ClipboardServiceProvider.instance.copyImage(imageBytes);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!readOnly)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.delete_forever_outlined,
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
title: Text(context.loc.remove),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Call the remove check callback if set
|
||||
if (await config.shouldRemoveImageCallback?.call(imageSource) ==
|
||||
false) {
|
||||
return;
|
||||
}
|
||||
|
||||
final offset = getEmbedNode(
|
||||
controller,
|
||||
controller.selection.start,
|
||||
).offset;
|
||||
controller.replaceText(
|
||||
offset,
|
||||
1,
|
||||
'',
|
||||
TextSelection.collapsed(offset: offset),
|
||||
);
|
||||
// Call the post remove callback if set
|
||||
await config.onImageRemovedCallback.call(imageSource);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text(context.loc.save),
|
||||
onTap: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final localizations = context.loc;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
SaveImageResult? result;
|
||||
try {
|
||||
result = await ImageSaver.instance.saveImage(
|
||||
imageUrl: imageSource,
|
||||
imageProvider: imageProvider,
|
||||
prefersGallerySave: prefersGallerySave,
|
||||
);
|
||||
} on GalleryImageSaveAccessDeniedException {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
localizations.saveImagePermissionDenied,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
localizations.errorUnexpectedSavingImage,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(localizations.successImageDownloaded)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isGallerySave) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(localizations.successImageSavedGallery),
|
||||
action: SnackBarAction(
|
||||
label: localizations.openGallery,
|
||||
onPressed: () =>
|
||||
QuillNativeProvider.instance.openGalleryApp(),
|
||||
),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopApp) {
|
||||
final imageFilePath = result.imageFilePath;
|
||||
if (imageFilePath == null) {
|
||||
// User canceled the system save dialog.
|
||||
return;
|
||||
}
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(localizations.successImageSaved),
|
||||
// On macOS the app only has access to the picked file from the system save
|
||||
// dialog and not the directory where it was saved.
|
||||
// Opening the directory of that file requires entitlements on macOS
|
||||
// See https://pub.dev/packages/url_launcher#macos-file-access-configuration
|
||||
// Open the saved image file instead of the directory
|
||||
action: defaultTargetPlatform == TargetPlatform.macOS
|
||||
? SnackBarAction(
|
||||
label: localizations.openFile,
|
||||
onPressed: () => launchUrl(Uri.file(imageFilePath)),
|
||||
)
|
||||
: SnackBarAction(
|
||||
label: localizations.openFileLocation,
|
||||
onPressed: () => launchUrl(
|
||||
Uri.directory(p.dirname(imageFilePath))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw StateError(
|
||||
'Image save result is not handled on $defaultTargetPlatform');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.zoom_in),
|
||||
title: Text(context.loc.zoom),
|
||||
onTap: () => Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ImageTapWrapper(
|
||||
imageUrl: imageSource,
|
||||
config: config,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,254 +0,0 @@
|
||||
@internal
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'image_load_utils.dart';
|
||||
|
||||
const defaultImageFileExtension = 'png';
|
||||
|
||||
// The [imageSourcePath] could be file, asset path or HTTP image URL.
|
||||
String extractImageFileExtensionFromImageSource(String? imageSourcePath) {
|
||||
if (imageSourcePath == null || imageSourcePath.isEmpty) {
|
||||
return defaultImageFileExtension;
|
||||
}
|
||||
|
||||
if (!imageSourcePath.contains('.')) {
|
||||
return defaultImageFileExtension;
|
||||
}
|
||||
|
||||
return p.extension(imageSourcePath).replaceFirst('.', '');
|
||||
}
|
||||
|
||||
// The [imageSourcePath] could be file, asset path or HTTP image URL.
|
||||
String? extractImageNameFromImageSource(String? imageSourcePath) {
|
||||
if (imageSourcePath == null || imageSourcePath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final uri = Uri.parse(imageSourcePath);
|
||||
final pathWithoutQuery = uri.path;
|
||||
|
||||
final imageName = p.basenameWithoutExtension(pathWithoutQuery);
|
||||
if (imageName.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return imageName;
|
||||
}
|
||||
|
||||
class SaveImageResult {
|
||||
const SaveImageResult({
|
||||
required this.imageFilePath,
|
||||
required this.isGallerySave,
|
||||
});
|
||||
|
||||
/// Returns `null` on web platforms, if [isGallerySave] is `true`
|
||||
/// or in case the user cancels the save operation on desktop platforms.
|
||||
final String? imageFilePath;
|
||||
|
||||
final bool isGallerySave;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(other, this)) return true;
|
||||
if (other is! SaveImageResult) return false;
|
||||
return other.imageFilePath == imageFilePath &&
|
||||
other.isGallerySave == isGallerySave;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(imageFilePath, isGallerySave);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SaveImageResult(imageFilePath: $imageFilePath, isGallerySave: $isGallerySave)';
|
||||
}
|
||||
|
||||
const String defaultImageFileNamePrefix = 'IMG';
|
||||
|
||||
String getDefaultImageFileName({required bool isGallerySave}) {
|
||||
if (kIsWeb) {
|
||||
// The browser handles name conflicts.
|
||||
return defaultImageFileNamePrefix;
|
||||
}
|
||||
if (isGallerySave) {
|
||||
// The gallery app handles name conflicts.
|
||||
return defaultImageFileNamePrefix;
|
||||
}
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS ||
|
||||
defaultTargetPlatform == TargetPlatform.windows) {
|
||||
// Windows and macOS system native save dialog prompts the user to confirm file overwrite.
|
||||
return defaultImageFileNamePrefix;
|
||||
}
|
||||
final uniqueFileName =
|
||||
'${defaultImageFileNamePrefix}_${DateTime.now().toIso8601String()}';
|
||||
if (defaultTargetPlatform == TargetPlatform.linux) {
|
||||
// IMPORTANT: On Linux, it depends on the desktop environment
|
||||
// and name conflicts may not be handled. Always provide a unique image file name.
|
||||
return uniqueFileName;
|
||||
}
|
||||
|
||||
return uniqueFileName;
|
||||
}
|
||||
|
||||
Future<bool> shouldSaveToGallery({required bool prefersGallerySave}) async {
|
||||
final supportsGallerySave = await QuillNativeProvider.instance
|
||||
.isSupported(QuillNativeBridgeFeature.saveImageToGallery);
|
||||
if (!supportsGallerySave) {
|
||||
return false;
|
||||
}
|
||||
final supportsImageSave = await QuillNativeProvider.instance
|
||||
.isSupported(QuillNativeBridgeFeature.saveImage);
|
||||
if (!supportsImageSave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return supportsGallerySave && prefersGallerySave;
|
||||
}
|
||||
|
||||
/// Thrown when the gallery image save operation is denied
|
||||
/// due to insufficient or denied permissions.
|
||||
class GalleryImageSaveAccessDeniedException implements Exception {
|
||||
GalleryImageSaveAccessDeniedException([this.message]);
|
||||
|
||||
final String? message;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
message ??
|
||||
'Permission to save the image to the gallery was denied or insufficient.';
|
||||
}
|
||||
|
||||
class ImageSaver {
|
||||
ImageSaver._();
|
||||
|
||||
static ImageSaver _instance = ImageSaver._();
|
||||
|
||||
static ImageSaver get instance => _instance;
|
||||
|
||||
/// Allows overriding the instance for testing
|
||||
@visibleForTesting
|
||||
static set instance(ImageSaver newInstance) => _instance = newInstance;
|
||||
|
||||
/// Saves an image to the user's device based on the platform:
|
||||
///
|
||||
/// - **Web**: Downloads the image using the browser's download functionality.
|
||||
/// - **Desktop**: Prompts the user to choose a location for the image using
|
||||
/// native save dialog, defaulting to the user's `Pictures` directory. Or
|
||||
/// saves the image to the gallery in case [prefersGallerySave] is `true` and
|
||||
// TODO(quill_native_bridge): Update this doc comment once saveImageToGallery()
|
||||
// is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features
|
||||
/// the gallery is supported (currently only macOS is applicable).
|
||||
/// - **Mobile**: Saves the image to the gallery, requesting permission if needed.
|
||||
///
|
||||
/// The [imageUrl] could be file or network image URL and is used to extract
|
||||
/// image file extension and the image name.
|
||||
///
|
||||
/// The [imageProvider] is used to load the image bytes from using [ImageLoader].
|
||||
///
|
||||
/// Returns `null` on failure.
|
||||
///
|
||||
/// Throws [GalleryImageSaveAccessDeniedException] in case permission was denied or insuffeicnet.
|
||||
Future<SaveImageResult?> saveImage({
|
||||
required String imageUrl,
|
||||
required ImageProvider imageProvider,
|
||||
required bool prefersGallerySave,
|
||||
}) async {
|
||||
assert(() {
|
||||
if (imageUrl.isEmpty) {
|
||||
throw ArgumentError.value(imageUrl, 'imageUrl', 'cannot be empty');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
final imageFileExtension =
|
||||
extractImageFileExtensionFromImageSource(imageUrl);
|
||||
final imageName = extractImageNameFromImageSource(imageUrl);
|
||||
|
||||
final imageBytes = await ImageLoader.instance
|
||||
.loadImageBytesFromImageProvider(imageProvider: imageProvider);
|
||||
if (imageBytes == null || imageBytes.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
await QuillNativeProvider.instance.saveImage(
|
||||
imageBytes,
|
||||
options: ImageSaveOptions(
|
||||
name: imageName ?? getDefaultImageFileName(isGallerySave: false),
|
||||
fileExtension: imageFileExtension),
|
||||
);
|
||||
return const SaveImageResult(
|
||||
imageFilePath: null,
|
||||
isGallerySave: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (await shouldSaveToGallery(prefersGallerySave: prefersGallerySave)) {
|
||||
try {
|
||||
await QuillNativeProvider.instance.saveImageToGallery(
|
||||
imageBytes,
|
||||
options: GalleryImageSaveOptions(
|
||||
name: imageName ?? getDefaultImageFileName(isGallerySave: true),
|
||||
fileExtension: imageFileExtension,
|
||||
// Specifying the album name requires read-write permission
|
||||
// on iOS and macOS on all versions. Pass null to request add-only on
|
||||
// supported versions (previous versions still use read-write).
|
||||
albumName: null,
|
||||
),
|
||||
);
|
||||
|
||||
return const SaveImageResult(
|
||||
imageFilePath: null,
|
||||
isGallerySave: true,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
// TODO(save-image): Part of https://github.com/FlutterQuill/quill-native-bridge/issues/2
|
||||
|
||||
// Permission request is required only on iOS, macOS and Android API 28 and earlier.
|
||||
if (e.code == 'PERMISSION_DENIED') {
|
||||
// macOS imposes security restrictions when running the app
|
||||
// on sources other than Xcode or the macOS terminal, such as Android Studio or VS Code.
|
||||
// This is not an issue in production. Throwing [GalleryImageSaveAccessDeniedException] will indicate
|
||||
// that the user denied the permission, even though it will always deny the permission even if granted.
|
||||
// Make sure we don't handle that error (it has details) during development to avoid confusion.
|
||||
// For more details, see https://github.com/flutter/flutter/issues/134191#issuecomment-2506248266
|
||||
// and https://pub.dev/packages/quill_native_bridge#-saving-images-to-the-gallery
|
||||
|
||||
final possiblePermissionIssueDuringDevelopmentOnMacOS =
|
||||
kDebugMode && defaultTargetPlatform == TargetPlatform.macOS;
|
||||
if (possiblePermissionIssueDuringDevelopmentOnMacOS) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
throw GalleryImageSaveAccessDeniedException(e.toString());
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
if (await QuillNativeProvider.instance
|
||||
.isSupported(QuillNativeBridgeFeature.saveImage)) {
|
||||
assert(!isMobileApp,
|
||||
'Mobile platforms support saving images to the gallery only');
|
||||
|
||||
final result = await QuillNativeProvider.instance.saveImage(
|
||||
imageBytes,
|
||||
options: ImageSaveOptions(
|
||||
name: imageName ?? getDefaultImageFileName(isGallerySave: false),
|
||||
fileExtension: imageFileExtension,
|
||||
),
|
||||
);
|
||||
return SaveImageResult(
|
||||
imageFilePath: result.filePath,
|
||||
isGallerySave: false,
|
||||
);
|
||||
}
|
||||
|
||||
throw StateError('Image save is not handled on $defaultTargetPlatform');
|
||||
}
|
||||
}
|
||||
@ -1,64 +1 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_web_utils.dart';
|
||||
import '../../common/utils/utils.dart';
|
||||
import '../../common/utils/web/web.dart';
|
||||
import 'config/image_web_config.dart';
|
||||
|
||||
class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
|
||||
const QuillEditorWebImageEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
|
||||
final QuillEditorWebImageEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.imageType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform');
|
||||
|
||||
final (height, width, margin, alignment) =
|
||||
getWebElementAttributes(embedContext.node);
|
||||
|
||||
var imageSource = embedContext.node.value.data.toString();
|
||||
|
||||
// This logic make sure if the image is imageBase64 then
|
||||
// it make sure if the pattern is like
|
||||
// data:image/png;base64, [base64 encoded image string here]
|
||||
// if not then it will add the data:image/png;base64, at the first
|
||||
if (isImageBase64(imageSource)) {
|
||||
// Sometimes the image base 64 for some reasons
|
||||
// doesn't displayed with the 'data:image/png;base64'
|
||||
if (!(imageSource.startsWith('data:image/') &&
|
||||
imageSource.contains('base64'))) {
|
||||
imageSource = 'data:image/png;base64, $imageSource';
|
||||
}
|
||||
}
|
||||
|
||||
createHtmlImageElement(
|
||||
src: imageSource,
|
||||
alignSelf: alignment,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints:
|
||||
config.constraints ?? BoxConstraints.loose(const Size(200, 200)),
|
||||
child: HtmlElementView(
|
||||
viewType: imageSource,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
import 'dart:convert' show base64;
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
import '../../../common/utils/utils.dart';
|
||||
import '../config/image_config.dart';
|
||||
import '../image_embed_types.dart';
|
||||
|
||||
String getImageStyleString(QuillController controller) {
|
||||
final String? s = controller
|
||||
.getAllSelectionStyles()
|
||||
.firstWhere((s) => s.attributes.containsKey(Attribute.style.key),
|
||||
orElse: Style.new)
|
||||
.attributes[Attribute.style.key]
|
||||
?.value;
|
||||
return s ?? '';
|
||||
}
|
||||
|
||||
/// [imageProviderBuilder] To override the return value pass value to it
|
||||
/// [imageSource] The source of the image in the quill delta json document
|
||||
/// It could be http, file, network, asset, or base 64 image
|
||||
ImageProvider getImageProviderByImageSource(
|
||||
String imageSource, {
|
||||
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
if (imageProviderBuilder != null) {
|
||||
final imageProvider = imageProviderBuilder(context, imageSource);
|
||||
if (imageProvider != null) {
|
||||
return imageProvider;
|
||||
}
|
||||
}
|
||||
|
||||
if (isImageBase64(imageSource)) {
|
||||
return MemoryImage(base64.decode(imageSource));
|
||||
}
|
||||
|
||||
if (isHttpUrl(imageSource)) {
|
||||
return NetworkImage(imageSource);
|
||||
}
|
||||
|
||||
// File image
|
||||
if (kIsWeb) {
|
||||
return NetworkImage(imageSource);
|
||||
}
|
||||
return FileImage(File(imageSource));
|
||||
}
|
||||
|
||||
Image getImageWidgetByImageSource(
|
||||
String imageSource, {
|
||||
required BuildContext context,
|
||||
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||
required ImageErrorWidgetBuilder? imageErrorWidgetBuilder,
|
||||
double? width,
|
||||
double? height,
|
||||
AlignmentGeometry alignment = Alignment.center,
|
||||
}) {
|
||||
return Image(
|
||||
image: getImageProviderByImageSource(
|
||||
context: context,
|
||||
imageSource,
|
||||
imageProviderBuilder: imageProviderBuilder,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
alignment: alignment,
|
||||
errorBuilder: imageErrorWidgetBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
String standardizeImageUrl(String url) {
|
||||
if (url.contains('base64')) {
|
||||
return url.split(',')[1];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
const List<String> _imageFileExtensions = [
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.gif',
|
||||
'.webp',
|
||||
'.tif',
|
||||
'.heic'
|
||||
];
|
||||
|
||||
/// This is a bug of Gallery Saver Package.
|
||||
/// It can not save image that's filename does not end with it's file extension
|
||||
/// like below.
|
||||
// "https://firebasestorage.googleapis.com/v0/b/eventat-4ba96.appspot.com/o/2019-Metrology-Events.jpg?alt=media&token=bfc47032-5173-4b3f-86bb-9659f46b362a"
|
||||
/// If imageUrl does not end with it's file extension,
|
||||
/// file extension is added to image url for saving.
|
||||
String appendFileExtensionToImageUrl(String url) {
|
||||
final endsWithImageFileExtension = _imageFileExtensions
|
||||
.firstWhere((s) => url.toLowerCase().endsWith(s), orElse: () => '');
|
||||
if (endsWithImageFileExtension.isNotEmpty) {
|
||||
return url;
|
||||
}
|
||||
|
||||
final imageFileExtension = _imageFileExtensions
|
||||
.firstWhere((s) => url.toLowerCase().contains(s), orElse: () => '');
|
||||
|
||||
return url + imageFileExtension;
|
||||
}
|
||||
|
||||
class ImageTapWrapper extends StatelessWidget {
|
||||
const ImageTapWrapper({
|
||||
required this.imageUrl,
|
||||
required this.config,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String imageUrl;
|
||||
final QuillEditorImageEmbedConfig config;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
constraints: BoxConstraints.expand(
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
PhotoView(
|
||||
imageProvider: getImageProviderByImageSource(
|
||||
context: context,
|
||||
imageUrl,
|
||||
imageProviderBuilder: config.imageProviderBuilder,
|
||||
),
|
||||
errorBuilder: config.imageErrorWidgetBuilder,
|
||||
loadingBuilder: (context, event) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
right: 10,
|
||||
top: MediaQuery.paddingOf(context).top + 10.0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.2,
|
||||
child: Container(
|
||||
height: 30,
|
||||
width: 30,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: Colors.grey[400],
|
||||
size: 28,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart'
|
||||
show CupertinoActionSheet, CupertinoActionSheetAction;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart' show SchedulerBinding;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
class ImageResizer extends StatefulWidget {
|
||||
const ImageResizer({
|
||||
required this.imageWidth,
|
||||
required this.imageHeight,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
required this.onImageResize,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double? imageWidth;
|
||||
final double? imageHeight;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
final Function(double width, double height) onImageResize;
|
||||
|
||||
@override
|
||||
ImageResizerState createState() => ImageResizerState();
|
||||
}
|
||||
|
||||
class ImageResizerState extends State<ImageResizer> {
|
||||
late double _width;
|
||||
late double _height;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_width = widget.imageWidth ?? widget.maxWidth;
|
||||
_height = widget.imageHeight ?? widget.maxHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Theme.of(context).isCupertino) {
|
||||
return _showCupertinoMenu();
|
||||
}
|
||||
return _showMaterialMenu();
|
||||
}
|
||||
|
||||
Widget _showMaterialMenu() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_widthSlider(),
|
||||
_heightSlider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _showCupertinoMenu() {
|
||||
return CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {},
|
||||
child: _widthSlider(),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {},
|
||||
child: _heightSlider(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _slider({
|
||||
required bool isWidth,
|
||||
required ValueChanged<double> onChanged,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Card(
|
||||
child: Slider.adaptive(
|
||||
value: isWidth ? _width : _height,
|
||||
max: isWidth ? widget.maxWidth : widget.maxHeight,
|
||||
divisions: 1000,
|
||||
// Might need to be changed
|
||||
label: isWidth ? context.loc.width : context.loc.height,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
onChanged(val);
|
||||
_resizeImage();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _heightSlider() {
|
||||
return _slider(
|
||||
isWidth: false,
|
||||
onChanged: (value) {
|
||||
_height = value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _widthSlider() {
|
||||
return _slider(
|
||||
isWidth: true,
|
||||
onChanged: (value) {
|
||||
_width = value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _scheduled = false;
|
||||
|
||||
void _resizeImage() {
|
||||
if (_scheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scheduled = true;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onImageResize(_width, _height);
|
||||
_scheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,46 +1 @@
|
||||
import 'package:flutter/widgets.dart' show GlobalKey, Widget;
|
||||
import 'package:meta/meta.dart' show experimental, immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorVideoEmbedConfig {
|
||||
const QuillEditorVideoEmbedConfig({
|
||||
this.onVideoInit,
|
||||
this.customVideoBuilder,
|
||||
});
|
||||
|
||||
/// [onVideoInit] is a callback function that gets triggered when
|
||||
/// a video is initialized.
|
||||
/// You can use this to perform actions or setup configurations related
|
||||
/// to video embedding.
|
||||
///
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// onVideoInit: (videoContainerKey) {
|
||||
/// // Custom video initialization logic
|
||||
/// },
|
||||
/// // Customize other callback functions as needed
|
||||
/// ```
|
||||
final void Function(GlobalKey videoContainerKey)? onVideoInit;
|
||||
|
||||
/// [customVideoBuilder] is a callback function that receives the
|
||||
/// video URL and a read-only flag. This allows users to define
|
||||
/// their own logic for rendering video widgets, enabling support
|
||||
/// for various video platforms, such as YouTube.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// customVideoBuilder: (videoUrl, readOnly) {
|
||||
/// // Return `null` to fallback to defualt logic of QuillEditorVideoEmbedBuilder
|
||||
///
|
||||
/// // Return a custom video widget based on the videoUrl
|
||||
/// return CustomVideoWidget(videoUrl: videoUrl, readOnly: readOnly);
|
||||
/// },
|
||||
/// ```
|
||||
///
|
||||
/// It's a quick solution as response to https://github.com/singerdmx/flutter-quill/issues/2284
|
||||
///
|
||||
/// **Might be removed or changed in future releases.**
|
||||
@experimental
|
||||
final Widget? Function(String videoUrl, bool readOnly)? customVideoBuilder;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,6 +1 @@
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorWebVideoEmbedConfig {
|
||||
const QuillEditorWebVideoEmbedConfig();
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,55 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_utils.dart';
|
||||
import 'config/video_config.dart';
|
||||
import 'widgets/video_app.dart';
|
||||
|
||||
class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
|
||||
const QuillEditorVideoEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
|
||||
final QuillEditorVideoEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.videoType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
final videoUrl = embedContext.node.value.data;
|
||||
|
||||
final customVideoBuilder = config.customVideoBuilder;
|
||||
if (customVideoBuilder != null) {
|
||||
final videoWidget = customVideoBuilder(videoUrl, embedContext.readOnly);
|
||||
if (videoWidget != null) {
|
||||
return videoWidget;
|
||||
}
|
||||
}
|
||||
|
||||
final ((elementSize), margin, alignment) = getElementAttributes(
|
||||
embedContext.node,
|
||||
context,
|
||||
);
|
||||
|
||||
final width = elementSize.width;
|
||||
final height = elementSize.height;
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: EdgeInsets.all(margin ?? 0.0),
|
||||
alignment: alignment,
|
||||
child: VideoApp(
|
||||
videoUrl: videoUrl,
|
||||
readOnly: embedContext.readOnly,
|
||||
onVideoInit: config.onVideoInit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,55 +1 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_web_utils.dart';
|
||||
import '../../common/utils/utils.dart';
|
||||
import '../../common/utils/web/web.dart';
|
||||
import 'config/video_web_config.dart';
|
||||
import 'youtube_video_url.dart';
|
||||
|
||||
class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder {
|
||||
const QuillEditorWebVideoEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
|
||||
final QuillEditorWebVideoEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.videoType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
var videoUrl = embedContext.node.value.data;
|
||||
if (isYouTubeUrl(videoUrl)) {
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
final youtubeID = convertVideoUrlToId(videoUrl);
|
||||
if (youtubeID != null) {
|
||||
videoUrl = 'https://www.youtube.com/embed/$youtubeID';
|
||||
}
|
||||
}
|
||||
|
||||
final (height, width, margin, alignment) =
|
||||
getWebElementAttributes(embedContext.node);
|
||||
|
||||
createHtmlIFrameElement(
|
||||
src: videoUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
alignSelf: alignment,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: 500,
|
||||
child: HtmlElementView(
|
||||
viewType: videoUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../../common/utils/utils.dart';
|
||||
|
||||
/// Widget for playing back video
|
||||
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
|
||||
class VideoApp extends StatefulWidget {
|
||||
const VideoApp({
|
||||
required this.videoUrl,
|
||||
required this.readOnly,
|
||||
super.key,
|
||||
this.onVideoInit,
|
||||
});
|
||||
|
||||
final String videoUrl;
|
||||
final bool readOnly;
|
||||
final void Function(GlobalKey videoContainerKey)? onVideoInit;
|
||||
|
||||
@override
|
||||
VideoAppState createState() => VideoAppState();
|
||||
}
|
||||
|
||||
class VideoAppState extends State<VideoApp> {
|
||||
late VideoPlayerController _controller;
|
||||
GlobalKey videoContainerKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = isHttpUrl(widget.videoUrl)
|
||||
? VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl))
|
||||
: VideoPlayerController.file(File(widget.videoUrl))
|
||||
..initialize().then((_) {
|
||||
// Ensure the first frame is shown after the video is initialized,
|
||||
// even before the play button has been pressed.
|
||||
setState(() {});
|
||||
if (widget.onVideoInit != null) {
|
||||
widget.onVideoInit?.call(videoContainerKey);
|
||||
}
|
||||
}).catchError((error) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultStyles = DefaultStyles.getInstance(context);
|
||||
if (_controller.value.hasError) {
|
||||
if (widget.readOnly) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: widget.videoUrl,
|
||||
style: defaultStyles.link,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(
|
||||
Uri.parse(widget.videoUrl),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: widget.videoUrl,
|
||||
style: defaultStyles.link,
|
||||
),
|
||||
);
|
||||
} else if (!_controller.value.isInitialized) {
|
||||
return VideoProgressIndicator(
|
||||
_controller,
|
||||
allowScrubbing: true,
|
||||
colors: const VideoProgressColors(playedColor: Colors.blue),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
key: videoContainerKey,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_controller.value.isPlaying
|
||||
? _controller.pause()
|
||||
: _controller.play();
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)),
|
||||
_controller.value.isPlaying
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
color: const Color(0xfff5f5f5),
|
||||
child: const Icon(
|
||||
Icons.play_arrow,
|
||||
size: 60,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Function copied from https://github.com/sarbagyastha/youtube_player_flutter/blob/f8e1e79991066bcc70f0a7c93941ca0d54b7370e/packages/youtube_player_flutter/lib/src/player/youtube_player.dart#L154
|
||||
/// and is not written as part of this project.
|
||||
///
|
||||
/// Used as quick response for https://github.com/singerdmx/flutter-quill/issues/2284
|
||||
@experimental
|
||||
@internal
|
||||
@Deprecated(
|
||||
'Will be removed in future releases, for now included as quick response to https://github.com/singerdmx/flutter-quill/issues/2284',
|
||||
)
|
||||
String? convertVideoUrlToId(String url, {bool trimWhitespaces = true}) {
|
||||
if (!url.contains('http') && (url.length == 11)) return url;
|
||||
if (trimWhitespaces) url = url.trim();
|
||||
|
||||
for (final exp in [
|
||||
RegExp(
|
||||
r'^https:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(
|
||||
r'^https:\/\/(?:music\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(
|
||||
r'^https:\/\/(?:www\.|m\.)?youtube\.com\/shorts\/([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(
|
||||
r'^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(r'^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$')
|
||||
]) {
|
||||
final Match? match = exp.firstMatch(url);
|
||||
if (match != null && match.groupCount >= 1) return match.group(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,106 +1 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import 'editor/image/config/image_config.dart';
|
||||
import 'editor/image/image_embed.dart';
|
||||
import 'editor/video/config/video_config.dart';
|
||||
import 'editor/video/config/video_web_config.dart';
|
||||
import 'editor/video/video_embed.dart';
|
||||
import 'editor/video/video_web_embed.dart';
|
||||
import 'toolbar/camera/camera_button.dart';
|
||||
import 'toolbar/camera/config/camera_config.dart';
|
||||
import 'toolbar/image/config/image_config.dart';
|
||||
import 'toolbar/image/image_button.dart';
|
||||
import 'toolbar/video/config/video_config.dart';
|
||||
import 'toolbar/video/video_button.dart';
|
||||
|
||||
abstract final class FlutterQuillEmbeds {
|
||||
/// Returns a list of embed builders for [QuillEditor]
|
||||
/// to provide basic support for loading images and videos.
|
||||
///
|
||||
static List<EmbedBuilder> editorBuilders({
|
||||
QuillEditorImageEmbedConfig? imageEmbedConfig =
|
||||
const QuillEditorImageEmbedConfig(),
|
||||
QuillEditorVideoEmbedConfig? videoEmbedConfig =
|
||||
const QuillEditorVideoEmbedConfig(),
|
||||
}) {
|
||||
return [
|
||||
if (imageEmbedConfig != null)
|
||||
QuillEditorImageEmbedBuilder(
|
||||
config: imageEmbedConfig,
|
||||
),
|
||||
if (videoEmbedConfig != null)
|
||||
QuillEditorVideoEmbedBuilder(
|
||||
config: videoEmbedConfig,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns a list of embed builders specifically designed for web support
|
||||
/// to load images and videos.
|
||||
///
|
||||
static List<EmbedBuilder> editorWebBuilders({
|
||||
QuillEditorImageEmbedConfig? imageEmbedConfig =
|
||||
const QuillEditorImageEmbedConfig(),
|
||||
QuillEditorWebVideoEmbedConfig? videoEmbedConfig =
|
||||
const QuillEditorWebVideoEmbedConfig(),
|
||||
}) {
|
||||
if (!kIsWeb) {
|
||||
throw UnsupportedError(
|
||||
'The ${FlutterQuillEmbeds.editorWebBuilders} is for web, use ${FlutterQuillEmbeds.editorBuilders} '
|
||||
'instead for non-web platforms',
|
||||
);
|
||||
}
|
||||
return [
|
||||
if (imageEmbedConfig != null)
|
||||
QuillEditorImageEmbedBuilder(
|
||||
config: imageEmbedConfig,
|
||||
),
|
||||
if (videoEmbedConfig != null)
|
||||
QuillEditorWebVideoEmbedBuilder(
|
||||
config: videoEmbedConfig,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns a list of embed builders for [QuillEditor].
|
||||
///
|
||||
/// It will use [editorWebBuilders] for web and [editorBuilders] for non-web platforms.
|
||||
static List<EmbedBuilder> defaultEditorBuilders() {
|
||||
return kIsWeb ? editorWebBuilders() : editorBuilders();
|
||||
}
|
||||
|
||||
/// Returns a list of embed button builders to support images and videos.
|
||||
///
|
||||
/// Pass `null` to options of a button to not show it.
|
||||
static List<EmbedButtonBuilder> toolbarButtons({
|
||||
QuillToolbarImageButtonOptions? imageButtonOptions =
|
||||
const QuillToolbarImageButtonOptions(),
|
||||
QuillToolbarVideoButtonOptions? videoButtonOptions =
|
||||
const QuillToolbarVideoButtonOptions(),
|
||||
QuillToolbarCameraButtonOptions? cameraButtonOptions,
|
||||
}) =>
|
||||
[
|
||||
if (imageButtonOptions != null)
|
||||
(context, embedContext) => QuillToolbarImageButton(
|
||||
controller: embedContext.controller,
|
||||
options: imageButtonOptions,
|
||||
// ignore: invalid_use_of_internal_member
|
||||
baseOptions: embedContext.baseButtonOptions,
|
||||
),
|
||||
if (videoButtonOptions != null)
|
||||
(context, embedContext) => QuillToolbarVideoButton(
|
||||
controller: embedContext.controller,
|
||||
options: videoButtonOptions,
|
||||
// ignore: invalid_use_of_internal_member
|
||||
baseOptions: embedContext.baseButtonOptions,
|
||||
),
|
||||
if (cameraButtonOptions != null)
|
||||
(context, embedContext) => QuillToolbarCameraButton(
|
||||
controller: embedContext.controller,
|
||||
options: cameraButtonOptions,
|
||||
// ignore: invalid_use_of_internal_member
|
||||
baseOptions: embedContext.baseButtonOptions,
|
||||
),
|
||||
];
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,132 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import '../../common/default_image_insert.dart';
|
||||
import '../../common/default_video_insert.dart';
|
||||
import '../quill_simple_toolbar_api.dart';
|
||||
import 'camera_types.dart';
|
||||
import 'config/camera_config.dart';
|
||||
import 'select_camera_action.dart';
|
||||
|
||||
// ignore: invalid_use_of_internal_member
|
||||
class QuillToolbarCameraButton extends QuillToolbarBaseButtonStateless {
|
||||
const QuillToolbarCameraButton({
|
||||
required super.controller,
|
||||
QuillToolbarCameraButtonOptions? options,
|
||||
|
||||
/// Shares common options between all buttons, prefer the [options]
|
||||
/// over the [baseOptions].
|
||||
super.baseOptions,
|
||||
super.key,
|
||||
}) : _options = options,
|
||||
super(options: options);
|
||||
|
||||
final QuillToolbarCameraButtonOptions? _options;
|
||||
|
||||
@override
|
||||
QuillToolbarCameraButtonOptions? get options => _options;
|
||||
|
||||
void _sharedOnPressed(BuildContext context) {
|
||||
_onPressedHandler(
|
||||
context,
|
||||
controller,
|
||||
);
|
||||
afterButtonPressed(context);
|
||||
}
|
||||
|
||||
Future<CameraAction?> _getCameraAction(BuildContext context) async {
|
||||
final customCallback = options?.cameraConfig?.onRequestCameraActionCallback;
|
||||
if (customCallback != null) {
|
||||
return await customCallback(context);
|
||||
}
|
||||
final cameraAction = await showSelectCameraActionDialog(
|
||||
context: context,
|
||||
);
|
||||
|
||||
return cameraAction;
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(
|
||||
BuildContext context,
|
||||
QuillController controller,
|
||||
) async {
|
||||
final cameraAction = await _getCameraAction(context);
|
||||
|
||||
if (cameraAction == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cameraAction) {
|
||||
case CameraAction.video:
|
||||
final videoFile =
|
||||
await ImagePicker().pickVideo(source: ImageSource.camera);
|
||||
if (videoFile == null) {
|
||||
return;
|
||||
}
|
||||
await handleVideoInsert(
|
||||
videoFile.path,
|
||||
controller: controller,
|
||||
onVideoInsertCallback: options?.cameraConfig?.onVideoInsertCallback,
|
||||
onVideoInsertedCallback:
|
||||
options?.cameraConfig?.onVideoInsertedCallback,
|
||||
);
|
||||
case CameraAction.image:
|
||||
final imageFile =
|
||||
await ImagePicker().pickImage(source: ImageSource.camera);
|
||||
if (imageFile == null) {
|
||||
return;
|
||||
}
|
||||
await handleImageInsert(
|
||||
imageFile.path,
|
||||
controller: controller,
|
||||
onImageInsertCallback: options?.cameraConfig?.onImageInsertCallback,
|
||||
onImageInsertedCallback:
|
||||
options?.cameraConfig?.onImageInsertedCallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildButton(BuildContext context) {
|
||||
return QuillToolbarIconButton(
|
||||
icon: Icon(
|
||||
iconData(context),
|
||||
size: iconButtonFactor(context) * iconSize(context),
|
||||
),
|
||||
tooltip: tooltip(context),
|
||||
isSelected: false,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
iconTheme: iconTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||
return childBuilder?.call(
|
||||
QuillToolbarCameraButtonOptions(
|
||||
afterButtonPressed: afterButtonPressed(context),
|
||||
iconData: iconData(context),
|
||||
iconSize: iconSize(context),
|
||||
iconButtonFactor: iconButtonFactor(context),
|
||||
iconTheme: options?.iconTheme,
|
||||
tooltip: tooltip(context),
|
||||
cameraConfig: options?.cameraConfig,
|
||||
),
|
||||
QuillToolbarCameraButtonExtraOptions(
|
||||
controller: controller,
|
||||
context: context,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||
(context) => Icons.photo_camera;
|
||||
|
||||
@override
|
||||
String Function(BuildContext context) get getDefaultTooltip =>
|
||||
(context) => context.loc.camera;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,39 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BuildContext;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../editor/image/image_embed_types.dart';
|
||||
import '../video/config/video.dart';
|
||||
|
||||
enum CameraAction {
|
||||
video,
|
||||
image,
|
||||
}
|
||||
|
||||
/// When the user click the camera button, should we take a photo or record
|
||||
/// a video using the camera
|
||||
///
|
||||
/// by default will show a dialog that ask the user which option he/she wants
|
||||
typedef OnRequestCameraActionCallback = Future<CameraAction?> Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
@immutable
|
||||
class QuillToolbarCameraConfig {
|
||||
const QuillToolbarCameraConfig({
|
||||
this.onRequestCameraActionCallback,
|
||||
this.onImageInsertCallback,
|
||||
this.onImageInsertedCallback,
|
||||
this.onVideoInsertedCallback,
|
||||
this.onVideoInsertCallback,
|
||||
});
|
||||
|
||||
final OnRequestCameraActionCallback? onRequestCameraActionCallback;
|
||||
|
||||
final OnImageInsertedCallback? onImageInsertedCallback;
|
||||
|
||||
final OnImageInsertCallback? onImageInsertCallback;
|
||||
|
||||
final OnVideoInsertedCallback? onVideoInsertedCallback;
|
||||
|
||||
final OnVideoInsertCallback? onVideoInsertCallback;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,28 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../camera_types.dart';
|
||||
|
||||
class QuillToolbarCameraButtonExtraOptions
|
||||
extends QuillToolbarBaseButtonExtraOptions {
|
||||
const QuillToolbarCameraButtonExtraOptions({
|
||||
required super.controller,
|
||||
required super.context,
|
||||
required super.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||
QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> {
|
||||
const QuillToolbarCameraButtonOptions({
|
||||
this.cameraConfig,
|
||||
super.iconSize,
|
||||
super.iconButtonFactor,
|
||||
super.iconData,
|
||||
super.afterButtonPressed,
|
||||
super.tooltip,
|
||||
super.iconTheme,
|
||||
super.childBuilder,
|
||||
});
|
||||
|
||||
final QuillToolbarCameraConfig? cameraConfig;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'camera_types.dart';
|
||||
|
||||
class SelectCameraActionDialog extends StatelessWidget {
|
||||
const SelectCameraActionDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.loc.photo),
|
||||
subtitle: Text(
|
||||
context.loc.takeAPhotoUsingYourCamera,
|
||||
),
|
||||
leading: const Icon(Icons.photo_sharp),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(CameraAction.image),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.video),
|
||||
subtitle: Text(
|
||||
context.loc.recordAVideoUsingYourCamera,
|
||||
),
|
||||
leading: const Icon(Icons.camera),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(CameraAction.video),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<CameraAction?> showSelectCameraActionDialog({
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final imageSource = await showModalBottomSheet<CameraAction>(
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
builder: (context) => const SelectCameraActionDialog(),
|
||||
);
|
||||
return imageSource;
|
||||
}
|
||||
@ -1,39 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../../editor/image/image_embed_types.dart';
|
||||
|
||||
class QuillToolbarImageButtonExtraOptions
|
||||
extends QuillToolbarBaseButtonExtraOptions {
|
||||
const QuillToolbarImageButtonExtraOptions({
|
||||
required super.controller,
|
||||
required super.context,
|
||||
required super.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||
QuillToolbarImageButtonOptions, QuillToolbarImageButtonExtraOptions> {
|
||||
const QuillToolbarImageButtonOptions({
|
||||
super.iconData,
|
||||
super.iconSize,
|
||||
super.iconButtonFactor,
|
||||
|
||||
/// specifies the tooltip text for the image button.
|
||||
super.tooltip,
|
||||
super.afterButtonPressed,
|
||||
super.childBuilder,
|
||||
super.iconTheme,
|
||||
this.dialogTheme,
|
||||
this.linkRegExp,
|
||||
this.imageButtonConfig = const QuillToolbarImageConfig(),
|
||||
});
|
||||
|
||||
final QuillDialogTheme? dialogTheme;
|
||||
|
||||
/// [imageLinkRegExp] is a regular expression to identify image links.
|
||||
final RegExp? linkRegExp;
|
||||
|
||||
final QuillToolbarImageConfig? imageButtonConfig;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,135 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../common/default_image_insert.dart';
|
||||
import '../../common/image_video_utils.dart';
|
||||
import '../../editor/image/image_embed_types.dart';
|
||||
import '../quill_simple_toolbar_api.dart';
|
||||
import 'config/image_config.dart';
|
||||
import 'select_image_source.dart';
|
||||
|
||||
// ignore: invalid_use_of_internal_member
|
||||
class QuillToolbarImageButton extends QuillToolbarBaseButtonStateless {
|
||||
const QuillToolbarImageButton({
|
||||
required super.controller,
|
||||
QuillToolbarImageButtonOptions? options,
|
||||
|
||||
/// Shares common options between all buttons, prefer the [options]
|
||||
/// over the [baseOptions].
|
||||
super.baseOptions,
|
||||
super.key,
|
||||
}) : _options = options,
|
||||
super(options: options);
|
||||
|
||||
final QuillToolbarImageButtonOptions? _options;
|
||||
|
||||
@override
|
||||
QuillToolbarImageButtonOptions? get options => _options;
|
||||
|
||||
void _sharedOnPressed(BuildContext context) {
|
||||
_onPressedHandler(context);
|
||||
afterButtonPressed(context);
|
||||
}
|
||||
|
||||
Future<void> _handleImageInsert(String imageUrl) async {
|
||||
await handleImageInsert(
|
||||
imageUrl,
|
||||
controller: controller,
|
||||
onImageInsertCallback: options?.imageButtonConfig?.onImageInsertCallback,
|
||||
onImageInsertedCallback:
|
||||
options?.imageButtonConfig?.onImageInsertedCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
final onRequestPickImage = options?.imageButtonConfig?.onRequestPickImage;
|
||||
if (onRequestPickImage != null) {
|
||||
final imageUrl = await onRequestPickImage(
|
||||
context,
|
||||
);
|
||||
if (imageUrl != null) {
|
||||
await _handleImageInsert(imageUrl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final source = await showSelectImageSourceDialog(
|
||||
context: context,
|
||||
);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final imageUrl = switch (source) {
|
||||
InsertImageSource.gallery =>
|
||||
(await ImagePicker().pickImage(source: ImageSource.gallery))?.path,
|
||||
InsertImageSource.link =>
|
||||
context.mounted ? await _typeLink(context) : null,
|
||||
InsertImageSource.camera =>
|
||||
(await ImagePicker().pickImage(source: ImageSource.camera))?.path,
|
||||
};
|
||||
if (imageUrl == null) {
|
||||
return;
|
||||
}
|
||||
if (imageUrl.trim().isNotEmpty) {
|
||||
await _handleImageInsert(imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _typeLink(BuildContext context) async {
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => TypeLinkDialog(
|
||||
dialogTheme: options?.dialogTheme,
|
||||
linkRegExp: options?.linkRegExp,
|
||||
linkType: LinkType.image,
|
||||
),
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildButton(BuildContext context) {
|
||||
return QuillToolbarIconButton(
|
||||
icon: Icon(
|
||||
iconData(context),
|
||||
size: iconButtonFactor(context) * iconSize(context),
|
||||
),
|
||||
tooltip: tooltip(context),
|
||||
isSelected: false,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
iconTheme: iconTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||
return childBuilder?.call(
|
||||
QuillToolbarImageButtonOptions(
|
||||
afterButtonPressed: afterButtonPressed(context),
|
||||
iconData: iconData(context),
|
||||
iconSize: iconSize(context),
|
||||
iconButtonFactor: iconButtonFactor(context),
|
||||
dialogTheme: options?.dialogTheme,
|
||||
iconTheme: options?.iconTheme,
|
||||
linkRegExp: options?.linkRegExp,
|
||||
tooltip: tooltip(context),
|
||||
imageButtonConfig: options?.imageButtonConfig,
|
||||
),
|
||||
QuillToolbarImageButtonExtraOptions(
|
||||
context: context,
|
||||
controller: controller,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||
(context) => Icons.image;
|
||||
|
||||
@override
|
||||
String Function(BuildContext context) get getDefaultTooltip =>
|
||||
(context) => context.loc.insertImage;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import '../../editor/image/image_embed_types.dart';
|
||||
|
||||
class SelectImageSourceDialog extends StatelessWidget {
|
||||
const SelectImageSourceDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 200),
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.loc.gallery),
|
||||
subtitle: Text(
|
||||
context.loc.pickAPhotoFromYourGallery,
|
||||
),
|
||||
leading: const Icon(Icons.photo_sharp),
|
||||
onTap: () => Navigator.of(context).pop(InsertImageSource.gallery),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.camera),
|
||||
subtitle: Text(
|
||||
context.loc.takeAPhotoUsingYourCamera,
|
||||
),
|
||||
leading: const Icon(Icons.camera),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(InsertImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.link),
|
||||
subtitle: Text(
|
||||
context.loc.pasteAPhotoUsingALink,
|
||||
),
|
||||
leading: const Icon(Icons.link),
|
||||
onTap: () => Navigator.of(context).pop(InsertImageSource.link),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<InsertImageSource?> showSelectImageSourceDialog({
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final imageSource = await showModalBottomSheet<InsertImageSource>(
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
builder: (_) => const SelectImageSourceDialog(),
|
||||
);
|
||||
return imageSource;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
/// APIs that are meant to be used by the `flutter_quil_extensions` only.
|
||||
///
|
||||
/// Breaking changes can be introduced from `flutter_quill` in minor versions,
|
||||
/// the `flutter_quill_extensions` will be updated and published at the same time.
|
||||
///
|
||||
/// Update both packages and use the same version for compatibility by running `flutter pub upgrade`.
|
||||
@internal
|
||||
library;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
export 'package:flutter_quill/src/toolbar/base_button/stateless_base_button.dart';
|
||||
@ -1,50 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BuildContext;
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
/// When request picking an video, for example when the video button toolbar
|
||||
/// clicked, it should be null in case the user didn't choose any video or
|
||||
/// any other reasons, and it should be the video file path as string that is
|
||||
/// exists in case the user picked the video successfully
|
||||
///
|
||||
/// by default we already have a default implementation that show a dialog
|
||||
/// request the source for picking the video, from gallery, link or camera
|
||||
typedef OnRequestPickVideo = Future<String?> Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
/// A callback will called when inserting a video in the editor
|
||||
/// it have the logic that will insert the video block using the controller
|
||||
typedef OnVideoInsertCallback = Future<void> Function(
|
||||
String video,
|
||||
QuillController controller,
|
||||
);
|
||||
|
||||
/// When a new video picked this callback will called and you might want to
|
||||
/// do some logic depending on your use case
|
||||
typedef OnVideoInsertedCallback = Future<void> Function(
|
||||
String video,
|
||||
);
|
||||
|
||||
enum InsertVideoSource {
|
||||
gallery,
|
||||
camera,
|
||||
link,
|
||||
}
|
||||
|
||||
/// Configurations for dealing with videos, on insert a video
|
||||
/// on request picking a video
|
||||
@immutable
|
||||
class QuillToolbarVideoConfig {
|
||||
const QuillToolbarVideoConfig({
|
||||
this.onRequestPickVideo,
|
||||
this.onVideoInsertedCallback,
|
||||
this.onVideoInsertCallback,
|
||||
});
|
||||
|
||||
final OnRequestPickVideo? onRequestPickVideo;
|
||||
|
||||
final OnVideoInsertedCallback? onVideoInsertedCallback;
|
||||
|
||||
final OnVideoInsertCallback? onVideoInsertCallback;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,32 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import 'video.dart';
|
||||
|
||||
class QuillToolbarVideoButtonExtraOptions
|
||||
extends QuillToolbarBaseButtonExtraOptions {
|
||||
const QuillToolbarVideoButtonExtraOptions({
|
||||
required super.controller,
|
||||
required super.context,
|
||||
required super.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||
QuillToolbarVideoButtonOptions, QuillToolbarVideoButtonExtraOptions> {
|
||||
const QuillToolbarVideoButtonOptions({
|
||||
this.linkRegExp,
|
||||
this.dialogTheme,
|
||||
super.iconSize,
|
||||
super.iconButtonFactor,
|
||||
super.iconData,
|
||||
super.afterButtonPressed,
|
||||
super.tooltip,
|
||||
super.iconTheme,
|
||||
super.childBuilder,
|
||||
this.videoConfig,
|
||||
});
|
||||
|
||||
final RegExp? linkRegExp;
|
||||
final QuillDialogTheme? dialogTheme;
|
||||
final QuillToolbarVideoConfig? videoConfig;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'config/video.dart';
|
||||
|
||||
class SelectVideoSourceDialog extends StatelessWidget {
|
||||
const SelectVideoSourceDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 200),
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.loc.gallery),
|
||||
subtitle: Text(
|
||||
context.loc.pickAVideoFromYourGallery,
|
||||
),
|
||||
leading: const Icon(Icons.photo_sharp),
|
||||
onTap: () => Navigator.of(context).pop(InsertVideoSource.gallery),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.camera),
|
||||
subtitle: Text(context.loc.recordAVideoUsingYourCamera),
|
||||
leading: const Icon(Icons.camera),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(InsertVideoSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.link),
|
||||
subtitle: Text(
|
||||
context.loc.pasteAVideoUsingALink,
|
||||
),
|
||||
leading: const Icon(Icons.link),
|
||||
onTap: () => Navigator.of(context).pop(InsertVideoSource.link),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<InsertVideoSource?> showSelectVideoSourceDialog({
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final imageSource = await showModalBottomSheet<InsertVideoSource>(
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
builder: (context) => const SelectVideoSourceDialog(),
|
||||
);
|
||||
return imageSource;
|
||||
}
|
||||
@ -1,134 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../common/default_video_insert.dart';
|
||||
import '../../common/image_video_utils.dart';
|
||||
import '../quill_simple_toolbar_api.dart';
|
||||
|
||||
import 'config/video.dart';
|
||||
import 'config/video_config.dart';
|
||||
import 'select_video_source.dart';
|
||||
|
||||
// ignore: invalid_use_of_internal_member
|
||||
class QuillToolbarVideoButton extends QuillToolbarBaseButtonStateless {
|
||||
const QuillToolbarVideoButton({
|
||||
required super.controller,
|
||||
QuillToolbarVideoButtonOptions? options,
|
||||
|
||||
/// Shares common options between all buttons, prefer the [options]
|
||||
/// over the [baseOptions].
|
||||
super.baseOptions,
|
||||
super.key,
|
||||
}) : _options = options,
|
||||
super(options: options);
|
||||
|
||||
final QuillToolbarVideoButtonOptions? _options;
|
||||
|
||||
@override
|
||||
QuillToolbarVideoButtonOptions? get options => _options;
|
||||
|
||||
void _sharedOnPressed(BuildContext context) {
|
||||
_onPressedHandler(context);
|
||||
afterButtonPressed(context);
|
||||
}
|
||||
|
||||
Future<void> _handleVideoInsert(String videoUrl) async {
|
||||
await handleVideoInsert(
|
||||
videoUrl,
|
||||
controller: controller,
|
||||
onVideoInsertCallback: options?.videoConfig?.onVideoInsertCallback,
|
||||
onVideoInsertedCallback: options?.videoConfig?.onVideoInsertedCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
final onRequestPickVideo = options?.videoConfig?.onRequestPickVideo;
|
||||
if (onRequestPickVideo != null) {
|
||||
final videoUrl = await onRequestPickVideo(context);
|
||||
if (videoUrl != null) {
|
||||
await _handleVideoInsert(videoUrl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final imageSource = await showSelectVideoSourceDialog(context: context);
|
||||
|
||||
if (imageSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final videoUrl = switch (imageSource) {
|
||||
InsertVideoSource.gallery =>
|
||||
(await ImagePicker().pickVideo(source: ImageSource.gallery))?.path,
|
||||
InsertVideoSource.camera =>
|
||||
(await ImagePicker().pickVideo(source: ImageSource.camera))?.path,
|
||||
InsertVideoSource.link =>
|
||||
context.mounted ? await _typeLink(context) : null,
|
||||
};
|
||||
if (videoUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoUrl.trim().isNotEmpty) {
|
||||
_handleVideoInsert(videoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _typeLink(BuildContext context) async {
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => TypeLinkDialog(
|
||||
dialogTheme: options?.dialogTheme,
|
||||
linkType: LinkType.video,
|
||||
),
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildButton(BuildContext context) {
|
||||
return QuillToolbarIconButton(
|
||||
icon: Icon(
|
||||
iconData(context),
|
||||
size: iconSize(context) * iconButtonFactor(context),
|
||||
),
|
||||
tooltip: tooltip(context),
|
||||
isSelected: false,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
iconTheme: iconTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||
return childBuilder?.call(
|
||||
QuillToolbarVideoButtonOptions(
|
||||
afterButtonPressed: afterButtonPressed(context),
|
||||
iconData: iconData(context),
|
||||
dialogTheme: options?.dialogTheme,
|
||||
iconSize: iconSize(context),
|
||||
iconButtonFactor: iconButtonFactor(context),
|
||||
linkRegExp: options?.linkRegExp,
|
||||
tooltip: tooltip(context),
|
||||
iconTheme: options?.iconTheme,
|
||||
videoConfig: options?.videoConfig,
|
||||
),
|
||||
QuillToolbarVideoButtonExtraOptions(
|
||||
context: context,
|
||||
controller: controller,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||
(context) => Icons.movie_creation;
|
||||
|
||||
@override
|
||||
String Function(BuildContext context) get getDefaultTooltip =>
|
||||
(context) => context.loc.insertVideo;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,244 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/quill_delta_sample.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const Template({super.key, required this.templateData});
|
||||
|
||||
static Template fromState(GoRouterState state) {
|
||||
return Template(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateState createState() => TemplateState();
|
||||
}
|
||||
|
||||
class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
// "org_id": orgId;
|
||||
"template_name": controllers["templateName"]?.text,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": controllers["bodyData"]?.text,
|
||||
"placeholder": [],
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}");
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
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) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteSubject(isDesktop),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.output),
|
||||
tooltip: 'Print Delta JSON to log',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'The JSON Delta has been printed to the console.')));
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteBody(isDesktop)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle:
|
||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,19 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:delta_to_html/delta_to_html.dart';
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart' as quill;
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element, Text;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -11,18 +21,20 @@ import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/quill_delta_sample.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
import 'dialog_placeholders.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
@ -41,21 +53,44 @@ class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
// "org_id": orgId;
|
||||
"template_name": controllers["templateName"]?.text,
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": controllers["bodyData"]?.text,
|
||||
"placeholder": [],
|
||||
"body_html": DeltaToHTML.encodeJson(
|
||||
_controller.document.toDelta().toJson(),
|
||||
),
|
||||
// "body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
print('start 123');
|
||||
print(jsonEncode(_controller.document.toDelta().toJson()));
|
||||
// print(jsonEncode(_controller.document));
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
@ -73,7 +108,7 @@ class TemplateState extends State<Template> {
|
||||
}
|
||||
|
||||
updateData();
|
||||
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@ -84,32 +119,255 @@ class TemplateState extends State<Template> {
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
print("convertQuillDocToHtml");
|
||||
for (final op in doc.toDelta().toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes ?? {};
|
||||
|
||||
if (insert is String) {
|
||||
var content = insert;
|
||||
|
||||
// Handle formatting (bold, italic, etc.)
|
||||
if (attrs.containsKey('bold')) {
|
||||
content = '<strong>$content</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
content = '<em>$content</em>';
|
||||
}
|
||||
|
||||
// Wrap each paragraph with <p>
|
||||
if (content.trim().isNotEmpty) {
|
||||
buffer.write('<p>${content.trim()}</p>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml2(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
final lines = <String>[];
|
||||
final delta = doc.toDelta();
|
||||
|
||||
String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
if (attrs == null) return text;
|
||||
if (attrs.containsKey('bold')) {
|
||||
text = '<strong>$text</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
text = '<em>$text</em>';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
for (final op in delta.toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes;
|
||||
|
||||
if (insert is String) {
|
||||
final parts = insert.split('\n');
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
final part = applyStyles(parts[i], attrs);
|
||||
lines.add(part);
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
// End of line: wrap accumulated content into <p>
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
lines.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining lines
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String extractPlainTextFromHtml(String html) {
|
||||
final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
final matches = regex.allMatches(html);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
for (final match in matches) {
|
||||
final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
buffer.writeln(text.trim());
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String decodeHtmlEntities(String text) {
|
||||
return text
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'"); // add more as needed
|
||||
}
|
||||
|
||||
// quill.Document convertSimpleHtmlToQuill(String htmlString) {
|
||||
// final delta = quill.Delta();
|
||||
// final doc = html_parser.parse(htmlString);
|
||||
// final body = doc.body;
|
||||
//
|
||||
// void walk(Node node) {
|
||||
// if (node is Text) {
|
||||
// delta.insert(node.text);
|
||||
// } else if (node is Element) {
|
||||
// switch (node.localName) {
|
||||
// case 'p':
|
||||
// node.nodes.forEach(walk);
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'br':
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'strong':
|
||||
// case 'b':
|
||||
// delta.insert(node.text, {'bold': true});
|
||||
// break;
|
||||
// case 'em':
|
||||
// case 'i':
|
||||
// delta.insert(node.text, {'italic': true});
|
||||
// break;
|
||||
// case 'a':
|
||||
// delta.insert(node.text, {'link': node.attributes['href']});
|
||||
// break;
|
||||
// default:
|
||||
// node.nodes.forEach(walk);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (body != null) {
|
||||
// walk(body);
|
||||
// }
|
||||
//
|
||||
// return quill.Document.fromDelta(delta..insert('\n'));
|
||||
// }
|
||||
|
||||
String formatTemplateName(String input) {
|
||||
return input
|
||||
.split('_') // split by underscore
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}");
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
String html = widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
final htmlToDelta = HtmlToDelta();
|
||||
|
||||
// Convert the HTML string to Quill Delta format
|
||||
// This is where the magic happens, but also where complex HTML might be simplified
|
||||
final quill.Delta initialDelta = htmlToDelta.convert(html);
|
||||
|
||||
// Create a Quill Document from the Delta
|
||||
final quill.Document quillDoc = quill.Document.fromDelta(initialDelta);
|
||||
|
||||
// Initialize the QuillController with the converted document
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
), // Cursor at the start
|
||||
);
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
@ -121,21 +379,92 @@ class TemplateState extends State<Template> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
|
||||
print('TemplateData - $data');
|
||||
// final String deltaJsonString = TemplateData?["body_html"] ?? "[]";
|
||||
//
|
||||
// print('deltaJsonString $deltaJsonString');
|
||||
//
|
||||
// // Decode the JSON string into a list
|
||||
// final List<dynamic> deltaJson = jsonDecode(deltaJsonString);
|
||||
//
|
||||
// print(DeltaToHTML.encodeJson(deltaJson));
|
||||
//
|
||||
// // Then convert it to a Quill document
|
||||
// final doc = Document.fromJson(List<Map<String, dynamic>>.from(deltaJson));
|
||||
//
|
||||
// // Set it to the controller
|
||||
// _controller = QuillController(
|
||||
// document: doc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
// print('doc JSON: ${jsonEncode(doc.toDelta().toJson())}');
|
||||
// print('doc plain text: ${doc.toPlainText()}');
|
||||
// print('doc $doc');
|
||||
|
||||
setState(() {
|
||||
updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
@ -144,19 +473,29 @@ class TemplateState extends State<Template> {
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop, context, bodyColor, layoutColor)),
|
||||
isDesktop,
|
||||
context,
|
||||
bodyColor,
|
||||
layoutColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop, context, Color? bodyColor, Color layoutColor) {
|
||||
bool isDesktop,
|
||||
context,
|
||||
Color? bodyColor,
|
||||
Color layoutColor,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 5.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
@ -165,24 +504,22 @@ class TemplateState extends State<Template> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
Text(
|
||||
formatTemplateName(templateName),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteSubject(isDesktop),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.output),
|
||||
tooltip: 'Print Delta JSON to log',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'The JSON Delta has been printed to the console.')));
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteBody(isDesktop)
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -196,11 +533,16 @@ class TemplateState extends State<Template> {
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
color: Colors.white,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
@ -211,9 +553,11 @@ class TemplateState extends State<Template> {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle:
|
||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
||||
labelText: "Enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
@ -233,11 +577,138 @@ class TemplateState extends State<Template> {
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
color: Color(0xFFFFFEF0),
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
child: IconTheme(
|
||||
data: IconThemeData(size: 18), // Set icon size here
|
||||
child: QuillSimpleToolbar(controller: _controller),
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final selected = await showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) =>
|
||||
PlaceholdersModal(placeholders: placeholderList),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
print("User selected placeholder: $selected");
|
||||
// You can now insert into a controller or editor
|
||||
// Ensure the editor is focused
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
|
||||
final selection = _controller.selection;
|
||||
final position = selection.baseOffset;
|
||||
|
||||
// if (position >= 0) {
|
||||
// final intPosition = position.toInt();
|
||||
//
|
||||
// _controller.document.insert(intPosition, selected);
|
||||
//
|
||||
// _controller.updateSelection(
|
||||
// TextSelection.collapsed(
|
||||
// offset: intPosition + selected.length,
|
||||
// ),
|
||||
// ChangeSource.local,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.grey,
|
||||
// padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Placeholders',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: MediaQuery.of(context).size.height * 0.3,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Color(0xFFD6D5E6), width: 0.5),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/templateList');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: layoutColor, width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: layoutColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
830
lib/Screens/myTemplates/templateForex.dart
Normal file
830
lib/Screens/myTemplates/templateForex.dart
Normal file
@ -0,0 +1,830 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:delta_to_html/delta_to_html.dart';
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart' as quill;
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/templateForex.dart'
|
||||
as _editorFocusNode;
|
||||
import 'package:frontend/Screens/myTemplates/templateForex.dart'
|
||||
as _editorScrollController;
|
||||
import 'package:frontend/Screens/myTemplates/templateForex.dart' as _controller;
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element, Text;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
import 'dialog_placeholders.dart';
|
||||
|
||||
class TemplateForex extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const TemplateForex({super.key, required this.templateData});
|
||||
|
||||
static TemplateForex fromState(GoRouterState state) {
|
||||
return TemplateForex(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateForexState createState() => TemplateForexState();
|
||||
}
|
||||
|
||||
class TemplateForexState extends State<TemplateForex> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": DeltaToHTML.encodeJson(
|
||||
_controller.document.toDelta().toJson(),
|
||||
),
|
||||
// "body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
print('start 123');
|
||||
print(jsonEncode(_controller.document.toDelta().toJson()));
|
||||
// print(jsonEncode(_controller.document));
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
print("convertQuillDocToHtml");
|
||||
for (final op in doc.toDelta().toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes ?? {};
|
||||
|
||||
if (insert is String) {
|
||||
var content = insert;
|
||||
|
||||
// Handle formatting (bold, italic, etc.)
|
||||
if (attrs.containsKey('bold')) {
|
||||
content = '<strong>$content</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
content = '<em>$content</em>';
|
||||
}
|
||||
|
||||
// Wrap each paragraph with <p>
|
||||
if (content.trim().isNotEmpty) {
|
||||
buffer.write('<p>${content.trim()}</p>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml2(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
final lines = <String>[];
|
||||
final delta = doc.toDelta();
|
||||
|
||||
String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
if (attrs == null) return text;
|
||||
if (attrs.containsKey('bold')) {
|
||||
text = '<strong>$text</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
text = '<em>$text</em>';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
for (final op in delta.toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes;
|
||||
|
||||
if (insert is String) {
|
||||
final parts = insert.split('\n');
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
final part = applyStyles(parts[i], attrs);
|
||||
lines.add(part);
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
// End of line: wrap accumulated content into <p>
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
lines.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining lines
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String extractPlainTextFromHtml(String html) {
|
||||
final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
final matches = regex.allMatches(html);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
for (final match in matches) {
|
||||
final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
buffer.writeln(text.trim());
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String decodeHtmlEntities(String text) {
|
||||
return text
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'"); // add more as needed
|
||||
}
|
||||
|
||||
// quill.Document convertSimpleHtmlToQuill(String htmlString) {
|
||||
// final delta = quill.Delta();
|
||||
// final doc = html_parser.parse(htmlString);
|
||||
// final body = doc.body;
|
||||
//
|
||||
// void walk(Node node) {
|
||||
// if (node is Text) {
|
||||
// delta.insert(node.text);
|
||||
// } else if (node is Element) {
|
||||
// switch (node.localName) {
|
||||
// case 'p':
|
||||
// node.nodes.forEach(walk);
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'br':
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'strong':
|
||||
// case 'b':
|
||||
// delta.insert(node.text, {'bold': true});
|
||||
// break;
|
||||
// case 'em':
|
||||
// case 'i':
|
||||
// delta.insert(node.text, {'italic': true});
|
||||
// break;
|
||||
// case 'a':
|
||||
// delta.insert(node.text, {'link': node.attributes['href']});
|
||||
// break;
|
||||
// default:
|
||||
// node.nodes.forEach(walk);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (body != null) {
|
||||
// walk(body);
|
||||
// }
|
||||
//
|
||||
// return quill.Document.fromDelta(delta..insert('\n'));
|
||||
// }
|
||||
|
||||
String formatTemplateName(String input) {
|
||||
return input
|
||||
.split('_') // split by underscore
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
String html = widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
final htmlToDelta = HtmlToDelta();
|
||||
|
||||
// Convert the HTML string to Quill Delta format
|
||||
// This is where the magic happens, but also where complex HTML might be simplified
|
||||
final quill.Delta initialDelta = htmlToDelta.convert(html);
|
||||
|
||||
// Create a Quill Document from the Delta
|
||||
final quill.Document quillDoc = quill.Document.fromDelta(initialDelta);
|
||||
|
||||
// Initialize the QuillController with the converted document
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
), // Cursor at the start
|
||||
);
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
|
||||
print('TemplateData - $data');
|
||||
// final String deltaJsonString = TemplateData?["body_html"] ?? "[]";
|
||||
//
|
||||
// print('deltaJsonString $deltaJsonString');
|
||||
//
|
||||
// // Decode the JSON string into a list
|
||||
// final List<dynamic> deltaJson = jsonDecode(deltaJsonString);
|
||||
//
|
||||
// print(DeltaToHTML.encodeJson(deltaJson));
|
||||
//
|
||||
// // Then convert it to a Quill document
|
||||
// final doc = Document.fromJson(List<Map<String, dynamic>>.from(deltaJson));
|
||||
//
|
||||
// // Set it to the controller
|
||||
// _controller = QuillController(
|
||||
// document: doc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
// print('doc JSON: ${jsonEncode(doc.toDelta().toJson())}');
|
||||
// print('doc plain text: ${doc.toPlainText()}');
|
||||
// print('doc $doc');
|
||||
|
||||
setState(() {
|
||||
updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
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,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 5.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
formatTemplateName(templateName),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
|
||||
// SizedBox(height: 10),
|
||||
// buildTempalteSubject(isDesktop),
|
||||
//
|
||||
// SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
color: Colors.white,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "Enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Text(
|
||||
// "Content",
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: Color(0xFF575A74),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
color: Color(0xFFFFFEF0),
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
child: IconTheme(
|
||||
data: IconThemeData(size: 18), // Set icon size here
|
||||
child: QuillSimpleToolbar(
|
||||
controller: _controller,
|
||||
config: QuillSimpleToolbarConfig(
|
||||
embedButtons: FlutterQuillEmbeds.toolbarButtons(),
|
||||
showClipboardPaste: true,
|
||||
customButtons: [
|
||||
QuillToolbarCustomButtonOptions(
|
||||
icon: const Icon(Icons.add_alarm_rounded),
|
||||
onPressed: () {
|
||||
_controller.document.insert(
|
||||
_controller.selection.extentOffset,
|
||||
TimeStampEmbed(DateTime.now().toString()),
|
||||
);
|
||||
|
||||
_controller.updateSelection(
|
||||
TextSelection.collapsed(
|
||||
offset: _controller.selection.extentOffset + 1,
|
||||
),
|
||||
ChangeSource.local,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
buttonOptions: QuillSimpleToolbarButtonOptions(
|
||||
base: QuillToolbarBaseButtonOptions(
|
||||
afterButtonPressed: () {
|
||||
final isDesktop = {
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.macOS,
|
||||
}.contains(defaultTargetPlatform);
|
||||
// if (isDesktop) {
|
||||
// _editorFocusNode.requestFocus();
|
||||
// }
|
||||
},
|
||||
),
|
||||
linkStyle: QuillToolbarLinkStyleButtonOptions(
|
||||
validateLink: (link) {
|
||||
// Treats all links as valid. When launching the URL,
|
||||
// `https://` is prefixed if the link is incomplete (e.g., `google.com` → `https://google.com`)
|
||||
// however this happens only within the editor.
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final selected = await showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) =>
|
||||
PlaceholdersModal(placeholders: placeholderList),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
print("User selected placeholder: $selected");
|
||||
// You can now insert into a controller or editor
|
||||
// Ensure the editor is focused
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
|
||||
final selection = _controller.selection;
|
||||
final position = selection.baseOffset;
|
||||
|
||||
// if (position >= 0) {
|
||||
// final intPosition = position.toInt();
|
||||
//
|
||||
// _controller.document.insert(intPosition, selected);
|
||||
//
|
||||
// _controller.updateSelection(
|
||||
// TextSelection.collapsed(
|
||||
// offset: intPosition + selected.length,
|
||||
// ),
|
||||
// ChangeSource.local,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.grey,
|
||||
// padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Placeholders',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Color(0xFFD6D5E6), width: 0.5),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
|
||||
focusNode: _focusNode,
|
||||
config: QuillEditorConfig(
|
||||
placeholder: 'Start writing your notes...',
|
||||
padding: const EdgeInsets.all(16),
|
||||
embedBuilders: [
|
||||
...FlutterQuillEmbeds.editorBuilders(
|
||||
imageEmbedConfig: QuillEditorImageEmbedConfig(
|
||||
imageProviderBuilder: (context, imageUrl) {
|
||||
// https://pub.dev/packages/flutter_quill_extensions#-image-assets
|
||||
if (imageUrl.startsWith('assets/')) {
|
||||
return AssetImage(imageUrl);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
videoEmbedConfig: QuillEditorVideoEmbedConfig(
|
||||
customVideoBuilder: (videoUrl, readOnly) {
|
||||
// To load YouTube videos https://github.com/singerdmx/flutter-quill/releases/tag/v10.8.0
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
TimeStampEmbedBuilder(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/OrganizationSettings');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: layoutColor, width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: layoutColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_editorScrollController.dispose();
|
||||
_editorFocusNode.dispose();
|
||||
}
|
||||
|
||||
class TimeStampEmbed extends Embeddable {
|
||||
const TimeStampEmbed(String value) : super(timeStampType, value);
|
||||
|
||||
static const String timeStampType = 'timeStamp';
|
||||
|
||||
static TimeStampEmbed fromDocument(Document document) =>
|
||||
TimeStampEmbed(jsonEncode(document.toDelta().toJson()));
|
||||
|
||||
Document get document => Document.fromJson(jsonDecode(data));
|
||||
}
|
||||
|
||||
class TimeStampEmbedBuilder extends EmbedBuilder {
|
||||
@override
|
||||
String get key => 'timeStamp';
|
||||
|
||||
@override
|
||||
String toPlainText(Embed node) {
|
||||
return node.value.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, EmbedContext embedContext) {
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time_rounded),
|
||||
Text(embedContext.node.value.data as String),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/Screens/myTemplates/templateTest.dart
Normal file
37
lib/Screens/myTemplates/templateTest.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
MyHomePageState createState() => MyHomePageState();
|
||||
}
|
||||
|
||||
class MyHomePageState extends State<MyHomePage> {
|
||||
final QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("title")),
|
||||
body: Column(
|
||||
children: [
|
||||
QuillSimpleToolbar(controller: _controller),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
@ -66,11 +67,13 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -99,7 +102,7 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
|
||||
"created_by": null,
|
||||
"updated_by": null,
|
||||
"is_active": 1
|
||||
"is_active": 1,
|
||||
|
||||
// "org_id": orgId,
|
||||
// "created_by": userId,
|
||||
@ -144,10 +147,14 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
try {
|
||||
print("getUpdatedServices");
|
||||
|
||||
final result = await apiService.fetchOrganization();
|
||||
print("UUPdatedServices - $result");
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String? orgDataString = prefs.getString('org_data');
|
||||
|
||||
if (orgDataString != null) {
|
||||
final Map<String, dynamic> orgData = jsonDecode(orgDataString);
|
||||
print("UUPdatedServices - $orgData");
|
||||
setState(() {
|
||||
selectedOrg = result;
|
||||
selectedOrg = orgData;
|
||||
|
||||
String? rawLogoPath = selectedOrg?['logo'];
|
||||
if (rawLogoPath != null && rawLogoPath.contains('/assets')) {
|
||||
@ -158,16 +165,27 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
|
||||
_orgNameController.text = selectedOrg?['name'];
|
||||
|
||||
layoutColor = selectedOrg?['layout_color'] != null
|
||||
? Color(int.parse(
|
||||
selectedOrg!['layout_color'].toString().replaceFirst('0x', ''),
|
||||
radix: 16))
|
||||
layoutColor =
|
||||
selectedOrg?['layout_color'] != null
|
||||
? Color(
|
||||
int.parse(
|
||||
selectedOrg!['layout_color'].toString().replaceFirst(
|
||||
'0x',
|
||||
'',
|
||||
),
|
||||
radix: 16,
|
||||
),
|
||||
)
|
||||
: Colors.white;
|
||||
|
||||
bodyColor = selectedOrg?['color'] != null
|
||||
? Color(int.parse(
|
||||
bodyColor =
|
||||
selectedOrg?['color'] != null
|
||||
? Color(
|
||||
int.parse(
|
||||
selectedOrg!['color'].toString().replaceFirst('0x', ''),
|
||||
radix: 16))
|
||||
radix: 16,
|
||||
),
|
||||
)
|
||||
: Colors.blue;
|
||||
|
||||
// Set mail config fields
|
||||
@ -200,12 +218,11 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
services = [];
|
||||
}
|
||||
|
||||
selectedServiceIds = services.map<Map<String, dynamic>>((item) {
|
||||
selectedServiceIds =
|
||||
services.map<Map<String, dynamic>>((item) {
|
||||
// force cast or copy to a regular map
|
||||
final map = Map<String, dynamic>.from(item);
|
||||
return {
|
||||
"service_id": map['service_id'].toString(),
|
||||
};
|
||||
return {"service_id": map['service_id'].toString()};
|
||||
}).toList();
|
||||
});
|
||||
|
||||
@ -213,6 +230,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
|
||||
print("selectedOrg - $selectedOrg");
|
||||
print("mailConfig - $mailConfig");
|
||||
}
|
||||
|
||||
// final result = await apiService.fetchOrganization();
|
||||
} catch (e) {
|
||||
print('Error fetching updatedServices list: $e');
|
||||
}
|
||||
@ -222,10 +242,7 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
errorMessages.clear(); // Reset errors
|
||||
|
||||
// Required fields that must not be empty
|
||||
List<String> requiredFields = [
|
||||
"name",
|
||||
"description",
|
||||
];
|
||||
List<String> requiredFields = ["name", "description"];
|
||||
|
||||
// Check validation for each field
|
||||
for (String field in requiredFields) {
|
||||
@ -298,8 +315,27 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
print("✅ User submitted successfully!");
|
||||
print("📨 Response: ${response.body}");
|
||||
context.go('/listPlan');
|
||||
print("📨 Response Organizt Update: ${response.body}");
|
||||
|
||||
final data = json.decode(response.body);
|
||||
|
||||
if (!data.containsKey('data') || data['data'] is! Map) {
|
||||
throw Exception(
|
||||
"Invalid response format: 'data' field is missing or not a Map",
|
||||
);
|
||||
}
|
||||
|
||||
// Make sure each item is a Map<String, dynamic>
|
||||
final Map<String, dynamic> orgList = Map<String, dynamic>.from(
|
||||
data['data'],
|
||||
);
|
||||
|
||||
print(orgList);
|
||||
await updateOrgDataWithNewValues(orgList);
|
||||
print("📨 Response Organizt Update:");
|
||||
// return orgList;
|
||||
context.go('/OrganizationSettings');
|
||||
// context.go('/listPlan');
|
||||
} else {
|
||||
print("❌ Submission failed. Status: ${response.statusCode}");
|
||||
print("📨 Body: ${response.body}");
|
||||
@ -309,6 +345,39 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateOrgDataWithNewValues(Map<String, dynamic> newData) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String? orgDataString = prefs.getString('org_data');
|
||||
|
||||
Map<String, dynamic> orgData = {};
|
||||
if (orgDataString != null) {
|
||||
try {
|
||||
orgData = jsonDecode(orgDataString);
|
||||
|
||||
layoutColor =
|
||||
orgData['layout_color'] != null
|
||||
? Color(
|
||||
int.parse(
|
||||
orgData['layout_color'].toString().replaceFirst('0x', ''),
|
||||
radix: 16,
|
||||
),
|
||||
)
|
||||
: Colors.white;
|
||||
} catch (e) {
|
||||
print('❌ Failed to decode org_data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Merge in the new data
|
||||
orgData.addAll(newData);
|
||||
|
||||
// Save back
|
||||
await prefs.setString('org_data', jsonEncode(orgData));
|
||||
await prefs.setString('layout_color', orgData['layout_color']);
|
||||
|
||||
print("✅ Updated org_data saved.");
|
||||
}
|
||||
|
||||
void handleSubmit() {
|
||||
print("HandleSubmiy - $orgData");
|
||||
createOrgData(orgData);
|
||||
@ -327,8 +396,10 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
// backgroundColor: Colors.white,
|
||||
@ -336,23 +407,27 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
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: buildOrganizationLayout(isDesktop))
|
||||
Expanded(child: buildOrganizationLayout(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildOrganizationLayout(isDesktop) {
|
||||
@ -382,28 +457,49 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
color: Colors.white,
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
// children: [Text("Button")],
|
||||
children:
|
||||
_buildSubmit(isDesktop, isViewMode, layoutColor),
|
||||
children: _buildSubmit(
|
||||
isDesktop,
|
||||
isViewMode,
|
||||
layoutColor,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children:
|
||||
_buildSubmit(isDesktop, isViewMode, layoutColor),
|
||||
))
|
||||
children: _buildSubmit(
|
||||
isDesktop,
|
||||
isViewMode,
|
||||
layoutColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildOrgLayout(bool isDesktop) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
final double responsiveLogoWidth =
|
||||
screenWidth * 0.15; // 15% of screen width
|
||||
final double responsiveLogoHeight =
|
||||
screenHeight * 0.07; // 7% of screen height
|
||||
|
||||
final double largeResponsiveLogoWidth =
|
||||
screenWidth * 0.6; // 60% of screen width
|
||||
final double largeResponsiveLogoHeight = screenHeight * 0.15;
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final picker = ImagePicker();
|
||||
final XFile? pickedFile =
|
||||
await picker.pickImage(source: ImageSource.gallery);
|
||||
final XFile? pickedFile = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
|
||||
if (pickedFile != null && kIsWeb) {
|
||||
try {
|
||||
@ -424,9 +520,11 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
// margin: isDesktop
|
||||
// ? EdgeInsets.all(10.0)
|
||||
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
|
||||
height: isDesktop
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
|
||||
// decoration: BoxDecoration(
|
||||
// border: isDesktop
|
||||
// ? Border.all(
|
||||
@ -446,7 +544,11 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20, right: 20, bottom: 20, top: 5),
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
top: 5,
|
||||
),
|
||||
// height: MediaQuery.of(context).size.height * 0.8,
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
@ -463,7 +565,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
? "Update Organization"
|
||||
: "Create Organization",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 15, fontWeight: FontWeight.w500),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -472,7 +576,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // now -> .center , old -> .start
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.center, // now -> .center , old -> .start
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 1.0),
|
||||
@ -481,7 +587,8 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121)),
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
@ -496,7 +603,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
decoration: InputDecoration(
|
||||
hintText: "Enter Organization Name",
|
||||
hintStyle: GoogleFonts.poppins(
|
||||
fontSize: 14, color: Colors.grey),
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior:
|
||||
FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
@ -510,25 +619,35 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: _imageBytes != null
|
||||
|
||||
child:
|
||||
_imageBytes != null
|
||||
? ClipOval(
|
||||
child: Image.memory(
|
||||
_imageBytes!,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.cover,
|
||||
// width: 50,
|
||||
// height: 50,
|
||||
width:
|
||||
responsiveLogoWidth, // Use responsive width
|
||||
height: responsiveLogoHeight,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
: selectedOrg?['logo'] != null
|
||||
? ClipRect(
|
||||
child: Image.network(
|
||||
selectedOrg!['logo'],
|
||||
width: 250, // increased
|
||||
height: 75, // increased
|
||||
|
||||
width:
|
||||
responsiveLogoWidth, // Use responsive width
|
||||
height: responsiveLogoHeight,
|
||||
// width: 250,
|
||||
// height: 55,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) {
|
||||
errorBuilder: (
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
) {
|
||||
return const CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Colors.redAccent,
|
||||
@ -547,30 +666,33 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
|
||||
Text(
|
||||
"Services",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121)),
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Color(0xFFF4F4FB)),
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
// color: bodyColor,
|
||||
// color: Color(0xFFF5F5F5),
|
||||
color: Colors.white),
|
||||
padding:
|
||||
EdgeInsets.only(left: 5, right: 5, top: 15, bottom: 5),
|
||||
child: isDesktop
|
||||
color: Colors.white,
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
left: 5,
|
||||
right: 5,
|
||||
top: 15,
|
||||
bottom: 5,
|
||||
),
|
||||
child:
|
||||
isDesktop
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
@ -579,15 +701,11 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
: Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: _buildOptions(),
|
||||
child: Row(children: _buildOptions()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -597,7 +715,8 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121)),
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
@ -606,18 +725,26 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
// color: Color(0xFFF4F4FB),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
left: 5, right: 5, top: 15, bottom: 5),
|
||||
child: layoutColor != null && bodyColor != null
|
||||
left: 5,
|
||||
right: 5,
|
||||
top: 15,
|
||||
bottom: 5,
|
||||
),
|
||||
child:
|
||||
layoutColor != null && bodyColor != null
|
||||
? ColorThemePickerWidget(
|
||||
initialLayoutColor: layoutColor,
|
||||
initialBodyColor: bodyColor,
|
||||
onLayoutColorSelected:
|
||||
(Color selectedLayoutColor) {
|
||||
onLayoutColorSelected: (
|
||||
Color selectedLayoutColor,
|
||||
) {
|
||||
setState(() {
|
||||
layoutColor = selectedLayoutColor;
|
||||
});
|
||||
},
|
||||
onBodyColorSelected: (Color selectedBodyColor) {
|
||||
onBodyColorSelected: (
|
||||
Color selectedBodyColor,
|
||||
) {
|
||||
setState(() {
|
||||
bodyColor = selectedBodyColor;
|
||||
});
|
||||
@ -628,9 +755,7 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
@ -643,7 +768,8 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121)),
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
|
||||
// GestureDetector(
|
||||
@ -660,11 +786,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
// ),
|
||||
],
|
||||
),
|
||||
// if (showMail)
|
||||
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
// if (showMail)
|
||||
SizedBox(height: 10),
|
||||
Container(
|
||||
// width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
@ -678,7 +802,8 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
// color: Color(0xFFF5F5F5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: isDesktop
|
||||
mainAxisAlignment:
|
||||
isDesktop
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -688,17 +813,18 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
initialMailData: mailConfig,
|
||||
onMailDataChanged: (updatedData) {
|
||||
// You can setState here or do something else with updatedData
|
||||
print(
|
||||
"Updated Mail Data: $updatedData");
|
||||
print("Updated Mail Data: $updatedData");
|
||||
|
||||
mailConfig = updatedData;
|
||||
},
|
||||
)
|
||||
: CircularProgressIndicator(),
|
||||
],
|
||||
))
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
// isDesktop
|
||||
// ? Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -738,8 +864,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
|
||||
String serviceId = service['service_id'].toString();
|
||||
// bool isSelected = selectedServiceIds.contains(serviceId);
|
||||
bool isSelected =
|
||||
selectedServiceIds.any((item) => item["service_id"] == serviceId);
|
||||
bool isSelected = selectedServiceIds.any(
|
||||
(item) => item["service_id"] == serviceId,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@ -747,8 +874,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
String serviceId = service['service_id'].toString();
|
||||
|
||||
// Check if already selected
|
||||
int existingIndex = selectedServiceIds
|
||||
.indexWhere((item) => item["service_id"] == serviceId);
|
||||
int existingIndex = selectedServiceIds.indexWhere(
|
||||
(item) => item["service_id"] == serviceId,
|
||||
);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
selectedServiceIds.removeAt(existingIndex);
|
||||
@ -757,24 +885,30 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Row(children: [
|
||||
child: Row(
|
||||
children: [
|
||||
iconUrl.isNotEmpty
|
||||
? Image.network(
|
||||
iconUrl,
|
||||
width: 18,
|
||||
height: 18,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(fallbackIcon,
|
||||
size: 18,
|
||||
color: isSelected == name
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569));
|
||||
},
|
||||
)
|
||||
: Icon(fallbackIcon,
|
||||
return Icon(
|
||||
fallbackIcon,
|
||||
size: 18,
|
||||
color:
|
||||
isSelected == name ? Color(0xFF114D8B) : Color(0xFF475569)),
|
||||
isSelected == name
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Icon(
|
||||
fallbackIcon,
|
||||
size: 18,
|
||||
color:
|
||||
isSelected == name ? Color(0xFF114D8B) : Color(0xFF475569),
|
||||
),
|
||||
|
||||
SizedBox(width: 2),
|
||||
|
||||
@ -784,7 +918,8 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
fontSize: 12,
|
||||
color: isSelected == name ? Color(0xFF114D8B) : Color(0xFF475569),
|
||||
fontWeight:
|
||||
isSelected == name ? FontWeight.bold : FontWeight.w500),
|
||||
isSelected == name ? FontWeight.bold : FontWeight.w500,
|
||||
),
|
||||
// fontWeight: selectedListOption == title ? FontWeight.bold : FontWeight.normal,)),
|
||||
),
|
||||
|
||||
@ -796,15 +931,19 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.green : Colors.grey, width: 1),
|
||||
color: isSelected ? Colors.green : Colors.grey,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
size: 10,
|
||||
color: isSelected ? Colors.green : Colors.grey,
|
||||
// color: Colors.grey,
|
||||
)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -848,13 +987,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
onPressed: () {
|
||||
context.go('/listPlan');
|
||||
},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
)),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
child: Text("Cancel", style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
MouseRegion(
|
||||
// cursor: widget.isViewMode
|
||||
// ? SystemMouseCursors.forbidden
|
||||
@ -873,12 +1008,9 @@ class _OrgSetUpState extends State<OrgSetUp> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: handleSubmit, // Disable when in view mode
|
||||
child: Text(
|
||||
"Submit",
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
child: Text("Submit", style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,8 @@ class _ColorThemePickerWidgetState extends State<ColorThemePickerWidget> {
|
||||
children: [
|
||||
// Layout Color Picker
|
||||
GestureDetector(
|
||||
onTap: () => _showColorPickerDialog(
|
||||
onTap:
|
||||
() => _showColorPickerDialog(
|
||||
title: "Choose Layout Color",
|
||||
colors: layoutThemeColors,
|
||||
onColorSelected: (color) {
|
||||
@ -68,24 +69,26 @@ class _ColorThemePickerWidgetState extends State<ColorThemePickerWidget> {
|
||||
},
|
||||
),
|
||||
child: _buildColorBox(
|
||||
selectedLayoutColor ?? Colors.grey.shade300, Icons.palette),
|
||||
selectedLayoutColor ?? Colors.grey.shade300,
|
||||
Icons.palette,
|
||||
),
|
||||
SizedBox(width: 15),
|
||||
// Body Color Picker
|
||||
GestureDetector(
|
||||
onTap: () => _showColorPickerDialog(
|
||||
title: "Choose Body Color",
|
||||
colors: bodyThemeColors,
|
||||
onColorSelected: (color) {
|
||||
setState(() {
|
||||
selectedBodyColor = color; // low opacity
|
||||
});
|
||||
widget.onBodyColorSelected(selectedBodyColor!);
|
||||
},
|
||||
),
|
||||
child: _buildColorBox(
|
||||
selectedBodyColor ?? Colors.grey.shade300, Icons.opacity),
|
||||
),
|
||||
// SizedBox(width: 15),
|
||||
// // Body Color Picker
|
||||
// GestureDetector(
|
||||
// onTap: () => _showColorPickerDialog(
|
||||
// title: "Choose Body Color",
|
||||
// colors: bodyThemeColors,
|
||||
// onColorSelected: (color) {
|
||||
// setState(() {
|
||||
// selectedBodyColor = color; // low opacity
|
||||
// });
|
||||
// widget.onBodyColorSelected(selectedBodyColor!);
|
||||
// },
|
||||
// ),
|
||||
// child: _buildColorBox(
|
||||
// selectedBodyColor ?? Colors.grey.shade300, Icons.opacity),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -110,12 +113,14 @@ class _ColorThemePickerWidgetState extends State<ColorThemePickerWidget> {
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: colors.map((color) {
|
||||
children:
|
||||
colors.map((color) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onColorSelected(color);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -39,8 +39,8 @@ class DynamicItinerary extends StatefulWidget {
|
||||
final GlobalKey<FlightScreenState> flightScreenKey;
|
||||
final ValueNotifier<String?> tripTypeNotifier;
|
||||
|
||||
const DynamicItinerary(
|
||||
{super.key,
|
||||
const DynamicItinerary({
|
||||
super.key,
|
||||
required this.apiData,
|
||||
required this.onItineraryUpdate,
|
||||
required this.apiCountryData,
|
||||
@ -51,7 +51,8 @@ class DynamicItinerary extends StatefulWidget {
|
||||
this.tripType,
|
||||
this.apiDataForClass,
|
||||
required this.tripTypeNotifier,
|
||||
required this.flightScreenKey});
|
||||
required this.flightScreenKey,
|
||||
});
|
||||
|
||||
@override
|
||||
DynamicItineraryState createState() => DynamicItineraryState();
|
||||
@ -135,7 +136,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
if (rawServices != null && rawServices is String) {
|
||||
try {
|
||||
List<dynamic> decoded = json.decode(rawServices);
|
||||
List<Map<String, String>> formatted = decoded
|
||||
List<Map<String, String>> formatted =
|
||||
decoded
|
||||
.map((e) => {"service_id": e['service_id'].toString()})
|
||||
.toList();
|
||||
|
||||
@ -168,7 +170,7 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
"insurance",
|
||||
"visa",
|
||||
"miscellaneous",
|
||||
"taxi"
|
||||
"taxi",
|
||||
];
|
||||
} else {
|
||||
// tripType is null or not 1/2, allow everything
|
||||
@ -242,21 +244,25 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
final selectedIds =
|
||||
selectedOrgServiceIds.map((e) => e['service_id']).toSet();
|
||||
|
||||
final additionalServices = selectedAllServices!.where((service) {
|
||||
final additionalServices =
|
||||
selectedAllServices!.where((service) {
|
||||
final name = (service['name'] ?? "").toString().toLowerCase();
|
||||
final id = service['service_id'].toString();
|
||||
final isNameAllowed =
|
||||
allowedServiceNames.isEmpty || allowedServiceNames.contains(name);
|
||||
allowedServiceNames.isEmpty ||
|
||||
allowedServiceNames.contains(name);
|
||||
return filledItineraryKeys.contains(name) &&
|
||||
!selectedIds.contains(id) &&
|
||||
isNameAllowed;
|
||||
}).toList();
|
||||
|
||||
final originalFiltered = selectedAllServices!.where((service) {
|
||||
final originalFiltered =
|
||||
selectedAllServices!.where((service) {
|
||||
final name = (service['name'] ?? "").toString().toLowerCase();
|
||||
final id = service['service_id'].toString();
|
||||
final isNameAllowed =
|
||||
allowedServiceNames.isEmpty || allowedServiceNames.contains(name);
|
||||
allowedServiceNames.isEmpty ||
|
||||
allowedServiceNames.contains(name);
|
||||
return selectedIds.contains(id) && isNameAllowed;
|
||||
}).toList();
|
||||
|
||||
@ -266,21 +272,25 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
});
|
||||
|
||||
print(
|
||||
"Services chosen based on filled keys + selected: $ServicesChoosed");
|
||||
"Services chosen based on filled keys + selected: $ServicesChoosed",
|
||||
);
|
||||
} else {
|
||||
final selectedIds =
|
||||
selectedOrgServiceIds.map((e) => e['service_id']).toSet();
|
||||
|
||||
final filtered = selectedAllServices!.where((service) {
|
||||
final filtered =
|
||||
selectedAllServices!.where((service) {
|
||||
final name = (service['name'] ?? "").toString().toLowerCase();
|
||||
final isNameAllowed =
|
||||
allowedServiceNames.isEmpty || allowedServiceNames.contains(name);
|
||||
allowedServiceNames.isEmpty ||
|
||||
allowedServiceNames.contains(name);
|
||||
return selectedIds.contains(service['service_id'].toString()) &&
|
||||
isNameAllowed;
|
||||
}).toList();
|
||||
|
||||
setState(() {
|
||||
ServicesChoosed = filtered
|
||||
ServicesChoosed =
|
||||
filtered
|
||||
..sort((a, b) => (a['order'] ?? 0).compareTo(b['order'] ?? 0));
|
||||
});
|
||||
|
||||
@ -296,23 +306,32 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
setState(() {
|
||||
itineraryData = {
|
||||
"Train": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['train'] ?? []),
|
||||
widget.selectedPlanData['train'] ?? [],
|
||||
),
|
||||
"Bus": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['bus'] ?? []),
|
||||
widget.selectedPlanData['bus'] ?? [],
|
||||
),
|
||||
"Taxi": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['taxi'] ?? []),
|
||||
widget.selectedPlanData['taxi'] ?? [],
|
||||
),
|
||||
"Miscellaneous": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['miscellaneous'] ?? []),
|
||||
widget.selectedPlanData['miscellaneous'] ?? [],
|
||||
),
|
||||
"Flight": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['flight'] ?? []),
|
||||
widget.selectedPlanData['flight'] ?? [],
|
||||
),
|
||||
"Accomodation": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['accomodation'] ?? []),
|
||||
widget.selectedPlanData['accomodation'] ?? [],
|
||||
),
|
||||
"Insurance": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['insurance'] ?? []),
|
||||
widget.selectedPlanData['insurance'] ?? [],
|
||||
),
|
||||
"Visa": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['visa'] ?? []),
|
||||
widget.selectedPlanData['visa'] ?? [],
|
||||
),
|
||||
"Forex": List<Map<String, dynamic>>.from(
|
||||
widget.selectedPlanData['forex'] ?? []),
|
||||
widget.selectedPlanData['forex'] ?? [],
|
||||
),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
@ -330,7 +349,7 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
"accomodation",
|
||||
"insurance",
|
||||
"visa",
|
||||
"forex"
|
||||
"forex",
|
||||
];
|
||||
|
||||
// for (String key in keys) {
|
||||
@ -416,7 +435,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
if (existingId != null && existingId != 0) {
|
||||
// int itemId = itemList.indexWhere((item) => item["id"] == existingId);
|
||||
int itemId = itemList.indexWhere(
|
||||
(item) => item[idKey]?.toString() == existingId.toString());
|
||||
(item) => item[idKey]?.toString() == existingId.toString(),
|
||||
);
|
||||
|
||||
if (itemId != -1) {
|
||||
print(" Updating existing item with id: $existingId");
|
||||
@ -428,8 +448,9 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
|
||||
// CASE 1: Update if indx exists in list
|
||||
if (existingIndex != null && existingIndex != 0) {
|
||||
int itemIndex =
|
||||
itemList.indexWhere((item) => item["indx"] == existingIndex);
|
||||
int itemIndex = itemList.indexWhere(
|
||||
(item) => item["indx"] == existingIndex,
|
||||
);
|
||||
if (itemIndex != -1) {
|
||||
print("Updating existing item with indx: $existingIndex");
|
||||
newData["is_active"] = "1";
|
||||
@ -494,6 +515,7 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
});
|
||||
print(" onItineraryUpdate - $type - ${itineraryData[type]!} ");
|
||||
widget.onItineraryUpdate(type, itineraryData[type]!); // Notify parent
|
||||
print("ItienreayDATE - $itineraryData");
|
||||
}
|
||||
|
||||
// void handleItineraryUpdate(String type, Map<String, dynamic> newData) {
|
||||
@ -583,8 +605,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
onOpen: handleEdit,
|
||||
onAddNew: handlecreateNewPlan,
|
||||
isViewMode: widget.isViewMode,
|
||||
onDeleteAccommodation: (data) =>
|
||||
handleItinerarydelete("Accomodation", data),
|
||||
onDeleteAccommodation:
|
||||
(data) => handleItinerarydelete("Accomodation", data),
|
||||
);
|
||||
break;
|
||||
case "Miscellaneous":
|
||||
@ -594,8 +616,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
isViewMode: widget.isViewMode,
|
||||
onAddNew: handlecreateNewPlan,
|
||||
apiData: widget.apiData,
|
||||
onDeleteMiscellaneous: (data) =>
|
||||
handleItinerarydelete("Miscellaneous", data),
|
||||
onDeleteMiscellaneous:
|
||||
(data) => handleItinerarydelete("Miscellaneous", data),
|
||||
);
|
||||
break;
|
||||
case "Flight":
|
||||
@ -608,7 +630,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
onAddNew: handlecreateNewPlan,
|
||||
isViewMode: widget.isViewMode,
|
||||
apiData: widget.apiData,
|
||||
onDeleteFlight: (data) => handleItinerarydelete("Flight", data));
|
||||
onDeleteFlight: (data) => handleItinerarydelete("Flight", data),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -621,7 +644,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
loginUser: widget.loginUser,
|
||||
onSavetrain: (data) => handleItineraryUpdate("Train", data),
|
||||
tripType: widget.tripType,
|
||||
selectedItem: selectedItem);
|
||||
selectedItem: selectedItem,
|
||||
);
|
||||
break;
|
||||
case "Taxi":
|
||||
selectedWidget = TaxiScreen(
|
||||
@ -629,7 +653,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
apiData: widget.apiData,
|
||||
loginUser: widget.loginUser,
|
||||
onSavetaxi: (data) => handleItineraryUpdate("Taxi", data),
|
||||
selectedItem: selectedItem);
|
||||
selectedItem: selectedItem,
|
||||
);
|
||||
break;
|
||||
case "Bus":
|
||||
selectedWidget = BusScreen(
|
||||
@ -637,7 +662,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
apiData: widget.apiData,
|
||||
loginUser: widget.loginUser,
|
||||
onSaveBus: (data) => handleItineraryUpdate("Bus", data),
|
||||
selectedItem: selectedItem);
|
||||
selectedItem: selectedItem,
|
||||
);
|
||||
break;
|
||||
case "Insurance":
|
||||
selectedWidget = InsuranceScreen(
|
||||
@ -666,8 +692,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
onClose: handleClose,
|
||||
apiData: widget.apiData,
|
||||
loginUser: widget.loginUser,
|
||||
onSaveMiscellaneous: (data) =>
|
||||
handleItineraryUpdate("Miscellaneous", data),
|
||||
onSaveMiscellaneous:
|
||||
(data) => handleItineraryUpdate("Miscellaneous", data),
|
||||
selectedItem: selectedItem,
|
||||
selectedIndex: selectedIndex,
|
||||
);
|
||||
@ -676,8 +702,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
selectedWidget = AccomodationScreen(
|
||||
onClose: handleClose,
|
||||
loginUser: widget.loginUser,
|
||||
onSaveAccomadation: (data) =>
|
||||
handleItineraryUpdate("Accomodation", data),
|
||||
onSaveAccomadation:
|
||||
(data) => handleItineraryUpdate("Accomodation", data),
|
||||
selectedItem: selectedItem,
|
||||
flightData: itineraryData["Flight"]!,
|
||||
);
|
||||
@ -776,7 +802,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
// );
|
||||
// });
|
||||
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isMobile = sizingInfo.isMobile;
|
||||
|
||||
return Stack(
|
||||
@ -785,7 +812,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
// Second container (yellow box)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: 40), // Push it down to make room for the tab bar
|
||||
top: 40,
|
||||
), // Push it down to make room for the tab bar
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // Card background
|
||||
@ -835,12 +863,11 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: isMobile
|
||||
child:
|
||||
isMobile
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: _buildOptions(),
|
||||
),
|
||||
child: Row(children: _buildOptions()),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
@ -850,11 +877,31 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool hasValidItineraryEntries() {
|
||||
if (ServicesChoosed == null || ServicesChoosed!.isEmpty) return false;
|
||||
|
||||
for (var service in ServicesChoosed!) {
|
||||
final serviceName = service['name'];
|
||||
final entries = itineraryData[serviceName];
|
||||
|
||||
// Check if there is at least one active entry (is_active == 1)
|
||||
final hasActive =
|
||||
entries?.any((entry) => entry['is_active'] == 1) ?? false;
|
||||
|
||||
if (!hasActive) {
|
||||
return false; // Fail fast if any one service has no active entries
|
||||
}
|
||||
}
|
||||
|
||||
return true; // All selected services have at least one active entry
|
||||
}
|
||||
|
||||
List<Widget> _buildOptions() {
|
||||
if (ServicesChoosed == null) return [];
|
||||
if (ServicesChoosed == null && !hasValidItineraryEntries()) return [];
|
||||
|
||||
if (ServicesChoosed != null &&
|
||||
ServicesChoosed!.isNotEmpty &&
|
||||
@ -875,18 +922,28 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
// }
|
||||
|
||||
return ServicesChoosed!.map((service) {
|
||||
final serviceName = service['name'];
|
||||
final serviceEntries = itineraryData[serviceName];
|
||||
|
||||
final hasActive =
|
||||
serviceEntries?.any((entry) => entry['is_active'] == "1") ?? false;
|
||||
|
||||
print("Service1: $serviceName");
|
||||
print("Entries1: $serviceEntries");
|
||||
print("Has Active1: $hasActive");
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: _buildOption(
|
||||
service, itineraryData[service['name']]?.isNotEmpty ?? false),
|
||||
service,
|
||||
hasActive,
|
||||
// itineraryData[service['name']]?.isNotEmpty ?? false,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _buildOption(
|
||||
Map<String, dynamic> service,
|
||||
bool hasData,
|
||||
) {
|
||||
Widget _buildOption(Map<String, dynamic> service, bool hasData) {
|
||||
String name = service['name'];
|
||||
String iconUrl = service['icon']; // Can be empty string
|
||||
IconData fallbackIcon = _getLocalIconForService(name);
|
||||
@ -921,7 +978,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
return Icon(
|
||||
fallbackIcon,
|
||||
size: 25,
|
||||
color: isOptionSelected
|
||||
color:
|
||||
isOptionSelected
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
);
|
||||
@ -930,9 +988,8 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
: Icon(
|
||||
fallbackIcon,
|
||||
size: 25,
|
||||
color: isOptionSelected
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
color:
|
||||
isOptionSelected ? Color(0xFF114D8B) : Color(0xFF475569),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Row(
|
||||
@ -943,10 +1000,10 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
// style: GoogleFonts.poppins( fontSize: 12,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: Color(0xFF575A74))
|
||||
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isOptionSelected
|
||||
color:
|
||||
isOptionSelected
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
fontFamily: "Inter",
|
||||
@ -964,86 +1021,87 @@ class DynamicItineraryState extends State<DynamicItinerary> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOption1(
|
||||
Map<String, dynamic> service,
|
||||
bool hasData,
|
||||
) {
|
||||
String name = service['name'];
|
||||
String iconUrl = service['icon']; // Can be empty string
|
||||
// Optional: define local icon fallback if iconUrl is empty
|
||||
IconData fallbackIcon = _getLocalIconForService(name);
|
||||
// final idMap = {"service_id": service['service_id'].toString()};
|
||||
// final isSelected = selectedServiceIds.contains(idMap);
|
||||
|
||||
String serviceId = service['service_id'].toString();
|
||||
// bool isSelected = selectedServiceIds.contains(serviceId);
|
||||
// bool isSelected =
|
||||
// selectedServiceIds.any((item) => item["service_id"] == serviceId);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedListOption = name;
|
||||
isSelected = false;
|
||||
});
|
||||
},
|
||||
child: Row(children: [
|
||||
iconUrl.isNotEmpty
|
||||
? Image.network(
|
||||
iconUrl,
|
||||
width: 18,
|
||||
height: 18,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
fallbackIcon,
|
||||
size: 18,
|
||||
color: selectedListOption == name
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Icon(
|
||||
fallbackIcon,
|
||||
size: 18,
|
||||
color: selectedListOption == name
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
),
|
||||
|
||||
SizedBox(width: 2),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
// color: selectedListOption == title ? Colors.blueAccent : Color(0xFF575A74),
|
||||
color: selectedListOption == name
|
||||
? Color(0xFF114D8B)
|
||||
: Color(0xFF475569),
|
||||
fontFamily: "Archivo",
|
||||
fontWeight: selectedListOption == name
|
||||
? FontWeight.bold
|
||||
: FontWeight.w500),
|
||||
// fontWeight: selectedListOption == title ? FontWeight.bold : FontWeight.normal,)),
|
||||
),
|
||||
|
||||
SizedBox(width: 2),
|
||||
// if (selectedListOption == title && widget.isViewMode == false)
|
||||
if (hasData)
|
||||
Icon(Icons.circle, size: 8, color: Colors.green
|
||||
// color: Colors.grey,
|
||||
)
|
||||
// Container(
|
||||
// height: 10,
|
||||
// width: 10,
|
||||
// // decoration: BoxDecoration(
|
||||
// // shape: BoxShape.circle,
|
||||
// // border: Border.all(color: Colors.green, width: 1.5),
|
||||
// // ),
|
||||
// child:),
|
||||
]),
|
||||
);
|
||||
}
|
||||
// Widget _buildOption1(
|
||||
// Map<String, dynamic> service,
|
||||
// bool hasData,
|
||||
// )
|
||||
// {
|
||||
// String name = service['name'];
|
||||
// String iconUrl = service['icon']; // Can be empty string
|
||||
// // Optional: define local icon fallback if iconUrl is empty
|
||||
// IconData fallbackIcon = _getLocalIconForService(name);
|
||||
// // final idMap = {"service_id": service['service_id'].toString()};
|
||||
// // final isSelected = selectedServiceIds.contains(idMap);
|
||||
//
|
||||
// String serviceId = service['service_id'].toString();
|
||||
// // bool isSelected = selectedServiceIds.contains(serviceId);
|
||||
// // bool isSelected =
|
||||
// // selectedServiceIds.any((item) => item["service_id"] == serviceId);
|
||||
//
|
||||
// return GestureDetector(
|
||||
// onTap: () {
|
||||
// setState(() {
|
||||
// selectedListOption = name;
|
||||
// isSelected = false;
|
||||
// });
|
||||
// },
|
||||
// child: Row(children: [
|
||||
// iconUrl.isNotEmpty
|
||||
// ? Image.network(
|
||||
// iconUrl,
|
||||
// width: 18,
|
||||
// height: 18,
|
||||
// errorBuilder: (context, error, stackTrace) {
|
||||
// return Icon(
|
||||
// fallbackIcon,
|
||||
// size: 18,
|
||||
// color: selectedListOption == name
|
||||
// ? Color(0xFF114D8B)
|
||||
// : Color(0xFF475569),
|
||||
// );
|
||||
// },
|
||||
// )
|
||||
// : Icon(
|
||||
// fallbackIcon,
|
||||
// size: 18,
|
||||
// color: selectedListOption == name
|
||||
// ? Color(0xFF114D8B)
|
||||
// : Color(0xFF475569),
|
||||
// ),
|
||||
//
|
||||
// SizedBox(width: 2),
|
||||
// Text(
|
||||
// name,
|
||||
// style: TextStyle(
|
||||
// fontSize: 14,
|
||||
// // color: selectedListOption == title ? Colors.blueAccent : Color(0xFF575A74),
|
||||
// color: selectedListOption == name
|
||||
// ? Color(0xFF114D8B)
|
||||
// : Color(0xFF475569),
|
||||
// fontFamily: "Archivo",
|
||||
// fontWeight: selectedListOption == name
|
||||
// ? FontWeight.bold
|
||||
// : FontWeight.w500),
|
||||
// // fontWeight: selectedListOption == title ? FontWeight.bold : FontWeight.normal,)),
|
||||
// ),
|
||||
//
|
||||
// SizedBox(width: 2),
|
||||
// // if (selectedListOption == title && widget.isViewMode == false)
|
||||
// if (hasData)
|
||||
// Icon(Icons.circle, size: 8, color: Colors.green
|
||||
// // color: Colors.grey,
|
||||
// )
|
||||
// // Container(
|
||||
// // height: 10,
|
||||
// // width: 10,
|
||||
// // // decoration: BoxDecoration(
|
||||
// // // shape: BoxShape.circle,
|
||||
// // // border: Border.all(color: Colors.green, width: 1.5),
|
||||
// // // ),
|
||||
// // child:),
|
||||
// ]),
|
||||
// );
|
||||
// }
|
||||
|
||||
IconData _getLocalIconForService(String name) {
|
||||
switch (name.toLowerCase()) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -52,6 +52,7 @@ class _PolicyState extends State<Policy> {
|
||||
late String policyType = "domestic";
|
||||
// int? selectedServiceIndex = 1;
|
||||
ValueNotifier<String> selectedServiceIndex = ValueNotifier("1");
|
||||
ValueNotifier<String> selectedServicePriorityIndex = ValueNotifier("1");
|
||||
String selectedService = "train";
|
||||
// ValueNotifier<String> selectedService = ValueNotifier("Train");
|
||||
bool isViewMode = false;
|
||||
@ -78,14 +79,15 @@ class _PolicyState extends State<Policy> {
|
||||
List<Map<String, dynamic>>? policy_details = [];
|
||||
|
||||
Map<String, dynamic> get policyData {
|
||||
List<Map<String, dynamic>> policyDetails = policy_details!.where((service) {
|
||||
List<Map<String, dynamic>> policyDetails =
|
||||
policy_details!.where((service) {
|
||||
// Only check these specific fields for emptiness
|
||||
final fieldsToCheck = [
|
||||
'cost',
|
||||
'class',
|
||||
'a1_action',
|
||||
'a2_action',
|
||||
'a3_action'
|
||||
'a3_action',
|
||||
];
|
||||
|
||||
// If any of the important fields has a value, keep it
|
||||
@ -123,8 +125,9 @@ class _PolicyState extends State<Policy> {
|
||||
loadInitialData();
|
||||
|
||||
if (widget.policy != null) {
|
||||
final details =
|
||||
List<Map<String, dynamic>>.from(widget.policy!['policy_details']);
|
||||
final details = List<Map<String, dynamic>>.from(
|
||||
widget.policy!['policy_details'],
|
||||
);
|
||||
policyCriteriaKey.currentState?.loadPolicyDetails(details);
|
||||
|
||||
policyCriteriaKey.currentState?.fetchTrainFlightClass();
|
||||
@ -137,11 +140,13 @@ class _PolicyState extends State<Policy> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -174,7 +179,8 @@ class _PolicyState extends State<Policy> {
|
||||
if (rawServices != null && rawServices is String) {
|
||||
try {
|
||||
List<dynamic> decoded = json.decode(rawServices);
|
||||
List<Map<String, String>> formatted = decoded
|
||||
List<Map<String, String>> formatted =
|
||||
decoded
|
||||
.map((e) => {"service_id": e['service_id'].toString()})
|
||||
.toList();
|
||||
|
||||
@ -204,16 +210,21 @@ class _PolicyState extends State<Policy> {
|
||||
selectedOrgServiceIds.map((e) => e['service_id']).toSet();
|
||||
|
||||
if (widget.policy != null) {
|
||||
final details =
|
||||
List<Map<String, dynamic>>.from(widget.policy!['policy_details']);
|
||||
final details = List<Map<String, dynamic>>.from(
|
||||
widget.policy!['policy_details'],
|
||||
);
|
||||
|
||||
print(
|
||||
"UUFiltered Selected Services - ${widget.policy!['services_ids']} ");
|
||||
"UUFiltered Selected Services - ${widget.policy!['services_ids']} ",
|
||||
);
|
||||
// pr int("UUFiltered Selected Services - $details");
|
||||
|
||||
final filtered = selectedAllServices!
|
||||
.where((service) =>
|
||||
selectedIds.contains(service['service_id'].toString()))
|
||||
final filtered =
|
||||
selectedAllServices!
|
||||
.where(
|
||||
(service) =>
|
||||
selectedIds.contains(service['service_id'].toString()),
|
||||
)
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
@ -235,12 +246,15 @@ class _PolicyState extends State<Policy> {
|
||||
final decoded = jsonDecode(widget.policy!['services_ids']);
|
||||
|
||||
setState(() {
|
||||
services = List<Map<String, dynamic>>.from(decoded)
|
||||
.map((service) => {
|
||||
services =
|
||||
List<Map<String, dynamic>>.from(decoded)
|
||||
.map(
|
||||
(service) => {
|
||||
'service_id': service['service_id'].toString(),
|
||||
'name': service['name'].toString(),
|
||||
'order': service['order'].toString(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
|
||||
@ -249,9 +263,12 @@ class _PolicyState extends State<Policy> {
|
||||
|
||||
print("Filtered Selected Services Added to Policy: $ServicesChoosed");
|
||||
} else {
|
||||
final filtered = selectedAllServices!
|
||||
.where((service) =>
|
||||
selectedIds.contains(service['service_id'].toString()))
|
||||
final filtered =
|
||||
selectedAllServices!
|
||||
.where(
|
||||
(service) =>
|
||||
selectedIds.contains(service['service_id'].toString()),
|
||||
)
|
||||
.toList();
|
||||
|
||||
print("ServicesChoosedYY: $ServicesChoosed");
|
||||
@ -263,13 +280,16 @@ class _PolicyState extends State<Policy> {
|
||||
|
||||
// ServicesChoosed = filtered;
|
||||
|
||||
services = ServicesChoosed!
|
||||
.map((service) => {
|
||||
services =
|
||||
ServicesChoosed!
|
||||
.map(
|
||||
(service) => {
|
||||
'service_id': service['service_id'].toString(),
|
||||
'name':
|
||||
service['name'].toString(), // ✅ no space before 'name'
|
||||
'order': service['order'].toString(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
|
||||
@ -295,7 +315,7 @@ class _PolicyState extends State<Policy> {
|
||||
if (SelectedDomestic == "1") {
|
||||
_selectedTripType = "1";
|
||||
} else if (SelectedInternational == "1") {
|
||||
_selectedTripType = "1";
|
||||
_selectedTripType = "2";
|
||||
}
|
||||
|
||||
/// ✅ Load policy_details list safely
|
||||
@ -310,7 +330,7 @@ class _PolicyState extends State<Policy> {
|
||||
print("Services - $services");
|
||||
print("USR Detail Submit - $policyData");
|
||||
|
||||
policyCriteriaKey.currentState?.saveCurrentPolicy();
|
||||
policyCriteriaKey.currentState?.saveCurrentPolicy(services);
|
||||
|
||||
// Now the full data is ready in policyDataFromChild
|
||||
print("Submitting full policyData: $policyData");
|
||||
@ -335,7 +355,7 @@ class _PolicyState extends State<Policy> {
|
||||
|
||||
// Validate required fields
|
||||
if (data["name"] == null || data["name"].toString().trim().isEmpty) {
|
||||
errorMessages["name"] = "Policy name is required.";
|
||||
errorMessages["name"] = "Required"; // "Policy name is required.";
|
||||
}
|
||||
|
||||
// Validate that either domestic or international is selected
|
||||
@ -343,10 +363,12 @@ class _PolicyState extends State<Policy> {
|
||||
final international = data["international"]?.toString() ?? "0";
|
||||
|
||||
print(
|
||||
"domestic: ${data["domestic"]}, international: ${data["international"]}");
|
||||
"domestic: ${data["domestic"]}, international: ${data["international"]}",
|
||||
);
|
||||
|
||||
if (domestic != "1" && international != "1") {
|
||||
errorMessages["trip_type"] = "Please select Domestic or International.";
|
||||
errorMessages["trip_type"] =
|
||||
"Required"; // "Please select Domestic or International.";
|
||||
}
|
||||
|
||||
// Validate at least one policy_detail with valid content
|
||||
@ -358,7 +380,7 @@ class _PolicyState extends State<Policy> {
|
||||
'class',
|
||||
'a1_action',
|
||||
'a2_action',
|
||||
'a3_action'
|
||||
'a3_action',
|
||||
];
|
||||
return fieldsToCheck.any((field) {
|
||||
final value = service[field];
|
||||
@ -368,7 +390,7 @@ class _PolicyState extends State<Policy> {
|
||||
|
||||
if (!hasAtLeastOneDetail) {
|
||||
errorMessages["policy_details"] =
|
||||
"At least one valid policy detail is required.";
|
||||
"Required"; // "At least one valid policy detail is required.";
|
||||
}
|
||||
|
||||
return errorMessages.isEmpty;
|
||||
@ -423,19 +445,24 @@ class _PolicyState extends State<Policy> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(8),
|
||||
@ -445,7 +472,6 @@ class _PolicyState extends State<Policy> {
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
|
||||
Expanded(child: buildData(isDesktop, context)),
|
||||
// Expanded(
|
||||
// child: Container(
|
||||
@ -492,7 +518,8 @@ class _PolicyState extends State<Policy> {
|
||||
// ],
|
||||
// ),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildData(bool isDesktop, context) {
|
||||
@ -523,7 +550,8 @@ class _PolicyState extends State<Policy> {
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: _buildSubmit(isDesktop),
|
||||
@ -543,7 +571,8 @@ class _PolicyState extends State<Policy> {
|
||||
// margin: isDesktop
|
||||
// ? EdgeInsets.all(10.0)
|
||||
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
|
||||
height: isDesktop
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
// decoration: BoxDecoration(
|
||||
@ -581,7 +610,8 @@ class _PolicyState extends State<Policy> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -592,7 +622,8 @@ class _PolicyState extends State<Policy> {
|
||||
isDesktop ? SizedBox(height: 0) : SizedBox(height: 5),
|
||||
Container(
|
||||
padding: isDesktop ? const EdgeInsets.only(left: 35) : null,
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -610,18 +641,13 @@ class _PolicyState extends State<Policy> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Divider(
|
||||
thickness: 0.1,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Divider(thickness: 0.1, color: Colors.grey),
|
||||
if (errorMessages["policy_details"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["policy_details"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
style: GoogleFonts.poppins(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
isDesktop
|
||||
@ -635,15 +661,17 @@ class _PolicyState extends State<Policy> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Service Priority",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
)
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -665,11 +693,7 @@ class _PolicyState extends State<Policy> {
|
||||
_buildPolicyCategory(isDesktop),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
_buildPolicyCategoryList(isDesktop),
|
||||
],
|
||||
),
|
||||
Column(children: [_buildPolicyCategoryList(isDesktop)]),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -718,11 +742,14 @@ class _PolicyState extends State<Policy> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Policy Name",
|
||||
Text(
|
||||
"Policy Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74))),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
@ -735,8 +762,10 @@ class _PolicyState extends State<Policy> {
|
||||
onChanged: (value) => _clearError("name"),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Policy Name",
|
||||
labelStyle:
|
||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
@ -746,8 +775,10 @@ class _PolicyState extends State<Policy> {
|
||||
),
|
||||
if (errorMessages["name"] != null) ...[
|
||||
SizedBox(height: 5),
|
||||
Text(errorMessages["name"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
Text(
|
||||
errorMessages["name"]!,
|
||||
style: GoogleFonts.poppins(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
@ -757,11 +788,14 @@ class _PolicyState extends State<Policy> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Policy Type",
|
||||
Text(
|
||||
"Policy Type *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74))),
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@ -769,8 +803,10 @@ class _PolicyState extends State<Policy> {
|
||||
),
|
||||
if (errorMessages["trip_type"] != null) ...[
|
||||
SizedBox(height: 5),
|
||||
Text(errorMessages["trip_type"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
Text(
|
||||
errorMessages["trip_type"]!,
|
||||
style: GoogleFonts.poppins(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
@ -785,8 +821,10 @@ class _PolicyState extends State<Policy> {
|
||||
// color: Colors.blueGrey.shade200,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.15 : null,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.blueGrey.shade100, width: 0.35)),
|
||||
child: isDesktop
|
||||
border: Border.all(color: Colors.blueGrey.shade100, width: 0.35),
|
||||
),
|
||||
child:
|
||||
isDesktop
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
@ -876,13 +914,15 @@ class _PolicyState extends State<Policy> {
|
||||
}
|
||||
|
||||
Widget _buildServiceTile(Map<String, dynamic> service, String index) {
|
||||
bool isSelected = selectedServiceIndex.value == index;
|
||||
// bool isSelected = selectedServiceIndex.value == index;
|
||||
bool isSelected = selectedServicePriorityIndex.value == index;
|
||||
String name = service['name']; // or 'service_id', as needed
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedServiceIndex.value = index;
|
||||
// selectedServiceIndex.value = index;
|
||||
selectedServicePriorityIndex.value = index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
@ -897,16 +937,13 @@ class _PolicyState extends State<Policy> {
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
name,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
),
|
||||
style: GoogleFonts.poppins(color: Colors.black, fontSize: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -939,7 +976,7 @@ class _PolicyState extends State<Policy> {
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
@ -964,10 +1001,10 @@ class _PolicyState extends State<Policy> {
|
||||
padding: isDesktop ? const EdgeInsets.only(left: 30, top: 8) : null,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.62 : null,
|
||||
// width: isDesktop ? MediaQuery.of(context).size.width * 0.75 : null,
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? Container(
|
||||
// color: Colors.amber,
|
||||
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [_buildPolicyServiceOrdering(isDesktop)],
|
||||
@ -989,8 +1026,9 @@ class _PolicyState extends State<Policy> {
|
||||
}
|
||||
|
||||
// Sort services by 'order'
|
||||
ServicesChoosed!
|
||||
.sort((a, b) => (a['order'] ?? 0).compareTo(b['order'] ?? 0));
|
||||
ServicesChoosed!.sort(
|
||||
(a, b) => (a['order'] ?? 0).compareTo(b['order'] ?? 0),
|
||||
);
|
||||
|
||||
List<String> services =
|
||||
ServicesChoosed!.map((service) => service['name'].toString()).toList();
|
||||
@ -1000,15 +1038,22 @@ class _PolicyState extends State<Policy> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: services.asMap().entries.map((entry) {
|
||||
children:
|
||||
services.asMap().entries.map((entry) {
|
||||
int index = entry.key + 1;
|
||||
String service = entry.value;
|
||||
bool isSelected = selectedServiceIndex.value == index.toString();
|
||||
String serviceId = index.toString();
|
||||
bool isSelected =
|
||||
selectedServiceIndex.value == index.toString();
|
||||
|
||||
return SizedBox(
|
||||
// width: isDesktop ? 40 : null,
|
||||
height: isDesktop
|
||||
? max((MediaQuery.of(context).size.height * 0.075), 10)
|
||||
height:
|
||||
isDesktop
|
||||
? max(
|
||||
(MediaQuery.of(context).size.height * 0.075),
|
||||
10,
|
||||
)
|
||||
: 45,
|
||||
|
||||
// max((MediaQuery.of(context).size.height * 0.09), 10)
|
||||
@ -1019,12 +1064,21 @@ class _PolicyState extends State<Policy> {
|
||||
selectedServiceIndex.value = index.toString();
|
||||
selectedService = service;
|
||||
|
||||
print(
|
||||
" selectedServiceIndex.value - ${selectedServiceIndex.value}",
|
||||
);
|
||||
|
||||
// policyCriteriaKey.currentState?.fieldForPolicy();
|
||||
// policyCriteriaKey.currentState
|
||||
// ?.addOrUpdatePolicy(selectedServiceIndex.value);
|
||||
if (selectedService == "Flight" ||
|
||||
selectedService == "Train") {
|
||||
showClass = true;
|
||||
showCost = true;
|
||||
int serviceCode = selectedService == "Flight" ? 1 : 2;
|
||||
policyCriteriaKey.currentState?.fetchTrainFlightClass();
|
||||
|
||||
policyCriteriaKey.currentState
|
||||
?.fetchTrainFlightClass();
|
||||
} else if (selectedService == "Accommodation") {
|
||||
showClass = true;
|
||||
showCost = false;
|
||||
@ -1036,10 +1090,13 @@ class _PolicyState extends State<Policy> {
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? const EdgeInsets.all(8)
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 3),
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -1047,19 +1104,24 @@ class _PolicyState extends State<Policy> {
|
||||
Text(
|
||||
service,
|
||||
style: GoogleFonts.poppins(
|
||||
color: isSelected
|
||||
color:
|
||||
isSelected
|
||||
? const Color(0xFF114D8B)
|
||||
: Colors.black87,
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
decoration: TextDecoration
|
||||
isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.w500,
|
||||
decoration:
|
||||
TextDecoration
|
||||
.none, // remove built-in underline
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
const SizedBox(
|
||||
height: 1), // spacing between text and underline
|
||||
height: 1,
|
||||
), // spacing between text and underline
|
||||
if (isSelected)
|
||||
Container(
|
||||
height: 2,
|
||||
@ -1069,7 +1131,8 @@ class _PolicyState extends State<Policy> {
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
@ -1101,15 +1164,21 @@ class _PolicyState extends State<Policy> {
|
||||
scrollDirection: isDesktop ? Axis.vertical : Axis.horizontal,
|
||||
child: Flex(
|
||||
direction: isDesktop ? Axis.vertical : Axis.horizontal,
|
||||
children: services.asMap().entries.map((entry) {
|
||||
children:
|
||||
services.asMap().entries.map((entry) {
|
||||
int index = entry.key + 1;
|
||||
String service = entry.value;
|
||||
bool isSelected = selectedServiceIndex.value == index.toString();
|
||||
bool isSelected =
|
||||
selectedServiceIndex.value == index.toString();
|
||||
|
||||
return SizedBox(
|
||||
width: isDesktop ? 180 : null,
|
||||
height: isDesktop
|
||||
? max((MediaQuery.of(context).size.height * 0.075), 10)
|
||||
height:
|
||||
isDesktop
|
||||
? max(
|
||||
(MediaQuery.of(context).size.height * 0.075),
|
||||
10,
|
||||
)
|
||||
: 45,
|
||||
|
||||
// max((MediaQuery.of(context).size.height * 0.09), 10)
|
||||
@ -1137,9 +1206,15 @@ class _PolicyState extends State<Policy> {
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.all(5),
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.all(8)
|
||||
: EdgeInsets.only(top: 3, bottom: 3, left: 8, right: 8),
|
||||
: EdgeInsets.only(
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
// color: Colors.blue,
|
||||
color: isSelected ? Color(0xFF114D8B) : Colors.white,
|
||||
@ -1165,10 +1240,12 @@ class _PolicyState extends State<Policy> {
|
||||
color: isSelected ? Colors.white : Colors.black87,
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.w100),
|
||||
isSelected ? FontWeight.bold : FontWeight.w100,
|
||||
),
|
||||
),
|
||||
));
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
@ -1199,7 +1276,8 @@ class _PolicyState extends State<Policy> {
|
||||
});
|
||||
});
|
||||
},
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildTripType(bool isDesktop) {
|
||||
@ -1221,7 +1299,8 @@ class _PolicyState extends State<Policy> {
|
||||
style: GoogleFonts.poppins(
|
||||
color: _selectedTripType == "1" ? Colors.white : Colors.black,
|
||||
fontWeight: _selectedTripType == "1" ? FontWeight.w600 : null,
|
||||
fontSize: 13),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
@ -1248,11 +1327,12 @@ class _PolicyState extends State<Policy> {
|
||||
width: _selectedTripType == "1" ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: _selectedTripType == "1"
|
||||
child:
|
||||
_selectedTripType == "1"
|
||||
? Icon(Icons.rectangle, size: 8, color: Colors.white)
|
||||
: null, // Add checkmark if selected
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -1303,11 +1383,12 @@ class _PolicyState extends State<Policy> {
|
||||
width: _selectedTripType == "2" ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: _selectedTripType == "2"
|
||||
child:
|
||||
_selectedTripType == "2"
|
||||
? Icon(Icons.rectangle, size: 8, color: Colors.white)
|
||||
: null, // Add checkmark if selected
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1345,15 +1426,12 @@ class _PolicyState extends State<Policy> {
|
||||
onPressed: () {
|
||||
context.go('/PolicyList');
|
||||
},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: GoogleFonts.poppins(fontSize: 10),
|
||||
)),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
child: Text("Cancel", style: GoogleFonts.poppins(fontSize: 10)),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
MouseRegion(
|
||||
cursor: isViewMode
|
||||
cursor:
|
||||
isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
child: ElevatedButton(
|
||||
@ -1373,12 +1451,9 @@ class _PolicyState extends State<Policy> {
|
||||
),
|
||||
onPressed:
|
||||
isViewMode ? null : handleSubmit, // Disable when in view mode
|
||||
child: Text(
|
||||
"Submit",
|
||||
style: GoogleFonts.poppins(fontSize: 10),
|
||||
child: Text("Submit", style: GoogleFonts.poppins(fontSize: 10)),
|
||||
),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@ -1399,10 +1474,7 @@ class _PolicyState extends State<Policy> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"Domestic",
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
title: Text("Domestic", style: GoogleFonts.poppins(fontSize: 12)),
|
||||
value: "1",
|
||||
groupValue: _selectedTripType,
|
||||
onChanged: (value) {
|
||||
|
||||
@ -87,6 +87,7 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
fieldForPolicy();
|
||||
|
||||
widget.selectedTabNotifier.addListener(() {
|
||||
print("selectedTab changed: ${widget.selectedTabNotifier.value}");
|
||||
fieldForPolicy();
|
||||
@ -99,11 +100,57 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
void saveCurrentPolicy() {
|
||||
// void saveCurrentPolicy(List<Map<String, dynamic>> services) {
|
||||
// print("saveCurrentPolicy- $services");
|
||||
// if (ServiceId != null) {
|
||||
// print("Saving curremt Add or Update");
|
||||
// addOrUpdatePolicy(ServiceId!);
|
||||
// }
|
||||
// }
|
||||
|
||||
void saveCurrentPolicy(List<Map<String, dynamic>> services) {
|
||||
print("saveCurrentPolicy- $services");
|
||||
|
||||
// Step 1: Save currently selected policy first (if not already saved)
|
||||
if (ServiceId != null) {
|
||||
print("Saving curremt Add or Update");
|
||||
print("Saving current Add or Update");
|
||||
addOrUpdatePolicy(ServiceId!);
|
||||
}
|
||||
|
||||
// Step 2: Collect existing service_ids from policyData
|
||||
final existingServiceIds =
|
||||
policyData?.map((e) => e['service_id'].toString()).toSet();
|
||||
|
||||
// Step 3: Loop through all service definitions
|
||||
for (var service in services) {
|
||||
String id = service['service_id'].toString();
|
||||
|
||||
// Skip if already present
|
||||
if (existingServiceIds!.contains(id)) continue;
|
||||
|
||||
// Step 4: Initialize any missing controllers or data
|
||||
costController.putIfAbsent(id, () => TextEditingController());
|
||||
classAction.putIfAbsent(id, () => "1");
|
||||
FirstApproverAction.putIfAbsent(id, () => "None");
|
||||
SecondApproverAction.putIfAbsent(id, () => "None");
|
||||
ThirdApproverAction.putIfAbsent(id, () => "None");
|
||||
SelectedParallelProcess.putIfAbsent(id, () => "3");
|
||||
|
||||
// Step 5: Add default policy entry
|
||||
policyData?.add({
|
||||
"service_id": int.parse(id),
|
||||
"cost": "",
|
||||
"class": classAction[id],
|
||||
"a1_action": FirstApproverAction[id],
|
||||
"a2_action": SecondApproverAction[id],
|
||||
"a3_action": ThirdApproverAction[id],
|
||||
"parallel_process_from": SelectedParallelProcess[id],
|
||||
"created_by": widget.userId,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 6: Emit updated policy data
|
||||
widget.onPolicyDataChanged(policyData);
|
||||
}
|
||||
|
||||
// To set the data (update)
|
||||
@ -148,6 +195,7 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
}
|
||||
|
||||
void addOrUpdatePolicy(String serviceId) {
|
||||
print("addOrUpdatePolicyserviceId - $serviceId");
|
||||
// 1. First, find existing item if any
|
||||
final existingIndex =
|
||||
policyData!.indexWhere((item) => item["service_id"] == serviceId);
|
||||
@ -172,8 +220,11 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
final a2 = SecondApproverAction[serviceId];
|
||||
final a3 = ThirdApproverAction[serviceId];
|
||||
|
||||
bool hasValue = cost.trim().isNotEmpty;
|
||||
// bool hasValue = cost.trim().isNotEmpty || travelClass.trim().isNotEmpty;
|
||||
// bool hasValue = cost.trim().isNotEmpty;
|
||||
// bool hasValue = cost.trim().isNotEmpty || travelClass!.isNotEmpty;
|
||||
bool hasValue =
|
||||
cost.trim().isNotEmpty || (travelClass?.isNotEmpty ?? false);
|
||||
|
||||
bool allActionsNull = a1 == null && a2 == null && a3 == null;
|
||||
bool someActionsMissing = [a1, a2, a3].where((a) => a != null).length > 0 &&
|
||||
[a1, a2, a3].where((a) => a == null).length > 0;
|
||||
@ -294,6 +345,7 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
|
||||
// Save current input to policyData before switching
|
||||
if (ServiceId != null) {
|
||||
print("Calling addOrUpdatePolicy");
|
||||
addOrUpdatePolicy(
|
||||
ServiceId!); // 👈 Save current values for existing service
|
||||
}
|
||||
@ -301,12 +353,12 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
ServiceId = widget.selectedTabNotifier.value ?? "1";
|
||||
// Initialize controllers and variables if not present
|
||||
costController.putIfAbsent(ServiceId!, () => TextEditingController());
|
||||
classAction.putIfAbsent(ServiceId!, () => null);
|
||||
classAction.putIfAbsent(ServiceId!, () => "1");
|
||||
// classController.putIfAbsent(ServiceId!, () => TextEditingController());
|
||||
|
||||
FirstApproverAction.putIfAbsent(ServiceId!, () => null);
|
||||
SecondApproverAction.putIfAbsent(ServiceId!, () => null);
|
||||
ThirdApproverAction.putIfAbsent(ServiceId!, () => null);
|
||||
FirstApproverAction.putIfAbsent(ServiceId!, () => "None");
|
||||
SecondApproverAction.putIfAbsent(ServiceId!, () => "None");
|
||||
ThirdApproverAction.putIfAbsent(ServiceId!, () => "None");
|
||||
SelectedParallelProcess.putIfAbsent(ServiceId!, () => "3");
|
||||
});
|
||||
|
||||
@ -380,7 +432,7 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
Row(
|
||||
children: [
|
||||
Text(validationErrors[ServiceId]!,
|
||||
style: TextStyle(
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.red,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold))
|
||||
@ -961,6 +1013,8 @@ class PolicyCriteriaState extends State<PolicyCriteria> {
|
||||
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
hintText: "Select",
|
||||
hintStyle: GoogleFonts.poppins(fontSize: 12),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10), // Proper padding
|
||||
|
||||
428
lib/Screens/policy/policyListBackup.dart
Normal file
428
lib/Screens/policy/policyListBackup.dart
Normal file
@ -0,0 +1,428 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/Screens/group/group.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
|
||||
class PolicyListBackup extends StatefulWidget {
|
||||
@override
|
||||
_PolicyListBackupState createState() => _PolicyListBackupState();
|
||||
}
|
||||
|
||||
class _PolicyListBackupState extends State<PolicyListBackup> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
List<dynamic>? apiAllGroups;
|
||||
Color? layoutColor;
|
||||
Color? bodyColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadAllGroups();
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadAllGroups() async {
|
||||
try {
|
||||
final result = await apiService.fetchAllPolicy();
|
||||
|
||||
// Sort by policy_id descending (latest first)
|
||||
result.sort((a, b) {
|
||||
int idA = int.tryParse(a['policy_id'].toString()) ?? 0;
|
||||
int idB = int.tryParse(b['policy_id'].toString()) ?? 0;
|
||||
return idB.compareTo(idA); // latest first
|
||||
});
|
||||
|
||||
setState(() {
|
||||
apiAllGroups = result;
|
||||
});
|
||||
print("Fetched services: $apiAllGroups");
|
||||
} catch (e) {
|
||||
print('Error fetching role list: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void handleActiveStatus(
|
||||
Map<String, dynamic> policyData,
|
||||
String policyId,
|
||||
String currentStatus,
|
||||
) async {
|
||||
print("Toggling user status - $policyId (Current: $currentStatus)");
|
||||
|
||||
final String apiUrlData =
|
||||
'$apiUrl/api/policy/createOrUpdate'; // API for updating user
|
||||
final String? token = await getToken();
|
||||
|
||||
if (token == null) {
|
||||
print("Error: Token not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1")
|
||||
String newStatus = (currentStatus == "1") ? "0" : "1";
|
||||
|
||||
print("STatus 1 - $newStatus");
|
||||
|
||||
final int? selectedPolicyId;
|
||||
|
||||
if (policyId.isNotEmpty) {
|
||||
selectedPolicyId = int.tryParse(policyId);
|
||||
policyData['policy_id'] = selectedPolicyId; // Add only if updating
|
||||
policyData['is_active'] = newStatus; // Add only if updating
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse(apiUrlData),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
loadAllGroups();
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void deletePolicy(Map<String, dynamic> policydata, policyId, status) {
|
||||
print("policyId : $policyId");
|
||||
print("policystatus: $status");
|
||||
print("policysData: $policydata");
|
||||
|
||||
// handleActiveStatus(groupdata, groupId, status);
|
||||
print("Calling handleActiveStatus with: id=$policyId, status=$status");
|
||||
handleActiveStatus(policydata, policyId.toString(), status.toString());
|
||||
}
|
||||
|
||||
// Future<void> deleteGroupFromApi(int groupId) async {
|
||||
// try {
|
||||
// await apiService.deleteGroup(groupId); // your delete API call
|
||||
// deleteGroup(groupId); // remove from UI list
|
||||
// } catch (e) {
|
||||
// print('Error deleting group: $e');
|
||||
// }
|
||||
// }'
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
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: buildGroupList(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
// decoration: BoxDecoration(
|
||||
// // color: Colors.amber,
|
||||
// color: Color(0xFFE1F5FE),
|
||||
// // color: bodyColor,
|
||||
// border: Border.all(
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// color: Colors.white,
|
||||
// width: 3.5)),
|
||||
child: buildGroupListLayout(isDesktop),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupListLayout(bool isDesktop) {
|
||||
return 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,
|
||||
// ),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Policy List',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFF114D8B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// side: BorderSide(color: , width: 1),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: () async {
|
||||
// List<dynamic> users = await futureUsers;
|
||||
context.go('/Policy');
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'New Policy',
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Icon(Icons.add_circle_outline_rounded, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
padding: const EdgeInsets.all(10),
|
||||
// margin: const EdgeInsets.only(bottom: 10),
|
||||
color: Colors.white,
|
||||
// color: Colors.red.shade100,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(children: [buildGroupListView(isDesktop)]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget buildGroupListView(bool isDesktop) {
|
||||
// return Container(
|
||||
// child: Text("DAta"),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget buildGroupListView(bool isDesktop) {
|
||||
if (apiAllGroups == null || apiAllGroups!.isEmpty) {
|
||||
return Center(child: Text("No Policy Found."));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: apiAllGroups!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final policy = apiAllGroups![index];
|
||||
return Card(
|
||||
// color: bodyColor,
|
||||
// color: Color(0xFFF5F5F5),
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"Policy Name",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"Policy Type",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(flex: 1, child: Text("${policy['created_by']}")),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"${policy['name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
policy['domestic'] == "1"
|
||||
? "Domestic"
|
||||
: "International",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(flex: 1, child: Text("${policy['created_by']}")),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final rawId = policy['policy_id'];
|
||||
final intPolicyId =
|
||||
rawId is int
|
||||
? rawId
|
||||
: int.tryParse(rawId.toString()) ?? 0;
|
||||
|
||||
Map<String, dynamic> policyData = await apiService
|
||||
.getSinglePolicy(intPolicyId);
|
||||
|
||||
print("PolicyDATa: $policyData");
|
||||
|
||||
context.go("/Policy", extra: policyData);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final idStr = policy['policy_id'];
|
||||
final id = int.tryParse(idStr.toString());
|
||||
|
||||
if (id == null) {
|
||||
print("group_id is null");
|
||||
return;
|
||||
}
|
||||
final status = policy['is_active'];
|
||||
|
||||
deletePolicy(policy, id, status);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
542
lib/Screens/traveller/travellerDetails.dart
Normal file
542
lib/Screens/traveller/travellerDetails.dart
Normal file
@ -0,0 +1,542 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_text_forex.dart';
|
||||
import 'travellerList.dart';
|
||||
|
||||
class TravellerData extends StatefulWidget {
|
||||
final Future<List<dynamic>> Function() fetchGetTraveller;
|
||||
final bool isDesktop;
|
||||
final Color? layoutColor;
|
||||
|
||||
final int? travellerId; // <-- Add this
|
||||
final Map<String, dynamic>? travellerData;
|
||||
|
||||
const TravellerData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetTraveller,
|
||||
this.travellerId,
|
||||
this.travellerData,
|
||||
});
|
||||
|
||||
@override
|
||||
TravellerDataState createState() => TravellerDataState();
|
||||
}
|
||||
|
||||
class TravellerDataState extends State<TravellerData> {
|
||||
final ApiService apiService = ApiService();
|
||||
Map<String, dynamic>? apiData;
|
||||
|
||||
final Map<String, FocusNode> focusNodes = {
|
||||
"name": FocusNode(),
|
||||
"description": FocusNode(),
|
||||
};
|
||||
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
Map<String, String> errorMessages = {};
|
||||
|
||||
String? selectedName;
|
||||
String? selectedDescription;
|
||||
String? userId;
|
||||
int? travellerDataId;
|
||||
late String isActive = "1";
|
||||
|
||||
List<String> dataHeader = ["first_name", "last_name", "email", "mobile"];
|
||||
|
||||
Map<String, dynamic> travellerDetails() {
|
||||
final data = {
|
||||
// "traveller_id": int.parse(travellerId),
|
||||
"first_name": controllers["first_name"]?.text,
|
||||
"last_name": controllers["last_name"]?.text,
|
||||
"email": controllers["email"]?.text,
|
||||
"mobile": controllers["mobile"]?.text,
|
||||
"is_active": isActive,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
apiData = null;
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
if (widget.travellerId != null) {
|
||||
print('Editing D ID: ${widget.travellerId}');
|
||||
updateTravellerDetails();
|
||||
}
|
||||
}
|
||||
|
||||
void _clearError() {
|
||||
setState(() {
|
||||
errorMessages.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in controllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void updateTravellerDetails() {
|
||||
print("Inside Update Function - ${widget.travellerData}");
|
||||
|
||||
final data = widget.travellerData;
|
||||
|
||||
if (data == null) return;
|
||||
setState(() {
|
||||
controllers['first_name']?.text = data['first_name'] ?? '';
|
||||
controllers['last_name']?.text = data['last_name'] ?? '';
|
||||
controllers['email']?.text = data['email'].toString();
|
||||
controllers['mobile']?.text = data['mobile'].toString();
|
||||
isActive = data["is_active"];
|
||||
final travellerId = int.tryParse(data['traveller_id'].toString());
|
||||
travellerDataId = travellerId;
|
||||
});
|
||||
}
|
||||
|
||||
void toggleStatus() {
|
||||
setState(() {
|
||||
isActive = isActive == "1" ? "0" : "1";
|
||||
});
|
||||
}
|
||||
|
||||
bool validateData() {
|
||||
errorMessages.clear();
|
||||
|
||||
final data = {
|
||||
"first_name": controllers["first_name"]?.text,
|
||||
"last_name": controllers["last_name"]?.text,
|
||||
"email": controllers["email"]?.text,
|
||||
"mobile": controllers["mobile"]?.text,
|
||||
};
|
||||
|
||||
final requiredFields = ["first_name", "last_name", "email", "mobile"];
|
||||
bool hasFocused = false;
|
||||
|
||||
// Check validation for each field
|
||||
for (String field in requiredFields) {
|
||||
if (data[field] == null || data[field]!.trim().isEmpty) {
|
||||
errorMessages[field] = "Required";
|
||||
|
||||
if (!hasFocused) {
|
||||
focusNodes[field]?.requestFocus();
|
||||
hasFocused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data["mobile"] != null && data["mobile"].toString().isNotEmpty) {
|
||||
if (!RegExp(r"^\d{10}$").hasMatch(data["mobile"].toString())) {
|
||||
errorMessages["mobile"] =
|
||||
"Enter 10 digits"; // Invalid mobile number format
|
||||
}
|
||||
}
|
||||
|
||||
if (data["email"] != null && data["email"].toString().isNotEmpty) {
|
||||
if (!RegExp(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
).hasMatch(data["email"].toString())) {
|
||||
errorMessages["email"] = "Invalid email format"; // Invalid email format
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessages.isEmpty;
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
userId = await getUserId();
|
||||
|
||||
setState(() {
|
||||
// This triggers UI rebuild with error messages
|
||||
if (validateData()) {
|
||||
postTravellerData();
|
||||
}
|
||||
});
|
||||
|
||||
final travellerData1 = travellerDetails();
|
||||
print("submit data - $travellerData1");
|
||||
}
|
||||
|
||||
Future<void> postTravellerData({int isActive = 1}) async {
|
||||
// final remarksData = getData();
|
||||
|
||||
final travellerData = travellerDetails();
|
||||
|
||||
print("initially value of the Traveller - $travellerData");
|
||||
// static here
|
||||
final orgId = await getOrgId();
|
||||
|
||||
final String apiUrldata;
|
||||
travellerData["org_id"] = orgId;
|
||||
|
||||
if (travellerDataId != null) {
|
||||
print("for edit traveller id - $travellerDataId");
|
||||
apiUrldata = '$apiUrl/api/travellers/update/$travellerDataId';
|
||||
travellerData["traveller_id"] = travellerDataId.toString();
|
||||
travellerData["updated_by"] = userId;
|
||||
(travellerData.containsKey("created_by"))
|
||||
? travellerData.remove("created_by")
|
||||
: '';
|
||||
} else {
|
||||
print("for add Traveller id - null");
|
||||
apiUrldata = '$apiUrl/api/travellers/create';
|
||||
print("called apiUrl - $apiUrldata");
|
||||
travellerData["created_by"] = userId;
|
||||
}
|
||||
print("recently Traveller data - $travellerData");
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(apiUrldata);
|
||||
final headers = {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final body = jsonEncode(travellerData);
|
||||
|
||||
final response =
|
||||
travellerDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
print("Update - Response: ${response.body}");
|
||||
_clearError();
|
||||
widget.fetchGetTraveller();
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
|
||||
case 201:
|
||||
print("Save - Response: ${response.body}");
|
||||
_clearError();
|
||||
await widget.fetchGetTraveller();
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
|
||||
default:
|
||||
print("Failed to submit traveller. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting plan: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
content: SizedBox(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Row 1: Title + Edit + Delete buttons
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
(travellerDataId != null)
|
||||
? 'Edit Traveller'
|
||||
: 'Create Traveller',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 5),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"First Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["first_name"],
|
||||
focusNode: focusNodes["first_name"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "First Name",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["first_name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["first_name"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Last Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["last_name"],
|
||||
focusNode: focusNodes["last_name"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Last Name",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["last_name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["last_name"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Email *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["email"],
|
||||
focusNode: focusNodes["email"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Email",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["email"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["email"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Mobile *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["mobile"],
|
||||
focusNode: focusNodes["mobile"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Mobile",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["mobile"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["mobile"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
if (travellerDataId != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Change Status ",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
isActive == "1"
|
||||
? "Tap to deactivate"
|
||||
: "Tap to activate",
|
||||
child: GestureDetector(
|
||||
onTap: toggleStatus,
|
||||
child: Text(
|
||||
isActive == "1" ? "Active" : "Inactive",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color: isActive == "1" ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (travellerDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// SizedBox(
|
||||
// child: ElevatedButton(
|
||||
// onPressed: () {
|
||||
// // You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
// },
|
||||
// style: ElevatedButton.styleFrom(
|
||||
// backgroundColor: widget.layoutColor,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// ),
|
||||
// ),
|
||||
// child: Text('Cancel',
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 13, color: Colors.white)),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 10,
|
||||
// ),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
848
lib/Screens/traveller/travellerList.dart
Normal file
848
lib/Screens/traveller/travellerList.dart
Normal file
@ -0,0 +1,848 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../utils/pagination.dart';
|
||||
import 'travellerDetails.dart';
|
||||
|
||||
class TravellerList extends StatefulWidget {
|
||||
const TravellerList({super.key});
|
||||
|
||||
@override
|
||||
TravellerListState createState() => TravellerListState();
|
||||
}
|
||||
|
||||
class TravellerListState extends State<TravellerList> {
|
||||
final GlobalKey<TravellerListState> travellerListKey =
|
||||
GlobalKey<TravellerListState>();
|
||||
|
||||
final ApiService apiService = ApiService();
|
||||
late Future<List<dynamic>> futureTraveller;
|
||||
|
||||
late Map<String, dynamic> depSingleData;
|
||||
String? selectedTravellerId;
|
||||
String? orgId;
|
||||
|
||||
Color? layoutColor;
|
||||
Color? bodyColor;
|
||||
|
||||
List allTraveller = [];
|
||||
List filteredTraveller = [];
|
||||
TextEditingController searchController = TextEditingController();
|
||||
|
||||
int currentPage = 0;
|
||||
int itemsPerPage = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
futureTraveller = fetchGetTraveller();
|
||||
|
||||
futureTraveller.then((object) {
|
||||
setState(() {
|
||||
allTraveller = object;
|
||||
});
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadInitialData();
|
||||
});
|
||||
|
||||
// futurePlans = fetchPlans();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString('auth_token');
|
||||
}
|
||||
|
||||
Future<List<dynamic>> refreshData() {
|
||||
print("Calling Refresh Data");
|
||||
|
||||
futureTraveller = fetchGetTraveller();
|
||||
|
||||
return futureTraveller.then((object) {
|
||||
print("Calling Refresh Data $object");
|
||||
setState(() {
|
||||
allTraveller = object;
|
||||
});
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchGetTraveller() async {
|
||||
String? ordId = await getOrgId();
|
||||
final String apiUrlData =
|
||||
'$apiUrl/api/travellers?for=table_view&org_id=$ordId';
|
||||
|
||||
final String? token = await getToken();
|
||||
|
||||
print("Fetch Traveller");
|
||||
print("2KN Here : $token");
|
||||
|
||||
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',
|
||||
},
|
||||
);
|
||||
print("called api : $apiUrlData");
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return data['data']; // Returning raw JSON list
|
||||
} else {
|
||||
throw Exception('Failed to load users');
|
||||
}
|
||||
}
|
||||
|
||||
void filterTraveller(String query) {
|
||||
// print("all before filtering: $query");
|
||||
// final lowerQuery = query.toLowerCase();
|
||||
// setState(() {
|
||||
// filteredTraveller = allTraveller.where((object) {
|
||||
// return (object['traveller_id']?.toLowerCase().contains(lowerQuery) ??
|
||||
// false) ||
|
||||
// (object['description']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
// (object['user']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
// (object['is_active']?.toLowerCase().contains(lowerQuery) ?? false);
|
||||
// }).toList();
|
||||
// });
|
||||
// print("filteredPlans: $filteredTraveller");
|
||||
|
||||
print("all before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredTraveller =
|
||||
allTraveller.where((object) {
|
||||
final isActiveStatus =
|
||||
object['is_active'] == "1" ? "active" : "inactive";
|
||||
return (object['traveller_id']?.toLowerCase().contains(
|
||||
lowerQuery,
|
||||
) ??
|
||||
false) ||
|
||||
(object['first_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['last_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['mobile']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['email']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(isActiveStatus.contains(lowerQuery));
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredTraveller: $filteredTraveller");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
// appBar: isDesktop ? null : const CustomAppBar(title: 'User Management'),
|
||||
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
|
||||
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),
|
||||
// const Expanded(child: Center(child: Text("User Page Content"))),
|
||||
Expanded(child: buildGroupList(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
// decoration: BoxDecoration(
|
||||
// // color: Colors.amber,
|
||||
// // color: bodyColor,
|
||||
// color: Color(0xFFE1F5FE),
|
||||
// border: Border.all(
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// width: 3.5)),
|
||||
child: buildUserTable(isDesktop),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(bool isDesktop) {
|
||||
return 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,
|
||||
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Divider(
|
||||
// thickness: 0.2, // how "thick" the line is
|
||||
// color: Colors.grey, // optional
|
||||
// ),
|
||||
Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Traveller Details',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.16),
|
||||
|
||||
if (isDesktop)
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.2,
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: filterTraveller,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
size: 18,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
Spacer(),
|
||||
|
||||
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: EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => TravellerData(
|
||||
isDesktop: isDesktop,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetTraveller: refreshData,
|
||||
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min, // Ensures content fits nicely
|
||||
children: [
|
||||
Text(
|
||||
"Add Traveller",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 13 : 11,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8), // spacing between icon and text
|
||||
Icon(
|
||||
Icons.add_circle_outline_rounded,
|
||||
size: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (!isDesktop) SizedBox(height: 5),
|
||||
isDesktop
|
||||
? SizedBox.shrink()
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: filterTraveller,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
size: 18,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder<List<dynamic>>(
|
||||
future: futureTraveller,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"No Traveller Available ",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Please Create Traveller Details",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
/* Here collect the list to displayed the data in table or card Used */
|
||||
List<dynamic> object =
|
||||
filteredTraveller.isNotEmpty
|
||||
? filteredTraveller
|
||||
: allTraveller;
|
||||
|
||||
/* List is Sorting here */
|
||||
object.sort((a, b) {
|
||||
DateTime dateA = DateTime.parse(a['created_on']);
|
||||
DateTime dateB = DateTime.parse(b['created_on']);
|
||||
|
||||
return dateB.compareTo(dateA); // Descending: newest first
|
||||
});
|
||||
|
||||
/* For pagination for list ... */
|
||||
List paginatedTraveller =
|
||||
object
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
|
||||
/* Table ... */
|
||||
Widget table = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double minWidth = isDesktop ? constraints.maxWidth : 1300;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: minWidth),
|
||||
child: DataTable(
|
||||
dividerThickness: 0.5,
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Email',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Mobile',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows:
|
||||
paginatedTraveller.map((tableObject) {
|
||||
String fullName =
|
||||
'${tableObject['first_name'] ?? ''} ${tableObject['last_name'] ?? ''}';
|
||||
String travellerId =
|
||||
tableObject['traveller_id']
|
||||
.toString(); // Get user ID
|
||||
bool isSelected =
|
||||
selectedTravellerId == travellerId;
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
fullName ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['email'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['mobile'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['is_active'] == "1"
|
||||
? 'Active'
|
||||
: 'Inactive',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color:
|
||||
tableObject['is_active'] == "1"
|
||||
? Colors.green
|
||||
: Colors.grey,
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
GestureDetector(
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final travellerId = int.tryParse(
|
||||
tableObject['traveller_id']
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (travellerId != null) {
|
||||
print(
|
||||
"Table cell - traveller Id -- $travellerId",
|
||||
);
|
||||
final data = await apiService
|
||||
.getTravellerDetailsFind(
|
||||
travellerId,
|
||||
);
|
||||
print("TravellerId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => TravellerData(
|
||||
isDesktop: isDesktop,
|
||||
travellerId:
|
||||
travellerId, // Pass the ID
|
||||
travellerData: data,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetTraveller:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("Invalid ID");
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/* Card ... */
|
||||
Widget buildMobileCardView(List<dynamic> paginatedUser) {
|
||||
return ListView.builder(
|
||||
itemCount: paginatedUser.length,
|
||||
itemBuilder: (context, index) {
|
||||
final cardObject = paginatedUser[index];
|
||||
String fullName =
|
||||
'${cardObject['first_name'] ?? ''} ${cardObject['last_name'] ?? ''}';
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status and Employee Code
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
fullName ?? 'N/A',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final travellerId = int.tryParse(
|
||||
cardObject['traveller_id'].toString(),
|
||||
);
|
||||
|
||||
if (travellerId != null) {
|
||||
print("travellerId -- $travellerId");
|
||||
final data = await apiService
|
||||
.getTravellerDetailsFind(
|
||||
travellerId,
|
||||
);
|
||||
print("TravellerId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => TravellerData(
|
||||
isDesktop: isDesktop,
|
||||
travellerId:
|
||||
travellerId, // Pass the ID
|
||||
travellerData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetTraveller: fetchGetTraveller,
|
||||
fetchGetTraveller:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("Invalid ID");
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 2),
|
||||
// Trip Id and Trip Name
|
||||
// Name
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${cardObject['email'] ?? 'N/A'}',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${cardObject['mobile'] ?? 'N/A'}',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// Actions
|
||||
// Actions
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredTraveller.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: table,
|
||||
))
|
||||
: (searchController.text.isNotEmpty &&
|
||||
filteredTraveller.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(
|
||||
paginatedTraveller,
|
||||
)),
|
||||
),
|
||||
// Expanded(
|
||||
// child: isDesktop
|
||||
// ? SingleChildScrollView(
|
||||
// scrollDirection: Axis.vertical,
|
||||
// child: table, // <-- your existing table
|
||||
// )
|
||||
// : buildMobileCardView(paginatedTraveller),
|
||||
// ),
|
||||
PaginationControls(
|
||||
currentPage: currentPage,
|
||||
itemsPerPage: itemsPerPage,
|
||||
totalItems: object.length,
|
||||
activeColor: layoutColor, // your theme color
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
currentPage = page;
|
||||
});
|
||||
},
|
||||
onItemsPerPageChanged: (items) {
|
||||
setState(() {
|
||||
itemsPerPage = items;
|
||||
currentPage = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
383
lib/Screens/userManagement/create_user/change_password.dart
Normal file
383
lib/Screens/userManagement/create_user/change_password.dart
Normal file
@ -0,0 +1,383 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../config/apiUrl.dart';
|
||||
import '../../../services/apiService.dart';
|
||||
import '../../../utils/auth_utils.dart';
|
||||
import '../../../widgets/custom_user_form.dart';
|
||||
|
||||
|
||||
class ChangePasswordDialogData extends StatefulWidget {
|
||||
|
||||
final dynamic isDesktop;
|
||||
final dynamic layoutColor;
|
||||
final dynamic updaterUserId;
|
||||
final dynamic updaterEmail;
|
||||
|
||||
const ChangePasswordDialogData({
|
||||
super.key,
|
||||
this.isDesktop,
|
||||
this.layoutColor,
|
||||
this.updaterUserId,
|
||||
this.updaterEmail
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
ChangePasswordDialogDataState createState() => ChangePasswordDialogDataState();
|
||||
}
|
||||
|
||||
class ChangePasswordDialogDataState extends State<ChangePasswordDialogData> {
|
||||
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
Map<String, String> errorMessages = {};
|
||||
|
||||
|
||||
String? loggeduserId;
|
||||
String? updaterUserIdForAPI;
|
||||
|
||||
List<String> dataHeader = [
|
||||
"email",
|
||||
"changePassword",
|
||||
"confirmPassword"
|
||||
];
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
//
|
||||
// for (var field in dataHeader) {
|
||||
// controllers[field] = TextEditingController();
|
||||
// }
|
||||
//
|
||||
// setState(() {
|
||||
// controllers['email']?.text = widget.updaterEmail ?? '';
|
||||
// controllers['changePassword']?.text = '';
|
||||
// controllers['confirmPassword']?.text = '';
|
||||
// });
|
||||
// }
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print("widget.updaterEmail: ${widget.updaterEmail}");
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
controllers['email']?.text = widget.updaterEmail ;
|
||||
controllers['changePassword']?.text = '';
|
||||
controllers['confirmPassword']?.text = '';
|
||||
updaterUserIdForAPI = widget.updaterUserId;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _clearError() {
|
||||
setState(() {
|
||||
errorMessages.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in controllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool validateData() {
|
||||
errorMessages.clear();
|
||||
|
||||
final String? email = controllers["email"]?.text;
|
||||
final String? changePassword = controllers["changePassword"]?.text;
|
||||
final String? confirmPassword = controllers["confirmPassword"]?.text;
|
||||
|
||||
// Required fields check
|
||||
if (email == null || email.trim().isEmpty) {
|
||||
errorMessages["email"] = "Required";
|
||||
}
|
||||
|
||||
if (changePassword == null || changePassword.trim().isEmpty) {
|
||||
errorMessages["changePassword"] = "Required";
|
||||
}
|
||||
|
||||
if (confirmPassword == null || confirmPassword.trim().isEmpty) {
|
||||
errorMessages["confirmPassword"] = "Required";
|
||||
}
|
||||
|
||||
// Password match check
|
||||
if ((changePassword?.isNotEmpty ?? false) &&
|
||||
(confirmPassword?.isNotEmpty ?? false) &&
|
||||
changePassword != confirmPassword) {
|
||||
errorMessages["changePassword"] = "Passwords do not match";
|
||||
errorMessages["confirmPassword"] = "Passwords do not match";
|
||||
}
|
||||
|
||||
// setState(() {}); // Update UI with any error messages
|
||||
return errorMessages.isEmpty;
|
||||
}
|
||||
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
loggeduserId = await getUserId();
|
||||
|
||||
setState(() {
|
||||
// This triggers UI rebuild with error messages
|
||||
if (validateData()) {
|
||||
postData();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Future<void> postData() async {
|
||||
// final remarksData = getData();
|
||||
print('sss$updaterUserIdForAPI');
|
||||
final loggedInUserId = await getUserId();
|
||||
|
||||
final password = controllers["changePassword"]?.text ?? '';
|
||||
final confirmPassword = controllers["confirmPassword"]?.text ?? '';
|
||||
final String apiUrldata = '$apiUrl/api/user/user-password/$updaterUserIdForAPI';
|
||||
|
||||
final token = await getToken();
|
||||
final headers = {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(apiUrldata);
|
||||
final headers = {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final body = jsonEncode({
|
||||
"password": password,
|
||||
"updated_by": loggedInUserId,
|
||||
});
|
||||
|
||||
final response = await http.put(uri, headers: headers, body: body);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
print("Forex Details Created successfully!");
|
||||
print("Response: ${response.body}");
|
||||
_clearError();
|
||||
Navigator.of(context).pop();
|
||||
} else if (response.statusCode == 404) {
|
||||
Navigator.of(context).pop();
|
||||
final message = jsonDecode(response.body)['message'] ?? 'Unknown error';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.redAccent,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("Failed to submit plan. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting plan: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Row 1: Title + Edit + Delete buttons
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Change Password',
|
||||
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Email",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
// width: isDesktop
|
||||
// ? MediaQuery.of(context).size.width * 0.330
|
||||
// : MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["email"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Email",
|
||||
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (errorMessages["email"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["email"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Change Password",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["changePassword"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Change Password",
|
||||
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (errorMessages["changePassword"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["changePassword"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Confirm Password",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["confirmPassword"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Confirm Password",
|
||||
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (errorMessages["confirmPassword"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["confirmPassword"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,11 +49,12 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
// late List<Map<String, dynamic>?> travelDetailsData;
|
||||
Map<String, dynamic>? travelDetailsData;
|
||||
Map<String, dynamic>? travelDetailsDataFromAPI;
|
||||
|
||||
Map<String, String> errorMessagesTravel = {};
|
||||
late TabController _tabController;
|
||||
|
||||
String? userId;
|
||||
String? orgId;
|
||||
String? userIdApi;
|
||||
|
||||
String? token;
|
||||
|
||||
@ -130,7 +131,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
"delegationEndDate",
|
||||
// "dateOfIssue",
|
||||
// "dateOfExpiry",
|
||||
"changePassword"
|
||||
"changePassword",
|
||||
];
|
||||
|
||||
Color? layoutColor;
|
||||
@ -201,7 +202,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
print("API Selected User Has Data - $apiselectedUser");
|
||||
}
|
||||
|
||||
userIdApi = apiselectedUser?["user_id"] ?? "";
|
||||
controllers["Fname"]?.text = apiselectedUser?["first_name"] ?? "";
|
||||
controllers["Lname"]?.text = apiselectedUser?["last_name"] ?? "";
|
||||
controllers["email"]?.text = apiselectedUser?["email"] ?? "";
|
||||
@ -285,7 +286,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
if (apiselectedUser?["delegated_to_user_id"] != null) {
|
||||
print(
|
||||
"UPDADele- ${apiselectedUser?["delegated_to_user_id"]?.toString()}");
|
||||
"UPDADele- ${apiselectedUser?["delegated_to_user_id"]?.toString()}",
|
||||
);
|
||||
selectedSubstituteApprover =
|
||||
apiselectedUser?["delegated_to_user_id"]?.toString() ?? "";
|
||||
|
||||
@ -302,11 +304,10 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
List<dynamic> decodedList = jsonDecode(fixedJson);
|
||||
|
||||
selectedServiceIds = decodedList.map<Map<String, dynamic>>((item) {
|
||||
selectedServiceIds =
|
||||
decodedList.map<Map<String, dynamic>>((item) {
|
||||
final map = Map<String, dynamic>.from(item);
|
||||
return {
|
||||
"service_id": map['service_id'].toString(),
|
||||
};
|
||||
return {"service_id": map['service_id'].toString()};
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
print("❌ Error decoding fixed agent_supported_service_ids: $e");
|
||||
@ -379,7 +380,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
// userIdsApi = userMap.keys.toList();
|
||||
|
||||
// Handle selectedUser as a Map (not a List)
|
||||
apiselectedUser = extraData['selectedUser']
|
||||
apiselectedUser =
|
||||
extraData['selectedUser']
|
||||
as Map<String, dynamic>?; // Cast it as a Map
|
||||
isViewMode = extraData['isViewMode'] ?? false;
|
||||
isEditProfile = extraData['isEditProfile'] ?? false;
|
||||
@ -442,7 +444,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
userMap = {
|
||||
for (var user in userList)
|
||||
user['user_id'].toString():
|
||||
"${user['first_name']} ${user['last_name']}"
|
||||
"${user['first_name']} ${user['last_name']}",
|
||||
};
|
||||
userIdsApi = userMap.keys.toList();
|
||||
});
|
||||
@ -480,11 +482,13 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
@ -537,6 +541,38 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
}
|
||||
}
|
||||
|
||||
void handleGoBack() async {
|
||||
print("hello, please Go Back");
|
||||
printFormData();
|
||||
}
|
||||
|
||||
void handleBack() async {
|
||||
print("USR Detail back");
|
||||
|
||||
final tabs = {
|
||||
"personal": "Personal Details",
|
||||
"office": "Office Details",
|
||||
"travel": "Travel Details",
|
||||
};
|
||||
|
||||
final tabKeys = tabs.keys.toList(); // ["personal", "office", "travel"]
|
||||
|
||||
final currentIndex = tabKeys.indexOf(selectedTab ?? "travel");
|
||||
|
||||
print("currentIndex - $currentIndex");
|
||||
|
||||
if (currentIndex > 0) {
|
||||
// Move to next tab
|
||||
setState(() {
|
||||
selectedTab = tabKeys[currentIndex - 1];
|
||||
});
|
||||
} else {
|
||||
// Final step — submit or show done
|
||||
print("All tabs completed!");
|
||||
// Submit the full form here
|
||||
}
|
||||
}
|
||||
|
||||
void handleNext() async {
|
||||
print("USR Detail Next");
|
||||
printFormData();
|
||||
@ -545,20 +581,13 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
travelDetailsData = travellerDetailsKey.currentState?.travel_Detials;
|
||||
|
||||
|
||||
print("TRAVEL DETAILS FROM CHILD: $travelDetailsData");
|
||||
|
||||
Map<String, dynamic> data = userDetials;
|
||||
|
||||
print("USERDETAILS : $data");
|
||||
|
||||
if (!isValidData(data)) {
|
||||
print("USERDETAILS : $userDetials");
|
||||
print("Validation Failed: Required fields are missing.");
|
||||
setState(() {});
|
||||
return; // Stop execution if validation fails
|
||||
} else {
|
||||
print("USERDETAILS : $userDetials");
|
||||
|
||||
final tabs = {
|
||||
"personal": "Personal Details",
|
||||
"office": "Office Details",
|
||||
@ -569,24 +598,78 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
final currentIndex = tabKeys.indexOf(selectedTab ?? "personal");
|
||||
|
||||
print("currentIndex - $currentIndex");
|
||||
|
||||
bool isValid = false;
|
||||
|
||||
final currentTab = tabKeys[currentIndex];
|
||||
if (currentTab == "personal") {
|
||||
isValid = isValidData(userDetials);
|
||||
} else if (currentTab == "office") {
|
||||
isValid = isValidDataTwo(userDetials);
|
||||
} else {
|
||||
isValid = true; // Travel tab might not need validation at this point
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
print("Validation Failed on $currentTab: $userDetials");
|
||||
setState(() {}); // To trigger UI update showing errors
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex < tabKeys.length - 1) {
|
||||
// Move to next tab
|
||||
setState(() {
|
||||
selectedTab = tabKeys[currentIndex + 1];
|
||||
});
|
||||
} else {
|
||||
// Already at last tab (travel), maybe submit form or show done message
|
||||
// Final step — submit or show done
|
||||
print("All tabs completed!");
|
||||
// You can trigger full form submit here
|
||||
}
|
||||
// Submit the full form here
|
||||
}
|
||||
|
||||
// if (!isValidData(data))
|
||||
// {
|
||||
// print("USERDETAILS : $userDetials");
|
||||
// print("Validation Failed: Required fields are missing.");
|
||||
// setState(() {});
|
||||
// return; // Stop execution if validation fails
|
||||
// } else
|
||||
// {
|
||||
// print("USERDETAILS : $userDetials");
|
||||
//
|
||||
// if (currentIndex < tabKeys.length - 1) {
|
||||
// // Move to next tab
|
||||
// setState(() {
|
||||
// selectedTab = tabKeys[currentIndex + 1];
|
||||
// });
|
||||
// } else {
|
||||
// // Already at last tab (travel), maybe submit form or show done message
|
||||
// print("All tabs completed!");
|
||||
// // You can trigger full form submit here
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
void handleSubmit() async {
|
||||
print("USR Detail Submit");
|
||||
// printFormData();
|
||||
|
||||
bool isValid = travellerDetailsKey.currentState?.boolValidation() ?? false;
|
||||
|
||||
print("isValid- $isValid");
|
||||
if (!isValid) {
|
||||
print("Validation failed. Please check the inputs.");
|
||||
setState(() {});
|
||||
return; // ❌ STOP execution here if not valid
|
||||
}
|
||||
|
||||
travelDetailsData = travellerDetailsKey.currentState?.travel_Detials;
|
||||
|
||||
|
||||
|
||||
print("travelDetailsData - $travelDetailsData");
|
||||
|
||||
print("TRAVEL DETAILS FROM CHILD");
|
||||
// print("TRAVEL DETAILS FROM CHILD: $travelDetailsData");
|
||||
|
||||
@ -600,7 +683,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
// Map<String, dynamic> data = userDetials;
|
||||
|
||||
if (!isValidData(data)) {
|
||||
if (!isValidData(data) &&
|
||||
isValidDataTwo(data) ) {
|
||||
print("USERDETAILS : $userDetials");
|
||||
print("Validation Failed: Required fields are missing.");
|
||||
setState(() {});
|
||||
@ -622,7 +706,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
"last_name",
|
||||
"email",
|
||||
"mobile_no",
|
||||
// "employeeCode"
|
||||
"role_id",
|
||||
// "employeeCode",
|
||||
];
|
||||
|
||||
if (apiselectedUser == null) {
|
||||
@ -646,8 +731,9 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
if (data["alternate_mobile_no"] != null &&
|
||||
data["alternate_mobile_no"].toString().isNotEmpty) {
|
||||
if (!RegExp(r"^\d{10}$")
|
||||
.hasMatch(data["alternate_mobile_no"].toString())) {
|
||||
if (!RegExp(
|
||||
r"^\d{10}$",
|
||||
).hasMatch(data["alternate_mobile_no"].toString())) {
|
||||
errorMessages["alternate_mobile_no"] =
|
||||
"Enter 10 digits"; // Invalid mobile number format
|
||||
}
|
||||
@ -655,14 +741,31 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
// Email validation
|
||||
if (data["email"] != null && data["email"].toString().isNotEmpty) {
|
||||
if (!RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
.hasMatch(data["email"].toString())) {
|
||||
if (!RegExp(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
).hasMatch(data["email"].toString())) {
|
||||
errorMessages["email"] = "Invalid email format"; // Invalid email format
|
||||
}
|
||||
}
|
||||
return errorMessages.isEmpty; // Valid if there are no errors
|
||||
}
|
||||
|
||||
bool isValidDataTwo(Map<String, dynamic> data) {
|
||||
errorMessages.clear(); // Reset errors
|
||||
|
||||
// Required fields that must not be empty
|
||||
List<String> requiredFields = ["employee_code"];
|
||||
|
||||
// Check validation for each field
|
||||
for (String field in requiredFields) {
|
||||
if (data[field] == null || data[field].toString().trim().isEmpty) {
|
||||
errorMessages[field] = "Required";
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessages.isEmpty; // Valid if there are no errors
|
||||
}
|
||||
|
||||
void _clearError(String field) {
|
||||
if (mounted && errorMessages.containsKey(field)) {
|
||||
setState(() {
|
||||
@ -813,8 +916,10 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
print("enteredPassword - $enteredPassword ");
|
||||
|
||||
if (hashedPassword != null && hashedPassword.isNotEmpty) {
|
||||
bool isMatch =
|
||||
BCrypt.checkpw(enteredPassword, hashedPassword); // Compare passwords
|
||||
bool isMatch = BCrypt.checkpw(
|
||||
enteredPassword,
|
||||
hashedPassword,
|
||||
); // Compare passwords
|
||||
|
||||
setState(() {
|
||||
// ✅ Ensure UI updates
|
||||
@ -824,8 +929,9 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
print(" Password match!");
|
||||
} else {
|
||||
print(" Password NOT match!");
|
||||
errorMessages
|
||||
.remove("password"); // Clear error if password is different
|
||||
errorMessages.remove(
|
||||
"password",
|
||||
); // Clear error if password is different
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -835,8 +941,10 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
@ -845,22 +953,24 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: buildData(isDesktop, context)),
|
||||
],
|
||||
children: [Expanded(child: buildData(isDesktop, context))],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildData(bool isDesktop, context) {
|
||||
@ -874,7 +984,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: isDesktop
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
child: Padding(
|
||||
@ -887,24 +998,42 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: isDesktop
|
||||
child:
|
||||
isDesktop
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: (selectedTab == "travel" ||
|
||||
children: [
|
||||
if (selectedTab != "personal")
|
||||
..._buildBack(isDesktop, layoutColor!),
|
||||
Spacer(), // spacing between buttons
|
||||
// Next or Submit based on role or user type
|
||||
if (selectedTab == "travel" ||
|
||||
selectedRole == "5" ||
|
||||
setSelectesUserType == true)
|
||||
? _buildSubmit(isDesktop, layoutColor!)
|
||||
: _buildNext(isDesktop, layoutColor!),
|
||||
..._buildSubmit(isDesktop, layoutColor!)
|
||||
else
|
||||
..._buildNext(isDesktop, layoutColor!),
|
||||
// (selectedTab == "travel" ||
|
||||
// selectedRole == "5" ||
|
||||
// setSelectesUserType == true)
|
||||
// ? _buildSubmit(isDesktop, layoutColor!)
|
||||
// : _buildNext(
|
||||
// isDesktop,
|
||||
// layoutColor!,
|
||||
// ), // _buildGoBack(isDesktop, layoutColor!),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: (selectedTab == "travel" ||
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children:
|
||||
(selectedTab == "travel" ||
|
||||
selectedRole == "5" ||
|
||||
setSelectesUserType == true)
|
||||
? _buildSubmit(isDesktop, layoutColor!)
|
||||
: _buildNext(isDesktop, layoutColor!),
|
||||
)),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -943,9 +1072,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
// })
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 18,
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
isDesktop
|
||||
? buildTabsForUser()
|
||||
: SingleChildScrollView(
|
||||
@ -954,14 +1081,13 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
),
|
||||
Container(
|
||||
// color: Colors.yellow.shade50,
|
||||
|
||||
height: MediaQuery.of(context).size.height * 0.64,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: buildTabContents(isDesktop, isViewMode)),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -976,6 +1102,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
personalDetailsKey: personalDetailsKey,
|
||||
isDesktop: isDesktop, // pass isDesktop as a named argument
|
||||
isViewMode: isViewMode,
|
||||
userIdApi: userIdApi,
|
||||
controllers: controllers,
|
||||
errorMessages: errorMessages,
|
||||
selectedGender: selectedGender,
|
||||
@ -1010,6 +1137,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
return OfficeDetails(
|
||||
isDesktop: isDesktop, // pass isDesktop as a named argument
|
||||
isViewMode: isViewMode,
|
||||
userIdApi: userIdApi,
|
||||
controllers: controllers,
|
||||
errorMessages: errorMessages,
|
||||
selectedLevel: selectedLevel,
|
||||
@ -1052,6 +1180,9 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
},
|
||||
);
|
||||
case "travel":
|
||||
final fullName =
|
||||
"${controllers["Fname"]?.text ?? ""} ${controllers["Lname"]?.text ?? ""}"
|
||||
.trim();
|
||||
return TravellerDetails(
|
||||
key: travellerDetailsKey,
|
||||
isDesktop: isDesktop,
|
||||
@ -1060,12 +1191,15 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
errorMessages: errorMessages,
|
||||
travelDetails: travelDetailsDataFromAPI, // 👈 Pass this down
|
||||
passportFileUrlFromApi: passportFileUrlFromApi,
|
||||
userIdApi: userIdApi,
|
||||
fullName: fullName,
|
||||
);
|
||||
default:
|
||||
return PersonalDetails(
|
||||
personalDetailsKey: personalDetailsKey,
|
||||
isDesktop: isDesktop, // pass isDesktop as a named argument
|
||||
isViewMode: isViewMode,
|
||||
userIdApi: userIdApi,
|
||||
controllers: controllers,
|
||||
errorMessages: errorMessages,
|
||||
selectedGender: selectedGender,
|
||||
@ -1096,9 +1230,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
Map<String, String> getTabs(bool setSelectesUserType) {
|
||||
if (setSelectesUserType || selectedRole == "5") {
|
||||
return {
|
||||
"personal": "Personal Details",
|
||||
};
|
||||
return {"personal": "Personal Details"};
|
||||
} else {
|
||||
return allTabs;
|
||||
}
|
||||
@ -1108,16 +1240,46 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end, // important
|
||||
children: tabs.entries.map((entry) {
|
||||
children:
|
||||
tabs.entries.map((entry) {
|
||||
final targetTab = entry.key;
|
||||
|
||||
print("TargetsTAb: $targetTab");
|
||||
|
||||
final isSelected = selectedTab == entry.key;
|
||||
print("isSelected: $isSelected");
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
bool isValid = false;
|
||||
|
||||
final currentTab = selectedTab;
|
||||
if (currentTab == "personal") {
|
||||
isValid = isValidData(userDetials);
|
||||
|
||||
if (isValid) {
|
||||
selectedTab = entry.key;
|
||||
}
|
||||
} else if (currentTab == "office" &&
|
||||
targetTab == "personal") {
|
||||
selectedTab = entry.key;
|
||||
} else if (currentTab == "office") {
|
||||
isValid = isValidDataTwo(userDetials);
|
||||
if (isValid) {
|
||||
selectedTab = entry.key;
|
||||
}
|
||||
} else {
|
||||
isValid =
|
||||
true; // Travel tab might not need validation at this point
|
||||
selectedTab = entry.key;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0), // space between tabs
|
||||
padding: const EdgeInsets.only(
|
||||
right: 24.0,
|
||||
), // space between tabs
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -1126,7 +1288,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Color(0xFF114D8B) : Color(0xFF475569),
|
||||
color:
|
||||
isSelected ? Color(0xFF114D8B) : Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@ -1146,10 +1309,40 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
|
||||
// ---- Submit ---------------------------------------
|
||||
|
||||
List<Widget> _buildGoBack(isDesktop, Color layoutColor) {
|
||||
return [
|
||||
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: handleGoBack,
|
||||
child: Text("Back"),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildNext(isDesktop, Color layoutColor) {
|
||||
return [
|
||||
MouseRegion(
|
||||
cursor: isViewMode
|
||||
cursor:
|
||||
isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
child: ElevatedButton(
|
||||
@ -1172,7 +1365,38 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
// isViewMode ? null : handleNext, // Disable when in view mode
|
||||
child: Text("Next"),
|
||||
),
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildBack(isDesktop, Color layoutColor) {
|
||||
return [
|
||||
MouseRegion(
|
||||
cursor:
|
||||
isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
child: TextButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
isViewMode ? Colors.white : Colors.white, // Keep original color
|
||||
foregroundColor:
|
||||
isViewMode ? layoutColor : layoutColor, // Keep original color
|
||||
disabledBackgroundColor:
|
||||
layoutColor, // Ensure color remains when disabled
|
||||
disabledForegroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||
),
|
||||
onPressed: handleBack,
|
||||
// onPressed:
|
||||
// isViewMode ? null : handleNext, // Disable when in view mode
|
||||
child: Text("Back"),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@ -1191,20 +1415,21 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
onPressed: () {
|
||||
isEditProfile ? context.go('/listPlan') : context.go('/listUser');
|
||||
},
|
||||
child: Text("Cancel")),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
if (!isViewMode)
|
||||
MouseRegion(
|
||||
cursor: isViewMode
|
||||
cursor:
|
||||
isViewMode
|
||||
? SystemMouseCursors.forbidden
|
||||
: SystemMouseCursors.click,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
isViewMode ? layoutColor : layoutColor, // Keep original color
|
||||
foregroundColor: isViewMode
|
||||
foregroundColor:
|
||||
isViewMode
|
||||
? Colors.white
|
||||
: Colors.white, // Keep original color
|
||||
disabledBackgroundColor:
|
||||
@ -1220,7 +1445,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
||||
isViewMode ? null : handleSubmit, // Disable when in view mode
|
||||
child: Text("Submit"),
|
||||
),
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user