latest commit 06-06-2025

This commit is contained in:
vadivelJ96 2025-06-06 11:51:45 +05:30
commit ef79391915
117 changed files with 23708 additions and 16813 deletions

View File

@ -5,15 +5,7 @@
android:label="frontend"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".MainActivity"
android:exported="true"
@ -36,6 +28,15 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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,12 +46,14 @@ Future<dynamic> showApprovalDialog(
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: selectedAction == "accept"
? layoutColor
: Colors.grey.shade200,
foregroundColor: selectedAction == "accept"
? Colors.white
: Colors.black,
backgroundColor:
selectedAction == "accept"
? Colors.green
: Colors.grey.shade200,
foregroundColor:
selectedAction == "accept"
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@ -68,12 +71,14 @@ Future<dynamic> showApprovalDialog(
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: selectedAction == "reject"
? Colors.redAccent
: Colors.grey.shade200,
foregroundColor: selectedAction == "reject"
? Colors.white
: Colors.black,
backgroundColor:
selectedAction == "reject"
? Colors.redAccent
: Colors.grey.shade200,
foregroundColor:
selectedAction == "reject"
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@ -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,94 +196,14 @@ Future<dynamic> showApprovalDialog(
Future<bool?> showApproveDialog1(BuildContext context, Color layoutColor) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
title: const Text(
"Confirm Approval",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
content: const Text("Are you sure you want to approve this plan?"),
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: layoutColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () => Navigator.pop(context, true),
child: const Text("OK"),
),
],
),
);
}
/// Show confirm dialog for rejection with remarks input
Future<String?> showRejectDialog1(
BuildContext context, Color layoutColor) async {
String remarks = "";
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) => AlertDialog(
builder:
(context) => AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.all(36),
// title: const Text("Confirm Rejection"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Confirm Rejection",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text("Please enter reason for rejection."),
const SizedBox(height: 10),
TextField(
maxLines: 3,
onChanged: (value) => remarks = value,
decoration: const InputDecoration(
hintText: "Remarks...",
hintStyle: TextStyle(
fontSize: 10, // 👈 Set your desired font size here
color: Colors.grey,
fontFamily: "Inter", // optional if you want consistent font
),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey, width: 0.5),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey, width: 0.5),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey, width: 1),
),
),
),
],
title: const Text(
"Confirm Approval",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: const Text("Are you sure you want to approve this plan?"),
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(
@ -299,14 +228,102 @@ Future<String?> showRejectDialog1(
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
if (remarks.trim().isEmpty) return;
Navigator.pop(context, true);
},
onPressed: () => Navigator.pop(context, true),
child: const Text("OK"),
),
],
),
);
}
/// Show confirm dialog for rejection with remarks input
Future<String?> showRejectDialog1(
BuildContext context,
Color layoutColor,
) async {
String remarks = "";
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
return StatefulBuilder(
builder:
(context, setState) => AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.all(36),
// title: const Text("Confirm Rejection"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Confirm Rejection",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text("Please enter reason for rejection."),
const SizedBox(height: 10),
TextField(
maxLines: 3,
onChanged: (value) => remarks = value,
decoration: const InputDecoration(
hintText: "Remarks...",
hintStyle: TextStyle(
fontSize: 10, // 👈 Set your desired font size here
color: Colors.grey,
fontFamily:
"Inter", // optional if you want consistent font
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blueGrey,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blueGrey,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey, width: 1),
),
),
),
],
),
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: layoutColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
if (remarks.trim().isEmpty) return;
Navigator.pop(context, true);
},
child: const Text("OK"),
),
],
),
);
},
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -19,13 +19,14 @@ class CostCenterData extends StatefulWidget {
final int? costcenterId; // <-- Add this
final Map<String, dynamic>? costcenterData;
const CostCenterData(
{super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetCostCenter,
this.costcenterId,
this.costcenterData});
const CostCenterData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetCostCenter,
this.costcenterId,
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,10 +191,10 @@ class CostCenterDataState extends State<CostCenterData> {
};
final body = jsonEncode(costcenterData);
final response = costcenterDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
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:
@ -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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -266,19 +262,20 @@ class CostCenterDataState extends State<CostCenterData> {
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["name"],
focusNode: focusNodes["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["name"],
focusNode: focusNodes["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -334,41 +330,37 @@ class CostCenterDataState extends State<CostCenterData> {
],
],
),
SizedBox(
height: 15,
),
SizedBox(height: 15),
if (costcenterDataId != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
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,
),
),
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.grey,
),
),
)
],
),
if (costcenterDataId != null)
SizedBox(
height: 15,
),
),
],
),
if (costcenterDataId != null) SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@ -405,16 +397,20 @@ class CostCenterDataState extends State<CostCenterData> {
borderRadius: BorderRadius.circular(8),
),
),
child: Text('Save',
style: GoogleFonts.poppins(
fontSize: 11, color: Colors.white)),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
)
),
// : SizedBox.shrink(),
],
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -195,65 +195,78 @@ class StatusDashboardState extends State<StatusDashboard> {
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: 10, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
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
),
// color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Wrap(
spacing: 10,
runSpacing: 10,
children: typeBasedCount.map((item) {
double cardWidth = isDesktop
? (MediaQuery.of(context).size.width * 0.75 - 10) / 2 // 80% width padding adjusted
: MediaQuery.of(context).size.width - 24; // full width with padding
padding: isDesktop
? EdgeInsets.symmetric(
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(
decoration: BoxDecoration(
color: isDesktop ? Colors.white : const Color(0xFFFCFCFC),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Wrap(
spacing: 10,
runSpacing: 10,
children: typeBasedCount.map((item) {
double cardWidth = isDesktop
? (MediaQuery.of(context).size.width * 0.75 - 10) / 2 // 80% width padding adjusted
: MediaQuery.of(context).size.width - 24; // full width with padding
return SizedBox(
width: cardWidth,
child: buildInfoCard(item['value'], item['count'], cardWidth),
);
}).toList(),
return SizedBox(
width: cardWidth,
child: buildInfoCard(item['value'], item['count'], cardWidth),
);
}).toList(),
),
const SizedBox(height: 20),
Wrap(
spacing: 10,
runSpacing: 10,
children: statusBasedCount.map((item) {
double cardWidth = isDesktop
? (MediaQuery.of(context).size.width * 0.90 - 50) / 6 // desktop layout: 6 cards per row
: MediaQuery.of(context).size.width - 24; // mobile: full width
return SizedBox(
width: cardWidth,
child: buildInfoCard(item['value'], item['count'], cardWidth),
);
}).toList(),
),
],
),
),
),
],
),
),
const SizedBox(height: 20),
Wrap(
spacing: 10,
runSpacing: 10,
children: statusBasedCount.map((item) {
double cardWidth = isDesktop
? (MediaQuery.of(context).size.width * 0.90 - 50) / 6 // desktop layout: 6 cards per row
: MediaQuery.of(context).size.width - 24; // mobile: full width
return SizedBox(
width: cardWidth,
child: buildInfoCard(item['value'], item['count'], cardWidth),
);
}).toList(),
),
],
),
),
),
),
],
),
)
);
},
)
),
);
});

View File

@ -19,13 +19,14 @@ class DepartmentData extends StatefulWidget {
final int? departmentId; // <-- Add this
final Map<String, dynamic>? departmentData;
const DepartmentData(
{super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetDepartment,
this.departmentId,
this.departmentData});
const DepartmentData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetDepartment,
this.departmentId,
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,10 +191,10 @@ class DepartmentDataState extends State<DepartmentData> {
};
final body = jsonEncode(departmentData);
final response = departmentDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
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:
@ -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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -266,19 +262,20 @@ class DepartmentDataState extends State<DepartmentData> {
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["name"],
focusNode: focusNodes["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["name"],
focusNode: focusNodes["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -334,41 +330,37 @@ class DepartmentDataState extends State<DepartmentData> {
],
],
),
SizedBox(
height: 15,
),
SizedBox(height: 15),
if (departmentDataId != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
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,
),
),
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 (departmentDataId != null)
SizedBox(
height: 15,
),
),
],
),
if (departmentDataId != null) SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@ -405,16 +397,20 @@ class DepartmentDataState extends State<DepartmentData> {
borderRadius: BorderRadius.circular(8),
),
),
child: Text('Save',
style: GoogleFonts.poppins(
fontSize: 11, color: Colors.white)),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
)
),
// : SizedBox.shrink(),
],
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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,56 +140,63 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
List<dynamic> travellerList = responseBody['data'];
setState(() {
_traveller = travellerList
.map((user) => SearchTraveler.fromJson(user))
.toList();
_traveller =
travellerList
.map((user) => SearchTraveler.fromJson(user))
.toList();
_filteredTraveller = List.from(_traveller);
});
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,19 +209,23 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
];
} else {
_filteredList = [
..._users.where((user) {
print("usersLLL : ${user}");
..._users
.where((user) {
print("usersLLL : ${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()));
}).map((user) => {"type": "user", "data": user}),
List<String> searchFields = [
"${user.firstName} ${user.lastName}".toLowerCase(),
user.email.toLowerCase() ?? "",
user.userId.toLowerCase() ?? "",
user.mobileNo ?? "",
user.alternateMobileNo ?? "",
user.empCode?.toLowerCase() ?? "",
];
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) {
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}),
..._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}),
];
}
});
@ -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) {
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()));
}).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}),
..._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()),
);
})
.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}),
];
}
});
@ -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}",
);
}
}
@ -324,10 +350,14 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
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)),
? 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
@ -344,11 +374,14 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
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),
@ -366,9 +399,13 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("or create a new traveler",
style: GoogleFonts.poppins(
fontSize: 14, color: Color(0xFF575A74))),
Text(
"or create a new traveler",
style: GoogleFonts.poppins(
fontSize: 14,
color: Color(0xFF575A74),
),
),
TextButton(
onPressed: () {
setState(() {
@ -376,9 +413,13 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
_searchController.clear();
});
},
child: Text("Create",
style: GoogleFonts.poppins(
fontSize: 14, color: widget.layoutColorForUser)),
child: Text(
"Create",
style: GoogleFonts.poppins(
fontSize: 14,
color: widget.layoutColorForUser,
),
),
),
],
),
@ -389,17 +430,20 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
// User List or Message
_searchController.text.isNotEmpty
? SizedBox(
height: 300, // Limit height to avoid overflow
// child: _filteredUsers.isEmpty
child: _filteredList.isEmpty
? Center(
height: 300, // Limit height to avoid overflow
// child: _filteredUsers.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(
: ListView.builder(
// itemCount: _filteredUsers.length,
itemCount: _filteredList.length,
itemBuilder: (context, index) {
@ -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,37 +465,42 @@ class _UserSelectionDialogState extends State<UserSelectionDialog> {
"${user.firstName ?? "Unknown"} ${user.lastName ?? ""}",
style: GoogleFonts.poppins(fontSize: 11),
),
subtitle: userType == "user"
? Text(
"Employee ID: ${user.empCode ?? ""} ",
// "Employee ID: ${userType == "user" ? user.userId : user.travellerId}",
style:
GoogleFonts.poppins(fontSize: 10),
)
: Text(
"Mobile : ${user.mobileNo ?? ""} ",
// "Employee ID: ${userType == "user" ? user.userId : user.travellerId}",
style:
GoogleFonts.poppins(fontSize: 10),
),
subtitle:
userType == "user"
? Text(
"Employee ID: ${user.empCode ?? ""} ",
// "Employee ID: ${userType == "user" ? user.userId : user.travellerId}",
style: GoogleFonts.poppins(
fontSize: 10,
),
)
: Text(
"Mobile : ${user.mobileNo ?? ""} ",
// "Employee ID: ${userType == "user" ? user.userId : user.travellerId}",
style: GoogleFonts.poppins(
fontSize: 10,
),
),
onTap: () {
String selectedUser =
"${user.firstName ?? "Unknown"} ${user.lastName ?? ""}";
setState(() {
_searchController.text = selectedUser;
userIdSelected = userType == "user"
? user.userId
: user.travellerId;
userIdSelected =
userType == "user"
? user.userId
: user.travellerId;
isTraveller = userType == "traveller";
});
print(
"Selected: $selectedUser, ID: ${userType == "user" ? user.userId : user.travellerId},"
" isTraveller: $userIdSelected");
"Selected: $selectedUser, ID: ${userType == "user" ? user.userId : user.travellerId},"
" isTraveller: $userIdSelected",
);
},
);
},
),
)
)
: SizedBox.shrink(),
// Traveler Form
@ -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,
required this.orgId,
required this.firstNameController,
required this.lastNameController,
required this.emailController,
required this.mobileController,
required this.onSubmit});
TravelerForm({
required this.formKey,
required this.orgId,
required this.firstNameController,
required this.lastNameController,
required this.emailController,
required this.mobileController,
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)),
),
),
],
),

View File

@ -21,13 +21,14 @@ class ForexData extends StatefulWidget {
final int? forexId; // <-- Add this
final Map<String, dynamic>? forexData;
const ForexData(
{super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetForex,
this.forexId,
this.forexData});
const ForexData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetForex,
this.forexId,
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,9 +274,10 @@ class ForexDataState extends State<ForexData> {
};
final body = jsonEncode(forexData);
final response = forexDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
final response =
forexDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
// final response = await http.post(
// Uri.parse(apiUrldata),
@ -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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -381,18 +381,19 @@ 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(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
item,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
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...",
@ -405,25 +406,25 @@ 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(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select Country",
style: GoogleFonts.poppins(fontSize: 11),
),
),
),
),
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;
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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -459,18 +461,19 @@ class ForexDataState extends State<ForexData> {
// ? MediaQuery.of(context).size.width * 0.330
// : MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["currency"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Currency",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["currency"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Currency",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
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,38 +494,43 @@ class ForexDataState extends State<ForexData> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Cash (%)",
"Cash (%) *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
width: widget.isDesktop
? MediaQuery.of(context).size.width * 0.09
: MediaQuery.of(context).size.width * 0.66,
width:
widget.isDesktop
? MediaQuery.of(context).size.width * 0.09
: MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["cash"],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Cash",
labelStyle:
TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
height: 40,
child: TextField(
controller: controllers["cash"],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Cash",
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,38 +546,43 @@ class ForexDataState extends State<ForexData> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Card (%)",
"Card (%) *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
width: widget.isDesktop
? MediaQuery.of(context).size.width * 0.09
: MediaQuery.of(context).size.width * 0.66,
width:
widget.isDesktop
? MediaQuery.of(context).size.width * 0.09
: MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["card"],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Card",
labelStyle:
TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
height: 40,
child: TextField(
controller: controllers["card"],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Card",
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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -603,18 +613,19 @@ class ForexDataState extends State<ForexData> {
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["perdiemAmount"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Perdiem Amount",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["perdiemAmount"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Perdiem Amount",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
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(
@ -636,9 +645,10 @@ class ForexDataState extends State<ForexData> {
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
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',
style: GoogleFonts.poppins(
fontSize: 11, color: Colors.white)),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
)
),
// : SizedBox.shrink(),
],
),

File diff suppressed because it is too large Load Diff

View File

@ -21,13 +21,14 @@ class GroupData extends StatefulWidget {
final int? groupId; // <-- Add this
final Map<String, dynamic>? groupData;
const GroupData(
{super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetGroup,
this.groupId,
this.groupData});
const GroupData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetGroup,
this.groupId,
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 = [];
@ -75,12 +75,12 @@ class GroupDataState extends State<GroupData> {
Map<String, dynamic> group_Detials() {
final data = {
"name":controllers["name"]?.text,
"description":controllers["description"]?.text,
"domestic_policy_id":selectedDomesticPolicyID,
"international_policy_id":selectedInternationalPolicyID,
"domestic_policy_name":selectedDomesticPolicyName,
"international_policy_name":selectedInternationalPolicyName,
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
"domestic_policy_id": selectedDomesticPolicyID,
"international_policy_id": selectedInternationalPolicyID,
"domestic_policy_name": selectedDomesticPolicyName,
"international_policy_name": selectedInternationalPolicyName,
"is_active": isActive,
};
return data;
@ -166,23 +166,19 @@ class GroupDataState extends State<GroupData> {
});
}
bool validateData() {
errorMessages.clear();
final data = {
"name":controllers["name"]?.text,
"description":controllers["description"]?.text,
"domestic_policy_id":selectedDomesticPolicyID,
"international_policy_id":selectedInternationalPolicyID,
"domestic_policy_name":selectedDomesticPolicyName,
"international_policy_name":selectedInternationalPolicyName,
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
"domestic_policy_id": selectedDomesticPolicyID,
"international_policy_id": selectedInternationalPolicyID,
"domestic_policy_name": selectedDomesticPolicyName,
"international_policy_name": selectedInternationalPolicyName,
};
final requiredFields = [
"name",
"description",
];
final requiredFields = ["name", "description"];
// Check validation for each field
for (String field in requiredFields) {
@ -240,9 +236,10 @@ class GroupDataState extends State<GroupData> {
};
final body = jsonEncode(groupData);
final response = groupDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
final response =
groupDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
if (response.statusCode == 200 || response.statusCode == 201) {
print("Group Details Created successfully!");
@ -288,8 +285,8 @@ 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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -351,18 +346,19 @@ class GroupDataState extends State<GroupData> {
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["name"] != null) ...[
SizedBox(height: 5), // Space before error message
@ -380,9 +376,10 @@ class GroupDataState extends State<GroupData> {
Text(
"Select Policy For International",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -391,21 +388,23 @@ 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(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
item,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
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: "Select Policy For International",
@ -418,25 +417,25 @@ 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(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select Policy For International",
style: GoogleFonts.poppins(fontSize: 11),
),
),
),
),
dropdownBuilder: (context, selectedItem) => Align(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select Policy For International",
style: GoogleFonts.poppins(fontSize: 11),
),
),
onChanged: (String? newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedInternationalPolicyID = InternationalMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
selectedInternationalPolicyID =
InternationalMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
selectedInternationalPolicyName = newValue;
});
},
@ -452,9 +451,10 @@ class GroupDataState extends State<GroupData> {
Text(
"Select Policy For Domestic",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -466,18 +466,19 @@ 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(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
object,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
itemBuilder:
(context, object, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 6.0,
),
child: Text(
object,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Select Policy For Domestic...",
@ -490,25 +491,25 @@ 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(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select Policy For Domestic",
style: GoogleFonts.poppins(fontSize: 11),
),
),
),
),
dropdownBuilder: (context, selectedItem) => Align(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select Policy For Domestic",
style: GoogleFonts.poppins(fontSize: 11),
),
),
onChanged: (String? newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedDomesticPolicyID = DomesticMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
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)),
fontSize: 12,
fontWeight: FontWeight.w600,
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,
@ -569,13 +569,14 @@ class GroupDataState extends State<GroupData> {
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
Tooltip(
message:
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
child: GestureDetector(
onTap: toggleStatus,
child: Text(
@ -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,16 +628,20 @@ class GroupDataState extends State<GroupData> {
borderRadius: BorderRadius.circular(8),
),
),
child: Text('Save',
style: GoogleFonts.poppins(
fontSize: 11, color: Colors.white)),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
)
),
// : SizedBox.shrink(),
],
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View 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),
),
],
),
],
),
),
);
},
);
}
}

View File

@ -20,13 +20,14 @@ class HotelsData extends StatefulWidget {
final int? hotelsId; // <-- Add this
final Map<String, dynamic>? hotelsData;
const HotelsData(
{super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetHotels,
this.hotelsId,
this.hotelsData});
const HotelsData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetHotels,
this.hotelsId,
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,9 +220,10 @@ class HotelsDataState extends State<HotelsData> {
};
final body = jsonEncode(hotelsData);
final response = hotelsDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
final response =
hotelsDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
if (response.statusCode == 200 || response.statusCode == 201) {
print("Hotels Details Created successfully!");
@ -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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -302,18 +311,19 @@ class HotelsDataState extends State<HotelsData> {
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["hotel_name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Hotel Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["hotel_name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Hotel Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
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)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -341,18 +352,19 @@ class HotelsDataState extends State<HotelsData> {
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["hotel_chain"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Hotel Chain",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
height: 40,
child: TextField(
controller: controllers["hotel_chain"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Hotel Chain",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["hotel_chain"] != null) ...[
SizedBox(height: 5), // Space before error message
@ -363,55 +375,23 @@ class HotelsDataState extends State<HotelsData> {
],
],
),
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(
isFocused: false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["city"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "City",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
)),
),
if (errorMessages["city"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["city"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
// - 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(
"Country",
"Country *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
@ -423,18 +403,19 @@ class HotelsDataState extends State<HotelsData> {
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(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
child: Text(
item,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
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...",
@ -447,25 +428,25 @@ class HotelsDataState extends State<HotelsData> {
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 1,
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),
),
),
),
),
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;
selectedCountry =
countryMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
selectedCountryName = newValue;
});
},
@ -481,7 +462,48 @@ class HotelsDataState extends State<HotelsData> {
],
],
),
SizedBox( height: 15 ),
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(
isFocused: false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["city"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "City",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["city"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["city"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 10),
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
if (hotelsDataId != null)
Row(
@ -490,13 +512,14 @@ class HotelsDataState extends State<HotelsData> {
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
Tooltip(
message:
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
child: GestureDetector(
onTap: toggleStatus,
child: Text(
@ -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',
style: GoogleFonts.poppins(
fontSize: 11, color: Colors.white)),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
)
),
// : SizedBox.shrink(),
],
),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,12 +14,13 @@ class TaxiScreen extends StatefulWidget {
final Map<String, dynamic>? selectedItem;
final String? loginUser;
TaxiScreen(
{required this.onClose,
this.apiData,
required this.onSavetaxi,
required this.selectedItem,
required this.loginUser});
TaxiScreen({
required this.onClose,
this.apiData,
required this.onSavetaxi,
required this.selectedItem,
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,30 +204,33 @@ class _TaxiScreenState extends State<TaxiScreen> {
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Container(
// color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(28.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
return Container(
// color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(28.0),
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>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
))
.toList();
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),
),
),
);
}
@ -281,31 +294,28 @@ class _TaxiScreenState extends State<TaxiScreen> {
Text(
"Taxi Required For",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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: [
Text(
"Number of Passenger",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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,28 +345,21 @@ 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: [
Text(
"Car Type",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -369,18 +373,21 @@ 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
? (newValue) {
setState(() {
selectedCarType = newValue;
});
print(
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
}
: null,
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedCarType = newValue;
});
print(
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}",
);
}
: null,
items: dropdownItems,
),
@ -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>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
))
.toList();
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,9 +432,10 @@ class _TaxiScreenState extends State<TaxiScreen> {
CustomTextFieldWrapper(
isFocused: _taxiReqFocused,
isDesktop: isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.66,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
child: DropdownButtonFormField<String>(
@ -436,18 +444,21 @@ 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
? (newValue) {
setState(() {
selectedReqTaxi = newValue;
});
print(
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
}
: null,
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedReqTaxi = newValue;
});
print(
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}",
);
}
: null,
items: dropdownItems,
),
@ -466,10 +477,11 @@ class _TaxiScreenState extends State<TaxiScreen> {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedCheckOutDate != null &&
_selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
initialDate:
_selectedCheckOutDate != null &&
_selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
firstDate: today,
lastDate: DateTime(2100),
);
@ -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;
});
@ -509,9 +526,10 @@ class _TaxiScreenState extends State<TaxiScreen> {
Text(
"City",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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)),
fontSize: 12,
fontWeight: FontWeight.w500,
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,28 +591,21 @@ 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: [
Text(
"Date",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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,28 +639,21 @@ 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: [
Text(
"Time",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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)),
],
],
),
@ -702,17 +702,19 @@ class _TaxiScreenState extends State<TaxiScreen> {
Text(
"Comments",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: _commentsFocus, // Dropdown doesn't use focus
isDesktop: isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.66,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
@ -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(

View File

@ -17,14 +17,15 @@ class TrainScreen extends StatefulWidget {
final String? loginUser;
final String? tripType;
TrainScreen(
{required this.onClose,
this.apiData,
required this.onSavetrain,
required this.selectedItem,
required this.loginUser,
this.apiDataForClass,
this.tripType});
TrainScreen({
required this.onClose,
this.apiData,
required this.onSavetrain,
required this.selectedItem,
required this.loginUser,
this.apiDataForClass,
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,51 +288,54 @@ class _TrainScreenState extends State<TrainScreen> {
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Container(
// color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Align(
// alignment: Alignment.centerRight,
// child: InkWell(
// onTap: () {
// widget.onClose(false);
// },
// child: Icon(
// Icons.close,
// size: 18,
// color: Color(0xFF575A74),
// ),
// ),
// ),
// Text("Train Booking List",
// style: TextStyle(
// fontSize: 18,
// fontWeight: FontWeight.bold,
// color: Color(0xFF575A74))),
// SizedBox(
// height: 6,
// ),
Padding(
padding: const EdgeInsets.all(28.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
return Container(
// color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Align(
// alignment: Alignment.centerRight,
// child: InkWell(
// onTap: () {
// widget.onClose(false);
// },
// child: Icon(
// Icons.close,
// size: 18,
// color: Color(0xFF575A74),
// ),
// ),
// ),
// Text("Train Booking List",
// style: TextStyle(
// fontSize: 18,
// fontWeight: FontWeight.bold,
// color: Color(0xFF575A74))),
// SizedBox(
// height: 6,
// ),
Padding(
padding: const EdgeInsets.all(28.0),
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 [
@ -367,9 +371,10 @@ class _TrainScreenState extends State<TrainScreen> {
Text(
"Train Number",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
))
.toList();
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,10 +457,11 @@ class _TrainScreenState extends State<TrainScreen> {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedCheckOutDate != null &&
_selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
initialDate:
_selectedCheckOutDate != null &&
_selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
firstDate: today,
lastDate: DateTime(2100),
);
@ -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>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
))
.toList();
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),
),
),
);
}
@ -523,9 +536,10 @@ class _TrainScreenState extends State<TrainScreen> {
Text(
"Class *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -543,86 +557,91 @@ 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
? (newValue) {
setState(() {
selectedClass = newValue;
});
}
: null,
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedClass = newValue;
});
}
: null,
items: dropdownItems,
),
),
),
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: [
Text(
"From",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
isFocused: _fromFocus,
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: isCountryLoading
? Center(child: CircularProgressIndicator())
: DropdownSearch<String>(
isFocused: _fromFocus,
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child:
isCountryLoading
? Center(child: CircularProgressIndicator())
: DropdownSearch<String>(
// selectedItem: selectedFrom != null
// ? countryMap[selectedFrom]
// : null,
selectedItem: selectedFrom != null
? countryMap[
selectedFrom] // get the display value from code
: null,
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(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
// onChanged: (String? newValue) {
// setState(() {
// // selectedFrom[index] = countryMap.entries
@ -636,56 +655,51 @@ class _TrainScreenState extends State<TrainScreen> {
// print(selectedFrom);
// });
// },
onChanged: (String? newValue) {
setState(() {
selectedFrom = countryMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
selectedFrom =
countryMap.entries
.firstWhere(
(entry) => entry.value == newValue,
)
.key;
});
},
),
)
// child: SizedBox(
// height: 40,
// child: TextField(
// focusNode: _fromFocusNode,
// controller: _fromController,
// style: const TextStyle(fontSize: 12),
// decoration: const InputDecoration(
// labelText: "From",
// labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
// floatingLabelBehavior: FloatingLabelBehavior.never,
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(vertical: 16),
// ),
// ),
// ),
),
),
// child: SizedBox(
// height: 40,
// child: TextField(
// focusNode: _fromFocusNode,
// controller: _fromController,
// style: const TextStyle(fontSize: 12),
// decoration: const InputDecoration(
// labelText: "From",
// labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
// floatingLabelBehavior: FloatingLabelBehavior.never,
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(vertical: 16),
// ),
// ),
// ),
),
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: [
Text(
"To",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -693,71 +707,72 @@ class _TrainScreenState extends State<TrainScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: isCountryLoading
? Center(child: CircularProgressIndicator())
: DropdownSearch<String>(
selectedItem: selectedTo != null
? countryMap[
selectedTo] // get the display value from code
: null,
popupProps: PopupProps.menu(
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search ...",
contentPadding:
EdgeInsets.symmetric(horizontal: 10),
child:
isCountryLoading
? Center(child: CircularProgressIndicator())
: DropdownSearch<String>(
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,
),
),
),
),
),
items: countryMap.values.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1),
items: countryMap.values.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
setState(() {
selectedTo =
countryMap.entries
.firstWhere(
(entry) => entry.value == newValue,
)
.key;
});
},
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
setState(() {
selectedTo = countryMap.entries
.firstWhere((entry) => entry.value == newValue)
.key;
});
},
),
),
),
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: [
Text(
"Date",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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,28 +807,21 @@ 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: [
Text(
"Time",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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)),
],
],
),
@ -860,9 +871,10 @@ class _TrainScreenState extends State<TrainScreen> {
Text(
"Comments",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w500,
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(

View File

@ -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,
),
),
),
],
),

View File

@ -224,14 +224,26 @@ class BusListWidget extends StatelessWidget {
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,
),
),
),
],
),

View File

@ -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,16 +119,18 @@ class _FlightListWidgetState extends State<FlightListWidget> {
// style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
// ),
MouseRegion(
cursor: widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
cursor:
widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.isViewMode
? null
: () {
checkClass();
print("New data");
},
onTap:
widget.isViewMode
? null
: () {
checkClass();
print("New data");
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
@ -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,70 +310,76 @@ 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(
children: [
Expanded(
flex: 2,
child: Text(
"Class",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
flex: 2,
child: Text(
"Class",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 4,
child: Text(
"Sector",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
flex: 4,
child: Text(
"Sector",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
"Date",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
flex: 2,
child: Text(
"Date",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
"Time",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
flex: 2,
child: Text(
"Time",
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) {
String fromPlaceCountry =
countryMap[trip["from_place"]?.toString()] ??
"Unknown Country";
"Unknown Country";
String toPlaceCountry =
countryMap[trip["to_place"]?.toString()] ??
"Unknown Country";
"Unknown Country";
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
@ -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,
@ -423,15 +449,22 @@ class _FlightListWidgetState extends State<FlightListWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildKeyValueRow(
"Class",
getRequestForClass(trip["class"].toString()) ??
"N/A"),
_buildKeyValueRow("Sector",
"${trip["from_place"] ?? "N/A"} - ${trip["to_place"] ?? "N/A"}"),
"Class",
getRequestForClass(trip["class"].toString()) ??
"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)),
),
],
),

View File

@ -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(

View File

@ -202,14 +202,26 @@ class InsuranceListWidget extends StatelessWidget {
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,
),
),
),
],
),
@ -445,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,

View File

@ -9,14 +9,15 @@ class MiscellaneousListWidget extends StatelessWidget {
final Function(String, bool) onAddNew;
final bool isViewMode;
const MiscellaneousListWidget(
{super.key,
required this.miscellaneousList,
required this.onOpen,
required this.onDeleteMiscellaneous,
required this.apiData,
required this.onAddNew,
required this.isViewMode});
const MiscellaneousListWidget({
super.key,
required this.miscellaneousList,
required this.onOpen,
required this.onDeleteMiscellaneous,
required this.apiData,
required this.onAddNew,
required this.isViewMode,
});
@override
Widget build(BuildContext context) {
@ -34,16 +35,18 @@ class MiscellaneousListWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MouseRegion(
cursor: isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
cursor:
isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: GestureDetector(
onTap: isViewMode
? null
: () {
print("New data");
onAddNew("Miscellaneous", true);
},
onTap:
isViewMode
? null
: () {
print("New data");
onAddNew("Miscellaneous", true);
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
@ -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),
],
),
),
@ -162,79 +165,89 @@ class MiscellaneousListWidget extends StatelessWidget {
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(
children: [
Expanded(
flex: 2,
child: Text(
"Special Request",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
Expanded(
flex: 3,
child: Text(
" Comments",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
],
)
children: [
Expanded(
flex: 2,
child: Text(
"Special Request",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 3,
child: Text(
" Comments",
style: GoogleFonts.poppins(fontSize: 11),
),
),
],
)
: SizedBox.shrink(),
isDesktop
? Row(
children: [
Expanded(
flex: 2,
child: Text(
getRequestValue(
item["special_request"]?.toString()),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
children: [
Expanded(
flex: 2,
child: Text(
getRequestValue(
item["special_request"]?.toString(),
),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
Expanded(
flex: 3,
child: Text(
item["comments"],
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
Expanded(
flex: 3,
child: Text(
item["comments"],
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
)
),
],
)
: Column(
children: [
_buildRow(
"Special Request:",
getRequestValue(
item["special_request"]?.toString())),
SizedBox(width: 10),
_buildRow("Comments:", item["comments"] ?? "N/A"),
],
),
children: [
_buildRow(
"Special Request:",
getRequestValue(item["special_request"]?.toString()),
),
SizedBox(width: 10),
_buildRow("Comments:", item["comments"] ?? "N/A"),
],
),
],
),
),
@ -263,31 +276,35 @@ class MiscellaneousListWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
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
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(value.substring(0, limit) + "...",
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
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
overflow: TextOverflow.ellipsis,
),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
);
@ -314,36 +331,36 @@ 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
? Tooltip(
message: wrapText(value, 50),
decoration: BoxDecoration(
color: Colors.grey.shade200, // Black background
borderRadius: BorderRadius.circular(6),
),
textStyle: TextStyle(
color: Colors.black, fontSize: 12), // Tooltip text color
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
overflow: TextOverflow.ellipsis,
),
)
: Text(value),
child:
exceedsLimit
? Tooltip(
message: wrapText(value, 50),
decoration: BoxDecoration(
color: Colors.grey.shade200, // Black background
borderRadius: BorderRadius.circular(6),
),
textStyle: TextStyle(
color: Colors.black,
fontSize: 12,
), // Tooltip text color
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
overflow: TextOverflow.ellipsis,
),
)
: Text(value),
),
],
);
}
//
//
// Widget _buildData(BuildContext context) {
// return Container(
// width: MediaQuery.of(context).size.width,

View File

@ -11,14 +11,15 @@ class TaxiListWidget extends StatelessWidget {
final Function(String, bool) onAddNew;
final bool isViewMode;
const TaxiListWidget(
{super.key,
required this.taxiList,
required this.apiData,
required this.onOpen,
required this.onDeleteTaxi,
required this.onAddNew,
required this.isViewMode});
const TaxiListWidget({
super.key,
required this.taxiList,
required this.apiData,
required this.onOpen,
required this.onDeleteTaxi,
required this.onAddNew,
required this.isViewMode,
});
@override
Widget build(BuildContext context) {
@ -36,17 +37,19 @@ class TaxiListWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MouseRegion(
cursor: isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
cursor:
isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: GestureDetector(
onTap: isViewMode
? null
: () {
print("New data");
onAddNew("Taxi", true);
},
onTap:
isViewMode
? null
: () {
print("New data");
onAddNew("Taxi", true);
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
@ -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");
@ -238,127 +244,136 @@ class TaxiListWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"${getRequestForTaxiClass(item["car_type"]!.toString())} For ${item["no_of_passengers"]!} Passenger",
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w800,
)),
"${getRequestForTaxiClass(item["car_type"]!.toString())} For ${item["no_of_passengers"]!} Passenger",
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(
children: [
Expanded(
flex: 2,
child: Text(
"City",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
Expanded(
flex: 2,
child: Text(
" Location",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
Expanded(
flex: 2,
child: Text(
" Date",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
Expanded(
flex: 2,
child: Text(
"Comments",
style: GoogleFonts.poppins(
fontSize: 11,
),
)),
],
)
children: [
Expanded(
flex: 2,
child: Text(
"City",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
" Location",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
" Date",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
"Comments",
style: GoogleFonts.poppins(fontSize: 11),
),
),
],
)
: SizedBox.shrink(),
SizedBox(height: 4),
isDesktop
? Row(
children: [
Expanded(
flex: 2,
child: Text(
"${item["destination_city"]}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
children: [
Expanded(
flex: 2,
child: Text(
"${item["destination_city"]}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
"${item["location_of_pickup"]}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
"${item["location_of_pickup"]}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
"${formatDate(item["date"]!)} ${formatTime(item["time"]!)}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
"${formatDate(item["date"]!)} ${formatTime(item["time"]!)}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: _buildComments(
" Comments:", item["comments"] ?? "N/A"),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: _buildComments(
" Comments:",
item["comments"] ?? "N/A",
),
],
)
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRow("City:", item["destination_city"]),
SizedBox(width: 10),
_buildRow("Pickup:", item["location_of_pickup"]!),
SizedBox(width: 10),
_buildDateTimeRow(
"Date:",
formatDate(item["date"]!),
formatTime(item["time"]!),
),
_buildRow("TaxiFor:", item["car_required_for"]!),
_buildRow("Comments:", item["comments"] ?? "N/A"),
],
)
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRow("City:", item["destination_city"]),
SizedBox(width: 10),
_buildRow("Pickup:", item["location_of_pickup"]!),
SizedBox(width: 10),
_buildDateTimeRow(
"Date:",
formatDate(item["date"]!),
formatTime(item["time"]!),
),
_buildRow("TaxiFor:", item["car_required_for"]!),
_buildRow("Comments:", item["comments"] ?? "N/A"),
],
),
],
),
),
@ -387,31 +402,35 @@ class TaxiListWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
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
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(value.substring(0, limit) + "...",
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
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
overflow: TextOverflow.ellipsis,
),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
);
@ -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
? 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
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(value.substring(0, limit) + "...",
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
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
),
),
overflow: TextOverflow.ellipsis,
),
)
: 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)),
),
],
);

View File

@ -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,
),
),
),
],
),

View File

@ -301,14 +301,26 @@ class VisaListWidget extends StatelessWidget {
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,
),
),
),
],
),

View 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('&nbsp;', ' ')
// .replaceAll('&amp;', '&')
// .replaceAll('&lt;', '<')
// .replaceAll('&gt;', '>')
// .replaceAll('&quot;', '"')
// .replaceAll('&#39;', "'"); // 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),
),
),
),
],
);
}
}

View File

@ -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.

View File

@ -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,18 +64,12 @@ 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),
],
),
);
}
}
}

View 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)),
),
],
);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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 '';
}
}

View File

@ -1 +0,0 @@
export 'dart:ui' if (dart.library.js_interop) 'dart:ui_web';

View File

@ -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;
}

View File

@ -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,
);
}
}

View File

@ -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);
}

View File

@ -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,
);

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export './web_stub.dart' if (dart.library.js_interop) './web_real.dart';

View File

@ -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;
},
);
}

View File

@ -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.');

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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,
),
),
),
),
],
),
);
}
}

View File

@ -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');
}
}

View File

@ -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.

View File

@ -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,
),
)
],
),
),
),
],
),
),
);
}
}

View File

@ -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;
});
}
}

View File

@ -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.

View File

@ -1,6 +1 @@
import 'package:meta/meta.dart' show immutable;
@immutable
class QuillEditorWebVideoEmbedConfig {
const QuillEditorWebVideoEmbedConfig();
}
// TODO Implement this library.

View File

@ -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.

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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;
}

View File

@ -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.

View File

@ -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.

View File

@ -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;
}

View File

@ -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';

View File

@ -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.

View File

@ -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.

View File

@ -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;
}

View File

@ -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.

View File

@ -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),
],
);
}
}

View File

@ -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
? Color(int.parse(layoutString))
: Colors.redAccent;
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor = bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
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('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'"); // 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,42 +379,123 @@ class TemplateState extends State<Template> {
}
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
Future<void> handleSubmit() async {
Map<String, dynamic> data = TemplateData;
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)),
],
),
),
);
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) {
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),
),
),
),
],
);
}

View 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('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'"); // 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),
],
);
}
}

View 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,
),
),
),
],
),
);
}
}

View File

@ -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,13 +67,15 @@ class _OrgSetUpState extends State<OrgSetUp> {
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor = layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor = bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
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,75 +147,92 @@ class _OrgSetUpState extends State<OrgSetUp> {
try {
print("getUpdatedServices");
final result = await apiService.fetchOrganization();
print("UUPdatedServices - $result");
setState(() {
selectedOrg = result;
final prefs = await SharedPreferences.getInstance();
final String? orgDataString = prefs.getString('org_data');
String? rawLogoPath = selectedOrg?['logo'];
if (rawLogoPath != null && rawLogoPath.contains('/assets')) {
const baseUrl = "https://apitest.tripapprovaltool.com";
final assetPath = rawLogoPath.split('/assets').last;
selectedOrg!['logo'] = "$baseUrl/assets$assetPath";
}
if (orgDataString != null) {
final Map<String, dynamic> orgData = jsonDecode(orgDataString);
print("UUPdatedServices - $orgData");
setState(() {
selectedOrg = orgData;
_orgNameController.text = selectedOrg?['name'];
String? rawLogoPath = selectedOrg?['logo'];
if (rawLogoPath != null && rawLogoPath.contains('/assets')) {
const baseUrl = "https://apitest.tripapprovaltool.com";
final assetPath = rawLogoPath.split('/assets').last;
selectedOrg!['logo'] = "$baseUrl/assets$assetPath";
}
layoutColor = selectedOrg?['layout_color'] != null
? Color(int.parse(
selectedOrg!['layout_color'].toString().replaceFirst('0x', ''),
radix: 16))
: Colors.white;
_orgNameController.text = selectedOrg?['name'];
bodyColor = selectedOrg?['color'] != null
? Color(int.parse(
selectedOrg!['color'].toString().replaceFirst('0x', ''),
radix: 16))
: Colors.blue;
layoutColor =
selectedOrg?['layout_color'] != null
? Color(
int.parse(
selectedOrg!['layout_color'].toString().replaceFirst(
'0x',
'',
),
radix: 16,
),
)
: Colors.white;
// Set mail config fields
mailConfig['sender_email'] = selectedOrg?['sender_email'];
mailConfig['mail_user_name'] = selectedOrg?['mail_user_name'];
mailConfig['mail_password'] = selectedOrg?['mail_password'];
mailConfig['mail_host'] = selectedOrg?['mail_host'];
mailConfig['mail_port'] = selectedOrg?['mail_port'];
bodyColor =
selectedOrg?['color'] != null
? Color(
int.parse(
selectedOrg!['color'].toString().replaceFirst('0x', ''),
radix: 16,
),
)
: Colors.blue;
// Set selected service IDs
// final services = selectedOrg?['services_ids'] as List<dynamic>? ?? [];
// selectedServiceIds =
// services.map((item) => item['service_id'].toString()).toList();
//
// Set mail config fields
mailConfig['sender_email'] = selectedOrg?['sender_email'];
mailConfig['mail_user_name'] = selectedOrg?['mail_user_name'];
mailConfig['mail_password'] = selectedOrg?['mail_password'];
mailConfig['mail_host'] = selectedOrg?['mail_host'];
mailConfig['mail_port'] = selectedOrg?['mail_port'];
final servicesRaw = selectedOrg?['services_ids'];
// Set selected service IDs
// final services = selectedOrg?['services_ids'] as List<dynamic>? ?? [];
// selectedServiceIds =
// services.map((item) => item['service_id'].toString()).toList();
//
List<dynamic> services;
final servicesRaw = selectedOrg?['services_ids'];
if (servicesRaw is String) {
try {
services = jsonDecode(servicesRaw);
} catch (e) {
print('❌ Failed to decode services_ids: $e');
List<dynamic> services;
if (servicesRaw is String) {
try {
services = jsonDecode(servicesRaw);
} catch (e) {
print('❌ Failed to decode services_ids: $e');
services = [];
}
} else if (servicesRaw is List) {
services = servicesRaw;
} else {
services = [];
}
} else if (servicesRaw is List) {
services = servicesRaw;
} else {
services = [];
}
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(),
};
}).toList();
});
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()};
}).toList();
});
orgId = await getOrgId();
orgId = await getOrgId();
print("selectedOrg - $selectedOrg");
print("mailConfig - $mailConfig");
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,32 +396,38 @@ 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,
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: buildOrganizationLayout(isDesktop))
],
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(0),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
Expanded(child: buildOrganizationLayout(isDesktop)),
],
),
),
),
);
});
);
},
);
}
Widget buildOrganizationLayout(isDesktop) {
@ -380,30 +455,51 @@ class _OrgSetUpState extends State<OrgSetUp> {
),
),
Container(
padding: const EdgeInsets.all(5),
color: Colors.white,
child: isDesktop
? Row(
padding: const EdgeInsets.all(5),
color: Colors.white,
child:
isDesktop
? Row(
mainAxisAlignment: MainAxisAlignment.end,
// children: [Text("Button")],
children:
_buildSubmit(isDesktop, isViewMode, layoutColor),
children: _buildSubmit(
isDesktop,
isViewMode,
layoutColor,
),
)
: Row(
: 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
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
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,16 +576,19 @@ 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),
child: Text(
"Name:",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121)),
fontSize: 14,
fontWeight: FontWeight.w600,
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,84 +619,93 @@ class _OrgSetUpState extends State<OrgSetUp> {
Spacer(),
GestureDetector(
onTap: _pickImage,
child: _imageBytes != null
? ClipOval(
child: Image.memory(
_imageBytes!,
width: 50,
height: 50,
fit: BoxFit.cover,
),
)
: selectedOrg?['logo'] != null
? ClipRect(
child: Image.network(
selectedOrg!['logo'],
width: 250, // increased
height: 75, // increased
fit: BoxFit.contain,
errorBuilder:
(context, error, stackTrace) {
return const CircleAvatar(
radius: 20,
backgroundColor: Colors.redAccent,
child: Icon(Icons.error, size: 10),
);
},
),
)
: const CircleAvatar(
radius: 20,
backgroundColor: Colors.amber,
child: Icon(Icons.add_a_photo, size: 10),
child:
_imageBytes != null
? ClipOval(
child: Image.memory(
_imageBytes!,
// width: 50,
// height: 50,
width:
responsiveLogoWidth, // Use responsive width
height: responsiveLogoHeight,
fit: BoxFit.contain,
),
)
: selectedOrg?['logo'] != null
? ClipRect(
child: Image.network(
selectedOrg!['logo'],
width:
responsiveLogoWidth, // Use responsive width
height: responsiveLogoHeight,
// width: 250,
// height: 55,
fit: BoxFit.contain,
errorBuilder: (
context,
error,
stackTrace,
) {
return const CircleAvatar(
radius: 20,
backgroundColor: Colors.redAccent,
child: Icon(Icons.error, size: 10),
);
},
),
)
: const CircleAvatar(
radius: 20,
backgroundColor: Colors.amber,
child: Icon(Icons.add_a_photo, size: 10),
),
),
],
),
),
SizedBox(
height: 10,
),
SizedBox(height: 10),
Text(
"Services",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121)),
),
SizedBox(
height: 10,
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
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
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// mainAxisSize: MainAxisSize.min,
children: _buildOptions(),
)
: Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _buildOptions(),
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
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// mainAxisSize: MainAxisSize.min,
children: _buildOptions(),
)
: Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: _buildOptions()),
),
),
),
),
SizedBox(
height: 15,
),
SizedBox(height: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -595,9 +713,10 @@ class _OrgSetUpState extends State<OrgSetUp> {
Text(
"Choose Theme",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121)),
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
Container(
decoration: BoxDecoration(
@ -606,99 +725,106 @@ class _OrgSetUpState extends State<OrgSetUp> {
// color: Color(0xFFF4F4FB),
),
padding: EdgeInsets.only(
left: 5, right: 5, top: 15, bottom: 5),
child: layoutColor != null && bodyColor != null
? ColorThemePickerWidget(
initialLayoutColor: layoutColor,
initialBodyColor: bodyColor,
onLayoutColorSelected:
(Color selectedLayoutColor) {
setState(() {
layoutColor = selectedLayoutColor;
});
},
onBodyColorSelected: (Color selectedBodyColor) {
setState(() {
bodyColor = selectedBodyColor;
});
},
)
: CircularProgressIndicator(),
left: 5,
right: 5,
top: 15,
bottom: 5,
),
child:
layoutColor != null && bodyColor != null
? ColorThemePickerWidget(
initialLayoutColor: layoutColor,
initialBodyColor: bodyColor,
onLayoutColorSelected: (
Color selectedLayoutColor,
) {
setState(() {
layoutColor = selectedLayoutColor;
});
},
onBodyColorSelected: (
Color selectedBodyColor,
) {
setState(() {
bodyColor = selectedBodyColor;
});
},
)
: CircularProgressIndicator(),
),
],
),
SizedBox(
height: 15,
),
SizedBox(height: 15),
Container(
color: Colors.white,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Mail Settings",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121)),
color: Colors.white,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Mail Settings",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
// GestureDetector(
// onTap: () {
// setState(() {
// showMail = !showMail;
// });
// },
// child: Icon(
// Icons.keyboard_arrow_down_outlined,
// color: Color(0xFF114D8B),
// size: 30,
// ),
// ),
],
),
// if (showMail)
// GestureDetector(
// onTap: () {
// setState(() {
// showMail = !showMail;
// });
// },
// child: Icon(
// Icons.keyboard_arrow_down_outlined,
// color: Color(0xFF114D8B),
// size: 30,
// ),
// ),
],
),
SizedBox(
height: 10,
// if (showMail)
SizedBox(height: 10),
Container(
// width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFF5F5F5),
// color: bodyColor ?? Colors.grey,
width: 1.5,
),
// color: bodyColor,
color: Colors.white70,
// color: Color(0xFFF5F5F5),
),
Container(
// width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFF5F5F5),
// color: bodyColor ?? Colors.grey,
width: 1.5,
),
// color: bodyColor,
color: Colors.white70,
// color: Color(0xFFF5F5F5),
),
child: Row(
mainAxisAlignment: isDesktop
child: Row(
mainAxisAlignment:
isDesktop
? MainAxisAlignment.start
: MainAxisAlignment.center,
children: [
mailConfig['sender_email'] != null
? MailSetting(
isDesktop: isDesktop,
initialMailData: mailConfig,
onMailDataChanged: (updatedData) {
// You can setState here or do something else with updatedData
print(
"Updated Mail Data: $updatedData");
children: [
mailConfig['sender_email'] != null
? MailSetting(
isDesktop: isDesktop,
initialMailData: mailConfig,
onMailDataChanged: (updatedData) {
// You can setState here or do something else with updatedData
print("Updated Mail Data: $updatedData");
mailConfig = updatedData;
},
)
: CircularProgressIndicator(),
],
))
],
)),
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,54 +885,65 @@ class _OrgSetUpState extends State<OrgSetUp> {
}
});
},
child: Row(children: [
iconUrl.isNotEmpty
? Image.network(
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));
return Icon(
fallbackIcon,
size: 18,
color:
isSelected == name
? Color(0xFF114D8B)
: Color(0xFF475569),
);
},
)
: Icon(fallbackIcon,
: Icon(
fallbackIcon,
size: 18,
color:
isSelected == name ? Color(0xFF114D8B) : Color(0xFF475569)),
isSelected == name ? Color(0xFF114D8B) : Color(0xFF475569),
),
SizedBox(width: 2),
SizedBox(width: 2),
Text(
name,
style: GoogleFonts.poppins(
Text(
name,
style: GoogleFonts.poppins(
fontSize: 12,
color: isSelected == name ? Color(0xFF114D8B) : Color(0xFF475569),
fontWeight:
isSelected == name ? FontWeight.bold : FontWeight.w500),
// fontWeight: selectedListOption == title ? FontWeight.bold : FontWeight.normal,)),
),
isSelected == name ? FontWeight.bold : FontWeight.w500,
),
// fontWeight: selectedListOption == title ? FontWeight.bold : FontWeight.normal,)),
),
SizedBox(width: 2),
// if (selectedListOption == title && widget.isViewMode == false)
Container(
SizedBox(width: 2),
// if (selectedListOption == title && widget.isViewMode == false)
Container(
height: 15,
width: 15,
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,
)),
]),
),
),
],
),
);
}
@ -836,25 +975,21 @@ class _OrgSetUpState extends State<OrgSetUp> {
List<Widget> _buildSubmit(isDesktop, bool isViewMode, Color? layoutColor) {
return [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor ?? Colors.grey, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor ?? Colors.grey, width: 2),
),
onPressed: () {
context.go('/listPlan');
},
child: Text(
"Cancel",
style: GoogleFonts.poppins(fontSize: 12),
)),
SizedBox(
width: 20,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
context.go('/listPlan');
},
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)),
),
)
),
];
}
}

View File

@ -57,35 +57,38 @@ class _ColorThemePickerWidgetState extends State<ColorThemePickerWidget> {
children: [
// Layout Color Picker
GestureDetector(
onTap: () => _showColorPickerDialog(
title: "Choose Layout Color",
colors: layoutThemeColors,
onColorSelected: (color) {
setState(() {
selectedLayoutColor = color;
});
widget.onLayoutColorSelected(color);
},
),
onTap:
() => _showColorPickerDialog(
title: "Choose Layout Color",
colors: layoutThemeColors,
onColorSelected: (color) {
setState(() {
selectedLayoutColor = color;
});
widget.onLayoutColorSelected(color);
},
),
child: _buildColorBox(
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!);
},
selectedLayoutColor ?? Colors.grey.shade300,
Icons.palette,
),
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,30 +113,32 @@ class _ColorThemePickerWidgetState extends State<ColorThemePickerWidget> {
}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Wrap(
spacing: 10,
runSpacing: 10,
children: colors.map((color) {
return GestureDetector(
onTap: () {
onColorSelected(color);
Navigator.of(context).pop();
},
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.black26),
),
),
);
}).toList(),
),
),
builder:
(context) => AlertDialog(
title: Text(title),
content: Wrap(
spacing: 10,
runSpacing: 10,
children:
colors.map((color) {
return GestureDetector(
onTap: () {
onColorSelected(color);
Navigator.of(context).pop();
},
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.black26),
),
),
);
}).toList(),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -33,25 +33,26 @@ class DynamicItinerary extends StatefulWidget {
final List<dynamic>? apiCountryData;
final String? loginUser;
final Function(String, List<Map<String, dynamic>>)
onItineraryUpdate; // Updated Signature
onItineraryUpdate; // Updated Signature
final Map<String, dynamic> selectedPlanData;
final bool isViewMode;
final GlobalKey<FlightScreenState> flightScreenKey;
final ValueNotifier<String?> tripTypeNotifier;
const DynamicItinerary(
{super.key,
required this.apiData,
required this.onItineraryUpdate,
required this.apiCountryData,
required this.loginUser,
required this.selectedPlanData,
required this.isViewMode,
required this.hasAction,
this.tripType,
this.apiDataForClass,
required this.tripTypeNotifier,
required this.flightScreenKey});
const DynamicItinerary({
super.key,
required this.apiData,
required this.onItineraryUpdate,
required this.apiCountryData,
required this.loginUser,
required this.selectedPlanData,
required this.isViewMode,
required this.hasAction,
this.tripType,
this.apiDataForClass,
required this.tripTypeNotifier,
required this.flightScreenKey,
});
@override
DynamicItineraryState createState() => DynamicItineraryState();
@ -135,9 +136,10 @@ class DynamicItineraryState extends State<DynamicItinerary> {
if (rawServices != null && rawServices is String) {
try {
List<dynamic> decoded = json.decode(rawServices);
List<Map<String, String>> formatted = decoded
.map((e) => {"service_id": e['service_id'].toString()})
.toList();
List<Map<String, String>> formatted =
decoded
.map((e) => {"service_id": e['service_id'].toString()})
.toList();
setState(() {
selectedOrgServiceIds = formatted;
@ -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,23 +244,27 @@ class DynamicItineraryState extends State<DynamicItinerary> {
final selectedIds =
selectedOrgServiceIds.map((e) => e['service_id']).toSet();
final additionalServices = selectedAllServices!.where((service) {
final name = (service['name'] ?? "").toString().toLowerCase();
final id = service['service_id'].toString();
final isNameAllowed =
allowedServiceNames.isEmpty || allowedServiceNames.contains(name);
return filledItineraryKeys.contains(name) &&
!selectedIds.contains(id) &&
isNameAllowed;
}).toList();
final additionalServices =
selectedAllServices!.where((service) {
final name = (service['name'] ?? "").toString().toLowerCase();
final id = service['service_id'].toString();
final isNameAllowed =
allowedServiceNames.isEmpty ||
allowedServiceNames.contains(name);
return filledItineraryKeys.contains(name) &&
!selectedIds.contains(id) &&
isNameAllowed;
}).toList();
final originalFiltered = selectedAllServices!.where((service) {
final name = (service['name'] ?? "").toString().toLowerCase();
final id = service['service_id'].toString();
final isNameAllowed =
allowedServiceNames.isEmpty || allowedServiceNames.contains(name);
return selectedIds.contains(id) && isNameAllowed;
}).toList();
final originalFiltered =
selectedAllServices!.where((service) {
final name = (service['name'] ?? "").toString().toLowerCase();
final id = service['service_id'].toString();
final isNameAllowed =
allowedServiceNames.isEmpty ||
allowedServiceNames.contains(name);
return selectedIds.contains(id) && isNameAllowed;
}).toList();
setState(() {
ServicesChoosed = [...originalFiltered, ...additionalServices]
@ -266,22 +272,26 @@ 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 name = (service['name'] ?? "").toString().toLowerCase();
final isNameAllowed =
allowedServiceNames.isEmpty || allowedServiceNames.contains(name);
return selectedIds.contains(service['service_id'].toString()) &&
isNameAllowed;
}).toList();
final filtered =
selectedAllServices!.where((service) {
final name = (service['name'] ?? "").toString().toLowerCase();
final isNameAllowed =
allowedServiceNames.isEmpty ||
allowedServiceNames.contains(name);
return selectedIds.contains(service['service_id'].toString()) &&
isNameAllowed;
}).toList();
setState(() {
ServicesChoosed = filtered
..sort((a, b) => (a['order'] ?? 0).compareTo(b['order'] ?? 0));
ServicesChoosed =
filtered
..sort((a, b) => (a['order'] ?? 0).compareTo(b['order'] ?? 0));
});
print("Filtered Selected Services Chosen: $ServicesChoosed");
@ -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,50 +616,54 @@ 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":
default:
selectedListWidget = FlightListWidget(
hasAction: widget.hasAction,
tripType: widget.tripType,
flightList: itineraryData["Flight"]!,
onOpen: handleEdit,
onAddNew: handlecreateNewPlan,
isViewMode: widget.isViewMode,
apiData: widget.apiData,
onDeleteFlight: (data) => handleItinerarydelete("Flight", data));
hasAction: widget.hasAction,
tripType: widget.tripType,
flightList: itineraryData["Flight"]!,
onOpen: handleEdit,
onAddNew: handlecreateNewPlan,
isViewMode: widget.isViewMode,
apiData: widget.apiData,
onDeleteFlight: (data) => handleItinerarydelete("Flight", data),
);
break;
}
switch (selectedOption) {
case "Train":
selectedWidget = TrainScreen(
onClose: handleClose,
apiData: widget.apiData,
apiDataForClass: widget.apiDataForClass,
loginUser: widget.loginUser,
onSavetrain: (data) => handleItineraryUpdate("Train", data),
tripType: widget.tripType,
selectedItem: selectedItem);
onClose: handleClose,
apiData: widget.apiData,
apiDataForClass: widget.apiDataForClass,
loginUser: widget.loginUser,
onSavetrain: (data) => handleItineraryUpdate("Train", data),
tripType: widget.tripType,
selectedItem: selectedItem,
);
break;
case "Taxi":
selectedWidget = TaxiScreen(
onClose: handleClose,
apiData: widget.apiData,
loginUser: widget.loginUser,
onSavetaxi: (data) => handleItineraryUpdate("Taxi", data),
selectedItem: selectedItem);
onClose: handleClose,
apiData: widget.apiData,
loginUser: widget.loginUser,
onSavetaxi: (data) => handleItineraryUpdate("Taxi", data),
selectedItem: selectedItem,
);
break;
case "Bus":
selectedWidget = BusScreen(
onClose: handleClose,
apiData: widget.apiData,
loginUser: widget.loginUser,
onSaveBus: (data) => handleItineraryUpdate("Bus", data),
selectedItem: selectedItem);
onClose: handleClose,
apiData: widget.apiData,
loginUser: widget.loginUser,
onSaveBus: (data) => handleItineraryUpdate("Bus", data),
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,85 +802,106 @@ class DynamicItineraryState extends State<DynamicItinerary> {
// );
// });
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
return Stack(
clipBehavior: Clip.none,
children: [
// Second container (yellow box)
Container(
margin: EdgeInsets.only(
top: 40), // Push it down to make room for the tab bar
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white, // Card background
// color: Colors.yellow.shade50, // Card background
// color: Color(0xFFF9F9F9), // Slightly lighter than white
// border: Border.all(color: Color(0xFFE6E7F5), width: 1.3),
border: Border.all(color: Color(0xFFE6E7F5), width: 1.2),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
// color: Color(0x0D000000), // 5% opacity black
color: Colors.black12, // 5% opacity black
blurRadius: 5,
offset: Offset(0, 0.2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 2),
isSelected ? selectedWidget : selectedListWidget,
],
),
),
// First container (tab bar) positioned above
Positioned(
top: 0,
left: MediaQuery.of(context).size.width * 0.05,
right: MediaQuery.of(context).size.width * 0.05,
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 12),
return Stack(
clipBehavior: Clip.none,
children: [
// Second container (yellow box)
Container(
margin: EdgeInsets.only(
top: 40,
), // Push it down to make room for the tab bar
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white, // Card background
borderRadius: BorderRadius.circular(8),
// color: Color(0xFFE6E7F5)
// border: Border.all(color: Colors.black12, width: 1.3),
// color: Colors.yellow.shade50, // Card background
// color: Color(0xFFF9F9F9), // Slightly lighter than white
// border: Border.all(color: Color(0xFFE6E7F5), width: 1.3),
border: Border.all(color: Color(0xFFE6E7F5), width: 1.2),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black12,
// color: Color(0x0D000000), // 5% opacity black
blurRadius: 10,
color: Colors.black12, // 5% opacity black
blurRadius: 5,
offset: Offset(0, 0.2),
),
],
),
child: isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _buildOptions(),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: _buildOptions(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 2),
isSelected ? selectedWidget : selectedListWidget,
],
),
),
),
],
);
});
// First container (tab bar) positioned above
Positioned(
top: 0,
left: MediaQuery.of(context).size.width * 0.05,
right: MediaQuery.of(context).size.width * 0.05,
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 12),
decoration: BoxDecoration(
color: Colors.white, // Card background
borderRadius: BorderRadius.circular(8),
// color: Color(0xFFE6E7F5)
// border: Border.all(color: Colors.black12, width: 1.3),
border: Border.all(color: Color(0xFFE6E7F5), width: 1.2),
boxShadow: [
BoxShadow(
color: Colors.black12,
// color: Color(0x0D000000), // 5% opacity black
blurRadius: 10,
offset: Offset(0, 0.2),
),
],
),
child:
isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: _buildOptions()),
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: _buildOptions(),
),
),
),
],
);
},
);
}
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);
@ -914,26 +971,26 @@ class DynamicItineraryState extends State<DynamicItinerary> {
children: [
iconUrl.isNotEmpty
? Image.network(
iconUrl,
width: 18,
height: 18,
errorBuilder: (context, error, stackTrace) {
return Icon(
fallbackIcon,
size: 25,
color: isOptionSelected
? Color(0xFF114D8B)
: Color(0xFF475569),
);
},
)
iconUrl,
width: 18,
height: 18,
errorBuilder: (context, error, stackTrace) {
return Icon(
fallbackIcon,
size: 25,
color:
isOptionSelected
? Color(0xFF114D8B)
: Color(0xFF475569),
);
},
)
: Icon(
fallbackIcon,
size: 25,
color: isOptionSelected
? Color(0xFF114D8B)
: Color(0xFF475569),
),
fallbackIcon,
size: 25,
color:
isOptionSelected ? Color(0xFF114D8B) : Color(0xFF475569),
),
SizedBox(height: 2),
Row(
children: [
@ -943,12 +1000,12 @@ class DynamicItineraryState extends State<DynamicItinerary> {
// style: GoogleFonts.poppins( fontSize: 12,
// fontWeight: FontWeight.w600,
// color: Color(0xFF575A74))
style: TextStyle(
fontSize: 14,
color: isOptionSelected
? Color(0xFF114D8B)
: Color(0xFF475569),
color:
isOptionSelected
? Color(0xFF114D8B)
: Color(0xFF475569),
fontFamily: "Inter",
fontWeight:
isOptionSelected ? FontWeight.bold : FontWeight.w500,
@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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);
@ -297,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
}
@ -304,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");
});

View 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

View 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(),
],
),
),
),
);
}
}

View 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;
});
},
),
],
),
);
},
),
],
),
),
),
);
}
}

View 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(),
],
),
);
}
}

View File

@ -54,6 +54,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
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,12 +304,11 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
List<dynamic> decodedList = jsonDecode(fixedJson);
selectedServiceIds = decodedList.map<Map<String, dynamic>>((item) {
final map = Map<String, dynamic>.from(item);
return {
"service_id": map['service_id'].toString(),
};
}).toList();
selectedServiceIds =
decodedList.map<Map<String, dynamic>>((item) {
final map = Map<String, dynamic>.from(item);
return {"service_id": map['service_id'].toString()};
}).toList();
} catch (e) {
print("❌ Error decoding fixed agent_supported_service_ids: $e");
selectedServiceIds = [];
@ -379,8 +380,9 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
// userIdsApi = userMap.keys.toList();
// Handle selectedUser as a Map (not a List)
apiselectedUser = extraData['selectedUser']
as Map<String, dynamic>?; // Cast it as a Map
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,13 +482,15 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor = layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor = bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
bodyColor =
bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
});
}
@ -537,6 +541,11 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
}
}
void handleGoBack() async {
print("hello, please Go Back");
printFormData();
}
void handleNext() async {
print("USR Detail Next");
printFormData();
@ -551,35 +560,67 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
print("USERDETAILS : $data");
if (!isValidData(data)) {
print("USERDETAILS : $userDetials");
print("Validation Failed: Required fields are missing.");
setState(() {});
return; // Stop execution if validation fails
final tabs = {
"personal": "Personal Details",
"office": "Office Details",
"travel": "Travel Details",
};
final tabKeys = tabs.keys.toList(); // ["personal", "office", "travel"]
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 {
print("USERDETAILS : $userDetials");
final tabs = {
"personal": "Personal Details",
"office": "Office Details",
"travel": "Travel Details",
};
final tabKeys = tabs.keys.toList(); // ["personal", "office", "travel"]
final currentIndex = tabKeys.indexOf(selectedTab ?? "personal");
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
}
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 {
// Final step submit or show done
print("All tabs completed!");
// 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 {
@ -600,7 +641,7 @@ 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 +663,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
"last_name",
"email",
"mobile_no",
// "employeeCode"
"role_id",
// "employeeCode",
];
if (apiselectedUser == null) {
@ -646,8 +688,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 +698,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 +873,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 +886,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,32 +898,36 @@ 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),
// appBar: isDesktop ? null : const CustomAppBar(title: 'Create User '),
// 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: [
Expanded(child: buildData(isDesktop, context)),
],
return Scaffold(
backgroundColor: Color(0xFFf5f5f5),
// appBar: isDesktop ? null : const CustomAppBar(title: 'Create User '),
// 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: [Expanded(child: buildData(isDesktop, context))],
),
),
),
);
});
);
},
);
}
Widget buildData(bool isDesktop, context) {
@ -874,9 +941,10 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
children: [
Expanded(
child: Container(
height: isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
height:
isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
child: Padding(
padding: EdgeInsets.all(0.0),
child: _buildUserDetails(isDesktop),
@ -886,25 +954,32 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: isDesktop
? Row(
padding: const EdgeInsets.all(8.0),
child:
isDesktop
? Row(
mainAxisAlignment: MainAxisAlignment.end,
children: (selectedTab == "travel" ||
selectedRole == "5" ||
setSelectesUserType == true)
? _buildSubmit(isDesktop, layoutColor!)
: _buildNext(isDesktop, layoutColor!),
children:
(selectedTab == "travel" ||
selectedRole == "5" ||
setSelectesUserType == true)
? _buildSubmit(isDesktop, layoutColor!)
: _buildNext(
isDesktop,
layoutColor!,
), // _buildGoBack(isDesktop, layoutColor!),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: (selectedTab == "travel" ||
selectedRole == "5" ||
setSelectesUserType == true)
? _buildSubmit(isDesktop, layoutColor!)
: _buildNext(isDesktop, layoutColor!),
)),
)
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children:
(selectedTab == "travel" ||
selectedRole == "5" ||
setSelectesUserType == true)
? _buildSubmit(isDesktop, layoutColor!)
: _buildNext(isDesktop, layoutColor!),
),
),
),
],
),
);
@ -943,25 +1018,22 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
// })
],
),
SizedBox(
height: 18,
),
SizedBox(height: 18),
isDesktop
? buildTabsForUser()
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: buildTabsForUser(),
),
scrollDirection: Axis.horizontal,
child: buildTabsForUser(),
),
Container(
// color: Colors.yellow.shade50,
height: MediaQuery.of(context).size.height * 0.64,
child: Row(
children: [
Expanded(child: buildTabContents(isDesktop, isViewMode)),
],
),
)
),
],
),
),
@ -976,6 +1048,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 +1083,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 +1126,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 +1137,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 +1176,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,50 +1186,111 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
return Row(
crossAxisAlignment: CrossAxisAlignment.end, // important
children: tabs.entries.map((entry) {
final isSelected = selectedTab == entry.key;
return GestureDetector(
onTap: () {
setState(() {
selectedTab = entry.key;
});
},
child: Padding(
padding: const EdgeInsets.only(right: 24.0), // space between tabs
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
entry.value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isSelected ? Color(0xFF114D8B) : Color(0xFF475569),
),
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
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
entry.value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color:
isSelected ? Color(0xFF114D8B) : Color(0xFF475569),
),
),
const SizedBox(height: 8),
AnimatedContainer(
duration: Duration(milliseconds: 300),
height: 2,
width: isSelected ? 50 : 0, // small line
color: Color(0xFF114D8B),
),
],
),
const SizedBox(height: 8),
AnimatedContainer(
duration: Duration(milliseconds: 300),
height: 2,
width: isSelected ? 50 : 0, // small line
color: Color(0xFF114D8B),
),
],
),
),
);
}).toList(),
),
);
}).toList(),
);
}
// ---- Submit ---------------------------------------
// ---- 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
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
cursor:
isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
@ -1172,41 +1311,73 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
// isViewMode ? null : handleNext, // Disable when in view mode
child: Text("Next"),
),
)
),
];
}
List<Widget> _buildSubmit(isDesktop, Color layoutColor) {
List<Widget> _buildBack(isDesktop, Color layoutColor) {
return [
ElevatedButton(
MouseRegion(
cursor:
isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: TextButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
backgroundColor:
isViewMode ? layoutColor : layoutColor, // Keep original color
foregroundColor:
isViewMode ? Colors.white : Colors.red, // 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: () {
isEditProfile ? context.go('/listPlan') : context.go('/listUser');
},
child: Text("Cancel")),
SizedBox(
width: 20,
onPressed: handleNext,
// onPressed:
// isViewMode ? null : handleNext, // Disable when in view mode
child: Text("Next"),
),
),
];
}
List<Widget> _buildSubmit(isDesktop, Color layoutColor) {
return [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
isEditProfile ? context.go('/listPlan') : context.go('/listUser');
},
child: Text("Cancel"),
),
SizedBox(width: 20),
if (!isViewMode)
MouseRegion(
cursor: isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
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
foregroundColor:
isViewMode
? Colors.white
: Colors.white, // Keep original color
disabledBackgroundColor:
layoutColor, // Ensure color remains when disabled
disabledForegroundColor: Colors.white,
@ -1220,7 +1391,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

Some files were not shown because too many files have changed in this diff Show More